From 83fd96c52a235e409922a0c0a2e78a226154f885 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Thu, 17 Nov 2016 21:25:25 +0100 Subject: [PATCH] Add recursive DefinitionResolver --- composer.json | 3 +- src/Definition.php | 111 +++++- src/DefinitionResolver.php | 437 ++++++++++++++++++++++++ src/Fqn.php | 248 -------------- src/NodeVisitor/DefinitionCollector.php | 11 +- src/NodeVisitor/ReferencesCollector.php | 12 +- src/PhpDocument.php | 71 ++-- src/Project.php | 42 ++- src/Protocol/SymbolInformation.php | 55 ++- src/Server/TextDocument.php | 51 ++- src/utils.php | 18 + 11 files changed, 704 insertions(+), 355 deletions(-) create mode 100644 src/DefinitionResolver.php delete mode 100644 src/Fqn.php diff --git a/composer.json b/composer.json index 9c78d3e..df9110a 100644 --- a/composer.json +++ b/composer.json @@ -41,8 +41,7 @@ "LanguageServer\\": "src/" }, "files" : [ - "src/utils.php", - "src/Fqn.php" + "src/utils.php" ] }, "autoload-dev": { diff --git a/src/Definition.php b/src/Definition.php index 88aa8b2..ce8aca6 100644 --- a/src/Definition.php +++ b/src/Definition.php @@ -1,4 +1,5 @@ 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; + } + } + } } diff --git a/src/DefinitionResolver.php b/src/DefinitionResolver.php new file mode 100644 index 0000000..f191491 --- /dev/null +++ b/src/DefinitionResolver.php @@ -0,0 +1,437 @@ +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; + } +} diff --git a/src/Fqn.php b/src/Fqn.php deleted file mode 100644 index f5ef00d..0000000 --- a/src/Fqn.php +++ /dev/null @@ -1,248 +0,0 @@ -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; - } - } -} diff --git a/src/NodeVisitor/DefinitionCollector.php b/src/NodeVisitor/DefinitionCollector.php index 4766338..e0c3521 100644 --- a/src/NodeVisitor/DefinitionCollector.php +++ b/src/NodeVisitor/DefinitionCollector.php @@ -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; } } diff --git a/src/NodeVisitor/ReferencesCollector.php b/src/NodeVisitor/ReferencesCollector.php index 841fc7e..7e35beb 100644 --- a/src/NodeVisitor/ReferencesCollector.php +++ b/src/NodeVisitor/ReferencesCollector.php @@ -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 diff --git a/src/PhpDocument.php b/src/PhpDocument.php index b91d3e0..8a3fec0 100644 --- a/src/PhpDocument.php +++ b/src/PhpDocument.php @@ -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 - */ - 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; diff --git a/src/Project.php b/src/Project.php index 2fa74e4..e037266 100644 --- a/src/Project.php +++ b/src/Project.php @@ -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) diff --git a/src/Protocol/SymbolInformation.php b/src/Protocol/SymbolInformation.php index 19ca6a6..1111dc0 100644 --- a/src/Protocol/SymbolInformation.php +++ b/src/Protocol/SymbolInformation.php @@ -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]; - $symbol->name = (string)$node->name; + 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); diff --git a/src/Server/TextDocument.php b/src/Server/TextDocument.php index bb7667e..a928e8b 100644 --- a/src/Server/TextDocument.php +++ b/src/Server/TextDocument.php @@ -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 ($def === null) { - return new Hover([], $range); + 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(); } diff --git a/src/utils.php b/src/utils.php index 061eff7..859032d 100644 --- a/src/utils.php +++ b/src/utils.php @@ -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; + } + } +}