Add Definition class
parent
c19aedcef2
commit
e83f95efca
|
@ -0,0 +1,94 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer;
|
||||
|
||||
use PhpParser\Node;
|
||||
use phpDocumentor\Reflection\{Types, Type, Fqsen, TypeResolver};
|
||||
use LanguageServer\Protocol\SymbolInformation;
|
||||
use Exception;
|
||||
use function LanguageServer\Fqn\getDefinedFqn;
|
||||
|
||||
/**
|
||||
* Class used to represent definitions that can be referenced by an FQN
|
||||
*/
|
||||
class Definition
|
||||
{
|
||||
/**
|
||||
* @var Protocol\SymbolInformation
|
||||
*/
|
||||
public $symbolInformation;
|
||||
|
||||
/**
|
||||
* 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 any other declaration it will be null.
|
||||
* Can also be a compound type.
|
||||
* If it is unknown, will be Types\Mixed.
|
||||
*
|
||||
* @var \phpDocumentor\Type
|
||||
*/
|
||||
public $type;
|
||||
|
||||
/**
|
||||
* Returns the definition defined in a node
|
||||
*
|
||||
* @return self
|
||||
* @throws Exception If the node is not a declaration node
|
||||
*/
|
||||
public static function fromNode(Node $node): self
|
||||
{
|
||||
$def = new self;
|
||||
$def->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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 <Node|null>
|
||||
*/
|
||||
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 <Node[]>
|
||||
*/
|
||||
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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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()']);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue