1
0
Fork 0

Merge branch 'master' into master_75

Conflicts:
	src/PhpDocument.php
pull/76/head
Michal Niewrzal 2016-10-20 14:05:23 +02:00
commit 1fed89e0e3
19 changed files with 482 additions and 278 deletions

1
.gitignore vendored
View File

@ -2,4 +2,5 @@
.vscode .vscode
.idea .idea
vendor/ vendor/
.phpls/
composer.lock composer.lock

View File

@ -27,7 +27,7 @@
"nikic/php-parser": "dev-master#90834bff8eaf7b7f893253f312e73d8f532341ca", "nikic/php-parser": "dev-master#90834bff8eaf7b7f893253f312e73d8f532341ca",
"phpdocumentor/reflection-docblock": "^3.0", "phpdocumentor/reflection-docblock": "^3.0",
"sabre/event": "^4.0", "sabre/event": "^4.0",
"felixfbecker/advanced-json-rpc": "^1.2", "felixfbecker/advanced-json-rpc": "^2.0",
"squizlabs/php_codesniffer" : "^2.7", "squizlabs/php_codesniffer" : "^2.7",
"symfony/debug": "^3.1" "symfony/debug": "^3.1"
}, },
@ -37,7 +37,10 @@
"psr-4": { "psr-4": {
"LanguageServer\\": "src/" "LanguageServer\\": "src/"
}, },
"files" : ["src/utils.php"] "files" : [
"src/utils.php",
"src/Fqn.php"
]
}, },
"autoload-dev": { "autoload-dev": {
"psr-4": { "psr-4": {

View File

@ -46,7 +46,7 @@ class TestClass implements TestInterface
*/ */
public static function staticTestMethod() public static function staticTestMethod()
{ {
echo self::TEST_CLASS_CONST;
} }
/** /**
@ -57,7 +57,7 @@ class TestClass implements TestInterface
*/ */
public function testMethod($testParameter): TestInterface public function testMethod($testParameter): TestInterface
{ {
$testVariable = 123; $this->testProperty = $testParameter;
} }
} }

View File

@ -46,7 +46,7 @@ class TestClass implements TestInterface
*/ */
public static function staticTestMethod() public static function staticTestMethod()
{ {
echo self::TEST_CLASS_CONST;
} }
/** /**
@ -57,7 +57,7 @@ class TestClass implements TestInterface
*/ */
public function testMethod($testParameter): TestInterface public function testMethod($testParameter): TestInterface
{ {
$testVariable = 123; $this->testProperty = $testParameter;
} }
} }

245
src/Fqn.php Normal file
View File

@ -0,0 +1,245 @@
<?php
/**
* Contains pure functions for converting AST nodes from and to FQNs
*
* Examples of FQNs:
* - testFunction()
* - TestNamespace\TestClass
* - TestNamespace\TestClass::TEST_CONSTANT
* - TestNamespace\TestClass::staticTestProperty
* - TestNamespace\TestClass::testProperty
* - TestNamespace\TestClass::staticTestMethod()
* - TestNamespace\TestClass::testMethod()
*/
namespace LanguageServer\Fqn;
use PhpParser\Node;
/**
* Returns the FQN that is referenced by a node
*
* @param Node $node
* @return string|null
*/
function getReferencedFqn(Node $node)
{
$parent = $node->getAttribute('parentNode');
if (
$node instanceof Node\Name && (
$parent instanceof Node\Stmt\ClassLike
|| $parent instanceof Node\Param
|| $parent instanceof Node\FunctionLike
|| $parent instanceof Node\Expr\StaticCall
|| $parent instanceof Node\Expr\ClassConstFetch
|| $parent instanceof Node\Expr\StaticPropertyFetch
)
) {
// For extends, implements, type hints and classes of classes of static calls use the name directly
$name = (string)$node;
// Only the name node should be considered a reference, not the UseUse node itself
} else if ($parent instanceof Node\Stmt\UseUse) {
$name = (string)$parent->name;
$grandParent = $parent->getAttribute('parentNode');
if ($grandParent instanceof Node\Stmt\GroupUse) {
$name = $grandParent->prefix . '\\' . $name;
}
// Only the name node should be considered a reference, not the New_ node itself
} else if ($parent instanceof Node\Expr\New_) {
if (!($parent->class instanceof Node\Name)) {
// Cannot get definition of dynamic calls
return null;
}
$name = (string)$parent->class;
} else if ($node instanceof Node\Expr\MethodCall || $node instanceof Node\Expr\PropertyFetch) {
if ($node->name instanceof Node\Expr || !($node->var instanceof Node\Expr\Variable)) {
// Cannot get definition of dynamic calls
return null;
}
// Need to resolve variable to a class
if ($node->var->name === 'this') {
// $this resolved to the class it is contained in
$n = $node;
while ($n = $n->getAttribute('parentNode')) {
if ($n instanceof Node\Stmt\Class_) {
if ($n->isAnonymous()) {
return null;
}
$name = (string)$n->namespacedName;
break;
}
}
if (!isset($name)) {
return null;
}
} else {
// Other variables resolve to their definition
$varDef = 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 ($parent instanceof Node\Expr\FuncCall) {
if ($parent->name instanceof Node\Expr) {
return null;
}
$name = (string)($node->getAttribute('namespacedName') ?? $parent->name);
} else if ($parent instanceof Node\Expr\ConstFetch) {
$name = (string)($node->getAttribute('namespacedName') ?? $parent->name);
} else if (
$node instanceof Node\Expr\ClassConstFetch
|| $node instanceof Node\Expr\StaticPropertyFetch
|| $node instanceof Node\Expr\StaticCall
) {
if ($node->class instanceof Node\Expr || $node->name instanceof Node\Expr) {
// Cannot get definition of dynamic names
return null;
}
$className = (string)$node->class;
if ($className === 'self' || $className === 'static' || $className === 'parent') {
// self and static are resolved to the containing class
$n = $node;
while ($n = $n->getAttribute('parentNode')) {
if ($n instanceof Node\Stmt\Class_) {
if ($n->isAnonymous()) {
return null;
}
if ($className === 'parent') {
// parent is resolved to the parent class
if (!isset($n->extends)) {
return null;
}
$className = (string)$n->extends;
} else {
$className = (string)$n->namespacedName;
}
break;
}
}
}
$name = (string)$className . '::' . $node->name;
} else {
return null;
}
if (
$node instanceof Node\Expr\MethodCall
|| $node instanceof Node\Expr\StaticCall
|| $parent instanceof Node\Expr\FuncCall
) {
$name .= '()';
}
if (!isset($name)) {
return null;
}
return $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
*/
function getVariableDefinition(Node\Expr\Variable $var)
{
$n = $var;
// Traverse the AST up
do {
// 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 instanceof Node\Expr\Variable && $n->var->name === $var->name) {
return $n;
}
}
} while (isset($n) && $n = $n->getAttribute('parentNode'));
// Return null if nothing was found
return null;
}
/**
* Returns the fully qualified name (FQN) that is defined by a node
*
* @param Node $node
* @return string|null
*/
function getDefinedFqn(Node $node)
{
// Anonymous classes don't count as a definition
if ($node instanceof Node\Stmt\ClassLike && isset($node->name)) {
// Class, interface or trait declaration
return (string)$node->namespacedName;
} else if ($node instanceof Node\Stmt\Function_) {
// Function: use functionName() as the name
return (string)$node->namespacedName . '()';
} 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 null;
}
return (string)$class->namespacedName . '::' . (string)$node->name . '()';
} 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 null;
}
return (string)$class->namespacedName . '::' . (string)$node->name;
} else if ($node instanceof Node\Const_) {
$parent = $node->getAttribute('parentNode');
if ($parent instanceof Node\Stmt\Const_) {
// Basic constant: use CONSTANT_NAME as name
return (string)$node->namespacedName;
}
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 null;
}
return (string)$class->namespacedName . '::' . $node->name;
}
}
}

