From 33211c68caff77d969200501437f8ef85a06c83a Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Fri, 18 Nov 2016 15:22:24 +0100 Subject: [PATCH] Resolve expressions recursively (#155) * Add Definition class * Add recursive DefinitionResolver * Cache hover --- composer.json | 3 +- fixtures/global_references.php | 4 + fixtures/global_symbols.php | 2 +- fixtures/references.php | 4 + fixtures/symbols.php | 2 +- src/Definition.php | 62 ++ src/DefinitionResolver.php | 688 ++++++++++++++++++ src/Fqn.php | 248 ------- src/NodeVisitor/DefinitionCollector.php | 34 +- src/NodeVisitor/ReferencesCollector.php | 20 +- .../VariableReferencesCollector.php | 4 +- src/PhpDocument.php | 129 ++-- src/Project.php | 88 ++- src/Protocol/SymbolInformation.php | 55 +- src/Server/TextDocument.php | 72 +- src/Server/Workspace.php | 9 +- src/utils.php | 18 + tests/NodeVisitor/DefinitionCollectorTest.php | 36 +- tests/Server/ServerTestCase.php | 24 +- .../TextDocument/Definition/GlobalTest.php | 24 + 20 files changed, 1061 insertions(+), 465 deletions(-) create mode 100644 src/Definition.php create mode 100644 src/DefinitionResolver.php delete mode 100644 src/Fqn.php diff --git a/composer.json b/composer.json index 9c78d3e5..df9110aa 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/fixtures/global_references.php b/fixtures/global_references.php index 8346923c..0ac117b5 100644 --- a/fixtures/global_references.php +++ b/fixtures/global_references.php @@ -34,3 +34,7 @@ function whatever(TestClass $param): TestClass { if ($abc instanceof TestInterface) { } + +// Nested expression +$obj->testProperty->testMethod(); +TestClass::$staticTestProperty[123]->testProperty; diff --git a/fixtures/global_symbols.php b/fixtures/global_symbols.php index 6eba8bbb..f5c755bc 100644 --- a/fixtures/global_symbols.php +++ b/fixtures/global_symbols.php @@ -30,7 +30,7 @@ class TestClass implements TestInterface /** * Lorem excepteur officia sit anim velit veniam enim. * - * @var TestClass + * @var TestClass[] */ public static $staticTestProperty; diff --git a/fixtures/references.php b/fixtures/references.php index ca274438..2ac1a634 100644 --- a/fixtures/references.php +++ b/fixtures/references.php @@ -34,3 +34,7 @@ function whatever(TestClass $param): TestClass { if ($abc instanceof TestInterface) { } + +// Nested expressions +$obj->testProperty->testMethod(); +TestClass::$staticTestProperty[123]->testProperty; diff --git a/fixtures/symbols.php b/fixtures/symbols.php index 89111015..4b0a6d94 100644 --- a/fixtures/symbols.php +++ b/fixtures/symbols.php @@ -30,7 +30,7 @@ class TestClass implements TestInterface /** * Lorem excepteur officia sit anim velit veniam enim. * - * @var TestClass + * @var TestClass[] */ public static $staticTestProperty; diff --git a/src/Definition.php b/src/Definition.php new file mode 100644 index 00000000..cba69ab9 --- /dev/null +++ b/src/Definition.php @@ -0,0 +1,62 @@ +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; + } + } + } +} diff --git a/src/Fqn.php b/src/Fqn.php deleted file mode 100644 index f5ef00db..00000000 --- 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 a723bdcc..162f6708 100644 --- a/src/NodeVisitor/DefinitionCollector.php +++ b/src/NodeVisitor/DefinitionCollector.php @@ -4,8 +4,8 @@ namespace LanguageServer\NodeVisitor; use PhpParser\{NodeVisitorAbstract, Node}; +use LanguageServer\{Definition, DefinitionResolver}; use LanguageServer\Protocol\SymbolInformation; -use function LanguageServer\Fqn\getDefinedFqn; /** * Collects definitions of classes, interfaces, traits, methods, properties and constants @@ -14,27 +14,41 @@ 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 = []; /** - * 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) { - $fqn = getDefinedFqn($node); + $fqn = DefinitionResolver::getDefinedFqn($node); + // Only index definitions with an FQN (no variables) if ($fqn === null) { return; } - $this->definitions[$fqn] = $node; - $symbol = SymbolInformation::fromNode($node, $fqn); - $this->symbols[$fqn] = $symbol; + $this->nodes[$fqn] = $node; + $def = new Definition; + $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; } } diff --git a/src/NodeVisitor/ReferencesCollector.php b/src/NodeVisitor/ReferencesCollector.php index 08b660df..7e35beb9 100644 --- a/src/NodeVisitor/ReferencesCollector.php +++ b/src/NodeVisitor/ReferencesCollector.php @@ -3,8 +3,8 @@ 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 @@ -17,12 +17,20 @@ class ReferencesCollector extends NodeVisitorAbstract * * @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) { // 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 @@ -41,9 +49,9 @@ public function enterNode(Node $node) private function addReference(string $fqn, Node $node) { - if (!isset($this->references[$fqn])) { - $this->references[$fqn] = []; + if (!isset($this->nodes[$fqn])) { + $this->nodes[$fqn] = []; } - $this->references[$fqn][] = $node; + $this->nodes[$fqn][] = $node; } } diff --git a/src/NodeVisitor/VariableReferencesCollector.php b/src/NodeVisitor/VariableReferencesCollector.php index a113a7e4..bb44bdc8 100644 --- a/src/NodeVisitor/VariableReferencesCollector.php +++ b/src/NodeVisitor/VariableReferencesCollector.php @@ -15,7 +15,7 @@ class VariableReferencesCollector extends NodeVisitorAbstract * * @var Node\Expr\Variable[] */ - public $references = []; + public $nodes = []; /** * @var string @@ -33,7 +33,7 @@ public function __construct(string $name) public function enterNode(Node $node) { if ($node instanceof Node\Expr\Variable && $node->name === $this->name) { - $this->references[] = $node; + $this->nodes[] = $node; } else if ($node instanceof Node\FunctionLike) { // 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 diff --git a/src/PhpDocument.php b/src/PhpDocument.php index 214f97b7..e24b9acb 100644 --- a/src/PhpDocument.php +++ b/src/PhpDocument.php @@ -16,7 +16,6 @@ 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 * @@ -75,25 +81,25 @@ class PhpDocument private $stmts; /** - * Map from fully qualified name (FQN) to Node + * Map from fully qualified name (FQN) to Definition * - * @var Node[] + * @var Definition[] */ private $definitions; /** - * Map from fully qualified name (FQN) to array of nodes that reference the symbol + * Map from fully qualified name (FQN) to Node * - * @var Node[][] + * @var Node[] */ - private $references; + private $definitionNodes; /** - * Map from fully qualified name (FQN) to SymbolInformation + * Map from fully qualified name (FQN) to array of nodes that reference the symbol * - * @var SymbolInformation[] + * @var Node[][] */ - private $symbols; + private $referenceNodes; /** * @param string $uri 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,9 +133,9 @@ public function __construct(string $uri, string $content, Project $project, Lang * @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->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 @@ public function updateContent(string $content) $traverser = new NodeTraverser; // Collect all definitions - $definitionCollector = new DefinitionCollector; + $definitionCollector = new DefinitionCollector($this->definitionResolver); $traverser->addVisitor($definitionCollector); // Collect all references - $referencesCollector = new ReferencesCollector; + $referencesCollector = new ReferencesCollector($this->definitionResolver); $traverser->addVisitor($referencesCollector); $traverser->traverse($stmts); // Unregister old definitions if (isset($this->definitions)) { - foreach ($this->definitions as $fqn => $node) { - $this->project->removeSymbol($fqn); + foreach ($this->definitions as $fqn => $definition) { + $this->project->removeDefinition($fqn); } } // Register this document on the project for all the symbols defined in it $this->definitions = $definitionCollector->definitions; - $this->symbols = $definitionCollector->symbols; - foreach ($definitionCollector->symbols as $fqn => $symbol) { - $this->project->setSymbol($fqn, $symbol); + $this->definitionNodes = $definitionCollector->nodes; + foreach ($definitionCollector->definitions as $fqn => $definition) { + $this->project->setDefinition($fqn, $definition); } // Unregister old references - if (isset($this->references)) { - foreach ($this->references as $fqn => $node) { + if (isset($this->referenceNodes)) { + foreach ($this->referenceNodes as $fqn => $node) { $this->project->removeReferenceUri($fqn, $this->uri); } } // Register this document on the project for references - $this->references = $referencesCollector->references; - foreach ($referencesCollector->references as $fqn => $nodes) { + $this->referenceNodes = $referencesCollector->nodes; + foreach ($referencesCollector->nodes as $fqn => $nodes) { $this->project->addReferenceUri($fqn, $this->uri); } @@ -289,9 +303,9 @@ public function getNodeAtPosition(Position $position) * @param string $fqn * @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 @@ public function getDefinitionByFqn(string $fqn) * * @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 @@ public function isDefined(string $fqn): bool 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 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 * The references node MAY be in other documents, check the ownerDocument attribute @@ -369,12 +346,16 @@ public function getDefinitionByNode(Node $node): Promise * @param Node $node * @return Promise */ - public function getReferencesByNode(Node $node): Promise + public function getReferenceNodesByNode(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 || $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; } @@ -390,17 +371,17 @@ public function getReferencesByNode(Node $node): Promise $refCollector = new VariableReferencesCollector($node->name); $traverser->addVisitor($refCollector); $traverser->traverse($n->getStmts()); - return $refCollector->references; + return $refCollector->nodes; } // Definition with a global FQN - $fqn = getDefinedFqn($node); + $fqn = DefinitionResolver::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 49bc6462..3021a1c9 100644 --- a/src/Project.php +++ b/src/Project.php @@ -19,11 +19,11 @@ class Project 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 @@ -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 @@ public function __construct(LanguageClient $client, ClientCapabilities $clientCa $this->clientCapabilities = $clientCapabilities; $this->parser = new Parser; $this->docBlockFactory = DocBlockFactory::createInstance(); + $this->definitionResolver = new DefinitionResolver($this); } /** @@ -122,7 +130,15 @@ public function loadDocument(string $uri): Promise $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 @@ public function openDocument(string $uri, string $content) $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; @@ -170,49 +194,67 @@ public function isDocumentOpen(string $uri): bool } /** - * Returns an associative array [string => string] that maps fully qualified symbol names - * to URIs of the document where the symbol is defined + * Returns an associative array [string => Definition] that maps fully qualified symbol names + * to Definitions + * + * @return Definitions[] + */ + public function getDefinitions() + { + return $this->definitions; + } + + /** + * Returns the Definition object by a specific FQN * - * @return SymbolInformation[] + * @param string $fqn + * @param bool $globalFallback Whether to fallback to global if the namespaced FQN was not found + * @return Definition|null */ - public function getSymbols() + public function getDefinition(string $fqn, $globalFallback = false) { - return $this->symbols; + if (isset($this->definitions[$fqn])) { + return $this->definitions[$fqn]; + } else if ($globalFallback) { + $parts = explode('\\', $fqn); + $fqn = end($parts); + return $this->getDefinition($fqn); + } } /** - * Adds a SymbolInformation for a specific symbol + * Registers a definition * * @param string $fqn The fully qualified name of the symbol - * @param string $uri The URI + * @param string $definition The Definition object * @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 */ - 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 * * @param string $fqn The fully qualified name of the symbol * @return void */ - public function removeSymbol(string $fqn) + public function removeDefinition(string $fqn) { - unset($this->symbols[$fqn]); + unset($this->definitions[$fqn]); unset($this->references[$fqn]); } @@ -296,10 +338,10 @@ public function setReferenceUris(array $references) */ public function getDefinitionDocument(string $fqn): Promise { - if (!isset($this->symbols[$fqn])) { + if (!isset($this->definitions[$fqn])) { return Promise\resolve(null); } - return $this->getOrLoadDocument($this->symbols[$fqn]->location->uri); + return $this->getOrLoadDocument($this->definitions[$fqn]->symbolInformation->location->uri); } /** diff --git a/src/Protocol/SymbolInformation.php b/src/Protocol/SymbolInformation.php index 19ca6a68..1111dc08 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 1fc8f279..6c673888 100644 --- a/src/Server/TextDocument.php +++ b/src/Server/TextDocument.php @@ -3,7 +3,7 @@ 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); } /** @@ -62,7 +68,11 @@ public function __construct(Project $project, LanguageClient $client) public function documentSymbol(TextDocumentIdentifier $textDocument): Promise { 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 @@ public function references( if ($node === null) { return []; } - $refs = yield $document->getReferencesByNode($node); + $refs = yield $document->getReferenceNodesByNode($node); $locations = []; foreach ($refs as $ref) { $locations[] = Location::fromNode($ref); @@ -161,11 +171,11 @@ public function definition(TextDocumentIdentifier $textDocument, Position $posit if ($node === null) { return []; } - $def = yield $document->getDefinitionByNode($node); - if ($def === null) { + $def = $this->definitionResolver->resolveReferenceNodeToDefinition($node); + if ($def === null || $def->symbolInformation === null) { return []; } - return Location::fromNode($def); + return $def->symbolInformation->location; }); } @@ -186,55 +196,17 @@ public function hover(TextDocumentIdentifier $textDocument, Position $position): return new Hover([]); } $range = Range::fromNode($node); - // Get the definition node for whatever node is under the cursor - $def = yield $document->getDefinitionByNode($node); + // Get the definition for whatever node is under the cursor + $def = $this->definitionResolver->resolveReferenceNodeToDefinition($node); if ($def === null) { return new Hover([], $range); } - $contents = []; - - // 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 - $defLine->setAttribute('comments', []); - if (isset($defLine->stmts)) { - $defLine->stmts = []; + if ($def->declarationLine) { + $contents[] = new MarkedString('php', "declarationLine); } - $defText = $this->prettyPrinter->prettyPrint([$defLine]); - $lines = explode("\n", $defText); - if (isset($lines[0])) { - $contents[] = new MarkedString('php', "documentation) { + $contents[] = $def->documentation; } - - // 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); }); } diff --git a/src/Server/Workspace.php b/src/Server/Workspace.php index e894c492..26feb72a 100644 --- a/src/Server/Workspace.php +++ b/src/Server/Workspace.php @@ -39,13 +39,10 @@ public function __construct(Project $project, LanguageClient $client) */ public function symbol(string $query): array { - if ($query === '') { - return array_values($this->project->getSymbols()); - } $symbols = []; - foreach ($this->project->getSymbols() as $fqn => $symbol) { - if (stripos($fqn, $query) !== false) { - $symbols[] = $symbol; + foreach ($this->project->getDefinitions() as $fqn => $definition) { + if ($query === '' || stripos($fqn, $query) !== false) { + $symbols[] = $definition->symbolInformation; } } return $symbols; diff --git a/src/utils.php b/src/utils.php index 061eff7a..859032d1 100644 --- a/src/utils.php +++ b/src/utils.php @@ -5,6 +5,7 @@ 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; + } + } +} diff --git a/tests/NodeVisitor/DefinitionCollectorTest.php b/tests/NodeVisitor/DefinitionCollectorTest.php index 6df59404..ded65d1c 100644 --- a/tests/NodeVisitor/DefinitionCollectorTest.php +++ b/tests/NodeVisitor/DefinitionCollectorTest.php @@ -6,7 +6,7 @@ use PHPUnit\Framework\TestCase; use PhpParser\{NodeTraverser, Node}; use PhpParser\NodeVisitor\NameResolver; -use LanguageServer\{LanguageClient, Project, PhpDocument, Parser}; +use LanguageServer\{LanguageClient, Project, PhpDocument, Parser, DefinitionResolver}; use LanguageServer\Protocol\ClientCapabilities; use LanguageServer\Tests\MockProtocolStream; use LanguageServer\NodeVisitor\{ReferencesAdder, DefinitionCollector}; @@ -24,11 +24,11 @@ public function testCollectsSymbols() $traverser = new NodeTraverser; $traverser->addVisitor(new NameResolver); $traverser->addVisitor(new ReferencesAdder($document)); - $definitionCollector = new DefinitionCollector; + $definitionCollector = new DefinitionCollector(new DefinitionResolver($project)); $traverser->addVisitor($definitionCollector); $stmts = $parser->parse(file_get_contents($uri)); $traverser->traverse($stmts); - $defs = $definitionCollector->definitions; + $defNodes = $definitionCollector->nodes; $this->assertEquals([ 'TestNamespace\\TEST_CONST', 'TestNamespace\\TestClass', @@ -40,17 +40,17 @@ public function testCollectsSymbols() 'TestNamespace\\TestTrait', 'TestNamespace\\TestInterface', 'TestNamespace\\test_function()' - ], array_keys($defs)); - $this->assertInstanceOf(Node\Const_::class, $defs['TestNamespace\\TEST_CONST']); - $this->assertInstanceOf(Node\Stmt\Class_::class, $defs['TestNamespace\\TestClass']); - $this->assertInstanceOf(Node\Const_::class, $defs['TestNamespace\\TestClass::TEST_CLASS_CONST']); - $this->assertInstanceOf(Node\Stmt\PropertyProperty::class, $defs['TestNamespace\\TestClass::staticTestProperty']); - $this->assertInstanceOf(Node\Stmt\PropertyProperty::class, $defs['TestNamespace\\TestClass::testProperty']); - $this->assertInstanceOf(Node\Stmt\ClassMethod::class, $defs['TestNamespace\\TestClass::staticTestMethod()']); - $this->assertInstanceOf(Node\Stmt\ClassMethod::class, $defs['TestNamespace\\TestClass::testMethod()']); - $this->assertInstanceOf(Node\Stmt\Trait_::class, $defs['TestNamespace\\TestTrait']); - $this->assertInstanceOf(Node\Stmt\Interface_::class, $defs['TestNamespace\\TestInterface']); - $this->assertInstanceOf(Node\Stmt\Function_::class, $defs['TestNamespace\\test_function()']); + ], array_keys($defNodes)); + $this->assertInstanceOf(Node\Const_::class, $defNodes['TestNamespace\\TEST_CONST']); + $this->assertInstanceOf(Node\Stmt\Class_::class, $defNodes['TestNamespace\\TestClass']); + $this->assertInstanceOf(Node\Const_::class, $defNodes['TestNamespace\\TestClass::TEST_CLASS_CONST']); + $this->assertInstanceOf(Node\Stmt\PropertyProperty::class, $defNodes['TestNamespace\\TestClass::staticTestProperty']); + $this->assertInstanceOf(Node\Stmt\PropertyProperty::class, $defNodes['TestNamespace\\TestClass::testProperty']); + $this->assertInstanceOf(Node\Stmt\ClassMethod::class, $defNodes['TestNamespace\\TestClass::staticTestMethod()']); + $this->assertInstanceOf(Node\Stmt\ClassMethod::class, $defNodes['TestNamespace\\TestClass::testMethod()']); + $this->assertInstanceOf(Node\Stmt\Trait_::class, $defNodes['TestNamespace\\TestTrait']); + $this->assertInstanceOf(Node\Stmt\Interface_::class, $defNodes['TestNamespace\\TestInterface']); + $this->assertInstanceOf(Node\Stmt\Function_::class, $defNodes['TestNamespace\\test_function()']); } public function testDoesNotCollectReferences() @@ -63,12 +63,12 @@ public function testDoesNotCollectReferences() $traverser = new NodeTraverser; $traverser->addVisitor(new NameResolver); $traverser->addVisitor(new ReferencesAdder($document)); - $definitionCollector = new DefinitionCollector; + $definitionCollector = new DefinitionCollector(new DefinitionResolver($project)); $traverser->addVisitor($definitionCollector); $stmts = $parser->parse(file_get_contents($uri)); $traverser->traverse($stmts); - $defs = $definitionCollector->definitions; - $this->assertEquals(['TestNamespace\\whatever()'], array_keys($defs)); - $this->assertInstanceOf(Node\Stmt\Function_::class, $defs['TestNamespace\\whatever()']); + $defNodes = $definitionCollector->nodes; + $this->assertEquals(['TestNamespace\\whatever()'], array_keys($defNodes)); + $this->assertInstanceOf(Node\Stmt\Function_::class, $defNodes['TestNamespace\\whatever()']); } } diff --git a/tests/Server/ServerTestCase.php b/tests/Server/ServerTestCase.php index c5818a96..1a608ca4 100644 --- a/tests/Server/ServerTestCase.php +++ b/tests/Server/ServerTestCase.php @@ -105,7 +105,8 @@ public function setUp() 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) 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' => [ 0 => new Location($symbolsUri, new Range(new Position(20, 27), new Position(20, 40))), // class TestClass implements TestInterface @@ -118,16 +119,20 @@ public function setUp() ], 'TestNamespace\\TestClass::testProperty' => [ 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' => [ - 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()' => [ 0 => new Location($referencesUri, new Range(new Position( 7, 0), new Position( 7, 29))) ], '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()' => [ 0 => new Location($referencesUri, new Range(new Position(10, 0), new Position(10, 13))), @@ -146,6 +151,7 @@ public function setUp() 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) 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' => [ 0 => new Location($globalSymbolsUri, new Range(new Position(20, 27), new Position(20, 40))), // class TestClass implements TestInterface @@ -158,16 +164,20 @@ public function setUp() ], 'TestClass::testProperty' => [ 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' => [ - 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()' => [ 0 => new Location($globalReferencesUri, new Range(new Position( 7, 0), new Position( 7, 29))) ], '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()' => [ 0 => new Location($globalReferencesUri, new Range(new Position(10, 0), new Position(10, 13))), diff --git a/tests/Server/TextDocument/Definition/GlobalTest.php b/tests/Server/TextDocument/Definition/GlobalTest.php index bc5f6e6d..2b1e353d 100644 --- a/tests/Server/TextDocument/Definition/GlobalTest.php +++ b/tests/Server/TextDocument/Definition/GlobalTest.php @@ -292,4 +292,28 @@ public function testDefinitionForInstanceOf() )->wait(); $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); + } }