1
0
Fork 0

Add recursive DefinitionResolver

pull/155/head
Felix Becker 2016-11-17 21:25:25 +01:00
parent e83f95efca
commit 83fd96c52a
11 changed files with 704 additions and 355 deletions

View File

@ -41,8 +41,7 @@
"LanguageServer\\": "src/"
},
"files" : [
"src/utils.php",
"src/Fqn.php"
"src/utils.php"
]
},
"autoload-dev": {

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types = 1);
namespace LanguageServer;
@ -6,13 +7,28 @@ use PhpParser\Node;
use phpDocumentor\Reflection\{Types, Type, Fqsen, TypeResolver};
use LanguageServer\Protocol\SymbolInformation;
use Exception;
use function LanguageServer\Fqn\getDefinedFqn;
/**
* Class used to represent definitions that can be referenced by an FQN
* 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
*/
public $fqn;
/**
* @var Protocol\SymbolInformation
*/
@ -30,20 +46,6 @@ class Definition
*/
public $type;
/**
* Returns the definition defined in a node
*
* @return self
* @throws Exception If the node is not a declaration node
*/
public static function fromNode(Node $node): self
{
$def = new self;
$def->symbolInformation = SymbolInformation::fromNode($node, getDefinedFqn($node));
$def->type = self::getTypeFromNode($node);
return $def;
}
/**
* Returns the type a reference to this symbol will resolve to.
* For properties and constants, this is the type of the property/constant.
@ -59,6 +61,32 @@ class Definition
*/
public static function getTypeFromNode(Node $node)
{
if ($node instanceof Node\Param) {
// Parameters
$docBlock = $node->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 = (new TypeResolver)->resolve($node->type);
}
$type = new Types\Object_(new Fqsen('\\' . (string)$node->type));
if ($node->default !== null) {
if (is_string($node->default)) {
// Resolve a string like "bool" to a type object
$defaultType = (new TypeResolver)->resolve($node->default);
}
$defaultType = new Types\Object_(new Fqsen('\\' . (string)$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');
@ -69,7 +97,7 @@ class Definition
if ($node->returnType !== null) {
// Use PHP7 return type hint
if (is_string($node->returnType)) {
// Resolve a string like "integer" to a type object
// Resolve a string like "bool" to a type object
return (new TypeResolver)->resolve($node->returnType);
}
return new Types\Object_(new Fqsen('\\' . (string)$node->returnType));
@ -91,4 +119,53 @@ class Definition
}
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;
}
}
}
}

437
src/DefinitionResolver.php Normal file
View File