View File

@ -10,13 +10,16 @@ use LanguageServer\Protocol\{
TextDocumentSyncKind, TextDocumentSyncKind,
Message, Message,
MessageType, MessageType,
InitializeResult InitializeResult,
SymbolInformation
}; };
use AdvancedJsonRpc\{Dispatcher, ResponseError, Response as ResponseBody, Request as RequestBody}; use AdvancedJsonRpc;
use Sabre\Event\Loop; use Sabre\Event\Loop;
use JsonMapper;
use Exception; use Exception;
use Throwable;
class LanguageServer extends \AdvancedJsonRpc\Dispatcher class LanguageServer extends AdvancedJsonRpc\Dispatcher
{ {
/** /**
* Handles textDocument/* method calls * Handles textDocument/* method calls
@ -41,6 +44,12 @@ class LanguageServer extends \AdvancedJsonRpc\Dispatcher
private $protocolWriter; private $protocolWriter;
private $client; private $client;
/**
* The root project path that was passed to initialize()
*
* @var string
*/
private $rootPath;
private $project; private $project;
public function __construct(ProtocolReader $reader, ProtocolWriter $writer) public function __construct(ProtocolReader $reader, ProtocolWriter $writer)
@ -48,27 +57,27 @@ class LanguageServer extends \AdvancedJsonRpc\Dispatcher
parent::__construct($this, '/'); parent::__construct($this, '/');
$this->protocolReader = $reader; $this->protocolReader = $reader;
$this->protocolReader->onMessage(function (Message $msg) { $this->protocolReader->onMessage(function (Message $msg) {
$err = null;
$result = null; $result = null;
$error = null;
try { try {
// Invoke the method handler to get a result // Invoke the method handler to get a result
$result = $this->dispatch($msg->body); $result = $this->dispatch($msg->body);
} catch (ResponseError $e) { } catch (AdvancedJsonRpc\Error $e) {
// If a ResponseError is thrown, send it back in the Response (result will be null) // If a ResponseError is thrown, send it back in the Response
$err = $e; $error = $e;
} catch (Throwable $e) { } catch (Throwable $e) {
// If an unexpected error occured, send back an INTERNAL_ERROR error response (result will be null) // If an unexpected error occured, send back an INTERNAL_ERROR error response
$err = new ResponseError( $error = new AdvancedJsonRpc\Error($e->getMessage(), AdvancedJsonRpc\ErrorCode::INTERNAL_ERROR, null, $e);
$e->getMessage(),
$e->getCode() === 0 ? ErrorCode::INTERNAL_ERROR : $e->getCode(),
null,
$e
);
} }
// Only send a Response for a Request // Only send a Response for a Request
// Notifications do not send Responses // Notifications do not send Responses
if (RequestBody::isRequest($msg->body)) { if (AdvancedJsonRpc\Request::isRequest($msg->body)) {
$this->protocolWriter->write(new Message(new ResponseBody($msg->body->id, $result, $err))); if ($error !== null) {
$responseBody = new AdvancedJsonRpc\ErrorResponse($msg->body->id, $error);
} else {
$responseBody = new AdvancedJsonRpc\SuccessResponse($msg->body->id, $result);
}
$this->protocolWriter->write(new Message($responseBody));
} }
}); });
$this->protocolWriter = $writer; $this->protocolWriter = $writer;
@ -90,9 +99,12 @@ class LanguageServer extends \AdvancedJsonRpc\Dispatcher
*/ */
public function initialize(int $processId, ClientCapabilities $capabilities, string $rootPath = null): InitializeResult public function initialize(int $processId, ClientCapabilities $capabilities, string $rootPath = null): InitializeResult
{ {
$this->rootPath = $rootPath;
// start building project index // start building project index
if ($rootPath) { if ($rootPath !== null) {
$this->indexProject($rootPath); $this->restoreCache();
$this->indexProject();
} }
$serverCapabilities = new ServerCapabilities(); $serverCapabilities = new ServerCapabilities();
@ -123,7 +135,9 @@ class LanguageServer extends \AdvancedJsonRpc\Dispatcher
*/ */
public function shutdown() public function shutdown()
{ {
if ($this->rootPath !== null) {
$this->saveCache();
}
} }
/** /**
@ -139,23 +153,23 @@ class LanguageServer extends \AdvancedJsonRpc\Dispatcher
/** /**
* Parses workspace files, one at a time. * Parses workspace files, one at a time.
* *
* @param string $rootPath The rootPath of the workspace.
* @return void * @return void
*/ */
private function indexProject(string $rootPath) private function indexProject()
{ {
$fileList = findFilesRecursive($rootPath, '/^.+\.php$/i'); $fileList = findFilesRecursive($this->rootPath, '/^.+\.php$/i');
$numTotalFiles = count($fileList); $numTotalFiles = count($fileList);
$startTime = microtime(true); $startTime = microtime(true);
$fileNum = 0; $fileNum = 0;
$processFile = function() use (&$fileList, &$fileNum, &$processFile, $rootPath, $numTotalFiles, $startTime) { $processFile = function() use (&$fileList, &$fileNum, &$processFile, $numTotalFiles, $startTime) {
if ($fileNum < $numTotalFiles) { if ($fileNum < $numTotalFiles) {
$file = $fileList[$fileNum]; $file = $fileList[$fileNum];
$uri = pathToUri($file); $uri = pathToUri($file);
$fileNum++; $fileNum++;
$shortName = substr($file, strlen($rootPath) + 1); $shortName = substr($file, strlen($this->rootPath) + 1);
$this->client->window->logMessage(MessageType::INFO, "Parsing file $fileNum/$numTotalFiles: $shortName.");
if (filesize($file) > 500000) { if (filesize($file) > 500000) {
$this->client->window->logMessage(MessageType::INFO, "Not parsing $shortName because it exceeds size limit of 0.5MB"); $this->client->window->logMessage(MessageType::INFO, "Not parsing $shortName because it exceeds size limit of 0.5MB");
@ -168,14 +182,69 @@ class LanguageServer extends \AdvancedJsonRpc\Dispatcher
} }
} }
if ($fileNum % 1000 === 0) {
$this->saveCache();
}
Loop\setTimeout($processFile, 0); Loop\setTimeout($processFile, 0);
} else { } else {
$duration = (int)(microtime(true) - $startTime); $duration = (int)(microtime(true) - $startTime);
$mem = (int)(memory_get_usage(true) / (1024 * 1024)); $mem = (int)(memory_get_usage(true) / (1024 * 1024));
$this->client->window->logMessage(MessageType::INFO, "All PHP files parsed in $duration seconds. $mem MiB allocated."); $this->client->window->logMessage(MessageType::INFO, "All PHP files parsed in $duration seconds. $mem MiB allocated.");
$this->saveCache();
} }
}; };
Loop\setTimeout($processFile, 0); Loop\setTimeout($processFile, 0);
} }
/**
* Restores the definition and reference index from the .phpls cache directory, if available
*
* @return void
*/
public function restoreCache()
{
$cacheDir = $this->rootPath . '/.phpls';
if (is_dir($cacheDir)) {
if (file_exists($cacheDir . '/symbols')) {
$symbols = unserialize(file_get_contents($cacheDir . '/symbols'));
$count = count($symbols);
$this->project->setSymbols($symbols);
$this->client->window->logMessage(MessageType::INFO, "Restoring $count symbols");
}
if (file_exists($cacheDir . '/references')) {
$references = unserialize(file_get_contents($cacheDir . '/references'));
$count = array_sum(array_map('count', $references));
$this->project->setReferenceUris($references);
$this->client->window->logMessage(MessageType::INFO, "Restoring $count references");
}
} else {
$this->client->window->logMessage(MessageType::INFO, 'No cache found');
}
}
/**
* Saves the definition and reference index to the .phpls cache directory
*
* @return void
*/
public function saveCache()
{
// Cache definitions, references
$cacheDir = $this->rootPath . '/.phpls';
if (!is_dir($cacheDir)) {
mkdir($cacheDir);
}
$symbols = $this->project->getSymbols();
$count = count($symbols);
$this->client->window->logMessage(MessageType::INFO, "Saving $count symbols to cache");
file_put_contents($cacheDir . "/symbols", serialize($symbols));
$references = $this->project->getReferenceUris();
$count = array_sum(array_map('count', $references));
$this->client->window->logMessage(MessageType::INFO, "Saving $count references to cache");
file_put_contents($cacheDir . "/references", serialize($references));
}
} }

