1
0
Fork 0

Add support for find all references

pull/52/head
Felix Becker 2016-10-09 14:47:14 +02:00
parent 66b5176a43
commit a92d25535a
7 changed files with 514 additions and 6 deletions

View File

@ -105,6 +105,8 @@ class LanguageServer extends \AdvancedJsonRpc\Dispatcher
$serverCapabilities->documentFormattingProvider = true; $serverCapabilities->documentFormattingProvider = true;
// Support "Go to definition" // Support "Go to definition"
$serverCapabilities->definitionProvider = true; $serverCapabilities->definitionProvider = true;
// Support "Find all references"
$serverCapabilities->referencesProvider = true;
return new InitializeResult($serverCapabilities); return new InitializeResult($serverCapabilities);
} }

View File

@ -0,0 +1,55 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\NodeVisitor;
use PhpParser\{NodeVisitorAbstract, Node};
/**
* Collects references to classes, interfaces, traits, methods, properties and constants
* Depends on ReferencesAdder and NameResolver
*/
class ReferencesCollector extends NodeVisitorAbstract
{
/**
* Map from fully qualified name (FQN) to array of nodes that reference the symbol
*
* @var Node[][]
*/
public $references;
/**
* @var Node[]
*/
private $definitions;
/**
* @param Node[] $definitions The definitions that references should be tracked for
*/
public function __construct(array $definitions)
{
$this->definitions = $definitions;
$this->references = array_fill_keys(array_keys($definitions), []);
}
public function enterNode(Node $node)
{
// Check if the node references any global symbol
$fqn = $node->getAttribute('ownerDocument')->getReferencedFqn($node);
if ($fqn) {
$this->references[$fqn][] = $node;
// Static method calls, constant and property fetches also need to register a reference to the class
// A reference like TestNamespace\TestClass::myStaticMethod() registers a reference for
// - TestNamespace\TestClass
// - TestNamespace\TestClass::myStaticMethod()
if (
($node instanceof Node\Expr\StaticCall
|| $node instanceof Node\Expr\StaticPropertyFetch
|| $node instanceof Node\Expr\ClassConstFetch)
&& $node->class instanceof Node\Name
) {
$this->references[(string)$node->class][] = $node->class;
}
}
}
}

View File

