From db6f4f7e5d63435fca018592c9346e466c1ec14b Mon Sep 17 00:00:00 2001 From: Michal Niewrzal Date: Wed, 7 Dec 2016 21:17:55 +0100 Subject: [PATCH 01/10] Don't filter properties on typed prefix (#207) --- src/CompletionProvider.php | 43 ++++++++------------ tests/Server/TextDocument/CompletionTest.php | 32 ++++++++++++++- 2 files changed, 49 insertions(+), 26 deletions(-) diff --git a/src/CompletionProvider.php b/src/CompletionProvider.php index 261bc1d..5d65ea3 100644 --- a/src/CompletionProvider.php +++ b/src/CompletionProvider.php @@ -4,12 +4,10 @@ declare(strict_types = 1); namespace LanguageServer; use PhpParser\Node; -use phpDocumentor\Reflection\Types; use LanguageServer\Protocol\{ TextEdit, Range, Position, - SymbolKind, CompletionList, CompletionItem, CompletionItemKind @@ -134,30 +132,25 @@ class CompletionProvider || $node instanceof Node\Expr\StaticPropertyFetch || $node instanceof Node\Expr\ClassConstFetch ) { - if (!is_string($node->name)) { - // If the name is an Error node, just filter by the class - if ($node instanceof Node\Expr\MethodCall || $node instanceof Node\Expr\PropertyFetch) { - // For instances, resolve the variable type - $prefixes = DefinitionResolver::getFqnsFromType( - $this->definitionResolver->resolveExpressionNodeToType($node->var) - ); - } else { - $prefixes = [$node->class instanceof Node\Name ? (string)$node->class : '']; - } - // If we are just filtering by the class, add the appropiate operator to the prefix - // to filter the type of symbol - foreach ($prefixes as &$prefix) { - if ($node instanceof Node\Expr\MethodCall || $node instanceof Node\Expr\PropertyFetch) { - $prefix .= '->'; - } else if ($node instanceof Node\Expr\StaticCall || $node instanceof Node\Expr\ClassConstFetch) { - $prefix .= '::'; - } else if ($node instanceof Node\Expr\StaticPropertyFetch) { - $prefix .= '::$'; - } - } + // If the name is an Error node, just filter by the class + if ($node instanceof Node\Expr\MethodCall || $node instanceof Node\Expr\PropertyFetch) { + // For instances, resolve the variable type + $prefixes = DefinitionResolver::getFqnsFromType( + $this->definitionResolver->resolveExpressionNodeToType($node->var) + ); } else { - $fqn = $this->definitionResolver->resolveReferenceNodeToFqn($node); - $prefixes = $fqn !== null ? [$fqn] : []; + $prefixes = [$node->class instanceof Node\Name ? (string)$node->class : '']; + } + // If we are just filtering by the class, add the appropiate operator to the prefix + // to filter the type of symbol + foreach ($prefixes as &$prefix) { + if ($node instanceof Node\Expr\MethodCall || $node instanceof Node\Expr\PropertyFetch) { + $prefix .= '->'; + } else if ($node instanceof Node\Expr\StaticCall || $node instanceof Node\Expr\ClassConstFetch) { + $prefix .= '::'; + } else if ($node instanceof Node\Expr\StaticPropertyFetch) { + $prefix .= '::$'; + } } foreach ($this->project->getDefinitions() as $fqn => $def) { diff --git a/tests/Server/TextDocument/CompletionTest.php b/tests/Server/TextDocument/CompletionTest.php index 7b5dd0a..f32503c 100644 --- a/tests/Server/TextDocument/CompletionTest.php +++ b/tests/Server/TextDocument/CompletionTest.php @@ -254,10 +254,25 @@ class CompletionTest extends TestCase new Position(2, 13) )->wait(); $this->assertEquals(new CompletionList([ + new CompletionItem( + 'TEST_CLASS_CONST', + CompletionItemKind::VARIABLE, + 'int', + 'Anim labore veniam consectetur laboris minim quis aute aute esse nulla ad.' + ), + new CompletionItem( + 'staticTestProperty', + CompletionItemKind::PROPERTY, + '\TestClass[]', + 'Lorem excepteur officia sit anim velit veniam enim.', + null, + null, + '$staticTestProperty' + ), new CompletionItem( 'staticTestMethod', CompletionItemKind::METHOD, - 'mixed', // Method return type + 'mixed', 'Do magna consequat veniam minim proident eiusmod incididunt aute proident.' ) ], true), $items); @@ -277,6 +292,21 @@ class CompletionTest extends TestCase CompletionItemKind::VARIABLE, 'int', 'Anim labore veniam consectetur laboris minim quis aute aute esse nulla ad.' + ), + new CompletionItem( + 'staticTestProperty', + CompletionItemKind::PROPERTY, + '\TestClass[]', + 'Lorem excepteur officia sit anim velit veniam enim.', + null, + null, + '$staticTestProperty' + ), + new CompletionItem( + 'staticTestMethod', + CompletionItemKind::METHOD, + 'mixed', + 'Do magna consequat veniam minim proident eiusmod incididunt aute proident.' ) ], true), $items); } From ebd1cc613304cc35fd6896320d196064eca6d40d Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Thu, 8 Dec 2016 01:51:32 +0100 Subject: [PATCH 02/10] Refactor content retrieval (#208) --- .../ClientContentRetriever.php | 36 +++++++++++++++++++ src/ContentRetriever/ContentRetriever.php | 20 +++++++++++ .../FileSystemContentRetriever.php | 24 +++++++++++++ src/Project.php | 30 +++++++++------- 4 files changed, 97 insertions(+), 13 deletions(-) create mode 100644 src/ContentRetriever/ClientContentRetriever.php create mode 100644 src/ContentRetriever/ContentRetriever.php create mode 100644 src/ContentRetriever/FileSystemContentRetriever.php diff --git a/src/ContentRetriever/ClientContentRetriever.php b/src/ContentRetriever/ClientContentRetriever.php new file mode 100644 index 0000000..b88042c --- /dev/null +++ b/src/ContentRetriever/ClientContentRetriever.php @@ -0,0 +1,36 @@ +client = $client; + } + + /** + * Retrieves the content of a text document identified by the URI through a textDocument/xcontent request + * + * @param string $uri The URI of the document + * @return Promise Resolved with the content as a string + */ + public function retrieve(string $uri): Promise + { + return $this->client->textDocument->xcontent(new TextDocumentIdentifier($uri)) + ->then(function (TextDocumentItem $textDocument) { + return $textDocument->text; + }); + } +} diff --git a/src/ContentRetriever/ContentRetriever.php b/src/ContentRetriever/ContentRetriever.php new file mode 100644 index 0000000..4d16b98 --- /dev/null +++ b/src/ContentRetriever/ContentRetriever.php @@ -0,0 +1,20 @@ + Resolved with the content as a string + */ + public function retrieve(string $uri): Promise; +} diff --git a/src/ContentRetriever/FileSystemContentRetriever.php b/src/ContentRetriever/FileSystemContentRetriever.php new file mode 100644 index 0000000..82e7002 --- /dev/null +++ b/src/ContentRetriever/FileSystemContentRetriever.php @@ -0,0 +1,24 @@ + Resolved with the content as a string + */ + public function retrieve(string $uri): Promise + { + return Promise\resolve(file_get_contents(uriToPath($uri))); + } +} diff --git a/src/Project.php b/src/Project.php index 3021a1c..b62b295 100644 --- a/src/Project.php +++ b/src/Project.php @@ -5,6 +5,7 @@ namespace LanguageServer; use LanguageServer\Protocol\{SymbolInformation, TextDocumentIdentifier, ClientCapabilities}; use phpDocumentor\Reflection\DocBlockFactory; +use LanguageServer\ContentRetriever\{ContentRetriever, ClientContentRetriever, FileSystemContentRetriever}; use Sabre\Event\Promise; use function Sabre\Event\coroutine; @@ -67,6 +68,13 @@ class Project */ private $clientCapabilities; + /** + * The content retriever + * + * @var ContentRetriever + */ + private $contentRetriever; + public function __construct(LanguageClient $client, ClientCapabilities $clientCapabilities) { $this->client = $client; @@ -74,6 +82,11 @@ class Project $this->parser = new Parser; $this->docBlockFactory = DocBlockFactory::createInstance(); $this->definitionResolver = new DefinitionResolver($this); + if ($clientCapabilities->xcontentProvider) { + $this->contentRetriever = new ClientContentRetriever($client); + } else { + $this->contentRetriever = new FileSystemContentRetriever; + } } /** @@ -112,19 +125,10 @@ class Project { return coroutine(function () use ($uri) { $limit = 150000; - if ($this->clientCapabilities->xcontentProvider) { - $content = (yield $this->client->textDocument->xcontent(new TextDocumentIdentifier($uri)))->text; - $size = strlen($content); - if ($size > $limit) { - throw new ContentTooLargeException($uri, $size, $limit); - } - } else { - $path = uriToPath($uri); - $size = filesize($path); - if ($size > $limit) { - throw new ContentTooLargeException($uri, $size, $limit); - } - $content = file_get_contents($path); + $content = yield $this->contentRetriever->retrieve($uri); + $size = strlen($content); + if ($size > $limit) { + throw new ContentTooLargeException($uri, $size, $limit); } if (isset($this->documents[$uri])) { $document = $this->documents[$uri]; From b9f9871156642afaf42f75d8e2637a8d80c19484 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Thu, 8 Dec 2016 02:33:48 +0100 Subject: [PATCH 03/10] Files finder (#209) --- src/FilesFinder/ClientFilesFinder.php | 49 ++++++++++++ src/FilesFinder/FileSystemFilesFinder.php | 31 +++++++ src/FilesFinder/FilesFinder.php | 21 +++++ src/LanguageServer.php | 80 ++++++++----------- src/Project.php | 18 +---- tests/NodeVisitor/DefinitionCollectorTest.php | 5 +- tests/PhpDocumentTest.php | 3 +- tests/ProjectTest.php | 3 +- tests/Server/ServerTestCase.php | 3 +- tests/Server/TextDocument/CompletionTest.php | 3 +- .../Definition/GlobalFallbackTest.php | 3 +- tests/Server/TextDocument/DidChangeTest.php | 3 +- tests/Server/TextDocument/DidCloseTest.php | 3 +- tests/Server/TextDocument/FormattingTest.php | 5 +- tests/Server/TextDocument/ParseErrorsTest.php | 3 +- .../References/GlobalFallbackTest.php | 3 +- 16 files changed, 162 insertions(+), 74 deletions(-) create mode 100644 src/FilesFinder/ClientFilesFinder.php create mode 100644 src/FilesFinder/FileSystemFilesFinder.php create mode 100644 src/FilesFinder/FilesFinder.php diff --git a/src/FilesFinder/ClientFilesFinder.php b/src/FilesFinder/ClientFilesFinder.php new file mode 100644 index 0000000..4315ede --- /dev/null +++ b/src/FilesFinder/ClientFilesFinder.php @@ -0,0 +1,49 @@ +client = $client; + } + + /** + * Returns all files in the workspace that match a glob. + * If the client does not support workspace/files, it falls back to searching the file system directly. + * + * @param string $glob + * @return Promise The URIs + */ + public function find(string $glob): Promise + { + return $this->client->workspace->xfiles()->then(function (array $textDocuments) use ($glob) { + $uris = []; + foreach ($textDocuments as $textDocument) { + $path = Uri\parse($textDocument->uri)['path']; + if (Glob::match($path, $glob)) { + $uris[] = $textDocument->uri; + } + } + return $uris; + }); + } +} diff --git a/src/FilesFinder/FileSystemFilesFinder.php b/src/FilesFinder/FileSystemFilesFinder.php new file mode 100644 index 0000000..52df4b6 --- /dev/null +++ b/src/FilesFinder/FileSystemFilesFinder.php @@ -0,0 +1,31 @@ + + */ + public function find(string $glob): Promise + { + return coroutine(function () use ($glob) { + $uris = []; + foreach (new GlobIterator($glob) as $path) { + $uris[] = pathToUri($path); + yield timeout(); + } + return $uris; + }); + } +} diff --git a/src/FilesFinder/FilesFinder.php b/src/FilesFinder/FilesFinder.php new file mode 100644 index 0000000..81d6de5 --- /dev/null +++ b/src/FilesFinder/FilesFinder.php @@ -0,0 +1,21 @@ + + */ + public function find(string $glob): Promise; +} diff --git a/src/LanguageServer.php b/src/LanguageServer.php index 42a1b64..6cda587 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -14,13 +14,13 @@ use LanguageServer\Protocol\{ TextDocumentIdentifier, CompletionOptions }; +use LanguageServer\FilesFinder\{FilesFinder, ClientFilesFinder, FileSystemFilesFinder}; +use LanguageServer\ContentRetriever\{ContentRetriever, ClientContentRetriever, FileSystemContentRetriever}; use AdvancedJsonRpc; use Sabre\Event\{Loop, Promise}; use function Sabre\Event\coroutine; use Exception; use Throwable; -use Webmozart\Glob\Iterator\GlobIterator; -use Webmozart\Glob\Glob; use Webmozart\PathUtil\Path; use Sabre\Uri; @@ -45,11 +45,6 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher public $completionItem; public $codeLens; - /** - * ClientCapabilities - */ - private $clientCapabilities; - private $protocolReader; private $protocolWriter; private $client; @@ -62,6 +57,16 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher private $rootPath; private $project; + /** + * @var FilesFinder + */ + private $filesFinder; + + /** + * @var ContentRetriever + */ + private $contentRetrieverFinder; + public function __construct(ProtocolReader $reader, ProtocolWriter $writer) { parent::__construct($this, '/'); @@ -120,8 +125,20 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher public function initialize(ClientCapabilities $capabilities, string $rootPath = null, int $processId = null): InitializeResult { $this->rootPath = $rootPath; - $this->clientCapabilities = $capabilities; - $this->project = new Project($this->client, $capabilities); + + if ($capabilities->xfilesProvider) { + $this->filesFinder = new ClientFilesFinder($this->client); + } else { + $this->filesFinder = new FileSystemFilesFinder; + } + + if ($capabilities->xcontentProvider) { + $this->contentRetriever = new ClientContentRetriever($this->client); + } else { + $this->contentRetriever = new FileSystemContentRetriever; + } + + $this->project = new Project($this->client, $this->contentRetriever); $this->textDocument = new Server\TextDocument($this->project, $this->client); $this->workspace = new Server\Workspace($this->project, $this->client); @@ -183,29 +200,30 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher private function indexProject(): Promise { return coroutine(function () { - $textDocuments = yield $this->findPhpFiles(); - $count = count($textDocuments); + $pattern = Path::makeAbsolute('**/*.php', $this->rootPath); + $uris = yield $this->filesFinder->find($pattern); + $count = count($uris); $startTime = microtime(true); - foreach ($textDocuments as $i => $textDocument) { + foreach ($uris as $i => $uri) { // Give LS to the chance to handle requests while indexing yield timeout(); $this->client->window->logMessage( MessageType::LOG, - "Parsing file $i/$count: {$textDocument->uri}" + "Parsing file $i/$count: {$uri}" ); try { - yield $this->project->loadDocument($textDocument->uri); + yield $this->project->loadDocument($uri); } catch (ContentTooLargeException $e) { $this->client->window->logMessage( MessageType::INFO, - "Ignoring file {$textDocument->uri} because it exceeds size limit of {$e->limit} bytes ({$e->size})" + "Ignoring file {$uri} because it exceeds size limit of {$e->limit} bytes ({$e->size})" ); } catch (Exception $e) { $this->client->window->logMessage( MessageType::ERROR, - "Error parsing file {$textDocument->uri}: " . (string)$e + "Error parsing file {$uri}: " . (string)$e ); } } @@ -218,34 +236,4 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher ); }); } - - /** - * Returns all PHP files in the workspace. - * If the client does not support workspace/files, it falls back to searching the file system directly. - * - * @return Promise - */ - private function findPhpFiles(): Promise - { - return coroutine(function () { - $textDocuments = []; - $pattern = Path::makeAbsolute('**/*.php', $this->rootPath); - if ($this->clientCapabilities->xfilesProvider) { - // Use xfiles request - foreach (yield $this->client->workspace->xfiles() as $textDocument) { - $path = Uri\parse($textDocument->uri)['path']; - if (Glob::match($path, $pattern)) { - $textDocuments[] = $textDocument; - } - } - } else { - // Use the file system - foreach (new GlobIterator($pattern) as $path) { - $textDocuments[] = new TextDocumentIdentifier(pathToUri($path)); - yield timeout(); - } - } - return $textDocuments; - }); - } } diff --git a/src/Project.php b/src/Project.php index b62b295..0582301 100644 --- a/src/Project.php +++ b/src/Project.php @@ -5,7 +5,7 @@ namespace LanguageServer; use LanguageServer\Protocol\{SymbolInformation, TextDocumentIdentifier, ClientCapabilities}; use phpDocumentor\Reflection\DocBlockFactory; -use LanguageServer\ContentRetriever\{ContentRetriever, ClientContentRetriever, FileSystemContentRetriever}; +use LanguageServer\ContentRetriever\ContentRetriever; use Sabre\Event\Promise; use function Sabre\Event\coroutine; @@ -61,13 +61,6 @@ class Project */ private $client; - /** - * The client's capabilities - * - * @var ClientCapabilities - */ - private $clientCapabilities; - /** * The content retriever * @@ -75,18 +68,13 @@ class Project */ private $contentRetriever; - public function __construct(LanguageClient $client, ClientCapabilities $clientCapabilities) + public function __construct(LanguageClient $client, ContentRetriever $contentRetriever) { $this->client = $client; - $this->clientCapabilities = $clientCapabilities; $this->parser = new Parser; $this->docBlockFactory = DocBlockFactory::createInstance(); $this->definitionResolver = new DefinitionResolver($this); - if ($clientCapabilities->xcontentProvider) { - $this->contentRetriever = new ClientContentRetriever($client); - } else { - $this->contentRetriever = new FileSystemContentRetriever; - } + $this->contentRetriever = $contentRetriever; } /** diff --git a/tests/NodeVisitor/DefinitionCollectorTest.php b/tests/NodeVisitor/DefinitionCollectorTest.php index 74e0d5c..800373d 100644 --- a/tests/NodeVisitor/DefinitionCollectorTest.php +++ b/tests/NodeVisitor/DefinitionCollectorTest.php @@ -7,6 +7,7 @@ use PHPUnit\Framework\TestCase; use PhpParser\{NodeTraverser, Node}; use PhpParser\NodeVisitor\NameResolver; use LanguageServer\{LanguageClient, Project, PhpDocument, Parser, DefinitionResolver}; +use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\Protocol\ClientCapabilities; use LanguageServer\Tests\MockProtocolStream; use LanguageServer\NodeVisitor\{ReferencesAdder, DefinitionCollector}; @@ -17,7 +18,7 @@ class DefinitionCollectorTest extends TestCase public function testCollectsSymbols() { $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $project = new Project($client, new ClientCapabilities); + $project = new Project($client, new FileSystemContentRetriever); $parser = new Parser; $uri = pathToUri(realpath(__DIR__ . '/../../fixtures/symbols.php')); $document = $project->loadDocument($uri)->wait(); @@ -57,7 +58,7 @@ class DefinitionCollectorTest extends TestCase public function testDoesNotCollectReferences() { $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $project = new Project($client, new ClientCapabilities); + $project = new Project($client, new FileSystemContentRetriever); $parser = new Parser; $uri = pathToUri(realpath(__DIR__ . '/../../fixtures/references.php')); $document = $project->loadDocument($uri)->wait(); diff --git a/tests/PhpDocumentTest.php b/tests/PhpDocumentTest.php index 057551e..a4e8ffa 100644 --- a/tests/PhpDocumentTest.php +++ b/tests/PhpDocumentTest.php @@ -7,6 +7,7 @@ use PHPUnit\Framework\TestCase; use LanguageServer\Tests\MockProtocolStream; use LanguageServer\{LanguageClient, Project}; use LanguageServer\NodeVisitor\NodeAtPositionFinder; +use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\Protocol\{SymbolKind, Position, ClientCapabilities}; use PhpParser\Node; @@ -20,7 +21,7 @@ class PhpDocumentTest extends TestCase public function setUp() { $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $this->project = new Project($client, new ClientCapabilities); + $this->project = new Project($client, new FileSystemContentRetriever); } public function testParsesVariableVariables() diff --git a/tests/ProjectTest.php b/tests/ProjectTest.php index 161370c..6fef176 100644 --- a/tests/ProjectTest.php +++ b/tests/ProjectTest.php @@ -6,6 +6,7 @@ namespace LanguageServer\Tests\Server; use PHPUnit\Framework\TestCase; use LanguageServer\Tests\MockProtocolStream; use LanguageServer\{Server, Client, LanguageClient, Project, PhpDocument}; +use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\Protocol\{ TextDocumentItem, TextDocumentIdentifier, @@ -27,7 +28,7 @@ class ProjectTest extends TestCase public function setUp() { $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $this->project = new Project($client, new ClientCapabilities); + $this->project = new Project($client, new FileSystemContentRetriever); } public function testGetOrLoadDocumentLoadsDocument() diff --git a/tests/Server/ServerTestCase.php b/tests/Server/ServerTestCase.php index 23d1763..38e2c8a 100644 --- a/tests/Server/ServerTestCase.php +++ b/tests/Server/ServerTestCase.php @@ -6,6 +6,7 @@ namespace LanguageServer\Tests\Server; use PHPUnit\Framework\TestCase; use LanguageServer\Tests\MockProtocolStream; use LanguageServer\{Server, LanguageClient, Project}; +use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\Protocol\{Position, Location, Range, ClientCapabilities}; use function LanguageServer\pathToUri; use Sabre\Event\Promise; @@ -44,7 +45,7 @@ abstract class ServerTestCase extends TestCase public function setUp() { $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $this->project = new Project($client, new ClientCapabilities); + $this->project = new Project($client, new FileSystemContentRetriever); $this->textDocument = new Server\TextDocument($this->project, $client); $this->workspace = new Server\Workspace($this->project, $client); diff --git a/tests/Server/TextDocument/CompletionTest.php b/tests/Server/TextDocument/CompletionTest.php index f32503c..587f171 100644 --- a/tests/Server/TextDocument/CompletionTest.php +++ b/tests/Server/TextDocument/CompletionTest.php @@ -6,6 +6,7 @@ namespace LanguageServer\Tests\Server\TextDocument; use PHPUnit\Framework\TestCase; use LanguageServer\Tests\MockProtocolStream; use LanguageServer\{Server, LanguageClient, Project, CompletionProvider}; +use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\Protocol\{ TextDocumentIdentifier, TextEdit, @@ -33,7 +34,7 @@ class CompletionTest extends TestCase public function setUp() { $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $this->project = new Project($client, new ClientCapabilities); + $this->project = new Project($client, new FileSystemContentRetriever); $this->project->loadDocument(pathToUri(__DIR__ . '/../../../fixtures/global_symbols.php'))->wait(); $this->project->loadDocument(pathToUri(__DIR__ . '/../../../fixtures/symbols.php'))->wait(); $this->textDocument = new Server\TextDocument($this->project, $client); diff --git a/tests/Server/TextDocument/Definition/GlobalFallbackTest.php b/tests/Server/TextDocument/Definition/GlobalFallbackTest.php index c4b021d..20ea70b 100644 --- a/tests/Server/TextDocument/Definition/GlobalFallbackTest.php +++ b/tests/Server/TextDocument/Definition/GlobalFallbackTest.php @@ -6,6 +6,7 @@ namespace LanguageServer\Tests\Server\TextDocument\Definition; use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\Server\ServerTestCase; use LanguageServer\{Server, LanguageClient, Project}; +use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\Protocol\{TextDocumentIdentifier, Position, Range, Location, ClientCapabilities}; use Sabre\Event\Promise; @@ -14,7 +15,7 @@ class GlobalFallbackTest extends ServerTestCase public function setUp() { $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $project = new Project($client, new ClientCapabilities); + $project = new Project($client, new FileSystemContentRetriever); $this->textDocument = new Server\TextDocument($project, $client); $project->openDocument('global_fallback', file_get_contents(__DIR__ . '/../../../../fixtures/global_fallback.php')); $project->openDocument('global_symbols', file_get_contents(__DIR__ . '/../../../../fixtures/global_symbols.php')); diff --git a/tests/Server/TextDocument/DidChangeTest.php b/tests/Server/TextDocument/DidChangeTest.php index 1df0505..9df301a 100644 --- a/tests/Server/TextDocument/DidChangeTest.php +++ b/tests/Server/TextDocument/DidChangeTest.php @@ -6,6 +6,7 @@ namespace LanguageServer\Tests\Server\TextDocument; use PHPUnit\Framework\TestCase; use LanguageServer\Tests\MockProtocolStream; use LanguageServer\{Server, Client, LanguageClient, Project}; +use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\Protocol\{ TextDocumentIdentifier, TextDocumentItem, @@ -21,7 +22,7 @@ class DidChangeTest extends TestCase public function test() { $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $project = new Project($client, new ClientCapabilities); + $project = new Project($client, new FileSystemContentRetriever); $textDocument = new Server\TextDocument($project, $client); $phpDocument = $project->openDocument('whatever', "openDocument('whatever', 'hello world'); diff --git a/tests/Server/TextDocument/FormattingTest.php b/tests/Server/TextDocument/FormattingTest.php index b7d0609..32aee22 100644 --- a/tests/Server/TextDocument/FormattingTest.php +++ b/tests/Server/TextDocument/FormattingTest.php @@ -6,6 +6,7 @@ namespace LanguageServer\Tests\Server\TextDocument; use PHPUnit\Framework\TestCase; use LanguageServer\Tests\MockProtocolStream; use LanguageServer\{Server, Client, LanguageClient, Project}; +use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\Protocol\{ TextDocumentIdentifier, TextDocumentItem, @@ -27,14 +28,14 @@ class FormattingTest extends TestCase public function setUp() { $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $project = new Project($client, new ClientCapabilities); + $project = new Project($client, new FileSystemContentRetriever); $this->textDocument = new Server\TextDocument($project, $client); } public function testFormatting() { $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $project = new Project($client, new ClientCapabilities); + $project = new Project($client, new FileSystemContentRetriever); $textDocument = new Server\TextDocument($project, $client); $path = realpath(__DIR__ . '/../../../fixtures/format.php'); $uri = pathToUri($path); diff --git a/tests/Server/TextDocument/ParseErrorsTest.php b/tests/Server/TextDocument/ParseErrorsTest.php index 2a02efe..af2bdd8 100644 --- a/tests/Server/TextDocument/ParseErrorsTest.php +++ b/tests/Server/TextDocument/ParseErrorsTest.php @@ -6,6 +6,7 @@ namespace LanguageServer\Tests\Server\TextDocument; use PHPUnit\Framework\TestCase; use LanguageServer\Tests\MockProtocolStream; use LanguageServer\{Server, Client, LanguageClient, Project, ClientHandler}; +use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\Protocol\{TextDocumentIdentifier, TextDocumentItem, DiagnosticSeverity, ClientCapabilities}; use Sabre\Event\Promise; use JsonMapper; @@ -35,7 +36,7 @@ class ParseErrorsTest extends TestCase return Promise\resolve(null); } }; - $project = new Project($client, new ClientCapabilities); + $project = new Project($client, new FileSystemContentRetriever); $this->textDocument = new Server\TextDocument($project, $client); } diff --git a/tests/Server/TextDocument/References/GlobalFallbackTest.php b/tests/Server/TextDocument/References/GlobalFallbackTest.php index 9f68cb9..4e6d07a 100644 --- a/tests/Server/TextDocument/References/GlobalFallbackTest.php +++ b/tests/Server/TextDocument/References/GlobalFallbackTest.php @@ -6,6 +6,7 @@ namespace LanguageServer\Tests\Server\TextDocument\References; use PHPUnit\Framework\TestCase; use LanguageServer\Tests\MockProtocolStream; use LanguageServer\{Server, LanguageClient, Project}; +use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\Protocol\{TextDocumentIdentifier, Position, ReferenceContext, Location, Range, ClientCapabilities}; use LanguageServer\Tests\Server\ServerTestCase; @@ -14,7 +15,7 @@ class GlobalFallbackTest extends ServerTestCase public function setUp() { $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $project = new Project($client, new ClientCapabilities); + $project = new Project($client, new FileSystemContentRetriever); $this->textDocument = new Server\TextDocument($project, $client); $project->openDocument('global_fallback', file_get_contents(__DIR__ . '/../../../../fixtures/global_fallback.php')); $project->openDocument('global_symbols', file_get_contents(__DIR__ . '/../../../../fixtures/global_symbols.php')); From a7d77d844e5860ad4072b5402cbe78da314121ce Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Tue, 13 Dec 2016 01:51:02 +0100 Subject: [PATCH 04/10] =?UTF-8?q?Add=C2=A0Index=20classes=20and=20stubs=20?= =?UTF-8?q?(#214)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + .travis.yml | 3 +- README.md | 12 + composer.json | 24 +- src/CompletionProvider.php | 24 +- src/ComposerScripts.php | 57 +++ src/DefinitionResolver.php | 26 +- src/Index/AbstractAggregateIndex.php | 66 ++++ src/Index/DependenciesIndex.php | 52 +++ src/Index/GlobalIndex.php | 38 ++ src/Index/Index.php | 129 +++++++ src/Index/ProjectIndex.php | 51 +++ src/Index/ReadableIndex.php | 37 ++ src/Index/StubsIndex.php | 27 ++ src/LanguageServer.php | 152 +++++--- src/PhpDocument.php | 133 ++----- src/PhpDocumentLoader.php | 178 +++++++++ src/Project.php | 349 ------------------ src/Server/TextDocument.php | 108 +++++- src/Server/Workspace.php | 16 +- tests/LanguageServerTest.php | 62 ++-- tests/NodeVisitor/DefinitionCollectorTest.php | 40 +- ...jectTest.php => PhpDocumentLoaderTest.php} | 27 +- tests/PhpDocumentTest.php | 32 +- tests/Server/ServerTestCase.php | 28 +- tests/Server/TextDocument/CompletionTest.php | 48 +-- .../Definition/GlobalFallbackTest.php | 14 +- tests/Server/TextDocument/DidChangeTest.php | 11 +- tests/Server/TextDocument/DidCloseTest.php | 13 +- tests/Server/TextDocument/FormattingTest.php | 22 +- tests/Server/TextDocument/ParseErrorsTest.php | 9 +- .../References/GlobalFallbackTest.php | 15 +- 32 files changed, 1109 insertions(+), 695 deletions(-) create mode 100644 src/ComposerScripts.php create mode 100644 src/Index/AbstractAggregateIndex.php create mode 100644 src/Index/DependenciesIndex.php create mode 100644 src/Index/GlobalIndex.php create mode 100644 src/Index/Index.php create mode 100644 src/Index/ProjectIndex.php create mode 100644 src/Index/ReadableIndex.php create mode 100644 src/Index/StubsIndex.php create mode 100644 src/PhpDocumentLoader.php delete mode 100644 src/Project.php rename tests/{ProjectTest.php => PhpDocumentLoaderTest.php} (50%) diff --git a/.gitignore b/.gitignore index c018fa6..4791ea2 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ vendor/ .phpls/ composer.lock +stubs diff --git a/.travis.yml b/.travis.yml index 0199d52..7fb9c9a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,10 +8,11 @@ services: cache: directories: - - vendor + - $HOME/.composer/cache install: - composer install + - composer run-script parse-stubs script: - vendor/bin/phpcs -n diff --git a/README.md b/README.md index de9167b..9f50476 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,10 @@ Non-Standard: An empty query will return _all_ symbols found in the workspace. PHP parse errors are reported as errors, parse errors of docblocks are reported as warnings. Errors/Warnings from the `vendor` directory are ignored. +### Stubs for PHP built-ins + +Completion, type resolval etc. will use the standard PHP library and common extensions. + ### What is considered a definition? Globally searchable definitions are: @@ -131,6 +135,11 @@ Simply run and you will get the latest stable release and all dependencies. Running `composer update` will update the server to the latest non-breaking version. +After installing the language server and its dependencies, +you must parse the stubs for standard PHP symbols and save the index for fast initialization. + + composer run-script --working-dir=vendor/felixfbecker/language-server parse-stubs + ## Running Start the language server with @@ -178,6 +187,9 @@ Clone the repository and run composer install to install dependencies. +Then parse the stubs with + + composer run-script parse-stubs Run the tests with diff --git a/composer.json b/composer.json index 224d3d0..898da3b 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,9 @@ "refactor" ], "bin": ["bin/php-language-server.php"], + "scripts": { + "parse-stubs": "LanguageServer\\ComposerScripts::parseStubs" + }, "require": { "php": ">=7.0", "nikic/php-parser": "dev-master#e52ffc4447e034514339a03b450aab9cd625e37c", @@ -32,8 +35,27 @@ "netresearch/jsonmapper": "^1.0", "webmozart/path-util": "^2.3", "webmozart/glob": "^4.1", - "sabre/uri": "^2.0" + "sabre/uri": "^2.0", + "JetBrains/phpstorm-stubs": "dev-master" }, + "repositories": [ + { + "type": "package", + "package": { + "name": "JetBrains/phpstorm-stubs", + "version": "dev-master", + "dist": { + "url": "https://github.com/JetBrains/phpstorm-stubs/archive/master.zip", + "type": "zip" + }, + "source": { + "url": "https://github.com/JetBrains/phpstorm-stubs", + "type": "git", + "reference": "master" + } + } + } + ], "minimum-stability": "dev", "prefer-stable": true, "autoload": { diff --git a/src/CompletionProvider.php b/src/CompletionProvider.php index 5d65ea3..fb9ebb8 100644 --- a/src/CompletionProvider.php +++ b/src/CompletionProvider.php @@ -4,6 +4,7 @@ declare(strict_types = 1); namespace LanguageServer; use PhpParser\Node; +use LanguageServer\Index\ReadableIndex; use LanguageServer\Protocol\{ TextEdit, Range, @@ -97,13 +98,18 @@ class CompletionProvider private $project; /** - * @param DefinitionResolver $definitionResolver - * @param Project $project + * @var ReadableIndex */ - public function __construct(DefinitionResolver $definitionResolver, Project $project) + private $index; + + /** + * @param DefinitionResolver $definitionResolver + * @param ReadableIndex $index + */ + public function __construct(DefinitionResolver $definitionResolver, ReadableIndex $index) { $this->definitionResolver = $definitionResolver; - $this->project = $project; + $this->index = $index; } /** @@ -153,7 +159,7 @@ class CompletionProvider } } - foreach ($this->project->getDefinitions() as $fqn => $def) { + foreach ($this->index->getDefinitions() as $fqn => $def) { foreach ($prefixes as $prefix) { if (substr($fqn, 0, strlen($prefix)) === $prefix && !$def->isGlobal) { $list->items[] = CompletionItem::fromDefinition($def); @@ -185,7 +191,9 @@ class CompletionProvider // Get the definition for the used namespace, class-like, function or constant // And save it under the alias $fqn = (string)Node\Name::concat($stmt->prefix ?? null, $use->name); - $aliasedDefs[$use->alias] = $this->project->getDefinition($fqn); + if ($def = $this->index->getDefinition($fqn)) { + $aliasedDefs[$use->alias] = $def; + } } } else { // Use statements are always the first statements in a namespace @@ -206,7 +214,7 @@ class CompletionProvider // Additionally, suggest global symbols that either // - start with the current namespace + prefix, if the Name node is not fully qualified // - start with just the prefix, if the Name node is fully qualified - foreach ($this->project->getDefinitions() as $fqn => $def) { + foreach ($this->index->getDefinitions() as $fqn => $def) { if ( $def->isGlobal // exclude methods, properties etc. && ( @@ -326,7 +334,7 @@ class CompletionProvider } if ($level instanceof Node\Expr\Closure) { foreach ($level->uses as $use) { - if (!isset($vars[$param->name]) && substr($param->name, 0, strlen($namePrefix)) === $namePrefix) { + if (!isset($vars[$use->var]) && substr($use->var, 0, strlen($namePrefix)) === $namePrefix) { $vars[$use->var] = $use; } } diff --git a/src/ComposerScripts.php b/src/ComposerScripts.php new file mode 100644 index 0000000..67060bc --- /dev/null +++ b/src/ComposerScripts.php @@ -0,0 +1,57 @@ +find("$stubsLocation/**/*.php"); + + foreach ($uris as $uri) { + echo "Parsing $uri\n"; + $content = yield $contentRetriever->retrieve($uri); + + // Change URI to phpstubs:// + $parts = Uri\parse($uri); + $parts['path'] = Path::makeRelative($parts['path'], $stubsLocation); + $parts['scheme'] = 'phpstubs'; + $uri = Uri\build($parts); + + $document = new PhpDocument($uri, $content, $index, $parser, $docBlockFactory, $definitionResolver); + } + + echo "Saving Index\n"; + + $index->save(); + + echo "Finished\n"; + })->wait(); + } +} diff --git a/src/DefinitionResolver.php b/src/DefinitionResolver.php index 87c5aa0..a2f30e4 100644 --- a/src/DefinitionResolver.php +++ b/src/DefinitionResolver.php @@ -7,15 +7,14 @@ use PhpParser\Node; use PhpParser\PrettyPrinter\Standard as PrettyPrinter; use phpDocumentor\Reflection\{Types, Type, Fqsen, TypeResolver}; use LanguageServer\Protocol\SymbolInformation; -use Sabre\Event\Promise; -use function Sabre\Event\coroutine; +use LanguageServer\Index\ReadableIndex; class DefinitionResolver { /** - * @var \LanguageServer\Project + * @var \LanguageServer\Index */ - private $project; + private $index; /** * @var \phpDocumentor\Reflection\TypeResolver @@ -27,9 +26,12 @@ class DefinitionResolver */ private $prettyPrinter; - public function __construct(Project $project) + /** + * @param ReadableIndex $index + */ + public function __construct(ReadableIndex $index) { - $this->project = $project; + $this->index = $index; $this->typeResolver = new TypeResolver; $this->prettyPrinter = new PrettyPrinter; } @@ -147,8 +149,8 @@ class DefinitionResolver // http://php.net/manual/en/language.namespaces.fallback.php $parent = $node->getAttribute('parentNode'); $globalFallback = $parent instanceof Node\Expr\ConstFetch || $parent instanceof Node\Expr\FuncCall; - // Return the Definition object from the project index - return $this->project->getDefinition($fqn, $globalFallback); + // Return the Definition object from the index index + return $this->index->getDefinition($fqn, $globalFallback); } /** @@ -403,7 +405,7 @@ class DefinitionResolver return new Types\Mixed; } $fqn = (string)($expr->getAttribute('namespacedName') ?? $expr->name); - $def = $this->project->getDefinition($fqn, true); + $def = $this->index->getDefinition($fqn, true); if ($def !== null) { return $def->type; } @@ -414,7 +416,7 @@ class DefinitionResolver } // Resolve constant $fqn = (string)($expr->getAttribute('namespacedName') ?? $expr->name); - $def = $this->project->getDefinition($fqn, true); + $def = $this->index->getDefinition($fqn, true); if ($def !== null) { return $def->type; } @@ -443,7 +445,7 @@ class DefinitionResolver if ($expr instanceof Node\Expr\MethodCall) { $fqn .= '()'; } - $def = $this->project->getDefinition($fqn); + $def = $this->index->getDefinition($fqn); if ($def !== null) { return $def->type; } @@ -466,7 +468,7 @@ class DefinitionResolver if ($expr instanceof Node\Expr\StaticCall) { $fqn .= '()'; } - $def = $this->project->getDefinition($fqn); + $def = $this->index->getDefinition($fqn); if ($def === null) { return new Types\Mixed; } diff --git a/src/Index/AbstractAggregateIndex.php b/src/Index/AbstractAggregateIndex.php new file mode 100644 index 0000000..f8934c6 --- /dev/null +++ b/src/Index/AbstractAggregateIndex.php @@ -0,0 +1,66 @@ + Definition] that maps fully qualified symbol names + * to Definitions + * + * @return Definition[] + */ + public function getDefinitions(): array + { + $defs = []; + foreach ($this->getIndexes() as $index) { + foreach ($index->getDefinitions() as $fqn => $def) { + $defs[$fqn] = $def; + } + } + return $defs; + } + + /** + * Returns the Definition object by a specific FQN + * + * @param string $fqn + * @param bool $globalFallback Whether to fallback to global if the namespaced FQN was not found + * @return Definition|null + */ + public function getDefinition(string $fqn, bool $globalFallback = false) + { + foreach ($this->getIndexes() as $index) { + if ($def = $index->getDefinition($fqn, $globalFallback)) { + return $def; + } + } + } + + /** + * Returns all URIs in this index that reference a symbol + * + * @param string $fqn The fully qualified name of the symbol + * @return string[] + */ + public function getReferenceUris(string $fqn): array + { + $refs = []; + foreach ($this->getIndexes() as $index) { + foreach ($index->getReferenceUris($fqn) as $ref) { + $refs[] = $ref; + } + } + return $refs; + } +} diff --git a/src/Index/DependenciesIndex.php b/src/Index/DependenciesIndex.php new file mode 100644 index 0000000..a355821 --- /dev/null +++ b/src/Index/DependenciesIndex.php @@ -0,0 +1,52 @@ +indexes; + } + + /** + * @param string $packageName + * @return Index + */ + public function getDependencyIndex(string $packageName): Index + { + if (!isset($this->indexes[$packageName])) { + $this->indexes[$packageName] = new Index; + } + return $this->indexes[$packageName]; + } + + /** + * @param string $packageName + * @return void + */ + public function removeDependencyIndex(string $packageName) + { + unset($this->indexes[$packageName]); + } + + /** + * @param string $packageName + * @return bool + */ + public function hasDependencyIndex(string $packageName): bool + { + return isset($this->indexes[$packageName]); + } +} diff --git a/src/Index/GlobalIndex.php b/src/Index/GlobalIndex.php new file mode 100644 index 0000000..e1e6d48 --- /dev/null +++ b/src/Index/GlobalIndex.php @@ -0,0 +1,38 @@ +stubsIndex = $stubsIndex; + $this->projectIndex = $projectIndex; + } + + /** + * @return ReadableIndex[] + */ + protected function getIndexes(): array + { + return [$this->stubsIndex, $this->projectIndex]; + } +} diff --git a/src/Index/Index.php b/src/Index/Index.php new file mode 100644 index 0000000..29cbb99 --- /dev/null +++ b/src/Index/Index.php @@ -0,0 +1,129 @@ + Definition] that maps fully qualified symbol names + * to Definitions + * + * @return Definition[] + */ + public function getDefinitions(): array + { + return $this->definitions; + } + + /** + * Returns the Definition object by a specific FQN + * + * @param string $fqn + * @param bool $globalFallback Whether to fallback to global if the namespaced FQN was not found + * @return Definition|null + */ + public function getDefinition(string $fqn, bool $globalFallback = false) + { + if (isset($this->definitions[$fqn])) { + return $this->definitions[$fqn]; + } + if ($globalFallback) { + $parts = explode('\\', $fqn); + $fqn = end($parts); + return $this->getDefinition($fqn); + } + } + + /** + * Registers a definition + * + * @param string $fqn The fully qualified name of the symbol + * @param string $definition The Definition object + * @return void + */ + public function setDefinition(string $fqn, Definition $definition) + { + $this->definitions[$fqn] = $definition; + } + + /** + * 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 removeDefinition(string $fqn) + { + unset($this->definitions[$fqn]); + unset($this->references[$fqn]); + } + + /** + * Returns all URIs in this index that reference a symbol + * + * @param string $fqn The fully qualified name of the symbol + * @return string[] + */ + public function getReferenceUris(string $fqn): array + { + return $this->references[$fqn] ?? []; + } + + /** + * Adds a document URI as a referencee of a specific symbol + * + * @param string $fqn The fully qualified name of the symbol + * @return void + */ + public function addReferenceUri(string $fqn, string $uri) + { + if (!isset($this->references[$fqn])) { + $this->references[$fqn] = []; + } + // TODO: use DS\Set instead of searching array + if (array_search($uri, $this->references[$fqn], true) === false) { + $this->references[$fqn][] = $uri; + } + } + + /** + * Removes a document URI as the container for a specific symbol + * + * @param string $fqn The fully qualified name of the symbol + * @param string $uri The URI + * @return void + */ + public function removeReferenceUri(string $fqn, string $uri) + { + if (!isset($this->references[$fqn])) { + return; + } + $index = array_search($fqn, $this->references[$fqn], true); + if ($index === false) { + return; + } + array_splice($this->references[$fqn], $index, 1); + } +} diff --git a/src/Index/ProjectIndex.php b/src/Index/ProjectIndex.php new file mode 100644 index 0000000..8b42f8f --- /dev/null +++ b/src/Index/ProjectIndex.php @@ -0,0 +1,51 @@ +sourceIndex = $sourceIndex; + $this->dependenciesIndex = $dependenciesIndex; + } + + /** + * @return ReadableIndex[] + */ + protected function getIndexes(): array + { + return [$this->sourceIndex, $this->dependenciesIndex]; + } + + /** + * @param string $uri + * @return Index + */ + public function getIndexForUri(string $uri): Index + { + if (preg_match('/\/vendor\/(\w+\/\w+)\//', $uri, $matches)) { + $packageName = $matches[0]; + return $this->dependenciesIndex->getDependencyIndex($packageName); + } + return $this->sourceIndex; + } +} diff --git a/src/Index/ReadableIndex.php b/src/Index/ReadableIndex.php new file mode 100644 index 0000000..40f4e40 --- /dev/null +++ b/src/Index/ReadableIndex.php @@ -0,0 +1,37 @@ + Definition] that maps fully qualified symbol names + * to Definitions + * + * @return Definitions[] + */ + public function getDefinitions(): array; + + /** + * Returns the Definition object by a specific FQN + * + * @param string $fqn + * @param bool $globalFallback Whether to fallback to global if the namespaced FQN was not found + * @return Definition|null + */ + public function getDefinition(string $fqn, bool $globalFallback = false); + + /** + * Returns all URIs in this index that reference a symbol + * + * @param string $fqn The fully qualified name of the symbol + * @return string[] + */ + public function getReferenceUris(string $fqn): array; +} diff --git a/src/Index/StubsIndex.php b/src/Index/StubsIndex.php new file mode 100644 index 0000000..3828f31 --- /dev/null +++ b/src/Index/StubsIndex.php @@ -0,0 +1,27 @@ +getMessage(), + (string)$e, AdvancedJsonRpc\ErrorCode::INTERNAL_ERROR, null, $e @@ -120,54 +133,74 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher * @param ClientCapabilities $capabilities The capabilities provided by the client (editor) * @param string|null $rootPath The rootPath of the workspace. Is null if no folder is open. * @param int|null $processId The process Id of the parent process that started the server. Is null if the process has not been started by another process. If the parent process is not alive then the server should exit (see exit notification) its process. - * @return InitializeResult + * @return Promise */ - public function initialize(ClientCapabilities $capabilities, string $rootPath = null, int $processId = null): InitializeResult + public function initialize(ClientCapabilities $capabilities, string $rootPath = null, int $processId = null): Promise { - $this->rootPath = $rootPath; + return coroutine(function () use ($capabilities, $rootPath, $processId) { - if ($capabilities->xfilesProvider) { - $this->filesFinder = new ClientFilesFinder($this->client); - } else { - $this->filesFinder = new FileSystemFilesFinder; - } + if ($capabilities->xfilesProvider) { + $this->filesFinder = new ClientFilesFinder($this->client); + } else { + $this->filesFinder = new FileSystemFilesFinder; + } - if ($capabilities->xcontentProvider) { - $this->contentRetriever = new ClientContentRetriever($this->client); - } else { - $this->contentRetriever = new FileSystemContentRetriever; - } + if ($capabilities->xcontentProvider) { + $this->contentRetriever = new ClientContentRetriever($this->client); + } else { + $this->contentRetriever = new FileSystemContentRetriever; + } - $this->project = new Project($this->client, $this->contentRetriever); - $this->textDocument = new Server\TextDocument($this->project, $this->client); - $this->workspace = new Server\Workspace($this->project, $this->client); + $projectIndex = new ProjectIndex(new Index, new DependenciesIndex); + $stubsIndex = StubsIndex::read(); + $globalIndex = new GlobalIndex($stubsIndex, $projectIndex); - // start building project index - if ($rootPath !== null) { - $this->indexProject()->otherwise('\\LanguageServer\\crash'); - } + // The DefinitionResolver should look in stubs, the project source and dependencies + $definitionResolver = new DefinitionResolver($globalIndex); - $serverCapabilities = new ServerCapabilities(); - // Ask the client to return always full documents (because we need to rebuild the AST from scratch) - $serverCapabilities->textDocumentSync = TextDocumentSyncKind::FULL; - // Support "Find all symbols" - $serverCapabilities->documentSymbolProvider = true; - // Support "Find all symbols in workspace" - $serverCapabilities->workspaceSymbolProvider = true; - // Support "Format Code" - $serverCapabilities->documentFormattingProvider = true; - // Support "Go to definition" - $serverCapabilities->definitionProvider = true; - // Support "Find all references" - $serverCapabilities->referencesProvider = true; - // Support "Hover" - $serverCapabilities->hoverProvider = true; - // Support "Completion" - $serverCapabilities->completionProvider = new CompletionOptions; - $serverCapabilities->completionProvider->resolveProvider = false; - $serverCapabilities->completionProvider->triggerCharacters = ['$', '>']; + $this->documentLoader = new PhpDocumentLoader( + $this->contentRetriever, + $projectIndex, + $definitionResolver + ); - return new InitializeResult($serverCapabilities); + if ($rootPath !== null) { + $pattern = Path::makeAbsolute('**/*.php', $rootPath); + $uris = yield $this->filesFinder->find($pattern); + $this->index($uris)->otherwise('\\LanguageServer\\crash'); + } + + $this->textDocument = new Server\TextDocument( + $this->documentLoader, + $definitionResolver, + $this->client, + $globalIndex + ); + // workspace/symbol should only look inside the project source and dependencies + $this->workspace = new Server\Workspace($projectIndex, $this->client); + + $serverCapabilities = new ServerCapabilities(); + // Ask the client to return always full documents (because we need to rebuild the AST from scratch) + $serverCapabilities->textDocumentSync = TextDocumentSyncKind::FULL; + // Support "Find all symbols" + $serverCapabilities->documentSymbolProvider = true; + // Support "Find all symbols in workspace" + $serverCapabilities->workspaceSymbolProvider = true; + // Support "Format Code" + $serverCapabilities->documentFormattingProvider = true; + // Support "Go to definition" + $serverCapabilities->definitionProvider = true; + // Support "Find all references" + $serverCapabilities->referencesProvider = true; + // Support "Hover" + $serverCapabilities->hoverProvider = true; + // Support "Completion" + $serverCapabilities->completionProvider = new CompletionOptions; + $serverCapabilities->completionProvider->resolveProvider = false; + $serverCapabilities->completionProvider->triggerCharacters = ['$', '>']; + + return new InitializeResult($serverCapabilities); + }); } /** @@ -193,20 +226,24 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher } /** - * Parses workspace files, one at a time. + * Will read and parse the passed source files in the project and add them to the appropiate indexes * * @return Promise */ - private function indexProject(): Promise + private function index(array $uris): Promise { - return coroutine(function () { - $pattern = Path::makeAbsolute('**/*.php', $this->rootPath); - $uris = yield $this->filesFinder->find($pattern); + return coroutine(function () use ($uris) { + $count = count($uris); $startTime = microtime(true); + // Parse PHP files foreach ($uris as $i => $uri) { + if ($this->documentLoader->isOpen($uri)) { + continue; + } + // Give LS to the chance to handle requests while indexing yield timeout(); $this->client->window->logMessage( @@ -214,7 +251,10 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher "Parsing file $i/$count: {$uri}" ); try { - yield $this->project->loadDocument($uri); + $document = yield $this->documentLoader->load($uri); + if (!$document->isVendored()) { + $this->client->textDocument->publishDiagnostics($uri, $document->getDiagnostics()); + } } catch (ContentTooLargeException $e) { $this->client->window->logMessage( MessageType::INFO, diff --git a/src/PhpDocument.php b/src/PhpDocument.php index 1e1a726..76009a9 100644 --- a/src/PhpDocument.php +++ b/src/PhpDocument.php @@ -10,34 +10,16 @@ use LanguageServer\NodeVisitor\{ DocBlockParser, DefinitionCollector, ColumnCalculator, - ReferencesCollector, - VariableReferencesCollector + ReferencesCollector }; +use LanguageServer\Index\Index; use PhpParser\{Error, ErrorHandler, Node, NodeTraverser}; use PhpParser\NodeVisitor\NameResolver; use phpDocumentor\Reflection\DocBlockFactory; -use Sabre\Event\Promise; -use function Sabre\Event\coroutine; use Sabre\Uri; class PhpDocument { - /** - * The LanguageClient instance (to report errors etc) - * - * @var LanguageClient - */ - private $client; - - /** - * The Project this document belongs to (to register definitions etc) - * - * @var Project - */ - public $project; - // for whatever reason I get "cannot access private property" error if $project is not public - // https://github.com/felixfbecker/php-language-server/pull/49#issuecomment-252427359 - /** * The PHPParser instance * @@ -59,6 +41,11 @@ class PhpDocument */ private $definitionResolver; + /** + * @var Index + */ + private $index; + /** * The URI of the document * @@ -102,25 +89,30 @@ class PhpDocument private $referenceNodes; /** - * @param string $uri The URI of the document - * @param string $content The content of the document - * @param Project $project The Project this document belongs to (to register definitions etc) - * @param LanguageClient $client The LanguageClient instance (to report errors etc) - * @param Parser $parser The PHPParser instance - * @param DocBlockFactory $docBlockFactory The DocBlockFactory instance to parse docblocks + * Diagnostics for this document that were collected while parsing + * + * @var Diagnostic[] + */ + private $diagnostics; + + /** + * @param string $uri The URI of the document + * @param string $content The content of the document + * @param Index $index The Index to register definitions and references to + * @param Parser $parser The PHPParser instance + * @param DocBlockFactory $docBlockFactory The DocBlockFactory instance to parse docblocks + * @param DefinitionResolver $definitionResolver The DefinitionResolver to resolve definitions to symbols in the workspace */ public function __construct( string $uri, string $content, - Project $project, - LanguageClient $client, + Index $index, Parser $parser, DocBlockFactory $docBlockFactory, DefinitionResolver $definitionResolver ) { $this->uri = $uri; - $this->project = $project; - $this->client = $client; + $this->index = $index; $this->parser = $parser; $this->docBlockFactory = $docBlockFactory; $this->definitionResolver = $definitionResolver; @@ -154,9 +146,9 @@ class PhpDocument $errorHandler = new ErrorHandler\Collecting; $stmts = $this->parser->parse($content, $errorHandler); - $diagnostics = []; + $this->diagnostics = []; foreach ($errorHandler->getErrors() as $error) { - $diagnostics[] = Diagnostic::fromError($error, $this->content, DiagnosticSeverity::ERROR, 'php'); + $this->diagnostics[] = Diagnostic::fromError($error, $this->content, DiagnosticSeverity::ERROR, 'php'); } // $stmts can be null in case of a fatal parsing error @@ -180,7 +172,7 @@ class PhpDocument // Report errors from parsing docblocks foreach ($docBlockParser->errors as $error) { - $diagnostics[] = Diagnostic::fromError($error, $this->content, DiagnosticSeverity::WARNING, 'php'); + $this->diagnostics[] = Diagnostic::fromError($error, $this->content, DiagnosticSeverity::WARNING, 'php'); } $traverser = new NodeTraverser; @@ -198,34 +190,30 @@ class PhpDocument // Unregister old definitions if (isset($this->definitions)) { foreach ($this->definitions as $fqn => $definition) { - $this->project->removeDefinition($fqn); + $this->index->removeDefinition($fqn); } } // Register this document on the project for all the symbols defined in it $this->definitions = $definitionCollector->definitions; $this->definitionNodes = $definitionCollector->nodes; foreach ($definitionCollector->definitions as $fqn => $definition) { - $this->project->setDefinition($fqn, $definition); + $this->index->setDefinition($fqn, $definition); } // Unregister old references if (isset($this->referenceNodes)) { foreach ($this->referenceNodes as $fqn => $node) { - $this->project->removeReferenceUri($fqn, $this->uri); + $this->index->removeReferenceUri($fqn, $this->uri); } } // Register this document on the project for references $this->referenceNodes = $referencesCollector->nodes; foreach ($referencesCollector->nodes as $fqn => $nodes) { - $this->project->addReferenceUri($fqn, $this->uri); + $this->index->addReferenceUri($fqn, $this->uri); } $this->stmts = $stmts; } - - if (!$this->isVendored()) { - $this->client->textDocument->publishDiagnostics($this->uri, $diagnostics); - } } /** @@ -262,6 +250,16 @@ class PhpDocument return $this->content; } + /** + * Returns this document's diagnostics + * + * @return Diagnostic[] + */ + public function getDiagnostics() + { + return $this->diagnostics; + } + /** * Returns the URI of the document * @@ -357,57 +355,4 @@ class PhpDocument { return isset($this->definitions[$fqn]); } - - /** - * Returns the reference nodes for any node - * The references node MAY be in other documents, check the ownerDocument attribute - * - * @param Node $node - * @return 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 - // by traversing the AST - if ( - $node instanceof Node\Expr\Variable - || $node instanceof Node\Param - || $node instanceof Node\Expr\ClosureUse - ) { - 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->nodes; - } - // Definition with a global FQN - $fqn = DefinitionResolver::getDefinedFqn($node); - if ($fqn === null) { - return []; - } - $refDocuments = yield $this->project->getReferenceDocuments($fqn); - $nodes = []; - foreach ($refDocuments as $document) { - $refs = $document->getReferenceNodesByFqn($fqn); - if ($refs !== null) { - foreach ($refs as $ref) { - $nodes[] = $ref; - } - } - } - return $nodes; - }); - } } diff --git a/src/PhpDocumentLoader.php b/src/PhpDocumentLoader.php new file mode 100644 index 0000000..b6c04cb --- /dev/null +++ b/src/PhpDocumentLoader.php @@ -0,0 +1,178 @@ + PhpDocument of open documents that should be kept in memory + * + * @var PhpDocument + */ + private $documents = []; + + /** + * @var ContentRetriever + */ + private $contentRetriever; + + /** + * @var ProjectIndex + */ + private $projectIndex; + + /** + * @var Parser + */ + private $parser; + + /** + * @var DocBlockFactory + */ + private $docBlockFactory; + + /** + * @var DefinitionResolver + */ + private $definitionResolver; + + /** + * @param ContentRetriever $contentRetriever + * @param ProjectIndex $project + * @param DefinitionResolver $definitionResolver + */ + public function __construct( + ContentRetriever $contentRetriever, + ProjectIndex $projectIndex, + DefinitionResolver $definitionResolver + ) { + $this->contentRetriever = $contentRetriever; + $this->projectIndex = $projectIndex; + $this->definitionResolver = $definitionResolver; + $this->parser = new Parser; + $this->docBlockFactory = DocBlockFactory::createInstance(); + } + + /** + * Returns the document indicated by uri. + * Returns null if the document if not loaded. + * + * @param string $uri + * @return PhpDocument|null + */ + public function get(string $uri) + { + return $this->documents[$uri] ?? null; + } + + /** + * Returns the document indicated by uri. + * If the document is not open, loads it. + * + * @param string $uri + * @return Promise + */ + public function getOrLoad(string $uri): Promise + { + return isset($this->documents[$uri]) ? Promise\resolve($this->documents[$uri]) : $this->load($uri); + } + + /** + * Loads the document by doing a textDocument/xcontent request to the client. + * If the client does not support textDocument/xcontent, tries to read the file from the file system. + * The document is NOT added to the list of open documents, but definitions are registered. + * + * @param string $uri + * @return Promise + */ + public function load(string $uri): Promise + { + return coroutine(function () use ($uri) { + + $limit = 150000; + $content = yield $this->contentRetriever->retrieve($uri); + $size = strlen($content); + if ($size > $limit) { + throw new ContentTooLargeException($uri, $size, $limit); + } + + if (isset($this->documents[$uri])) { + $document = $this->documents[$uri]; + $document->updateContent($content); + } else { + $document = $this->create($uri, $content); + } + return $document; + }); + } + + /** + * Builds a PhpDocument instance + * + * @param string $uri + * @param string $content + * @return PhpDocument + */ + public function create(string $uri, string $content): PhpDocument + { + return new PhpDocument( + $uri, + $content, + $this->projectIndex->getIndexForUri($uri), + $this->parser, + $this->docBlockFactory, + $this->definitionResolver + ); + } + + /** + * Ensures a document is loaded and added to the list of open documents. + * + * @param string $uri + * @param string $content + * @return void + */ + public function open(string $uri, string $content) + { + if (isset($this->documents[$uri])) { + $document = $this->documents[$uri]; + $document->updateContent($content); + } else { + $document = $this->create($uri, $content); + $this->documents[$uri] = $document; + } + return $document; + } + + /** + * Removes the document with the specified URI from the list of open documents + * + * @param string $uri + * @return void + */ + public function close(string $uri) + { + unset($this->documents[$uri]); + } + + /** + * Returns true if the document is open (and loaded) + * + * @param string $uri + * @return bool + */ + public function isOpen(string $uri): bool + { + return isset($this->documents[$uri]); + } +} diff --git a/src/Project.php b/src/Project.php deleted file mode 100644 index 0582301..0000000 --- a/src/Project.php +++ /dev/null @@ -1,349 +0,0 @@ - PhpDocument] - * that maps URIs to loaded PhpDocuments - * - * @var PhpDocument[] - */ - private $documents = []; - - /** - * An associative array that maps fully qualified symbol names to Definitions - * - * @var Definition[] - */ - 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 - * - * @var Parser - */ - private $parser; - - /** - * The DocBlockFactory instance to parse docblocks - * - * @var DocBlockFactory - */ - private $docBlockFactory; - - /** - * The DefinitionResolver instance to resolve reference nodes to Definitions - * - * @var DefinitionResolver - */ - private $definitionResolver; - - /** - * Reference to the language server client interface - * - * @var LanguageClient - */ - private $client; - - /** - * The content retriever - * - * @var ContentRetriever - */ - private $contentRetriever; - - public function __construct(LanguageClient $client, ContentRetriever $contentRetriever) - { - $this->client = $client; - $this->parser = new Parser; - $this->docBlockFactory = DocBlockFactory::createInstance(); - $this->definitionResolver = new DefinitionResolver($this); - $this->contentRetriever = $contentRetriever; - } - - /** - * Returns the document indicated by uri. - * Returns null if the document if not loaded. - * - * @param string $uri - * @return PhpDocument|null - */ - public function getDocument(string $uri) - { - return $this->documents[$uri] ?? null; - } - - /** - * Returns the document indicated by uri. - * If the document is not open, loads it. - * - * @param string $uri - * @return Promise - */ - public function getOrLoadDocument(string $uri) - { - return isset($this->documents[$uri]) ? Promise\resolve($this->documents[$uri]) : $this->loadDocument($uri); - } - - /** - * Loads the document by doing a textDocument/xcontent request to the client. - * If the client does not support textDocument/xcontent, tries to read the file from the file system. - * The document is NOT added to the list of open documents, but definitions are registered. - * - * @param string $uri - * @return Promise - */ - public function loadDocument(string $uri): Promise - { - return coroutine(function () use ($uri) { - $limit = 150000; - $content = yield $this->contentRetriever->retrieve($uri); - $size = strlen($content); - if ($size > $limit) { - throw new ContentTooLargeException($uri, $size, $limit); - } - if (isset($this->documents[$uri])) { - $document = $this->documents[$uri]; - $document->updateContent($content); - } else { - $document = new PhpDocument( - $uri, - $content, - $this, - $this->client, - $this->parser, - $this->docBlockFactory, - $this->definitionResolver - ); - } - return $document; - }); - } - - /** - * Ensures a document is loaded and added to the list of open documents. - * - * @param string $uri - * @param string $content - * @return void - */ - public function openDocument(string $uri, string $content) - { - if (isset($this->documents[$uri])) { - $document = $this->documents[$uri]; - $document->updateContent($content); - } else { - $document = new PhpDocument( - $uri, - $content, - $this, - $this->client, - $this->parser, - $this->docBlockFactory, - $this->definitionResolver - ); - $this->documents[$uri] = $document; - } - return $document; - } - - /** - * Removes the document with the specified URI from the list of open documents - * - * @param string $uri - * @return void - */ - public function closeDocument(string $uri) - { - unset($this->documents[$uri]); - } - - /** - * Returns true if the document is open (and loaded) - * - * @param string $uri - * @return bool - */ - public function isDocumentOpen(string $uri): bool - { - return isset($this->documents[$uri]); - } - - /** - * Returns an associative array [string => Definition] that maps fully qualified symbol names - * to Definitions - * - * @return Definitions[] - */ - public function getDefinitions() - { - return $this->definitions; - } - - /** - * Returns the Definition object by a specific FQN - * - * @param string $fqn - * @param bool $globalFallback Whether to fallback to global if the namespaced FQN was not found - * @return Definition|null - */ - public function getDefinition(string $fqn, $globalFallback = false) - { - if (isset($this->definitions[$fqn])) { - return $this->definitions[$fqn]; - } else if ($globalFallback) { - $parts = explode('\\', $fqn); - $fqn = end($parts); - return $this->getDefinition($fqn); - } - } - - /** - * Registers a definition - * - * @param string $fqn The fully qualified name of the symbol - * @param string $definition The Definition object - * @return void - */ - public function setDefinition(string $fqn, Definition $definition) - { - $this->definitions[$fqn] = $definition; - } - - /** - * Sets the Definition index - * - * @param Definition[] $definitions Map from FQN to Definition - * @return void - */ - public function setDefinitions(array $definitions) - { - $this->definitions = $definitions; - } - - /** - * 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 removeDefinition(string $fqn) - { - unset($this->definitions[$fqn]); - unset($this->references[$fqn]); - } - - /** - * Adds a document URI as a referencee of a specific symbol - * - * @param string $fqn The fully qualified name of the symbol - * @return void - */ - public function addReferenceUri(string $fqn, string $uri) - { - if (!isset($this->references[$fqn])) { - $this->references[$fqn] = []; - } - // TODO: use DS\Set instead of searching array - if (array_search($uri, $this->references[$fqn], true) === false) { - $this->references[$fqn][] = $uri; - } - } - - /** - * Removes a document URI as the container for a specific symbol - * - * @param string $fqn The fully qualified name of the symbol - * @param string $uri The URI - * @return void - */ - public function removeReferenceUri(string $fqn, string $uri) - { - if (!isset($this->references[$fqn])) { - return; - } - $index = array_search($fqn, $this->references[$fqn], true); - if ($index === false) { - return; - } - array_splice($this->references[$fqn], $index, 1); - } - - /** - * Returns all documents that reference a symbol - * - * @param string $fqn The fully qualified name of the symbol - * @return Promise - */ - public function getReferenceDocuments(string $fqn): Promise - { - if (!isset($this->references[$fqn])) { - return Promise\resolve([]); - } - return Promise\all(array_map([$this, 'getOrLoadDocument'], $this->references[$fqn])); - } - - /** - * Returns an associative array [string => string[]] that maps fully qualified symbol names - * to URIs of the document where the symbol is referenced - * - * @return string[][] - */ - public function getReferenceUris() - { - return $this->references; - } - - /** - * Sets the reference index - * - * @param string[][] $references an associative array [string => string[]] from FQN to URIs - * @return void - */ - public function setReferenceUris(array $references) - { - $this->references = $references; - } - - /** - * Returns the document where a symbol is defined - * - * @param string $fqn The fully qualified name of the symbol - * @return Promise - */ - public function getDefinitionDocument(string $fqn): Promise - { - if (!isset($this->definitions[$fqn])) { - return Promise\resolve(null); - } - return $this->getOrLoadDocument($this->definitions[$fqn]->symbolInformation->location->uri); - } - - /** - * Returns true if the given FQN is defined in the project - * - * @param string $fqn The fully qualified name of the symbol - * @return bool - */ - public function isDefined(string $fqn): bool - { - return isset($this->definitions[$fqn]); - } -} diff --git a/src/Server/TextDocument.php b/src/Server/TextDocument.php index 5998acc..84103df 100644 --- a/src/Server/TextDocument.php +++ b/src/Server/TextDocument.php @@ -3,9 +3,10 @@ declare(strict_types = 1); namespace LanguageServer\Server; -use LanguageServer\{LanguageClient, Project, PhpDocument, DefinitionResolver, CompletionProvider}; use PhpParser\PrettyPrinter\Standard as PrettyPrinter; -use PhpParser\Node; +use PhpParser\{Node, NodeTraverser}; +use LanguageServer\{LanguageClient, PhpDocumentLoader, PhpDocument, DefinitionResolver, CompletionProvider}; +use LanguageServer\NodeVisitor\VariableReferencesCollector; use LanguageServer\Protocol\{ TextDocumentItem, TextDocumentIdentifier, @@ -23,7 +24,9 @@ use LanguageServer\Protocol\{ CompletionItem, CompletionItemKind }; +use LanguageServer\Index\ReadableIndex; use Sabre\Event\Promise; +use Sabre\Uri; use function Sabre\Event\coroutine; /** @@ -58,13 +61,29 @@ class TextDocument */ private $completionProvider; - public function __construct(Project $project, LanguageClient $client) - { - $this->project = $project; + /** + * @var ReadableIndex + */ + private $index; + + /** + * @param PhpDocumentLoader $documentLoader + * @param DefinitionResolver $definitionResolver + * @param LanguageClient $client + * @param ReadableIndex $index + */ + public function __construct( + PhpDocumentLoader $documentLoader, + DefinitionResolver $definitionResolver, + LanguageClient $client, + ReadableIndex $index + ) { + $this->documentLoader = $documentLoader; $this->client = $client; $this->prettyPrinter = new PrettyPrinter(); - $this->definitionResolver = new DefinitionResolver($project); - $this->completionProvider = new CompletionProvider($this->definitionResolver, $project); + $this->definitionResolver = $definitionResolver; + $this->completionProvider = new CompletionProvider($this->definitionResolver, $index); + $this->index = $index; } /** @@ -76,7 +95,7 @@ class TextDocument */ public function documentSymbol(TextDocumentIdentifier $textDocument): Promise { - return $this->project->getOrLoadDocument($textDocument->uri)->then(function (PhpDocument $document) { + return $this->documentLoader->getOrLoad($textDocument->uri)->then(function (PhpDocument $document) { $symbols = []; foreach ($document->getDefinitions() as $fqn => $definition) { $symbols[] = $definition->symbolInformation; @@ -95,7 +114,10 @@ class TextDocument */ public function didOpen(TextDocumentItem $textDocument) { - $this->project->openDocument($textDocument->uri, $textDocument->text); + $document = $this->documentLoader->open($textDocument->uri, $textDocument->text); + if (!$document->isVendored()) { + $this->client->textDocument->publishDiagnostics($textDocument->uri, $document->getDiagnostics()); + } } /** @@ -107,7 +129,9 @@ class TextDocument */ public function didChange(VersionedTextDocumentIdentifier $textDocument, array $contentChanges) { - $this->project->getDocument($textDocument->uri)->updateContent($contentChanges[0]->text); + $document = $this->documentLoader->get($textDocument->uri); + $document->updateContent($contentChanges[0]->text); + $this->client->textDocument->publishDiagnostics($textDocument->uri, $document->getDiagnostics()); } /** @@ -120,7 +144,7 @@ class TextDocument */ public function didClose(TextDocumentIdentifier $textDocument) { - $this->project->closeDocument($textDocument->uri); + $this->documentLoader->close($textDocument->uri); } /** @@ -132,7 +156,7 @@ class TextDocument */ public function formatting(TextDocumentIdentifier $textDocument, FormattingOptions $options) { - return $this->project->getOrLoadDocument($textDocument->uri)->then(function (PhpDocument $document) { + return $this->documentLoader->getOrLoad($textDocument->uri)->then(function (PhpDocument $document) { return $document->getFormattedText(); }); } @@ -150,15 +174,55 @@ class TextDocument Position $position ): Promise { return coroutine(function () use ($textDocument, $position) { - $document = yield $this->project->getOrLoadDocument($textDocument->uri); + $document = yield $this->documentLoader->getOrLoad($textDocument->uri); $node = $document->getNodeAtPosition($position); if ($node === null) { return []; } - $refs = yield $document->getReferenceNodesByNode($node); $locations = []; - foreach ($refs as $ref) { - $locations[] = Location::fromNode($ref); + // 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 + || $node instanceof Node\Param + || $node instanceof Node\Expr\ClosureUse + ) { + 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()); + foreach ($refCollector->nodes as $ref) { + $locations[] = Location::fromNode($ref); + } + } else { + // Definition with a global FQN + $fqn = DefinitionResolver::getDefinedFqn($node); + if ($fqn === null) { + return []; + } + $refDocuments = yield Promise\all(array_map( + [$this->documentLoader, 'getOrLoad'], + $this->index->getReferenceUris($fqn) + )); + foreach ($refDocuments as $document) { + $refs = $document->getReferenceNodesByFqn($fqn); + if ($refs !== null) { + foreach ($refs as $ref) { + $locations[] = Location::fromNode($ref); + } + } + } } return $locations; }); @@ -175,13 +239,17 @@ class TextDocument public function definition(TextDocumentIdentifier $textDocument, Position $position): Promise { return coroutine(function () use ($textDocument, $position) { - $document = yield $this->project->getOrLoadDocument($textDocument->uri); + $document = yield $this->documentLoader->getOrLoad($textDocument->uri); $node = $document->getNodeAtPosition($position); if ($node === null) { return []; } $def = $this->definitionResolver->resolveReferenceNodeToDefinition($node); - if ($def === null || $def->symbolInformation === null) { + if ( + $def === null + || $def->symbolInformation === null + || Uri\parse($def->symbolInformation->location->uri)['scheme'] === 'phpstubs' + ) { return []; } return $def->symbolInformation->location; @@ -198,7 +266,7 @@ class TextDocument public function hover(TextDocumentIdentifier $textDocument, Position $position): Promise { return coroutine(function () use ($textDocument, $position) { - $document = yield $this->project->getOrLoadDocument($textDocument->uri); + $document = yield $this->documentLoader->getOrLoad($textDocument->uri); // Find the node under the cursor $node = $document->getNodeAtPosition($position); if ($node === null) { @@ -237,7 +305,7 @@ class TextDocument public function completion(TextDocumentIdentifier $textDocument, Position $position): Promise { return coroutine(function () use ($textDocument, $position) { - $document = yield $this->project->getOrLoadDocument($textDocument->uri); + $document = yield $this->documentLoader->getOrLoad($textDocument->uri); return $this->completionProvider->provideCompletion($document, $position); }); } diff --git a/src/Server/Workspace.php b/src/Server/Workspace.php index 26feb72..66f8606 100644 --- a/src/Server/Workspace.php +++ b/src/Server/Workspace.php @@ -4,6 +4,7 @@ declare(strict_types = 1); namespace LanguageServer\Server; use LanguageServer\{LanguageClient, Project}; +use LanguageServer\Index\ProjectIndex; use LanguageServer\Protocol\SymbolInformation; /** @@ -19,15 +20,18 @@ class Workspace private $client; /** - * The current project database + * The symbol index for the workspace * - * @var Project + * @var ProjectIndex */ - private $project; + private $index; - public function __construct(Project $project, LanguageClient $client) + /** + * @param ProjectIndex $index Index that is searched on a workspace/symbol request + */ + public function __construct(ProjectIndex $index, LanguageClient $client) { - $this->project = $project; + $this->index = $index; $this->client = $client; } @@ -40,7 +44,7 @@ class Workspace public function symbol(string $query): array { $symbols = []; - foreach ($this->project->getDefinitions() as $fqn => $definition) { + foreach ($this->index->getDefinitions() as $fqn => $definition) { if ($query === '' || stripos($fqn, $query) !== false) { $symbols[] = $definition->symbolInformation; } diff --git a/tests/LanguageServerTest.php b/tests/LanguageServerTest.php index 8bd7b4f..d9f0d81 100644 --- a/tests/LanguageServerTest.php +++ b/tests/LanguageServerTest.php @@ -6,7 +6,16 @@ namespace LanguageServer\Tests; use PHPUnit\Framework\TestCase; use LanguageServer\LanguageServer; use LanguageServer\Protocol\{ - Message, ClientCapabilities, TextDocumentSyncKind, MessageType, TextDocumentItem, TextDocumentIdentifier}; + Message, + ClientCapabilities, + TextDocumentSyncKind, + MessageType, + TextDocumentItem, + TextDocumentIdentifier, + InitializeResult, + ServerCapabilities, + CompletionOptions +}; use AdvancedJsonRpc; use Webmozart\Glob\Glob; use Webmozart\PathUtil\Path; @@ -18,41 +27,22 @@ class LanguageServerTest extends TestCase { public function testInitialize() { - $reader = new MockProtocolStream(); - $writer = new MockProtocolStream(); - $server = new LanguageServer($reader, $writer); - $promise = new Promise; - $writer->once('message', [$promise, 'fulfill']); - $reader->write(new Message(new AdvancedJsonRpc\Request(1, 'initialize', [ - 'rootPath' => __DIR__, - 'processId' => getmypid(), - 'capabilities' => new ClientCapabilities() - ]))); - $msg = $promise->wait(); - $this->assertNotNull($msg, 'message event should be emitted'); - $this->assertInstanceOf(AdvancedJsonRpc\SuccessResponse::class, $msg->body); - $this->assertEquals((object)[ - 'capabilities' => (object)[ - 'textDocumentSync' => TextDocumentSyncKind::FULL, - 'documentSymbolProvider' => true, - 'hoverProvider' => true, - 'completionProvider' => (object)[ - 'resolveProvider' => false, - 'triggerCharacters' => ['$', '>'] - ], - 'signatureHelpProvider' => null, - 'definitionProvider' => true, - 'referencesProvider' => true, - 'documentHighlightProvider' => null, - 'workspaceSymbolProvider' => true, - 'codeActionProvider' => null, - 'codeLensProvider' => null, - 'documentFormattingProvider' => true, - 'documentRangeFormattingProvider' => null, - 'documentOnTypeFormattingProvider' => null, - 'renameProvider' => null - ] - ], $msg->body->result); + $server = new LanguageServer(new MockProtocolStream, new MockProtocolStream); + $result = $server->initialize(new ClientCapabilities, __DIR__, getmypid())->wait(); + + $serverCapabilities = new ServerCapabilities(); + $serverCapabilities->textDocumentSync = TextDocumentSyncKind::FULL; + $serverCapabilities->documentSymbolProvider = true; + $serverCapabilities->workspaceSymbolProvider = true; + $serverCapabilities->documentFormattingProvider = true; + $serverCapabilities->definitionProvider = true; + $serverCapabilities->referencesProvider = true; + $serverCapabilities->hoverProvider = true; + $serverCapabilities->completionProvider = new CompletionOptions; + $serverCapabilities->completionProvider->resolveProvider = false; + $serverCapabilities->completionProvider->triggerCharacters = ['$', '>']; + + $this->assertEquals(new InitializeResult($serverCapabilities), $result); } public function testIndexingWithDirectFileAccess() diff --git a/tests/NodeVisitor/DefinitionCollectorTest.php b/tests/NodeVisitor/DefinitionCollectorTest.php index 800373d..9b60814 100644 --- a/tests/NodeVisitor/DefinitionCollectorTest.php +++ b/tests/NodeVisitor/DefinitionCollectorTest.php @@ -6,9 +6,11 @@ namespace LanguageServer\Tests\Server\TextDocument; use PHPUnit\Framework\TestCase; use PhpParser\{NodeTraverser, Node}; use PhpParser\NodeVisitor\NameResolver; -use LanguageServer\{LanguageClient, Project, PhpDocument, Parser, DefinitionResolver}; +use phpDocumentor\Reflection\DocBlockFactory; +use LanguageServer\{LanguageClient, PhpDocument, PhpDocumentLoader, Parser, DefinitionResolver}; use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\Protocol\ClientCapabilities; +use LanguageServer\Index\{ProjectIndex, Index, DependenciesIndex}; use LanguageServer\Tests\MockProtocolStream; use LanguageServer\NodeVisitor\{ReferencesAdder, DefinitionCollector}; use function LanguageServer\pathToUri; @@ -17,19 +19,25 @@ class DefinitionCollectorTest extends TestCase { public function testCollectsSymbols() { - $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $project = new Project($client, new FileSystemContentRetriever); + $path = realpath(__DIR__ . '/../../fixtures/symbols.php'); + $uri = pathToUri($path); $parser = new Parser; - $uri = pathToUri(realpath(__DIR__ . '/../../fixtures/symbols.php')); - $document = $project->loadDocument($uri)->wait(); + $docBlockFactory = DocBlockFactory::createInstance(); + $index = new Index; + $definitionResolver = new DefinitionResolver($index); + $content = file_get_contents($path); + $document = new PhpDocument($uri, $content, $index, $parser, $docBlockFactory, $definitionResolver); + $stmts = $parser->parse($content); + $traverser = new NodeTraverser; $traverser->addVisitor(new NameResolver); $traverser->addVisitor(new ReferencesAdder($document)); - $definitionCollector = new DefinitionCollector(new DefinitionResolver($project)); + $definitionCollector = new DefinitionCollector($definitionResolver); $traverser->addVisitor($definitionCollector); - $stmts = $parser->parse(file_get_contents($uri)); $traverser->traverse($stmts); + $defNodes = $definitionCollector->nodes; + $this->assertEquals([ 'TestNamespace', 'TestNamespace\\TEST_CONST', @@ -57,19 +65,25 @@ class DefinitionCollectorTest extends TestCase public function testDoesNotCollectReferences() { - $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $project = new Project($client, new FileSystemContentRetriever); + $path = realpath(__DIR__ . '/../../fixtures/references.php'); + $uri = pathToUri($path); $parser = new Parser; - $uri = pathToUri(realpath(__DIR__ . '/../../fixtures/references.php')); - $document = $project->loadDocument($uri)->wait(); + $docBlockFactory = DocBlockFactory::createInstance(); + $index = new Index; + $definitionResolver = new DefinitionResolver($index); + $content = file_get_contents($path); + $document = new PhpDocument($uri, $content, $index, $parser, $docBlockFactory, $definitionResolver); + $stmts = $parser->parse($content); + $traverser = new NodeTraverser; $traverser->addVisitor(new NameResolver); $traverser->addVisitor(new ReferencesAdder($document)); - $definitionCollector = new DefinitionCollector(new DefinitionResolver($project)); + $definitionCollector = new DefinitionCollector($definitionResolver); $traverser->addVisitor($definitionCollector); - $stmts = $parser->parse(file_get_contents($uri)); $traverser->traverse($stmts); + $defNodes = $definitionCollector->nodes; + $this->assertEquals(['TestNamespace', 'TestNamespace\\whatever()'], array_keys($defNodes)); $this->assertInstanceOf(Node\Stmt\Namespace_::class, $defNodes['TestNamespace']); $this->assertInstanceOf(Node\Stmt\Function_::class, $defNodes['TestNamespace\\whatever()']); diff --git a/tests/ProjectTest.php b/tests/PhpDocumentLoaderTest.php similarity index 50% rename from tests/ProjectTest.php rename to tests/PhpDocumentLoaderTest.php index 6fef176..7be062d 100644 --- a/tests/ProjectTest.php +++ b/tests/PhpDocumentLoaderTest.php @@ -5,8 +5,9 @@ namespace LanguageServer\Tests\Server; use PHPUnit\Framework\TestCase; use LanguageServer\Tests\MockProtocolStream; -use LanguageServer\{Server, Client, LanguageClient, Project, PhpDocument}; +use LanguageServer\{Server, Client, LanguageClient, Project, PhpDocument, PhpDocumentLoader, DefinitionResolver}; use LanguageServer\ContentRetriever\FileSystemContentRetriever; +use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex}; use LanguageServer\Protocol\{ TextDocumentItem, TextDocumentIdentifier, @@ -18,31 +19,35 @@ use LanguageServer\Protocol\{ use AdvancedJsonRpc\{Request as RequestBody, Response as ResponseBody}; use function LanguageServer\pathToUri; -class ProjectTest extends TestCase +class PhpDocumentLoaderTest extends TestCase { /** - * @var Project $project + * @var PhpDocumentLoader */ - private $project; + private $loader; public function setUp() { - $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $this->project = new Project($client, new FileSystemContentRetriever); + $projectIndex = new ProjectIndex(new Index, new DependenciesIndex); + $this->loader = new PhpDocumentLoader( + new FileSystemContentRetriever, + $projectIndex, + new DefinitionResolver($projectIndex) + ); } - public function testGetOrLoadDocumentLoadsDocument() + public function testGetOrLoadLoadsDocument() { - $document = $this->project->getOrLoadDocument(pathToUri(__FILE__))->wait(); + $document = $this->loader->getOrLoad(pathToUri(__FILE__))->wait(); $this->assertNotNull($document); $this->assertInstanceOf(PhpDocument::class, $document); } - public function testGetDocumentReturnsOpenedInstance() + public function testGetReturnsOpenedInstance() { - $document1 = $this->project->openDocument(pathToUri(__FILE__), file_get_contents(__FILE__)); - $document2 = $this->project->getDocument(pathToUri(__FILE__)); + $document1 = $this->loader->open(pathToUri(__FILE__), file_get_contents(__FILE__)); + $document2 = $this->loader->get(pathToUri(__FILE__)); $this->assertSame($document1, $document2); } diff --git a/tests/PhpDocumentTest.php b/tests/PhpDocumentTest.php index a4e8ffa..b9b3704 100644 --- a/tests/PhpDocumentTest.php +++ b/tests/PhpDocumentTest.php @@ -4,36 +4,36 @@ declare(strict_types = 1); namespace LanguageServer\Tests\Server; use PHPUnit\Framework\TestCase; +use phpDocumentor\Reflection\DocBlockFactory; use LanguageServer\Tests\MockProtocolStream; -use LanguageServer\{LanguageClient, Project}; +use LanguageServer\{LanguageClient, PhpDocument, DefinitionResolver, Parser}; use LanguageServer\NodeVisitor\NodeAtPositionFinder; use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\Protocol\{SymbolKind, Position, ClientCapabilities}; +use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex}; use PhpParser\Node; class PhpDocumentTest extends TestCase { - /** - * @var Project $project - */ - private $project; - - public function setUp() + public function createDocument(string $uri, string $content) { - $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $this->project = new Project($client, new FileSystemContentRetriever); + $parser = new Parser; + $docBlockFactory = DocBlockFactory::createInstance(); + $index = new Index; + $definitionResolver = new DefinitionResolver($index); + return new PhpDocument($uri, $content, $index, $parser, $docBlockFactory, $definitionResolver); } public function testParsesVariableVariables() { - $document = $this->project->openDocument('whatever', "createDocument('whatever', "assertEquals([], $document->getDefinitions()); } public function testGetNodeAtPosition() { - $document = $this->project->openDocument('whatever', "createDocument('whatever', "getNodeAtPosition(new Position(1, 13)); $this->assertInstanceOf(Node\Name\FullyQualified::class, $node); $this->assertEquals('SomeClass', (string)$node); @@ -41,19 +41,19 @@ class PhpDocumentTest extends TestCase public function testIsVendored() { - $document = $this->project->openDocument('file:///dir/vendor/x.php', "createDocument('file:///dir/vendor/x.php', "assertEquals(true, $document->isVendored()); - $document = $this->project->openDocument('file:///c:/dir/vendor/x.php', "createDocument('file:///c:/dir/vendor/x.php', "assertEquals(true, $document->isVendored()); - $document = $this->project->openDocument('file:///vendor/x.php', "createDocument('file:///vendor/x.php', "assertEquals(true, $document->isVendored()); - $document = $this->project->openDocument('file:///dir/vendor.php', "createDocument('file:///dir/vendor.php', "assertEquals(false, $document->isVendored()); - $document = $this->project->openDocument('file:///dir/x.php', "createDocument('file:///dir/x.php', "assertEquals(false, $document->isVendored()); } } diff --git a/tests/Server/ServerTestCase.php b/tests/Server/ServerTestCase.php index 38e2c8a..602bda0 100644 --- a/tests/Server/ServerTestCase.php +++ b/tests/Server/ServerTestCase.php @@ -5,7 +5,8 @@ namespace LanguageServer\Tests\Server; use PHPUnit\Framework\TestCase; use LanguageServer\Tests\MockProtocolStream; -use LanguageServer\{Server, LanguageClient, Project}; +use LanguageServer\{Server, LanguageClient, PhpDocumentLoader, DefinitionResolver}; +use LanguageServer\Index\{ProjectIndex, StubsIndex, GlobalIndex, DependenciesIndex, Index}; use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\Protocol\{Position, Location, Range, ClientCapabilities}; use function LanguageServer\pathToUri; @@ -24,9 +25,9 @@ abstract class ServerTestCase extends TestCase protected $workspace; /** - * @var Project + * @var PhpDocumentLoader */ - protected $project; + protected $documentLoader; /** * Map from FQN to Location of definition @@ -44,10 +45,13 @@ abstract class ServerTestCase extends TestCase public function setUp() { - $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $this->project = new Project($client, new FileSystemContentRetriever); - $this->textDocument = new Server\TextDocument($this->project, $client); - $this->workspace = new Server\Workspace($this->project, $client); + $projectIndex = new ProjectIndex(new Index, new DependenciesIndex); + + $definitionResolver = new DefinitionResolver($projectIndex); + $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); + $this->documentLoader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver); + $this->textDocument = new Server\TextDocument($this->documentLoader, $definitionResolver, $client, $projectIndex); + $this->workspace = new Server\Workspace($projectIndex, $client); $globalSymbolsUri = pathToUri(realpath(__DIR__ . '/../../fixtures/global_symbols.php')); $globalReferencesUri = pathToUri(realpath(__DIR__ . '/../../fixtures/global_references.php')); @@ -55,11 +59,11 @@ abstract class ServerTestCase extends TestCase $referencesUri = pathToUri(realpath(__DIR__ . '/../../fixtures/references.php')); $useUri = pathToUri(realpath(__DIR__ . '/../../fixtures/use.php')); - $this->project->loadDocument($symbolsUri)->wait(); - $this->project->loadDocument($referencesUri)->wait(); - $this->project->loadDocument($globalSymbolsUri)->wait(); - $this->project->loadDocument($globalReferencesUri)->wait(); - $this->project->loadDocument($useUri)->wait(); + $this->documentLoader->load($symbolsUri)->wait(); + $this->documentLoader->load($referencesUri)->wait(); + $this->documentLoader->load($globalSymbolsUri)->wait(); + $this->documentLoader->load($globalReferencesUri)->wait(); + $this->documentLoader->load($useUri)->wait(); // @codingStandardsIgnoreStart $this->definitionLocations = [ diff --git a/tests/Server/TextDocument/CompletionTest.php b/tests/Server/TextDocument/CompletionTest.php index 587f171..1e48d7b 100644 --- a/tests/Server/TextDocument/CompletionTest.php +++ b/tests/Server/TextDocument/CompletionTest.php @@ -5,7 +5,8 @@ namespace LanguageServer\Tests\Server\TextDocument; use PHPUnit\Framework\TestCase; use LanguageServer\Tests\MockProtocolStream; -use LanguageServer\{Server, LanguageClient, Project, CompletionProvider}; +use LanguageServer\{Server, LanguageClient, PhpDocumentLoader, CompletionProvider, DefinitionResolver}; +use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex, GlobalIndex, StubsIndex}; use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\Protocol\{ TextDocumentIdentifier, @@ -27,23 +28,26 @@ class CompletionTest extends TestCase private $textDocument; /** - * @var Project + * @var PhpDocumentLoader */ - private $project; + private $loader; public function setUp() { $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $this->project = new Project($client, new FileSystemContentRetriever); - $this->project->loadDocument(pathToUri(__DIR__ . '/../../../fixtures/global_symbols.php'))->wait(); - $this->project->loadDocument(pathToUri(__DIR__ . '/../../../fixtures/symbols.php'))->wait(); - $this->textDocument = new Server\TextDocument($this->project, $client); + $projectIndex = new ProjectIndex(new Index, new DependenciesIndex); + $definitionResolver = new DefinitionResolver($projectIndex); + $contentRetriever = new FileSystemContentRetriever; + $this->loader = new PhpDocumentLoader($contentRetriever, $projectIndex, $definitionResolver); + $this->loader->load(pathToUri(__DIR__ . '/../../../fixtures/global_symbols.php'))->wait(); + $this->loader->load(pathToUri(__DIR__ . '/../../../fixtures/symbols.php'))->wait(); + $this->textDocument = new Server\TextDocument($this->loader, $definitionResolver, $client, $projectIndex); } public function testPropertyAndMethodWithPrefix() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/property_with_prefix.php'); - $this->project->openDocument($completionUri, file_get_contents($completionUri)); + $this->loader->open($completionUri, file_get_contents($completionUri)); $items = $this->textDocument->completion( new TextDocumentIdentifier($completionUri), new Position(3, 7) @@ -67,7 +71,7 @@ class CompletionTest extends TestCase public function testPropertyAndMethodWithoutPrefix() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/property.php'); - $this->project->openDocument($completionUri, file_get_contents($completionUri)); + $this->loader->open($completionUri, file_get_contents($completionUri)); $items = $this->textDocument->completion( new TextDocumentIdentifier($completionUri), new Position(3, 6) @@ -91,7 +95,7 @@ class CompletionTest extends TestCase public function testVariable() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/variable.php'); - $this->project->openDocument($completionUri, file_get_contents($completionUri)); + $this->loader->open($completionUri, file_get_contents($completionUri)); $items = $this->textDocument->completion( new TextDocumentIdentifier($completionUri), new Position(8, 5) @@ -123,7 +127,7 @@ class CompletionTest extends TestCase public function testVariableWithPrefix() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/variable_with_prefix.php'); - $this->project->openDocument($completionUri, file_get_contents($completionUri)); + $this->loader->open($completionUri, file_get_contents($completionUri)); $items = $this->textDocument->completion( new TextDocumentIdentifier($completionUri), new Position(8, 6) @@ -145,7 +149,7 @@ class CompletionTest extends TestCase public function testNewInNamespace() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/used_new.php'); - $this->project->openDocument($completionUri, file_get_contents($completionUri)); + $this->loader->open($completionUri, file_get_contents($completionUri)); $items = $this->textDocument->completion( new TextDocumentIdentifier($completionUri), new Position(6, 10) @@ -177,7 +181,7 @@ class CompletionTest extends TestCase public function testUsedClass() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/used_class.php'); - $this->project->openDocument($completionUri, file_get_contents($completionUri)); + $this->loader->open($completionUri, file_get_contents($completionUri)); $items = $this->textDocument->completion( new TextDocumentIdentifier($completionUri), new Position(6, 5) @@ -195,7 +199,7 @@ class CompletionTest extends TestCase public function testStaticPropertyWithPrefix() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/static_property_with_prefix.php'); - $this->project->openDocument($completionUri, file_get_contents($completionUri)); + $this->loader->open($completionUri, file_get_contents($completionUri)); $items = $this->textDocument->completion( new TextDocumentIdentifier($completionUri), new Position(2, 14) @@ -216,7 +220,7 @@ class CompletionTest extends TestCase public function testStaticWithoutPrefix() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/static.php'); - $this->project->openDocument($completionUri, file_get_contents($completionUri)); + $this->loader->open($completionUri, file_get_contents($completionUri)); $items = $this->textDocument->completion( new TextDocumentIdentifier($completionUri), new Position(2, 11) @@ -249,7 +253,7 @@ class CompletionTest extends TestCase public function testStaticMethodWithPrefix() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/static_method_with_prefix.php'); - $this->project->openDocument($completionUri, file_get_contents($completionUri)); + $this->loader->open($completionUri, file_get_contents($completionUri)); $items = $this->textDocument->completion( new TextDocumentIdentifier($completionUri), new Position(2, 13) @@ -282,7 +286,7 @@ class CompletionTest extends TestCase public function testClassConstWithPrefix() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/class_const_with_prefix.php'); - $this->project->openDocument($completionUri, file_get_contents($completionUri)); + $this->loader->open($completionUri, file_get_contents($completionUri)); $items = $this->textDocument->completion( new TextDocumentIdentifier($completionUri), new Position(2, 13) @@ -315,7 +319,7 @@ class CompletionTest extends TestCase public function testFullyQualifiedClass() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/fully_qualified_class.php'); - $this->project->openDocument($completionUri, file_get_contents($completionUri)); + $this->loader->open($completionUri, file_get_contents($completionUri)); $items = $this->textDocument->completion( new TextDocumentIdentifier($completionUri), new Position(6, 6) @@ -336,7 +340,7 @@ class CompletionTest extends TestCase public function testKeywords() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/keywords.php'); - $this->project->openDocument($completionUri, file_get_contents($completionUri)); + $this->loader->open($completionUri, file_get_contents($completionUri)); $items = $this->textDocument->completion( new TextDocumentIdentifier($completionUri), new Position(2, 1) @@ -350,7 +354,7 @@ class CompletionTest extends TestCase public function testHtmlWithoutPrefix() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html.php'); - $this->project->openDocument($completionUri, file_get_contents($completionUri)); + $this->loader->open($completionUri, file_get_contents($completionUri)); $items = $this->textDocument->completion( new TextDocumentIdentifier($completionUri), new Position(0, 0) @@ -372,7 +376,7 @@ class CompletionTest extends TestCase public function testHtmlWithPrefix() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html_with_prefix.php'); - $this->project->openDocument($completionUri, file_get_contents($completionUri)); + $this->loader->open($completionUri, file_get_contents($completionUri)); $items = $this->textDocument->completion( new TextDocumentIdentifier($completionUri), new Position(0, 1) @@ -394,7 +398,7 @@ class CompletionTest extends TestCase public function testNamespace() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/namespace.php'); - $this->project->openDocument($completionUri, file_get_contents($completionUri)); + $this->loader->open($completionUri, file_get_contents($completionUri)); $items = $this->textDocument->completion( new TextDocumentIdentifier($completionUri), new Position(4, 6) diff --git a/tests/Server/TextDocument/Definition/GlobalFallbackTest.php b/tests/Server/TextDocument/Definition/GlobalFallbackTest.php index 20ea70b..4c09e18 100644 --- a/tests/Server/TextDocument/Definition/GlobalFallbackTest.php +++ b/tests/Server/TextDocument/Definition/GlobalFallbackTest.php @@ -5,7 +5,8 @@ namespace LanguageServer\Tests\Server\TextDocument\Definition; use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\Server\ServerTestCase; -use LanguageServer\{Server, LanguageClient, Project}; +use LanguageServer\{Server, LanguageClient, PhpDocumentLoader, DefinitionResolver}; +use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex}; use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\Protocol\{TextDocumentIdentifier, Position, Range, Location, ClientCapabilities}; use Sabre\Event\Promise; @@ -14,11 +15,14 @@ class GlobalFallbackTest extends ServerTestCase { public function setUp() { + $projectIndex = new ProjectIndex(new Index, new DependenciesIndex); $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $project = new Project($client, new FileSystemContentRetriever); - $this->textDocument = new Server\TextDocument($project, $client); - $project->openDocument('global_fallback', file_get_contents(__DIR__ . '/../../../../fixtures/global_fallback.php')); - $project->openDocument('global_symbols', file_get_contents(__DIR__ . '/../../../../fixtures/global_symbols.php')); + $definitionResolver = new DefinitionResolver($projectIndex); + $contentRetriever = new FileSystemContentRetriever; + $loader = new PhpDocumentLoader($contentRetriever, $projectIndex, $definitionResolver); + $this->textDocument = new Server\TextDocument($loader, $definitionResolver, $client, $projectIndex); + $loader->open('global_fallback', file_get_contents(__DIR__ . '/../../../../fixtures/global_fallback.php')); + $loader->open('global_symbols', file_get_contents(__DIR__ . '/../../../../fixtures/global_symbols.php')); } public function testClassDoesNotFallback() diff --git a/tests/Server/TextDocument/DidChangeTest.php b/tests/Server/TextDocument/DidChangeTest.php index 9df301a..bdd3b22 100644 --- a/tests/Server/TextDocument/DidChangeTest.php +++ b/tests/Server/TextDocument/DidChangeTest.php @@ -5,8 +5,9 @@ namespace LanguageServer\Tests\Server\TextDocument; use PHPUnit\Framework\TestCase; use LanguageServer\Tests\MockProtocolStream; -use LanguageServer\{Server, Client, LanguageClient, Project}; +use LanguageServer\{Server, Client, LanguageClient, PhpDocumentLoader, DefinitionResolver}; use LanguageServer\ContentRetriever\FileSystemContentRetriever; +use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex}; use LanguageServer\Protocol\{ TextDocumentIdentifier, TextDocumentItem, @@ -21,10 +22,12 @@ class DidChangeTest extends TestCase { public function test() { + $projectIndex = new ProjectIndex(new Index, new DependenciesIndex); $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $project = new Project($client, new FileSystemContentRetriever); - $textDocument = new Server\TextDocument($project, $client); - $phpDocument = $project->openDocument('whatever', "open('whatever', "openDocument('whatever', 'hello world'); + $definitionResolver = new DefinitionResolver($projectIndex); + $loader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver); + $textDocument = new Server\TextDocument($loader, $definitionResolver, $client, $projectIndex); + $phpDocument = $loader->open('whatever', "uri = 'whatever'; @@ -28,6 +31,6 @@ class DidCloseTest extends TestCase $textDocument->didClose(new TextDocumentIdentifier($textDocumentItem->uri)); - $this->assertFalse($project->isDocumentOpen($textDocumentItem->uri)); + $this->assertFalse($loader->isOpen($textDocumentItem->uri)); } } diff --git a/tests/Server/TextDocument/FormattingTest.php b/tests/Server/TextDocument/FormattingTest.php index 32aee22..abb3d6d 100644 --- a/tests/Server/TextDocument/FormattingTest.php +++ b/tests/Server/TextDocument/FormattingTest.php @@ -5,7 +5,8 @@ namespace LanguageServer\Tests\Server\TextDocument; use PHPUnit\Framework\TestCase; use LanguageServer\Tests\MockProtocolStream; -use LanguageServer\{Server, Client, LanguageClient, Project}; +use LanguageServer\{Server, Client, LanguageClient, PhpDocumentLoader, DefinitionResolver}; +use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex}; use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\Protocol\{ TextDocumentIdentifier, @@ -20,23 +21,14 @@ use function LanguageServer\{pathToUri, uriToPath}; class FormattingTest extends TestCase { - /** - * @var Server\TextDocument - */ - private $textDocument; - - public function setUp() - { - $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $project = new Project($client, new FileSystemContentRetriever); - $this->textDocument = new Server\TextDocument($project, $client); - } - public function testFormatting() { + $projectIndex = new ProjectIndex(new Index, new DependenciesIndex); $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $project = new Project($client, new FileSystemContentRetriever); - $textDocument = new Server\TextDocument($project, $client); + $definitionResolver = new DefinitionResolver($projectIndex); + $loader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver); + $textDocument = new Server\TextDocument($loader, $definitionResolver, $client, $projectIndex); + $path = realpath(__DIR__ . '/../../../fixtures/format.php'); $uri = pathToUri($path); diff --git a/tests/Server/TextDocument/ParseErrorsTest.php b/tests/Server/TextDocument/ParseErrorsTest.php index af2bdd8..6c927f8 100644 --- a/tests/Server/TextDocument/ParseErrorsTest.php +++ b/tests/Server/TextDocument/ParseErrorsTest.php @@ -5,7 +5,8 @@ namespace LanguageServer\Tests\Server\TextDocument; use PHPUnit\Framework\TestCase; use LanguageServer\Tests\MockProtocolStream; -use LanguageServer\{Server, Client, LanguageClient, Project, ClientHandler}; +use LanguageServer\{Server, Client, LanguageClient, ClientHandler, PhpDocumentLoader, DefinitionResolver}; +use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex}; use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\Protocol\{TextDocumentIdentifier, TextDocumentItem, DiagnosticSeverity, ClientCapabilities}; use Sabre\Event\Promise; @@ -36,8 +37,10 @@ class ParseErrorsTest extends TestCase return Promise\resolve(null); } }; - $project = new Project($client, new FileSystemContentRetriever); - $this->textDocument = new Server\TextDocument($project, $client); + $projectIndex = new ProjectIndex(new Index, new DependenciesIndex); + $definitionResolver = new DefinitionResolver($projectIndex); + $loader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver); + $this->textDocument = new Server\TextDocument($loader, $definitionResolver, $client, $projectIndex); } private function openFile($file) diff --git a/tests/Server/TextDocument/References/GlobalFallbackTest.php b/tests/Server/TextDocument/References/GlobalFallbackTest.php index 4e6d07a..2679d15 100644 --- a/tests/Server/TextDocument/References/GlobalFallbackTest.php +++ b/tests/Server/TextDocument/References/GlobalFallbackTest.php @@ -5,7 +5,8 @@ namespace LanguageServer\Tests\Server\TextDocument\References; use PHPUnit\Framework\TestCase; use LanguageServer\Tests\MockProtocolStream; -use LanguageServer\{Server, LanguageClient, Project}; +use LanguageServer\{Server, LanguageClient, PhpDocumentLoader, DefinitionResolver}; +use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex}; use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\Protocol\{TextDocumentIdentifier, Position, ReferenceContext, Location, Range, ClientCapabilities}; use LanguageServer\Tests\Server\ServerTestCase; @@ -14,11 +15,13 @@ class GlobalFallbackTest extends ServerTestCase { public function setUp() { - $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $project = new Project($client, new FileSystemContentRetriever); - $this->textDocument = new Server\TextDocument($project, $client); - $project->openDocument('global_fallback', file_get_contents(__DIR__ . '/../../../../fixtures/global_fallback.php')); - $project->openDocument('global_symbols', file_get_contents(__DIR__ . '/../../../../fixtures/global_symbols.php')); + $projectIndex = new ProjectIndex(new Index, new DependenciesIndex); + $definitionResolver = new DefinitionResolver($projectIndex); + $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); + $this->documentLoader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver); + $this->textDocument = new Server\TextDocument($this->documentLoader, $definitionResolver, $client, $projectIndex); + $this->documentLoader->open('global_fallback', file_get_contents(__DIR__ . '/../../../../fixtures/global_fallback.php')); + $this->documentLoader->open('global_symbols', file_get_contents(__DIR__ . '/../../../../fixtures/global_symbols.php')); } public function testClassDoesNotFallback() From d7fc9e0425a3e79faa00b1bcfcfd063a5e7c2714 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Tue, 13 Dec 2016 02:11:29 +0100 Subject: [PATCH 05/10] Index twice to collect dynamic references (#206) --- src/LanguageServer.php | 69 ++++++++++++++++++------------------ tests/LanguageServerTest.php | 10 ++++-- 2 files changed, 42 insertions(+), 37 deletions(-) diff --git a/src/LanguageServer.php b/src/LanguageServer.php index 2a5a716..f1706e9 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -238,42 +238,43 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher $startTime = microtime(true); - // Parse PHP files - foreach ($uris as $i => $uri) { - if ($this->documentLoader->isOpen($uri)) { - continue; - } - - // Give LS to the chance to handle requests while indexing - yield timeout(); - $this->client->window->logMessage( - MessageType::LOG, - "Parsing file $i/$count: {$uri}" - ); - try { - $document = yield $this->documentLoader->load($uri); - if (!$document->isVendored()) { - $this->client->textDocument->publishDiagnostics($uri, $document->getDiagnostics()); + foreach (['Collecting definitions and static references', 'Collecting dynamic references'] as $run) { + $this->client->window->logMessage(MessageType::INFO, $run); + foreach ($uris as $i => $uri) { + if ($this->documentLoader->isOpen($uri)) { + continue; } - } catch (ContentTooLargeException $e) { - $this->client->window->logMessage( - MessageType::INFO, - "Ignoring file {$uri} because it exceeds size limit of {$e->limit} bytes ({$e->size})" - ); - } catch (Exception $e) { - $this->client->window->logMessage( - MessageType::ERROR, - "Error parsing file {$uri}: " . (string)$e - ); - } - } - $duration = (int)(microtime(true) - $startTime); - $mem = (int)(memory_get_usage(true) / (1024 * 1024)); - $this->client->window->logMessage( - MessageType::INFO, - "All $count PHP files parsed in $duration seconds. $mem MiB allocated." - ); + // Give LS to the chance to handle requests while indexing + yield timeout(); + $this->client->window->logMessage( + MessageType::LOG, + "Parsing file $i/$count: {$uri}" + ); + try { + $document = yield $this->documentLoader->load($uri); + if (!$document->isVendored()) { + $this->client->textDocument->publishDiagnostics($uri, $document->getDiagnostics()); + } + } catch (ContentTooLargeException $e) { + $this->client->window->logMessage( + MessageType::INFO, + "Ignoring file {$uri} because it exceeds size limit of {$e->limit} bytes ({$e->size})" + ); + } catch (Exception $e) { + $this->client->window->logMessage( + MessageType::ERROR, + "Error parsing file {$uri}: " . (string)$e + ); + } + } + $duration = (int)(microtime(true) - $startTime); + $mem = (int)(memory_get_usage(true) / (1024 * 1024)); + $this->client->window->logMessage( + MessageType::INFO, + "All $count PHP files parsed in $duration seconds. $mem MiB allocated." + ); + } }); } } diff --git a/tests/LanguageServerTest.php b/tests/LanguageServerTest.php index d9f0d81..6f8e705 100644 --- a/tests/LanguageServerTest.php +++ b/tests/LanguageServerTest.php @@ -73,7 +73,8 @@ class LanguageServerTest extends TestCase $rootPath = realpath(__DIR__ . '/../fixtures'); $input = new MockProtocolStream; $output = new MockProtocolStream; - $output->on('message', function (Message $msg) use ($promise, $input, $rootPath, &$filesCalled, &$contentCalled) { + $run = 1; + $output->on('message', function (Message $msg) use ($promise, $input, $rootPath, &$filesCalled, &$contentCalled, &$run) { if ($msg->body->method === 'textDocument/xcontent') { // Document content requested $contentCalled = true; @@ -100,8 +101,11 @@ class LanguageServerTest extends TestCase $promise->reject(new Exception($msg->body->params->message)); } } else if (strpos($msg->body->params->message, 'All 25 PHP files parsed') !== false) { - // Indexing finished - $promise->fulfill(); + if ($run === 1) { + $run++; + } else { + $promise->fulfill(); + } } } }); From d90a88e625157bc820c59e679f5bf1b642f49448 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Tue, 13 Dec 2016 02:13:57 +0100 Subject: [PATCH 06/10] Add missing property declaration --- src/NodeVisitor/ReferencesCollector.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/NodeVisitor/ReferencesCollector.php b/src/NodeVisitor/ReferencesCollector.php index 7e35beb..ae3027e 100644 --- a/src/NodeVisitor/ReferencesCollector.php +++ b/src/NodeVisitor/ReferencesCollector.php @@ -19,6 +19,11 @@ class ReferencesCollector extends NodeVisitorAbstract */ public $nodes = []; + /** + * @var DefinitionResolver + */ + private $definitionResolver; + /** * @param DefinitionResolver $definitionResolver The DefinitionResolver to resolve reference nodes to definitions */ From b8a113ddd0cf798d48727a82b24781c3369d4fc5 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Tue, 13 Dec 2016 02:40:39 +0100 Subject: [PATCH 07/10] Correct reference collection for New_ nodes Closes #202 --- fixtures/references.php | 2 +- src/DefinitionResolver.php | 8 +------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/fixtures/references.php b/fixtures/references.php index 2ac1a63..4a34698 100644 --- a/fixtures/references.php +++ b/fixtures/references.php @@ -2,7 +2,7 @@ namespace TestNamespace; -$obj = new TestClass(); +$obj = new TestClass($a, $b, $c); $obj->testMethod(); echo $obj->testProperty; TestClass::staticTestMethod(); diff --git a/src/DefinitionResolver.php b/src/DefinitionResolver.php index a2f30e4..e2b7212 100644 --- a/src/DefinitionResolver.php +++ b/src/DefinitionResolver.php @@ -195,6 +195,7 @@ class DefinitionResolver || $parent instanceof Node\Namespace_ || $parent instanceof Node\Param || $parent instanceof Node\FunctionLike + || $parent instanceof Node\Expr\New_ || $parent instanceof Node\Expr\StaticCall || $parent instanceof Node\Expr\ClassConstFetch || $parent instanceof Node\Expr\StaticPropertyFetch @@ -212,13 +213,6 @@ class DefinitionResolver } else if ($grandParent instanceof Node\Stmt\Use_ && $grandParent->type === Node\Stmt\Use_::TYPE_FUNCTION) { $name .= '()'; } - // Only the name node should be considered a reference, not the New_ node itself - } else if ($parent instanceof Node\Expr\New_) { - if (!($parent->class instanceof Node\Name)) { - // Cannot get definition of dynamic calls - return null; - } - $name = (string)$parent->class; } else if ($node instanceof Node\Expr\MethodCall || $node instanceof Node\Expr\PropertyFetch) { if ($node->name instanceof Node\Expr) { // Cannot get definition if right-hand side is expression From 96ea8608d77ae335a3447cea0aaefa14278049f8 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Tue, 13 Dec 2016 02:53:01 +0100 Subject: [PATCH 08/10] Support getting references from a reference Closes #201 --- src/Server/TextDocument.php | 5 ++++- tests/Server/TextDocument/References/GlobalTest.php | 13 +++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Server/TextDocument.php b/src/Server/TextDocument.php index 84103df..d5c9323 100644 --- a/src/Server/TextDocument.php +++ b/src/Server/TextDocument.php @@ -209,7 +209,10 @@ class TextDocument // Definition with a global FQN $fqn = DefinitionResolver::getDefinedFqn($node); if ($fqn === null) { - return []; + $fqn = $this->definitionResolver->resolveReferenceNodeToFqn($node); + if ($fqn === null) { + return []; + } } $refDocuments = yield Promise\all(array_map( [$this->documentLoader, 'getOrLoad'], diff --git a/tests/Server/TextDocument/References/GlobalTest.php b/tests/Server/TextDocument/References/GlobalTest.php index bf6eb97..4febaf3 100644 --- a/tests/Server/TextDocument/References/GlobalTest.php +++ b/tests/Server/TextDocument/References/GlobalTest.php @@ -146,4 +146,17 @@ class GlobalTest extends ServerTestCase new Location($referencesUri, new Range(new Position(31, 13), new Position(31, 40))) ], $result); } + + public function testReferencesForReference() + { + // $obj = new TestClass(); + // Get references for TestClass + $reference = $this->getReferenceLocations('TestClass')[0]; + $result = $this->textDocument->references( + new ReferenceContext, + new TextDocumentIdentifier($reference->uri), + $reference->range->start + )->wait(); + $this->assertEquals($this->getReferenceLocations('TestClass'), $result); + } } From 0b61951a9c29c59cc0f787b6fda2146b242a0b47 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Tue, 13 Dec 2016 03:08:52 +0100 Subject: [PATCH 09/10] Support hover for definitions #201 --- src/Server/TextDocument.php | 9 +++++++-- tests/Server/TextDocument/HoverTest.php | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/Server/TextDocument.php b/src/Server/TextDocument.php index d5c9323..1d58efc 100644 --- a/src/Server/TextDocument.php +++ b/src/Server/TextDocument.php @@ -276,8 +276,13 @@ class TextDocument return new Hover([]); } $range = Range::fromNode($node); - // Get the definition for whatever node is under the cursor - $def = $this->definitionResolver->resolveReferenceNodeToDefinition($node); + if ($definedFqn = DefinitionResolver::getDefinedFqn($node)) { + // Support hover for definitions + $def = $this->index->getDefinition($definedFqn); + } else { + // Get the definition for whatever node is under the cursor + $def = $this->definitionResolver->resolveReferenceNodeToDefinition($node); + } if ($def === null) { return new Hover([], $range); } diff --git a/tests/Server/TextDocument/HoverTest.php b/tests/Server/TextDocument/HoverTest.php index 80cef64..9f33268 100644 --- a/tests/Server/TextDocument/HoverTest.php +++ b/tests/Server/TextDocument/HoverTest.php @@ -26,6 +26,21 @@ class HoverTest extends ServerTestCase ], $reference->range), $result); } + public function testHoverForClassLikeDefinition() + { + // class TestClass implements TestInterface + // Get hover for TestClass + $definition = $this->getDefinitionLocation('TestClass'); + $result = $this->textDocument->hover( + new TextDocumentIdentifier($definition->uri), + $definition->range->start + )->wait(); + $this->assertEquals(new Hover([ + new MarkedString('php', "range), $result); + } + public function testHoverForMethod() { // $obj->testMethod(); From cd116a252b7fa83afbbf2a367376394e3a86d90e Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Tue, 13 Dec 2016 10:07:11 +0100 Subject: [PATCH 10/10] Update php-parser --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 898da3b..588d4a0 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ }, "require": { "php": ">=7.0", - "nikic/php-parser": "dev-master#e52ffc4447e034514339a03b450aab9cd625e37c", + "nikic/php-parser": "^3.0", "phpdocumentor/reflection-docblock": "^3.0", "sabre/event": "^5.0", "felixfbecker/advanced-json-rpc": "^2.0",