View File

@ -4,6 +4,8 @@ declare(strict_types = 1);
namespace LanguageServer\NodeVisitor; namespace LanguageServer\NodeVisitor;
use PhpParser\{NodeVisitorAbstract, Node}; use PhpParser\{NodeVisitorAbstract, Node};
use LanguageServer\Protocol\SymbolInformation;
use function LanguageServer\Fqn\getDefinedFqn;
/** /**
* Collects definitions of classes, interfaces, traits, methods, properties and constants * Collects definitions of classes, interfaces, traits, methods, properties and constants
@ -18,11 +20,21 @@ class DefinitionCollector extends NodeVisitorAbstract
*/ */
public $definitions = []; public $definitions = [];
/**
* Map from FQN to SymbolInformation
*
* @var SymbolInformation
*/
public $symbols = [];
public function enterNode(Node $node) public function enterNode(Node $node)
{ {
$fqn = $node->getAttribute('ownerDocument')->getDefinedFqn($node); $fqn = getDefinedFqn($node);
if ($fqn !== null) { if ($fqn === null) {
return;
}
$this->definitions[$fqn] = $node; $this->definitions[$fqn] = $node;
} $symbol = SymbolInformation::fromNode($node, $fqn);
$this->symbols[$fqn] = $symbol;
} }
} }

View File

