1
0
Fork 0

Add Definition class

pull/155/head
Felix Becker 2016-11-16 20:53:39 +01:00
parent c19aedcef2
commit e83f95efca
9 changed files with 185 additions and 91 deletions

94
src/Definition.php Normal file
View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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

View File

@ -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);

View File

@ -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);
}
/**

View File

@ -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);
}

View File

@ -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;

View File

@ -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()']);
}
}