From 6fe01183b0158e263706cad0e59910bdffaa423e Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Wed, 12 Oct 2016 01:45:15 +0200 Subject: [PATCH] References (#52) * Adds support for textDocument/references * Adds tests for global definitions and global fallback --- composer.json | 2 +- fixtures/global_fallback.php | 10 + fixtures/global_references.php | 24 ++ fixtures/global_symbols.php | 53 +++ src/LanguageServer.php | 2 + src/NodeVisitor/ReferencesCollector.php | 61 +++ .../VariableReferencesCollector.php | 50 +++ src/PhpDocument.php | 177 ++++++--- src/Project.php | 40 +- src/Server/TextDocument.php | 25 +- tests/LanguageServerTest.php | 2 +- .../Definition/GlobalFallbackTest.php | 74 ++++ .../TextDocument/Definition/GlobalTest.php | 321 +++++++++++++++ .../NamespacedTest.php} | 11 +- .../References/GlobalFallbackTest.php | 78 ++++ .../TextDocument/References/GlobalTest.php | 349 +++++++++++++++++ .../References/NamespacedTest.php | 366 ++++++++++++++++++ 17 files changed, 1588 insertions(+), 57 deletions(-) create mode 100644 fixtures/global_fallback.php create mode 100644 fixtures/global_references.php create mode 100644 fixtures/global_symbols.php create mode 100644 src/NodeVisitor/ReferencesCollector.php create mode 100644 src/NodeVisitor/VariableReferencesCollector.php create mode 100644 tests/Server/TextDocument/Definition/GlobalFallbackTest.php create mode 100644 tests/Server/TextDocument/Definition/GlobalTest.php rename tests/Server/TextDocument/{DefinitionTest.php => Definition/NamespacedTest.php} (97%) create mode 100644 tests/Server/TextDocument/References/GlobalFallbackTest.php create mode 100644 tests/Server/TextDocument/References/GlobalTest.php create mode 100644 tests/Server/TextDocument/References/NamespacedTest.php diff --git a/composer.json b/composer.json index 24a9d32..023b021 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "bin": ["bin/php-language-server.php"], "require": { "php": ">=7.0", - "nikic/php-parser": "^3.0.0beta1", + "nikic/php-parser": "dev-master#90834bff8eaf7b7f893253f312e73d8f532341ca", "phpdocumentor/reflection-docblock": "^3.0", "sabre/event": "^4.0", "felixfbecker/advanced-json-rpc": "^1.2", diff --git a/fixtures/global_fallback.php b/fixtures/global_fallback.php new file mode 100644 index 0000000..90917bb --- /dev/null +++ b/fixtures/global_fallback.php @@ -0,0 +1,10 @@ +testMethod(); +echo $obj->testProperty; +TestClass::staticTestMethod(); +echo TestClass::$staticTestProperty; +echo TestClass::TEST_CLASS_CONST; +test_function(); + +$var = 123; +echo $var; + +function whatever(TestClass $param): TestClass { + echo $param; +} + +$fn = function() use ($var) { + echo $var; +}; + +echo TEST_CONST; diff --git a/fixtures/global_symbols.php b/fixtures/global_symbols.php new file mode 100644 index 0000000..a8e2765 --- /dev/null +++ b/fixtures/global_symbols.php @@ -0,0 +1,53 @@ +documentFormattingProvider = true; // Support "Go to definition" $serverCapabilities->definitionProvider = true; + // Support "Find all references" + $serverCapabilities->referencesProvider = true; return new InitializeResult($serverCapabilities); } diff --git a/src/NodeVisitor/ReferencesCollector.php b/src/NodeVisitor/ReferencesCollector.php new file mode 100644 index 0000000..e02904c --- /dev/null +++ b/src/NodeVisitor/ReferencesCollector.php @@ -0,0 +1,61 @@ +getAttribute('ownerDocument')->getReferencedFqn($node); + if ($fqn) { + $this->addReference($fqn, $node); + // Namespaced constant access and function calls also need to register a reference + // to the global version because PHP falls back to global at runtime + // http://php.net/manual/en/language.namespaces.fallback.php + $parent = $node->getAttribute('parentNode'); + if ($parent instanceof Node\Expr\ConstFetch || $parent instanceof Node\Expr\FuncCall) { + $parts = explode('\\', $fqn); + if (count($parts) > 1) { + $globalFqn = end($parts); + $this->addReference($globalFqn, $node); + } + } + // Namespaced constant references and function calls also need to register a reference to the global + // Static method calls, constant and property fetches also need to register a reference to the class + // A reference like TestNamespace\TestClass::myStaticMethod() registers a reference for + // - TestNamespace\TestClass + // - TestNamespace\TestClass::myStaticMethod() + if ( + ($node instanceof Node\Expr\StaticCall + || $node instanceof Node\Expr\StaticPropertyFetch + || $node instanceof Node\Expr\ClassConstFetch) + && $node->class instanceof Node\Name + ) { + $this->addReference((string)$node->class, $node->class); + } + } + } + + private function addReference(string $fqn, Node $node) + { + if (!isset($this->references[$fqn])) { + $this->references[$fqn] = []; + } + $this->references[$fqn][] = $node; + } +} diff --git a/src/NodeVisitor/VariableReferencesCollector.php b/src/NodeVisitor/VariableReferencesCollector.php new file mode 100644 index 0000000..a113a7e --- /dev/null +++ b/src/NodeVisitor/VariableReferencesCollector.php @@ -0,0 +1,50 @@ +name = $name; + } + + public function enterNode(Node $node) + { + if ($node instanceof Node\Expr\Variable && $node->name === $this->name) { + $this->references[] = $node; + } else if ($node instanceof Node\FunctionLike) { + // If we meet a function node, dont traverse its statements, they are in another scope + // except it is a closure that has imported the variable through use + if ($node instanceof Node\Expr\Closure) { + foreach ($node->uses as $use) { + if ($use->var === $this->name) { + return; + } + } + } + return NodeTraverser::DONT_TRAVERSE_CHILDREN; + } + } +} diff --git a/src/PhpDocument.php b/src/PhpDocument.php index c231f03..9a716de 100644 --- a/src/PhpDocument.php +++ b/src/PhpDocument.php @@ -4,7 +4,14 @@ declare(strict_types = 1); namespace LanguageServer; use LanguageServer\Protocol\{Diagnostic, DiagnosticSeverity, Range, Position, TextEdit}; -use LanguageServer\NodeVisitor\{NodeAtPositionFinder, ReferencesAdder, DefinitionCollector, ColumnCalculator}; +use LanguageServer\NodeVisitor\{ + NodeAtPositionFinder, + ReferencesAdder, + DefinitionCollector, + ColumnCalculator, + ReferencesCollector, + VariableReferencesCollector +}; use PhpParser\{Error, Node, NodeTraverser, Parser}; use PhpParser\NodeVisitor\NameResolver; @@ -52,7 +59,7 @@ class PhpDocument * * @var Node[] */ - private $statements; + private $stmts; /** * Map from fully qualified name (FQN) to Node @@ -85,6 +92,18 @@ class PhpDocument } /** + * Get all references of a fully qualified name + * + * @param string $fqn The fully qualified name of the symbol + * @return Node[] + */ + public function getReferencesByFqn(string $fqn) + { + return isset($this->references) && isset($this->references[$fqn]) ? $this->references[$fqn] : null; + } + + /** + * Updates the content on this document. * Re-parses a source file, updates symbols and reports parsing errors * that may have occured as diagnostics. * @@ -135,19 +154,32 @@ class PhpDocument // Add column attributes to nodes $traverser->addVisitor(new ColumnCalculator($content)); + $traverser->traverse($stmts); + $traverser = new NodeTraverser; + // Collect all definitions $definitionCollector = new DefinitionCollector; $traverser->addVisitor($definitionCollector); + // Collect all references + $referencesCollector = new ReferencesCollector($this->definitions); + $traverser->addVisitor($referencesCollector); + $traverser->traverse($stmts); // Register this document on the project for all the symbols defined in it + $this->definitions = $definitionCollector->definitions; foreach ($definitionCollector->definitions as $fqn => $node) { $this->project->setDefinitionUri($fqn, $this->uri); } - $this->statements = $stmts; - $this->definitions = $definitionCollector->definitions; + // Register this document on the project for references + $this->references = $referencesCollector->references; + foreach ($referencesCollector->references as $fqn => $nodes) { + $this->project->addReferenceUri($fqn, $this->uri); + } + + $this->stmts = $stmts; } } @@ -184,6 +216,16 @@ class PhpDocument return $this->uri; } + /** + * Returns the AST of the document + * + * @return Node[] + */ + public function getStmts(): array + { + return $this->stmts; + } + /** * Returns the node at a specified position * @@ -195,7 +237,7 @@ class PhpDocument $traverser = new NodeTraverser; $finder = new NodeAtPositionFinder($position); $traverser->addVisitor($finder); - $traverser->traverse($this->statements); + $traverser->traverse($this->stmts); return $finder->node; } @@ -295,33 +337,31 @@ class PhpDocument */ public function getReferencedFqn(Node $node) { - if ($node instanceof Node\Name) { - $nameNode = $node; - $node = $node->getAttribute('parentNode'); - } + $parent = $node->getAttribute('parentNode'); if ( - ($node instanceof Node\Stmt\ClassLike - || $node instanceof Node\Param - || $node instanceof Node\Stmt\Function_) - && isset($nameNode) + $node instanceof Node\Name && ( + $parent instanceof Node\Stmt\ClassLike + || $parent instanceof Node\Param + || $parent instanceof Node\Stmt\Function_ + ) ) { // For extends, implements and type hints use the name directly - $name = (string)$nameNode; + $name = (string)$node; // Only the name node should be considered a reference, not the UseUse node itself - } else if ($node instanceof Node\Stmt\UseUse && isset($nameNode)) { - $name = (string)$node->name; - $parent = $node->getAttribute('parentNode'); - if ($parent instanceof Node\Stmt\GroupUse) { - $name = $parent->prefix . '\\' . $name; + } else if ($parent instanceof Node\Stmt\UseUse) { + $name = (string)$parent->name; + $grandParent = $parent->getAttribute('parentNode'); + if ($grandParent instanceof Node\Stmt\GroupUse) { + $name = $grandParent->prefix . '\\' . $name; } // Only the name node should be considered a reference, not the New_ node itself - } else if ($node instanceof Node\Expr\New_ && isset($nameNode)) { - if (!($node->class instanceof Node\Name)) { + } else if ($parent instanceof Node\Expr\New_) { + if (!($parent->class instanceof Node\Name)) { // Cannot get definition of dynamic calls return null; } - $name = (string)$node->class; + $name = (string)$parent->class; } else if ($node instanceof Node\Expr\MethodCall || $node instanceof Node\Expr\PropertyFetch) { if ($node->name instanceof Node\Expr || !($node->var instanceof Node\Expr\Variable)) { // Cannot get definition of dynamic calls @@ -353,13 +393,13 @@ class PhpDocument return null; } $name .= '::' . (string)$node->name; - } else if ($node instanceof Node\Expr\FuncCall) { - if ($node->name instanceof Node\Expr) { + } else if ($parent instanceof Node\Expr\FuncCall) { + if ($parent->name instanceof Node\Expr) { return null; } - $name = (string)$node->name; - } else if ($node instanceof Node\Expr\ConstFetch) { - $name = (string)$node->name; + $name = (string)($node->getAttribute('namespacedName') ?? $parent->name); + } else if ($parent instanceof Node\Expr\ConstFetch) { + $name = (string)($node->getAttribute('namespacedName') ?? $parent->name); } else if ( $node instanceof Node\Expr\ClassConstFetch || $node instanceof Node\Expr\StaticPropertyFetch @@ -370,35 +410,19 @@ class PhpDocument return null; } $name = (string)$node->class . '::' . $node->name; + } else { + return null; } if ( $node instanceof Node\Expr\MethodCall - || $node instanceof Node\Expr\FuncCall || $node instanceof Node\Expr\StaticCall + || $parent instanceof Node\Expr\FuncCall ) { $name .= '()'; } if (!isset($name)) { return null; } - // If the node is a function or constant, it could be namespaced, but PHP falls back to global - // The NameResolver therefor does not resolve these to namespaced names - // http://php.net/manual/en/language.namespaces.fallback.php - if ($node instanceof Node\Expr\FuncCall || $node instanceof Node\Expr\ConstFetch) { - // Find and try with namespace - $n = $node; - while (isset($n)) { - $n = $n->getAttribute('parentNode'); - if ($n instanceof Node\Stmt\Namespace_) { - $namespacedName = (string)$n->name . '\\' . $name; - // If the namespaced version is defined, return that - // Otherwise fall back to global - if ($this->project->isDefined($namespacedName)) { - return $namespacedName; - } - } - } - } return $name; } @@ -421,12 +445,69 @@ class PhpDocument return null; } $document = $this->project->getDefinitionDocument($fqn); + if (!isset($document)) { + // If the node is a function or constant, it could be namespaced, but PHP falls back to global + // http://php.net/manual/en/language.namespaces.fallback.php + $parent = $node->getAttribute('parentNode'); + if ($parent instanceof Node\Expr\ConstFetch || $parent instanceof Node\Expr\FuncCall) { + $parts = explode('\\', $fqn); + $fqn = end($parts); + $document = $this->project->getDefinitionDocument($fqn); + } + } if (!isset($document)) { return null; } return $document->getDefinitionByFqn($fqn); } + /** + * Returns the reference nodes for any node + * The references node MAY be in other documents, check the ownerDocument attribute + * + * @param Node $node + * @return Node[] + */ + public function getReferencesByNode(Node $node) + { + // Variables always stay in the boundary of the file and need to be searched inside their function scope + // by traversing the AST + if ($node instanceof Node\Expr\Variable || $node instanceof Node\Param) { + if ($node->name instanceof Node\Expr) { + return null; + } + // Find function/method/closure scope + $n = $node; + while (isset($n) && !($n instanceof Node\FunctionLike)) { + $n = $n->getAttribute('parentNode'); + } + if (!isset($n)) { + $n = $node->getAttribute('ownerDocument'); + } + $traverser = new NodeTraverser; + $refCollector = new VariableReferencesCollector($node->name); + $traverser->addVisitor($refCollector); + $traverser->traverse($n->getStmts()); + return $refCollector->references; + } + // Definition with a global FQN + $fqn = $this->getDefinedFqn($node); + if ($fqn === null) { + return []; + } + $refDocuments = $this->project->getReferenceDocuments($fqn); + $nodes = []; + foreach ($refDocuments as $document) { + $refs = $document->getReferencesByFqn($fqn); + if ($refs !== null) { + foreach ($refs as $ref) { + $nodes[] = $ref; + } + } + } + return $nodes; + } + /** * Returns the assignment or parameter node where a variable was defined * @@ -437,7 +518,7 @@ class PhpDocument { $n = $var; // Traverse the AST up - while (isset($n) && $n = $n->getAttribute('parentNode')) { + do { // If a function is met, check the parameters and use statements if ($n instanceof Node\FunctionLike) { foreach ($n->getParams() as $param) { @@ -461,7 +542,7 @@ class PhpDocument return $n; } } - } + } while (isset($n) && $n = $n->getAttribute('parentNode')); // Return null if nothing was found return null; } diff --git a/src/Project.php b/src/Project.php index b6204d5..06b3a08 100644 --- a/src/Project.php +++ b/src/Project.php @@ -16,12 +16,19 @@ class Project private $documents = []; /** - * An associative array that maps fully qualified symbol names to document URIs + * An associative array that maps fully qualified symbol names to document URIs that define the symbol * * @var string[] */ 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 * @@ -143,6 +150,37 @@ class Project $this->definitions[$fqn] = $uri; } + /** + * 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; + } + } + + /** + * Returns all documents that reference a symbol + * + * @param string $fqn The fully qualified name of the symbol + * @return PhpDocument[] + */ + public function getReferenceDocuments(string $fqn) + { + if (!isset($this->references[$fqn])) { + return []; + } + return array_map([$this, 'getDocument'], $this->references[$fqn]); + } + /** * Returns the document where a symbol is defined * diff --git a/src/Server/TextDocument.php b/src/Server/TextDocument.php index aead8d6..3d14730 100644 --- a/src/Server/TextDocument.php +++ b/src/Server/TextDocument.php @@ -12,7 +12,8 @@ use LanguageServer\Protocol\{ FormattingOptions, TextEdit, Location, - SymbolInformation + SymbolInformation, + ReferenceContext }; /** @@ -104,6 +105,28 @@ class TextDocument return $this->project->getDocument($textDocument->uri)->getFormattedText(); } + /** + * The references request is sent from the client to the server to resolve project-wide references for the symbol + * denoted by the given text document position. + * + * @param ReferenceContext $context + * @return Location[] + */ + public function references(ReferenceContext $context, TextDocumentIdentifier $textDocument, Position $position): array + { + $document = $this->project->getDocument($textDocument->uri); + $node = $document->getNodeAtPosition($position); + if ($node === null) { + return []; + } + $refs = $document->getReferencesByNode($node); + $locations = []; + foreach ($refs as $ref) { + $locations[] = Location::fromNode($ref); + } + return $locations; + } + /** * The goto definition request is sent from the client to the server to resolve the definition location of a symbol * at a given text document position. diff --git a/tests/LanguageServerTest.php b/tests/LanguageServerTest.php index 521d602..06742d8 100644 --- a/tests/LanguageServerTest.php +++ b/tests/LanguageServerTest.php @@ -35,7 +35,7 @@ class LanguageServerTest extends TestCase 'completionProvider' => null, 'signatureHelpProvider' => null, 'definitionProvider' => true, - 'referencesProvider' => null, + 'referencesProvider' => true, 'documentHighlightProvider' => null, 'workspaceSymbolProvider' => true, 'codeActionProvider' => null, diff --git a/tests/Server/TextDocument/Definition/GlobalFallbackTest.php b/tests/Server/TextDocument/Definition/GlobalFallbackTest.php new file mode 100644 index 0000000..d734d9e --- /dev/null +++ b/tests/Server/TextDocument/Definition/GlobalFallbackTest.php @@ -0,0 +1,74 @@ +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')); + } + + public function testClassDoesNotFallback() + { + // $obj = new TestClass(); + // Get definition for TestClass should not fall back to global + $result = $this->textDocument->definition(new TextDocumentIdentifier('global_fallback'), new Position(9, 16)); + $this->assertEquals([], $result); + } + + public function testFallsBackForConstants() + { + // echo TEST_CONST; + // Get definition for TEST_CONST + $result = $this->textDocument->definition(new TextDocumentIdentifier('global_fallback'), new Position(6, 10)); + $this->assertEquals([ + 'uri' => 'global_symbols', + 'range' => [ + 'start' => [ + 'line' => 4, + 'character' => 6 + ], + 'end' => [ + 'line' => 4, + 'character' => 22 + ] + ] + ], json_decode(json_encode($result), true)); + } + + public function testFallsBackForFunctions() + { + // test_function(); + // Get definition for test_function + $result = $this->textDocument->definition(new TextDocumentIdentifier('global_fallback'), new Position(5, 6)); + $this->assertEquals([ + 'uri' => 'global_symbols', + 'range' => [ + 'start' => [ + 'line' => 33, + 'character' => 0 + ], + 'end' => [ + 'line' => 36, + 'character' => 1 + ] + ] + ], json_decode(json_encode($result), true)); + } +} diff --git a/tests/Server/TextDocument/Definition/GlobalTest.php b/tests/Server/TextDocument/Definition/GlobalTest.php new file mode 100644 index 0000000..aacdceb --- /dev/null +++ b/tests/Server/TextDocument/Definition/GlobalTest.php @@ -0,0 +1,321 @@ +textDocument = new Server\TextDocument($project, $client); + $project->openDocument('references', file_get_contents(__DIR__ . '/../../../../fixtures/global_references.php')); + $project->openDocument('symbols', file_get_contents(__DIR__ . '/../../../../fixtures/global_symbols.php')); + // Load this to check that there are no conflicts + $project->openDocument('references_namespaced', file_get_contents(__DIR__ . '/../../../../fixtures/references.php')); + $project->openDocument('symbols_namespaced', file_get_contents(__DIR__ . '/../../../../fixtures/symbols.php')); + $project->openDocument('use', file_get_contents(__DIR__ . '/../../../../fixtures/use.php')); + } + + public function testDefinitionFileBeginning() { + // |textDocument->definition(new TextDocumentIdentifier('references'), new Position(0, 0)); + $this->assertEquals([], json_decode(json_encode($result), true)); + } + + public function testDefinitionEmptyResult() { + // namespace keyword + $result = $this->textDocument->definition(new TextDocumentIdentifier('references'), new Position(2, 4)); + $this->assertEquals([], json_decode(json_encode($result), true)); + } + + public function testDefinitionForClassLike() + { + // $obj = new TestClass(); + // Get definition for TestClass + $result = $this->textDocument->definition(new TextDocumentIdentifier('references'), new Position(4, 16)); + $this->assertEquals([ + 'uri' => 'symbols', + 'range' => [ + 'start' => [ + 'line' => 6, + 'character' => 0 + ], + 'end' => [ + 'line' => 21, + 'character' => 1 + ] + ] + ], json_decode(json_encode($result), true)); + } + + public function testDefinitionForImplements() + { + // class TestClass implements TestInterface + // Get definition for TestInterface + $result = $this->textDocument->definition(new TextDocumentIdentifier('symbols'), new Position(6, 33)); + $this->assertEquals([ + 'uri' => 'symbols', + 'range' => [ + 'start' => [ + 'line' => 28, + 'character' => 0 + ], + 'end' => [ + 'line' => 31, + 'character' => 1 + ] + ] + ], json_decode(json_encode($result), true)); + } + + public function testDefinitionForClassConstants() + { + // echo TestClass::TEST_CLASS_CONST; + // Get definition for TEST_CLASS_CONST + $result = $this->textDocument->definition(new TextDocumentIdentifier('references'), new Position(9, 21)); + $this->assertEquals([ + 'uri' => 'symbols', + 'range' => [ + 'start' => [ + 'line' => 8, + 'character' => 10 + ], + 'end' => [ + 'line' => 8, + 'character' => 32 + ] + ] + ], json_decode(json_encode($result), true)); + } + + public function testDefinitionForConstants() + { + // echo TEST_CONST; + // Get definition for TEST_CONST + $result = $this->textDocument->definition(new TextDocumentIdentifier('references'), new Position(23, 9)); + $this->assertEquals([ + 'uri' => 'symbols', + 'range' => [ + 'start' => [ + 'line' => 4, + 'character' => 6 + ], + 'end' => [ + 'line' => 4, + 'character' => 22 + ] + ] + ], json_decode(json_encode($result), true)); + } + + public function testDefinitionForStaticMethods() + { + // TestClass::staticTestMethod(); + // Get definition for staticTestMethod + $result = $this->textDocument->definition(new TextDocumentIdentifier('references'), new Position(7, 20)); + $this->assertEquals([ + 'uri' => 'symbols', + 'range' => [ + 'start' => [ + 'line' => 12, + 'character' => 4 + ], + 'end' => [ + 'line' => 15, + 'character' => 5 + ] + ] + ], json_decode(json_encode($result), true)); + } + + public function testDefinitionForStaticProperties() + { + // echo TestClass::$staticTestProperty; + // Get definition for staticTestProperty + $result = $this->textDocument->definition(new TextDocumentIdentifier('references'), new Position(8, 25)); + $this->assertEquals([ + 'uri' => 'symbols', + 'range' => [ + 'start' => [ + 'line' => 9, + 'character' => 18 + ], + 'end' => [ + 'line' => 9, + 'character' => 37 + ] + ] + ], json_decode(json_encode($result), true)); + } + + public function testDefinitionForMethods() + { + // $obj->testMethod(); + // Get definition for testMethod + $result = $this->textDocument->definition(new TextDocumentIdentifier('references'), new Position(5, 11)); + $this->assertEquals([ + 'uri' => 'symbols', + 'range' => [ + 'start' => [ + 'line' => 17, + 'character' => 4 + ], + 'end' => [ + 'line' => 20, + 'character' => 5 + ] + ] + ], json_decode(json_encode($result), true)); + } + + public function testDefinitionForProperties() + { + // echo $obj->testProperty; + // Get definition for testProperty + $result = $this->textDocument->definition(new TextDocumentIdentifier('references'), new Position(6, 18)); + $this->assertEquals([ + 'uri' => 'symbols', + 'range' => [ + 'start' => [ + 'line' => 10, + 'character' => 11 + ], + 'end' => [ + 'line' => 10, + 'character' => 24 + ] + ] + ], json_decode(json_encode($result), true)); + } + + public function testDefinitionForVariables() + { + // echo $var; + // Get definition for $var + $result = $this->textDocument->definition(new TextDocumentIdentifier('references'), new Position(13, 7)); + $this->assertEquals([ + 'uri' => 'references', + 'range' => [ + 'start' => [ + 'line' => 12, + 'character' => 0 + ], + 'end' => [ + 'line' => 12, + 'character' => 10 + ] + ] + ], json_decode(json_encode($result), true)); + } + + public function testDefinitionForParamTypeHints() + { + // function whatever(TestClass $param) { + // Get definition for TestClass + $result = $this->textDocument->definition(new TextDocumentIdentifier('references'), new Position(15, 23)); + $this->assertEquals([ + 'uri' => 'symbols', + 'range' => [ + 'start' => [ + 'line' => 6, + 'character' => 0 + ], + 'end' => [ + 'line' => 21, + 'character' => 1 + ] + ] + ], json_decode(json_encode($result), true)); + } + public function testDefinitionForReturnTypeHints() + { + // function whatever(TestClass $param) { + // Get definition for TestClass + $result = $this->textDocument->definition(new TextDocumentIdentifier('references'), new Position(15, 42)); + $this->assertEquals([ + 'uri' => 'symbols', + 'range' => [ + 'start' => [ + 'line' => 6, + 'character' => 0 + ], + 'end' => [ + 'line' => 21, + 'character' => 1 + ] + ] + ], json_decode(json_encode($result), true)); + } + + public function testDefinitionForParams() + { + // echo $param; + // Get definition for $param + $result = $this->textDocument->definition(new TextDocumentIdentifier('references'), new Position(16, 13)); + $this->assertEquals([ + 'uri' => 'references', + 'range' => [ + 'start' => [ + 'line' => 15, + 'character' => 18 + ], + 'end' => [ + 'line' => 15, + 'character' => 34 + ] + ] + ], json_decode(json_encode($result), true)); + } + + public function testDefinitionForUsedVariables() + { + // echo $var; + // Get definition for $var + $result = $this->textDocument->definition(new TextDocumentIdentifier('references'), new Position(20, 11)); + $this->assertEquals([ + 'uri' => 'references', + 'range' => [ + 'start' => [ + 'line' => 19, + 'character' => 22 + ], + 'end' => [ + 'line' => 19, + 'character' => 26 + ] + ] + ], json_decode(json_encode($result), true)); + } + + public function testDefinitionForFunctions() + { + // test_function(); + // Get definition for test_function + $result = $this->textDocument->definition(new TextDocumentIdentifier('references'), new Position(10, 4)); + $this->assertEquals([ + 'uri' => 'symbols', + 'range' => [ + 'start' => [ + 'line' => 33, + 'character' => 0 + ], + 'end' => [ + 'line' => 36, + 'character' => 1 + ] + ] + ], json_decode(json_encode($result), true)); + } +} diff --git a/tests/Server/TextDocument/DefinitionTest.php b/tests/Server/TextDocument/Definition/NamespacedTest.php similarity index 97% rename from tests/Server/TextDocument/DefinitionTest.php rename to tests/Server/TextDocument/Definition/NamespacedTest.php index f26bb49..1d5f1c9 100644 --- a/tests/Server/TextDocument/DefinitionTest.php +++ b/tests/Server/TextDocument/Definition/NamespacedTest.php @@ -1,14 +1,15 @@ textDocument = new Server\TextDocument($project, $client); - $project->openDocument('references', file_get_contents(__DIR__ . '/../../../fixtures/references.php')); - $project->openDocument('symbols', file_get_contents(__DIR__ . '/../../../fixtures/symbols.php')); - $project->openDocument('use', file_get_contents(__DIR__ . '/../../../fixtures/use.php')); + $project->openDocument('references', file_get_contents(__DIR__ . '/../../../../fixtures/references.php')); + $project->openDocument('symbols', file_get_contents(__DIR__ . '/../../../../fixtures/symbols.php')); + $project->openDocument('use', file_get_contents(__DIR__ . '/../../../../fixtures/use.php')); } public function testDefinitionFileBeginning() { diff --git a/tests/Server/TextDocument/References/GlobalFallbackTest.php b/tests/Server/TextDocument/References/GlobalFallbackTest.php new file mode 100644 index 0000000..a501613 --- /dev/null +++ b/tests/Server/TextDocument/References/GlobalFallbackTest.php @@ -0,0 +1,78 @@ +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')); + } + + public function testClassDoesNotFallback() + { + // class TestClass implements TestInterface + // Get references for TestClass + $result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier('global_symbols'), new Position(6, 9)); + $this->assertEquals([], $result); + } + + public function testFallsBackForConstants() + { + // const TEST_CONST = 123; + // Get references for TEST_CONST + $result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier('global_symbols'), new Position(4, 13)); + $this->assertEquals([ + [ + 'uri' => 'global_fallback', + 'range' => [ + 'start' => [ + 'line' => 6, + 'character' => 5 + ], + 'end' => [ + 'line' => 6, + 'character' => 15 + ] + ] + ] + ], json_decode(json_encode($result), true)); + } + + public function testFallsBackForFunctions() + { + // function test_function() + // Get references for test_function + $result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier('global_symbols'), new Position(33, 16)); + $this->assertEquals([ + [ + 'uri' => 'global_fallback', + 'range' => [ + 'start' => [ + 'line' => 5, + 'character' => 0 + ], + 'end' => [ + 'line' => 5, + 'character' => 13 + ] + ] + ] + ], json_decode(json_encode($result), true)); + } +} diff --git a/tests/Server/TextDocument/References/GlobalTest.php b/tests/Server/TextDocument/References/GlobalTest.php new file mode 100644 index 0000000..5e0e177 --- /dev/null +++ b/tests/Server/TextDocument/References/GlobalTest.php @@ -0,0 +1,349 @@ +textDocument = new Server\TextDocument($project, $client); + $this->symbolsUri = pathToUri(realpath(__DIR__ . '/../../../../fixtures/global_symbols.php')); + $this->referencesUri = pathToUri(realpath(__DIR__ . '/../../../../fixtures/global_references.php')); + $project->loadDocument($this->referencesUri); + $project->loadDocument($this->symbolsUri); + } + + public function testReferencesForClassLike() + { + // class TestClass implements TestInterface + // Get references for TestClass + $result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier($this->symbolsUri), new Position(6, 9)); + $this->assertEquals([ + // $obj = new TestClass(); + [ + 'uri' => $this->referencesUri, + 'range' => [ + 'start' => [ + 'line' => 4, + 'character' => 11 + ], + 'end' => [ + 'line' => 4, + 'character' => 20 + ] + ] + ], + // TestClass::staticTestMethod(); + [ + 'uri' => $this->referencesUri, + 'range' => [ + 'start' => [ + 'line' => 7, + 'character' => 0 + ], + 'end' => [ + 'line' => 7, + 'character' => 9 + ] + ] + ], + // echo TestClass::$staticTestProperty; + [ + 'uri' => $this->referencesUri, + 'range' => [ + 'start' => [ + 'line' => 8, + 'character' => 5 + ], + 'end' => [ + 'line' => 8, + 'character' => 14 + ] + ] + ], + // TestClass::TEST_CLASS_CONST; + [ + 'uri' => $this->referencesUri, + 'range' => [ + 'start' => [ + 'line' => 9, + 'character' => 5 + ], + 'end' => [ + 'line' => 9, + 'character' => 14 + ] + ] + ], + // function whatever(TestClass $param) + [ + 'uri' => $this->referencesUri, + 'range' => [ + 'start' => [ + 'line' => 15, + 'character' => 18 + ], + 'end' => [ + 'line' => 15, + 'character' => 27 + ] + ] + ], + // function whatever(TestClass $param): TestClass + [ + 'uri' => $this->referencesUri, + 'range' => [ + 'start' => [ + 'line' => 15, + 'character' => 37 + ], + 'end' => [ + 'line' => 15, + 'character' => 46 + ] + ] + ] + ], json_decode(json_encode($result), true)); + } + + public function testReferencesForClassConstants() + { + // const TEST_CLASS_CONST = 123; + // Get references for TEST_CLASS_CONST + $result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier($this->symbolsUri), new Position(8, 19)); + $this->assertEquals([ + [ + 'uri' => $this->referencesUri, + 'range' => [ + 'start' => [ + 'line' => 9, + 'character' => 5 + ], + 'end' => [ + 'line' => 9, + 'character' => 32 + ] + ] + ] + ], json_decode(json_encode($result), true)); + } + + public function testReferencesForConstants() + { + // const TEST_CONST = 123; + // Get references for TEST_CONST + $result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier($this->symbolsUri), new Position(4, 13)); + $this->assertEquals([ + [ + 'uri' => $this->referencesUri, + 'range' => [ + 'start' => [ + 'line' => 23, + 'character' => 5 + ], + 'end' => [ + 'line' => 23, + 'character' => 15 + ] + ] + ] + ], json_decode(json_encode($result), true)); + } + + public function testReferencesForStaticMethods() + { + // public static function staticTestMethod() + // Get references for staticTestMethod + $result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier($this->symbolsUri), new Position(12, 35)); + $this->assertEquals([ + [ + 'uri' => $this->referencesUri, + 'range' => [ + 'start' => [ + 'line' => 7, + 'character' => 0 + ], + 'end' => [ + 'line' => 7, + 'character' => 29 + ] + ] + ] + ], json_decode(json_encode($result), true)); + } + + public function testReferencesForStaticProperties() + { + // public static $staticTestProperty; + // Get references for $staticTestProperty + $result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier($this->symbolsUri), new Position(9, 27)); + $this->assertEquals([ + [ + 'uri' => $this->referencesUri, + 'range' => [ + 'start' => [ + 'line' => 8, + 'character' => 5 + ], + 'end' => [ + 'line' => 8, + 'character' => 35 + ] + ] + ] + ], json_decode(json_encode($result), true)); + } + + public function testReferencesForMethods() + { + // public function testMethod($testParameter) + // Get references for testMethod + $result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier($this->symbolsUri), new Position(17, 24)); + $this->assertEquals([ + [ + 'uri' => $this->referencesUri, + 'range' => [ + 'start' => [ + 'line' => 5, + 'character' => 0 + ], + 'end' => [ + 'line' => 5, + 'character' => 18 + ] + ] + ] + ], json_decode(json_encode($result), true)); + } + + public function testReferencesForProperties() + { + // public $testProperty; + // Get references for testProperty + $result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier($this->symbolsUri), new Position(10, 15)); + $this->assertEquals([ + [ + 'uri' => $this->referencesUri, + 'range' => [ + 'start' => [ + 'line' => 6, + 'character' => 5 + ], + 'end' => [ + 'line' => 6, + 'character' => 23 + ] + ] + ] + ], json_decode(json_encode($result), true)); + } + + public function testReferencesForVariables() + { + // $var = 123; + // Get definition for $var + $result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier($this->referencesUri), new Position(13, 7)); + $this->assertEquals([ + [ + 'uri' => $this->referencesUri, + 'range' => [ + 'start' => [ + 'line' => 12, + 'character' => 0 + ], + 'end' => [ + 'line' => 12, + 'character' => 4 + ] + ] + ], + [ + 'uri' => $this->referencesUri, + 'range' => [ + 'start' => [ + 'line' => 13, + 'character' => 5 + ], + 'end' => [ + 'line' => 13, + 'character' => 9 + ] + ] + ], + [ + 'uri' => $this->referencesUri, + 'range' => [ + 'start' => [ + 'line' => 20, + 'character' => 9 + ], + 'end' => [ + 'line' => 20, + 'character' => 13 + ] + ] + ] + ], json_decode(json_encode($result), true)); + } + + public function testReferencesForFunctionParams() + { + // function whatever(TestClass $param): TestClass + // Get references for $param + $result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier($this->referencesUri), new Position(15, 32)); + $this->assertEquals([ + [ + 'uri' => $this->referencesUri, + 'range' => [ + 'start' => [ + 'line' => 16, + 'character' => 9 + ], + 'end' => [ + 'line' => 16, + 'character' => 15 + ] + ] + ] + ], json_decode(json_encode($result), true)); + } + + public function testReferencesForFunctions() + { + // function test_function() + // Get references for test_function + $result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier($this->symbolsUri), new Position(33, 16)); + $this->assertEquals([ + [ + 'uri' => $this->referencesUri, + 'range' => [ + 'start' => [ + 'line' => 10, + 'character' => 0 + ], + 'end' => [ + 'line' => 10, + 'character' => 13 + ] + ] + ] + ], json_decode(json_encode($result), true)); + } +} diff --git a/tests/Server/TextDocument/References/NamespacedTest.php b/tests/Server/TextDocument/References/NamespacedTest.php new file mode 100644 index 0000000..eed82ee --- /dev/null +++ b/tests/Server/TextDocument/References/NamespacedTest.php @@ -0,0 +1,366 @@ +textDocument = new Server\TextDocument($project, $client); + $this->symbolsUri = pathToUri(realpath(__DIR__ . '/../../../../fixtures/symbols.php')); + $this->referencesUri = pathToUri(realpath(__DIR__ . '/../../../../fixtures/references.php')); + $this->useUri = pathToUri(realpath(__DIR__ . '/../../../../fixtures/use.php')); + $project->loadDocument($this->referencesUri); + $project->loadDocument($this->symbolsUri); + $project->loadDocument($this->useUri); + } + + public function testReferencesForClassLike() + { + // class TestClass implements TestInterface + // Get references for TestClass + $result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier($this->symbolsUri), new Position(6, 9)); + $this->assertEquals([ + // $obj = new TestClass(); + [ + 'uri' => $this->referencesUri, + 'range' => [ + 'start' => [ + 'line' => 4, + 'character' => 11 + ], + 'end' => [ + 'line' => 4, + 'character' => 20 + ] + ] + ], + // TestClass::staticTestMethod(); + [ + 'uri' => $this->referencesUri, + 'range' => [ + 'start' => [ + 'line' => 7, + 'character' => 0 + ], + 'end' => [ + 'line' => 7, + 'character' => 9 + ] + ] + ], + // echo TestClass::$staticTestProperty; + [ + 'uri' => $this->referencesUri, + 'range' => [ + 'start' => [ + 'line' => 8, + 'character' => 5 + ], + 'end' => [ + 'line' => 8, + 'character' => 14 + ] + ] + ], + // TestClass::TEST_CLASS_CONST; + [ + 'uri' => $this->referencesUri, + 'range' => [ + 'start' => [ + 'line' => 9, + 'character' => 5 + ], + 'end' => [ + 'line' => 9, + 'character' => 14 + ] + ] + ], + // function whatever(TestClass $param) + [ + 'uri' => $this->referencesUri, + 'range' => [ + 'start' => [ + 'line' => 15, + 'character' => 18 + ], + 'end' => [ + 'line' => 15, + 'character' => 27 + ] + ] + ], + // function whatever(TestClass $param): TestClass + [ + 'uri' => $this->referencesUri, + 'range' => [ + 'start' => [ + 'line' => 15, + 'character' => 37 + ], + 'end' => [ + 'line' => 15, + 'character' => 46 + ] + ] + ], + // use TestNamespace\TestClass; + [ + 'uri' => $this->useUri, + 'range' => [ + 'start' => [ + 'line' => 4, + 'character' => 4 + ], + 'end' => [ + 'line' => 4, + 'character' => 27 + ] + ] + ] + ], json_decode(json_encode($result), true)); + } + + public function testReferencesForClassConstants() + { + // const TEST_CLASS_CONST = 123; + // Get references for TEST_CLASS_CONST + $result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier($this->symbolsUri), new Position(8, 19)); + $this->assertEquals([ + [ + 'uri' => $this->referencesUri, + 'range' => [ + 'start' => [ + 'line' => 9, + 'character' => 5 + ], + 'end' => [ + 'line' => 9, + 'character' => 32 + ] + ] + ] + ], json_decode(json_encode($result), true)); + } + + public function testReferencesForConstants() + { + // const TEST_CONST = 123; + // Get references for TEST_CONST + $result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier($this->symbolsUri), new Position(4, 13)); + $this->assertEquals([ + [ + 'uri' => $this->referencesUri, + 'range' => [ + 'start' => [ + 'line' => 23, + 'character' => 5 + ], + 'end' => [ + 'line' => 23, + 'character' => 15 + ] + ] + ] + ], json_decode(json_encode($result), true)); + } + + public function testReferencesForStaticMethods() + { + // public static function staticTestMethod() + // Get references for staticTestMethod + $result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier($this->symbolsUri), new Position(12, 35)); + $this->assertEquals([ + [ + 'uri' => $this->referencesUri, + 'range' => [ + 'start' => [ + 'line' => 7, + 'character' => 0 + ], + 'end' => [ + 'line' => 7, + 'character' => 29 + ] + ] + ] + ], json_decode(json_encode($result), true)); + } + + public function testReferencesForStaticProperties() + { + // public static $staticTestProperty; + // Get references for $staticTestProperty + $result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier($this->symbolsUri), new Position(9, 27)); + $this->assertEquals([ + [ + 'uri' => $this->referencesUri, + 'range' => [ + 'start' => [ + 'line' => 8, + 'character' => 5 + ], + 'end' => [ + 'line' => 8, + 'character' => 35 + ] + ] + ] + ], json_decode(json_encode($result), true)); + } + + public function testReferencesForMethods() + { + // public function testMethod($testParameter) + // Get references for testMethod + $result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier($this->symbolsUri), new Position(17, 24)); + $this->assertEquals([ + [ + 'uri' => $this->referencesUri, + 'range' => [ + 'start' => [ + 'line' => 5, + 'character' => 0 + ], + 'end' => [ + 'line' => 5, + 'character' => 18 + ] + ] + ] + ], json_decode(json_encode($result), true)); + } + + public function testReferencesForProperties() + { + // public $testProperty; + // Get references for testProperty + $result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier($this->symbolsUri), new Position(10, 15)); + $this->assertEquals([ + [ + 'uri' => $this->referencesUri, + 'range' => [ + 'start' => [ + 'line' => 6, + 'character' => 5 + ], + 'end' => [ + 'line' => 6, + 'character' => 23 + ] + ] + ] + ], json_decode(json_encode($result), true)); + } + + public function testReferencesForVariables() + { + // $var = 123; + // Get definition for $var + $result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier($this->referencesUri), new Position(13, 7)); + $this->assertEquals([ + [ + 'uri' => $this->referencesUri, + 'range' => [ + 'start' => [ + 'line' => 12, + 'character' => 0 + ], + 'end' => [ + 'line' => 12, + 'character' => 4 + ] + ] + ], + [ + 'uri' => $this->referencesUri, + 'range' => [ + 'start' => [ + 'line' => 13, + 'character' => 5 + ], + 'end' => [ + 'line' => 13, + 'character' => 9 + ] + ] + ], + [ + 'uri' => $this->referencesUri, + 'range' => [ + 'start' => [ + 'line' => 20, + 'character' => 9 + ], + 'end' => [ + 'line' => 20, + 'character' => 13 + ] + ] + ] + ], json_decode(json_encode($result), true)); + } + + public function testReferencesForFunctionParams() + { + // function whatever(TestClass $param): TestClass + // Get references for $param + $result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier($this->referencesUri), new Position(15, 32)); + $this->assertEquals([ + [ + 'uri' => $this->referencesUri, + 'range' => [ + 'start' => [ + 'line' => 16, + 'character' => 9 + ], + 'end' => [ + 'line' => 16, + 'character' => 15 + ] + ] + ] + ], json_decode(json_encode($result), true)); + } + + public function testReferencesForFunctions() + { + // function test_function() + // Get references for test_function + $result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier($this->symbolsUri), new Position(33, 16)); + $this->assertEquals([ + [ + 'uri' => $this->referencesUri, + 'range' => [ + 'start' => [ + 'line' => 10, + 'character' => 0 + ], + 'end' => [ + 'line' => 10, + 'character' => 13 + ] + ] + ] + ], json_decode(json_encode($result), true)); + } +}