@ -35,11 +35,6 @@ class NodeAtPositionFinder extends NodeVisitorAbstract
public function leaveNode(Node $node) public function leaveNode(Node $node)
{ {
$range = Range::fromNode($node); $range = Range::fromNode($node);
// 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;
} }

View File

@ -3,6 +3,7 @@ declare(strict_types = 1);
namespace LanguageServer\NodeVisitor; namespace LanguageServer\NodeVisitor;
use function LanguageServer\Fqn\getReferencedFqn;
use PhpParser\{NodeVisitorAbstract, Node}; use PhpParser\{NodeVisitorAbstract, Node};
/** /**
@ -21,7 +22,7 @@ class ReferencesCollector extends NodeVisitorAbstract
public function enterNode(Node $node) public function enterNode(Node $node)
{ {
// Check if the node references any global symbol // Check if the node references any global symbol
$fqn = $node->getAttribute('ownerDocument')->getReferencedFqn($node); $fqn = getReferencedFqn($node);
if ($fqn) { if ($fqn) {
$this->addReference($fqn, $node); $this->addReference($fqn, $node);
// Namespaced constant access and function calls also need to register a reference // Namespaced constant access and function calls also need to register a reference

View File

@ -16,6 +16,7 @@ use LanguageServer\NodeVisitor\{
use PhpParser\{Error, Node, NodeTraverser, Parser}; use PhpParser\{Error, Node, NodeTraverser, Parser};
use PhpParser\NodeVisitor\NameResolver; use PhpParser\NodeVisitor\NameResolver;
use phpDocumentor\Reflection\DocBlockFactory; use phpDocumentor\Reflection\DocBlockFactory;
use function LanguageServer\Fqn\{getDefinedFqn, getVariableDefinition, getReferencedFqn};
class PhpDocument class PhpDocument
{ {
@ -84,6 +85,13 @@ class PhpDocument
*/ */
private $references; private $references;
/**
* Map from fully qualified name (FQN) to SymbolInformation
*
* @var SymbolInformation[]
*/
private $symbols;
/** /**
* @param string $uri The URI of the document * @param string $uri The URI of the document
* @param string $content The content of the document * @param string $content The content of the document
@ -172,7 +180,7 @@ class PhpDocument
$traverser->addVisitor($definitionCollector); $traverser->addVisitor($definitionCollector);
// Collect all references // Collect all references
$referencesCollector = new ReferencesCollector($this->definitions); $referencesCollector = new ReferencesCollector;
$traverser->addVisitor($referencesCollector); $traverser->addVisitor($referencesCollector);
$traverser->traverse($stmts); $traverser->traverse($stmts);
@ -180,13 +188,14 @@ class PhpDocument
// Unregister old definitions // Unregister old definitions
if (isset($this->definitions)) { if (isset($this->definitions)) {
foreach ($this->definitions as $fqn => $node) { foreach ($this->definitions as $fqn => $node) {
$this->project->removeDefinition($fqn); $this->project->removeSymbol($fqn);
} }
} }
// Register this document on the project for all the symbols defined in it // Register this document on the project for all the symbols defined in it
$this->definitions = $definitionCollector->definitions; $this->definitions = $definitionCollector->definitions;
foreach ($definitionCollector->definitions as $fqn => $node) { $this->symbols = $definitionCollector->symbols;
$this->project->setDefinitionUri($fqn, $this->uri); foreach ($definitionCollector->symbols as $fqn => $symbol) {
$this->project->setSymbol($fqn, $symbol);
} }
// Unregister old references // Unregister old references
@ -286,6 +295,16 @@ class PhpDocument
return $this->definitions; return $this->definitions;
} }
/**
* Returns a map from fully qualified name (FQN) to SymbolInformation
*
* @return SymbolInformation[]
*/
public function getSymbols()
{
return $this->symbols;
}
/** /**
* Returns true if the given FQN is defined in this document * Returns true if the given FQN is defined in this document
* *
@ -297,162 +316,6 @@ class PhpDocument
return isset($this->definitions[$fqn]); return isset($this->definitions[$fqn]);
} }
/**
* Returns the fully qualified name (FQN) that is defined by a node
* Examples of FQNs:
* - testFunction()
* - TestNamespace\TestClass
* - TestNamespace\TestClass::TEST_CONSTANT
* - TestNamespace\TestClass::staticTestProperty
* - TestNamespace\TestClass::testProperty
* - TestNamespace\TestClass::staticTestMethod()
* - TestNamespace\TestClass::testMethod()
*
* @param Node $node
* @return string|null
*/
public function getDefinedFqn(Node $node)
{
// Anonymous classes don't count as a definition
if ($node instanceof Node\Stmt\ClassLike && isset($node->name)) {
// Class, interface or trait declaration
return (string)$node->namespacedName;
} else if ($node instanceof Node\Stmt\Function_) {
// Function: use functionName() as the name
return (string)$node->namespacedName . '()';
} 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 null;
}
return (string)$class->namespacedName . '::' . (string)$node->name . '()';
} 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 null;
}
return (string)$class->namespacedName . '::' . (string)$node->name;
} else if ($node instanceof Node\Const_) {
$parent = $node->getAttribute('parentNode');
if ($parent instanceof Node\Stmt\Const_) {
// Basic constant: use CONSTANT_NAME as name
return (string)$node->namespacedName;
}
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 null;
}
return (string)$class->namespacedName . '::' . $node->name;
}
}
}
/**
* Returns the FQN that is referenced by a node
*
* @param Node $node
* @return string|null
*/
public function getReferencedFqn(Node $node)
{
$parent = $node->getAttribute('parentNode');
if (
$node instanceof Node\Name && (
$parent instanceof Node\Stmt\ClassLike
|| $parent instanceof Node\Param
|| $parent instanceof Node\FunctionLike
|| $parent instanceof Node\Expr\StaticCall
|| $parent instanceof Node\Expr\ClassConstFetch
|| $parent instanceof Node\Expr\StaticPropertyFetch
)
) {
// For extends, implements, type hints and classes of classes of static calls use the name directly
$name = (string)$node;
// Only the name node should be considered a reference, not the UseUse node itself
} else if ($parent instanceof Node\Stmt\UseUse) {
$name = (string)$parent->name;
$grandParent = $parent->getAttribute('parentNode');
if ($grandParent instanceof Node\Stmt\GroupUse) {
$name = $grandParent->prefix . '\\' . $name;
}
// Only the name node should be considered a reference, not the New_ node itself
} else if ($parent instanceof Node\Expr\New_) {
if (!($parent->class instanceof Node\Name)) {
// Cannot get definition of dynamic calls
return null;
}
$name = (string)$parent->class;
} else if ($node instanceof Node\Expr\MethodCall || $node instanceof Node\Expr\PropertyFetch) {
if ($node->name instanceof Node\Expr || !($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 ($parent instanceof Node\Expr\FuncCall) {
if ($parent->name instanceof Node\Expr) {
return null;
}
$name = (string)($node->getAttribute('namespacedName') ?? $parent->name);
} else if ($parent instanceof Node\Expr\ConstFetch) {
$name = (string)($node->getAttribute('namespacedName') ?? $parent->name);
} else if (
$node instanceof Node\Expr\ClassConstFetch
|| $node instanceof Node\Expr\StaticPropertyFetch
|| $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;
} else {
return null;
}
if (
$node instanceof Node\Expr\MethodCall
|| $node instanceof Node\Expr\StaticCall
|| $parent instanceof Node\Expr\FuncCall
) {
$name .= '()';
}
if (!isset($name)) {
return null;
}
return $name;
}
/** /**
* Returns the definition node for any node * Returns the definition node for any node
* The definition node MAY be in another document, check the ownerDocument attribute * The definition node MAY be in another document, check the ownerDocument attribute
@ -465,9 +328,9 @@ class PhpDocument
// Variables always stay in the boundary of the file and need to be searched inside their function scope // Variables always stay in the boundary of the file and need to be searched inside their function scope
// by traversing the AST // by traversing the AST
if ($node instanceof Node\Expr\Variable) { if ($node instanceof Node\Expr\Variable) {
return $this->getVariableDefinition($node); return getVariableDefinition($node);
} }
$fqn = $this->getReferencedFqn($node); $fqn = getReferencedFqn($node);
if (!isset($fqn)) { if (!isset($fqn)) {
return null; return null;
} }
@ -518,7 +381,7 @@ class PhpDocument
return $refCollector->references; return $refCollector->references;
} }
// Definition with a global FQN // Definition with a global FQN
$fqn = $this->getDefinedFqn($node); $fqn = getDefinedFqn($node);
if ($fqn === null) { if ($fqn === null) {
return []; return [];
} }
@ -534,43 +397,4 @@ class PhpDocument
} }
return $nodes; return $nodes;
} }
/**
* 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
do {
// 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 instanceof Node\Expr\Variable && $n->var->name === $var->name) {
return $n;
}
}
} while (isset($n) && $n = $n->getAttribute('parentNode'));
// Return null if nothing was found
return null;
}
} }

View File

@ -3,6 +3,7 @@ declare(strict_types = 1);
namespace LanguageServer; namespace LanguageServer;
use LanguageServer\Protocol\SymbolInformation;
use phpDocumentor\Reflection\DocBlockFactory; use phpDocumentor\Reflection\DocBlockFactory;
use PhpParser\{ParserFactory, Lexer}; use PhpParser\{ParserFactory, Lexer};
@ -17,11 +18,11 @@ class Project
private $documents = []; private $documents = [];
/** /**
* An associative array that maps fully qualified symbol names to document URIs that define the symbol * An associative array that maps fully qualified symbol names to SymbolInformations
* *
* @var string[] * @var SymbolInformation[]
*/ */
private $definitions = []; private $symbols = [];
/** /**
* An associative array that maps fully qualified symbol names to arrays of document URIs that reference the symbol * An associative array that maps fully qualified symbol names to arrays of document URIs that reference the symbol
@ -140,34 +141,45 @@ class Project
* Returns an associative array [string => string] that maps fully qualified symbol names * Returns an associative array [string => string] that maps fully qualified symbol names
* to URIs of the document where the symbol is defined * to URIs of the document where the symbol is defined
* *
* @return PhpDocument[] * @return SymbolInformation[]
*/ */
public function getDefinitionUris() public function getSymbols()
{ {
return $this->definitions; return $this->symbols;
} }
/** /**
* Adds a document URI as the container for a specific symbol * Adds a SymbolInformation for a specific symbol
* *
* @param string $fqn The fully qualified name of the symbol * @param string $fqn The fully qualified name of the symbol
* @param string $uri The URI * @param string $uri The URI
* @return void * @return void
*/ */
public function setDefinitionUri(string $fqn, string $uri) public function setSymbol(string $fqn, SymbolInformation $symbol)
{ {
$this->definitions[$fqn] = $uri; $this->symbols[$fqn] = $symbol;
} }
/** /**
* Unsets a document URI as the container for a specific symbol * Sets the SymbolInformation index
*
* @param SymbolInformation[] $symbols
* @return void
*/
public function setSymbols(array $symbols)
{
$this->symbols = $symbols;
}
/**
* Unsets the SymbolInformation for a specific symbol
* and removes all references pointing to that symbol * and removes all references pointing to that symbol
* *
* @param string $fqn The fully qualified name of the symbol * @param string $fqn The fully qualified name of the symbol
* @return void * @return void
*/ */
public function removeDefinition(string $fqn) { public function removeSymbol(string $fqn) {
unset($this->definitions[$fqn]); unset($this->symbols[$fqn]);
unset($this->references[$fqn]); unset($this->references[$fqn]);
} }
@ -220,6 +232,28 @@ class Project
return array_map([$this, 'getDocument'], $this->references[$fqn]); return array_map([$this, 'getDocument'], $this->references[$fqn]);
} }
/**
* Returns an associative array [string => string[]] that maps fully qualified symbol names
* to URIs of the document where the symbol is referenced
*
* @return string[][]
*/
public function getReferenceUris()
{
return $this->references;
}
/**
* Sets the reference index
*
* @param string[][] $references an associative array [string => string[]] from FQN to URIs
* @return void
*/
public function setReferenceUris(array $references)
{
$this->references = $references;
}
/** /**
* Returns the document where a symbol is defined * Returns the document where a symbol is defined
* *
@ -228,7 +262,7 @@ class Project
*/ */
public function getDefinitionDocument(string $fqn) public function getDefinitionDocument(string $fqn)
{ {
return isset($this->definitions[$fqn]) ? $this->getDocument($this->definitions[$fqn]) : null; return isset($this->symbols[$fqn]) ? $this->getDocument($this->symbols[$fqn]->location->uri) : null;
} }
/** /**

View File

@ -21,7 +21,7 @@ class SymbolInformation
/** /**
* The kind of this symbol. * The kind of this symbol.
* *
* @var number * @var int
*/ */
public $kind; public $kind;
@ -55,7 +55,7 @@ class SymbolInformation
Node\Stmt\Namespace_::class => SymbolKind::NAMESPACE, Node\Stmt\Namespace_::class => SymbolKind::NAMESPACE,
Node\Stmt\Function_::class => SymbolKind::FUNCTION, Node\Stmt\Function_::class => SymbolKind::FUNCTION,
Node\Stmt\ClassMethod::class => SymbolKind::METHOD, Node\Stmt\ClassMethod::class => SymbolKind::METHOD,
Node\Stmt\PropertyProperty::class => SymbolKind::FIELD, Node\Stmt\PropertyProperty::class => SymbolKind::PROPERTY,
Node\Const_::class => SymbolKind::CONSTANT Node\Const_::class => SymbolKind::CONSTANT
]; ];
$class = get_class($node); $class = get_class($node);