@ -4,7 +4,13 @@ declare(strict_types = 1);
namespace LanguageServer; namespace LanguageServer;
use LanguageServer\Protocol\{Diagnostic, DiagnosticSeverity, Range, Position, TextEdit}; use LanguageServer\Protocol\{Diagnostic, DiagnosticSeverity, Range, Position, TextEdit};
use LanguageServer\NodeVisitor\{NodeAtPositionFinder, ReferencesAdder, DefinitionCollector, ColumnCalculator}; use LanguageServer\NodeVisitor\{
NodeAtPositionFinder,
ReferencesAdder,
DefinitionCollector,
ColumnCalculator,
ReferencesCollector
};
use PhpParser\{Error, Node, NodeTraverser, Parser}; use PhpParser\{Error, Node, NodeTraverser, Parser};
use PhpParser\NodeVisitor\NameResolver; use PhpParser\NodeVisitor\NameResolver;
@ -85,6 +91,18 @@ class PhpDocument
} }
/** /**
* Get all references of a fully qualified name
*
* @param string $fqn The fully qualified name of the symbol
* @return Node[]
*/
public function getReferencesByFqn(string $fqn)
{
return isset($this->references) && isset($this->references[$fqn]) ? $this->references[$fqn] : null;
}
/**
* Updates the content on this document.
* Re-parses a source file, updates symbols and reports parsing errors * Re-parses a source file, updates symbols and reports parsing errors
* that may have occured as diagnostics. * that may have occured as diagnostics.
* *
@ -146,8 +164,20 @@ class PhpDocument
$this->project->setDefinitionUri($fqn, $this->uri); $this->project->setDefinitionUri($fqn, $this->uri);
} }
$this->statements = $stmts;
$this->definitions = $definitionCollector->definitions; $this->definitions = $definitionCollector->definitions;
// Collect all references
$traverser = new NodeTraverser;
$referencesCollector = new ReferencesCollector($this->definitions);
$traverser->addVisitor($referencesCollector);
$traverser->traverse($stmts);
$this->references = $referencesCollector->references;
// Register this document on the project for references
foreach ($referencesCollector->references as $fqn => $nodes) {
$this->project->addReferenceDocument($fqn, $this);
}
$this->statements = $stmts;
} }
} }

View File

@ -16,12 +16,19 @@ class Project
private $documents = []; private $documents = [];
/** /**
* An associative array that maps fully qualified symbol names to document URIs * An associative array that maps fully qualified symbol names to document URIs that define the symbol
* *
* @var string[] * @var string[]
*/ */
private $definitions = []; private $definitions = [];
/**
* An associative array that maps fully qualified symbol names to arrays of document URIs that reference the symbol
*
* @var PhpDocument[][]
*/
private $references = [];
/** /**
* Instance of the PHP parser * Instance of the PHP parser
* *
@ -143,6 +150,34 @@ class Project
$this->definitions[$fqn] = $uri; $this->definitions[$fqn] = $uri;
} }
/**
* Adds a document as a referencee of a specific symbol
*
* @param string $fqn The fully qualified name of the symbol
* @return void
*/
public function addReferenceDocument(string $fqn, PhpDocument $document)
{
if (!isset($this->references[$fqn])) {
$this->references[$fqn] = [];
}
// TODO: use DS\Set instead of searching array
if (array_search($document, $this->references[$fqn], true) === false) {
$this->references[$fqn][] = $document;
}
}
/**
* Returns all documents that reference a symbol
*
* @param string $fqn The fully qualified name of the symbol
* @return PhpDocument[]
*/
public function getReferenceDocuments(string $fqn)
{
return $this->references[$fqn] ?? [];
}
/** /**
* Returns the document where a symbol is defined * Returns the document where a symbol is defined
* *

View File

@ -12,7 +12,8 @@ use LanguageServer\Protocol\{
FormattingOptions, FormattingOptions,
TextEdit, TextEdit,
Location, Location,
SymbolInformation SymbolInformation,
ReferenceContext
}; };
/** /**
@ -30,7 +31,7 @@ class TextDocument
/** /**
* @var Project * @var Project
*/ */
private $project; public $project;
public function __construct(Project $project, LanguageClient $client) public function __construct(Project $project, LanguageClient $client)
{ {
@ -104,6 +105,37 @@ class TextDocument
return $this->project->getDocument($textDocument->uri)->getFormattedText(); return $this->project->getDocument($textDocument->uri)->getFormattedText();
} }
/**
* The references request is sent from the client to the server to resolve project-wide references for the symbol
* denoted by the given text document position.
*
* @param ReferenceContext $context
* @return Location[]|null
*/
public function references(ReferenceContext $context, TextDocumentIdentifier $textDocument, Position $position)
{
$document = $this->project->getDocument($textDocument->uri);
$node = $document->getNodeAtPosition($position);
if ($node === null) {
return null;
}
$fqn = $document->getDefinedFqn($node);
if ($fqn === null) {
return null;
}
$refDocuments = $this->project->getReferenceDocuments($fqn);
$locations = [];
foreach ($refDocuments as $document) {
$refs = $document->getReferencesByFqn($fqn);
if ($refs !== null) {
foreach ($refs as $ref) {
$locations[] = Location::fromNode($ref);
}
}
}
return $locations;
}
/** /**
* The goto definition request is sent from the client to the server to resolve the definition location of a symbol * The goto definition request is sent from the client to the server to resolve the definition location of a symbol
* at a given text document position. * at a given text document position.

View File

@ -35,7 +35,7 @@ class LanguageServerTest extends TestCase
'completionProvider' => null, 'completionProvider' => null,
'signatureHelpProvider' => null, 'signatureHelpProvider' => null,
'definitionProvider' => true, 'definitionProvider' => true,
'referencesProvider' => null, 'referencesProvider' => true,
'documentHighlightProvider' => null, 'documentHighlightProvider' => null,
'workspaceSymbolProvider' => true, 'workspaceSymbolProvider' => true,
'codeActionProvider' => null, 'codeActionProvider' => null,

View File

@ -0,0 +1,354 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Tests\Server\TextDocument;
use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, LanguageClient, Project};
use LanguageServer\Protocol\{TextDocumentIdentifier, Position, ReferenceContext};
use function LanguageServer\pathToUri;
class ReferencesTest extends TestCase
{
/**
* @var Server\TextDocument
*/
private $textDocument;
private $symbolsUri;
private $referencesUri;
private $useUri;
public function setUp()
{
$client = new LanguageClient(new MockProtocolStream());
$project = new Project($client);
$this->textDocument = new Server\TextDocument($project, $client);
$this->symbolsUri = pathToUri(realpath(__DIR__ . '/../../../fixtures/symbols.php'));
$this->referencesUri = pathToUri(realpath(__DIR__ . '/../../../fixtures/references.php'));
$this->useUri = pathToUri(realpath(__DIR__ . '/../../../fixtures/use.php'));
$project->loadDocument($this->referencesUri, file_get_contents($this->referencesUri));
$project->loadDocument($this->symbolsUri, file_get_contents($this->symbolsUri));
$project->loadDocument($this->useUri, file_get_contents($this->useUri));
}
public function testReferencesForClassLike()
{
// class TestClass implements TestInterface
// Get references for TestClass
$result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier($this->symbolsUri), new Position(6, 9));
$this->assertEquals([
// $obj = new TestClass();
[
'uri' => $this->referencesUri,
'range' => [
'start' => [
'line' => 4,
'character' => 11
],
'end' => [
'line' => 4,
'character' => 20
]
]
],
// TestClass::staticTestMethod();
[
'uri' => $this->referencesUri,
'range' => [
'start' => [
'line' => 7,
'character' => 0
],
'end' => [
'line' => 7,
'character' => 9
]
]
],
// echo TestClass::$staticTestProperty;
[
'uri' => $this->referencesUri,
'range' => [
'start' => [
'line' => 8,
'character' => 5
],
'end' => [
'line' => 8,
'character' => 14
]
]
],
// TestClass::TEST_CLASS_CONST;
[
'uri' => $this->referencesUri,
'range' => [
'start' => [
'line' => 9,
'character' => 5
],
'end' => [
'line' => 9,
'character' => 14
]
]
],
// function whatever(TestClass $param)
[
'uri' => $this->referencesUri,
'range' => [
'start' => [
'line' => 15,
'character' => 18
],
'end' => [
'line' => 15,
'character' => 27
]
]
],
// function whatever(TestClass $param): TestClass
[
'uri' => $this->referencesUri,
'range' => [
'start' => [
'line' => 15,
'character' => 37
],
'end' => [
'line' => 15,
'character' => 46
]
]
],
// use TestNamespace\TestClass;
[
'uri' => $this->useUri,
'range' => [
'start' => [
'line' => 4,
'character' => 4
],
'end' => [
'line' => 4,
'character' => 27
]
]
]
], json_decode(json_encode($result), true));
}
public function testReferencesForClassConstants()
{
// const TEST_CLASS_CONST = 123;
// Get references for TEST_CLASS_CONST
$result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier($this->symbolsUri), new Position(8, 19));
$this->assertEquals([
[
'uri' => $this->referencesUri,
'range' => [
'start' => [
'line' => 9,
'character' => 5
],
'end' => [
'line' => 9,
'character' => 32
]
]
]
], json_decode(json_encode($result), true));
}
public function testReferencesForConstants()
{
// const TEST_CONST = 123;
// Get references for TEST_CONST
$result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier($this->symbolsUri), new Position(4, 13));
$this->assertEquals([
[
'uri' => $this->referencesUri,
'range' => [
'start' => [
'line' => 23,
'character' => 5
],
'end' => [
'line' => 23,
'character' => 15
]
]
]
], json_decode(json_encode($result), true));
}
public function testReferencesForStaticMethods()
{
$this->markTestIncomplete();
// TestClass::staticTestMethod();
// Get definition for staticTestMethod
$result = $this->textDocument->definition(new TextDocumentIdentifier($this->referencesUri), new Position(7, 20));
$this->assertEquals([
'uri' => $this->symbolsUri,
'range' => [
'start' => [
'line' => 12,
'character' => 4
],
'end' => [
'line' => 15,
'character' => 4
]
]
], json_decode(json_encode($result), true));
}
public function testReferencesForStaticProperties()
{
$this->markTestIncomplete();
// echo TestClass::$staticTestProperty;
// Get definition for staticTestProperty
$result = $this->textDocument->definition(new TextDocumentIdentifier($this->referencesUri), new Position(8, 25));
$this->assertEquals([
'uri' => $this->symbolsUri,
'range' => [
'start' => [
'line' => 9,
'character' => 18
],
'end' => [
'line' => 9,
'character' => 36
]
]
], json_decode(json_encode($result), true));
}
public function testReferencesForMethods()
{
$this->markTestIncomplete();
// $obj->testMethod();
// Get definition for testMethod
$result = $this->textDocument->definition(new TextDocumentIdentifier($this->referencesUri), new Position(5, 11));
$this->assertEquals([
'uri' => $this->symbolsUri,
'range' => [
'start' => [
'line' => 17,
'character' => 4
],
'end' => [
'line' => 20,
'character' => 4
]
]
], json_decode(json_encode($result), true));
}
public function testReferencesForProperties()
{
$this->markTestIncomplete();
// echo $obj->testProperty;
// Get definition for testProperty
$result = $this->textDocument->definition(new TextDocumentIdentifier($this->referencesUri), new Position(6, 18));
$this->assertEquals([
'uri' => $this->symbolsUri,
'range' => [
'start' => [
'line' => 10,
'character' => 11
],
'end' => [
'line' => 10,
'character' => 23
]
]
], json_decode(json_encode($result), true));
}
public function testReferencesForVariables()
{
$this->markTestIncomplete();
// echo $var;
// Get definition for $var
$result = $this->textDocument->definition(new TextDocumentIdentifier($this->referencesUri), new Position(13, 7));
$this->assertEquals([
'uri' => $this->referencesUri,
'range' => [
'start' => [
'line' => 12,
'character' => 0
],
'end' => [
'line' => 12,
'character' => 9
]
]
], json_decode(json_encode($result), true));
}
public function testReferencesForParams()
{
$this->markTestIncomplete();
// echo $param;
// Get definition for $param
$result = $this->textDocument->definition(new TextDocumentIdentifier($this->referencesUri), new Position(16, 13));
$this->assertEquals([
'uri' => $this->referencesUri,
'range' => [
'start' => [
'line' => 15,
'character' => 18
],
'end' => [
'line' => 15,
'character' => 33
]
]
], json_decode(json_encode($result), true));
}
public function testReferencesForUsedVariables()
{
$this->markTestIncomplete();
// echo $var;
// Get definition for $var
$result = $this->textDocument->definition(new TextDocumentIdentifier($this->referencesUri), new Position(20, 11));
$this->assertEquals([
'uri' => $this->referencesUri,
'range' => [
'start' => [
'line' => 19,
'character' => 22
],
'end' => [
'line' => 19,
'character' => 25
]
]
], json_decode(json_encode($result), true));
}
public function testReferencesForFunctions()
{
$this->markTestIncomplete();
// test_function();
// Get definition for test_function
$result = $this->textDocument->definition(new TextDocumentIdentifier($this->referencesUri), new Position(10, 4));
$this->assertEquals([
'uri' => $this->symbolsUri,
'range' => [
'start' => [
'line' => 33,
'character' => 0
],
'end' => [
'line' => 36,
'character' => 0
]
]
], json_decode(json_encode($result), true));
}
}