diff --git a/composer.json b/composer.json index 023b021..468c292 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,10 @@ "psr-4": { "LanguageServer\\": "src/" }, - "files" : ["src/utils.php"] + "files" : [ + "src/utils.php", + "src/Fqn.php" + ] }, "autoload-dev": { "psr-4": { diff --git a/src/Fqn.php b/src/Fqn.php new file mode 100644 index 0000000..25e59ae --- /dev/null +++ b/src/Fqn.php @@ -0,0 +1,245 @@ +getAttribute('parentNode'); + + if ( + $node instanceof Node\Name && ( + $parent instanceof Node\Stmt\ClassLike + || $parent instanceof Node\Param + || $parent instanceof Node\Stmt\Function_ + || $parent instanceof Node\Expr\StaticCall + || $parent instanceof Node\Expr\ClassConstFetch + || $parent instanceof Node\Expr\StaticPropertyFetch + ) + ) { + // For extends, implements, type hints and classes of classes of static calls use the name directly + $name = (string)$node; + // Only the name node should be considered a reference, not the UseUse node itself + } else if ($parent instanceof Node\Stmt\UseUse) { + $name = (string)$parent->name; + $grandParent = $parent->getAttribute('parentNode'); + if ($grandParent instanceof Node\Stmt\GroupUse) { + $name = $grandParent->prefix . '\\' . $name; + } + // Only the name node should be considered a reference, not the New_ node itself + } else if ($parent instanceof Node\Expr\New_) { + if (!($parent->class instanceof Node\Name)) { + // Cannot get definition of dynamic calls + return null; + } + $name = (string)$parent->class; + } else if ($node instanceof Node\Expr\MethodCall || $node instanceof Node\Expr\PropertyFetch) { + if ($node->name instanceof Node\Expr || !($node->var instanceof Node\Expr\Variable)) { + // Cannot get definition of dynamic calls + return null; + } + // Need to resolve variable to a class + if ($node->var->name === 'this') { + // $this resolved to the class it is contained in + $n = $node; + while ($n = $n->getAttribute('parentNode')) { + if ($n instanceof Node\Stmt\Class_) { + if ($n->isAnonymous()) { + return null; + } + $name = (string)$n->namespacedName; + break; + } + } + if (!isset($name)) { + return null; + } + } else { + // Other variables resolve to their definition + $varDef = getVariableDefinition($node->var); + if (!isset($varDef)) { + return null; + } + if ($varDef instanceof Node\Param) { + if (!isset($varDef->type)) { + // Cannot resolve to class without a type hint + // TODO: parse docblock + return null; + } + $name = (string)$varDef->type; + } else if ($varDef instanceof Node\Expr\Assign) { + if ($varDef->expr instanceof Node\Expr\New_) { + if (!($varDef->expr->class instanceof Node\Name)) { + // Cannot get definition of dynamic calls + return null; + } + $name = (string)$varDef->expr->class; + } else { + return null; + } + } else { + return null; + } + } + $name .= '::' . (string)$node->name; + } else if ($parent instanceof Node\Expr\FuncCall) { + if ($parent->name instanceof Node\Expr) { + return null; + } + $name = (string)($node->getAttribute('namespacedName') ?? $parent->name); + } else if ($parent instanceof Node\Expr\ConstFetch) { + $name = (string)($node->getAttribute('namespacedName') ?? $parent->name); + } else if ( + $node instanceof Node\Expr\ClassConstFetch + || $node instanceof Node\Expr\StaticPropertyFetch + || $node instanceof Node\Expr\StaticCall + ) { + if ($node->class instanceof Node\Expr || $node->name instanceof Node\Expr) { + // Cannot get definition of dynamic names + return null; + } + $className = (string)$node->class; + if ($className === 'self' || $className === 'static' || $className === 'parent') { + // self and static are resolved to the containing class + $n = $node; + while ($n = $n->getAttribute('parentNode')) { + if ($n instanceof Node\Stmt\Class_) { + if ($n->isAnonymous()) { + return null; + } + if ($className === 'parent') { + // parent is resolved to the parent class + if (!isset($n->extends)) { + return null; + } + $className = (string)$n->extends; + } else { + $className = (string)$n->namespacedName; + } + break; + } + } + } + $name = (string)$className . '::' . $node->name; + } else { + return null; + } + if ( + $node instanceof Node\Expr\MethodCall + || $node instanceof Node\Expr\StaticCall + || $parent instanceof Node\Expr\FuncCall + ) { + $name .= '()'; + } + if (!isset($name)) { + return null; + } + return $name; +} + +/** + * Returns the assignment or parameter node where a variable was defined + * + * @param Node\Expr\Variable $n The variable access + * @return Node\Expr\Assign|Node\Param|Node\Expr\ClosureUse|null + */ +function getVariableDefinition(Node\Expr\Variable $var) +{ + $n = $var; + // Traverse the AST up + do { + // If a function is met, check the parameters and use statements + if ($n instanceof Node\FunctionLike) { + foreach ($n->getParams() as $param) { + if ($param->name === $var->name) { + return $param; + } + } + // If it is a closure, also check use statements + if ($n instanceof Node\Expr\Closure) { + foreach ($n->uses as $use) { + if ($use->var === $var->name) { + return $use; + } + } + } + break; + } + // Check each previous sibling node for a variable assignment to that variable + while ($n->getAttribute('previousSibling') && $n = $n->getAttribute('previousSibling')) { + if ($n instanceof Node\Expr\Assign && $n->var instanceof Node\Expr\Variable && $n->var->name === $var->name) { + return $n; + } + } + } while (isset($n) && $n = $n->getAttribute('parentNode')); + // Return null if nothing was found + return null; +} + +/** + * Returns the fully qualified name (FQN) that is defined by a node + * + * @param Node $node + * @return string|null + */ +function getDefinedFqn(Node $node) +{ + // Anonymous classes don't count as a definition + if ($node instanceof Node\Stmt\ClassLike && isset($node->name)) { + // Class, interface or trait declaration + return (string)$node->namespacedName; + } else if ($node instanceof Node\Stmt\Function_) { + // Function: use functionName() as the name + return (string)$node->namespacedName . '()'; + } else if ($node instanceof Node\Stmt\ClassMethod) { + // Class method: use ClassName::methodName() as name + $class = $node->getAttribute('parentNode'); + if (!isset($class->name)) { + // Ignore anonymous classes + return null; + } + return (string)$class->namespacedName . '::' . (string)$node->name . '()'; + } else if ($node instanceof Node\Stmt\PropertyProperty) { + // Property: use ClassName::propertyName as name + $class = $node->getAttribute('parentNode')->getAttribute('parentNode'); + if (!isset($class->name)) { + // Ignore anonymous classes + return null; + } + return (string)$class->namespacedName . '::' . (string)$node->name; + } else if ($node instanceof Node\Const_) { + $parent = $node->getAttribute('parentNode'); + if ($parent instanceof Node\Stmt\Const_) { + // Basic constant: use CONSTANT_NAME as name + return (string)$node->namespacedName; + } + if ($parent instanceof Node\Stmt\ClassConst) { + // Class constant: use ClassName::CONSTANT_NAME as name + $class = $parent->getAttribute('parentNode'); + if (!isset($class->name) || $class->name instanceof Node\Expr) { + return null; + } + return (string)$class->namespacedName . '::' . $node->name; + } + } +} diff --git a/src/NodeVisitor/DefinitionCollector.php b/src/NodeVisitor/DefinitionCollector.php index 9032155..f902fbf 100644 --- a/src/NodeVisitor/DefinitionCollector.php +++ b/src/NodeVisitor/DefinitionCollector.php @@ -4,6 +4,7 @@ declare(strict_types = 1); namespace LanguageServer\NodeVisitor; use PhpParser\{NodeVisitorAbstract, Node}; +use function LanguageServer\Fqn\getDefinedFqn; /** * Collects definitions of classes, interfaces, traits, methods, properties and constants @@ -20,7 +21,7 @@ class DefinitionCollector extends NodeVisitorAbstract public function enterNode(Node $node) { - $fqn = $node->getAttribute('ownerDocument')->getDefinedFqn($node); + $fqn = getDefinedFqn($node); if ($fqn !== null) { $this->definitions[$fqn] = $node; } diff --git a/src/NodeVisitor/ReferencesCollector.php b/src/NodeVisitor/ReferencesCollector.php index 749f635..08b660d 100644 --- a/src/NodeVisitor/ReferencesCollector.php +++ b/src/NodeVisitor/ReferencesCollector.php @@ -3,6 +3,7 @@ declare(strict_types = 1); namespace LanguageServer\NodeVisitor; +use function LanguageServer\Fqn\getReferencedFqn; use PhpParser\{NodeVisitorAbstract, Node}; /** @@ -21,7 +22,7 @@ class ReferencesCollector extends NodeVisitorAbstract public function enterNode(Node $node) { // Check if the node references any global symbol - $fqn = $node->getAttribute('ownerDocument')->getReferencedFqn($node); + $fqn = getReferencedFqn($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 2246a40..0a97ca2 100644 --- a/src/PhpDocument.php +++ b/src/PhpDocument.php @@ -16,6 +16,7 @@ use LanguageServer\NodeVisitor\{ use PhpParser\{Error, Node, NodeTraverser, Parser}; use PhpParser\NodeVisitor\NameResolver; use phpDocumentor\Reflection\DocBlockFactory; +use function LanguageServer\Fqn\{getDefinedFqn, getVariableDefinition, getReferencedFqn}; class PhpDocument { @@ -297,202 +298,6 @@ class PhpDocument return isset($this->definitions[$fqn]); } - /** - * Returns the fully qualified name (FQN) that is defined by a node - * Examples of FQNs: - * - testFunction() - * - TestNamespace\TestClass - * - TestNamespace\TestClass::TEST_CONSTANT - * - TestNamespace\TestClass::staticTestProperty - * - TestNamespace\TestClass::testProperty - * - TestNamespace\TestClass::staticTestMethod() - * - TestNamespace\TestClass::testMethod() - * - * @param Node $node - * @return string|null - */ - public function getDefinedFqn(Node $node) - { - // Anonymous classes don't count as a definition - if ($node instanceof Node\Stmt\ClassLike && isset($node->name)) { - // Class, interface or trait declaration - return (string)$node->namespacedName; - } else if ($node instanceof Node\Stmt\Function_) { - // Function: use functionName() as the name - return (string)$node->namespacedName . '()'; - } else if ($node instanceof Node\Stmt\ClassMethod) { - // Class method: use ClassName::methodName() as name - $class = $node->getAttribute('parentNode'); - if (!isset($class->name)) { - // Ignore anonymous classes - return null; - } - return (string)$class->namespacedName . '::' . (string)$node->name . '()'; - } else if ($node instanceof Node\Stmt\PropertyProperty) { - // Property: use ClassName::propertyName as name - $class = $node->getAttribute('parentNode')->getAttribute('parentNode'); - if (!isset($class->name)) { - // Ignore anonymous classes - return null; - } - return (string)$class->namespacedName . '::' . (string)$node->name; - } else if ($node instanceof Node\Const_) { - $parent = $node->getAttribute('parentNode'); - if ($parent instanceof Node\Stmt\Const_) { - // Basic constant: use CONSTANT_NAME as name - return (string)$node->namespacedName; - } - if ($parent instanceof Node\Stmt\ClassConst) { - // Class constant: use ClassName::CONSTANT_NAME as name - $class = $parent->getAttribute('parentNode'); - if (!isset($class->name) || $class->name instanceof Node\Expr) { - return null; - } - return (string)$class->namespacedName . '::' . $node->name; - } - } - } - - /** - * Returns the FQN that is referenced by a node - * - * @param Node $node - * @return string|null - */ - public function getReferencedFqn(Node $node) - { - $parent = $node->getAttribute('parentNode'); - - if ( - $node instanceof Node\Name && ( - $parent instanceof Node\Stmt\ClassLike - || $parent instanceof Node\Param - || $parent instanceof Node\Stmt\Function_ - || $parent instanceof Node\Expr\StaticCall - || $parent instanceof Node\Expr\ClassConstFetch - || $parent instanceof Node\Expr\StaticPropertyFetch - ) - ) { - // For extends, implements, type hints and classes of classes of static calls use the name directly - $name = (string)$node; - // Only the name node should be considered a reference, not the UseUse node itself - } else if ($parent instanceof Node\Stmt\UseUse) { - $name = (string)$parent->name; - $grandParent = $parent->getAttribute('parentNode'); - if ($grandParent instanceof Node\Stmt\GroupUse) { - $name = $grandParent->prefix . '\\' . $name; - } - // Only the name node should be considered a reference, not the New_ node itself - } else if ($parent instanceof Node\Expr\New_) { - if (!($parent->class instanceof Node\Name)) { - // Cannot get definition of dynamic calls - return null; - } - $name = (string)$parent->class; - } else if ($node instanceof Node\Expr\MethodCall || $node instanceof Node\Expr\PropertyFetch) { - if ($node->name instanceof Node\Expr || !($node->var instanceof Node\Expr\Variable)) { - // Cannot get definition of dynamic calls - return null; - } - // Need to resolve variable to a class - if ($node->var->name === 'this') { - // $this resolved to the class it is contained in - $n = $node; - while ($n = $n->getAttribute('parentNode')) { - if ($n instanceof Node\Stmt\Class_) { - if ($n->isAnonymous()) { - return null; - } - $name = (string)$n->namespacedName; - break; - } - } - if (!isset($name)) { - return null; - } - } else { - // Other variables resolve to their definition - $varDef = $this->getVariableDefinition($node->var); - if (!isset($varDef)) { - return null; - } - if ($varDef instanceof Node\Param) { - if (!isset($varDef->type)) { - // Cannot resolve to class without a type hint - // TODO: parse docblock - return null; - } - $name = (string)$varDef->type; - } else if ($varDef instanceof Node\Expr\Assign) { - if ($varDef->expr instanceof Node\Expr\New_) { - if (!($varDef->expr->class instanceof Node\Name)) { - // Cannot get definition of dynamic calls - return null; - } - $name = (string)$varDef->expr->class; - } else { - return null; - } - } else { - return null; - } - } - $name .= '::' . (string)$node->name; - } else if ($parent instanceof Node\Expr\FuncCall) { - if ($parent->name instanceof Node\Expr) { - return null; - } - $name = (string)($node->getAttribute('namespacedName') ?? $parent->name); - } else if ($parent instanceof Node\Expr\ConstFetch) { - $name = (string)($node->getAttribute('namespacedName') ?? $parent->name); - } else if ( - $node instanceof Node\Expr\ClassConstFetch - || $node instanceof Node\Expr\StaticPropertyFetch - || $node instanceof Node\Expr\StaticCall - ) { - if ($node->class instanceof Node\Expr || $node->name instanceof Node\Expr) { - // Cannot get definition of dynamic names - return null; - } - $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 definition node for any node * The definition node MAY be in another document, check the ownerDocument attribute @@ -505,9 +310,9 @@ class PhpDocument // 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 $this->getVariableDefinition($node); + return getVariableDefinition($node); } - $fqn = $this->getReferencedFqn($node); + $fqn = getReferencedFqn($node); if (!isset($fqn)) { return null; } @@ -558,7 +363,7 @@ class PhpDocument return $refCollector->references; } // Definition with a global FQN - $fqn = $this->getDefinedFqn($node); + $fqn = getDefinedFqn($node); if ($fqn === null) { return []; } @@ -574,43 +379,4 @@ class PhpDocument } return $nodes; } - - /** - * Returns the assignment or parameter node where a variable was defined - * - * @param Node\Expr\Variable $n The variable access - * @return Node\Expr\Assign|Node\Param|Node\Expr\ClosureUse|null - */ - public function getVariableDefinition(Node\Expr\Variable $var) - { - $n = $var; - // Traverse the AST up - do { - // If a function is met, check the parameters and use statements - if ($n instanceof Node\FunctionLike) { - foreach ($n->getParams() as $param) { - if ($param->name === $var->name) { - return $param; - } - } - // If it is a closure, also check use statements - if ($n instanceof Node\Expr\Closure) { - foreach ($n->uses as $use) { - if ($use->var === $var->name) { - return $use; - } - } - } - break; - } - // Check each previous sibling node for a variable assignment to that variable - while ($n->getAttribute('previousSibling') && $n = $n->getAttribute('previousSibling')) { - if ($n instanceof Node\Expr\Assign && $n->var instanceof Node\Expr\Variable && $n->var->name === $var->name) { - return $n; - } - } - } while (isset($n) && $n = $n->getAttribute('parentNode')); - // Return null if nothing was found - return null; - } }