View File

@ -59,11 +59,7 @@ class TextDocument
*/ */
public function documentSymbol(TextDocumentIdentifier $textDocument): array public function documentSymbol(TextDocumentIdentifier $textDocument): array
{ {
$symbols = []; return array_values($this->project->getDocument($textDocument->uri)->getSymbols());
foreach ($this->project->getDocument($textDocument->uri)->getDefinitions() as $fqn => $node) {
$symbols[] = SymbolInformation::fromNode($node, $fqn);
}
return $symbols;
} }
/** /**

View File

@ -39,10 +39,13 @@ class Workspace
*/ */
public function symbol(string $query): array public function symbol(string $query): array
{ {
if ($query === '') {
return array_values($this->project->getSymbols());
}
$symbols = []; $symbols = [];
foreach ($this->project->getDefinitionUris() as $fqn => $uri) { foreach ($this->project->getSymbols() as $fqn => $symbol) {
if ($query === '' || stripos($fqn, $query) !== false) { if (stripos($fqn, $query) !== false) {
$symbols[] = SymbolInformation::fromNode($this->project->getDocument($uri)->getDefinitionByFqn($fqn), $fqn); $symbols[] = $symbol;
} }
} }
return $symbols; return $symbols;

View File

@ -6,7 +6,7 @@ namespace LanguageServer\Tests;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use LanguageServer\LanguageServer; use LanguageServer\LanguageServer;
use LanguageServer\Protocol\{Message, ClientCapabilities, TextDocumentSyncKind}; use LanguageServer\Protocol\{Message, ClientCapabilities, TextDocumentSyncKind};
use AdvancedJsonRpc\{Request as RequestBody, Response as ResponseBody}; use AdvancedJsonRpc;
class LanguageServerTest extends TestCase class LanguageServerTest extends TestCase
{ {
@ -19,14 +19,13 @@ class LanguageServerTest extends TestCase
$writer->onMessage(function (Message $message) use (&$msg) { $writer->onMessage(function (Message $message) use (&$msg) {
$msg = $message; $msg = $message;
}); });
$reader->write(new Message(new RequestBody(1, 'initialize', [ $reader->write(new Message(new AdvancedJsonRpc\Request(1, 'initialize', [
'rootPath' => __DIR__, 'rootPath' => __DIR__,
'processId' => getmypid(), 'processId' => getmypid(),
'capabilities' => new ClientCapabilities() 'capabilities' => new ClientCapabilities()
]))); ])));
$this->assertNotNull($msg, 'onMessage callback should be called'); $this->assertNotNull($msg, 'onMessage callback should be called');
$this->assertInstanceOf(ResponseBody::class, $msg->body); $this->assertInstanceOf(AdvancedJsonRpc\SuccessResponse::class, $msg->body);
$this->assertNull($msg->body->error);
$this->assertEquals((object)[ $this->assertEquals((object)[
'capabilities' => (object)[ 'capabilities' => (object)[
'textDocumentSync' => TextDocumentSyncKind::FULL, 'textDocumentSync' => TextDocumentSyncKind::FULL,

View File

@ -108,10 +108,12 @@ abstract class ServerTestCase extends TestCase
1 => new Location($symbolsUri, new Range(new Position(57, 48), new Position(57, 61))) // public function testMethod($testParameter): TestInterface 1 => new Location($symbolsUri, new Range(new Position(57, 48), new Position(57, 61))) // public function testMethod($testParameter): TestInterface
], ],
'TestNamespace\\TestClass::TEST_CLASS_CONST' => [ 'TestNamespace\\TestClass::TEST_CLASS_CONST' => [
0 => new Location($referencesUri, new Range(new Position( 9, 5), new Position( 9, 32))) 0 => new Location($symbolsUri, new Range(new Position(48, 13), new Position(48, 35))), // echo self::TEST_CLASS_CONSTANT
1 => new Location($referencesUri, new Range(new Position( 9, 5), new Position( 9, 32)))
], ],
'TestNamespace\\TestClass::testProperty' => [ 'TestNamespace\\TestClass::testProperty' => [
0 => new Location($referencesUri, new Range(new Position( 6, 5), new Position( 6, 23))) 0 => new Location($symbolsUri, new Range(new Position(59, 8), new Position(59, 27))), // $this->testProperty = $testParameter;
1 => new Location($referencesUri, new Range(new Position( 6, 5), new Position( 6, 23)))
], ],
'TestNamespace\\TestClass::staticTestProperty' => [ 'TestNamespace\\TestClass::staticTestProperty' => [
0 => new Location($referencesUri, new Range(new Position( 8, 5), new Position( 8, 35))) 0 => new Location($referencesUri, new Range(new Position( 8, 5), new Position( 8, 35)))
@ -144,10 +146,12 @@ abstract class ServerTestCase extends TestCase
1 => new Location($globalSymbolsUri, new Range(new Position(57, 48), new Position(57, 61))) // public function testMethod($testParameter): TestInterface 1 => new Location($globalSymbolsUri, new Range(new Position(57, 48), new Position(57, 61))) // public function testMethod($testParameter): TestInterface
], ],
'TestClass::TEST_CLASS_CONST' => [ 'TestClass::TEST_CLASS_CONST' => [
0 => new Location($globalReferencesUri, new Range(new Position( 9, 5), new Position( 9, 32))) 0 => new Location($globalSymbolsUri, new Range(new Position(48, 13), new Position(48, 35))), // echo self::TEST_CLASS_CONSTANT
1 => new Location($globalReferencesUri, new Range(new Position( 9, 5), new Position( 9, 32)))
], ],
'TestClass::testProperty' => [ 'TestClass::testProperty' => [
0 => new Location($globalReferencesUri, new Range(new Position( 6, 5), new Position( 6, 23))) 0 => new Location($globalSymbolsUri, new Range(new Position(59, 8), new Position(59, 27))), // $this->testProperty = $testParameter;
1 => new Location($globalReferencesUri, new Range(new Position( 6, 5), new Position( 6, 23)))
], ],
'TestClass::staticTestProperty' => [ 'TestClass::staticTestProperty' => [
0 => new Location($globalReferencesUri, new Range(new Position( 8, 5), new Position( 8, 35))) 0 => new Location($globalReferencesUri, new Range(new Position( 8, 5), new Position( 8, 35)))

View File

@ -70,6 +70,15 @@ class GlobalTest extends ServerTestCase
{ {
// echo TestClass::TEST_CLASS_CONST; // echo TestClass::TEST_CLASS_CONST;
// Get definition for TEST_CLASS_CONST // Get definition for TEST_CLASS_CONST
$reference = $this->getReferenceLocations('TestClass::TEST_CLASS_CONST')[1];
$result = $this->textDocument->definition(new TextDocumentIdentifier($reference->uri), $reference->range->end);
$this->assertEquals($this->getDefinitionLocation('TestClass::TEST_CLASS_CONST'), $result);
}
public function testDefinitionForClassConstantsOnSelf()
{
// echo self::TEST_CLASS_CONST;
// Get definition for TEST_CLASS_CONST
$reference = $this->getReferenceLocations('TestClass::TEST_CLASS_CONST')[0]; $reference = $this->getReferenceLocations('TestClass::TEST_CLASS_CONST')[0];
$result = $this->textDocument->definition(new TextDocumentIdentifier($reference->uri), $reference->range->end); $result = $this->textDocument->definition(new TextDocumentIdentifier($reference->uri), $reference->range->end);
$this->assertEquals($this->getDefinitionLocation('TestClass::TEST_CLASS_CONST'), $result); $this->assertEquals($this->getDefinitionLocation('TestClass::TEST_CLASS_CONST'), $result);
@ -115,6 +124,15 @@ class GlobalTest extends ServerTestCase
{ {
// echo $obj->testProperty; // echo $obj->testProperty;
// Get definition for testProperty // Get definition for testProperty
$reference = $this->getReferenceLocations('TestClass::testProperty')[1];
$result = $this->textDocument->definition(new TextDocumentIdentifier($reference->uri), $reference->range->end);
$this->assertEquals($this->getDefinitionLocation('TestClass::testProperty'), $result);
}
public function testDefinitionForPropertiesOnThis()
{
// $this->testProperty = $testParameter;
// Get definition for testProperty
$reference = $this->getReferenceLocations('TestClass::testProperty')[0]; $reference = $this->getReferenceLocations('TestClass::testProperty')[0];
$result = $this->textDocument->definition(new TextDocumentIdentifier($reference->uri), $reference->range->end); $result = $this->textDocument->definition(new TextDocumentIdentifier($reference->uri), $reference->range->end);
$this->assertEquals($this->getDefinitionLocation('TestClass::testProperty'), $result); $this->assertEquals($this->getDefinitionLocation('TestClass::testProperty'), $result);

View File

@ -20,8 +20,8 @@ class DocumentSymbolTest extends ServerTestCase
new SymbolInformation('TEST_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TestNamespace\\TEST_CONST'), 'TestNamespace'), new SymbolInformation('TEST_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TestNamespace\\TEST_CONST'), 'TestNamespace'),
new SymbolInformation('TestClass', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\TestClass'), 'TestNamespace'), new SymbolInformation('TestClass', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\TestClass'), 'TestNamespace'),
new SymbolInformation('TEST_CLASS_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TestNamespace\\TestClass::TEST_CLASS_CONST'), 'TestNamespace\\TestClass'), new SymbolInformation('TEST_CLASS_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TestNamespace\\TestClass::TEST_CLASS_CONST'), 'TestNamespace\\TestClass'),
new SymbolInformation('staticTestProperty', SymbolKind::FIELD, $this->getDefinitionLocation('TestNamespace\\TestClass::staticTestProperty'), 'TestNamespace\\TestClass'), new SymbolInformation('staticTestProperty', SymbolKind::PROPERTY, $this->getDefinitionLocation('TestNamespace\\TestClass::staticTestProperty'), 'TestNamespace\\TestClass'),
new SymbolInformation('testProperty', SymbolKind::FIELD, $this->getDefinitionLocation('TestNamespace\\TestClass::testProperty'), 'TestNamespace\\TestClass'), new SymbolInformation('testProperty', SymbolKind::PROPERTY, $this->getDefinitionLocation('TestNamespace\\TestClass::testProperty'), 'TestNamespace\\TestClass'),
new SymbolInformation('staticTestMethod', SymbolKind::METHOD, $this->getDefinitionLocation('TestNamespace\\TestClass::staticTestMethod()'), 'TestNamespace\\TestClass'), new SymbolInformation('staticTestMethod', SymbolKind::METHOD, $this->getDefinitionLocation('TestNamespace\\TestClass::staticTestMethod()'), 'TestNamespace\\TestClass'),
new SymbolInformation('testMethod', SymbolKind::METHOD, $this->getDefinitionLocation('TestNamespace\\TestClass::testMethod()'), 'TestNamespace\\TestClass'), new SymbolInformation('testMethod', SymbolKind::METHOD, $this->getDefinitionLocation('TestNamespace\\TestClass::testMethod()'), 'TestNamespace\\TestClass'),
new SymbolInformation('TestTrait', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\TestTrait'), 'TestNamespace'), new SymbolInformation('TestTrait', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\TestTrait'), 'TestNamespace'),

View File

@ -21,8 +21,8 @@ class SymbolTest extends ServerTestCase
new SymbolInformation('TEST_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TestNamespace\\TEST_CONST'), 'TestNamespace'), new SymbolInformation('TEST_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TestNamespace\\TEST_CONST'), 'TestNamespace'),
new SymbolInformation('TestClass', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\TestClass'), 'TestNamespace'), new SymbolInformation('TestClass', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\TestClass'), 'TestNamespace'),
new SymbolInformation('TEST_CLASS_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TestNamespace\\TestClass::TEST_CLASS_CONST'), 'TestNamespace\\TestClass'), new SymbolInformation('TEST_CLASS_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TestNamespace\\TestClass::TEST_CLASS_CONST'), 'TestNamespace\\TestClass'),
new SymbolInformation('staticTestProperty', SymbolKind::FIELD, $this->getDefinitionLocation('TestNamespace\\TestClass::staticTestProperty'), 'TestNamespace\\TestClass'), new SymbolInformation('staticTestProperty', SymbolKind::PROPERTY, $this->getDefinitionLocation('TestNamespace\\TestClass::staticTestProperty'), 'TestNamespace\\TestClass'),
new SymbolInformation('testProperty', SymbolKind::FIELD, $this->getDefinitionLocation('TestNamespace\\TestClass::testProperty'), 'TestNamespace\\TestClass'), new SymbolInformation('testProperty', SymbolKind::PROPERTY, $this->getDefinitionLocation('TestNamespace\\TestClass::testProperty'), 'TestNamespace\\TestClass'),
new SymbolInformation('staticTestMethod', SymbolKind::METHOD, $this->getDefinitionLocation('TestNamespace\\TestClass::staticTestMethod()'), 'TestNamespace\\TestClass'), new SymbolInformation('staticTestMethod', SymbolKind::METHOD, $this->getDefinitionLocation('TestNamespace\\TestClass::staticTestMethod()'), 'TestNamespace\\TestClass'),
new SymbolInformation('testMethod', SymbolKind::METHOD, $this->getDefinitionLocation('TestNamespace\\TestClass::testMethod()'), 'TestNamespace\\TestClass'), new SymbolInformation('testMethod', SymbolKind::METHOD, $this->getDefinitionLocation('TestNamespace\\TestClass::testMethod()'), 'TestNamespace\\TestClass'),
new SymbolInformation('TestTrait', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\TestTrait'), 'TestNamespace'), new SymbolInformation('TestTrait', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\TestTrait'), 'TestNamespace'),
@ -33,8 +33,8 @@ class SymbolTest extends ServerTestCase
new SymbolInformation('TEST_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TEST_CONST'), ''), new SymbolInformation('TEST_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TEST_CONST'), ''),
new SymbolInformation('TestClass', SymbolKind::CLASS_, $this->getDefinitionLocation('TestClass'), ''), new SymbolInformation('TestClass', SymbolKind::CLASS_, $this->getDefinitionLocation('TestClass'), ''),
new SymbolInformation('TEST_CLASS_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TestClass::TEST_CLASS_CONST'), 'TestClass'), new SymbolInformation('TEST_CLASS_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TestClass::TEST_CLASS_CONST'), 'TestClass'),
new SymbolInformation('staticTestProperty', SymbolKind::FIELD, $this->getDefinitionLocation('TestClass::staticTestProperty'), 'TestClass'), new SymbolInformation('staticTestProperty', SymbolKind::PROPERTY, $this->getDefinitionLocation('TestClass::staticTestProperty'), 'TestClass'),
new SymbolInformation('testProperty', SymbolKind::FIELD, $this->getDefinitionLocation('TestClass::testProperty'), 'TestClass'), new SymbolInformation('testProperty', SymbolKind::PROPERTY, $this->getDefinitionLocation('TestClass::testProperty'), 'TestClass'),
new SymbolInformation('staticTestMethod', SymbolKind::METHOD, $this->getDefinitionLocation('TestClass::staticTestMethod()'), 'TestClass'), new SymbolInformation('staticTestMethod', SymbolKind::METHOD, $this->getDefinitionLocation('TestClass::staticTestMethod()'), 'TestClass'),
new SymbolInformation('testMethod', SymbolKind::METHOD, $this->getDefinitionLocation('TestClass::testMethod()'), 'TestClass'), new SymbolInformation('testMethod', SymbolKind::METHOD, $this->getDefinitionLocation('TestClass::testMethod()'), 'TestClass'),
new SymbolInformation('TestTrait', SymbolKind::CLASS_, $this->getDefinitionLocation('TestTrait'), ''), new SymbolInformation('TestTrait', SymbolKind::CLASS_, $this->getDefinitionLocation('TestTrait'), ''),