Add textDocument/definition support
parent
827ab4c842
commit
d4757e0a24
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace TestNamespace;
|
||||||
|
|
||||||
|
$obj = new TestClass();
|
||||||
|
$obj->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;
|
|
@ -4,7 +4,7 @@ namespace TestNamespace;
|
||||||
|
|
||||||
const TEST_CONST = 123;
|
const TEST_CONST = 123;
|
||||||
|
|
||||||
class TestClass
|
class TestClass implements TestInterface
|
||||||
{
|
{
|
||||||
const TEST_CLASS_CONST = 123;
|
const TEST_CLASS_CONST = 123;
|
||||||
public static $staticTestProperty;
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SecondTestNamespace;
|
||||||
|
|
||||||
|
use TestNamespace\TestClass;
|
||||||
|
use TestNamespace\{TestTrait, TestInterface};
|
|
@ -103,6 +103,9 @@ class LanguageServer extends \AdvancedJsonRpc\Dispatcher
|
||||||
$serverCapabilities->workspaceSymbolProvider = true;
|
$serverCapabilities->workspaceSymbolProvider = true;
|
||||||
// Support "Format Code"
|
// Support "Format Code"
|
||||||
$serverCapabilities->documentFormattingProvider = true;
|
$serverCapabilities->documentFormattingProvider = true;
|
||||||
|
// Support "Go to definition"
|
||||||
|
$serverCapabilities->definitionProvider = true;
|
||||||
|
|
||||||
return new InitializeResult($serverCapabilities);
|
return new InitializeResult($serverCapabilities);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace LanguageServer\NodeVisitors;
|
||||||
|
|
||||||
|
use PhpParser\{NodeVisitorAbstract, Node};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collects definitions of classes, interfaces, traits, methods, properties and constants
|
||||||
|
* Depends on ReferencesAdder and NameResolver
|
||||||
|
*/
|
||||||
|
class DefinitionCollector extends NodeVisitorAbstract
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 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[]
|
||||||
|
*/
|
||||||
|
public $definitions = [];
|
||||||
|
|
||||||
|
public function enterNode(Node $node)
|
||||||
|
{
|
||||||
|
if ($node instanceof Node\Stmt\ClassLike && isset($node->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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -38,6 +38,11 @@ class NodeAtPositionFinder extends NodeVisitorAbstract
|
||||||
new Position($node->getAttribute('startLine') - 1, $node->getAttribute('startColumn') - 1),
|
new Position($node->getAttribute('startLine') - 1, $node->getAttribute('startColumn') - 1),
|
||||||
new Position($node->getAttribute('endLine') - 1, $node->getAttribute('endColumn') - 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)) {
|
if (!isset($this->node) && $range->includes($this->position)) {
|
||||||
$this->node = $node;
|
$this->node = $node;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ declare(strict_types = 1);
|
||||||
namespace LanguageServer;
|
namespace LanguageServer;
|
||||||
|
|
||||||
use LanguageServer\Protocol\{Diagnostic, DiagnosticSeverity, Range, Position, SymbolKind, TextEdit};
|
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\{Error, Comment, Node, ParserFactory, NodeTraverser, Lexer, Parser};
|
||||||
use PhpParser\PrettyPrinter\Standard as PrettyPrinter;
|
use PhpParser\PrettyPrinter\Standard as PrettyPrinter;
|
||||||
use PhpParser\NodeVisitor\NameResolver;
|
use PhpParser\NodeVisitor\NameResolver;
|
||||||
|
@ -23,7 +23,9 @@ class PhpDocument
|
||||||
*
|
*
|
||||||
* @var Project
|
* @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
|
* The PHPParser instance
|
||||||
|
@ -46,6 +48,28 @@ class PhpDocument
|
||||||
*/
|
*/
|
||||||
private $content;
|
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[]
|
* @var SymbolInformation[]
|
||||||
*/
|
*/
|
||||||
|
@ -149,13 +173,24 @@ class PhpDocument
|
||||||
$traverser->addVisitor(new ColumnCalculator($this->content));
|
$traverser->addVisitor(new ColumnCalculator($this->content));
|
||||||
|
|
||||||
// Collect all symbols
|
// Collect all symbols
|
||||||
|
// TODO: use DefinitionCollector for this
|
||||||
$symbolFinder = new SymbolFinder($this->uri);
|
$symbolFinder = new SymbolFinder($this->uri);
|
||||||
$traverser->addVisitor($symbolFinder);
|
$traverser->addVisitor($symbolFinder);
|
||||||
|
|
||||||
|
// Collect all definitions
|
||||||
|
$definitionCollector = new DefinitionCollector;
|
||||||
|
$traverser->addVisitor($definitionCollector);
|
||||||
|
|
||||||
$traverser->traverse($stmts);
|
$traverser->traverse($stmts);
|
||||||
|
|
||||||
$this->symbols = $symbolFinder->symbols;
|
$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;
|
$this->stmts = $stmts;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -187,6 +222,16 @@ class PhpDocument
|
||||||
return $this->content;
|
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
|
* Returns the node at a specified position
|
||||||
*
|
*
|
||||||
|
@ -204,4 +249,173 @@ class PhpDocument
|
||||||
$traverser->traverse($this->stmts);
|
$traverser->traverse($this->stmts);
|
||||||
return $finder->node;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,14 @@ class Project
|
||||||
*/
|
*/
|
||||||
private $documents;
|
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
|
* Instance of the PHP parser
|
||||||
*
|
*
|
||||||
|
@ -54,6 +62,28 @@ class Project
|
||||||
return $this->documents[$uri];
|
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.
|
* Finds symbols in all documents, filtered by query parameter.
|
||||||
*
|
*
|
||||||
|
|
|
@ -13,7 +13,8 @@ use LanguageServer\Protocol\{
|
||||||
Range,
|
Range,
|
||||||
Position,
|
Position,
|
||||||
FormattingOptions,
|
FormattingOptions,
|
||||||
TextEdit
|
TextEdit,
|
||||||
|
Location
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -88,4 +89,32 @@ class TextDocument
|
||||||
{
|
{
|
||||||
return $this->project->getDocument($textDocument->uri)->getFormattedText();
|
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)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ class LanguageServerTest extends TestCase
|
||||||
'hoverProvider' => null,
|
'hoverProvider' => null,
|
||||||
'completionProvider' => null,
|
'completionProvider' => null,
|
||||||
'signatureHelpProvider' => null,
|
'signatureHelpProvider' => null,
|
||||||
'definitionProvider' => null,
|
'definitionProvider' => true,
|
||||||
'referencesProvider' => null,
|
'referencesProvider' => null,
|
||||||
'documentHighlightProvider' => null,
|
'documentHighlightProvider' => null,
|
||||||
'workspaceSymbolProvider' => true,
|
'workspaceSymbolProvider' => true,
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace LanguageServer\Tests\Server\TextDocument;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use PhpParser\{ParserFactory, NodeTraverser, Node};
|
||||||
|
use PhpParser\NodeVisitor\NameResolver;
|
||||||
|
use LanguageServer\NodeVisitors\{ReferencesAdder, DefinitionCollector};
|
||||||
|
|
||||||
|
class DefinitionCollectorTest extends TestCase
|
||||||
|
{
|
||||||
|
public function test()
|
||||||
|
{
|
||||||
|
$traverser = new NodeTraverser;
|
||||||
|
$traverser->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()']);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,346 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace LanguageServer\Tests\Server\TextDocument;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use LanguageServer\Tests\MockProtocolStream;
|
||||||
|
use LanguageServer\{Server, LanguageClient, Project};
|
||||||
|
use LanguageServer\Protocol\{TextDocumentIdentifier, Position};
|
||||||
|
|
||||||
|
class DefinitionTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Server\TextDocument
|
||||||
|
*/
|
||||||
|
private $textDocument;
|
||||||
|
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
$client = new LanguageClient(new MockProtocolStream());
|
||||||
|
$project = new Project($client);
|
||||||
|
$this->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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -207,6 +207,24 @@ class DocumentSymbolTest extends TestCase
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
'containerName' => 'TestNamespace'
|
'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));
|
], json_decode(json_encode($result), true));
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue