From d4757e0a24f2da0bf5e8f6f5a45479f73e640094 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Sat, 8 Oct 2016 14:59:08 +0200 Subject: [PATCH] Add textDocument/definition support --- fixtures/references.php | 24 ++ fixtures/symbols.php | 23 +- fixtures/use.php | 6 + src/LanguageServer.php | 3 + src/NodeVisitors/DefinitionCollector.php | 72 ++++ src/NodeVisitors/NodeAtPositionFinder.php | 5 + src/PhpDocument.php | 218 ++++++++++- src/Project.php | 30 ++ src/Server/TextDocument.php | 31 +- tests/LanguageServerTest.php | 2 +- .../NodeVisitors/DefinitionCollectorTest.php | 47 +++ tests/Server/TextDocument/DefinitionTest.php | 346 ++++++++++++++++++ .../TextDocument/DocumentSymbolTest.php | 18 + 13 files changed, 820 insertions(+), 5 deletions(-) create mode 100644 fixtures/references.php create mode 100644 fixtures/use.php create mode 100644 src/NodeVisitors/DefinitionCollector.php create mode 100644 tests/NodeVisitors/DefinitionCollectorTest.php create mode 100644 tests/Server/TextDocument/DefinitionTest.php diff --git a/fixtures/references.php b/fixtures/references.php new file mode 100644 index 0000000..6cb9190 --- /dev/null +++ b/fixtures/references.php @@ -0,0 +1,24 @@ +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/symbols.php b/fixtures/symbols.php index 77dff07..26379e4 100644 --- a/fixtures/symbols.php +++ b/fixtures/symbols.php @@ -4,7 +4,7 @@ namespace TestNamespace; const TEST_CONST = 123; -class TestClass +class TestClass implements TestInterface { const TEST_CLASS_CONST = 123; public static $staticTestProperty; @@ -30,3 +30,24 @@ interface TestInterface { } + +function test_function() +{ + +} + +new class { + const TEST_CLASS_CONST = 123; + public static $staticTestProperty; + public $testProperty; + + public static function staticTestMethod() + { + + } + + public function testMethod($testParameter) + { + $testVariable = 123; + } +}; diff --git a/fixtures/use.php b/fixtures/use.php new file mode 100644 index 0000000..28e3aec --- /dev/null +++ b/fixtures/use.php @@ -0,0 +1,6 @@ +workspaceSymbolProvider = true; // Support "Format Code" $serverCapabilities->documentFormattingProvider = true; + // Support "Go to definition" + $serverCapabilities->definitionProvider = true; + return new InitializeResult($serverCapabilities); } diff --git a/src/NodeVisitors/DefinitionCollector.php b/src/NodeVisitors/DefinitionCollector.php new file mode 100644 index 0000000..8ad80fd --- /dev/null +++ b/src/NodeVisitors/DefinitionCollector.php @@ -0,0 +1,72 @@ +name)) { + // Class, interface or trait declaration + $this->definitions[(string)$node->namespacedName] = $node; + } else if ($node instanceof Node\Stmt\Function_) { + // Function: use functioName() as the name + $name = (string)$node->namespacedName . '()'; + $this->definitions[$name] = $node; + } else if ($node instanceof Node\Stmt\ClassMethod) { + // Class method: use ClassName::methodName() as name + $class = $node->getAttribute('parentNode'); + if (!isset($class->name)) { + // Ignore anonymous classes + return; + } + $name = (string)$class->namespacedName . '::' . (string)$node->name . '()'; + $this->definitions[$name] = $node; + } else if ($node instanceof Node\Stmt\PropertyProperty) { + // Property: use ClassName::propertyName as name + $class = $node->getAttribute('parentNode')->getAttribute('parentNode'); + if (!isset($class->name)) { + // Ignore anonymous classes + return; + } + $name = (string)$class->namespacedName . '::' . (string)$node->name; + $this->definitions[$name] = $node; + } else if ($node instanceof Node\Const_) { + $parent = $node->getAttribute('parentNode'); + if ($parent instanceof Node\Stmt\Const_) { + // Basic constant: use CONSTANT_NAME as name + $name = (string)$node->namespacedName; + } else if ($parent instanceof Node\Stmt\ClassConst) { + // Class constant: use ClassName::CONSTANT_NAME as name + $class = $parent->getAttribute('parentNode'); + if (!isset($class->name) || $class->name instanceof Node\Expr) { + return; + } + $name = (string)$class->namespacedName . '::' . $node->name; + } + $this->definitions[$name] = $node; + } + } +} diff --git a/src/NodeVisitors/NodeAtPositionFinder.php b/src/NodeVisitors/NodeAtPositionFinder.php index 8f38eec..f68e39a 100644 --- a/src/NodeVisitors/NodeAtPositionFinder.php +++ b/src/NodeVisitors/NodeAtPositionFinder.php @@ -38,6 +38,11 @@ class NodeAtPositionFinder extends NodeVisitorAbstract new Position($node->getAttribute('startLine') - 1, $node->getAttribute('startColumn') - 1), new Position($node->getAttribute('endLine') - 1, $node->getAttribute('endColumn') - 1) ); + // Workaround for https://github.com/nikic/PHP-Parser/issues/311 + $parent = $node->getAttribute('parentNode'); + if (isset($parent) && $parent instanceof Node\Stmt\GroupUse && $parent->prefix === $node) { + return; + } if (!isset($this->node) && $range->includes($this->position)) { $this->node = $node; } diff --git a/src/PhpDocument.php b/src/PhpDocument.php index b0f9ede..1c37e87 100644 --- a/src/PhpDocument.php +++ b/src/PhpDocument.php @@ -4,7 +4,7 @@ declare(strict_types = 1); namespace LanguageServer; use LanguageServer\Protocol\{Diagnostic, DiagnosticSeverity, Range, Position, SymbolKind, TextEdit}; -use LanguageServer\NodeVisitors\{NodeAtPositionFinder, ReferencesAdder, SymbolFinder, ColumnCalculator}; +use LanguageServer\NodeVisitors\{NodeAtPositionFinder, ReferencesAdder, DefinitionCollector, SymbolFinder, ColumnCalculator}; use PhpParser\{Error, Comment, Node, ParserFactory, NodeTraverser, Lexer, Parser}; use PhpParser\PrettyPrinter\Standard as PrettyPrinter; use PhpParser\NodeVisitor\NameResolver; @@ -23,7 +23,9 @@ class PhpDocument * * @var Project */ - private $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 @@ -46,6 +48,28 @@ class PhpDocument */ private $content; + /** + * The AST of the document + * + * @var Node[] + */ + private $stmts = []; + + /** + * Map from fully qualified name (FQN) to Node + * Examples of fully qualified names: + * - testFunction() + * - TestNamespace\TestClass + * - TestNamespace\TestClass::TEST_CONSTANT + * - TestNamespace\TestClass::staticTestProperty + * - TestNamespace\TestClass::testProperty + * - TestNamespace\TestClass::staticTestMethod() + * - TestNamespace\TestClass::testMethod() + * + * @var Node[] + */ + private $definitions = []; + /** * @var SymbolInformation[] */ @@ -149,13 +173,24 @@ class PhpDocument $traverser->addVisitor(new ColumnCalculator($this->content)); // Collect all symbols + // TODO: use DefinitionCollector for this $symbolFinder = new SymbolFinder($this->uri); $traverser->addVisitor($symbolFinder); + // Collect all definitions + $definitionCollector = new DefinitionCollector; + $traverser->addVisitor($definitionCollector); + $traverser->traverse($stmts); $this->symbols = $symbolFinder->symbols; + $this->definitions = $definitionCollector->definitions; + // Register this document on the project for all the symbols defined in it + foreach ($definitionCollector->definitions as $fqn => $node) { + $this->project->addDefinitionDocument($fqn, $this); + } + $this->stmts = $stmts; } } @@ -187,6 +222,16 @@ class PhpDocument return $this->content; } + /** + * Returns the URI of the document + * + * @return string + */ + public function getUri(): string + { + return $this->uri; + } + /** * Returns the node at a specified position * @@ -204,4 +249,173 @@ class PhpDocument $traverser->traverse($this->stmts); return $finder->node; } + + /** + * Returns the definition node for a fully qualified name + * + * @param string $fqn + * @return Node|null + */ + public function getDefinitionByFqn(string $fqn) + { + return $this->definitions[$fqn] ?? null; + } + + /** + * Returns the definition node for any node + * The definition node MAY be in another document, check the ownerDocument attribute + * + * @param Node $node + * @return Node|null + */ + public function getDefinitionByNode(Node $node) + { + if ($node instanceof Node\Name) { + $nameNode = $node; + $node = $node->getAttribute('parentNode'); + } + // 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) { + return $this->getVariableDefinition($node); + } + + if ( + ($node instanceof Node\Stmt\ClassLike + || $node instanceof Node\Param + || $node instanceof Node\Stmt\Function_) + && isset($nameNode) + ) { + // For extends, implements and type hints use the name directly + $name = (string)$nameNode; + } else if ($node instanceof Node\Stmt\UseUse) { + $name = (string)$node->name; + $parent = $node->getAttribute('parentNode'); + if ($parent instanceof Node\Stmt\GroupUse) { + $name = $parent->prefix . '\\' . $name; + } + } else if ($node instanceof Node\Expr\New_) { + if (!($node->class instanceof Node\Name)) { + // Cannot get definition of dynamic calls + return null; + } + $name = (string)$node->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 + return null; + } + // Need to resolve variable to a class + $varDef = $this->getVariableDefinition($node->var); + if (!isset($varDef)) { + return null; + } + if ($varDef instanceof Node\Param) { + if (!isset($varDef->type)) { + // Cannot resolve to class without a type hint + // TODO: parse docblock + return null; + } + $name = (string)$varDef->type; + } else if ($varDef instanceof Node\Expr\Assign) { + if ($varDef->expr instanceof Node\Expr\New_) { + if (!($varDef->expr->class instanceof Node\Name)) { + // Cannot get definition of dynamic calls + return null; + } + $name = (string)$varDef->expr->class; + } else { + return null; + } + } else { + return null; + } + $name .= '::' . (string)$node->name; + } else if ($node instanceof Node\Expr\FuncCall) { + if ($node->name instanceof Node\Expr) { + return null; + } + $name = (string)$node->name; + } else if ($node instanceof Node\Expr\ConstFetch) { + $name = (string)$node->name; + } else if ( + $node instanceof Node\Expr\ClassConstFetch + || $node instanceof Node\Expr\StaticPropertyFetch + || $node instanceof Node\Expr\StaticCall + ) { + if ($node->class instanceof Node\Expr || $node->name instanceof Node\Expr) { + // Cannot get definition of dynamic names + return null; + } + $name = (string)$node->class . '::' . $node->name; + } + if ( + $node instanceof Node\Expr\MethodCall + || $node instanceof Node\Expr\FuncCall + || $node instanceof Node\Expr\StaticCall + ) { + $name .= '()'; + } + if (!isset($name)) { + return null; + } + // Search for the document where the class, interface, trait, function, method or property is defined + $document = $this->project->getDefinitionDocument($name); + if (!$document && $node instanceof Node\Expr\FuncCall) { + // Find and try with namespace + // Namespaces aren't added automatically by NameResolver because PHP falls back to global functions + $n = $node; + while (isset($n)) { + $n = $n->getAttribute('parentNode'); + if ($n instanceof Node\Stmt\Namespace_) { + $name = (string)$n->name . '\\' . $name; + $document = $this->project->getDefinitionDocument($name); + break; + } + } + } + if (!isset($document)) { + return null; + } + return $document->getDefinitionByFqn($name); + } + + /** + * Returns the assignment or parameter node where a variable was defined + * + * @param Node\Expr\Variable $n The variable access + * @return Node\Expr\Assign|Node\Param|Node\Expr\ClosureUse|null + */ + public function getVariableDefinition(Node\Expr\Variable $var) + { + $n = $var; + // Traverse the AST up + while (isset($n) && $n = $n->getAttribute('parentNode')) { + // If a function is met, check the parameters and use statements + if ($n instanceof Node\FunctionLike) { + foreach ($n->getParams() as $param) { + if ($param->name === $var->name) { + return $param; + } + } + // If it is a closure, also check use statements + if ($n instanceof Node\Expr\Closure) { + foreach ($n->uses as $use) { + if ($use->var === $var->name) { + return $use; + } + } + } + break; + } + // Check each previous sibling node for a variable assignment to that variable + while ($n->getAttribute('previousSibling') && $n = $n->getAttribute('previousSibling')) { + if ($n instanceof Node\Expr\Assign && $n->var->name === $var->name) { + return $n; + } + } + } + // Return null if nothing was found + return null; + } } diff --git a/src/Project.php b/src/Project.php index adf37f6..7dd3596 100644 --- a/src/Project.php +++ b/src/Project.php @@ -17,6 +17,14 @@ class Project */ private $documents; + /** + * An associative array [string => PhpDocument] + * that maps fully qualified symbol names to loaded PhpDocuments + * + * @var PhpDocument[] + */ + private $definitions; + /** * Instance of the PHP parser * @@ -54,6 +62,28 @@ class Project return $this->documents[$uri]; } + /** + * Adds a document as the container for a specific symbol + * + * @param string $fqn The fully qualified name of the symbol + * @return void + */ + public function addDefinitionDocument(string $fqn, PhpDocument $document) + { + $this->definitions[$fqn] = $document; + } + + /** + * Returns the document where a symbol is defined + * + * @param string $fqn The fully qualified name of the symbol + * @return PhpDocument|null + */ + public function getDefinitionDocument(string $fqn) + { + return $this->definitions[$fqn] ?? null; + } + /** * Finds symbols in all documents, filtered by query parameter. * diff --git a/src/Server/TextDocument.php b/src/Server/TextDocument.php index a6541c1..b13abd8 100644 --- a/src/Server/TextDocument.php +++ b/src/Server/TextDocument.php @@ -13,7 +13,8 @@ use LanguageServer\Protocol\{ Range, Position, FormattingOptions, - TextEdit + TextEdit, + Location }; /** @@ -88,4 +89,32 @@ class TextDocument { return $this->project->getDocument($textDocument->uri)->getFormattedText(); } + + /** + * 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. + * + * @param TextDocumentIdentifier $textDocument The text document + * @param Position $position The position inside the text document + * @return Location|Location[]|null + */ + public function definition(TextDocumentIdentifier $textDocument, Position $position) + { + $document = $this->project->getDocument($textDocument->uri); + $node = $document->getNodeAtPosition($position); + if ($node === null) { + return null; + } + $def = $document->getDefinitionByNode($node); + if ($def === null) { + return null; + } + return new Location( + $def->getAttribute('ownerDocument')->getUri(), + new Range( + new Position($def->getAttribute('startLine') - 1, $def->getAttribute('startColumn') - 1), + new Position($def->getAttribute('endLine') - 1, $def->getAttribute('endColumn') - 1) + ) + ); + } } diff --git a/tests/LanguageServerTest.php b/tests/LanguageServerTest.php index 7d82c23..521d602 100644 --- a/tests/LanguageServerTest.php +++ b/tests/LanguageServerTest.php @@ -34,7 +34,7 @@ class LanguageServerTest extends TestCase 'hoverProvider' => null, 'completionProvider' => null, 'signatureHelpProvider' => null, - 'definitionProvider' => null, + 'definitionProvider' => true, 'referencesProvider' => null, 'documentHighlightProvider' => null, 'workspaceSymbolProvider' => true, diff --git a/tests/NodeVisitors/DefinitionCollectorTest.php b/tests/NodeVisitors/DefinitionCollectorTest.php new file mode 100644 index 0000000..d22bd79 --- /dev/null +++ b/tests/NodeVisitors/DefinitionCollectorTest.php @@ -0,0 +1,47 @@ +addVisitor(new NameResolver); + $traverser->addVisitor(new ReferencesAdder); + $definitionCollector = new DefinitionCollector; + $traverser->addVisitor($definitionCollector); + $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7); + $stmts = $parser->parse(file_get_contents(__DIR__ . '/../../fixtures/symbols.php')); + $traverser->traverse($stmts); + $defs = $definitionCollector->definitions; + $this->assertEquals([ + 'TestNamespace\\TEST_CONST', + 'TestNamespace\\TestClass', + 'TestNamespace\\TestClass::TEST_CLASS_CONST', + 'TestNamespace\\TestClass::staticTestProperty', + 'TestNamespace\\TestClass::testProperty', + 'TestNamespace\\TestClass::staticTestMethod()', + 'TestNamespace\\TestClass::testMethod()', + 'TestNamespace\\TestTrait', + 'TestNamespace\\TestInterface', + 'TestNamespace\\test_function()' + ], array_keys($defs)); + $this->assertInstanceOf(Node\Const_::class, $defs['TestNamespace\\TEST_CONST']); + $this->assertInstanceOf(Node\Stmt\Class_::class, $defs['TestNamespace\\TestClass']); + $this->assertInstanceOf(Node\Const_::class, $defs['TestNamespace\\TestClass::TEST_CLASS_CONST']); + $this->assertInstanceOf(Node\Stmt\PropertyProperty::class, $defs['TestNamespace\\TestClass::staticTestProperty']); + $this->assertInstanceOf(Node\Stmt\PropertyProperty::class, $defs['TestNamespace\\TestClass::testProperty']); + $this->assertInstanceOf(Node\Stmt\ClassMethod::class, $defs['TestNamespace\\TestClass::staticTestMethod()']); + $this->assertInstanceOf(Node\Stmt\ClassMethod::class, $defs['TestNamespace\\TestClass::testMethod()']); + $this->assertInstanceOf(Node\Stmt\Trait_::class, $defs['TestNamespace\\TestTrait']); + $this->assertInstanceOf(Node\Stmt\Interface_::class, $defs['TestNamespace\\TestInterface']); + $this->assertInstanceOf(Node\Stmt\Function_::class, $defs['TestNamespace\\test_function()']); + } +} diff --git a/tests/Server/TextDocument/DefinitionTest.php b/tests/Server/TextDocument/DefinitionTest.php new file mode 100644 index 0000000..eca8722 --- /dev/null +++ b/tests/Server/TextDocument/DefinitionTest.php @@ -0,0 +1,346 @@ +textDocument = new Server\TextDocument($project, $client); + $project->getDocument('references')->updateContent(file_get_contents(__DIR__ . '/../../../fixtures/references.php')); + $project->getDocument('symbols')->updateContent(file_get_contents(__DIR__ . '/../../../fixtures/symbols.php')); + $project->getDocument('use')->updateContent(file_get_contents(__DIR__ . '/../../../fixtures/use.php')); + } + + 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' => 0 + ] + ] + ], json_decode(json_encode($result), true)); + } + + public function testDefinitionForClassLikeUseStatement() + { + // use TestNamespace\TestClass; + // Get definition for TestClass + $result = $this->textDocument->definition(new TextDocumentIdentifier('use'), new Position(4, 22)); + $this->assertEquals([ + 'uri' => 'symbols', + 'range' => [ + 'start' => [ + 'line' => 6, + 'character' => 0 + ], + 'end' => [ + 'line' => 21, + 'character' => 0 + ] + ] + ], json_decode(json_encode($result), true)); + } + + public function testDefinitionForClassLikeGroupUseStatement() + { + // use TestNamespace\{TestTrait, TestInterface}; + // Get definition for TestInterface + $result = $this->textDocument->definition(new TextDocumentIdentifier('use'), new Position(5, 37)); + $this->assertEquals([ + 'uri' => 'symbols', + 'range' => [ + 'start' => [ + 'line' => 28, + 'character' => 0 + ], + 'end' => [ + 'line' => 31, + 'character' => 0 + ] + ] + ], 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' => 0 + ] + ] + ], 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' => 31 + ] + ] + ], 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' => 21 + ] + ] + ], 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' => 4 + ] + ] + ], 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' => 36 + ] + ] + ], 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' => 4 + ] + ] + ], 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' => 23 + ] + ] + ], 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' => 9 + ] + ] + ], 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' => 0 + ] + ] + ], 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' => 0 + ] + ] + ], 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' => 33 + ] + ] + ], 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' => 25 + ] + ] + ], 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' => 0 + ] + ] + ], json_decode(json_encode($result), true)); + } +} diff --git a/tests/Server/TextDocument/DocumentSymbolTest.php b/tests/Server/TextDocument/DocumentSymbolTest.php index b46648b..307a1cb 100644 --- a/tests/Server/TextDocument/DocumentSymbolTest.php +++ b/tests/Server/TextDocument/DocumentSymbolTest.php @@ -207,6 +207,24 @@ class DocumentSymbolTest extends TestCase ] ], 'containerName' => 'TestNamespace' + ], + [ + 'name' => 'test_function', + 'kind' => SymbolKind::FUNCTION, + 'location' => [ + 'uri' => 'symbols', + 'range' => [ + 'start' => [ + 'line' => 33, + 'character' => 0 + ], + 'end' => [ + 'line' => 36, + 'character' => 1 + ] + ] + ], + 'containerName' => 'TestNamespace' ] ], json_decode(json_encode($result), true)); }