@ -0,0 +1,437 @@
<?php
declare(strict_types = 1);
namespace LanguageServer;
use PhpParser\Node;
use phpDocumentor\Reflection\{Types, Type, Fqsen, TypeResolver};
use LanguageServer\Protocol\SymbolInformation;
use Sabre\Event\Promise;
use function Sabre\Event\coroutine;
class DefinitionResolver
{
private $project;
public function __construct(Project $project)
{
$this->project = $project;
}
/**
* 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);
$def = new Definition;
// Get symbol information from node (range, symbol kind)
$def->symbolInformation = SymbolInformation::fromNode($defNode);
if ($defNode instanceof Node\Param) {
// Get parameter type
$def->type = Definition::getTypeFromNode($defNode);
} else {
// Resolve the type of the assignment/closure use node
$def->type = $this->resolveExpression($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;
}
// Return the Definition object from the project index
$def = $this->project->getDefinition($fqn);
if ($def === 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');
if ($parent instanceof Node\Expr\ConstFetch || $parent instanceof Node\Expr\FuncCall) {
$parts = explode('\\', $fqn);
$fqn = end($parts);
$def = $this->project->getDefinition($fqn);
}
}
return $def;
}
/**
* 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->resolveExpression($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->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 Node\Expr $expr
* @return Type
*/
private function resolveExpression(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->resolveExpression($defNode);
}
if ($defNode instanceof Node\Param) {
return Definition::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);
return $this->project->getDefinition($fqn)->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);
return $this->project->getDefinition($fqn)->type;
}
if ($expr instanceof Node\Expr\MethodCall) {
// Resolve object
$objType = $this->resolveExpression($expr->var);
if (!($objType instanceof Types\Object_) || $objType->getFqsen() === null || $expr->name instanceof Node\Expr) {
// Need the class FQN of the object
return new Types\Mixed;
}
$fqn = (string)$objType->getFqsen() . '::' . $expr->name . '()';
return $this->project->getDefinition($fqn)->type;
}
if ($expr instanceof Node\Expr\PropertyFetch) {
// Resolve object
$objType = $this->resolveExpression($expr->var);
if (!($objType instanceof Types\Object_) || $objType->getFqsen() === null || $expr->name instanceof Node\Expr) {
// Need the class FQN of the object
return new Types\Mixed;
}
$fqn = (string)$objType->getFqsen() . '::' . $expr->name;
return $this->project->getDefinition($fqn)->type;
}
if ($expr instanceof Node\Expr\StaticCall) {
if ($expr->class instanceof Node\Expr || $expr->name instanceof Node\Expr) {
// Need the FQN
return new Types\Mixed;
}
$fqn = (string)$expr->class . '::' . $expr->name . '()';
}
if ($expr instanceof Node\Expr\StaticPropertyFetch || $expr instanceof Node\Expr\ClassConstFetch) {
if ($expr->class instanceof Node\Expr || $expr->name instanceof Node\Expr) {
// Need the FQN
return new Types\Mixed;
}
$fqn = (string)$expr->class . '::' . $expr->name;
}
if ($expr instanceof Node\Expr\New_) {
if ($expr->class instanceof Node\Expr) {
return new Types\Mixed;
}
if ($expr->class instanceof Node\Stmt\Class_) {
// Anonymous class
return new Types\Object;
}
if ((string)$expr->class === 'self') {
return new Types\Object_;
}
return new Types\Object_(new Fqsen('\\' . (string)$expr->class));
}
if ($expr instanceof Node\Expr\Clone_ || $expr instanceof Node\Expr\Assign) {
return $this->resolveExpression($expr->expr);
}
if ($expr instanceof Node\Expr\Ternary) {
// ?:
if ($expr->if === null) {
return new Types\Compound([
$this->resolveExpression($expr->cond),
$this->resolveExpression($expr->else)
]);
}
// Ternary is a compound of the two possible values
return new Types\Compound([
$this->resolveExpression($expr->if),
$this->resolveExpression($expr->else)
]);
}
if ($expr instanceof Node\Expr\BinaryOp\Coalesce) {
// ?? operator
return new Types\Compound([
$this->resolveExpression($expr->left),
$this->resolveExpression($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->resolveExpression($item->value);
$keyTypes[] = $item->key ? $this->resolveExpression($item->key) : new Types\Integer;
}
$valueTypes = array_unique($keyTypes);
$keyTypes = array_unique($keyTypes);
$valueType = count($valueTypes) > 1 ? new Types\Compound($valueTypes) : $valueTypes[0];
$keyType = count($keyTypes) > 1 ? new Types\Compound($keyTypes) : $keyTypes[0];
return new Types\Array_($valueTypes, $keyTypes);
}
if ($expr instanceof Node\Expr\ArrayDimFetch) {
$varType = $this->resolveExpression($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;
}
}

View File

@ -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;
}
}
}

View File

