1
0
Fork 0

Implement global references protocol extension (#236)

pull/249/head v4.2.0
Felix Becker 2017-01-10 17:08:52 -08:00 committed by GitHub
parent 49245fd4d3
commit 106aa24b5d
11 changed files with 432 additions and 40 deletions

View File

@ -42,8 +42,8 @@ class ProjectIndex extends AbstractAggregateIndex
*/ */
public function getIndexForUri(string $uri): Index public function getIndexForUri(string $uri): Index
{ {
if (preg_match('/\/vendor\/(\w+\/\w+)\//', $uri, $matches)) { if (preg_match('/\/vendor\/([^\/]+\/[^\/]+)\//', $uri, $matches)) {
$packageName = $matches[0]; $packageName = $matches[1];
return $this->dependenciesIndex->getDependencyIndex($packageName); return $this->dependenciesIndex->getDependencyIndex($packageName);
} }
return $this->sourceIndex; return $this->sourceIndex;

View File

@ -81,6 +81,30 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
*/ */
protected $documentLoader; protected $documentLoader;
/**
* The parsed composer.json file in the project, if any
*
* @var \stdClass
*/
protected $composerJson;
/**
* The parsed composer.lock file in the project, if any
*
* @var \stdClass
*/
protected $composerLock;
/**
* @var GlobalIndex
*/
protected $globalIndex;
/**
* @var DefinitionResolver
*/
protected $definitionResolver;
/** /**
* @param PotocolReader $reader * @param PotocolReader $reader
* @param ProtocolWriter $writer * @param ProtocolWriter $writer
@ -144,8 +168,6 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
{ {
return coroutine(function () use ($capabilities, $rootPath, $processId) { return coroutine(function () use ($capabilities, $rootPath, $processId) {
yield null;
if ($capabilities->xfilesProvider) { if ($capabilities->xfilesProvider) {
$this->filesFinder = new ClientFilesFinder($this->client); $this->filesFinder = new ClientFilesFinder($this->client);
} else { } else {
@ -158,31 +180,53 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
$this->contentRetriever = new FileSystemContentRetriever; $this->contentRetriever = new FileSystemContentRetriever;
} }
$projectIndex = new ProjectIndex(new Index, new DependenciesIndex); $dependenciesIndex = new DependenciesIndex;
$sourceIndex = new Index;
$projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex);
$stubsIndex = StubsIndex::read(); $stubsIndex = StubsIndex::read();
$globalIndex = new GlobalIndex($stubsIndex, $projectIndex); $this->globalIndex = new GlobalIndex($stubsIndex, $projectIndex);
// The DefinitionResolver should look in stubs, the project source and dependencies // The DefinitionResolver should look in stubs, the project source and dependencies
$definitionResolver = new DefinitionResolver($globalIndex); $this->definitionResolver = new DefinitionResolver($this->globalIndex);
$this->documentLoader = new PhpDocumentLoader( $this->documentLoader = new PhpDocumentLoader(
$this->contentRetriever, $this->contentRetriever,
$projectIndex, $projectIndex,
$definitionResolver $this->definitionResolver
); );
if ($rootPath !== null) { if ($rootPath !== null) {
$this->index($rootPath)->otherwise('\\LanguageServer\\crash'); yield $this->index($rootPath)->otherwise('LanguageServer\\crash');
} }
$this->textDocument = new Server\TextDocument( // Find composer.json
$this->documentLoader, if ($this->composerJson === null) {
$definitionResolver, $composerJsonFiles = yield $this->filesFinder->find(Path::makeAbsolute('**/composer.json', $rootPath));
$this->client, if (!empty($composerJsonFiles)) {
$globalIndex $this->composerJson = json_decode(yield $this->contentRetriever->retrieve($composerJsonFiles[0]));
); }
// workspace/symbol should only look inside the project source and dependencies }
$this->workspace = new Server\Workspace($projectIndex, $this->client); // Find composer.lock
if ($this->composerLock === null) {
$composerLockFiles = yield $this->filesFinder->find(Path::makeAbsolute('**/composer.lock', $rootPath));
if (!empty($composerLockFiles)) {
$this->composerLock = json_decode(yield $this->contentRetriever->retrieve($composerLockFiles[0]));
}
}
if ($this->textDocument === null) {
$this->textDocument = new Server\TextDocument(
$this->documentLoader,
$this->definitionResolver,
$this->client,
$this->globalIndex,
$this->composerJson,
$this->composerLock
);
}
if ($this->workspace === null) {
$this->workspace = new Server\Workspace($projectIndex, $dependenciesIndex, $sourceIndex, $this->composerLock, $this->documentLoader);
}
$serverCapabilities = new ServerCapabilities(); $serverCapabilities = new ServerCapabilities();
// Ask the client to return always full documents (because we need to rebuild the AST from scratch) // Ask the client to return always full documents (because we need to rebuild the AST from scratch)
@ -203,6 +247,10 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
$serverCapabilities->completionProvider = new CompletionOptions; $serverCapabilities->completionProvider = new CompletionOptions;
$serverCapabilities->completionProvider->resolveProvider = false; $serverCapabilities->completionProvider->resolveProvider = false;
$serverCapabilities->completionProvider->triggerCharacters = ['$', '>']; $serverCapabilities->completionProvider->triggerCharacters = ['$', '>'];
// Support global references
$serverCapabilities->xworkspaceReferencesProvider = true;
$serverCapabilities->xdefinitionProvider = true;
$serverCapabilities->xdependenciesProvider = true;
return new InitializeResult($serverCapabilities); return new InitializeResult($serverCapabilities);
}); });

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Protocol;
class DependencyReference
{
/**
* @var mixed
*/
public $hints;
/**
* @var object
*/
public $attributes;
/**
* @param object $attributes
* @param mixed $hints
*/
public function __construct($attributes = null, $hints = null)
{
$this->attributes = $attributes ?? new \stdClass;
$this->hints = $hints;
}
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Protocol;
/**
* Metadata about the symbol that can be used to identify or locate its
* definition.
*/
class ReferenceInformation
{
/**
* The location in the workspace where the `symbol` is referenced.
*
* @var Location
*/
public $reference;
/**
* Metadata about the symbol that can be used to identify or locate its
* definition.
*
* @var SymbolDescriptor
*/
public $symbol;
/**
* @param Location $reference The location in the workspace where the `symbol` is referenced.
* @param SymbolDescriptor $symbol Metadata about the symbol that can be used to identify or locate its definition.
*/
public function __construct(Location $reference = null, SymbolDescriptor $symbol = null)
{
$this->reference = $reference;
$this->symbol = $symbol;
}
}

View File

@ -108,4 +108,25 @@ class ServerCapabilities
* @var bool|null * @var bool|null
*/ */
public $renameProvider; public $renameProvider;
/**
* The server provides workspace references exporting support.
*
* @var bool|null
*/
public $xworkspaceReferencesProvider;
/**
* The server provides extended text document definition support.
*
* @var bool|null
*/
public $xdefinitionProvider;
/**
* The server provides workspace dependencies support.
*
* @var bool|null
*/
public $dependenciesProvider;
} }

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Protocol;
class SymbolDescriptor extends SymbolInformation
{
/**
* The fully qualified structural element name, a globally unique identifier for the symbol.
*
* @var string
*/
public $fqsen;
/**
* A package from the composer.lock file or the contents of the composer.json
* Example: https://github.com/composer/composer/blob/master/composer.lock#L10
* Available fields may differ
*
* @var object|null
*/
public $package;
/**
* @param string $fqsen The fully qualified structural element name, a globally unique identifier for the symbol.
* @param object $package A package from the composer.lock file or the contents of the composer.json
*/
public function __construct(string $fqsen = null, $package = null)
{
$this->fqsen = $fqsen;
$this->package = $package;
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Protocol;
class SymbolLocationInformation
{
/**
* The location where the symbol is defined, if any.
*
* @var Location|null
*/
public $location;
/**
* Metadata about the symbol that can be used to identify or locate its
* definition.
*
* @var SymbolDescriptor
*/
public $symbol;
/**
* @param SymbolDescriptor $symbol The location where the symbol is defined, if any
* @param Location $location Metadata about the symbol that can be used to identify or locate its definition
*/
public function __construct(SymbolDescriptor $symbol = null, Location $location = null)
{
$this->symbol = $symbol;
$this->location = $location;
}
}

View File

@ -8,6 +8,8 @@ use PhpParser\{Node, NodeTraverser};
use LanguageServer\{LanguageClient, PhpDocumentLoader, PhpDocument, DefinitionResolver, CompletionProvider}; use LanguageServer\{LanguageClient, PhpDocumentLoader, PhpDocument, DefinitionResolver, CompletionProvider};
use LanguageServer\NodeVisitor\VariableReferencesCollector; use LanguageServer\NodeVisitor\VariableReferencesCollector;
use LanguageServer\Protocol\{ use LanguageServer\Protocol\{
SymbolLocationInformation,
SymbolDescriptor,
TextDocumentItem, TextDocumentItem,
TextDocumentIdentifier, TextDocumentIdentifier,
VersionedTextDocumentIdentifier, VersionedTextDocumentIdentifier,
@ -39,44 +41,58 @@ class TextDocument
* *
* @var \LanguageServer\LanguageClient * @var \LanguageServer\LanguageClient
*/ */
private $client; protected $client;
/** /**
* @var Project * @var Project
*/ */
private $project; protected $project;
/** /**
* @var PrettyPrinter * @var PrettyPrinter
*/ */
private $prettyPrinter; protected $prettyPrinter;
/** /**
* @var DefinitionResolver * @var DefinitionResolver
*/ */
private $definitionResolver; protected $definitionResolver;
/** /**
* @var CompletionProvider * @var CompletionProvider
*/ */
private $completionProvider; protected $completionProvider;
/** /**
* @var ReadableIndex * @var ReadableIndex
*/ */
private $index; protected $index;
/**
* @var \stdClass|null
*/
protected $composerJson;
/**
* @var \stdClass|null
*/
protected $composerLock;
/** /**
* @param PhpDocumentLoader $documentLoader * @param PhpDocumentLoader $documentLoader
* @param DefinitionResolver $definitionResolver * @param DefinitionResolver $definitionResolver
* @param LanguageClient $client * @param LanguageClient $client
* @param ReadableIndex $index * @param ReadableIndex $index
* @param \stdClass $composerJson
* @param \stdClass $composerLock
*/ */
public function __construct( public function __construct(
PhpDocumentLoader $documentLoader, PhpDocumentLoader $documentLoader,
DefinitionResolver $definitionResolver, DefinitionResolver $definitionResolver,
LanguageClient $client, LanguageClient $client,
ReadableIndex $index ReadableIndex $index,
\stdClass $composerJson = null,
\stdClass $composerLock = null
) { ) {
$this->documentLoader = $documentLoader; $this->documentLoader = $documentLoader;
$this->client = $client; $this->client = $client;
@ -84,6 +100,8 @@ class TextDocument
$this->definitionResolver = $definitionResolver; $this->definitionResolver = $definitionResolver;
$this->completionProvider = new CompletionProvider($this->definitionResolver, $index); $this->completionProvider = new CompletionProvider($this->definitionResolver, $index);
$this->index = $index; $this->index = $index;
$this->composerJson = $composerJson;
$this->composerLock = $composerLock;
} }
/** /**
@ -247,7 +265,14 @@ class TextDocument
if ($node === null) { if ($node === null) {
return []; return [];
} }
$def = $this->definitionResolver->resolveReferenceNodeToDefinition($node); // Handle definition nodes
$fqn = DefinitionResolver::getDefinedFqn($node);
if ($fqn !== null) {
$def = $this->index->getDefinition($fqn);
} else {
// Handle reference nodes
$def = $this->definitionResolver->resolveReferenceNodeToDefinition($node);
}
if ( if (
$def === null $def === null
|| $def->symbolInformation === null || $def->symbolInformation === null
@ -317,4 +342,61 @@ class TextDocument
return $this->completionProvider->provideCompletion($document, $position); return $this->completionProvider->provideCompletion($document, $position);
}); });
} }
/**
* This method is the same as textDocument/definition, except that
*
* The method returns metadata about the definition (the same metadata that workspace/xreferences searches for).
* The concrete location to the definition (location field) is optional. This is useful because the language server
* might not be able to resolve a goto definition request to a concrete location (e.g. due to lack of dependencies)
* but still may know some information about it.
*
* @param TextDocumentIdentifier $textDocument The text document
* @param Position $position The position inside the text document
* @return Promise <SymbolLocationInformation[]>
*/
public function xdefinition(TextDocumentIdentifier $textDocument, Position $position): Promise
{
return coroutine(function () use ($textDocument, $position) {
$document = yield $this->documentLoader->getOrLoad($textDocument->uri);
$node = $document->getNodeAtPosition($position);
if ($node === null) {
return [];
}
// Handle definition nodes
$fqn = DefinitionResolver::getDefinedFqn($node);
if ($fqn !== null) {
$def = $this->index->getDefinition($fqn);
} else {
// Handle reference nodes
$def = $this->definitionResolver->resolveReferenceNodeToDefinition($node);
}
if (
$def === null
|| $def->symbolInformation === null
|| Uri\parse($def->symbolInformation->location->uri)['scheme'] === 'phpstubs'
) {
return [];
}
$symbol = new SymbolDescriptor;
foreach (get_object_vars($def->symbolInformation) as $prop => $val) {
$symbol->$prop = $val;
}
$symbol->fqsen = $def->fqn;
if (preg_match('/\/vendor\/([^\/]+\/[^\/]+)\//', $def->symbolInformation->location->uri, $matches) && $this->composerLock !== null) {
// Definition is inside a dependency
$packageName = $matches[1];
foreach ($this->composerLock->packages as $package) {
if ($package->name === $packageName) {
$symbol->package = $package;
break;
}
}
} else if ($this->composerJson !== null) {
// Definition belongs to a root package
$symbol->package = $this->composerJson;
}
return [new SymbolLocationInformation($symbol, $symbol->location)];
});
}
} }

View File

@ -3,22 +3,17 @@ declare(strict_types = 1);
namespace LanguageServer\Server; namespace LanguageServer\Server;
use LanguageServer\{LanguageClient, Project}; use LanguageServer\{LanguageClient, Project, PhpDocumentLoader};
use LanguageServer\Index\ProjectIndex; use LanguageServer\Index\{ProjectIndex, DependenciesIndex, Index};
use LanguageServer\Protocol\SymbolInformation; use LanguageServer\Protocol\{SymbolInformation, SymbolDescriptor, ReferenceInformation, DependencyReference, Location};
use Sabre\Event\Promise;
use function Sabre\Event\coroutine;
/** /**
* Provides method handlers for all workspace/* methods * Provides method handlers for all workspace/* methods
*/ */
class Workspace class Workspace
{ {
/**
* The lanugage client object to call methods on the client
*
* @var \LanguageServer\LanguageClient
*/
private $client;
/** /**
* The symbol index for the workspace * The symbol index for the workspace
* *
@ -27,12 +22,39 @@ class Workspace
private $index; private $index;
/** /**
* @param ProjectIndex $index Index that is searched on a workspace/symbol request * @var DependenciesIndex
*/ */
public function __construct(ProjectIndex $index, LanguageClient $client) private $dependenciesIndex;
/**
* @var Index
*/
private $sourceIndex;
/**
* @var \stdClass
*/
public $composerLock;
/**
* @var PhpDocumentLoader
*/
public $documentLoader;
/**
* @param ProjectIndex $index Index that is searched on a workspace/symbol request
* @param DependenciesIndex $dependenciesIndex Index that is used on a workspace/xreferences request
* @param DependenciesIndex $sourceIndex Index that is used on a workspace/xreferences request
* @param \stdClass $composerLock The parsed composer.lock of the project, if any
* @param PhpDocumentLoader $documentLoader PhpDocumentLoader instance to load documents
*/
public function __construct(ProjectIndex $index, DependenciesIndex $dependenciesIndex, Index $sourceIndex, \stdClass $composerLock = null, PhpDocumentLoader $documentLoader)
{ {
$this->sourceIndex = $sourceIndex;
$this->index = $index; $this->index = $index;
$this->client = $client; $this->dependenciesIndex = $dependenciesIndex;
$this->composerLock = $composerLock;
$this->documentLoader = $documentLoader;
} }
/** /**
@ -51,4 +73,90 @@ class Workspace
} }
return $symbols; return $symbols;
} }
/**
* The workspace references request is sent from the client to the server to locate project-wide references to a symbol given its description / metadata.
*
* @param SymbolDescriptor $query Partial metadata about the symbol that is being searched for.
* @param string[] $files An optional list of files to restrict the search to.
* @return ReferenceInformation[]
*/
public function xreferences($query, array $files = null): Promise
{
return coroutine(function () use ($query, $files) {
if ($this->composerLock === null) {
return [];
}
/** Map from URI to array of referenced FQNs in dependencies */
$refs = [];
// Get all references TO dependencies
$fqns = isset($query->fqsen) ? [$query->fqsen] : array_values($this->dependenciesIndex->getDefinitions());
foreach ($fqns as $fqn) {
foreach ($this->sourceIndex->getReferenceUris($fqn) as $uri) {
if (!isset($refs[$uri])) {
$refs[$uri] = [];
}
if (array_search($uri, $refs[$uri]) === false) {
$refs[$uri][] = $fqn;
}
}
}
$refInfos = [];
foreach ($refs as $uri => $fqns) {
foreach ($fqns as $fqn) {
$def = $this->dependenciesIndex->getDefinition($fqn);
$symbol = new SymbolDescriptor;
$symbol->fqsen = $fqn;
foreach (get_object_vars($def->symbolInformation) as $prop => $val) {
$symbol->$prop = $val;
}
// Find out package name
preg_match('/\/vendor\/([^\/]+\/[^\/]+)\//', $def->symbolInformation->location->uri, $matches);
$packageName = $matches[1];
foreach ($this->composerLock->packages as $package) {
if ($package->name === $packageName) {
$symbol->package = $package;
break;
}
}
// If there was no FQSEN provided, check if query attributes match
if (!isset($query->fqsen)) {
$matches = true;
foreach (get_object_vars($query) as $prop => $val) {
if ($query->$prop != $symbol->$prop) {
$matches = false;
break;
}
}
if (!$matches) {
continue;
}
}
$doc = yield $this->documentLoader->getOrLoad($uri);
foreach ($doc->getReferenceNodesByFqn($fqn) as $node) {
$refInfo = new ReferenceInformation;
$refInfo->reference = Location::fromNode($node);
$refInfo->symbol = $symbol;
$refInfos[] = $refInfo;
}
}
}
return $refInfos;
});
}
/**
* @return DependencyReference[]
*/
public function xdependencies(): array
{
if ($this->composerLock === null) {
return [];
}
$dependencyReferences = [];
foreach ($this->composerLock->packages as $package) {
$dependencyReferences[] = new DependencyReference($package);
}
return $dependencyReferences;
}
} }

View File

@ -41,6 +41,9 @@ class LanguageServerTest extends TestCase
$serverCapabilities->completionProvider = new CompletionOptions; $serverCapabilities->completionProvider = new CompletionOptions;
$serverCapabilities->completionProvider->resolveProvider = false; $serverCapabilities->completionProvider->resolveProvider = false;
$serverCapabilities->completionProvider->triggerCharacters = ['$', '>']; $serverCapabilities->completionProvider->triggerCharacters = ['$', '>'];
$serverCapabilities->xworkspaceReferencesProvider = true;
$serverCapabilities->xdefinitionProvider = true;
$serverCapabilities->xdependenciesProvider = true;
$this->assertEquals(new InitializeResult($serverCapabilities), $result); $this->assertEquals(new InitializeResult($serverCapabilities), $result);
} }

