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;
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
// Support "Format Code"
|
||||
$serverCapabilities->documentFormattingProvider = true;
|
||||
// Support "Go to definition"
|
||||
$serverCapabilities->definitionProvider = true;
|
||||
|
||||
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('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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ class LanguageServerTest extends TestCase
|
|||
'hoverProvider' => null,
|
||||
'completionProvider' => null,
|
||||
'signatureHelpProvider' => null,
|
||||
'definitionProvider' => null,
|
||||
'definitionProvider' => true,
|
||||
'referencesProvider' => null,
|
||||
'documentHighlightProvider' => null,
|
||||
'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'
|
||||
],
|
||||
[
|
||||
'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));
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue