diff --git a/src/Definition.php b/src/Definition.php new file mode 100644 index 0000000..88aa8b2 --- /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 a723bdc..4766338 100644 --- a/src/NodeVisitor/DefinitionCollector.php +++ b/src/NodeVisitor/DefinitionCollector.php @@ -4,7 +4,7 @@ declare(strict_types = 1); namespace LanguageServer\NodeVisitor; use PhpParser\{NodeVisitorAbstract, Node}; -use LanguageServer\Protocol\SymbolInformation; +use LanguageServer\Definition; use function LanguageServer\Fqn\getDefinedFqn; /** @@ -14,18 +14,18 @@ use function LanguageServer\Fqn\getDefinedFqn; 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 @@ class DefinitionCollector extends NodeVisitorAbstract 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 08b660d..841fc7e 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 @@ class ReferencesCollector extends NodeVisitorAbstract 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 a113a7e..bb44bdc 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 @@ class VariableReferencesCollector extends NodeVisitorAbstract 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 214f97b..b91d3e0 100644 --- a/src/PhpDocument.php +++ b/src/PhpDocument.php @@ -74,26 +74,26 @@ class PhpDocument */ private $stmts; + /** + * Map from fully qualified name (FQN) to Definition + * + * @var Definition[] + */ + private $definitions; + /** * Map from fully qualified name (FQN) to Node * * @var Node[] */ - private $definitions; + private $definitionNodes; /** * Map from fully qualified name (FQN) to array of nodes that reference the symbol * * @var Node[][] */ - private $references; - - /** - * Map from fully qualified name (FQN) to SymbolInformation - * - * @var SymbolInformation[] - */ - private $symbols; + private $referenceNodes; /** * @param string $uri The URI of the document @@ -121,7 +121,7 @@ class PhpDocument */ 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 @@ class PhpDocument // 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 @@ class PhpDocument * @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 @@ class PhpDocument * * @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 @@ class PhpDocument * @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 @@ class PhpDocument if (!isset($document)) { return null; } - return $document->getDefinitionByFqn($fqn); + return $document->getDefinitionNodeByFqn($fqn); }); } @@ -369,7 +369,7 @@ class PhpDocument * @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 @@ class PhpDocument $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 49bc646..2fa74e4 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 @@ class Project } /** - * 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 @@ class Project */ 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 1fc8f27..bb7667e 100644 --- a/src/Server/TextDocument.php +++ b/src/Server/TextDocument.php @@ -62,7 +62,11 @@ class TextDocument 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 @@ class TextDocument 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 @@ class TextDocument if ($node === null) { return []; } - $def = yield $document->getDefinitionByNode($node); + $def = yield $document->getDefinitionNodeByNode($node); if ($def === null) { return []; } @@ -187,7 +191,7 @@ class TextDocument } $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 e894c49..26feb72 100644 --- a/src/Server/Workspace.php +++ b/src/Server/Workspace.php @@ -39,13 +39,10 @@ class Workspace */ 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 6df5940..4ed7508 100644 --- a/tests/NodeVisitor/DefinitionCollectorTest.php +++ b/tests/NodeVisitor/DefinitionCollectorTest.php @@ -28,7 +28,7 @@ class DefinitionCollectorTest extends TestCase $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 @@ class DefinitionCollectorTest extends TestCase '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 @@ class DefinitionCollectorTest extends TestCase $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()']); } }