View File

@ -45,13 +45,15 @@ abstract class ServerTestCase extends TestCase
public function setUp() public function setUp()
{ {
$projectIndex = new ProjectIndex(new Index, new DependenciesIndex); $sourceIndex = new Index;
$dependenciesIndex = new DependenciesIndex;
$projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex);
$definitionResolver = new DefinitionResolver($projectIndex); $definitionResolver = new DefinitionResolver($projectIndex);
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$this->documentLoader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver); $this->documentLoader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver);
$this->textDocument = new Server\TextDocument($this->documentLoader, $definitionResolver, $client, $projectIndex); $this->textDocument = new Server\TextDocument($this->documentLoader, $definitionResolver, $client, $projectIndex);
$this->workspace = new Server\Workspace($projectIndex, $client); $this->workspace = new Server\Workspace($projectIndex, $dependenciesIndex, $sourceIndex, null, $this->documentLoader);
$globalSymbolsUri = pathToUri(realpath(__DIR__ . '/../../fixtures/global_symbols.php')); $globalSymbolsUri = pathToUri(realpath(__DIR__ . '/../../fixtures/global_symbols.php'));
$globalReferencesUri = pathToUri(realpath(__DIR__ . '/../../fixtures/global_references.php')); $globalReferencesUri = pathToUri(realpath(__DIR__ . '/../../fixtures/global_references.php'));