From e83f95efca92fe6bc6fa1a0011612aa7dec860c1 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Wed, 16 Nov 2016 20:53:39 +0100 Subject: [PATCH 01/13] Add Definition class --- src/Definition.php | 94 +++++++++++++++++++ src/NodeVisitor/DefinitionCollector.php | 17 ++-- src/NodeVisitor/ReferencesCollector.php | 8 +- .../VariableReferencesCollector.php | 4 +- src/PhpDocument.php | 60 ++++++------ src/Project.php | 40 ++++---- src/Server/TextDocument.php | 12 ++- src/Server/Workspace.php | 9 +- tests/NodeVisitor/DefinitionCollectorTest.php | 30 +++--- 9 files changed, 184 insertions(+), 90 deletions(-) create mode 100644 src/Definition.php diff --git a/src/Definition.php b/src/Definition.php new file mode 100644 index 00000000..88aa8b29 --- /dev/null +++ b/src/Definition.php @@ -0,0 +1,94 @@ +symbolInformation = SymbolInformation::fromNode($node, getDefinedFqn($node)); + $def->type = self::getTypeFromNode($node); + return $def; + } + + /** + * Returns the type a reference to this symbol will resolve to. + * For properties and constants, this is the type of the property/constant. + * For functions and methods, this is the return type. + * For classes and interfaces, this is the class type (object). + * Variables are not indexed for performance reasons. + * Can also be a compound type. + * If it is unknown, will be Types\Mixed. + * Returns null if the node does not have a type. + * + * @param Node $node + * @return \phpDocumentor\Type|null + */ + public static function getTypeFromNode(Node $node) + { + if ($node instanceof Node\FunctionLike) { + // Functions/methods + $docBlock = $node->getAttribute('docBlock'); + if ($docBlock !== null && count($returnTags = $docBlock->getTagsByName('return')) > 0) { + // Use @return tag + return $returnTags[0]->getType(); + } + if ($node->returnType !== null) { + // Use PHP7 return type hint + if (is_string($node->returnType)) { + // Resolve a string like "integer" to a type object + return (new TypeResolver)->resolve($node->returnType); + } + return new Types\Object_(new Fqsen('\\' . (string)$node->returnType)); + } + // Unknown return type + return new Types\Mixed; + } + if ($node instanceof Node\Stmt\PropertyProperty || $node instanceof Node\Const_) { + // Property or constant + $docBlock = $node->getAttribute('parentNode')->getAttribute('docBlock'); + if ($docBlock !== null && count($varTags = $docBlock->getTagsByName('var')) > 0) { + // Use @var tag + return $varTags[0]->getType(); + } + // TODO: read @property tags of class + // TODO: Try to infer the type from default value / constant value + // Unknown + return new Types\Mixed; + } + return null; + } +} diff --git a/src/NodeVisitor/DefinitionCollector.php b/src/NodeVisitor/DefinitionCollector.php index a723bdcc..47663380 100644 --- a/src/NodeVisitor/DefinitionCollector.php +++ b/src/NodeVisitor/DefinitionCollector.php @@ -4,7 +4,7 @@ namespace LanguageServer\NodeVisitor; use PhpParser\{NodeVisitorAbstract, Node}; -use LanguageServer\Protocol\SymbolInformation; +use LanguageServer\Definition; use function LanguageServer\Fqn\getDefinedFqn; /** @@ -14,18 +14,18 @@ class DefinitionCollector extends NodeVisitorAbstract { /** - * Map from fully qualified name (FQN) to Node + * Map from fully qualified name (FQN) to Definition * - * @var Node[] + * @var Definition[] */ public $definitions = []; /** - * Map from FQN to SymbolInformation + * Map from fully qualified name (FQN) to Node * - * @var SymbolInformation + * @var Node[] */ - public $symbols = []; + public $nodes = []; public function enterNode(Node $node) { @@ -33,8 +33,7 @@ public function enterNode(Node $node) if ($fqn === null) { return; } - $this->definitions[$fqn] = $node; - $symbol = SymbolInformation::fromNode($node, $fqn); - $this->symbols[$fqn] = $symbol; + $this->nodes[$fqn] = $node; + $this->definitions[$fqn] = Definition::fromNode($node); } } diff --git a/src/NodeVisitor/ReferencesCollector.php b/src/NodeVisitor/ReferencesCollector.php index 08b660df..841fc7e4 100644 --- a/src/NodeVisitor/ReferencesCollector.php +++ b/src/NodeVisitor/ReferencesCollector.php @@ -17,7 +17,7 @@ class ReferencesCollector extends NodeVisitorAbstract * * @var Node[][] */ - public $references = []; + public $nodes = []; public function enterNode(Node $node) { @@ -41,9 +41,9 @@ public function enterNode(Node $node) private function addReference(string $fqn, Node $node) { - if (!isset($this->references[$fqn])) { - $this->references[$fqn] = []; + if (!isset($this->nodes[$fqn])) { + $this->nodes[$fqn] = []; } - $this->references[$fqn][] = $node; + $this->nodes[$fqn][] = $node; } } diff --git a/src/NodeVisitor/VariableReferencesCollector.php b/src/NodeVisitor/VariableReferencesCollector.php index a113a7e4..bb44bdc8 100644 --- a/src/NodeVisitor/VariableReferencesCollector.php +++ b/src/NodeVisitor/VariableReferencesCollector.php @@ -15,7 +15,7 @@ class VariableReferencesCollector extends NodeVisitorAbstract * * @var Node\Expr\Variable[] */ - public $references = []; + public $nodes = []; /** * @var string @@ -33,7 +33,7 @@ public function __construct(string $name) public function enterNode(Node $node) { if ($node instanceof Node\Expr\Variable && $node->name === $this->name) { - $this->references[] = $node; + $this->nodes[] = $node; } else if ($node instanceof Node\FunctionLike) { // If we meet a function node, dont traverse its statements, they are in another scope // except it is a closure that has imported the variable through use diff --git a/src/PhpDocument.php b/src/PhpDocument.php index 214f97b7..b91d3e0f 100644 --- a/src/PhpDocument.php +++ b/src/PhpDocument.php @@ -75,25 +75,25 @@ class PhpDocument private $stmts; /** - * Map from fully qualified name (FQN) to Node + * Map from fully qualified name (FQN) to Definition * - * @var Node[] + * @var Definition[] */ private $definitions; /** - * Map from fully qualified name (FQN) to array of nodes that reference the symbol + * Map from fully qualified name (FQN) to Node * - * @var Node[][] + * @var Node[] */ - private $references; + private $definitionNodes; /** - * Map from fully qualified name (FQN) to SymbolInformation + * Map from fully qualified name (FQN) to array of nodes that reference the symbol * - * @var SymbolInformation[] + * @var Node[][] */ - private $symbols; + private $referenceNodes; /** * @param string $uri The URI of the document @@ -121,7 +121,7 @@ public function __construct(string $uri, string $content, Project $project, Lang */ public function getReferencesByFqn(string $fqn) { - return isset($this->references) && isset($this->references[$fqn]) ? $this->references[$fqn] : null; + return isset($this->referenceNodes) && isset($this->referenceNodes[$fqn]) ? $this->referenceNodes[$fqn] : null; } /** @@ -183,26 +183,26 @@ public function updateContent(string $content) // Unregister old definitions if (isset($this->definitions)) { - foreach ($this->definitions as $fqn => $node) { - $this->project->removeSymbol($fqn); + foreach ($this->definitions as $fqn => $definition) { + $this->project->removeDefinition($fqn); } } // Register this document on the project for all the symbols defined in it $this->definitions = $definitionCollector->definitions; - $this->symbols = $definitionCollector->symbols; - foreach ($definitionCollector->symbols as $fqn => $symbol) { - $this->project->setSymbol($fqn, $symbol); + $this->definitionNodes = $definitionCollector->nodes; + foreach ($definitionCollector->definitions as $fqn => $definition) { + $this->project->setDefinition($fqn, $definition); } // Unregister old references - if (isset($this->references)) { - foreach ($this->references as $fqn => $node) { + if (isset($this->referenceNodes)) { + foreach ($this->referenceNodes as $fqn => $node) { $this->project->removeReferenceUri($fqn, $this->uri); } } // Register this document on the project for references - $this->references = $referencesCollector->references; - foreach ($referencesCollector->references as $fqn => $nodes) { + $this->referenceNodes = $referencesCollector->nodes; + foreach ($referencesCollector->nodes as $fqn => $nodes) { $this->project->addReferenceUri($fqn, $this->uri); } @@ -289,9 +289,9 @@ public function getNodeAtPosition(Position $position) * @param string $fqn * @return Node|null */ - public function getDefinitionByFqn(string $fqn) + public function getDefinitionNodeByFqn(string $fqn) { - return $this->definitions[$fqn] ?? null; + return $this->definitionNodes[$fqn] ?? null; } /** @@ -299,19 +299,19 @@ public function getDefinitionByFqn(string $fqn) * * @return Node[] */ - public function getDefinitions() + public function getDefinitionNodes() { - return $this->definitions; + return $this->definitionNodes; } /** - * Returns a map from fully qualified name (FQN) to SymbolInformation + * Returns a map from fully qualified name (FQN) to Definition defined in this document * - * @return SymbolInformation[] + * @return Definition[] */ - public function getSymbols() + public function getDefinitions() { - return $this->symbols; + return $this->definitions; } /** @@ -332,7 +332,7 @@ public function isDefined(string $fqn): bool * @param Node $node * @return Promise */ - public function getDefinitionByNode(Node $node): Promise + public function getDefinitionNodeByNode(Node $node): Promise { return coroutine(function () use ($node) { // Variables always stay in the boundary of the file and need to be searched inside their function scope @@ -358,7 +358,7 @@ public function getDefinitionByNode(Node $node): Promise if (!isset($document)) { return null; } - return $document->getDefinitionByFqn($fqn); + return $document->getDefinitionNodeByFqn($fqn); }); } @@ -369,7 +369,7 @@ public function getDefinitionByNode(Node $node): Promise * @param Node $node * @return Promise */ - public function getReferencesByNode(Node $node): Promise + public function getReferenceNodesByNode(Node $node): Promise { return coroutine(function () use ($node) { // Variables always stay in the boundary of the file and need to be searched inside their function scope @@ -390,7 +390,7 @@ public function getReferencesByNode(Node $node): Promise $refCollector = new VariableReferencesCollector($node->name); $traverser->addVisitor($refCollector); $traverser->traverse($n->getStmts()); - return $refCollector->references; + return $refCollector->nodes; } // Definition with a global FQN $fqn = getDefinedFqn($node); diff --git a/src/Project.php b/src/Project.php index 49bc6462..2fa74e4f 100644 --- a/src/Project.php +++ b/src/Project.php @@ -19,11 +19,11 @@ class Project private $documents = []; /** - * An associative array that maps fully qualified symbol names to SymbolInformations + * An associative array that maps fully qualified symbol names to Definitions * - * @var SymbolInformation[] + * @var Definition[] */ - private $symbols = []; + private $definitions = []; /** * An associative array that maps fully qualified symbol names to arrays of document URIs that reference the symbol @@ -170,49 +170,49 @@ public function isDocumentOpen(string $uri): bool } /** - * Returns an associative array [string => string] that maps fully qualified symbol names - * to URIs of the document where the symbol is defined + * Returns an associative array [string => Definition] that maps fully qualified symbol names + * to Definitions * - * @return SymbolInformation[] + * @return Definitions[] */ - public function getSymbols() + public function getDefinitions() { - return $this->symbols; + return $this->definitions; } /** - * Adds a SymbolInformation for a specific symbol + * Registers a definition * * @param string $fqn The fully qualified name of the symbol - * @param string $uri The URI + * @param string $definition The Definition object * @return void */ - public function setSymbol(string $fqn, SymbolInformation $symbol) + public function setDefinition(string $fqn, Definition $definition) { - $this->symbols[$fqn] = $symbol; + $this->definitions[$fqn] = $definition; } /** * Sets the SymbolInformation index * - * @param SymbolInformation[] $symbols + * @param Definition[] $symbols * @return void */ - public function setSymbols(array $symbols) + public function setDefinitions(array $definitions) { - $this->symbols = $symbols; + $this->definitions = $definitions; } /** - * Unsets the SymbolInformation for a specific symbol + * Unsets the Definition for a specific symbol * and removes all references pointing to that symbol * * @param string $fqn The fully qualified name of the symbol * @return void */ - public function removeSymbol(string $fqn) + public function removeDefinition(string $fqn) { - unset($this->symbols[$fqn]); + unset($this->definitions[$fqn]); unset($this->references[$fqn]); } @@ -296,10 +296,10 @@ public function setReferenceUris(array $references) */ public function getDefinitionDocument(string $fqn): Promise { - if (!isset($this->symbols[$fqn])) { + if (!isset($this->definitions[$fqn])) { return Promise\resolve(null); } - return $this->getOrLoadDocument($this->symbols[$fqn]->location->uri); + return $this->getOrLoadDocument($this->definitions[$fqn]->symbolInformation->location->uri); } /** diff --git a/src/Server/TextDocument.php b/src/Server/TextDocument.php index 1fc8f279..bb7667e7 100644 --- a/src/Server/TextDocument.php +++ b/src/Server/TextDocument.php @@ -62,7 +62,11 @@ public function __construct(Project $project, LanguageClient $client) public function documentSymbol(TextDocumentIdentifier $textDocument): Promise { return $this->project->getOrLoadDocument($textDocument->uri)->then(function (PhpDocument $document) { - return array_values($document->getSymbols()); + $symbols = []; + foreach ($document->getDefinitions() as $fqn => $definition) { + $symbols[] = $definition->symbolInformation; + } + return $symbols; }); } @@ -136,7 +140,7 @@ public function references( if ($node === null) { return []; } - $refs = yield $document->getReferencesByNode($node); + $refs = yield $document->getReferenceNodesByNode($node); $locations = []; foreach ($refs as $ref) { $locations[] = Location::fromNode($ref); @@ -161,7 +165,7 @@ public function definition(TextDocumentIdentifier $textDocument, Position $posit if ($node === null) { return []; } - $def = yield $document->getDefinitionByNode($node); + $def = yield $document->getDefinitionNodeByNode($node); if ($def === null) { return []; } @@ -187,7 +191,7 @@ public function hover(TextDocumentIdentifier $textDocument, Position $position): } $range = Range::fromNode($node); // Get the definition node for whatever node is under the cursor - $def = yield $document->getDefinitionByNode($node); + $def = yield $document->getDefinitionNodeByNode($node); if ($def === null) { return new Hover([], $range); } diff --git a/src/Server/Workspace.php b/src/Server/Workspace.php index e894c492..26feb72a 100644 --- a/src/Server/Workspace.php +++ b/src/Server/Workspace.php @@ -39,13 +39,10 @@ public function __construct(Project $project, LanguageClient $client) */ public function symbol(string $query): array { - if ($query === '') { - return array_values($this->project->getSymbols()); - } $symbols = []; - foreach ($this->project->getSymbols() as $fqn => $symbol) { - if (stripos($fqn, $query) !== false) { - $symbols[] = $symbol; + foreach ($this->project->getDefinitions() as $fqn => $definition) { + if ($query === '' || stripos($fqn, $query) !== false) { + $symbols[] = $definition->symbolInformation; } } return $symbols; diff --git a/tests/NodeVisitor/DefinitionCollectorTest.php b/tests/NodeVisitor/DefinitionCollectorTest.php index 6df59404..4ed75088 100644 --- a/tests/NodeVisitor/DefinitionCollectorTest.php +++ b/tests/NodeVisitor/DefinitionCollectorTest.php @@ -28,7 +28,7 @@ public function testCollectsSymbols() $traverser->addVisitor($definitionCollector); $stmts = $parser->parse(file_get_contents($uri)); $traverser->traverse($stmts); - $defs = $definitionCollector->definitions; + $defNodes = $definitionCollector->nodes; $this->assertEquals([ 'TestNamespace\\TEST_CONST', 'TestNamespace\\TestClass', @@ -40,17 +40,17 @@ public function testCollectsSymbols() 'TestNamespace\\TestTrait', 'TestNamespace\\TestInterface', 'TestNamespace\\test_function()' - ], array_keys($defs)); - $this->assertInstanceOf(Node\Const_::class, $defs['TestNamespace\\TEST_CONST']); - $this->assertInstanceOf(Node\Stmt\Class_::class, $defs['TestNamespace\\TestClass']); - $this->assertInstanceOf(Node\Const_::class, $defs['TestNamespace\\TestClass::TEST_CLASS_CONST']); - $this->assertInstanceOf(Node\Stmt\PropertyProperty::class, $defs['TestNamespace\\TestClass::staticTestProperty']); - $this->assertInstanceOf(Node\Stmt\PropertyProperty::class, $defs['TestNamespace\\TestClass::testProperty']); - $this->assertInstanceOf(Node\Stmt\ClassMethod::class, $defs['TestNamespace\\TestClass::staticTestMethod()']); - $this->assertInstanceOf(Node\Stmt\ClassMethod::class, $defs['TestNamespace\\TestClass::testMethod()']); - $this->assertInstanceOf(Node\Stmt\Trait_::class, $defs['TestNamespace\\TestTrait']); - $this->assertInstanceOf(Node\Stmt\Interface_::class, $defs['TestNamespace\\TestInterface']); - $this->assertInstanceOf(Node\Stmt\Function_::class, $defs['TestNamespace\\test_function()']); + ], array_keys($defNodes)); + $this->assertInstanceOf(Node\Const_::class, $defNodes['TestNamespace\\TEST_CONST']); + $this->assertInstanceOf(Node\Stmt\Class_::class, $defNodes['TestNamespace\\TestClass']); + $this->assertInstanceOf(Node\Const_::class, $defNodes['TestNamespace\\TestClass::TEST_CLASS_CONST']); + $this->assertInstanceOf(Node\Stmt\PropertyProperty::class, $defNodes['TestNamespace\\TestClass::staticTestProperty']); + $this->assertInstanceOf(Node\Stmt\PropertyProperty::class, $defNodes['TestNamespace\\TestClass::testProperty']); + $this->assertInstanceOf(Node\Stmt\ClassMethod::class, $defNodes['TestNamespace\\TestClass::staticTestMethod()']); + $this->assertInstanceOf(Node\Stmt\ClassMethod::class, $defNodes['TestNamespace\\TestClass::testMethod()']); + $this->assertInstanceOf(Node\Stmt\Trait_::class, $defNodes['TestNamespace\\TestTrait']); + $this->assertInstanceOf(Node\Stmt\Interface_::class, $defNodes['TestNamespace\\TestInterface']); + $this->assertInstanceOf(Node\Stmt\Function_::class, $defNodes['TestNamespace\\test_function()']); } public function testDoesNotCollectReferences() @@ -67,8 +67,8 @@ public function testDoesNotCollectReferences() $traverser->addVisitor($definitionCollector); $stmts = $parser->parse(file_get_contents($uri)); $traverser->traverse($stmts); - $defs = $definitionCollector->definitions; - $this->assertEquals(['TestNamespace\\whatever()'], array_keys($defs)); - $this->assertInstanceOf(Node\Stmt\Function_::class, $defs['TestNamespace\\whatever()']); + $defNodes = $definitionCollector->nodes; + $this->assertEquals(['TestNamespace\\whatever()'], array_keys($defNodes)); + $this->assertInstanceOf(Node\Stmt\Function_::class, $defNodes['TestNamespace\\whatever()']); } } From 83fd96c52a235e409922a0c0a2e78a226154f885 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Thu, 17 Nov 2016 21:25:25 +0100 Subject: [PATCH 02/13] Add recursive DefinitionResolver --- composer.json | 3 +- src/Definition.php | 111 +++++- src/DefinitionResolver.php | 437 ++++++++++++++++++++++++ src/Fqn.php | 248 -------------- src/NodeVisitor/DefinitionCollector.php | 11 +- src/NodeVisitor/ReferencesCollector.php | 12 +- src/PhpDocument.php | 71 ++-- src/Project.php | 42 ++- src/Protocol/SymbolInformation.php | 55 ++- src/Server/TextDocument.php | 51 ++- src/utils.php | 18 + 11 files changed, 704 insertions(+), 355 deletions(-) create mode 100644 src/DefinitionResolver.php delete mode 100644 src/Fqn.php diff --git a/composer.json b/composer.json index 9c78d3e5..df9110aa 100644 --- a/composer.json +++ b/composer.json @@ -41,8 +41,7 @@ "LanguageServer\\": "src/" }, "files" : [ - "src/utils.php", - "src/Fqn.php" + "src/utils.php" ] }, "autoload-dev": { diff --git a/src/Definition.php b/src/Definition.php index 88aa8b29..ce8aca61 100644 --- a/src/Definition.php +++ b/src/Definition.php @@ -1,4 +1,5 @@ symbolInformation = SymbolInformation::fromNode($node, getDefinedFqn($node)); - $def->type = self::getTypeFromNode($node); - return $def; - } - /** * Returns the type a reference to this symbol will resolve to. * For properties and constants, this is the type of the property/constant. @@ -59,6 +61,32 @@ public static function fromNode(Node $node): self */ public static function getTypeFromNode(Node $node) { + if ($node instanceof Node\Param) { + // Parameters + $docBlock = $node->getAttribute('docBlock'); + if ($docBlock !== null && count($paramTags = $docBlock->getTagsByName('param')) > 0) { + // Use @param tag + return $paramTags[0]->getType(); + } + if ($node->type !== null) { + // Use PHP7 return type hint + if (is_string($node->type)) { + // Resolve a string like "bool" to a type object + $type = (new TypeResolver)->resolve($node->type); + } + $type = new Types\Object_(new Fqsen('\\' . (string)$node->type)); + if ($node->default !== null) { + if (is_string($node->default)) { + // Resolve a string like "bool" to a type object + $defaultType = (new TypeResolver)->resolve($node->default); + } + $defaultType = new Types\Object_(new Fqsen('\\' . (string)$node->default)); + $type = new Types\Compound([$type, $defaultType]); + } + } + // Unknown parameter type + return new Types\Mixed; + } if ($node instanceof Node\FunctionLike) { // Functions/methods $docBlock = $node->getAttribute('docBlock'); @@ -69,7 +97,7 @@ public static function getTypeFromNode(Node $node) if ($node->returnType !== null) { // Use PHP7 return type hint if (is_string($node->returnType)) { - // Resolve a string like "integer" to a type object + // Resolve a string like "bool" to a type object return (new TypeResolver)->resolve($node->returnType); } return new Types\Object_(new Fqsen('\\' . (string)$node->returnType)); @@ -91,4 +119,53 @@ public static function getTypeFromNode(Node $node) } return null; } + + /** + * Returns the fully qualified name (FQN) that is defined by a node + * Returns null if the node does not declare any symbol that can be referenced by an FQN + * + * @param Node $node + * @return string|null + */ + public static function getDefinedFqn(Node $node) + { + // Anonymous classes don't count as a definition + if ($node instanceof Node\Stmt\ClassLike && isset($node->name)) { + // Class, interface or trait declaration + return (string)$node->namespacedName; + } else if ($node instanceof Node\Stmt\Function_) { + // Function: use functionName() as the name + return (string)$node->namespacedName . '()'; + } else if ($node instanceof Node\Stmt\ClassMethod) { + // Class method: use ClassName::methodName() as name + $class = $node->getAttribute('parentNode'); + if (!isset($class->name)) { + // Ignore anonymous classes + return null; + } + return (string)$class->namespacedName . '::' . (string)$node->name . '()'; + } else if ($node instanceof Node\Stmt\PropertyProperty) { + // Property: use ClassName::propertyName as name + $class = $node->getAttribute('parentNode')->getAttribute('parentNode'); + if (!isset($class->name)) { + // Ignore anonymous classes + return null; + } + return (string)$class->namespacedName . '::' . (string)$node->name; + } else if ($node instanceof Node\Const_) { + $parent = $node->getAttribute('parentNode'); + if ($parent instanceof Node\Stmt\Const_) { + // Basic constant: use CONSTANT_NAME as name + return (string)$node->namespacedName; + } + if ($parent instanceof Node\Stmt\ClassConst) { + // Class constant: use ClassName::CONSTANT_NAME as name + $class = $parent->getAttribute('parentNode'); + if (!isset($class->name) || $class->name instanceof Node\Expr) { + return null; + } + return (string)$class->namespacedName . '::' . $node->name; + } + } + } } diff --git a/src/DefinitionResolver.php b/src/DefinitionResolver.php new file mode 100644 index 00000000..f1914911 --- /dev/null +++ b/src/DefinitionResolver.php @@ -0,0 +1,437 @@ +project = $project; + } + + /** + * Given any node, returns the Definition object of the symbol that is referenced + * + * @param Node $node Any reference node + * @return Definition|null + */ + public function resolveReferenceNodeToDefinition(Node $node) + { + // Variables are not indexed globally, as they stay in the file scope anyway + if ($node instanceof Node\Expr\Variable) { + // Resolve the variable to a definition node (assignment, param or closure use) + $defNode = self::resolveVariableToNode($node); + $def = new Definition; + // Get symbol information from node (range, symbol kind) + $def->symbolInformation = SymbolInformation::fromNode($defNode); + if ($defNode instanceof Node\Param) { + // Get parameter type + $def->type = Definition::getTypeFromNode($defNode); + } else { + // Resolve the type of the assignment/closure use node + $def->type = $this->resolveExpression($defNode); + } + return $def; + } + // Other references are references to a global symbol that have an FQN + // Find out the FQN + $fqn = $this->resolveReferenceNodeToFqn($node); + if ($fqn === null) { + return null; + } + // Return the Definition object from the project index + $def = $this->project->getDefinition($fqn); + if ($def === null) { + // If the node is a function or constant, it could be namespaced, but PHP falls back to global + // http://php.net/manual/en/language.namespaces.fallback.php + $parent = $node->getAttribute('parentNode'); + if ($parent instanceof Node\Expr\ConstFetch || $parent instanceof Node\Expr\FuncCall) { + $parts = explode('\\', $fqn); + $fqn = end($parts); + $def = $this->project->getDefinition($fqn); + } + } + return $def; + } + + /** + * Given any node, returns the FQN of the symbol that is referenced + * Returns null if the FQN could not be resolved or the reference node references a variable + * + * @param Node $node + * @return string|null + */ + public function resolveReferenceNodeToFqn(Node $node) + { + $parent = $node->getAttribute('parentNode'); + + if ( + $node instanceof Node\Name && ( + $parent instanceof Node\Stmt\ClassLike + || $parent instanceof Node\Param + || $parent instanceof Node\FunctionLike + || $parent instanceof Node\Expr\StaticCall + || $parent instanceof Node\Expr\ClassConstFetch + || $parent instanceof Node\Expr\StaticPropertyFetch + || $parent instanceof Node\Expr\Instanceof_ + ) + ) { + // For extends, implements, type hints and classes of classes of static calls use the name directly + $name = (string)$node; + // Only the name node should be considered a reference, not the UseUse node itself + } else if ($parent instanceof Node\Stmt\UseUse) { + $name = (string)$parent->name; + $grandParent = $parent->getAttribute('parentNode'); + if ($grandParent instanceof Node\Stmt\GroupUse) { + $name = $grandParent->prefix . '\\' . $name; + } else if ($grandParent instanceof Node\Stmt\Use_ && $grandParent->type === Node\Stmt\Use_::TYPE_FUNCTION) { + $name .= '()'; + } + // Only the name node should be considered a reference, not the New_ node itself + } else if ($parent instanceof Node\Expr\New_) { + if (!($parent->class instanceof Node\Name)) { + // Cannot get definition of dynamic calls + return null; + } + $name = (string)$parent->class; + } else if ($node instanceof Node\Expr\MethodCall || $node instanceof Node\Expr\PropertyFetch) { + if ($node->name instanceof Node\Expr) { + // Cannot get definition if right-hand side is expression + return null; + } + // Get the type of the left-hand expression + $varType = $this->resolveExpression($node->var); + if ($varType instanceof Types\This) { + // $this is resolved to the containing class + $classFqn = self::getContainingClassFqn($node); + } else if (!($varType instanceof Types\Object_) || $varType->getFqsen() === null) { + // Left-hand expression could not be resolved to a class + return null; + } else { + $classFqn = substr((string)$varType->getFqsen(), 1); + } + $name = $classFqn . '::' . (string)$node->name; + } else if ($parent instanceof Node\Expr\FuncCall) { + if ($parent->name instanceof Node\Expr) { + return null; + } + $name = (string)($node->getAttribute('namespacedName') ?? $parent->name); + } else if ($parent instanceof Node\Expr\ConstFetch) { + $name = (string)($node->getAttribute('namespacedName') ?? $parent->name); + } else if ( + $node instanceof Node\Expr\ClassConstFetch + || $node instanceof Node\Expr\StaticPropertyFetch + || $node instanceof Node\Expr\StaticCall + ) { + if ($node->class instanceof Node\Expr || $node->name instanceof Node\Expr) { + // Cannot get definition of dynamic names + return null; + } + $className = (string)$node->class; + if ($className === 'self' || $className === 'static' || $className === 'parent') { + // self and static are resolved to the containing class + $classNode = getClosestNode($node, Node\Stmt\Class_::class); + if ($className === 'parent') { + // parent is resolved to the parent class + if (!isset($n->extends)) { + return null; + } + $className = (string)$classNode->extends; + } else { + $className = (string)$classNode->namespacedName; + } + } + $name = (string)$className . '::' . $node->name; + } else { + return null; + } + if ( + $node instanceof Node\Expr\MethodCall + || $node instanceof Node\Expr\StaticCall + || $parent instanceof Node\Expr\FuncCall + ) { + $name .= '()'; + } + if (!isset($name)) { + return null; + } + return $name; + } + + /** + * Returns FQN of the class a node is contained in + * Returns null if the class is anonymous or the node is not contained in a class + * + * @param Node $node + * @return string|null + */ + private static function getContainingClassFqn(Node $node) + { + $classNode = getClosestNode($node, Node\Stmt\Class_::class); + if ($classNode->isAnonymous()) { + return null; + } + return (string)$classNode->namespacedName; + } + + /** + * Returns the assignment or parameter node where a variable was defined + * + * @param Node\Expr\Variable $n The variable access + * @return Node\Expr\Assign|Node\Param|Node\Expr\ClosureUse|null + */ + public static function resolveVariableToNode(Node\Expr\Variable $var) + { + $n = $var; + // Traverse the AST up + do { + // If a function is met, check the parameters and use statements + if ($n instanceof Node\FunctionLike) { + foreach ($n->getParams() as $param) { + if ($param->name === $var->name) { + return $param; + } + } + // If it is a closure, also check use statements + if ($n instanceof Node\Expr\Closure) { + foreach ($n->uses as $use) { + if ($use->var === $var->name) { + return $use; + } + } + } + break; + } + // Check each previous sibling node for a variable assignment to that variable + while ($n->getAttribute('previousSibling') && $n = $n->getAttribute('previousSibling')) { + if ( + ($n instanceof Node\Expr\Assign || $n instanceof Node\Expr\AssignOp) + && $n->var instanceof Node\Expr\Variable && $n->var->name === $var->name + ) { + return $n; + } + } + } while (isset($n) && $n = $n->getAttribute('parentNode')); + // Return null if nothing was found + return null; + } + + /** + * Given an expression node, resolves that expression recursively to a type. + * If the type could not be resolved, returns Types\Mixed. + * + * @param Node\Expr $expr + * @return Type + */ + private function resolveExpression(Node\Expr $expr): Type + { + if ($expr instanceof Node\Expr\Variable) { + if ($expr->name === 'this') { + return new Types\This; + } + // Find variable definition + $defNode = $this->resolveVariableToNode($expr); + if ($defNode instanceof Node\Expr) { + return $this->resolveExpression($defNode); + } + if ($defNode instanceof Node\Param) { + return Definition::getTypeFromNode($defNode); + } + } + if ($expr instanceof Node\Expr\FuncCall) { + // Find the function definition + if ($expr->name instanceof Node\Expr) { + // Cannot get type for dynamic function call + return new Types\Mixed; + } + $fqn = (string)($expr->getAttribute('namespacedName') ?? $expr->name); + return $this->project->getDefinition($fqn)->type; + } + if ($expr instanceof Node\Expr\ConstFetch) { + if (strtolower((string)$expr->name) === 'true' || strtolower((string)$expr->name) === 'false') { + return new Types\Boolean; + } + // Resolve constant + $fqn = (string)($expr->getAttribute('namespacedName') ?? $expr->name); + return $this->project->getDefinition($fqn)->type; + } + if ($expr instanceof Node\Expr\MethodCall) { + // Resolve object + $objType = $this->resolveExpression($expr->var); + if (!($objType instanceof Types\Object_) || $objType->getFqsen() === null || $expr->name instanceof Node\Expr) { + // Need the class FQN of the object + return new Types\Mixed; + } + $fqn = (string)$objType->getFqsen() . '::' . $expr->name . '()'; + return $this->project->getDefinition($fqn)->type; + } + if ($expr instanceof Node\Expr\PropertyFetch) { + // Resolve object + $objType = $this->resolveExpression($expr->var); + if (!($objType instanceof Types\Object_) || $objType->getFqsen() === null || $expr->name instanceof Node\Expr) { + // Need the class FQN of the object + return new Types\Mixed; + } + $fqn = (string)$objType->getFqsen() . '::' . $expr->name; + return $this->project->getDefinition($fqn)->type; + } + if ($expr instanceof Node\Expr\StaticCall) { + if ($expr->class instanceof Node\Expr || $expr->name instanceof Node\Expr) { + // Need the FQN + return new Types\Mixed; + } + $fqn = (string)$expr->class . '::' . $expr->name . '()'; + } + if ($expr instanceof Node\Expr\StaticPropertyFetch || $expr instanceof Node\Expr\ClassConstFetch) { + if ($expr->class instanceof Node\Expr || $expr->name instanceof Node\Expr) { + // Need the FQN + return new Types\Mixed; + } + $fqn = (string)$expr->class . '::' . $expr->name; + } + if ($expr instanceof Node\Expr\New_) { + if ($expr->class instanceof Node\Expr) { + return new Types\Mixed; + } + if ($expr->class instanceof Node\Stmt\Class_) { + // Anonymous class + return new Types\Object; + } + if ((string)$expr->class === 'self') { + return new Types\Object_; + } + return new Types\Object_(new Fqsen('\\' . (string)$expr->class)); + } + if ($expr instanceof Node\Expr\Clone_ || $expr instanceof Node\Expr\Assign) { + return $this->resolveExpression($expr->expr); + } + if ($expr instanceof Node\Expr\Ternary) { + // ?: + if ($expr->if === null) { + return new Types\Compound([ + $this->resolveExpression($expr->cond), + $this->resolveExpression($expr->else) + ]); + } + // Ternary is a compound of the two possible values + return new Types\Compound([ + $this->resolveExpression($expr->if), + $this->resolveExpression($expr->else) + ]); + } + if ($expr instanceof Node\Expr\BinaryOp\Coalesce) { + // ?? operator + return new Types\Compound([ + $this->resolveExpression($expr->left), + $this->resolveExpression($expr->right) + ]); + } + if ( + $expr instanceof Node\Expr\InstanceOf_ + || $expr instanceof Node\Expr\Cast\Bool_ + || $expr instanceof Node\Expr\BooleanNot + || $expr instanceof Node\Expr\Empty_ + || $expr instanceof Node\Expr\Isset_ + || $expr instanceof Node\Expr\BinaryOp\Greater + || $expr instanceof Node\Expr\BinaryOp\GreaterOrEqual + || $expr instanceof Node\Expr\BinaryOp\Smaller + || $expr instanceof Node\Expr\BinaryOp\SmallerOrEqual + || $expr instanceof Node\Expr\BinaryOp\BooleanAnd + || $expr instanceof Node\Expr\BinaryOp\BooleanOr + || $expr instanceof Node\Expr\BinaryOp\LogicalAnd + || $expr instanceof Node\Expr\BinaryOp\LogicalOr + || $expr instanceof Node\Expr\BinaryOp\LogicalXor + || $expr instanceof Node\Expr\BinaryOp\NotEqual + || $expr instanceof Node\Expr\BinaryOp\NotIdentical + ) { + return new Types\Boolean_; + } + if ( + $expr instanceof Node\Expr\Concat + || $expr instanceof Node\Expr\Cast\String_ + || $expr instanceof Node\Expr\BinaryOp\Concat + || $expr instanceof Node\Expr\AssignOp\Concat + || $expr instanceof Node\Expr\Scalar\String_ + || $expr instanceof Node\Expr\Scalar\Encapsed + || $expr instanceof Node\Expr\Scalar\EncapsedStringPart + || $expr instanceof Node\Expr\Scalar\MagicConst\Class_ + || $expr instanceof Node\Expr\Scalar\MagicConst\Dir + || $expr instanceof Node\Expr\Scalar\MagicConst\Function_ + || $expr instanceof Node\Expr\Scalar\MagicConst\Method + || $expr instanceof Node\Expr\Scalar\MagicConst\Namespace_ + || $expr instanceof Node\Expr\Scalar\MagicConst\Trait_ + ) { + return new Types\String_; + } + if ( + $expr instanceof Node\Expr\BinaryOp\Minus + || $expr instanceof Node\Expr\BinaryOp\Plus + || $expr instanceof Node\Expr\BinaryOp\Pow + || $expr instanceof Node\Expr\BinaryOp\Mul + || $expr instanceof Node\Expr\AssignOp\Minus + || $expr instanceof Node\Expr\AssignOp\Plus + || $expr instanceof Node\Expr\AssignOp\Pow + || $expr instanceof Node\Expr\AssignOp\Mul + ) { + if ( + resolveType($expr->left) instanceof Types\Integer_ + && resolveType($expr->right) instanceof Types\Integer_ + ) { + return new Types\Integer; + } + return new Types\Float_; + } + if ( + $expr instanceof Node\Scalar\LNumber + || $expr instanceof Node\Expr\Cast\Int_ + || $expr instanceof Node\Expr\Scalar\MagicConst\Line + || $expr instanceof Node\Expr\BinaryOp\Spaceship + || $expr instanceof Node\Expr\BinaryOp\BitwiseAnd + || $expr instanceof Node\Expr\BinaryOp\BitwiseOr + || $expr instanceof Node\Expr\BinaryOp\BitwiseXor + ) { + return new Types\Integer; + } + if ( + $expr instanceof Node\Expr\BinaryOp\Div + || $expr instanceof Node\Expr\DNumber + || $expr instanceof Node\Expr\Cast\Double + ) { + return new Types\Float_; + } + if ($expr instanceof Node\Expr\Array_) { + $valueTypes = []; + $keyTypes = []; + foreach ($expr->items as $item) { + $valueTypes[] = $this->resolveExpression($item->value); + $keyTypes[] = $item->key ? $this->resolveExpression($item->key) : new Types\Integer; + } + $valueTypes = array_unique($keyTypes); + $keyTypes = array_unique($keyTypes); + $valueType = count($valueTypes) > 1 ? new Types\Compound($valueTypes) : $valueTypes[0]; + $keyType = count($keyTypes) > 1 ? new Types\Compound($keyTypes) : $keyTypes[0]; + return new Types\Array_($valueTypes, $keyTypes); + } + if ($expr instanceof Node\Expr\ArrayDimFetch) { + $varType = $this->resolveExpression($expr->var); + if (!($varType instanceof Types\Array_)) { + return new Types\Mixed; + } + return $varType->getValueType(); + } + if ($expr instanceof Node\Expr\Include_) { + // TODO: resolve path to PhpDocument and find return statement + return new Types\Mixed; + } + return new Types\Mixed; + } +} diff --git a/src/Fqn.php b/src/Fqn.php deleted file mode 100644 index f5ef00db..00000000 --- a/src/Fqn.php +++ /dev/null @@ -1,248 +0,0 @@ -getAttribute('parentNode'); - - if ( - $node instanceof Node\Name && ( - $parent instanceof Node\Stmt\ClassLike - || $parent instanceof Node\Param - || $parent instanceof Node\FunctionLike - || $parent instanceof Node\Expr\StaticCall - || $parent instanceof Node\Expr\ClassConstFetch - || $parent instanceof Node\Expr\StaticPropertyFetch - || $parent instanceof Node\Expr\Instanceof_ - ) - ) { - // For extends, implements, type hints and classes of classes of static calls use the name directly - $name = (string)$node; - // Only the name node should be considered a reference, not the UseUse node itself - } else if ($parent instanceof Node\Stmt\UseUse) { - $name = (string)$parent->name; - $grandParent = $parent->getAttribute('parentNode'); - if ($grandParent instanceof Node\Stmt\GroupUse) { - $name = $grandParent->prefix . '\\' . $name; - } else if ($grandParent instanceof Node\Stmt\Use_ && $grandParent->type === Node\Stmt\Use_::TYPE_FUNCTION) { - $name .= '()'; - } - // Only the name node should be considered a reference, not the New_ node itself - } else if ($parent instanceof Node\Expr\New_) { - if (!($parent->class instanceof Node\Name)) { - // Cannot get definition of dynamic calls - return null; - } - $name = (string)$parent->class; - } else if ($node instanceof Node\Expr\MethodCall || $node instanceof Node\Expr\PropertyFetch) { - if ($node->name instanceof Node\Expr || !($node->var instanceof Node\Expr\Variable)) { - // Cannot get definition of dynamic calls - return null; - } - // Need to resolve variable to a class - if ($node->var->name === 'this') { - // $this resolved to the class it is contained in - $n = $node; - while ($n = $n->getAttribute('parentNode')) { - if ($n instanceof Node\Stmt\Class_) { - if ($n->isAnonymous()) { - return null; - } - $name = (string)$n->namespacedName; - break; - } - } - if (!isset($name)) { - return null; - } - } else { - // Other variables resolve to their definition - $varDef = getVariableDefinition($node->var); - if (!isset($varDef)) { - return null; - } - if ($varDef instanceof Node\Param) { - if (!isset($varDef->type)) { - // Cannot resolve to class without a type hint - // TODO: parse docblock - return null; - } - $name = (string)$varDef->type; - } else if ($varDef instanceof Node\Expr\Assign) { - if ($varDef->expr instanceof Node\Expr\New_) { - if (!($varDef->expr->class instanceof Node\Name)) { - // Cannot get definition of dynamic calls - return null; - } - $name = (string)$varDef->expr->class; - } else { - return null; - } - } else { - return null; - } - } - $name .= '::' . (string)$node->name; - } else if ($parent instanceof Node\Expr\FuncCall) { - if ($parent->name instanceof Node\Expr) { - return null; - } - $name = (string)($node->getAttribute('namespacedName') ?? $parent->name); - } else if ($parent instanceof Node\Expr\ConstFetch) { - $name = (string)($node->getAttribute('namespacedName') ?? $parent->name); - } else if ( - $node instanceof Node\Expr\ClassConstFetch - || $node instanceof Node\Expr\StaticPropertyFetch - || $node instanceof Node\Expr\StaticCall - ) { - if ($node->class instanceof Node\Expr || $node->name instanceof Node\Expr) { - // Cannot get definition of dynamic names - return null; - } - $className = (string)$node->class; - if ($className === 'self' || $className === 'static' || $className === 'parent') { - // self and static are resolved to the containing class - $n = $node; - while ($n = $n->getAttribute('parentNode')) { - if ($n instanceof Node\Stmt\Class_) { - if ($n->isAnonymous()) { - return null; - } - if ($className === 'parent') { - // parent is resolved to the parent class - if (!isset($n->extends)) { - return null; - } - $className = (string)$n->extends; - } else { - $className = (string)$n->namespacedName; - } - break; - } - } - } - $name = (string)$className . '::' . $node->name; - } else { - return null; - } - if ( - $node instanceof Node\Expr\MethodCall - || $node instanceof Node\Expr\StaticCall - || $parent instanceof Node\Expr\FuncCall - ) { - $name .= '()'; - } - if (!isset($name)) { - return null; - } - return $name; -} - -/** - * Returns the assignment or parameter node where a variable was defined - * - * @param Node\Expr\Variable $n The variable access - * @return Node\Expr\Assign|Node\Param|Node\Expr\ClosureUse|null - */ -function getVariableDefinition(Node\Expr\Variable $var) -{ - $n = $var; - // Traverse the AST up - do { - // If a function is met, check the parameters and use statements - if ($n instanceof Node\FunctionLike) { - foreach ($n->getParams() as $param) { - if ($param->name === $var->name) { - return $param; - } - } - // If it is a closure, also check use statements - if ($n instanceof Node\Expr\Closure) { - foreach ($n->uses as $use) { - if ($use->var === $var->name) { - return $use; - } - } - } - break; - } - // Check each previous sibling node for a variable assignment to that variable - while ($n->getAttribute('previousSibling') && $n = $n->getAttribute('previousSibling')) { - if ($n instanceof Node\Expr\Assign && $n->var instanceof Node\Expr\Variable && $n->var->name === $var->name) { - return $n; - } - } - } while (isset($n) && $n = $n->getAttribute('parentNode')); - // Return null if nothing was found - return null; -} - -/** - * Returns the fully qualified name (FQN) that is defined by a node - * - * @param Node $node - * @return string|null - */ -function getDefinedFqn(Node $node) -{ - // Anonymous classes don't count as a definition - if ($node instanceof Node\Stmt\ClassLike && isset($node->name)) { - // Class, interface or trait declaration - return (string)$node->namespacedName; - } else if ($node instanceof Node\Stmt\Function_) { - // Function: use functionName() as the name - return (string)$node->namespacedName . '()'; - } else if ($node instanceof Node\Stmt\ClassMethod) { - // Class method: use ClassName::methodName() as name - $class = $node->getAttribute('parentNode'); - if (!isset($class->name)) { - // Ignore anonymous classes - return null; - } - return (string)$class->namespacedName . '::' . (string)$node->name . '()'; - } else if ($node instanceof Node\Stmt\PropertyProperty) { - // Property: use ClassName::propertyName as name - $class = $node->getAttribute('parentNode')->getAttribute('parentNode'); - if (!isset($class->name)) { - // Ignore anonymous classes - return null; - } - return (string)$class->namespacedName . '::' . (string)$node->name; - } else if ($node instanceof Node\Const_) { - $parent = $node->getAttribute('parentNode'); - if ($parent instanceof Node\Stmt\Const_) { - // Basic constant: use CONSTANT_NAME as name - return (string)$node->namespacedName; - } - if ($parent instanceof Node\Stmt\ClassConst) { - // Class constant: use ClassName::CONSTANT_NAME as name - $class = $parent->getAttribute('parentNode'); - if (!isset($class->name) || $class->name instanceof Node\Expr) { - return null; - } - return (string)$class->namespacedName . '::' . $node->name; - } - } -} diff --git a/src/NodeVisitor/DefinitionCollector.php b/src/NodeVisitor/DefinitionCollector.php index 47663380..e0c35210 100644 --- a/src/NodeVisitor/DefinitionCollector.php +++ b/src/NodeVisitor/DefinitionCollector.php @@ -5,7 +5,7 @@ use PhpParser\{NodeVisitorAbstract, Node}; use LanguageServer\Definition; -use function LanguageServer\Fqn\getDefinedFqn; +use LanguageServer\Protocol\SymbolInformation; /** * Collects definitions of classes, interfaces, traits, methods, properties and constants @@ -29,11 +29,16 @@ class DefinitionCollector extends NodeVisitorAbstract public function enterNode(Node $node) { - $fqn = getDefinedFqn($node); + $fqn = Definition::getDefinedFqn($node); + // Only index definitions with an FQN (no variables) if ($fqn === null) { return; } $this->nodes[$fqn] = $node; - $this->definitions[$fqn] = Definition::fromNode($node); + $def = new Definition; + $def->fqn = $fqn; + $def->symbolInformation = SymbolInformation::fromNode($node, $fqn); + $def->type = Definition::getTypeFromNode($node); + $this->definitions[$fqn] = $def; } } diff --git a/src/NodeVisitor/ReferencesCollector.php b/src/NodeVisitor/ReferencesCollector.php index 841fc7e4..7e35beb9 100644 --- a/src/NodeVisitor/ReferencesCollector.php +++ b/src/NodeVisitor/ReferencesCollector.php @@ -3,8 +3,8 @@ namespace LanguageServer\NodeVisitor; -use function LanguageServer\Fqn\getReferencedFqn; use PhpParser\{NodeVisitorAbstract, Node}; +use LanguageServer\DefinitionResolver; /** * Collects references to classes, interfaces, traits, methods, properties and constants @@ -19,10 +19,18 @@ class ReferencesCollector extends NodeVisitorAbstract */ public $nodes = []; + /** + * @param DefinitionResolver $definitionResolver The DefinitionResolver to resolve reference nodes to definitions + */ + public function __construct(DefinitionResolver $definitionResolver) + { + $this->definitionResolver = $definitionResolver; + } + public function enterNode(Node $node) { // Check if the node references any global symbol - $fqn = getReferencedFqn($node); + $fqn = $this->definitionResolver->resolveReferenceNodeToFqn($node); if ($fqn) { $this->addReference($fqn, $node); // Namespaced constant access and function calls also need to register a reference diff --git a/src/PhpDocument.php b/src/PhpDocument.php index b91d3e0f..8a3fec01 100644 --- a/src/PhpDocument.php +++ b/src/PhpDocument.php @@ -16,7 +16,6 @@ use PhpParser\{Error, ErrorHandler, Node, NodeTraverser}; use PhpParser\NodeVisitor\NameResolver; use phpDocumentor\Reflection\DocBlockFactory; -use function LanguageServer\Fqn\{getDefinedFqn, getVariableDefinition, getReferencedFqn}; use Sabre\Event\Promise; use function Sabre\Event\coroutine; use Sabre\Uri; @@ -53,6 +52,13 @@ class PhpDocument */ private $docBlockFactory; + /** + * The DefinitionResolver instance to resolve reference nodes to definitions + * + * @var DefinitionResolver + */ + private $definitionResolver; + /** * The URI of the document * @@ -103,13 +109,21 @@ class PhpDocument * @param Parser $parser The PHPParser instance * @param DocBlockFactory $docBlockFactory The DocBlockFactory instance to parse docblocks */ - public function __construct(string $uri, string $content, Project $project, LanguageClient $client, Parser $parser, DocBlockFactory $docBlockFactory) - { + public function __construct( + string $uri, + string $content, + Project $project, + LanguageClient $client, + Parser $parser, + DocBlockFactory $docBlockFactory, + DefinitionResolver $definitionResolver + ) { $this->uri = $uri; $this->project = $project; $this->client = $client; $this->parser = $parser; $this->docBlockFactory = $docBlockFactory; + $this->definitionResolver = $definitionResolver; $this->updateContent($content); } @@ -119,7 +133,7 @@ public function __construct(string $uri, string $content, Project $project, Lang * @param string $fqn The fully qualified name of the symbol * @return Node[] */ - public function getReferencesByFqn(string $fqn) + public function getReferenceNodesByFqn(string $fqn) { return isset($this->referenceNodes) && isset($this->referenceNodes[$fqn]) ? $this->referenceNodes[$fqn] : null; } @@ -176,7 +190,7 @@ public function updateContent(string $content) $traverser->addVisitor($definitionCollector); // Collect all references - $referencesCollector = new ReferencesCollector; + $referencesCollector = new ReferencesCollector($this->definitionResolver); $traverser->addVisitor($referencesCollector); $traverser->traverse($stmts); @@ -325,43 +339,6 @@ public function isDefined(string $fqn): bool return isset($this->definitions[$fqn]); } - /** - * Returns the definition node for any node - * The definition node MAY be in another document, check the ownerDocument attribute - * - * @param Node $node - * @return Promise - */ - public function getDefinitionNodeByNode(Node $node): Promise - { - return coroutine(function () use ($node) { - // Variables always stay in the boundary of the file and need to be searched inside their function scope - // by traversing the AST - if ($node instanceof Node\Expr\Variable) { - return getVariableDefinition($node); - } - $fqn = getReferencedFqn($node); - if (!isset($fqn)) { - return null; - } - $document = yield $this->project->getDefinitionDocument($fqn); - if (!isset($document)) { - // If the node is a function or constant, it could be namespaced, but PHP falls back to global - // http://php.net/manual/en/language.namespaces.fallback.php - $parent = $node->getAttribute('parentNode'); - if ($parent instanceof Node\Expr\ConstFetch || $parent instanceof Node\Expr\FuncCall) { - $parts = explode('\\', $fqn); - $fqn = end($parts); - $document = yield $this->project->getDefinitionDocument($fqn); - } - } - if (!isset($document)) { - return null; - } - return $document->getDefinitionNodeByFqn($fqn); - }); - } - /** * Returns the reference nodes for any node * The references node MAY be in other documents, check the ownerDocument attribute @@ -374,7 +351,11 @@ public function getReferenceNodesByNode(Node $node): Promise return coroutine(function () use ($node) { // Variables always stay in the boundary of the file and need to be searched inside their function scope // by traversing the AST - if ($node instanceof Node\Expr\Variable || $node instanceof Node\Param) { + if ( + $node instanceof Node\Expr\Variable + || $node instanceof Node\Param + || $node instanceof Node\Expr\ClosureUse + ) { if ($node->name instanceof Node\Expr) { return null; } @@ -393,14 +374,14 @@ public function getReferenceNodesByNode(Node $node): Promise return $refCollector->nodes; } // Definition with a global FQN - $fqn = getDefinedFqn($node); + $fqn = Definition::getDefinedFqn($node); if ($fqn === null) { return []; } $refDocuments = yield $this->project->getReferenceDocuments($fqn); $nodes = []; foreach ($refDocuments as $document) { - $refs = $document->getReferencesByFqn($fqn); + $refs = $document->getReferenceNodesByFqn($fqn); if ($refs !== null) { foreach ($refs as $ref) { $nodes[] = $ref; diff --git a/src/Project.php b/src/Project.php index 2fa74e4f..e037266d 100644 --- a/src/Project.php +++ b/src/Project.php @@ -46,6 +46,13 @@ class Project */ private $docBlockFactory; + /** + * The DefinitionResolver instance to resolve reference nodes to Definitions + * + * @var DefinitionResolver + */ + private $definitionResolver; + /** * Reference to the language server client interface * @@ -66,6 +73,7 @@ public function __construct(LanguageClient $client, ClientCapabilities $clientCa $this->clientCapabilities = $clientCapabilities; $this->parser = new Parser; $this->docBlockFactory = DocBlockFactory::createInstance(); + $this->definitionResolver = new DefinitionResolver($this); } /** @@ -122,7 +130,15 @@ public function loadDocument(string $uri): Promise $document = $this->documents[$uri]; $document->updateContent($content); } else { - $document = new PhpDocument($uri, $content, $this, $this->client, $this->parser, $this->docBlockFactory); + $document = new PhpDocument( + $uri, + $content, + $this, + $this->client, + $this->parser, + $this->docBlockFactory, + $this->definitionResolver + ); } return $document; }); @@ -141,7 +157,15 @@ public function openDocument(string $uri, string $content) $document = $this->documents[$uri]; $document->updateContent($content); } else { - $document = new PhpDocument($uri, $content, $this, $this->client, $this->parser, $this->docBlockFactory); + $document = new PhpDocument( + $uri, + $content, + $this, + $this->client, + $this->parser, + $this->docBlockFactory, + $this->definitionResolver + ); $this->documents[$uri] = $document; } return $document; @@ -180,6 +204,16 @@ public function getDefinitions() return $this->definitions; } + /** + * Returns the Definition object by a specific FQN + * + * @return Definition|null + */ + public function getDefinition(string $fqn) + { + return $this->definitions[$fqn] ?? null; + } + /** * Registers a definition * @@ -193,9 +227,9 @@ public function setDefinition(string $fqn, Definition $definition) } /** - * Sets the SymbolInformation index + * Sets the Definition index * - * @param Definition[] $symbols + * @param Definition[] $definitions Map from FQN to Definition * @return void */ public function setDefinitions(array $definitions) diff --git a/src/Protocol/SymbolInformation.php b/src/Protocol/SymbolInformation.php index 19ca6a68..1111dc08 100644 --- a/src/Protocol/SymbolInformation.php +++ b/src/Protocol/SymbolInformation.php @@ -44,27 +44,48 @@ class SymbolInformation * * @param Node $node * @param string $fqn If given, $containerName will be extracted from it - * @return self + * @return self|null */ public static function fromNode(Node $node, string $fqn = null) { - $nodeSymbolKindMap = [ - Node\Stmt\Class_::class => SymbolKind::CLASS_, - Node\Stmt\Trait_::class => SymbolKind::CLASS_, - Node\Stmt\Interface_::class => SymbolKind::INTERFACE, - Node\Stmt\Namespace_::class => SymbolKind::NAMESPACE, - Node\Stmt\Function_::class => SymbolKind::FUNCTION, - Node\Stmt\ClassMethod::class => SymbolKind::METHOD, - Node\Stmt\PropertyProperty::class => SymbolKind::PROPERTY, - Node\Const_::class => SymbolKind::CONSTANT - ]; - $class = get_class($node); - if (!isset($nodeSymbolKindMap[$class])) { - throw new Exception("Not a declaration node: $class"); - } $symbol = new self; - $symbol->kind = $nodeSymbolKindMap[$class]; - $symbol->name = (string)$node->name; + if ($node instanceof Node\Stmt\Class_) { + $symbol->kind = SymbolKind::CLASS_; + } else if ($node instanceof Node\Stmt\Trait_) { + $symbol->kind = SymbolKind::CLASS_; + } else if ($node instanceof Node\Stmt\Interface_) { + $symbol->kind = SymbolKind::INTERFACE; + } else if ($node instanceof Node\Stmt\Namespace_) { + $symbol->kind = SymbolKind::NAMESPACE; + } else if ($node instanceof Node\Stmt\Function_) { + $symbol->kind = SymbolKind::FUNCTION; + } else if ($node instanceof Node\Stmt\ClassMethod) { + $symbol->kind = SymbolKind::METHOD; + } else if ($node instanceof Node\Stmt\PropertyProperty) { + $symbol->kind = SymbolKind::PROPERTY; + } else if ($node instanceof Node\Const_) { + $symbol->kind = SymbolKind::CONSTANT; + } else if ( + ( + ($node instanceof Node\Expr\Assign || $node instanceof Node\Expr\AssignOp) + && $node->var instanceof Node\Expr\Variable + ) + || $node instanceof Node\Expr\ClosureUse + || $node instanceof Node\Param + ) { + $symbol->kind = SymbolKind::VARIABLE; + } else { + return null; + } + if ($node instanceof Node\Expr\Assign || $node instanceof Node\Expr\AssignOp) { + $symbol->name = $node->var->name; + } else if ($node instanceof Node\Expr\ClosureUse) { + $symbol->name = $node->var; + } else if (isset($node->name)) { + $symbol->name = (string)$node->name; + } else { + return null; + } $symbol->location = Location::fromNode($node); if ($fqn !== null) { $parts = preg_split('/(::|\\\\)/', $fqn); diff --git a/src/Server/TextDocument.php b/src/Server/TextDocument.php index bb7667e7..a928e8b2 100644 --- a/src/Server/TextDocument.php +++ b/src/Server/TextDocument.php @@ -3,7 +3,7 @@ namespace LanguageServer\Server; -use LanguageServer\{LanguageClient, Project, PhpDocument}; +use LanguageServer\{LanguageClient, Project, PhpDocument, DefinitionResolver}; use PhpParser\PrettyPrinter\Standard as PrettyPrinter; use PhpParser\Node; use LanguageServer\Protocol\{ @@ -45,11 +45,17 @@ class TextDocument */ private $prettyPrinter; + /** + * @var DefinitionResolver + */ + private $definitionResolver; + public function __construct(Project $project, LanguageClient $client) { $this->project = $project; $this->client = $client; $this->prettyPrinter = new PrettyPrinter(); + $this->definitionResolver = new DefinitionResolver($project); } /** @@ -165,11 +171,11 @@ public function definition(TextDocumentIdentifier $textDocument, Position $posit if ($node === null) { return []; } - $def = yield $document->getDefinitionNodeByNode($node); - if ($def === null) { + $def = $this->definitionResolver->resolveReferenceNodeToDefinition($node); + if ($def === null || $def->symbolInformation === null) { return []; } - return Location::fromNode($def); + return $def->symbolInformation->location; }); } @@ -190,23 +196,34 @@ public function hover(TextDocumentIdentifier $textDocument, Position $position): return new Hover([]); } $range = Range::fromNode($node); - // Get the definition node for whatever node is under the cursor - $def = yield $document->getDefinitionNodeByNode($node); - if ($def === null) { - return new Hover([], $range); + if ($node instanceof Node\Expr\Variable) { + $defNode = DefinitionResolver::resolveVariableToNode($node); + } else { + // Get the definition for whatever node is under the cursor + $def = $this->definitionResolver->resolveReferenceNodeToDefinition($node); + if ($def === null) { + return new Hover([], $range); + } + // TODO inefficient. Add documentation and declaration line to Definition class + // so document doesnt have to be loaded + $document = yield $this->project->getOrLoadDocument($def->symbolInformation->location->uri); + if ($document === null) { + return new Hover([], $range); + } + $defNode = $document->getDefinitionNodeByFqn($def->fqn); } $contents = []; // Build a declaration string - if ($def instanceof Node\Stmt\PropertyProperty || $def instanceof Node\Const_) { + if ($defNode instanceof Node\Stmt\PropertyProperty || $defNode instanceof Node\Const_) { // Properties and constants can have multiple declarations // Use the parent node (that includes the modifiers), but only render the requested declaration - $child = $def; - $def = $def->getAttribute('parentNode'); - $defLine = clone $def; + $child = $defNode; + $defNode = $defNode->getAttribute('parentNode'); + $defLine = clone $defNode; $defLine->props = [$child]; } else { - $defLine = clone $def; + $defLine = clone $defNode; } // Don't include the docblock in the declaration string $defLine->setAttribute('comments', []); @@ -220,20 +237,20 @@ public function hover(TextDocumentIdentifier $textDocument, Position $position): } // Get the documentation string - if ($def instanceof Node\Param) { - $fn = $def->getAttribute('parentNode'); + if ($defNode instanceof Node\Param) { + $fn = $defNode->getAttribute('parentNode'); $docBlock = $fn->getAttribute('docBlock'); if ($docBlock !== null) { $tags = $docBlock->getTagsByName('param'); foreach ($tags as $tag) { - if ($tag->getVariableName() === $def->name) { + if ($tag->getVariableName() === $defNode->name) { $contents[] = $tag->getDescription()->render(); break; } } } } else { - $docBlock = $def->getAttribute('docBlock'); + $docBlock = $defNode->getAttribute('docBlock'); if ($docBlock !== null) { $contents[] = $docBlock->getSummary(); } diff --git a/src/utils.php b/src/utils.php index 061eff7a..859032d1 100644 --- a/src/utils.php +++ b/src/utils.php @@ -5,6 +5,7 @@ use Throwable; use InvalidArgumentException; +use PhpParser\Node; use Sabre\Event\{Loop, Promise}; /** @@ -77,3 +78,20 @@ function timeout($seconds = 0): Promise Loop\setTimeout([$promise, 'fulfill'], $seconds); return $promise; } + +/** + * Returns the closest node of a specific type + * + * @param Node $node + * @param string $type The node class name + * @return Node|null $type + */ +function getClosestNode(Node $node, string $type) +{ + $n = $node; + while ($n = $n->getAttribute('parentNode')) { + if ($n instanceof $type) { + return $n; + } + } +} From fb9efd47279da3b78c938718c0d3b480afdcfb62 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Thu, 17 Nov 2016 21:26:41 +0100 Subject: [PATCH 03/13] Correct docblock types --- src/Definition.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Definition.php b/src/Definition.php index ce8aca61..ae52826a 100644 --- a/src/Definition.php +++ b/src/Definition.php @@ -25,7 +25,7 @@ class Definition * - TestNamespace\TestClass::staticTestMethod() * - TestNamespace\TestClass::testMethod() * - * @var string + * @var string|null */ public $fqn; @@ -42,7 +42,7 @@ class Definition * Can also be a compound type. * If it is unknown, will be Types\Mixed. * - * @var \phpDocumentor\Type + * @var \phpDocumentor\Type|null */ public $type; From 13e42cc7d29e75c17033eae82f0b0b5e0f72a6a9 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Thu, 17 Nov 2016 21:52:48 +0100 Subject: [PATCH 04/13] Move logic out of Definition class --- src/Definition.php | 123 ----------------- src/DefinitionResolver.php | 125 +++++++++++++++++- src/NodeVisitor/DefinitionCollector.php | 13 +- src/PhpDocument.php | 4 +- tests/NodeVisitor/DefinitionCollectorTest.php | 6 +- 5 files changed, 138 insertions(+), 133 deletions(-) diff --git a/src/Definition.php b/src/Definition.php index ae52826a..021c66ea 100644 --- a/src/Definition.php +++ b/src/Definition.php @@ -45,127 +45,4 @@ class Definition * @var \phpDocumentor\Type|null */ public $type; - - /** - * Returns the type a reference to this symbol will resolve to. - * For properties and constants, this is the type of the property/constant. - * For functions and methods, this is the return type. - * For classes and interfaces, this is the class type (object). - * Variables are not indexed for performance reasons. - * Can also be a compound type. - * If it is unknown, will be Types\Mixed. - * Returns null if the node does not have a type. - * - * @param Node $node - * @return \phpDocumentor\Type|null - */ - public static function getTypeFromNode(Node $node) - { - if ($node instanceof Node\Param) { - // Parameters - $docBlock = $node->getAttribute('docBlock'); - if ($docBlock !== null && count($paramTags = $docBlock->getTagsByName('param')) > 0) { - // Use @param tag - return $paramTags[0]->getType(); - } - if ($node->type !== null) { - // Use PHP7 return type hint - if (is_string($node->type)) { - // Resolve a string like "bool" to a type object - $type = (new TypeResolver)->resolve($node->type); - } - $type = new Types\Object_(new Fqsen('\\' . (string)$node->type)); - if ($node->default !== null) { - if (is_string($node->default)) { - // Resolve a string like "bool" to a type object - $defaultType = (new TypeResolver)->resolve($node->default); - } - $defaultType = new Types\Object_(new Fqsen('\\' . (string)$node->default)); - $type = new Types\Compound([$type, $defaultType]); - } - } - // Unknown parameter type - return new Types\Mixed; - } - if ($node instanceof Node\FunctionLike) { - // Functions/methods - $docBlock = $node->getAttribute('docBlock'); - if ($docBlock !== null && count($returnTags = $docBlock->getTagsByName('return')) > 0) { - // Use @return tag - return $returnTags[0]->getType(); - } - if ($node->returnType !== null) { - // Use PHP7 return type hint - if (is_string($node->returnType)) { - // Resolve a string like "bool" to a type object - return (new TypeResolver)->resolve($node->returnType); - } - return new Types\Object_(new Fqsen('\\' . (string)$node->returnType)); - } - // Unknown return type - return new Types\Mixed; - } - if ($node instanceof Node\Stmt\PropertyProperty || $node instanceof Node\Const_) { - // Property or constant - $docBlock = $node->getAttribute('parentNode')->getAttribute('docBlock'); - if ($docBlock !== null && count($varTags = $docBlock->getTagsByName('var')) > 0) { - // Use @var tag - return $varTags[0]->getType(); - } - // TODO: read @property tags of class - // TODO: Try to infer the type from default value / constant value - // Unknown - return new Types\Mixed; - } - return null; - } - - /** - * Returns the fully qualified name (FQN) that is defined by a node - * Returns null if the node does not declare any symbol that can be referenced by an FQN - * - * @param Node $node - * @return string|null - */ - public static function getDefinedFqn(Node $node) - { - // Anonymous classes don't count as a definition - if ($node instanceof Node\Stmt\ClassLike && isset($node->name)) { - // Class, interface or trait declaration - return (string)$node->namespacedName; - } else if ($node instanceof Node\Stmt\Function_) { - // Function: use functionName() as the name - return (string)$node->namespacedName . '()'; - } else if ($node instanceof Node\Stmt\ClassMethod) { - // Class method: use ClassName::methodName() as name - $class = $node->getAttribute('parentNode'); - if (!isset($class->name)) { - // Ignore anonymous classes - return null; - } - return (string)$class->namespacedName . '::' . (string)$node->name . '()'; - } else if ($node instanceof Node\Stmt\PropertyProperty) { - // Property: use ClassName::propertyName as name - $class = $node->getAttribute('parentNode')->getAttribute('parentNode'); - if (!isset($class->name)) { - // Ignore anonymous classes - return null; - } - return (string)$class->namespacedName . '::' . (string)$node->name; - } else if ($node instanceof Node\Const_) { - $parent = $node->getAttribute('parentNode'); - if ($parent instanceof Node\Stmt\Const_) { - // Basic constant: use CONSTANT_NAME as name - return (string)$node->namespacedName; - } - if ($parent instanceof Node\Stmt\ClassConst) { - // Class constant: use ClassName::CONSTANT_NAME as name - $class = $parent->getAttribute('parentNode'); - if (!isset($class->name) || $class->name instanceof Node\Expr) { - return null; - } - return (string)$class->namespacedName . '::' . $node->name; - } - } - } } diff --git a/src/DefinitionResolver.php b/src/DefinitionResolver.php index f1914911..80495d78 100644 --- a/src/DefinitionResolver.php +++ b/src/DefinitionResolver.php @@ -12,10 +12,12 @@ class DefinitionResolver { private $project; + private $typeResolver; public function __construct(Project $project) { $this->project = $project; + $this->typeResolver = new TypeResolver; } /** @@ -35,7 +37,7 @@ public function resolveReferenceNodeToDefinition(Node $node) $def->symbolInformation = SymbolInformation::fromNode($defNode); if ($defNode instanceof Node\Param) { // Get parameter type - $def->type = Definition::getTypeFromNode($defNode); + $def->type = $this->getTypeFromNode($defNode); } else { // Resolve the type of the assignment/closure use node $def->type = $this->resolveExpression($defNode); @@ -244,7 +246,7 @@ private function resolveExpression(Node\Expr $expr): Type return $this->resolveExpression($defNode); } if ($defNode instanceof Node\Param) { - return Definition::getTypeFromNode($defNode); + return $this->getTypeFromNode($defNode); } } if ($expr instanceof Node\Expr\FuncCall) { @@ -434,4 +436,123 @@ private function resolveExpression(Node\Expr $expr): Type } return new Types\Mixed; } + + /** + * Returns the type a reference to this symbol will resolve to. + * For properties and constants, this is the type of the property/constant. + * For functions and methods, this is the return type. + * For classes and interfaces, this is the class type (object). + * Variables are not indexed for performance reasons. + * Can also be a compound type. + * If it is unknown, will be Types\Mixed. + * Returns null if the node does not have a type. + * + * @param Node $node + * @return \phpDocumentor\Type|null + */ + public function getTypeFromNode(Node $node) + { + if ($node instanceof Node\Param) { + // Parameters + $docBlock = $node->getAttribute('docBlock'); + if ($docBlock !== null && count($paramTags = $docBlock->getTagsByName('param')) > 0) { + // Use @param tag + return $paramTags[0]->getType(); + } + if ($node->type !== null) { + // Use PHP7 return type hint + if (is_string($node->type)) { + // Resolve a string like "bool" to a type object + $type = $this->typeResolver->resolve($node->type); + } + $type = new Types\Object_(new Fqsen('\\' . (string)$node->type)); + if ($node->default !== null) { + $defaultType = $this->resolveExpression($node->default); + $type = new Types\Compound([$type, $defaultType]); + } + } + // Unknown parameter type + return new Types\Mixed; + } + if ($node instanceof Node\FunctionLike) { + // Functions/methods + $docBlock = $node->getAttribute('docBlock'); + if ($docBlock !== null && count($returnTags = $docBlock->getTagsByName('return')) > 0) { + // Use @return tag + return $returnTags[0]->getType(); + } + if ($node->returnType !== null) { + // Use PHP7 return type hint + if (is_string($node->returnType)) { + // Resolve a string like "bool" to a type object + return $this->typeResolver->resolve($node->returnType); + } + return new Types\Object_(new Fqsen('\\' . (string)$node->returnType)); + } + // Unknown return type + return new Types\Mixed; + } + if ($node instanceof Node\Stmt\PropertyProperty || $node instanceof Node\Const_) { + // Property or constant + $docBlock = $node->getAttribute('parentNode')->getAttribute('docBlock'); + if ($docBlock !== null && count($varTags = $docBlock->getTagsByName('var')) > 0) { + // Use @var tag + return $varTags[0]->getType(); + } + // TODO: read @property tags of class + // TODO: Try to infer the type from default value / constant value + // Unknown + return new Types\Mixed; + } + return null; + } + + /** + * Returns the fully qualified name (FQN) that is defined by a node + * Returns null if the node does not declare any symbol that can be referenced by an FQN + * + * @param Node $node + * @return string|null + */ + public static function getDefinedFqn(Node $node) + { + // Anonymous classes don't count as a definition + if ($node instanceof Node\Stmt\ClassLike && isset($node->name)) { + // Class, interface or trait declaration + return (string)$node->namespacedName; + } else if ($node instanceof Node\Stmt\Function_) { + // Function: use functionName() as the name + return (string)$node->namespacedName . '()'; + } else if ($node instanceof Node\Stmt\ClassMethod) { + // Class method: use ClassName::methodName() as name + $class = $node->getAttribute('parentNode'); + if (!isset($class->name)) { + // Ignore anonymous classes + return null; + } + return (string)$class->namespacedName . '::' . (string)$node->name . '()'; + } else if ($node instanceof Node\Stmt\PropertyProperty) { + // Property: use ClassName::propertyName as name + $class = $node->getAttribute('parentNode')->getAttribute('parentNode'); + if (!isset($class->name)) { + // Ignore anonymous classes + return null; + } + return (string)$class->namespacedName . '::' . (string)$node->name; + } else if ($node instanceof Node\Const_) { + $parent = $node->getAttribute('parentNode'); + if ($parent instanceof Node\Stmt\Const_) { + // Basic constant: use CONSTANT_NAME as name + return (string)$node->namespacedName; + } + if ($parent instanceof Node\Stmt\ClassConst) { + // Class constant: use ClassName::CONSTANT_NAME as name + $class = $parent->getAttribute('parentNode'); + if (!isset($class->name) || $class->name instanceof Node\Expr) { + return null; + } + return (string)$class->namespacedName . '::' . $node->name; + } + } + } } diff --git a/src/NodeVisitor/DefinitionCollector.php b/src/NodeVisitor/DefinitionCollector.php index e0c35210..0cf5ea9e 100644 --- a/src/NodeVisitor/DefinitionCollector.php +++ b/src/NodeVisitor/DefinitionCollector.php @@ -4,7 +4,7 @@ namespace LanguageServer\NodeVisitor; use PhpParser\{NodeVisitorAbstract, Node}; -use LanguageServer\Definition; +use LanguageServer\{Definition, DefinitionResolver}; use LanguageServer\Protocol\SymbolInformation; /** @@ -27,9 +27,16 @@ class DefinitionCollector extends NodeVisitorAbstract */ public $nodes = []; + public $definitionResolver; + + public function __construct(DefinitionResolver $definitionResolver) + { + $this->definitionResolver = $definitionResolver; + } + public function enterNode(Node $node) { - $fqn = Definition::getDefinedFqn($node); + $fqn = DefinitionResolver::getDefinedFqn($node); // Only index definitions with an FQN (no variables) if ($fqn === null) { return; @@ -38,7 +45,7 @@ public function enterNode(Node $node) $def = new Definition; $def->fqn = $fqn; $def->symbolInformation = SymbolInformation::fromNode($node, $fqn); - $def->type = Definition::getTypeFromNode($node); + $def->type = $this->definitionResolver->getTypeFromNode($node); $this->definitions[$fqn] = $def; } } diff --git a/src/PhpDocument.php b/src/PhpDocument.php index 8a3fec01..e24b9acb 100644 --- a/src/PhpDocument.php +++ b/src/PhpDocument.php @@ -186,7 +186,7 @@ public function updateContent(string $content) $traverser = new NodeTraverser; // Collect all definitions - $definitionCollector = new DefinitionCollector; + $definitionCollector = new DefinitionCollector($this->definitionResolver); $traverser->addVisitor($definitionCollector); // Collect all references @@ -374,7 +374,7 @@ public function getReferenceNodesByNode(Node $node): Promise return $refCollector->nodes; } // Definition with a global FQN - $fqn = Definition::getDefinedFqn($node); + $fqn = DefinitionResolver::getDefinedFqn($node); if ($fqn === null) { return []; } diff --git a/tests/NodeVisitor/DefinitionCollectorTest.php b/tests/NodeVisitor/DefinitionCollectorTest.php index 4ed75088..ded65d1c 100644 --- a/tests/NodeVisitor/DefinitionCollectorTest.php +++ b/tests/NodeVisitor/DefinitionCollectorTest.php @@ -6,7 +6,7 @@ use PHPUnit\Framework\TestCase; use PhpParser\{NodeTraverser, Node}; use PhpParser\NodeVisitor\NameResolver; -use LanguageServer\{LanguageClient, Project, PhpDocument, Parser}; +use LanguageServer\{LanguageClient, Project, PhpDocument, Parser, DefinitionResolver}; use LanguageServer\Protocol\ClientCapabilities; use LanguageServer\Tests\MockProtocolStream; use LanguageServer\NodeVisitor\{ReferencesAdder, DefinitionCollector}; @@ -24,7 +24,7 @@ public function testCollectsSymbols() $traverser = new NodeTraverser; $traverser->addVisitor(new NameResolver); $traverser->addVisitor(new ReferencesAdder($document)); - $definitionCollector = new DefinitionCollector; + $definitionCollector = new DefinitionCollector(new DefinitionResolver($project)); $traverser->addVisitor($definitionCollector); $stmts = $parser->parse(file_get_contents($uri)); $traverser->traverse($stmts); @@ -63,7 +63,7 @@ public function testDoesNotCollectReferences() $traverser = new NodeTraverser; $traverser->addVisitor(new NameResolver); $traverser->addVisitor(new ReferencesAdder($document)); - $definitionCollector = new DefinitionCollector; + $definitionCollector = new DefinitionCollector(new DefinitionResolver($project)); $traverser->addVisitor($definitionCollector); $stmts = $parser->parse(file_get_contents($uri)); $traverser->traverse($stmts); From ba475849100e88819e3281f19be10f390cc6dd0d Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Thu, 17 Nov 2016 22:56:57 +0100 Subject: [PATCH 05/13] Handle more cases in DefinitionResolver --- src/DefinitionResolver.php | 47 ++++++++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/src/DefinitionResolver.php b/src/DefinitionResolver.php index 80495d78..1bc10af8 100644 --- a/src/DefinitionResolver.php +++ b/src/DefinitionResolver.php @@ -256,7 +256,10 @@ private function resolveExpression(Node\Expr $expr): Type return new Types\Mixed; } $fqn = (string)($expr->getAttribute('namespacedName') ?? $expr->name); - return $this->project->getDefinition($fqn)->type; + $def = $this->project->getDefinition($fqn); + if ($def !== null) { + return $def->type; + } } if ($expr instanceof Node\Expr\ConstFetch) { if (strtolower((string)$expr->name) === 'true' || strtolower((string)$expr->name) === 'false') { @@ -264,7 +267,10 @@ private function resolveExpression(Node\Expr $expr): Type } // Resolve constant $fqn = (string)($expr->getAttribute('namespacedName') ?? $expr->name); - return $this->project->getDefinition($fqn)->type; + $def = $this->project->getDefinition($fqn); + if ($def !== null) { + return $def->type; + } } if ($expr instanceof Node\Expr\MethodCall) { // Resolve object @@ -274,7 +280,10 @@ private function resolveExpression(Node\Expr $expr): Type return new Types\Mixed; } $fqn = (string)$objType->getFqsen() . '::' . $expr->name . '()'; - return $this->project->getDefinition($fqn)->type; + $def = $this->project->getDefinition($fqn); + if ($def !== null) { + return $def->type; + } } if ($expr instanceof Node\Expr\PropertyFetch) { // Resolve object @@ -284,8 +293,10 @@ private function resolveExpression(Node\Expr $expr): Type return new Types\Mixed; } $fqn = (string)$objType->getFqsen() . '::' . $expr->name; - return $this->project->getDefinition($fqn)->type; - } + $def = $this->project->getDefinition($fqn); + if ($def !== null) { + return $def->type; } + } if ($expr instanceof Node\Expr\StaticCall) { if ($expr->class instanceof Node\Expr || $expr->name instanceof Node\Expr) { // Need the FQN @@ -306,12 +317,30 @@ private function resolveExpression(Node\Expr $expr): Type } if ($expr->class instanceof Node\Stmt\Class_) { // Anonymous class - return new Types\Object; - } - if ((string)$expr->class === 'self') { return new Types\Object_; } - return new Types\Object_(new Fqsen('\\' . (string)$expr->class)); + $class = (string)$expr->class; + if ($class === 'static') { + return new Types\Static_; + } + if ($class === 'self' || $class === 'parent') { + $classNode = getClosestNode($expr, Node\Stmt\Class_::class); + if ($class === 'parent') { + if ($classNode === null || $classNode->extends === null) { + return new Types\Object_; + } + // parent is resolved to the parent class + $classFqn = (string)$classNode->extends; + } else { + if ($classNode === null) { + return new Types\Self_; + } + // self is resolved to the containing class + $classFqn = (string)$classNode->namespacedName; + } + return new Types\Object_(new Fqsen('\\' . $classFqn)); + } + return new Types\Object_(new Fqsen('\\' . $class)); } if ($expr instanceof Node\Expr\Clone_ || $expr instanceof Node\Expr\Assign) { return $this->resolveExpression($expr->expr); From 137ad31b87c7b97404a3da9b65d5a15aa88f3a75 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Thu, 17 Nov 2016 23:01:38 +0100 Subject: [PATCH 06/13] Fix lint issue --- src/DefinitionResolver.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/DefinitionResolver.php b/src/DefinitionResolver.php index 1bc10af8..7ab4e8da 100644 --- a/src/DefinitionResolver.php +++ b/src/DefinitionResolver.php @@ -295,8 +295,9 @@ private function resolveExpression(Node\Expr $expr): Type $fqn = (string)$objType->getFqsen() . '::' . $expr->name; $def = $this->project->getDefinition($fqn); if ($def !== null) { - return $def->type; } + return $def->type; } + } if ($expr instanceof Node\Expr\StaticCall) { if ($expr->class instanceof Node\Expr || $expr->name instanceof Node\Expr) { // Need the FQN From 3c00a349a47ce88b7690b20cbbe4a98f0091a1a1 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Thu, 17 Nov 2016 23:29:12 +0100 Subject: [PATCH 07/13] Fix null pointer --- src/DefinitionResolver.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DefinitionResolver.php b/src/DefinitionResolver.php index 7ab4e8da..87e88370 100644 --- a/src/DefinitionResolver.php +++ b/src/DefinitionResolver.php @@ -179,7 +179,7 @@ public function resolveReferenceNodeToFqn(Node $node) private static function getContainingClassFqn(Node $node) { $classNode = getClosestNode($node, Node\Stmt\Class_::class); - if ($classNode->isAnonymous()) { + if ($classNode === null || $classNode->isAnonymous()) { return null; } return (string)$classNode->namespacedName; From 279e2fb996fdedc3c6b8dc7753ef970c288be618 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Thu, 17 Nov 2016 23:25:01 +0100 Subject: [PATCH 08/13] Cache hover --- src/Definition.php | 14 ++++++ src/DefinitionResolver.php | 60 +++++++++++++++++++++++ src/NodeVisitor/DefinitionCollector.php | 5 +- src/Server/TextDocument.php | 65 +++---------------------- 4 files changed, 86 insertions(+), 58 deletions(-) diff --git a/src/Definition.php b/src/Definition.php index 021c66ea..cba69ab9 100644 --- a/src/Definition.php +++ b/src/Definition.php @@ -45,4 +45,18 @@ class Definition * @var \phpDocumentor\Type|null */ public $type; + + /** + * The first line of the declaration, for use in textDocument/hover + * + * @var string + */ + public $declarationLine; + + /** + * A documentation string, for use in textDocument/hover + * + * @var string + */ + public $documentation; } diff --git a/src/DefinitionResolver.php b/src/DefinitionResolver.php index 87e88370..2bfd4adb 100644 --- a/src/DefinitionResolver.php +++ b/src/DefinitionResolver.php @@ -4,6 +4,7 @@ namespace LanguageServer; use PhpParser\Node; +use PhpParser\PrettyPrinter\Standard as PrettyPrinter; use phpDocumentor\Reflection\{Types, Type, Fqsen, TypeResolver}; use LanguageServer\Protocol\SymbolInformation; use Sabre\Event\Promise; @@ -18,6 +19,61 @@ public function __construct(Project $project) { $this->project = $project; $this->typeResolver = new TypeResolver; + $this->prettyPrinter = new PrettyPrinter; + } + + /** + * Builds the declaration line for a given node + * + * @param Node $node + * @return string + */ + public function getDeclarationLineFromNode(Node $node): string + { + if ($node instanceof Node\Stmt\PropertyProperty || $node instanceof Node\Const_) { + // Properties and constants can have multiple declarations + // Use the parent node (that includes the modifiers), but only render the requested declaration + $child = $node; + $node = $node->getAttribute('parentNode'); + $defLine = clone $node; + $defLine->props = [$child]; + } else { + $defLine = clone $node; + } + // Don't include the docblock in the declaration string + $defLine->setAttribute('comments', []); + if (isset($defLine->stmts)) { + $defLine->stmts = []; + } + $defText = $this->prettyPrinter->prettyPrint([$defLine]); + return strstr($defText, "\n", true) ?: $defText; + } + + /** + * Gets the documentation string for a node, if it has one + * + * @param Node $node + * @return string|null + */ + public function getDocumentationFromNode(Node $node) + { + if ($node instanceof Node\Param) { + $fn = $node->getAttribute('parentNode'); + $docBlock = $fn->getAttribute('docBlock'); + if ($docBlock !== null) { + $tags = $docBlock->getTagsByName('param'); + foreach ($tags as $tag) { + if ($tag->getVariableName() === $node->name) { + return $tag->getDescription()->render(); + } + } + } + } else { + $docBlock = $node->getAttribute('docBlock'); + if ($docBlock !== null) { + return $docBlock->getSummary(); + } + } } /** @@ -35,6 +91,10 @@ public function resolveReferenceNodeToDefinition(Node $node) $def = new Definition; // Get symbol information from node (range, symbol kind) $def->symbolInformation = SymbolInformation::fromNode($defNode); + // Declaration line + $def->declarationLine = $this->getDeclarationLineFromNode($node); + // Documentation + $def->documentation = $this->getDocumentationFromNode($node); if ($defNode instanceof Node\Param) { // Get parameter type $def->type = $this->getTypeFromNode($defNode); diff --git a/src/NodeVisitor/DefinitionCollector.php b/src/NodeVisitor/DefinitionCollector.php index 0cf5ea9e..162f6708 100644 --- a/src/NodeVisitor/DefinitionCollector.php +++ b/src/NodeVisitor/DefinitionCollector.php @@ -27,7 +27,7 @@ class DefinitionCollector extends NodeVisitorAbstract */ public $nodes = []; - public $definitionResolver; + private $definitionResolver; public function __construct(DefinitionResolver $definitionResolver) { @@ -46,6 +46,9 @@ public function enterNode(Node $node) $def->fqn = $fqn; $def->symbolInformation = SymbolInformation::fromNode($node, $fqn); $def->type = $this->definitionResolver->getTypeFromNode($node); + $def->declarationLine = $this->definitionResolver->getDeclarationLineFromNode($node); + $def->documentation = $this->definitionResolver->getDocumentationFromNode($node); + $this->definitions[$fqn] = $def; } } diff --git a/src/Server/TextDocument.php b/src/Server/TextDocument.php index a928e8b2..6c673888 100644 --- a/src/Server/TextDocument.php +++ b/src/Server/TextDocument.php @@ -196,66 +196,17 @@ public function hover(TextDocumentIdentifier $textDocument, Position $position): return new Hover([]); } $range = Range::fromNode($node); - if ($node instanceof Node\Expr\Variable) { - $defNode = DefinitionResolver::resolveVariableToNode($node); - } else { - // Get the definition for whatever node is under the cursor - $def = $this->definitionResolver->resolveReferenceNodeToDefinition($node); - if ($def === null) { - return new Hover([], $range); - } - // TODO inefficient. Add documentation and declaration line to Definition class - // so document doesnt have to be loaded - $document = yield $this->project->getOrLoadDocument($def->symbolInformation->location->uri); - if ($document === null) { - return new Hover([], $range); - } - $defNode = $document->getDefinitionNodeByFqn($def->fqn); - } - $contents = []; - - // Build a declaration string - if ($defNode instanceof Node\Stmt\PropertyProperty || $defNode instanceof Node\Const_) { - // Properties and constants can have multiple declarations - // Use the parent node (that includes the modifiers), but only render the requested declaration - $child = $defNode; - $defNode = $defNode->getAttribute('parentNode'); - $defLine = clone $defNode; - $defLine->props = [$child]; - } else { - $defLine = clone $defNode; - } - // Don't include the docblock in the declaration string - $defLine->setAttribute('comments', []); - if (isset($defLine->stmts)) { - $defLine->stmts = []; + // Get the definition for whatever node is under the cursor + $def = $this->definitionResolver->resolveReferenceNodeToDefinition($node); + if ($def === null) { + return new Hover([], $range); } - $defText = $this->prettyPrinter->prettyPrint([$defLine]); - $lines = explode("\n", $defText); - if (isset($lines[0])) { - $contents[] = new MarkedString('php', "declarationLine) { + $contents[] = new MarkedString('php', "declarationLine); } - - // Get the documentation string - if ($defNode instanceof Node\Param) { - $fn = $defNode->getAttribute('parentNode'); - $docBlock = $fn->getAttribute('docBlock'); - if ($docBlock !== null) { - $tags = $docBlock->getTagsByName('param'); - foreach ($tags as $tag) { - if ($tag->getVariableName() === $defNode->name) { - $contents[] = $tag->getDescription()->render(); - break; - } - } - } - } else { - $docBlock = $defNode->getAttribute('docBlock'); - if ($docBlock !== null) { - $contents[] = $docBlock->getSummary(); - } + if ($def->documentation) { + $contents[] = $def->documentation; } - return new Hover($contents, $range); }); } From f20a62f21c27a4522a39cea3fece0a9ff30b181f Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Fri, 18 Nov 2016 03:24:30 +0100 Subject: [PATCH 09/13] Improvements --- src/DefinitionResolver.php | 204 ++++++++++++++++++++++--------------- src/Project.php | 12 ++- 2 files changed, 132 insertions(+), 84 deletions(-) diff --git a/src/DefinitionResolver.php b/src/DefinitionResolver.php index 2bfd4adb..ab7582e7 100644 --- a/src/DefinitionResolver.php +++ b/src/DefinitionResolver.php @@ -12,7 +12,14 @@ class DefinitionResolver { + /** + * @var \LanguageServer\Project + */ private $project; + + /** + * @var \phpDocumentor\Reflection\TypeResolver + */ private $typeResolver; public function __construct(Project $project) @@ -57,6 +64,9 @@ public function getDeclarationLineFromNode(Node $node): string */ public function getDocumentationFromNode(Node $node) { + if ($node instanceof Node\Stmt\PropertyProperty || $node instanceof Node\Const_) { + $node = $node->getAttribute('parentNode'); + } if ($node instanceof Node\Param) { $fn = $node->getAttribute('parentNode'); $docBlock = $fn->getAttribute('docBlock'); @@ -88,13 +98,16 @@ public function resolveReferenceNodeToDefinition(Node $node) if ($node instanceof Node\Expr\Variable) { // Resolve the variable to a definition node (assignment, param or closure use) $defNode = self::resolveVariableToNode($node); + if ($defNode === null) { + return null; + } $def = new Definition; // Get symbol information from node (range, symbol kind) $def->symbolInformation = SymbolInformation::fromNode($defNode); // Declaration line - $def->declarationLine = $this->getDeclarationLineFromNode($node); + $def->declarationLine = $this->getDeclarationLineFromNode($defNode); // Documentation - $def->documentation = $this->getDocumentationFromNode($node); + $def->documentation = $this->getDocumentationFromNode($defNode); if ($defNode instanceof Node\Param) { // Get parameter type $def->type = $this->getTypeFromNode($defNode); @@ -110,19 +123,12 @@ public function resolveReferenceNodeToDefinition(Node $node) if ($fqn === null) { return null; } + // If the node is a function or constant, it could be namespaced, but PHP falls back to global + // http://php.net/manual/en/language.namespaces.fallback.php + $parent = $node->getAttribute('parentNode'); + $globalFallback = $parent instanceof Node\Expr\ConstFetch || $parent instanceof Node\Expr\FuncCall; // Return the Definition object from the project index - $def = $this->project->getDefinition($fqn); - if ($def === null) { - // If the node is a function or constant, it could be namespaced, but PHP falls back to global - // http://php.net/manual/en/language.namespaces.fallback.php - $parent = $node->getAttribute('parentNode'); - if ($parent instanceof Node\Expr\ConstFetch || $parent instanceof Node\Expr\FuncCall) { - $parts = explode('\\', $fqn); - $fqn = end($parts); - $def = $this->project->getDefinition($fqn); - } - } - return $def; + return $this->project->getDefinition($fqn, $globalFallback); } /** @@ -291,8 +297,8 @@ public static function resolveVariableToNode(Node\Expr\Variable $var) * Given an expression node, resolves that expression recursively to a type. * If the type could not be resolved, returns Types\Mixed. * - * @param Node\Expr $expr - * @return Type + * @param \PhpParser\Node\Expr $expr + * @return \phpDocumentor\Type */ private function resolveExpression(Node\Expr $expr): Type { @@ -316,7 +322,7 @@ private function resolveExpression(Node\Expr $expr): Type return new Types\Mixed; } $fqn = (string)($expr->getAttribute('namespacedName') ?? $expr->name); - $def = $this->project->getDefinition($fqn); + $def = $this->project->getDefinition($fqn, true); if ($def !== null) { return $def->type; } @@ -327,81 +333,62 @@ private function resolveExpression(Node\Expr $expr): Type } // Resolve constant $fqn = (string)($expr->getAttribute('namespacedName') ?? $expr->name); - $def = $this->project->getDefinition($fqn); + $def = $this->project->getDefinition($fqn, true); if ($def !== null) { return $def->type; } } - if ($expr instanceof Node\Expr\MethodCall) { - // Resolve object - $objType = $this->resolveExpression($expr->var); - if (!($objType instanceof Types\Object_) || $objType->getFqsen() === null || $expr->name instanceof Node\Expr) { - // Need the class FQN of the object + if ($expr instanceof Node\Expr\MethodCall || $expr instanceof Node\Expr\PropertyFetch) { + if ($expr->name instanceof Node\Expr) { return new Types\Mixed; } - $fqn = (string)$objType->getFqsen() . '::' . $expr->name . '()'; - $def = $this->project->getDefinition($fqn); - if ($def !== null) { - return $def->type; - } - } - if ($expr instanceof Node\Expr\PropertyFetch) { // Resolve object $objType = $this->resolveExpression($expr->var); - if (!($objType instanceof Types\Object_) || $objType->getFqsen() === null || $expr->name instanceof Node\Expr) { - // Need the class FQN of the object - return new Types\Mixed; - } - $fqn = (string)$objType->getFqsen() . '::' . $expr->name; - $def = $this->project->getDefinition($fqn); - if ($def !== null) { - return $def->type; + if (!($objType instanceof Types\Compound)) { + $objType = new Types\Compound([$objType]); + } + for ($i = 0; $t = $objType->get($i); $i++) { + if ($t instanceof Types\This) { + $classFqn = self::getContainingClassFqn($expr); + if ($classFqn === null) { + return new Types\Mixed; + } + } else if (!($t instanceof Types\Object_) || $t->getFqsen() === null) { + return new Types\Mixed; + } else { + $classFqn = substr((string)$t->getFqsen(), 1); + } + $fqn = $classFqn . '::' . $expr->name; + if ($expr instanceof Node\Expr\MethodCall) { + $fqn .= '()'; + } + $def = $this->project->getDefinition($fqn); + if ($def !== null) { + return $def->type; + } } } - if ($expr instanceof Node\Expr\StaticCall) { - if ($expr->class instanceof Node\Expr || $expr->name instanceof Node\Expr) { - // Need the FQN + if ( + $expr instanceof Node\Expr\StaticCall + || $expr instanceof Node\Expr\StaticPropertyFetch + || $expr instanceof Node\Expr\ClassConstFetch + ) { + $classType = self::resolveClassNameToType($expr->class); + if (!($classType instanceof Types\Object_) || $classType->getFqsen() === null || $expr->name instanceof Node\Expr) { return new Types\Mixed; } - $fqn = (string)$expr->class . '::' . $expr->name . '()'; - } - if ($expr instanceof Node\Expr\StaticPropertyFetch || $expr instanceof Node\Expr\ClassConstFetch) { - if ($expr->class instanceof Node\Expr || $expr->name instanceof Node\Expr) { - // Need the FQN + $fqn = substr((string)$classType->getFqsen(), 1) . '::' . $expr->name; + if ($expr instanceof Node\Expr\StaticCall) { + $fqn .= '()'; + } + $def = $this->project->getDefinition($fqn); + if ($def === null) { return new Types\Mixed; } - $fqn = (string)$expr->class . '::' . $expr->name; + return $def->type; } if ($expr instanceof Node\Expr\New_) { - if ($expr->class instanceof Node\Expr) { - return new Types\Mixed; - } - if ($expr->class instanceof Node\Stmt\Class_) { - // Anonymous class - return new Types\Object_; - } - $class = (string)$expr->class; - if ($class === 'static') { - return new Types\Static_; - } - if ($class === 'self' || $class === 'parent') { - $classNode = getClosestNode($expr, Node\Stmt\Class_::class); - if ($class === 'parent') { - if ($classNode === null || $classNode->extends === null) { - return new Types\Object_; - } - // parent is resolved to the parent class - $classFqn = (string)$classNode->extends; - } else { - if ($classNode === null) { - return new Types\Self_; - } - // self is resolved to the containing class - $classFqn = (string)$classNode->namespacedName; - } - return new Types\Object_(new Fqsen('\\' . $classFqn)); - } - return new Types\Object_(new Fqsen('\\' . $class)); + return self::resolveClassNameToType($expr->class); } if ($expr instanceof Node\Expr\Clone_ || $expr instanceof Node\Expr\Assign) { return $this->resolveExpression($expr->expr); @@ -445,7 +432,7 @@ private function resolveExpression(Node\Expr $expr): Type || $expr instanceof Node\Expr\BinaryOp\NotEqual || $expr instanceof Node\Expr\BinaryOp\NotIdentical ) { - return new Types\Boolean_; + return new Types\Boolean; } if ( $expr instanceof Node\Expr\Concat @@ -509,9 +496,21 @@ private function resolveExpression(Node\Expr $expr): Type } $valueTypes = array_unique($keyTypes); $keyTypes = array_unique($keyTypes); - $valueType = count($valueTypes) > 1 ? new Types\Compound($valueTypes) : $valueTypes[0]; - $keyType = count($keyTypes) > 1 ? new Types\Compound($keyTypes) : $keyTypes[0]; - return new Types\Array_($valueTypes, $keyTypes); + if (empty($valueTypes)) { + $valueType = null; + } else if (count($valueTypes) === 1) { + $valueType = $valueTypes[0]; + } else { + $valueType = new Types\Compound($valueTypes); + } + if (empty($keyTypes)) { + $keyType = null; + } else if (count($keyTypes) === 1) { + $keyType = $keyTypes[0]; + } else { + $keyType = new Types\Compound($keyTypes); + } + return new Types\Array_($valueType, $keyType); } if ($expr instanceof Node\Expr\ArrayDimFetch) { $varType = $this->resolveExpression($expr->var); @@ -527,10 +526,51 @@ private function resolveExpression(Node\Expr $expr): Type return new Types\Mixed; } - /** + /** + * Takes any class name node (from a static method call, or new node) and returns a Type object + * Resolves keywords like self, static and parent + * + * @param Node $class + * @return Type + */ + private static function resolveClassNameToType(Node $class): Type + { + if ($class instanceof Node\Expr) { + return new Types\Mixed; + } + if ($class instanceof Node\Stmt\Class_) { + // Anonymous class + return new Types\Object_; + } + $className = (string)$class; + if ($className === 'static') { + return new Types\Static_; + } + if ($className === 'self' || $className === 'parent') { + $classNode = getClosestNode($class, Node\Stmt\Class_::class); + if ($className === 'parent') { + if ($classNode === null || $classNode->extends === null) { + return new Types\Object_; + } + // parent is resolved to the parent class + $classFqn = (string)$classNode->extends; + } else { + if ($classNode === null) { + return new Types\Self_; + } + // self is resolved to the containing class + $classFqn = (string)$classNode->namespacedName; + } + return new Types\Object_(new Fqsen('\\' . $classFqn)); + } + return new Types\Object_(new Fqsen('\\' . $className)); + } + + /** * Returns the type a reference to this symbol will resolve to. * For properties and constants, this is the type of the property/constant. * For functions and methods, this is the return type. + * For parameters, this is the type of the parameter. * For classes and interfaces, this is the class type (object). * Variables are not indexed for performance reasons. * Can also be a compound type. @@ -544,7 +584,7 @@ public function getTypeFromNode(Node $node) { if ($node instanceof Node\Param) { // Parameters - $docBlock = $node->getAttribute('docBlock'); + $docBlock = $node->getAttribute('parentNode')->getAttribute('docBlock'); if ($docBlock !== null && count($paramTags = $docBlock->getTagsByName('param')) > 0) { // Use @param tag return $paramTags[0]->getType(); diff --git a/src/Project.php b/src/Project.php index e037266d..3021a1c9 100644 --- a/src/Project.php +++ b/src/Project.php @@ -207,11 +207,19 @@ public function getDefinitions() /** * Returns the Definition object by a specific FQN * + * @param string $fqn + * @param bool $globalFallback Whether to fallback to global if the namespaced FQN was not found * @return Definition|null */ - public function getDefinition(string $fqn) + public function getDefinition(string $fqn, $globalFallback = false) { - return $this->definitions[$fqn] ?? null; + if (isset($this->definitions[$fqn])) { + return $this->definitions[$fqn]; + } else if ($globalFallback) { + $parts = explode('\\', $fqn); + $fqn = end($parts); + return $this->getDefinition($fqn); + } } /** From d764b7d6cc2d1a65e23828bfe6f88cbb4b8c5416 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Fri, 18 Nov 2016 10:11:11 +0100 Subject: [PATCH 10/13] Add test for nested method call --- fixtures/global_references.php | 3 +++ fixtures/references.php | 3 +++ tests/Server/ServerTestCase.php | 12 ++++++++---- tests/Server/TextDocument/Definition/GlobalTest.php | 12 ++++++++++++ 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/fixtures/global_references.php b/fixtures/global_references.php index 8346923c..b4ec0f1d 100644 --- a/fixtures/global_references.php +++ b/fixtures/global_references.php @@ -34,3 +34,6 @@ function whatever(TestClass $param): TestClass { if ($abc instanceof TestInterface) { } + +// Nested expression +$obj->testProperty->testMethod(); diff --git a/fixtures/references.php b/fixtures/references.php index ca274438..992ec008 100644 --- a/fixtures/references.php +++ b/fixtures/references.php @@ -34,3 +34,6 @@ function whatever(TestClass $param): TestClass { if ($abc instanceof TestInterface) { } + +// Nested expressions +$obj->testProperty->testMethod(); diff --git a/tests/Server/ServerTestCase.php b/tests/Server/ServerTestCase.php index c5818a96..2fc4db1e 100644 --- a/tests/Server/ServerTestCase.php +++ b/tests/Server/ServerTestCase.php @@ -118,7 +118,8 @@ public function setUp() ], 'TestNamespace\\TestClass::testProperty' => [ 0 => new Location($symbolsUri, new Range(new Position(59, 8), new Position(59, 27))), // $this->testProperty = $testParameter; - 1 => new Location($referencesUri, new Range(new Position( 6, 5), new Position( 6, 23))) + 1 => new Location($referencesUri, new Range(new Position( 6, 5), new Position( 6, 23))), // echo $obj->testProperty; + 2 => new Location($referencesUri, new Range(new Position(38, 0), new Position(38, 18))) // $obj->testProperty->testMethod(); ], 'TestNamespace\\TestClass::staticTestProperty' => [ 0 => new Location($referencesUri, new Range(new Position( 8, 5), new Position( 8, 35))) @@ -127,7 +128,8 @@ public function setUp() 0 => new Location($referencesUri, new Range(new Position( 7, 0), new Position( 7, 29))) ], 'TestNamespace\\TestClass::testMethod()' => [ - 0 => new Location($referencesUri, new Range(new Position( 5, 0), new Position( 5, 18))) + 0 => new Location($referencesUri, new Range(new Position( 5, 0), new Position( 5, 18))), // $obj->testMethod(); + 1 => new Location($referencesUri, new Range(new Position(38, 0), new Position(38, 32))) // $obj->testProperty->testMethod(); ], 'TestNamespace\\test_function()' => [ 0 => new Location($referencesUri, new Range(new Position(10, 0), new Position(10, 13))), @@ -158,7 +160,8 @@ public function setUp() ], 'TestClass::testProperty' => [ 0 => new Location($globalSymbolsUri, new Range(new Position(59, 8), new Position(59, 27))), // $this->testProperty = $testParameter; - 1 => new Location($globalReferencesUri, new Range(new Position( 6, 5), new Position( 6, 23))) + 1 => new Location($globalReferencesUri, new Range(new Position( 6, 5), new Position( 6, 23))), // echo $obj->testProperty; + 2 => new Location($globalReferencesUri, new Range(new Position(38, 0), new Position(38, 18))) // $obj->testProperty->testMethod(); ], 'TestClass::staticTestProperty' => [ 0 => new Location($globalReferencesUri, new Range(new Position( 8, 5), new Position( 8, 35))) @@ -167,7 +170,8 @@ public function setUp() 0 => new Location($globalReferencesUri, new Range(new Position( 7, 0), new Position( 7, 29))) ], 'TestClass::testMethod()' => [ - 0 => new Location($globalReferencesUri, new Range(new Position( 5, 0), new Position( 5, 18))) + 0 => new Location($globalReferencesUri, new Range(new Position( 5, 0), new Position( 5, 18))), // $obj->testMethod(); + 1 => new Location($globalReferencesUri, new Range(new Position(38, 0), new Position(38, 32))) // $obj->testProperty->testMethod(); ], 'test_function()' => [ 0 => new Location($globalReferencesUri, new Range(new Position(10, 0), new Position(10, 13))), diff --git a/tests/Server/TextDocument/Definition/GlobalTest.php b/tests/Server/TextDocument/Definition/GlobalTest.php index bc5f6e6d..94d278b4 100644 --- a/tests/Server/TextDocument/Definition/GlobalTest.php +++ b/tests/Server/TextDocument/Definition/GlobalTest.php @@ -292,4 +292,16 @@ public function testDefinitionForInstanceOf() )->wait(); $this->assertEquals($this->getDefinitionLocation('TestInterface'), $result); } + + public function testDefinitionForNestedMethodCall() + { + // $obj->testProperty->testMethod(); + // Get definition for testMethod + $reference = $this->getReferenceLocations('TestClass::testMethod()')[1]; + $result = $this->textDocument->definition( + new TextDocumentIdentifier($reference->uri), + $reference->range->end + )->wait(); + $this->assertEquals($this->getDefinitionLocation('TestClass::testMethod()'), $result); + } } From 241960a4f78938cd60246ea1057f651ce94d1d73 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Fri, 18 Nov 2016 11:27:24 +0100 Subject: [PATCH 11/13] Add test for array access --- fixtures/global_references.php | 1 + fixtures/global_symbols.php | 2 +- fixtures/references.php | 1 + fixtures/symbols.php | 2 +- tests/Server/ServerTestCase.php | 16 +++++++++++----- .../TextDocument/Definition/GlobalTest.php | 12 ++++++++++++ 6 files changed, 27 insertions(+), 7 deletions(-) diff --git a/fixtures/global_references.php b/fixtures/global_references.php index b4ec0f1d..0ac117b5 100644 --- a/fixtures/global_references.php +++ b/fixtures/global_references.php @@ -37,3 +37,4 @@ function whatever(TestClass $param): TestClass { // Nested expression $obj->testProperty->testMethod(); +TestClass::$staticTestProperty[123]->testProperty; diff --git a/fixtures/global_symbols.php b/fixtures/global_symbols.php index 6eba8bbb..f5c755bc 100644 --- a/fixtures/global_symbols.php +++ b/fixtures/global_symbols.php @@ -30,7 +30,7 @@ class TestClass implements TestInterface /** * Lorem excepteur officia sit anim velit veniam enim. * - * @var TestClass + * @var TestClass[] */ public static $staticTestProperty; diff --git a/fixtures/references.php b/fixtures/references.php index 992ec008..2ac1a634 100644 --- a/fixtures/references.php +++ b/fixtures/references.php @@ -37,3 +37,4 @@ function whatever(TestClass $param): TestClass { // Nested expressions $obj->testProperty->testMethod(); +TestClass::$staticTestProperty[123]->testProperty; diff --git a/fixtures/symbols.php b/fixtures/symbols.php index 89111015..4b0a6d94 100644 --- a/fixtures/symbols.php +++ b/fixtures/symbols.php @@ -30,7 +30,7 @@ class TestClass implements TestInterface /** * Lorem excepteur officia sit anim velit veniam enim. * - * @var TestClass + * @var TestClass[] */ public static $staticTestProperty; diff --git a/tests/Server/ServerTestCase.php b/tests/Server/ServerTestCase.php index 2fc4db1e..1a608ca4 100644 --- a/tests/Server/ServerTestCase.php +++ b/tests/Server/ServerTestCase.php @@ -105,7 +105,8 @@ public function setUp() 3 => new Location($referencesUri, new Range(new Position( 9, 5), new Position( 9, 14))), // TestClass::TEST_CLASS_CONST; 4 => new Location($referencesUri, new Range(new Position(21, 18), new Position(21, 27))), // function whatever(TestClass $param) 5 => new Location($referencesUri, new Range(new Position(21, 37), new Position(21, 46))), // function whatever(TestClass $param): TestClass - 6 => new Location($useUri, new Range(new Position( 4, 4), new Position( 4, 27))), // use TestNamespace\TestClass; + 6 => new Location($referencesUri, new Range(new Position(39, 0), new Position(39, 9))), // TestClass::$staticTestProperty[123]->testProperty; + 7 => new Location($useUri, new Range(new Position( 4, 4), new Position( 4, 27))), // use TestNamespace\TestClass; ], 'TestNamespace\\TestInterface' => [ 0 => new Location($symbolsUri, new Range(new Position(20, 27), new Position(20, 40))), // class TestClass implements TestInterface @@ -119,10 +120,12 @@ public function setUp() 'TestNamespace\\TestClass::testProperty' => [ 0 => new Location($symbolsUri, new Range(new Position(59, 8), new Position(59, 27))), // $this->testProperty = $testParameter; 1 => new Location($referencesUri, new Range(new Position( 6, 5), new Position( 6, 23))), // echo $obj->testProperty; - 2 => new Location($referencesUri, new Range(new Position(38, 0), new Position(38, 18))) // $obj->testProperty->testMethod(); + 2 => new Location($referencesUri, new Range(new Position(38, 0), new Position(38, 18))), // $obj->testProperty->testMethod(); + 3 => new Location($referencesUri, new Range(new Position(39, 0), new Position(39, 49))) // TestClass::$staticTestProperty[123]->testProperty; ], 'TestNamespace\\TestClass::staticTestProperty' => [ - 0 => new Location($referencesUri, new Range(new Position( 8, 5), new Position( 8, 35))) + 0 => new Location($referencesUri, new Range(new Position( 8, 5), new Position( 8, 35))), // echo TestClass::$staticTestProperty; + 1 => new Location($referencesUri, new Range(new Position(39, 0), new Position(39, 30))) // TestClass::$staticTestProperty[123]->testProperty; ], 'TestNamespace\\TestClass::staticTestMethod()' => [ 0 => new Location($referencesUri, new Range(new Position( 7, 0), new Position( 7, 29))) @@ -148,6 +151,7 @@ public function setUp() 3 => new Location($globalReferencesUri, new Range(new Position( 9, 5), new Position( 9, 14))), // TestClass::TEST_CLASS_CONST; 4 => new Location($globalReferencesUri, new Range(new Position(21, 18), new Position(21, 27))), // function whatever(TestClass $param) 5 => new Location($globalReferencesUri, new Range(new Position(21, 37), new Position(21, 46))), // function whatever(TestClass $param): TestClass + 6 => new Location($globalReferencesUri, new Range(new Position(39, 0), new Position(39, 9))), // TestClass::$staticTestProperty[123]->testProperty; ], 'TestInterface' => [ 0 => new Location($globalSymbolsUri, new Range(new Position(20, 27), new Position(20, 40))), // class TestClass implements TestInterface @@ -161,10 +165,12 @@ public function setUp() 'TestClass::testProperty' => [ 0 => new Location($globalSymbolsUri, new Range(new Position(59, 8), new Position(59, 27))), // $this->testProperty = $testParameter; 1 => new Location($globalReferencesUri, new Range(new Position( 6, 5), new Position( 6, 23))), // echo $obj->testProperty; - 2 => new Location($globalReferencesUri, new Range(new Position(38, 0), new Position(38, 18))) // $obj->testProperty->testMethod(); + 2 => new Location($globalReferencesUri, new Range(new Position(38, 0), new Position(38, 18))), // $obj->testProperty->testMethod(); + 3 => new Location($globalReferencesUri, new Range(new Position(39, 0), new Position(39, 49))) // TestClass::$staticTestProperty[123]->testProperty; ], 'TestClass::staticTestProperty' => [ - 0 => new Location($globalReferencesUri, new Range(new Position( 8, 5), new Position( 8, 35))) + 0 => new Location($globalReferencesUri, new Range(new Position( 8, 5), new Position( 8, 35))), // echo TestClass::$staticTestProperty; + 1 => new Location($globalReferencesUri, new Range(new Position(39, 0), new Position(39, 30))) // TestClass::$staticTestProperty[123]->testProperty; ], 'TestClass::staticTestMethod()' => [ 0 => new Location($globalReferencesUri, new Range(new Position( 7, 0), new Position( 7, 29))) diff --git a/tests/Server/TextDocument/Definition/GlobalTest.php b/tests/Server/TextDocument/Definition/GlobalTest.php index 94d278b4..553cdc03 100644 --- a/tests/Server/TextDocument/Definition/GlobalTest.php +++ b/tests/Server/TextDocument/Definition/GlobalTest.php @@ -304,4 +304,16 @@ public function testDefinitionForNestedMethodCall() )->wait(); $this->assertEquals($this->getDefinitionLocation('TestClass::testMethod()'), $result); } + + public function testDefinitionForPropertyFetchOnArrayDimFetch() + { + // $obj->testProperty->testMethod(); + // Get definition for testProperty + $reference = $this->getReferenceLocations('TestClass::testProperty')[3]; + $result = $this->textDocument->definition( + new TextDocumentIdentifier($reference->uri), + $reference->range->end + )->wait(); + $this->assertEquals($this->getDefinitionLocation('TestClass::testProperty'), $result); + } } From 993c88b2ffb0a686a44c2401953f6b35281c4957 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Fri, 18 Nov 2016 11:56:29 +0100 Subject: [PATCH 12/13] Fix comment --- tests/Server/TextDocument/Definition/GlobalTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Server/TextDocument/Definition/GlobalTest.php b/tests/Server/TextDocument/Definition/GlobalTest.php index 553cdc03..2b1e353d 100644 --- a/tests/Server/TextDocument/Definition/GlobalTest.php +++ b/tests/Server/TextDocument/Definition/GlobalTest.php @@ -307,7 +307,7 @@ public function testDefinitionForNestedMethodCall() public function testDefinitionForPropertyFetchOnArrayDimFetch() { - // $obj->testProperty->testMethod(); + // TestClass::$staticTestProperty[123]->testProperty; // Get definition for testProperty $reference = $this->getReferenceLocations('TestClass::testProperty')[3]; $result = $this->textDocument->definition( From 54ad082677b1ea8dd64cc1a60657ac5b54b637a8 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Fri, 18 Nov 2016 15:15:59 +0100 Subject: [PATCH 13/13] Rename resolveExpression() --- src/DefinitionResolver.php | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/DefinitionResolver.php b/src/DefinitionResolver.php index ab7582e7..b9a253e2 100644 --- a/src/DefinitionResolver.php +++ b/src/DefinitionResolver.php @@ -113,7 +113,7 @@ public function resolveReferenceNodeToDefinition(Node $node) $def->type = $this->getTypeFromNode($defNode); } else { // Resolve the type of the assignment/closure use node - $def->type = $this->resolveExpression($defNode); + $def->type = $this->resolveExpressionNodeToType($defNode); } return $def; } @@ -177,7 +177,7 @@ public function resolveReferenceNodeToFqn(Node $node) return null; } // Get the type of the left-hand expression - $varType = $this->resolveExpression($node->var); + $varType = $this->resolveExpressionNodeToType($node->var); if ($varType instanceof Types\This) { // $this is resolved to the containing class $classFqn = self::getContainingClassFqn($node); @@ -300,7 +300,7 @@ public static function resolveVariableToNode(Node\Expr\Variable $var) * @param \PhpParser\Node\Expr $expr * @return \phpDocumentor\Type */ - private function resolveExpression(Node\Expr $expr): Type + private function resolveExpressionNodeToType(Node\Expr $expr): Type { if ($expr instanceof Node\Expr\Variable) { if ($expr->name === 'this') { @@ -309,7 +309,7 @@ private function resolveExpression(Node\Expr $expr): Type // Find variable definition $defNode = $this->resolveVariableToNode($expr); if ($defNode instanceof Node\Expr) { - return $this->resolveExpression($defNode); + return $this->resolveExpressionNodeToType($defNode); } if ($defNode instanceof Node\Param) { return $this->getTypeFromNode($defNode); @@ -343,7 +343,7 @@ private function resolveExpression(Node\Expr $expr): Type return new Types\Mixed; } // Resolve object - $objType = $this->resolveExpression($expr->var); + $objType = $this->resolveExpressionNodeToType($expr->var); if (!($objType instanceof Types\Compound)) { $objType = new Types\Compound([$objType]); } @@ -391,27 +391,27 @@ private function resolveExpression(Node\Expr $expr): Type return self::resolveClassNameToType($expr->class); } if ($expr instanceof Node\Expr\Clone_ || $expr instanceof Node\Expr\Assign) { - return $this->resolveExpression($expr->expr); + return $this->resolveExpressionNodeToType($expr->expr); } if ($expr instanceof Node\Expr\Ternary) { // ?: if ($expr->if === null) { return new Types\Compound([ - $this->resolveExpression($expr->cond), - $this->resolveExpression($expr->else) + $this->resolveExpressionNodeToType($expr->cond), + $this->resolveExpressionNodeToType($expr->else) ]); } // Ternary is a compound of the two possible values return new Types\Compound([ - $this->resolveExpression($expr->if), - $this->resolveExpression($expr->else) + $this->resolveExpressionNodeToType($expr->if), + $this->resolveExpressionNodeToType($expr->else) ]); } if ($expr instanceof Node\Expr\BinaryOp\Coalesce) { // ?? operator return new Types\Compound([ - $this->resolveExpression($expr->left), - $this->resolveExpression($expr->right) + $this->resolveExpressionNodeToType($expr->left), + $this->resolveExpressionNodeToType($expr->right) ]); } if ( @@ -491,8 +491,8 @@ private function resolveExpression(Node\Expr $expr): Type $valueTypes = []; $keyTypes = []; foreach ($expr->items as $item) { - $valueTypes[] = $this->resolveExpression($item->value); - $keyTypes[] = $item->key ? $this->resolveExpression($item->key) : new Types\Integer; + $valueTypes[] = $this->resolveExpressionNodeToType($item->value); + $keyTypes[] = $item->key ? $this->resolveExpressionNodeToType($item->key) : new Types\Integer; } $valueTypes = array_unique($keyTypes); $keyTypes = array_unique($keyTypes); @@ -513,7 +513,7 @@ private function resolveExpression(Node\Expr $expr): Type return new Types\Array_($valueType, $keyType); } if ($expr instanceof Node\Expr\ArrayDimFetch) { - $varType = $this->resolveExpression($expr->var); + $varType = $this->resolveExpressionNodeToType($expr->var); if (!($varType instanceof Types\Array_)) { return new Types\Mixed; } @@ -597,7 +597,7 @@ public function getTypeFromNode(Node $node) } $type = new Types\Object_(new Fqsen('\\' . (string)$node->type)); if ($node->default !== null) { - $defaultType = $this->resolveExpression($node->default); + $defaultType = $this->resolveExpressionNodeToType($node->default); $type = new Types\Compound([$type, $defaultType]); } }