Resolve expressions recursively (#155)
* Add Definition class * Add recursive DefinitionResolver * Cache hoverpull/166/head
parent
c19aedcef2
commit
33211c68ca
|
@ -41,8 +41,7 @@
|
||||||
"LanguageServer\\": "src/"
|
"LanguageServer\\": "src/"
|
||||||
},
|
},
|
||||||
"files" : [
|
"files" : [
|
||||||
"src/utils.php",
|
"src/utils.php"
|
||||||
"src/Fqn.php"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"autoload-dev": {
|
"autoload-dev": {
|
||||||
|
|
|
@ -34,3 +34,7 @@ use function test_function;
|
||||||
if ($abc instanceof TestInterface) {
|
if ($abc instanceof TestInterface) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Nested expression
|
||||||
|
$obj->testProperty->testMethod();
|
||||||
|
TestClass::$staticTestProperty[123]->testProperty;
|
||||||
|
|
|
@ -30,7 +30,7 @@ class TestClass implements TestInterface
|
||||||
/**
|
/**
|
||||||
* Lorem excepteur officia sit anim velit veniam enim.
|
* Lorem excepteur officia sit anim velit veniam enim.
|
||||||
*
|
*
|
||||||
* @var TestClass
|
* @var TestClass[]
|
||||||
*/
|
*/
|
||||||
public static $staticTestProperty;
|
public static $staticTestProperty;
|
||||||
|
|
||||||
|
|
|
@ -34,3 +34,7 @@ use function TestNamespace\test_function;
|
||||||
if ($abc instanceof TestInterface) {
|
if ($abc instanceof TestInterface) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Nested expressions
|
||||||
|
$obj->testProperty->testMethod();
|
||||||
|
TestClass::$staticTestProperty[123]->testProperty;
|
||||||
|
|
|
@ -30,7 +30,7 @@ class TestClass implements TestInterface
|
||||||
/**
|
/**
|
||||||
* Lorem excepteur officia sit anim velit veniam enim.
|
* Lorem excepteur officia sit anim velit veniam enim.
|
||||||
*
|
*
|
||||||
* @var TestClass
|
* @var TestClass[]
|
||||||
*/
|
*/
|
||||||
public static $staticTestProperty;
|
public static $staticTestProperty;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace LanguageServer;
|
||||||
|
|
||||||
|
use PhpParser\Node;
|
||||||
|
use phpDocumentor\Reflection\{Types, Type, Fqsen, TypeResolver};
|
||||||
|
use LanguageServer\Protocol\SymbolInformation;
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class used to represent symbols
|
||||||
|
*/
|
||||||
|
class Definition
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The fully qualified name of the symbol, if it has one
|
||||||
|
*
|
||||||
|
* Examples of FQNs:
|
||||||
|
* - testFunction()
|
||||||
|
* - TestNamespace\TestClass
|
||||||
|
* - TestNamespace\TestClass::TEST_CONSTANT
|
||||||
|
* - TestNamespace\TestClass::staticTestProperty
|
||||||
|
* - TestNamespace\TestClass::testProperty
|
||||||
|
* - TestNamespace\TestClass::staticTestMethod()
|
||||||
|
* - TestNamespace\TestClass::testMethod()
|
||||||
|
*
|
||||||
|
* @var string|null
|
||||||
|
*/
|
||||||
|
public $fqn;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Protocol\SymbolInformation
|
||||||
|
*/
|
||||||
|
public $symbolInformation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type a reference to this symbol will resolve to.
|
||||||
|
* For properties and constants, this is the type of the property/constant.
|
||||||
|
* For functions and methods, this is the return type.
|
||||||
|
* For any other declaration it will be null.
|
||||||
|
* Can also be a compound type.
|
||||||
|
* If it is unknown, will be Types\Mixed.
|
||||||
|
*
|
||||||
|
* @var \phpDocumentor\Type|null
|
||||||
|
*/
|
||||||
|
public $type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The first line of the declaration, for use in textDocument/hover
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $declarationLine;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A documentation string, for use in textDocument/hover
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $documentation;
|
||||||
|
}
|
|
@ -0,0 +1,688 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace LanguageServer;
|
||||||
|
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\PrettyPrinter\Standard as PrettyPrinter;
|
||||||
|
use phpDocumentor\Reflection\{Types, Type, Fqsen, TypeResolver};
|
||||||
|
use LanguageServer\Protocol\SymbolInformation;
|
||||||
|
use Sabre\Event\Promise;
|
||||||
|
use function Sabre\Event\coroutine;
|
||||||
|
|
||||||
|
class DefinitionResolver
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var \LanguageServer\Project
|
||||||
|
*/
|
||||||
|
private $project;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \phpDocumentor\Reflection\TypeResolver
|
||||||
|
*/
|
||||||
|
private $typeResolver;
|
||||||
|
|
||||||
|
public function __construct(Project $project)
|
||||||
|
{
|
||||||
|
$this->project = $project;
|
||||||
|
$this->typeResolver = new TypeResolver;
|
||||||
|
$this->prettyPrinter = new PrettyPrinter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the declaration line for a given node
|
||||||
|
*
|
||||||
|
* @param Node $node
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getDeclarationLineFromNode(Node $node): string
|
||||||
|
{
|
||||||
|
if ($node instanceof Node\Stmt\PropertyProperty || $node instanceof Node\Const_) {
|
||||||
|
// Properties and constants can have multiple declarations
|
||||||
|
// Use the parent node (that includes the modifiers), but only render the requested declaration
|
||||||
|
$child = $node;
|
||||||
|
$node = $node->getAttribute('parentNode');
|
||||||
|
$defLine = clone $node;
|
||||||
|
$defLine->props = [$child];
|
||||||
|
} else {
|
||||||
|
$defLine = clone $node;
|
||||||
|
}
|
||||||
|
// Don't include the docblock in the declaration string
|
||||||
|
$defLine->setAttribute('comments', []);
|
||||||
|
if (isset($defLine->stmts)) {
|
||||||
|
$defLine->stmts = [];
|
||||||
|
}
|
||||||
|
$defText = $this->prettyPrinter->prettyPrint([$defLine]);
|
||||||
|
return strstr($defText, "\n", true) ?: $defText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the documentation string for a node, if it has one
|
||||||
|
*
|
||||||
|
* @param Node $node
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function getDocumentationFromNode(Node $node)
|
||||||
|
{
|
||||||
|
if ($node instanceof Node\Stmt\PropertyProperty || $node instanceof Node\Const_) {
|
||||||
|
$node = $node->getAttribute('parentNode');
|
||||||
|
}
|
||||||
|
if ($node instanceof Node\Param) {
|
||||||
|
$fn = $node->getAttribute('parentNode');
|
||||||
|
$docBlock = $fn->getAttribute('docBlock');
|
||||||
|
if ($docBlock !== null) {
|
||||||
|
$tags = $docBlock->getTagsByName('param');
|
||||||
|
foreach ($tags as $tag) {
|
||||||
|
if ($tag->getVariableName() === $node->name) {
|
||||||
|
return $tag->getDescription()->render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$docBlock = $node->getAttribute('docBlock');
|
||||||
|
if ($docBlock !== null) {
|
||||||
|
return $docBlock->getSummary();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given any node, returns the Definition object of the symbol that is referenced
|
||||||
|
*
|
||||||
|
* @param Node $node Any reference node
|
||||||
|
* @return Definition|null
|
||||||
|
*/
|
||||||
|
public function resolveReferenceNodeToDefinition(Node $node)
|
||||||
|
{
|
||||||
|
// Variables are not indexed globally, as they stay in the file scope anyway
|
||||||
|
if ($node instanceof Node\Expr\Variable) {
|
||||||
|
// Resolve the variable to a definition node (assignment, param or closure use)
|
||||||
|
$defNode = self::resolveVariableToNode($node);
|
||||||
|
if ($defNode === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$def = new Definition;
|
||||||
|
// Get symbol information from node (range, symbol kind)
|
||||||
|
$def->symbolInformation = SymbolInformation::fromNode($defNode);
|
||||||
|
// Declaration line
|
||||||
|
$def->declarationLine = $this->getDeclarationLineFromNode($defNode);
|
||||||
|
// Documentation
|
||||||
|
$def->documentation = $this->getDocumentationFromNode($defNode);
|
||||||
|
if ($defNode instanceof Node\Param) {
|
||||||
|
// Get parameter type
|
||||||
|
$def->type = $this->getTypeFromNode($defNode);
|
||||||
|
} else {
|
||||||
|
// Resolve the type of the assignment/closure use node
|
||||||
|
$def->type = $this->resolveExpressionNodeToType($defNode);
|
||||||
|
}
|
||||||
|
return $def;
|
||||||
|
}
|
||||||
|
// Other references are references to a global symbol that have an FQN
|
||||||
|
// Find out the FQN
|
||||||
|
$fqn = $this->resolveReferenceNodeToFqn($node);
|
||||||
|
if ($fqn === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// If the node is a function or constant, it could be namespaced, but PHP falls back to global
|
||||||
|
// http://php.net/manual/en/language.namespaces.fallback.php
|
||||||
|
$parent = $node->getAttribute('parentNode');
|
||||||
|
$globalFallback = $parent instanceof Node\Expr\ConstFetch || $parent instanceof Node\Expr\FuncCall;
|
||||||
|
// Return the Definition object from the project index
|
||||||
|
return $this->project->getDefinition($fqn, $globalFallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given any node, returns the FQN of the symbol that is referenced
|
||||||
|
* Returns null if the FQN could not be resolved or the reference node references a variable
|
||||||
|
*
|
||||||
|
* @param Node $node
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function resolveReferenceNodeToFqn(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
|
||||||
|
|| $parent instanceof Node\Expr\Instanceof_
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// 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;
|
||||||
|
} else if ($grandParent instanceof Node\Stmt\Use_ && $grandParent->type === Node\Stmt\Use_::TYPE_FUNCTION) {
|
||||||
|
$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) {
|
||||||
|
// Cannot get definition if right-hand side is expression
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Get the type of the left-hand expression
|
||||||
|
$varType = $this->resolveExpressionNodeToType($node->var);
|
||||||
|
if ($varType instanceof Types\This) {
|
||||||
|
// $this is resolved to the containing class
|
||||||
|
$classFqn = self::getContainingClassFqn($node);
|
||||||
|
} else if (!($varType instanceof Types\Object_) || $varType->getFqsen() === null) {
|
||||||
|
// Left-hand expression could not be resolved to a class
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
$classFqn = substr((string)$varType->getFqsen(), 1);
|
||||||
|
}
|
||||||
|
$name = $classFqn . '::' . (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
|
||||||
|
$classNode = getClosestNode($node, Node\Stmt\Class_::class);
|
||||||
|
if ($className === 'parent') {
|
||||||
|
// parent is resolved to the parent class
|
||||||
|
if (!isset($n->extends)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$className = (string)$classNode->extends;
|
||||||
|
} else {
|
||||||
|
$className = (string)$classNode->namespacedName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$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 FQN of the class a node is contained in
|
||||||
|
* Returns null if the class is anonymous or the node is not contained in a class
|
||||||
|
*
|
||||||
|
* @param Node $node
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
private static function getContainingClassFqn(Node $node)
|
||||||
|
{
|
||||||
|
$classNode = getClosestNode($node, Node\Stmt\Class_::class);
|
||||||
|
if ($classNode === null || $classNode->isAnonymous()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (string)$classNode->namespacedName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 static function resolveVariableToNode(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 instanceof Node\Expr\AssignOp)
|
||||||
|
&& $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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an expression node, resolves that expression recursively to a type.
|
||||||
|
* If the type could not be resolved, returns Types\Mixed.
|
||||||
|
*
|
||||||
|
* @param \PhpParser\Node\Expr $expr
|
||||||
|
* @return \phpDocumentor\Type
|
||||||
|
*/
|
||||||
|
private function resolveExpressionNodeToType(Node\Expr $expr): Type
|
||||||
|
{
|
||||||
|
if ($expr instanceof Node\Expr\Variable) {
|
||||||
|
if ($expr->name === 'this') {
|
||||||
|
return new Types\This;
|
||||||
|
}
|
||||||
|
// Find variable definition
|
||||||
|
$defNode = $this->resolveVariableToNode($expr);
|
||||||
|
if ($defNode instanceof Node\Expr) {
|
||||||
|
return $this->resolveExpressionNodeToType($defNode);
|
||||||
|
}
|
||||||
|
if ($defNode instanceof Node\Param) {
|
||||||
|
return $this->getTypeFromNode($defNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($expr instanceof Node\Expr\FuncCall) {
|
||||||
|
// Find the function definition
|
||||||
|
if ($expr->name instanceof Node\Expr) {
|
||||||
|
// Cannot get type for dynamic function call
|
||||||
|
return new Types\Mixed;
|
||||||
|
}
|
||||||
|
$fqn = (string)($expr->getAttribute('namespacedName') ?? $expr->name);
|
||||||
|
$def = $this->project->getDefinition($fqn, true);
|
||||||
|
if ($def !== null) {
|
||||||
|
return $def->type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($expr instanceof Node\Expr\ConstFetch) {
|
||||||
|
if (strtolower((string)$expr->name) === 'true' || strtolower((string)$expr->name) === 'false') {
|
||||||
|
return new Types\Boolean;
|
||||||
|
}
|
||||||
|
// Resolve constant
|
||||||
|
$fqn = (string)($expr->getAttribute('namespacedName') ?? $expr->name);
|
||||||
|
$def = $this->project->getDefinition($fqn, true);
|
||||||
|
if ($def !== null) {
|
||||||
|
return $def->type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($expr instanceof Node\Expr\MethodCall || $expr instanceof Node\Expr\PropertyFetch) {
|
||||||
|
if ($expr->name instanceof Node\Expr) {
|
||||||
|
return new Types\Mixed;
|
||||||
|
}
|
||||||
|
// Resolve object
|
||||||
|
$objType = $this->resolveExpressionNodeToType($expr->var);
|
||||||
|
if (!($objType instanceof Types\Compound)) {
|
||||||
|
$objType = new Types\Compound([$objType]);
|
||||||
|
}
|
||||||
|
for ($i = 0; $t = $objType->get($i); $i++) {
|
||||||
|
if ($t instanceof Types\This) {
|
||||||
|
$classFqn = self::getContainingClassFqn($expr);
|
||||||
|
if ($classFqn === null) {
|
||||||
|
return new Types\Mixed;
|
||||||
|
}
|
||||||
|
} else if (!($t instanceof Types\Object_) || $t->getFqsen() === null) {
|
||||||
|
return new Types\Mixed;
|
||||||
|
} else {
|
||||||
|
$classFqn = substr((string)$t->getFqsen(), 1);
|
||||||
|
}
|
||||||
|
$fqn = $classFqn . '::' . $expr->name;
|
||||||
|
if ($expr instanceof Node\Expr\MethodCall) {
|
||||||
|
$fqn .= '()';
|
||||||
|
}
|
||||||
|
$def = $this->project->getDefinition($fqn);
|
||||||
|
if ($def !== null) {
|
||||||
|
return $def->type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
$expr instanceof Node\Expr\StaticCall
|
||||||
|
|| $expr instanceof Node\Expr\StaticPropertyFetch
|
||||||
|
|| $expr instanceof Node\Expr\ClassConstFetch
|
||||||
|
) {
|
||||||
|
$classType = self::resolveClassNameToType($expr->class);
|
||||||
|
if (!($classType instanceof Types\Object_) || $classType->getFqsen() === null || $expr->name instanceof Node\Expr) {
|
||||||
|
return new Types\Mixed;
|
||||||
|
}
|
||||||
|
$fqn = substr((string)$classType->getFqsen(), 1) . '::' . $expr->name;
|
||||||
|
if ($expr instanceof Node\Expr\StaticCall) {
|
||||||
|
$fqn .= '()';
|
||||||
|
}
|
||||||
|
$def = $this->project->getDefinition($fqn);
|
||||||
|
if ($def === null) {
|
||||||
|
return new Types\Mixed;
|
||||||
|
}
|
||||||
|
return $def->type;
|
||||||
|
}
|
||||||
|
if ($expr instanceof Node\Expr\New_) {
|
||||||
|
return self::resolveClassNameToType($expr->class);
|
||||||
|
}
|
||||||
|
if ($expr instanceof Node\Expr\Clone_ || $expr instanceof Node\Expr\Assign) {
|
||||||
|
return $this->resolveExpressionNodeToType($expr->expr);
|
||||||
|
}
|
||||||
|
if ($expr instanceof Node\Expr\Ternary) {
|
||||||
|
// ?:
|
||||||
|
if ($expr->if === null) {
|
||||||
|
return new Types\Compound([
|
||||||
|
$this->resolveExpressionNodeToType($expr->cond),
|
||||||
|
$this->resolveExpressionNodeToType($expr->else)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
// Ternary is a compound of the two possible values
|
||||||
|
return new Types\Compound([
|
||||||
|
$this->resolveExpressionNodeToType($expr->if),
|
||||||
|
$this->resolveExpressionNodeToType($expr->else)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if ($expr instanceof Node\Expr\BinaryOp\Coalesce) {
|
||||||
|
// ?? operator
|
||||||
|
return new Types\Compound([
|
||||||
|
$this->resolveExpressionNodeToType($expr->left),
|
||||||
|
$this->resolveExpressionNodeToType($expr->right)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
$expr instanceof Node\Expr\InstanceOf_
|
||||||
|
|| $expr instanceof Node\Expr\Cast\Bool_
|
||||||
|
|| $expr instanceof Node\Expr\BooleanNot
|
||||||
|
|| $expr instanceof Node\Expr\Empty_
|
||||||
|
|| $expr instanceof Node\Expr\Isset_
|
||||||
|
|| $expr instanceof Node\Expr\BinaryOp\Greater
|
||||||
|
|| $expr instanceof Node\Expr\BinaryOp\GreaterOrEqual
|
||||||
|
|| $expr instanceof Node\Expr\BinaryOp\Smaller
|
||||||
|
|| $expr instanceof Node\Expr\BinaryOp\SmallerOrEqual
|
||||||
|
|| $expr instanceof Node\Expr\BinaryOp\BooleanAnd
|
||||||
|
|| $expr instanceof Node\Expr\BinaryOp\BooleanOr
|
||||||
|
|| $expr instanceof Node\Expr\BinaryOp\LogicalAnd
|
||||||
|
|| $expr instanceof Node\Expr\BinaryOp\LogicalOr
|
||||||
|
|| $expr instanceof Node\Expr\BinaryOp\LogicalXor
|
||||||
|
|| $expr instanceof Node\Expr\BinaryOp\NotEqual
|
||||||
|
|| $expr instanceof Node\Expr\BinaryOp\NotIdentical
|
||||||
|
) {
|
||||||
|
return new Types\Boolean;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
$expr instanceof Node\Expr\Concat
|
||||||
|
|| $expr instanceof Node\Expr\Cast\String_
|
||||||
|
|| $expr instanceof Node\Expr\BinaryOp\Concat
|
||||||
|
|| $expr instanceof Node\Expr\AssignOp\Concat
|
||||||
|
|| $expr instanceof Node\Expr\Scalar\String_
|
||||||
|
|| $expr instanceof Node\Expr\Scalar\Encapsed
|
||||||
|
|| $expr instanceof Node\Expr\Scalar\EncapsedStringPart
|
||||||
|
|| $expr instanceof Node\Expr\Scalar\MagicConst\Class_
|
||||||
|
|| $expr instanceof Node\Expr\Scalar\MagicConst\Dir
|
||||||
|
|| $expr instanceof Node\Expr\Scalar\MagicConst\Function_
|
||||||
|
|| $expr instanceof Node\Expr\Scalar\MagicConst\Method
|
||||||
|
|| $expr instanceof Node\Expr\Scalar\MagicConst\Namespace_
|
||||||
|
|| $expr instanceof Node\Expr\Scalar\MagicConst\Trait_
|
||||||
|
) {
|
||||||
|
return new Types\String_;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
$expr instanceof Node\Expr\BinaryOp\Minus
|
||||||
|
|| $expr instanceof Node\Expr\BinaryOp\Plus
|
||||||
|
|| $expr instanceof Node\Expr\BinaryOp\Pow
|
||||||
|
|| $expr instanceof Node\Expr\BinaryOp\Mul
|
||||||
|
|| $expr instanceof Node\Expr\AssignOp\Minus
|
||||||
|
|| $expr instanceof Node\Expr\AssignOp\Plus
|
||||||
|
|| $expr instanceof Node\Expr\AssignOp\Pow
|
||||||
|
|| $expr instanceof Node\Expr\AssignOp\Mul
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
resolveType($expr->left) instanceof Types\Integer_
|
||||||
|
&& resolveType($expr->right) instanceof Types\Integer_
|
||||||
|
) {
|
||||||
|
return new Types\Integer;
|
||||||
|
}
|
||||||
|
return new Types\Float_;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
$expr instanceof Node\Scalar\LNumber
|
||||||
|
|| $expr instanceof Node\Expr\Cast\Int_
|
||||||
|
|| $expr instanceof Node\Expr\Scalar\MagicConst\Line
|
||||||
|
|| $expr instanceof Node\Expr\BinaryOp\Spaceship
|
||||||
|
|| $expr instanceof Node\Expr\BinaryOp\BitwiseAnd
|
||||||
|
|| $expr instanceof Node\Expr\BinaryOp\BitwiseOr
|
||||||
|
|| $expr instanceof Node\Expr\BinaryOp\BitwiseXor
|
||||||
|
) {
|
||||||
|
return new Types\Integer;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
$expr instanceof Node\Expr\BinaryOp\Div
|
||||||
|
|| $expr instanceof Node\Expr\DNumber
|
||||||
|
|| $expr instanceof Node\Expr\Cast\Double
|
||||||
|
) {
|
||||||
|
return new Types\Float_;
|
||||||
|
}
|
||||||
|
if ($expr instanceof Node\Expr\Array_) {
|
||||||
|
$valueTypes = [];
|
||||||
|
$keyTypes = [];
|
||||||
|
foreach ($expr->items as $item) {
|
||||||
|
$valueTypes[] = $this->resolveExpressionNodeToType($item->value);
|
||||||
|
$keyTypes[] = $item->key ? $this->resolveExpressionNodeToType($item->key) : new Types\Integer;
|
||||||
|
}
|
||||||
|
$valueTypes = array_unique($keyTypes);
|
||||||
|
$keyTypes = array_unique($keyTypes);
|
||||||
|
if (empty($valueTypes)) {
|
||||||
|
$valueType = null;
|
||||||
|
} else if (count($valueTypes) === 1) {
|
||||||
|
$valueType = $valueTypes[0];
|
||||||
|
} else {
|
||||||
|
$valueType = new Types\Compound($valueTypes);
|
||||||
|
}
|
||||||
|
if (empty($keyTypes)) {
|
||||||
|
$keyType = null;
|
||||||
|
} else if (count($keyTypes) === 1) {
|
||||||
|
$keyType = $keyTypes[0];
|
||||||
|
} else {
|
||||||
|
$keyType = new Types\Compound($keyTypes);
|
||||||
|
}
|
||||||
|
return new Types\Array_($valueType, $keyType);
|
||||||
|
}
|
||||||
|
if ($expr instanceof Node\Expr\ArrayDimFetch) {
|
||||||
|
$varType = $this->resolveExpressionNodeToType($expr->var);
|
||||||
|
if (!($varType instanceof Types\Array_)) {
|
||||||
|
return new Types\Mixed;
|
||||||
|
}
|
||||||
|
return $varType->getValueType();
|
||||||
|
}
|
||||||
|
if ($expr instanceof Node\Expr\Include_) {
|
||||||
|
// TODO: resolve path to PhpDocument and find return statement
|
||||||
|
return new Types\Mixed;
|
||||||
|
}
|
||||||
|
return new Types\Mixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes any class name node (from a static method call, or new node) and returns a Type object
|
||||||
|
* Resolves keywords like self, static and parent
|
||||||
|
*
|
||||||
|
* @param Node $class
|
||||||
|
* @return Type
|
||||||
|
*/
|
||||||
|
private static function resolveClassNameToType(Node $class): Type
|
||||||
|
{
|
||||||
|
if ($class instanceof Node\Expr) {
|
||||||
|
return new Types\Mixed;
|
||||||
|
}
|
||||||
|
if ($class instanceof Node\Stmt\Class_) {
|
||||||
|
// Anonymous class
|
||||||
|
return new Types\Object_;
|
||||||
|
}
|
||||||
|
$className = (string)$class;
|
||||||
|
if ($className === 'static') {
|
||||||
|
return new Types\Static_;
|
||||||
|
}
|
||||||
|
if ($className === 'self' || $className === 'parent') {
|
||||||
|
$classNode = getClosestNode($class, Node\Stmt\Class_::class);
|
||||||
|
if ($className === 'parent') {
|
||||||
|
if ($classNode === null || $classNode->extends === null) {
|
||||||
|
return new Types\Object_;
|
||||||
|
}
|
||||||
|
// parent is resolved to the parent class
|
||||||
|
$classFqn = (string)$classNode->extends;
|
||||||
|
} else {
|
||||||
|
if ($classNode === null) {
|
||||||
|
return new Types\Self_;
|
||||||
|
}
|
||||||
|
// self is resolved to the containing class
|
||||||
|
$classFqn = (string)$classNode->namespacedName;
|
||||||
|
}
|
||||||
|
return new Types\Object_(new Fqsen('\\' . $classFqn));
|
||||||
|
}
|
||||||
|
return new Types\Object_(new Fqsen('\\' . $className));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the type a reference to this symbol will resolve to.
|
||||||
|
* For properties and constants, this is the type of the property/constant.
|
||||||
|
* For functions and methods, this is the return type.
|
||||||
|
* For parameters, this is the type of the parameter.
|
||||||
|
* For classes and interfaces, this is the class type (object).
|
||||||
|
* Variables are not indexed for performance reasons.
|
||||||
|
* Can also be a compound type.
|
||||||
|
* If it is unknown, will be Types\Mixed.
|
||||||
|
* Returns null if the node does not have a type.
|
||||||
|
*
|
||||||
|
* @param Node $node
|
||||||
|
* @return \phpDocumentor\Type|null
|
||||||
|
*/
|
||||||
|
public function getTypeFromNode(Node $node)
|
||||||
|
{
|
||||||
|
if ($node instanceof Node\Param) {
|
||||||
|
// Parameters
|
||||||
|
$docBlock = $node->getAttribute('parentNode')->getAttribute('docBlock');
|
||||||
|
if ($docBlock !== null && count($paramTags = $docBlock->getTagsByName('param')) > 0) {
|
||||||
|
// Use @param tag
|
||||||
|
return $paramTags[0]->getType();
|
||||||
|
}
|
||||||
|
if ($node->type !== null) {
|
||||||
|
// Use PHP7 return type hint
|
||||||
|
if (is_string($node->type)) {
|
||||||
|
// Resolve a string like "bool" to a type object
|
||||||
|
$type = $this->typeResolver->resolve($node->type);
|
||||||
|
}
|
||||||
|
$type = new Types\Object_(new Fqsen('\\' . (string)$node->type));
|
||||||
|
if ($node->default !== null) {
|
||||||
|
$defaultType = $this->resolveExpressionNodeToType($node->default);
|
||||||
|
$type = new Types\Compound([$type, $defaultType]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Unknown parameter type
|
||||||
|
return new Types\Mixed;
|
||||||
|
}
|
||||||
|
if ($node instanceof Node\FunctionLike) {
|
||||||
|
// Functions/methods
|
||||||
|
$docBlock = $node->getAttribute('docBlock');
|
||||||
|
if ($docBlock !== null && count($returnTags = $docBlock->getTagsByName('return')) > 0) {
|
||||||
|
// Use @return tag
|
||||||
|
return $returnTags[0]->getType();
|
||||||
|
}
|
||||||
|
if ($node->returnType !== null) {
|
||||||
|
// Use PHP7 return type hint
|
||||||
|
if (is_string($node->returnType)) {
|
||||||
|
// Resolve a string like "bool" to a type object
|
||||||
|
return $this->typeResolver->resolve($node->returnType);
|
||||||
|
}
|
||||||
|
return new Types\Object_(new Fqsen('\\' . (string)$node->returnType));
|
||||||
|
}
|
||||||
|
// Unknown return type
|
||||||
|
return new Types\Mixed;
|
||||||
|
}
|
||||||
|
if ($node instanceof Node\Stmt\PropertyProperty || $node instanceof Node\Const_) {
|
||||||
|
// Property or constant
|
||||||
|
$docBlock = $node->getAttribute('parentNode')->getAttribute('docBlock');
|
||||||
|
if ($docBlock !== null && count($varTags = $docBlock->getTagsByName('var')) > 0) {
|
||||||
|
// Use @var tag
|
||||||
|
return $varTags[0]->getType();
|
||||||
|
}
|
||||||
|
// TODO: read @property tags of class
|
||||||
|
// TODO: Try to infer the type from default value / constant value
|
||||||
|
// Unknown
|
||||||
|
return new Types\Mixed;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the fully qualified name (FQN) that is defined by a node
|
||||||
|
* Returns null if the node does not declare any symbol that can be referenced by an FQN
|
||||||
|
*
|
||||||
|
* @param Node $node
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public static 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
248
src/Fqn.php
248
src/Fqn.php
|
@ -1,248 +0,0 @@
|
||||||
<?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
|
|
||||||
|| $parent instanceof Node\Expr\Instanceof_
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
// 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;
|
|
||||||
} else if ($grandParent instanceof Node\Stmt\Use_ && $grandParent->type === Node\Stmt\Use_::TYPE_FUNCTION) {
|
|
||||||
$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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,8 +4,8 @@ declare(strict_types = 1);
|
||||||
namespace LanguageServer\NodeVisitor;
|
namespace LanguageServer\NodeVisitor;
|
||||||
|
|
||||||
use PhpParser\{NodeVisitorAbstract, Node};
|
use PhpParser\{NodeVisitorAbstract, Node};
|
||||||
|
use LanguageServer\{Definition, DefinitionResolver};
|
||||||
use LanguageServer\Protocol\SymbolInformation;
|
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
|
||||||
|
@ -14,27 +14,41 @@ use function LanguageServer\Fqn\getDefinedFqn;
|
||||||
class DefinitionCollector extends NodeVisitorAbstract
|
class DefinitionCollector extends NodeVisitorAbstract
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Map from fully qualified name (FQN) to Node
|
* Map from fully qualified name (FQN) to Definition
|
||||||
*
|
*
|
||||||
* @var Node[]
|
* @var Definition[]
|
||||||
*/
|
*/
|
||||||
public $definitions = [];
|
public $definitions = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map from FQN to SymbolInformation
|
* Map from fully qualified name (FQN) to Node
|
||||||
*
|
*
|
||||||
* @var SymbolInformation
|
* @var Node[]
|
||||||
*/
|
*/
|
||||||
public $symbols = [];
|
public $nodes = [];
|
||||||
|
|
||||||
|
private $definitionResolver;
|
||||||
|
|
||||||
|
public function __construct(DefinitionResolver $definitionResolver)
|
||||||
|
{
|
||||||
|
$this->definitionResolver = $definitionResolver;
|
||||||
|
}
|
||||||
|
|
||||||
public function enterNode(Node $node)
|
public function enterNode(Node $node)
|
||||||
{
|
{
|
||||||
$fqn = getDefinedFqn($node);
|
$fqn = DefinitionResolver::getDefinedFqn($node);
|
||||||
|
// Only index definitions with an FQN (no variables)
|
||||||
if ($fqn === null) {
|
if ($fqn === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$this->definitions[$fqn] = $node;
|
$this->nodes[$fqn] = $node;
|
||||||
$symbol = SymbolInformation::fromNode($node, $fqn);
|
$def = new Definition;
|
||||||
$this->symbols[$fqn] = $symbol;
|
$def->fqn = $fqn;
|
||||||
|
$def->symbolInformation = SymbolInformation::fromNode($node, $fqn);
|
||||||
|
$def->type = $this->definitionResolver->getTypeFromNode($node);
|
||||||
|
$def->declarationLine = $this->definitionResolver->getDeclarationLineFromNode($node);
|
||||||
|
$def->documentation = $this->definitionResolver->getDocumentationFromNode($node);
|
||||||
|
|
||||||
|
$this->definitions[$fqn] = $def;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,8 @@ declare(strict_types = 1);
|
||||||
|
|
||||||
namespace LanguageServer\NodeVisitor;
|
namespace LanguageServer\NodeVisitor;
|
||||||
|
|
||||||
use function LanguageServer\Fqn\getReferencedFqn;
|
|
||||||
use PhpParser\{NodeVisitorAbstract, Node};
|
use PhpParser\{NodeVisitorAbstract, Node};
|
||||||
|
use LanguageServer\DefinitionResolver;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collects references to classes, interfaces, traits, methods, properties and constants
|
* Collects references to classes, interfaces, traits, methods, properties and constants
|
||||||
|
@ -17,12 +17,20 @@ class ReferencesCollector extends NodeVisitorAbstract
|
||||||
*
|
*
|
||||||
* @var Node[][]
|
* @var Node[][]
|
||||||
*/
|
*/
|
||||||
public $references = [];
|
public $nodes = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param DefinitionResolver $definitionResolver The DefinitionResolver to resolve reference nodes to definitions
|
||||||
|
*/
|
||||||
|
public function __construct(DefinitionResolver $definitionResolver)
|
||||||
|
{
|
||||||
|
$this->definitionResolver = $definitionResolver;
|
||||||
|
}
|
||||||
|
|
||||||
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 = getReferencedFqn($node);
|
$fqn = $this->definitionResolver->resolveReferenceNodeToFqn($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
|
||||||
|
@ -41,9 +49,9 @@ class ReferencesCollector extends NodeVisitorAbstract
|
||||||
|
|
||||||
private function addReference(string $fqn, Node $node)
|
private function addReference(string $fqn, Node $node)
|
||||||
{
|
{
|
||||||
if (!isset($this->references[$fqn])) {
|
if (!isset($this->nodes[$fqn])) {
|
||||||
$this->references[$fqn] = [];
|
$this->nodes[$fqn] = [];
|
||||||
}
|
}
|
||||||
$this->references[$fqn][] = $node;
|
$this->nodes[$fqn][] = $node;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ class VariableReferencesCollector extends NodeVisitorAbstract
|
||||||
*
|
*
|
||||||
* @var Node\Expr\Variable[]
|
* @var Node\Expr\Variable[]
|
||||||
*/
|
*/
|
||||||
public $references = [];
|
public $nodes = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var string
|
||||||
|
@ -33,7 +33,7 @@ class VariableReferencesCollector extends NodeVisitorAbstract
|
||||||
public function enterNode(Node $node)
|
public function enterNode(Node $node)
|
||||||
{
|
{
|
||||||
if ($node instanceof Node\Expr\Variable && $node->name === $this->name) {
|
if ($node instanceof Node\Expr\Variable && $node->name === $this->name) {
|
||||||
$this->references[] = $node;
|
$this->nodes[] = $node;
|
||||||
} else if ($node instanceof Node\FunctionLike) {
|
} else if ($node instanceof Node\FunctionLike) {
|
||||||
// If we meet a function node, dont traverse its statements, they are in another scope
|
// If we meet a function node, dont traverse its statements, they are in another scope
|
||||||
// except it is a closure that has imported the variable through use
|
// except it is a closure that has imported the variable through use
|
||||||
|
|
|
@ -16,7 +16,6 @@ use LanguageServer\NodeVisitor\{
|
||||||
use PhpParser\{Error, ErrorHandler, Node, NodeTraverser};
|
use PhpParser\{Error, ErrorHandler, Node, NodeTraverser};
|
||||||
use PhpParser\NodeVisitor\NameResolver;
|
use PhpParser\NodeVisitor\NameResolver;
|
||||||
use phpDocumentor\Reflection\DocBlockFactory;
|
use phpDocumentor\Reflection\DocBlockFactory;
|
||||||
use function LanguageServer\Fqn\{getDefinedFqn, getVariableDefinition, getReferencedFqn};
|
|
||||||
use Sabre\Event\Promise;
|
use Sabre\Event\Promise;
|
||||||
use function Sabre\Event\coroutine;
|
use function Sabre\Event\coroutine;
|
||||||
use Sabre\Uri;
|
use Sabre\Uri;
|
||||||
|
@ -53,6 +52,13 @@ class PhpDocument
|
||||||
*/
|
*/
|
||||||
private $docBlockFactory;
|
private $docBlockFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The DefinitionResolver instance to resolve reference nodes to definitions
|
||||||
|
*
|
||||||
|
* @var DefinitionResolver
|
||||||
|
*/
|
||||||
|
private $definitionResolver;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The URI of the document
|
* The URI of the document
|
||||||
*
|
*
|
||||||
|
@ -74,26 +80,26 @@ class PhpDocument
|
||||||
*/
|
*/
|
||||||
private $stmts;
|
private $stmts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map from fully qualified name (FQN) to Definition
|
||||||
|
*
|
||||||
|
* @var Definition[]
|
||||||
|
*/
|
||||||
|
private $definitions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map from fully qualified name (FQN) to Node
|
* Map from fully qualified name (FQN) to Node
|
||||||
*
|
*
|
||||||
* @var Node[]
|
* @var Node[]
|
||||||
*/
|
*/
|
||||||
private $definitions;
|
private $definitionNodes;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map from fully qualified name (FQN) to array of nodes that reference the symbol
|
* Map from fully qualified name (FQN) to array of nodes that reference the symbol
|
||||||
*
|
*
|
||||||
* @var Node[][]
|
* @var Node[][]
|
||||||
*/
|
*/
|
||||||
private $references;
|
private $referenceNodes;
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
||||||
|
@ -103,13 +109,21 @@ class PhpDocument
|
||||||
* @param Parser $parser The PHPParser instance
|
* @param Parser $parser The PHPParser instance
|
||||||
* @param DocBlockFactory $docBlockFactory The DocBlockFactory instance to parse docblocks
|
* @param DocBlockFactory $docBlockFactory The DocBlockFactory instance to parse docblocks
|
||||||
*/
|
*/
|
||||||
public function __construct(string $uri, string $content, Project $project, LanguageClient $client, Parser $parser, DocBlockFactory $docBlockFactory)
|
public function __construct(
|
||||||
{
|
string $uri,
|
||||||
|
string $content,
|
||||||
|
Project $project,
|
||||||
|
LanguageClient $client,
|
||||||
|
Parser $parser,
|
||||||
|
DocBlockFactory $docBlockFactory,
|
||||||
|
DefinitionResolver $definitionResolver
|
||||||
|
) {
|
||||||
$this->uri = $uri;
|
$this->uri = $uri;
|
||||||
$this->project = $project;
|
$this->project = $project;
|
||||||
$this->client = $client;
|
$this->client = $client;
|
||||||
$this->parser = $parser;
|
$this->parser = $parser;
|
||||||
$this->docBlockFactory = $docBlockFactory;
|
$this->docBlockFactory = $docBlockFactory;
|
||||||
|
$this->definitionResolver = $definitionResolver;
|
||||||
$this->updateContent($content);
|
$this->updateContent($content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,9 +133,9 @@ class PhpDocument
|
||||||
* @param string $fqn The fully qualified name of the symbol
|
* @param string $fqn The fully qualified name of the symbol
|
||||||
* @return Node[]
|
* @return Node[]
|
||||||
*/
|
*/
|
||||||
public function getReferencesByFqn(string $fqn)
|
public function getReferenceNodesByFqn(string $fqn)
|
||||||
{
|
{
|
||||||
return isset($this->references) && isset($this->references[$fqn]) ? $this->references[$fqn] : null;
|
return isset($this->referenceNodes) && isset($this->referenceNodes[$fqn]) ? $this->referenceNodes[$fqn] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -172,37 +186,37 @@ class PhpDocument
|
||||||
$traverser = new NodeTraverser;
|
$traverser = new NodeTraverser;
|
||||||
|
|
||||||
// Collect all definitions
|
// Collect all definitions
|
||||||
$definitionCollector = new DefinitionCollector;
|
$definitionCollector = new DefinitionCollector($this->definitionResolver);
|
||||||
$traverser->addVisitor($definitionCollector);
|
$traverser->addVisitor($definitionCollector);
|
||||||
|
|
||||||
// Collect all references
|
// Collect all references
|
||||||
$referencesCollector = new ReferencesCollector;
|
$referencesCollector = new ReferencesCollector($this->definitionResolver);
|
||||||
$traverser->addVisitor($referencesCollector);
|
$traverser->addVisitor($referencesCollector);
|
||||||
|
|
||||||
$traverser->traverse($stmts);
|
$traverser->traverse($stmts);
|
||||||
|
|
||||||
// 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 => $definition) {
|
||||||
$this->project->removeSymbol($fqn);
|
$this->project->removeDefinition($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;
|
||||||
$this->symbols = $definitionCollector->symbols;
|
$this->definitionNodes = $definitionCollector->nodes;
|
||||||
foreach ($definitionCollector->symbols as $fqn => $symbol) {
|
foreach ($definitionCollector->definitions as $fqn => $definition) {
|
||||||
$this->project->setSymbol($fqn, $symbol);
|
$this->project->setDefinition($fqn, $definition);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unregister old references
|
// Unregister old references
|
||||||
if (isset($this->references)) {
|
if (isset($this->referenceNodes)) {
|
||||||
foreach ($this->references as $fqn => $node) {
|
foreach ($this->referenceNodes as $fqn => $node) {
|
||||||
$this->project->removeReferenceUri($fqn, $this->uri);
|
$this->project->removeReferenceUri($fqn, $this->uri);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Register this document on the project for references
|
// Register this document on the project for references
|
||||||
$this->references = $referencesCollector->references;
|
$this->referenceNodes = $referencesCollector->nodes;
|
||||||
foreach ($referencesCollector->references as $fqn => $nodes) {
|
foreach ($referencesCollector->nodes as $fqn => $nodes) {
|
||||||
$this->project->addReferenceUri($fqn, $this->uri);
|
$this->project->addReferenceUri($fqn, $this->uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -289,9 +303,9 @@ class PhpDocument
|
||||||
* @param string $fqn
|
* @param string $fqn
|
||||||
* @return Node|null
|
* @return Node|null
|
||||||
*/
|
*/
|
||||||
public function getDefinitionByFqn(string $fqn)
|
public function getDefinitionNodeByFqn(string $fqn)
|
||||||
{
|
{
|
||||||
return $this->definitions[$fqn] ?? null;
|
return $this->definitionNodes[$fqn] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -299,19 +313,19 @@ class PhpDocument
|
||||||
*
|
*
|
||||||
* @return Node[]
|
* @return Node[]
|
||||||
*/
|
*/
|
||||||
public function getDefinitions()
|
public function getDefinitionNodes()
|
||||||
{
|
{
|
||||||
return $this->definitions;
|
return $this->definitionNodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a map from fully qualified name (FQN) to SymbolInformation
|
* Returns a map from fully qualified name (FQN) to Definition defined in this document
|
||||||
*
|
*
|
||||||
* @return SymbolInformation[]
|
* @return Definition[]
|
||||||
*/
|
*/
|
||||||
public function getSymbols()
|
public function getDefinitions()
|
||||||
{
|
{
|
||||||
return $this->symbols;
|
return $this->definitions;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -325,43 +339,6 @@ class PhpDocument
|
||||||
return isset($this->definitions[$fqn]);
|
return isset($this->definitions[$fqn]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the definition node for any node
|
|
||||||
* The definition node MAY be in another document, check the ownerDocument attribute
|
|
||||||
*
|
|
||||||
* @param Node $node
|
|
||||||
* @return Promise <Node|null>
|
|
||||||
*/
|
|
||||||
public function getDefinitionByNode(Node $node): Promise
|
|
||||||
{
|
|
||||||
return coroutine(function () use ($node) {
|
|
||||||
// Variables always stay in the boundary of the file and need to be searched inside their function scope
|
|
||||||
// by traversing the AST
|
|
||||||
if ($node instanceof Node\Expr\Variable) {
|
|
||||||
return getVariableDefinition($node);
|
|
||||||
}
|
|
||||||
$fqn = getReferencedFqn($node);
|
|
||||||
if (!isset($fqn)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
$document = yield $this->project->getDefinitionDocument($fqn);
|
|
||||||
if (!isset($document)) {
|
|
||||||
// If the node is a function or constant, it could be namespaced, but PHP falls back to global
|
|
||||||
// http://php.net/manual/en/language.namespaces.fallback.php
|
|
||||||
$parent = $node->getAttribute('parentNode');
|
|
||||||
if ($parent instanceof Node\Expr\ConstFetch || $parent instanceof Node\Expr\FuncCall) {
|
|
||||||
$parts = explode('\\', $fqn);
|
|
||||||
$fqn = end($parts);
|
|
||||||
$document = yield $this->project->getDefinitionDocument($fqn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!isset($document)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return $document->getDefinitionByFqn($fqn);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the reference nodes for any node
|
* Returns the reference nodes for any node
|
||||||
* The references node MAY be in other documents, check the ownerDocument attribute
|
* The references node MAY be in other documents, check the ownerDocument attribute
|
||||||
|
@ -369,12 +346,16 @@ class PhpDocument
|
||||||
* @param Node $node
|
* @param Node $node
|
||||||
* @return Promise <Node[]>
|
* @return Promise <Node[]>
|
||||||
*/
|
*/
|
||||||
public function getReferencesByNode(Node $node): Promise
|
public function getReferenceNodesByNode(Node $node): Promise
|
||||||
{
|
{
|
||||||
return coroutine(function () use ($node) {
|
return coroutine(function () use ($node) {
|
||||||
// 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 || $node instanceof Node\Param) {
|
if (
|
||||||
|
$node instanceof Node\Expr\Variable
|
||||||
|
|| $node instanceof Node\Param
|
||||||
|
|| $node instanceof Node\Expr\ClosureUse
|
||||||
|
) {
|
||||||
if ($node->name instanceof Node\Expr) {
|
if ($node->name instanceof Node\Expr) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -390,17 +371,17 @@ class PhpDocument
|
||||||
$refCollector = new VariableReferencesCollector($node->name);
|
$refCollector = new VariableReferencesCollector($node->name);
|
||||||
$traverser->addVisitor($refCollector);
|
$traverser->addVisitor($refCollector);
|
||||||
$traverser->traverse($n->getStmts());
|
$traverser->traverse($n->getStmts());
|
||||||
return $refCollector->references;
|
return $refCollector->nodes;
|
||||||
}
|
}
|
||||||
// Definition with a global FQN
|
// Definition with a global FQN
|
||||||
$fqn = getDefinedFqn($node);
|
$fqn = DefinitionResolver::getDefinedFqn($node);
|
||||||
if ($fqn === null) {
|
if ($fqn === null) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
$refDocuments = yield $this->project->getReferenceDocuments($fqn);
|
$refDocuments = yield $this->project->getReferenceDocuments($fqn);
|
||||||
$nodes = [];
|
$nodes = [];
|
||||||
foreach ($refDocuments as $document) {
|
foreach ($refDocuments as $document) {
|
||||||
$refs = $document->getReferencesByFqn($fqn);
|
$refs = $document->getReferenceNodesByFqn($fqn);
|
||||||
if ($refs !== null) {
|
if ($refs !== null) {
|
||||||
foreach ($refs as $ref) {
|
foreach ($refs as $ref) {
|
||||||
$nodes[] = $ref;
|
$nodes[] = $ref;
|
||||||
|
|
|
@ -19,11 +19,11 @@ class Project
|
||||||
private $documents = [];
|
private $documents = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An associative array that maps fully qualified symbol names to SymbolInformations
|
* An associative array that maps fully qualified symbol names to Definitions
|
||||||
*
|
*
|
||||||
* @var SymbolInformation[]
|
* @var Definition[]
|
||||||
*/
|
*/
|
||||||
private $symbols = [];
|
private $definitions = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
|
@ -46,6 +46,13 @@ class Project
|
||||||
*/
|
*/
|
||||||
private $docBlockFactory;
|
private $docBlockFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The DefinitionResolver instance to resolve reference nodes to Definitions
|
||||||
|
*
|
||||||
|
* @var DefinitionResolver
|
||||||
|
*/
|
||||||
|
private $definitionResolver;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reference to the language server client interface
|
* Reference to the language server client interface
|
||||||
*
|
*
|
||||||
|
@ -66,6 +73,7 @@ class Project
|
||||||
$this->clientCapabilities = $clientCapabilities;
|
$this->clientCapabilities = $clientCapabilities;
|
||||||
$this->parser = new Parser;
|
$this->parser = new Parser;
|
||||||
$this->docBlockFactory = DocBlockFactory::createInstance();
|
$this->docBlockFactory = DocBlockFactory::createInstance();
|
||||||
|
$this->definitionResolver = new DefinitionResolver($this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -122,7 +130,15 @@ class Project
|
||||||
$document = $this->documents[$uri];
|
$document = $this->documents[$uri];
|
||||||
$document->updateContent($content);
|
$document->updateContent($content);
|
||||||
} else {
|
} else {
|
||||||
$document = new PhpDocument($uri, $content, $this, $this->client, $this->parser, $this->docBlockFactory);
|
$document = new PhpDocument(
|
||||||
|
$uri,
|
||||||
|
$content,
|
||||||
|
$this,
|
||||||
|
$this->client,
|
||||||
|
$this->parser,
|
||||||
|
$this->docBlockFactory,
|
||||||
|
$this->definitionResolver
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return $document;
|
return $document;
|
||||||
});
|
});
|
||||||
|
@ -141,7 +157,15 @@ class Project
|
||||||
$document = $this->documents[$uri];
|
$document = $this->documents[$uri];
|
||||||
$document->updateContent($content);
|
$document->updateContent($content);
|
||||||
} else {
|
} else {
|
||||||
$document = new PhpDocument($uri, $content, $this, $this->client, $this->parser, $this->docBlockFactory);
|
$document = new PhpDocument(
|
||||||
|
$uri,
|
||||||
|
$content,
|
||||||
|
$this,
|
||||||
|
$this->client,
|
||||||
|
$this->parser,
|
||||||
|
$this->docBlockFactory,
|
||||||
|
$this->definitionResolver
|
||||||
|
);
|
||||||
$this->documents[$uri] = $document;
|
$this->documents[$uri] = $document;
|
||||||
}
|
}
|
||||||
return $document;
|
return $document;
|
||||||
|
@ -170,49 +194,67 @@ class Project
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an associative array [string => string] that maps fully qualified symbol names
|
* Returns an associative array [string => Definition] that maps fully qualified symbol names
|
||||||
* to URIs of the document where the symbol is defined
|
* to Definitions
|
||||||
*
|
*
|
||||||
* @return SymbolInformation[]
|
* @return Definitions[]
|
||||||
*/
|
*/
|
||||||
public function getSymbols()
|
public function getDefinitions()
|
||||||
{
|
{
|
||||||
return $this->symbols;
|
return $this->definitions;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a SymbolInformation for a specific symbol
|
* Returns the Definition object by a specific FQN
|
||||||
|
*
|
||||||
|
* @param string $fqn
|
||||||
|
* @param bool $globalFallback Whether to fallback to global if the namespaced FQN was not found
|
||||||
|
* @return Definition|null
|
||||||
|
*/
|
||||||
|
public function getDefinition(string $fqn, $globalFallback = false)
|
||||||
|
{
|
||||||
|
if (isset($this->definitions[$fqn])) {
|
||||||
|
return $this->definitions[$fqn];
|
||||||
|
} else if ($globalFallback) {
|
||||||
|
$parts = explode('\\', $fqn);
|
||||||
|
$fqn = end($parts);
|
||||||
|
return $this->getDefinition($fqn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a definition
|
||||||
*
|
*
|
||||||
* @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 $definition The Definition object
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function setSymbol(string $fqn, SymbolInformation $symbol)
|
public function setDefinition(string $fqn, Definition $definition)
|
||||||
{
|
{
|
||||||
$this->symbols[$fqn] = $symbol;
|
$this->definitions[$fqn] = $definition;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the SymbolInformation index
|
* Sets the Definition index
|
||||||
*
|
*
|
||||||
* @param SymbolInformation[] $symbols
|
* @param Definition[] $definitions Map from FQN to Definition
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function setSymbols(array $symbols)
|
public function setDefinitions(array $definitions)
|
||||||
{
|
{
|
||||||
$this->symbols = $symbols;
|
$this->definitions = $definitions;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unsets the SymbolInformation for a specific symbol
|
* Unsets the Definition 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 removeSymbol(string $fqn)
|
public function removeDefinition(string $fqn)
|
||||||
{
|
{
|
||||||
unset($this->symbols[$fqn]);
|
unset($this->definitions[$fqn]);
|
||||||
unset($this->references[$fqn]);
|
unset($this->references[$fqn]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -296,10 +338,10 @@ class Project
|
||||||
*/
|
*/
|
||||||
public function getDefinitionDocument(string $fqn): Promise
|
public function getDefinitionDocument(string $fqn): Promise
|
||||||
{
|
{
|
||||||
if (!isset($this->symbols[$fqn])) {
|
if (!isset($this->definitions[$fqn])) {
|
||||||
return Promise\resolve(null);
|
return Promise\resolve(null);
|
||||||
}
|
}
|
||||||
return $this->getOrLoadDocument($this->symbols[$fqn]->location->uri);
|
return $this->getOrLoadDocument($this->definitions[$fqn]->symbolInformation->location->uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -44,27 +44,48 @@ class SymbolInformation
|
||||||
*
|
*
|
||||||
* @param Node $node
|
* @param Node $node
|
||||||
* @param string $fqn If given, $containerName will be extracted from it
|
* @param string $fqn If given, $containerName will be extracted from it
|
||||||
* @return self
|
* @return self|null
|
||||||
*/
|
*/
|
||||||
public static function fromNode(Node $node, string $fqn = null)
|
public static function fromNode(Node $node, string $fqn = null)
|
||||||
{
|
{
|
||||||
$nodeSymbolKindMap = [
|
|
||||||
Node\Stmt\Class_::class => SymbolKind::CLASS_,
|
|
||||||
Node\Stmt\Trait_::class => SymbolKind::CLASS_,
|
|
||||||
Node\Stmt\Interface_::class => SymbolKind::INTERFACE,
|
|
||||||
Node\Stmt\Namespace_::class => SymbolKind::NAMESPACE,
|
|
||||||
Node\Stmt\Function_::class => SymbolKind::FUNCTION,
|
|
||||||
Node\Stmt\ClassMethod::class => SymbolKind::METHOD,
|
|
||||||
Node\Stmt\PropertyProperty::class => SymbolKind::PROPERTY,
|
|
||||||
Node\Const_::class => SymbolKind::CONSTANT
|
|
||||||
];
|
|
||||||
$class = get_class($node);
|
|
||||||
if (!isset($nodeSymbolKindMap[$class])) {
|
|
||||||
throw new Exception("Not a declaration node: $class");
|
|
||||||
}
|
|
||||||
$symbol = new self;
|
$symbol = new self;
|
||||||
$symbol->kind = $nodeSymbolKindMap[$class];
|
if ($node instanceof Node\Stmt\Class_) {
|
||||||
$symbol->name = (string)$node->name;
|
$symbol->kind = SymbolKind::CLASS_;
|
||||||
|
} else if ($node instanceof Node\Stmt\Trait_) {
|
||||||
|
$symbol->kind = SymbolKind::CLASS_;
|
||||||
|
} else if ($node instanceof Node\Stmt\Interface_) {
|
||||||
|
$symbol->kind = SymbolKind::INTERFACE;
|
||||||
|
} else if ($node instanceof Node\Stmt\Namespace_) {
|
||||||
|
$symbol->kind = SymbolKind::NAMESPACE;
|
||||||
|
} else if ($node instanceof Node\Stmt\Function_) {
|
||||||
|
$symbol->kind = SymbolKind::FUNCTION;
|
||||||
|
} else if ($node instanceof Node\Stmt\ClassMethod) {
|
||||||
|
$symbol->kind = SymbolKind::METHOD;
|
||||||
|
} else if ($node instanceof Node\Stmt\PropertyProperty) {
|
||||||
|
$symbol->kind = SymbolKind::PROPERTY;
|
||||||
|
} else if ($node instanceof Node\Const_) {
|
||||||
|
$symbol->kind = SymbolKind::CONSTANT;
|
||||||
|
} else if (
|
||||||
|
(
|
||||||
|
($node instanceof Node\Expr\Assign || $node instanceof Node\Expr\AssignOp)
|
||||||
|
&& $node->var instanceof Node\Expr\Variable
|
||||||
|
)
|
||||||
|
|| $node instanceof Node\Expr\ClosureUse
|
||||||
|
|| $node instanceof Node\Param
|
||||||
|
) {
|
||||||
|
$symbol->kind = SymbolKind::VARIABLE;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if ($node instanceof Node\Expr\Assign || $node instanceof Node\Expr\AssignOp) {
|
||||||
|
$symbol->name = $node->var->name;
|
||||||
|
} else if ($node instanceof Node\Expr\ClosureUse) {
|
||||||
|
$symbol->name = $node->var;
|
||||||
|
} else if (isset($node->name)) {
|
||||||
|
$symbol->name = (string)$node->name;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
$symbol->location = Location::fromNode($node);
|
$symbol->location = Location::fromNode($node);
|
||||||
if ($fqn !== null) {
|
if ($fqn !== null) {
|
||||||
$parts = preg_split('/(::|\\\\)/', $fqn);
|
$parts = preg_split('/(::|\\\\)/', $fqn);
|
||||||
|
|
|
@ -3,7 +3,7 @@ declare(strict_types = 1);
|
||||||
|
|
||||||
namespace LanguageServer\Server;
|
namespace LanguageServer\Server;
|
||||||
|
|
||||||
use LanguageServer\{LanguageClient, Project, PhpDocument};
|
use LanguageServer\{LanguageClient, Project, PhpDocument, DefinitionResolver};
|
||||||
use PhpParser\PrettyPrinter\Standard as PrettyPrinter;
|
use PhpParser\PrettyPrinter\Standard as PrettyPrinter;
|
||||||
use PhpParser\Node;
|
use PhpParser\Node;
|
||||||
use LanguageServer\Protocol\{
|
use LanguageServer\Protocol\{
|
||||||
|
@ -45,11 +45,17 @@ class TextDocument
|
||||||
*/
|
*/
|
||||||
private $prettyPrinter;
|
private $prettyPrinter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var DefinitionResolver
|
||||||
|
*/
|
||||||
|
private $definitionResolver;
|
||||||
|
|
||||||
public function __construct(Project $project, LanguageClient $client)
|
public function __construct(Project $project, LanguageClient $client)
|
||||||
{
|
{
|
||||||
$this->project = $project;
|
$this->project = $project;
|
||||||
$this->client = $client;
|
$this->client = $client;
|
||||||
$this->prettyPrinter = new PrettyPrinter();
|
$this->prettyPrinter = new PrettyPrinter();
|
||||||
|
$this->definitionResolver = new DefinitionResolver($project);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -62,7 +68,11 @@ class TextDocument
|
||||||
public function documentSymbol(TextDocumentIdentifier $textDocument): Promise
|
public function documentSymbol(TextDocumentIdentifier $textDocument): Promise
|
||||||
{
|
{
|
||||||
return $this->project->getOrLoadDocument($textDocument->uri)->then(function (PhpDocument $document) {
|
return $this->project->getOrLoadDocument($textDocument->uri)->then(function (PhpDocument $document) {
|
||||||
return array_values($document->getSymbols());
|
$symbols = [];
|
||||||
|
foreach ($document->getDefinitions() as $fqn => $definition) {
|
||||||
|
$symbols[] = $definition->symbolInformation;
|
||||||
|
}
|
||||||
|
return $symbols;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,7 +146,7 @@ class TextDocument
|
||||||
if ($node === null) {
|
if ($node === null) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
$refs = yield $document->getReferencesByNode($node);
|
$refs = yield $document->getReferenceNodesByNode($node);
|
||||||
$locations = [];
|
$locations = [];
|
||||||
foreach ($refs as $ref) {
|
foreach ($refs as $ref) {
|
||||||
$locations[] = Location::fromNode($ref);
|
$locations[] = Location::fromNode($ref);
|
||||||
|
@ -161,11 +171,11 @@ class TextDocument
|
||||||
if ($node === null) {
|
if ($node === null) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
$def = yield $document->getDefinitionByNode($node);
|
$def = $this->definitionResolver->resolveReferenceNodeToDefinition($node);
|
||||||
if ($def === null) {
|
if ($def === null || $def->symbolInformation === null) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return Location::fromNode($def);
|
return $def->symbolInformation->location;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,55 +196,17 @@ class TextDocument
|
||||||
return new Hover([]);
|
return new Hover([]);
|
||||||
}
|
}
|
||||||
$range = Range::fromNode($node);
|
$range = Range::fromNode($node);
|
||||||
// Get the definition node for whatever node is under the cursor
|
// Get the definition for whatever node is under the cursor
|
||||||
$def = yield $document->getDefinitionByNode($node);
|
$def = $this->definitionResolver->resolveReferenceNodeToDefinition($node);
|
||||||
if ($def === null) {
|
if ($def === null) {
|
||||||
return new Hover([], $range);
|
return new Hover([], $range);
|
||||||
}
|
}
|
||||||
$contents = [];
|
if ($def->declarationLine) {
|
||||||
|
$contents[] = new MarkedString('php', "<?php\n" . $def->declarationLine);
|
||||||
// Build a declaration string
|
|
||||||
if ($def instanceof Node\Stmt\PropertyProperty || $def instanceof Node\Const_) {
|
|
||||||
// Properties and constants can have multiple declarations
|
|
||||||
// Use the parent node (that includes the modifiers), but only render the requested declaration
|
|
||||||
$child = $def;
|
|
||||||
$def = $def->getAttribute('parentNode');
|
|
||||||
$defLine = clone $def;
|
|
||||||
$defLine->props = [$child];
|
|
||||||
} else {
|
|
||||||
$defLine = clone $def;
|
|
||||||
}
|
}
|
||||||
// Don't include the docblock in the declaration string
|
if ($def->documentation) {
|
||||||
$defLine->setAttribute('comments', []);
|
$contents[] = $def->documentation;
|
||||||
if (isset($defLine->stmts)) {
|
|
||||||
$defLine->stmts = [];
|
|
||||||
}
|
}
|
||||||
$defText = $this->prettyPrinter->prettyPrint([$defLine]);
|
|
||||||
$lines = explode("\n", $defText);
|
|
||||||
if (isset($lines[0])) {
|
|
||||||
$contents[] = new MarkedString('php', "<?php\n" . $lines[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the documentation string
|
|
||||||
if ($def instanceof Node\Param) {
|
|
||||||
$fn = $def->getAttribute('parentNode');
|
|
||||||
$docBlock = $fn->getAttribute('docBlock');
|
|
||||||
if ($docBlock !== null) {
|
|
||||||
$tags = $docBlock->getTagsByName('param');
|
|
||||||
foreach ($tags as $tag) {
|
|
||||||
if ($tag->getVariableName() === $def->name) {
|
|
||||||
$contents[] = $tag->getDescription()->render();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$docBlock = $def->getAttribute('docBlock');
|
|
||||||
if ($docBlock !== null) {
|
|
||||||
$contents[] = $docBlock->getSummary();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Hover($contents, $range);
|
return new Hover($contents, $range);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,13 +39,10 @@ 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->getSymbols() as $fqn => $symbol) {
|
foreach ($this->project->getDefinitions() as $fqn => $definition) {
|
||||||
if (stripos($fqn, $query) !== false) {
|
if ($query === '' || stripos($fqn, $query) !== false) {
|
||||||
$symbols[] = $symbol;
|
$symbols[] = $definition->symbolInformation;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $symbols;
|
return $symbols;
|
||||||
|
|
|
@ -5,6 +5,7 @@ namespace LanguageServer;
|
||||||
|
|
||||||
use Throwable;
|
use Throwable;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
|
use PhpParser\Node;
|
||||||
use Sabre\Event\{Loop, Promise};
|
use Sabre\Event\{Loop, Promise};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -77,3 +78,20 @@ function timeout($seconds = 0): Promise
|
||||||
Loop\setTimeout([$promise, 'fulfill'], $seconds);
|
Loop\setTimeout([$promise, 'fulfill'], $seconds);
|
||||||
return $promise;
|
return $promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the closest node of a specific type
|
||||||
|
*
|
||||||
|
* @param Node $node
|
||||||
|
* @param string $type The node class name
|
||||||
|
* @return Node|null $type
|
||||||
|
*/
|
||||||
|
function getClosestNode(Node $node, string $type)
|
||||||
|
{
|
||||||
|
$n = $node;
|
||||||
|
while ($n = $n->getAttribute('parentNode')) {
|
||||||
|
if ($n instanceof $type) {
|
||||||
|
return $n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ namespace LanguageServer\Tests\Server\TextDocument;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use PhpParser\{NodeTraverser, Node};
|
use PhpParser\{NodeTraverser, Node};
|
||||||
use PhpParser\NodeVisitor\NameResolver;
|
use PhpParser\NodeVisitor\NameResolver;
|
||||||
use LanguageServer\{LanguageClient, Project, PhpDocument, Parser};
|
use LanguageServer\{LanguageClient, Project, PhpDocument, Parser, DefinitionResolver};
|
||||||
use LanguageServer\Protocol\ClientCapabilities;
|
use LanguageServer\Protocol\ClientCapabilities;
|
||||||
use LanguageServer\Tests\MockProtocolStream;
|
use LanguageServer\Tests\MockProtocolStream;
|
||||||
use LanguageServer\NodeVisitor\{ReferencesAdder, DefinitionCollector};
|
use LanguageServer\NodeVisitor\{ReferencesAdder, DefinitionCollector};
|
||||||
|
@ -24,11 +24,11 @@ class DefinitionCollectorTest extends TestCase
|
||||||
$traverser = new NodeTraverser;
|
$traverser = new NodeTraverser;
|
||||||
$traverser->addVisitor(new NameResolver);
|
$traverser->addVisitor(new NameResolver);
|
||||||
$traverser->addVisitor(new ReferencesAdder($document));
|
$traverser->addVisitor(new ReferencesAdder($document));
|
||||||
$definitionCollector = new DefinitionCollector;
|
$definitionCollector = new DefinitionCollector(new DefinitionResolver($project));
|
||||||
$traverser->addVisitor($definitionCollector);
|
$traverser->addVisitor($definitionCollector);
|
||||||
$stmts = $parser->parse(file_get_contents($uri));
|
$stmts = $parser->parse(file_get_contents($uri));
|
||||||
$traverser->traverse($stmts);
|
$traverser->traverse($stmts);
|
||||||
$defs = $definitionCollector->definitions;
|
$defNodes = $definitionCollector->nodes;
|
||||||
$this->assertEquals([
|
$this->assertEquals([
|
||||||
'TestNamespace\\TEST_CONST',
|
'TestNamespace\\TEST_CONST',
|
||||||
'TestNamespace\\TestClass',
|
'TestNamespace\\TestClass',
|
||||||
|
@ -40,17 +40,17 @@ class DefinitionCollectorTest extends TestCase
|
||||||
'TestNamespace\\TestTrait',
|
'TestNamespace\\TestTrait',
|
||||||
'TestNamespace\\TestInterface',
|
'TestNamespace\\TestInterface',
|
||||||
'TestNamespace\\test_function()'
|
'TestNamespace\\test_function()'
|
||||||
], array_keys($defs));
|
], array_keys($defNodes));
|
||||||
$this->assertInstanceOf(Node\Const_::class, $defs['TestNamespace\\TEST_CONST']);
|
$this->assertInstanceOf(Node\Const_::class, $defNodes['TestNamespace\\TEST_CONST']);
|
||||||
$this->assertInstanceOf(Node\Stmt\Class_::class, $defs['TestNamespace\\TestClass']);
|
$this->assertInstanceOf(Node\Stmt\Class_::class, $defNodes['TestNamespace\\TestClass']);
|
||||||
$this->assertInstanceOf(Node\Const_::class, $defs['TestNamespace\\TestClass::TEST_CLASS_CONST']);
|
$this->assertInstanceOf(Node\Const_::class, $defNodes['TestNamespace\\TestClass::TEST_CLASS_CONST']);
|
||||||
$this->assertInstanceOf(Node\Stmt\PropertyProperty::class, $defs['TestNamespace\\TestClass::staticTestProperty']);
|
$this->assertInstanceOf(Node\Stmt\PropertyProperty::class, $defNodes['TestNamespace\\TestClass::staticTestProperty']);
|
||||||
$this->assertInstanceOf(Node\Stmt\PropertyProperty::class, $defs['TestNamespace\\TestClass::testProperty']);
|
$this->assertInstanceOf(Node\Stmt\PropertyProperty::class, $defNodes['TestNamespace\\TestClass::testProperty']);
|
||||||
$this->assertInstanceOf(Node\Stmt\ClassMethod::class, $defs['TestNamespace\\TestClass::staticTestMethod()']);
|
$this->assertInstanceOf(Node\Stmt\ClassMethod::class, $defNodes['TestNamespace\\TestClass::staticTestMethod()']);
|
||||||
$this->assertInstanceOf(Node\Stmt\ClassMethod::class, $defs['TestNamespace\\TestClass::testMethod()']);
|
$this->assertInstanceOf(Node\Stmt\ClassMethod::class, $defNodes['TestNamespace\\TestClass::testMethod()']);
|
||||||
$this->assertInstanceOf(Node\Stmt\Trait_::class, $defs['TestNamespace\\TestTrait']);
|
$this->assertInstanceOf(Node\Stmt\Trait_::class, $defNodes['TestNamespace\\TestTrait']);
|
||||||
$this->assertInstanceOf(Node\Stmt\Interface_::class, $defs['TestNamespace\\TestInterface']);
|
$this->assertInstanceOf(Node\Stmt\Interface_::class, $defNodes['TestNamespace\\TestInterface']);
|
||||||
$this->assertInstanceOf(Node\Stmt\Function_::class, $defs['TestNamespace\\test_function()']);
|
$this->assertInstanceOf(Node\Stmt\Function_::class, $defNodes['TestNamespace\\test_function()']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testDoesNotCollectReferences()
|
public function testDoesNotCollectReferences()
|
||||||
|
@ -63,12 +63,12 @@ class DefinitionCollectorTest extends TestCase
|
||||||
$traverser = new NodeTraverser;
|
$traverser = new NodeTraverser;
|
||||||
$traverser->addVisitor(new NameResolver);
|
$traverser->addVisitor(new NameResolver);
|
||||||
$traverser->addVisitor(new ReferencesAdder($document));
|
$traverser->addVisitor(new ReferencesAdder($document));
|
||||||
$definitionCollector = new DefinitionCollector;
|
$definitionCollector = new DefinitionCollector(new DefinitionResolver($project));
|
||||||
$traverser->addVisitor($definitionCollector);
|
$traverser->addVisitor($definitionCollector);
|
||||||
$stmts = $parser->parse(file_get_contents($uri));
|
$stmts = $parser->parse(file_get_contents($uri));
|
||||||
$traverser->traverse($stmts);
|
$traverser->traverse($stmts);
|
||||||
$defs = $definitionCollector->definitions;
|
$defNodes = $definitionCollector->nodes;
|
||||||
$this->assertEquals(['TestNamespace\\whatever()'], array_keys($defs));
|
$this->assertEquals(['TestNamespace\\whatever()'], array_keys($defNodes));
|
||||||
$this->assertInstanceOf(Node\Stmt\Function_::class, $defs['TestNamespace\\whatever()']);
|
$this->assertInstanceOf(Node\Stmt\Function_::class, $defNodes['TestNamespace\\whatever()']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -105,7 +105,8 @@ abstract class ServerTestCase extends TestCase
|
||||||
3 => new Location($referencesUri, new Range(new Position( 9, 5), new Position( 9, 14))), // TestClass::TEST_CLASS_CONST;
|
3 => new Location($referencesUri, new Range(new Position( 9, 5), new Position( 9, 14))), // TestClass::TEST_CLASS_CONST;
|
||||||
4 => new Location($referencesUri, new Range(new Position(21, 18), new Position(21, 27))), // function whatever(TestClass $param)
|
4 => new Location($referencesUri, new Range(new Position(21, 18), new Position(21, 27))), // function whatever(TestClass $param)
|
||||||
5 => new Location($referencesUri, new Range(new Position(21, 37), new Position(21, 46))), // function whatever(TestClass $param): TestClass
|
5 => new Location($referencesUri, new Range(new Position(21, 37), new Position(21, 46))), // function whatever(TestClass $param): TestClass
|
||||||
6 => new Location($useUri, new Range(new Position( 4, 4), new Position( 4, 27))), // use TestNamespace\TestClass;
|
6 => new Location($referencesUri, new Range(new Position(39, 0), new Position(39, 9))), // TestClass::$staticTestProperty[123]->testProperty;
|
||||||
|
7 => new Location($useUri, new Range(new Position( 4, 4), new Position( 4, 27))), // use TestNamespace\TestClass;
|
||||||
],
|
],
|
||||||
'TestNamespace\\TestInterface' => [
|
'TestNamespace\\TestInterface' => [
|
||||||
0 => new Location($symbolsUri, new Range(new Position(20, 27), new Position(20, 40))), // class TestClass implements TestInterface
|
0 => new Location($symbolsUri, new Range(new Position(20, 27), new Position(20, 40))), // class TestClass implements TestInterface
|
||||||
|
@ -118,16 +119,20 @@ abstract class ServerTestCase extends TestCase
|
||||||
],
|
],
|
||||||
'TestNamespace\\TestClass::testProperty' => [
|
'TestNamespace\\TestClass::testProperty' => [
|
||||||
0 => new Location($symbolsUri, new Range(new Position(59, 8), new Position(59, 27))), // $this->testProperty = $testParameter;
|
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)))
|
1 => new Location($referencesUri, new Range(new Position( 6, 5), new Position( 6, 23))), // echo $obj->testProperty;
|
||||||
|
2 => new Location($referencesUri, new Range(new Position(38, 0), new Position(38, 18))), // $obj->testProperty->testMethod();
|
||||||
|
3 => new Location($referencesUri, new Range(new Position(39, 0), new Position(39, 49))) // TestClass::$staticTestProperty[123]->testProperty;
|
||||||
],
|
],
|
||||||
'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))), // echo TestClass::$staticTestProperty;
|
||||||
|
1 => new Location($referencesUri, new Range(new Position(39, 0), new Position(39, 30))) // TestClass::$staticTestProperty[123]->testProperty;
|
||||||
],
|
],
|
||||||
'TestNamespace\\TestClass::staticTestMethod()' => [
|
'TestNamespace\\TestClass::staticTestMethod()' => [
|
||||||
0 => new Location($referencesUri, new Range(new Position( 7, 0), new Position( 7, 29)))
|
0 => new Location($referencesUri, new Range(new Position( 7, 0), new Position( 7, 29)))
|
||||||
],
|
],
|
||||||
'TestNamespace\\TestClass::testMethod()' => [
|
'TestNamespace\\TestClass::testMethod()' => [
|
||||||
0 => new Location($referencesUri, new Range(new Position( 5, 0), new Position( 5, 18)))
|
0 => new Location($referencesUri, new Range(new Position( 5, 0), new Position( 5, 18))), // $obj->testMethod();
|
||||||
|
1 => new Location($referencesUri, new Range(new Position(38, 0), new Position(38, 32))) // $obj->testProperty->testMethod();
|
||||||
],
|
],
|
||||||
'TestNamespace\\test_function()' => [
|
'TestNamespace\\test_function()' => [
|
||||||
0 => new Location($referencesUri, new Range(new Position(10, 0), new Position(10, 13))),
|
0 => new Location($referencesUri, new Range(new Position(10, 0), new Position(10, 13))),
|
||||||
|
@ -146,6 +151,7 @@ abstract class ServerTestCase extends TestCase
|
||||||
3 => new Location($globalReferencesUri, new Range(new Position( 9, 5), new Position( 9, 14))), // TestClass::TEST_CLASS_CONST;
|
3 => new Location($globalReferencesUri, new Range(new Position( 9, 5), new Position( 9, 14))), // TestClass::TEST_CLASS_CONST;
|
||||||
4 => new Location($globalReferencesUri, new Range(new Position(21, 18), new Position(21, 27))), // function whatever(TestClass $param)
|
4 => new Location($globalReferencesUri, new Range(new Position(21, 18), new Position(21, 27))), // function whatever(TestClass $param)
|
||||||
5 => new Location($globalReferencesUri, new Range(new Position(21, 37), new Position(21, 46))), // function whatever(TestClass $param): TestClass
|
5 => new Location($globalReferencesUri, new Range(new Position(21, 37), new Position(21, 46))), // function whatever(TestClass $param): TestClass
|
||||||
|
6 => new Location($globalReferencesUri, new Range(new Position(39, 0), new Position(39, 9))), // TestClass::$staticTestProperty[123]->testProperty;
|
||||||
],
|
],
|
||||||
'TestInterface' => [
|
'TestInterface' => [
|
||||||
0 => new Location($globalSymbolsUri, new Range(new Position(20, 27), new Position(20, 40))), // class TestClass implements TestInterface
|
0 => new Location($globalSymbolsUri, new Range(new Position(20, 27), new Position(20, 40))), // class TestClass implements TestInterface
|
||||||
|
@ -158,16 +164,20 @@ abstract class ServerTestCase extends TestCase
|
||||||
],
|
],
|
||||||
'TestClass::testProperty' => [
|
'TestClass::testProperty' => [
|
||||||
0 => new Location($globalSymbolsUri, new Range(new Position(59, 8), new Position(59, 27))), // $this->testProperty = $testParameter;
|
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)))
|
1 => new Location($globalReferencesUri, new Range(new Position( 6, 5), new Position( 6, 23))), // echo $obj->testProperty;
|
||||||
|
2 => new Location($globalReferencesUri, new Range(new Position(38, 0), new Position(38, 18))), // $obj->testProperty->testMethod();
|
||||||
|
3 => new Location($globalReferencesUri, new Range(new Position(39, 0), new Position(39, 49))) // TestClass::$staticTestProperty[123]->testProperty;
|
||||||
],
|
],
|
||||||
'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))), // echo TestClass::$staticTestProperty;
|
||||||
|
1 => new Location($globalReferencesUri, new Range(new Position(39, 0), new Position(39, 30))) // TestClass::$staticTestProperty[123]->testProperty;
|
||||||
],
|
],
|
||||||
'TestClass::staticTestMethod()' => [
|
'TestClass::staticTestMethod()' => [
|
||||||
0 => new Location($globalReferencesUri, new Range(new Position( 7, 0), new Position( 7, 29)))
|
0 => new Location($globalReferencesUri, new Range(new Position( 7, 0), new Position( 7, 29)))
|
||||||
],
|
],
|
||||||
'TestClass::testMethod()' => [
|
'TestClass::testMethod()' => [
|
||||||
0 => new Location($globalReferencesUri, new Range(new Position( 5, 0), new Position( 5, 18)))
|
0 => new Location($globalReferencesUri, new Range(new Position( 5, 0), new Position( 5, 18))), // $obj->testMethod();
|
||||||
|
1 => new Location($globalReferencesUri, new Range(new Position(38, 0), new Position(38, 32))) // $obj->testProperty->testMethod();
|
||||||
],
|
],
|
||||||
'test_function()' => [
|
'test_function()' => [
|
||||||
0 => new Location($globalReferencesUri, new Range(new Position(10, 0), new Position(10, 13))),
|
0 => new Location($globalReferencesUri, new Range(new Position(10, 0), new Position(10, 13))),
|
||||||
|
|
|
@ -292,4 +292,28 @@ class GlobalTest extends ServerTestCase
|
||||||
)->wait();
|
)->wait();
|
||||||
$this->assertEquals($this->getDefinitionLocation('TestInterface'), $result);
|
$this->assertEquals($this->getDefinitionLocation('TestInterface'), $result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testDefinitionForNestedMethodCall()
|
||||||
|
{
|
||||||
|
// $obj->testProperty->testMethod();
|
||||||
|
// Get definition for testMethod
|
||||||
|
$reference = $this->getReferenceLocations('TestClass::testMethod()')[1];
|
||||||
|
$result = $this->textDocument->definition(
|
||||||
|
new TextDocumentIdentifier($reference->uri),
|
||||||
|
$reference->range->end
|
||||||
|
)->wait();
|
||||||
|
$this->assertEquals($this->getDefinitionLocation('TestClass::testMethod()'), $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDefinitionForPropertyFetchOnArrayDimFetch()
|
||||||
|
{
|
||||||
|
// TestClass::$staticTestProperty[123]->testProperty;
|
||||||
|
// Get definition for testProperty
|
||||||
|
$reference = $this->getReferenceLocations('TestClass::testProperty')[3];
|
||||||
|
$result = $this->textDocument->definition(
|
||||||
|
new TextDocumentIdentifier($reference->uri),
|
||||||
|
$reference->range->end
|
||||||
|
)->wait();
|
||||||
|
$this->assertEquals($this->getDefinitionLocation('TestClass::testProperty'), $result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue