1
0
Fork 0

Add support for getting references of variables

pull/52/head
Felix Becker 2016-10-11 20:38:02 +02:00
parent a5f8e86095
commit ccfbf9c8f2
4 changed files with 145 additions and 20 deletions

View File

@ -0,0 +1,50 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\NodeVisitor;
use PhpParser\{NodeVisitorAbstract, Node, NodeTraverser};
/**
* Collects all references to a variable
*/
class VariableReferencesCollector extends NodeVisitorAbstract
{
/**
* Array of references to the variable
*
* @var Node\Expr\Variable[]
*/
public $references = [];
/**
* @var string
*/
private $name;
/**
* @param string $name The variable name
*/
public function __construct(string $name)
{
$this->name = $name;
}
public function enterNode(Node $node)
{
if ($node instanceof Node\Expr\Variable && $node->name === $this->name) {
$this->references[] = $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
if ($node instanceof Node\Expr\Closure) {
foreach ($node->uses as $use) {
if ($use->var === $this->name) {
return;
}
}
}
return NodeTraverser::DONT_TRAVERSE_CHILDREN;
}
}
}

View File

@ -9,7 +9,8 @@ use LanguageServer\NodeVisitor\{
ReferencesAdder, ReferencesAdder,
DefinitionCollector, DefinitionCollector,
ColumnCalculator, ColumnCalculator,
ReferencesCollector ReferencesCollector,
VariableReferencesCollector
}; };
use PhpParser\{Error, Node, NodeTraverser, Parser}; use PhpParser\{Error, Node, NodeTraverser, Parser};
use PhpParser\NodeVisitor\NameResolver; use PhpParser\NodeVisitor\NameResolver;
@ -58,7 +59,7 @@ class PhpDocument
* *
* @var Node[] * @var Node[]
*/ */
private $statements; private $stmts;
/** /**
* Map from fully qualified name (FQN) to Node * Map from fully qualified name (FQN) to Node
@ -177,7 +178,7 @@ class PhpDocument
$this->project->addReferenceDocument($fqn, $this); $this->project->addReferenceDocument($fqn, $this);
} }
$this->statements = $stmts; $this->stmts = $stmts;
} }
} }
@ -214,6 +215,16 @@ class PhpDocument
return $this->uri; return $this->uri;
} }
/**
* Returns the AST of the document
*
* @return Node[]
*/
public function getStmts(): array
{
return $this->stmts;
}
/** /**
* Returns the node at a specified position * Returns the node at a specified position
* *
@ -225,7 +236,7 @@ class PhpDocument
$traverser = new NodeTraverser; $traverser = new NodeTraverser;
$finder = new NodeAtPositionFinder($position); $finder = new NodeAtPositionFinder($position);
$traverser->addVisitor($finder); $traverser->addVisitor($finder);
$traverser->traverse($this->statements); $traverser->traverse($this->stmts);
return $finder->node; return $finder->node;
} }
@ -453,6 +464,53 @@ class PhpDocument
return $document->getDefinitionByFqn($fqn); return $document->getDefinitionByFqn($fqn);
} }
/**
* Returns the reference nodes for any node
* The references node MAY be in other documents, check the ownerDocument attribute
*
* @param Node $node
* @return Node[]
*/
public function getReferencesByNode(Node $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) {
if ($node->name instanceof Node\Expr) {
return null;
}
// Find function/method/closure scope
$n = $node;
while (isset($n) && !($n instanceof Node\FunctionLike)) {
$n = $n->getAttribute('parentNode');
}
if (!isset($n)) {
$n = $node->getAttribute('ownerDocument');
}
$traverser = new NodeTraverser;
$refCollector = new VariableReferencesCollector($node->name);
$traverser->addVisitor($refCollector);
$traverser->traverse($n->getStmts());
return $refCollector->references;
}
// Definition with a global FQN
$fqn = $this->getDefinedFqn($node);
if ($fqn === null) {
return [];
}
$refDocuments = $this->project->getReferenceDocuments($fqn);
$nodes = [];
foreach ($refDocuments as $document) {
$refs = $document->getReferencesByFqn($fqn);
if ($refs !== null) {
foreach ($refs as $ref) {
$nodes[] = $ref;
}
}
}
return $nodes;
}
/** /**
* Returns the assignment or parameter node where a variable was defined * Returns the assignment or parameter node where a variable was defined
* *

View File

@ -110,28 +110,19 @@ class TextDocument
* denoted by the given text document position. * denoted by the given text document position.
* *
* @param ReferenceContext $context * @param ReferenceContext $context
* @return Location[]|null * @return Location[]
*/ */
public function references(ReferenceContext $context, TextDocumentIdentifier $textDocument, Position $position) public function references(ReferenceContext $context, TextDocumentIdentifier $textDocument, Position $position): array
{ {
$document = $this->project->getDocument($textDocument->uri); $document = $this->project->getDocument($textDocument->uri);
$node = $document->getNodeAtPosition($position); $node = $document->getNodeAtPosition($position);
if ($node === null) { if ($node === null) {
return null; return [];
} }
$fqn = $document->getDefinedFqn($node); $refs = $document->getReferencesByNode($node);
if ($fqn === null) {
return null;
}
$refDocuments = $this->project->getReferenceDocuments($fqn);
$locations = []; $locations = [];
foreach ($refDocuments as $document) { foreach ($refs as $ref) {
$refs = $document->getReferencesByFqn($fqn); $locations[] = Location::fromNode($ref);
if ($refs !== null) {
foreach ($refs as $ref) {
$locations[] = Location::fromNode($ref);
}
}
} }
return $locations; return $locations;
} }

View File

@ -278,6 +278,19 @@ class NamespacedTest extends TestCase
// Get definition for $var // Get definition for $var
$result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier($this->referencesUri), new Position(13, 7)); $result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier($this->referencesUri), new Position(13, 7));
$this->assertEquals([ $this->assertEquals([
[
'uri' => $this->referencesUri,
'range' => [
'start' => [
'line' => 12,
'character' => 0
],
'end' => [
'line' => 12,
'character' => 4
]
]
],
[ [
'uri' => $this->referencesUri, 'uri' => $this->referencesUri,
'range' => [ 'range' => [
@ -287,7 +300,20 @@ class NamespacedTest extends TestCase
], ],
'end' => [ 'end' => [
'line' => 13, 'line' => 13,
'character' => 8 'character' => 9
]
]
],
[
'uri' => $this->referencesUri,
'range' => [
'start' => [
'line' => 20,
'character' => 9
],
'end' => [
'line' => 20,
'character' => 13
] ]
] ]
] ]