commit
1fed89e0e3
|
@ -2,4 +2,5 @@
|
||||||
.vscode
|
.vscode
|
||||||
.idea
|
.idea
|
||||||
vendor/
|
vendor/
|
||||||
|
.phpls/
|
||||||
composer.lock
|
composer.lock
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
$this->definitions[$fqn] = $node;
|
return;
|
||||||
}
|
}
|
||||||
|
$this->definitions[$fqn] = $node;
|
||||||
|
$symbol = SymbolInformation::fromNode($node, $fqn);
|
||||||
|
$this->symbols[$fqn] = $symbol;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)))
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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'),
|
||||||
|
|
|
@ -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'), ''),
|
||||||
|
|
Loading…
Reference in New Issue