@ -5,7 +5,7 @@ namespace LanguageServer\NodeVisitor;
use PhpParser\{NodeVisitorAbstract, Node};
use LanguageServer\Definition;
use function LanguageServer\Fqn\getDefinedFqn;
use LanguageServer\Protocol\SymbolInformation;
/**
* Collects definitions of classes, interfaces, traits, methods, properties and constants
@ -29,11 +29,16 @@ class DefinitionCollector extends NodeVisitorAbstract
public function enterNode(Node $node)
{
$fqn = getDefinedFqn($node);
$fqn = Definition::getDefinedFqn($node);
// Only index definitions with an FQN (no variables)
if ($fqn === null) {
return;
}
$this->nodes[$fqn] = $node;
$this->definitions[$fqn] = Definition::fromNode($node);
$def = new Definition;
$def->fqn = $fqn;
$def->symbolInformation = SymbolInformation::fromNode($node, $fqn);
$def->type = Definition::getTypeFromNode($node);
$this->definitions[$fqn] = $def;
}
}

View File

@ -3,8 +3,8 @@ declare(strict_types = 1);
namespace LanguageServer\NodeVisitor;
use function LanguageServer\Fqn\getReferencedFqn;
use PhpParser\{NodeVisitorAbstract, Node};
use LanguageServer\DefinitionResolver;
/**
* Collects references to classes, interfaces, traits, methods, properties and constants
@ -19,10 +19,18 @@ class ReferencesCollector extends NodeVisitorAbstract
*/
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)
{
// Check if the node references any global symbol
$fqn = getReferencedFqn($node);
$fqn = $this->definitionResolver->resolveReferenceNodeToFqn($node);
if ($fqn) {
$this->addReference($fqn, $node);
// Namespaced constant access and function calls also need to register a reference

View File

@ -16,7 +16,6 @@ use LanguageServer\NodeVisitor\{
use PhpParser\{Error, ErrorHandler, Node, NodeTraverser};
use PhpParser\NodeVisitor\NameResolver;
use phpDocumentor\Reflection\DocBlockFactory;
use function LanguageServer\Fqn\{getDefinedFqn, getVariableDefinition, getReferencedFqn};
use Sabre\Event\Promise;
use function Sabre\Event\coroutine;
use Sabre\Uri;
@ -53,6 +52,13 @@ class PhpDocument
*/
private $docBlockFactory;
/**
* The DefinitionResolver instance to resolve reference nodes to definitions
*
* @var DefinitionResolver
*/
private $definitionResolver;
/**
* The URI of the document
*
@ -103,13 +109,21 @@ class PhpDocument
* @param Parser $parser The PHPParser instance
* @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->project = $project;
$this->client = $client;
$this->parser = $parser;
$this->docBlockFactory = $docBlockFactory;
$this->definitionResolver = $definitionResolver;
$this->updateContent($content);
}
@ -119,7 +133,7 @@ class PhpDocument
* @param string $fqn The fully qualified name of the symbol
* @return Node[]
*/
public function getReferencesByFqn(string $fqn)
public function getReferenceNodesByFqn(string $fqn)
{
return isset($this->referenceNodes) && isset($this->referenceNodes[$fqn]) ? $this->referenceNodes[$fqn] : null;
}
@ -176,7 +190,7 @@ class PhpDocument
$traverser->addVisitor($definitionCollector);
// Collect all references
$referencesCollector = new ReferencesCollector;
$referencesCollector = new ReferencesCollector($this->definitionResolver);
$traverser->addVisitor($referencesCollector);
$traverser->traverse($stmts);
@ -325,43 +339,6 @@ class PhpDocument
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 getDefinitionNodeByNode(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->getDefinitionNodeByFqn($fqn);
});
}
/**
* Returns the reference nodes for any node
* The references node MAY be in other documents, check the ownerDocument attribute
@ -374,7 +351,11 @@ class PhpDocument
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 || $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) {
return null;
}
@ -393,14 +374,14 @@ class PhpDocument
return $refCollector->nodes;
}
// Definition with a global FQN
$fqn = getDefinedFqn($node);
$fqn = Definition::getDefinedFqn($node);
if ($fqn === null) {
return [];
}
$refDocuments = yield $this->project->getReferenceDocuments($fqn);
$nodes = [];
foreach ($refDocuments as $document) {
$refs = $document->getReferencesByFqn($fqn);
$refs = $document->getReferenceNodesByFqn($fqn);
if ($refs !== null) {
foreach ($refs as $ref) {
$nodes[] = $ref;

View File

@ -46,6 +46,13 @@ class Project
*/
private $docBlockFactory;
/**
* The DefinitionResolver instance to resolve reference nodes to Definitions
*
* @var DefinitionResolver
*/
private $definitionResolver;
/**
* Reference to the language server client interface
*
@ -66,6 +73,7 @@ class Project
$this->clientCapabilities = $clientCapabilities;
$this->parser = new Parser;
$this->docBlockFactory = DocBlockFactory::createInstance();
$this->definitionResolver = new DefinitionResolver($this);
}
/**
@ -122,7 +130,15 @@ class Project
$document = $this->documents[$uri];
$document->updateContent($content);
} 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;
});
@ -141,7 +157,15 @@ class Project
$document = $this->documents[$uri];
$document->updateContent($content);
} 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;
}
return $document;
@ -180,6 +204,16 @@ class Project
return $this->definitions;
}
/**
* Returns the Definition object by a specific FQN
*
* @return Definition|null
*/
public function getDefinition(string $fqn)
{
return $this->definitions[$fqn] ?? null;
}
/**
* Registers a definition
*
@ -193,9 +227,9 @@ class Project
}
/**
* Sets the SymbolInformation index
* Sets the Definition index
*
* @param Definition[] $symbols
* @param Definition[] $definitions Map from FQN to Definition
* @return void
*/
public function setDefinitions(array $definitions)

View File

@ -44,27 +44,48 @@ class SymbolInformation
*
* @param Node $node
* @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)
{
$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->kind = $nodeSymbolKindMap[$class];
if ($node instanceof Node\Stmt\Class_) {
$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);
if ($fqn !== null) {
$parts = preg_split('/(::|\\\\)/', $fqn);

View File

@ -3,7 +3,7 @@ declare(strict_types = 1);
namespace LanguageServer\Server;
use LanguageServer\{LanguageClient, Project, PhpDocument};
use LanguageServer\{LanguageClient, Project, PhpDocument, DefinitionResolver};
use PhpParser\PrettyPrinter\Standard as PrettyPrinter;
use PhpParser\Node;
use LanguageServer\Protocol\{
@ -45,11 +45,17 @@ class TextDocument
*/
private $prettyPrinter;
/**
* @var DefinitionResolver
*/
private $definitionResolver;
public function __construct(Project $project, LanguageClient $client)
{
$this->project = $project;
$this->client = $client;
$this->prettyPrinter = new PrettyPrinter();
$this->definitionResolver = new DefinitionResolver($project);
}
/**
@ -165,11 +171,11 @@ class TextDocument
if ($node === null) {
return [];
}
$def = yield $document->getDefinitionNodeByNode($node);
if ($def === null) {
$def = $this->definitionResolver->resolveReferenceNodeToDefinition($node);
if ($def === null || $def->symbolInformation === null) {
return [];
}
return Location::fromNode($def);
return $def->symbolInformation->location;
});
}
@ -190,23 +196,34 @@ class TextDocument
return new Hover([]);
}
$range = Range::fromNode($node);
// Get the definition node for whatever node is under the cursor
$def = yield $document->getDefinitionNodeByNode($node);
if ($node instanceof Node\Expr\Variable) {
$defNode = DefinitionResolver::resolveVariableToNode($node);
} else {
// Get the definition for whatever node is under the cursor
$def = $this->definitionResolver->resolveReferenceNodeToDefinition($node);
if ($def === null) {
return new Hover([], $range);
}
// TODO inefficient. Add documentation and declaration line to Definition class
// so document doesnt have to be loaded
$document = yield $this->project->getOrLoadDocument($def->symbolInformation->location->uri);
if ($document === null) {
return new Hover([], $range);
}
$defNode = $document->getDefinitionNodeByFqn($def->fqn);
}
$contents = [];
// Build a declaration string
if ($def instanceof Node\Stmt\PropertyProperty || $def instanceof Node\Const_) {
if ($defNode instanceof Node\Stmt\PropertyProperty || $defNode 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;
$child = $defNode;
$defNode = $defNode->getAttribute('parentNode');
$defLine = clone $defNode;
$defLine->props = [$child];
} else {
$defLine = clone $def;
$defLine = clone $defNode;
}
// Don't include the docblock in the declaration string
$defLine->setAttribute('comments', []);
@ -220,20 +237,20 @@ class TextDocument
}
// Get the documentation string
if ($def instanceof Node\Param) {
$fn = $def->getAttribute('parentNode');
if ($defNode instanceof Node\Param) {
$fn = $defNode->getAttribute('parentNode');
$docBlock = $fn->getAttribute('docBlock');
if ($docBlock !== null) {
$tags = $docBlock->getTagsByName('param');
foreach ($tags as $tag) {
if ($tag->getVariableName() === $def->name) {
if ($tag->getVariableName() === $defNode->name) {
$contents[] = $tag->getDescription()->render();
break;
}
}
}
} else {
$docBlock = $def->getAttribute('docBlock');
$docBlock = $defNode->getAttribute('docBlock');
if ($docBlock !== null) {
$contents[] = $docBlock->getSummary();
}

View File

@ -5,6 +5,7 @@ namespace LanguageServer;
use Throwable;
use InvalidArgumentException;
use PhpParser\Node;
use Sabre\Event\{Loop, Promise};
/**
@ -77,3 +78,20 @@ function timeout($seconds = 0): Promise
Loop\setTimeout([$promise, 'fulfill'], $seconds);
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;
}
}
}