2017-06-09 18:25:30 +00:00
|
|
|
<?php
|
|
|
|
declare(strict_types = 1);
|
|
|
|
|
|
|
|
namespace LanguageServer;
|
|
|
|
|
|
|
|
use LanguageServer\Protocol\{Diagnostic, DiagnosticSeverity, Range, Position, TextEdit};
|
|
|
|
use LanguageServer\Index\Index;
|
2018-02-24 16:25:48 +00:00
|
|
|
use LanguageServer\Scope\Scope;
|
|
|
|
use LanguageServer\Scope\TreeTraverser;
|
2017-06-09 18:25:30 +00:00
|
|
|
use phpDocumentor\Reflection\DocBlockFactory;
|
|
|
|
use Sabre\Uri;
|
|
|
|
use Microsoft\PhpParser;
|
|
|
|
use Microsoft\PhpParser\Node;
|
2017-10-23 05:54:38 +00:00
|
|
|
use Microsoft\PhpParser\Token;
|
2017-06-09 18:25:30 +00:00
|
|
|
|
|
|
|
class TreeAnalyzer
|
|
|
|
{
|
|
|
|
/** @var PhpParser\Parser */
|
|
|
|
private $parser;
|
|
|
|
|
2017-10-23 05:54:38 +00:00
|
|
|
/** @var DocBlockFactory */
|
|
|
|
private $docBlockFactory;
|
|
|
|
|
|
|
|
/** @var DefinitionResolver */
|
|
|
|
private $definitionResolver;
|
|
|
|
|
2017-06-10 19:36:16 +00:00
|
|
|
/** @var Node\SourceFileNode */
|
|
|
|
private $sourceFileNode;
|
2017-06-09 18:25:30 +00:00
|
|
|
|
|
|
|
/** @var Diagnostic[] */
|
|
|
|
private $diagnostics;
|
|
|
|
|
|
|
|
/** @var string */
|
|
|
|
private $content;
|
|
|
|
|
|
|
|
/** @var Node[] */
|
|
|
|
private $referenceNodes;
|
|
|
|
|
|
|
|
/** @var Definition[] */
|
|
|
|
private $definitions;
|
|
|
|
|
|
|
|
/** @var Node[] */
|
|
|
|
private $definitionNodes;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param PhpParser\Parser $parser
|
|
|
|
* @param string $content
|
|
|
|
* @param DocBlockFactory $docBlockFactory
|
|
|
|
* @param DefinitionResolver $definitionResolver
|
|
|
|
* @param string $uri
|
|
|
|
*/
|
|
|
|
public function __construct(PhpParser\Parser $parser, string $content, DocBlockFactory $docBlockFactory, DefinitionResolver $definitionResolver, string $uri)
|
|
|
|
{
|
|
|
|
$this->parser = $parser;
|
|
|
|
$this->docBlockFactory = $docBlockFactory;
|
|
|
|
$this->definitionResolver = $definitionResolver;
|
2017-06-10 19:36:16 +00:00
|
|
|
$this->sourceFileNode = $this->parser->parseSourceFile($content, $uri);
|
2017-06-09 18:25:30 +00:00
|
|
|
|
|
|
|
// TODO - docblock errors
|
|
|
|
|
2018-02-24 16:25:48 +00:00
|
|
|
$traverser = new TreeTraverser($definitionResolver);
|
|
|
|
$traverser->traverse(
|
|
|
|
$this->sourceFileNode,
|
|
|
|
function ($nodeOrToken, Scope $scope) {
|
|
|
|
$this->collectDiagnostics($nodeOrToken, $scope);
|
|
|
|
if ($nodeOrToken instanceof Node) {
|
|
|
|
$this->collectDefinitionsAndReferences($nodeOrToken, $scope);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
2017-06-09 18:25:30 +00:00
|
|
|
}
|
|
|
|
|
2017-09-28 19:53:12 +00:00
|
|
|
/**
|
|
|
|
* Collects Parser diagnostic messages for the Node/Token
|
|
|
|
* and transforms them into LSP Format
|
|
|
|
*
|
|
|
|
* @param Node|Token $node
|
2017-10-23 05:54:38 +00:00
|
|
|
* @return void
|
2017-09-28 19:53:12 +00:00
|
|
|
*/
|
2018-02-24 16:25:48 +00:00
|
|
|
private function collectDiagnostics($node, Scope $scope)
|
2017-06-09 18:25:30 +00:00
|
|
|
{
|
2017-11-19 01:41:37 +00:00
|
|
|
// Get errors from the parser.
|
2017-09-28 19:53:12 +00:00
|
|
|
if (($error = PhpParser\DiagnosticsProvider::checkDiagnostics($node)) !== null) {
|
|
|
|
$range = PhpParser\PositionUtilities::getRangeFromPosition($error->start, $error->length, $this->sourceFileNode->fileContents);
|
|
|
|
|
|
|
|
switch ($error->kind) {
|
2017-10-30 10:33:19 +00:00
|
|
|
case PhpParser\DiagnosticKind::Error:
|
2017-09-28 19:53:12 +00:00
|
|
|
$severity = DiagnosticSeverity::ERROR;
|
|
|
|
break;
|
2017-10-30 10:33:19 +00:00
|
|
|
case PhpParser\DiagnosticKind::Warning:
|
2017-09-28 19:53:12 +00:00
|
|
|
default:
|
|
|
|
$severity = DiagnosticSeverity::WARNING;
|
|
|
|
break;
|
2017-06-09 18:25:30 +00:00
|
|
|
}
|
|
|
|
|
2017-09-28 19:53:12 +00:00
|
|
|
$this->diagnostics[] = new Diagnostic(
|
|
|
|
$error->message,
|
|
|
|
new Range(
|
|
|
|
new Position($range->start->line, $range->start->character),
|
|
|
|
new Position($range->end->line, $range->start->character)
|
|
|
|
),
|
|
|
|
null,
|
|
|
|
$severity,
|
|
|
|
'php'
|
|
|
|
);
|
|
|
|
}
|
2017-11-19 01:41:37 +00:00
|
|
|
|
|
|
|
// Check for invalid usage of $this.
|
2018-02-24 16:25:48 +00:00
|
|
|
if ($scope->thisVariable === null &&
|
|
|
|
$node instanceof Node\Expression\Variable &&
|
|
|
|
$node->getName() === 'this'
|
|
|
|
) {
|
|
|
|
$this->diagnostics[] = new Diagnostic(
|
|
|
|
"\$this can not be used in static methods.",
|
|
|
|
Range::fromNode($node),
|
|
|
|
null,
|
|
|
|
DiagnosticSeverity::ERROR,
|
|
|
|
'php'
|
|
|
|
);
|
2017-06-09 18:25:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Collect definitions and references for the given node
|
|
|
|
*
|
|
|
|
* @param Node $node
|
|
|
|
*/
|
2018-02-24 16:25:48 +00:00
|
|
|
private function collectDefinitionsAndReferences(Node $node, Scope $scope)
|
2017-06-09 18:25:30 +00:00
|
|
|
{
|
2018-02-24 16:25:48 +00:00
|
|
|
$fqn = $this->definitionResolver->getDefinedFqn($node, $scope);
|
2017-06-09 18:25:30 +00:00
|
|
|
// Only index definitions with an FQN (no variables)
|
|
|
|
if ($fqn !== null) {
|
|
|
|
$this->definitionNodes[$fqn] = $node;
|
2018-02-24 16:25:48 +00:00
|
|
|
$this->definitions[$fqn] = $this->definitionResolver->createDefinitionFromNode($node, $fqn, $scope);
|
2017-06-09 18:25:30 +00:00
|
|
|
} else {
|
2017-11-19 00:59:57 +00:00
|
|
|
|
2017-06-09 18:25:30 +00:00
|
|
|
$parent = $node->parent;
|
2017-11-19 00:59:57 +00:00
|
|
|
if (
|
2017-06-09 18:25:30 +00:00
|
|
|
(
|
|
|
|
// $node->parent instanceof Node\Expression\ScopedPropertyAccessExpression ||
|
|
|
|
($node instanceof Node\Expression\ScopedPropertyAccessExpression ||
|
|
|
|
$node instanceof Node\Expression\MemberAccessExpression)
|
|
|
|
&& !(
|
2018-02-24 16:25:48 +00:00
|
|
|
$parent instanceof Node\Expression\CallExpression ||
|
2017-06-09 18:25:30 +00:00
|
|
|
$node->memberName instanceof PhpParser\Token
|
|
|
|
))
|
2017-11-19 00:59:57 +00:00
|
|
|
|| ($parent instanceof Node\Statement\NamespaceDefinition && $parent->name !== null && $parent->name->getStart() === $node->getStart())
|
2017-06-09 18:25:30 +00:00
|
|
|
) {
|
2017-11-19 00:59:57 +00:00
|
|
|
return;
|
|
|
|
}
|
2017-06-09 18:25:30 +00:00
|
|
|
|
2018-02-24 16:25:48 +00:00
|
|
|
$fqn = $this->definitionResolver->resolveReferenceNodeToFqn($node, $scope);
|
2017-11-19 00:59:57 +00:00
|
|
|
if (!$fqn) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($fqn === 'self' || $fqn === 'static') {
|
|
|
|
// Resolve self and static keywords to the containing class
|
|
|
|
// (This is not 100% correct for static but better than nothing)
|
2018-02-24 16:25:48 +00:00
|
|
|
if (!$scope->currentClassLikeVariable) {
|
2017-11-19 00:59:57 +00:00
|
|
|
return;
|
|
|
|
}
|
2018-02-24 16:25:48 +00:00
|
|
|
$fqn = substr((string)$scope->currentClassLikeVariable->type->getFqsen(), 1);
|
2017-11-19 00:59:57 +00:00
|
|
|
} else if ($fqn === 'parent') {
|
|
|
|
// Resolve parent keyword to the base class FQN
|
2018-02-24 16:25:48 +00:00
|
|
|
if ($scope->currentClassLikeVariable === null) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
$classNode = $scope->currentClassLikeVariable->definitionNode;
|
|
|
|
if (empty($classNode->classBaseClause)
|
|
|
|
|| !$classNode->classBaseClause->baseClass instanceof Node\QualifiedName
|
|
|
|
) {
|
2017-11-19 00:59:57 +00:00
|
|
|
return;
|
|
|
|
}
|
2018-02-24 16:25:48 +00:00
|
|
|
$fqn = $scope->getResolvedName($classNode->classBaseClause->baseClass);
|
2017-11-19 00:59:57 +00:00
|
|
|
if (!$fqn) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->addReference($fqn, $node);
|
|
|
|
|
|
|
|
if (
|
|
|
|
$node instanceof Node\QualifiedName
|
|
|
|
&& ($node->isQualifiedName() || $node->parent instanceof Node\NamespaceUseClause)
|
|
|
|
&& !($parent instanceof Node\Statement\NamespaceDefinition && $parent->name->getStart() === $node->getStart()
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
// Add references for each referenced namespace
|
|
|
|
$ns = $fqn;
|
|
|
|
while (($pos = strrpos($ns, '\\')) !== false) {
|
|
|
|
$ns = substr($ns, 0, $pos);
|
|
|
|
$this->addReference($ns, $node);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Namespaced constant access and function calls also need to register a reference
|
|
|
|
// to the global version because PHP falls back to global at runtime
|
|
|
|
// http://php.net/manual/en/language.namespaces.fallback.php
|
|
|
|
if (ParserHelpers\isConstantFetch($node) ||
|
|
|
|
($parent instanceof Node\Expression\CallExpression
|
|
|
|
&& !(
|
|
|
|
$node instanceof Node\Expression\ScopedPropertyAccessExpression ||
|
|
|
|
$node instanceof Node\Expression\MemberAccessExpression
|
|
|
|
))) {
|
|
|
|
$parts = explode('\\', $fqn);
|
|
|
|
if (count($parts) > 1) {
|
|
|
|
$globalFqn = end($parts);
|
|
|
|
$this->addReference($globalFqn, $node);
|
2017-06-09 18:25:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return Diagnostic[]
|
|
|
|
*/
|
|
|
|
public function getDiagnostics(): array
|
|
|
|
{
|
|
|
|
return $this->diagnostics ?? [];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return void
|
|
|
|
*/
|
|
|
|
private function addReference(string $fqn, Node $node)
|
|
|
|
{
|
|
|
|
if (!isset($this->referenceNodes[$fqn])) {
|
|
|
|
$this->referenceNodes[$fqn] = [];
|
|
|
|
}
|
|
|
|
$this->referenceNodes[$fqn][] = $node;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-10-23 05:54:38 +00:00
|
|
|
* @return Definition[]
|
2017-06-09 18:25:30 +00:00
|
|
|
*/
|
|
|
|
public function getDefinitions()
|
|
|
|
{
|
|
|
|
return $this->definitions ?? [];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return Node[]
|
|
|
|
*/
|
|
|
|
public function getDefinitionNodes()
|
|
|
|
{
|
|
|
|
return $this->definitionNodes ?? [];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return Node[]
|
|
|
|
*/
|
|
|
|
public function getReferenceNodes()
|
|
|
|
{
|
|
|
|
return $this->referenceNodes ?? [];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-06-10 19:36:16 +00:00
|
|
|
* @return Node\SourceFileNode
|
2017-06-09 18:25:30 +00:00
|
|
|
*/
|
2017-06-10 19:36:16 +00:00
|
|
|
public function getSourceFileNode()
|
2017-06-09 18:25:30 +00:00
|
|
|
{
|
2017-06-10 19:36:16 +00:00
|
|
|
return $this->sourceFileNode;
|
2017-06-09 18:25:30 +00:00
|
|
|
}
|
|
|
|
}
|