diff --git a/composer.json b/composer.json index a21535bb..acb4ae6c 100644 --- a/composer.json +++ b/composer.json @@ -44,7 +44,8 @@ "files" : [ "src/utils.php", "src/FqnUtilities.php", - "src/ParserHelpers.php" + "src/ParserHelpers.php", + "src/Scope/GetScopeAtNode.php" ] }, "autoload-dev": { diff --git a/src/CompletionProvider.php b/src/CompletionProvider.php index 8b5b5c00..8c39c776 100644 --- a/src/CompletionProvider.php +++ b/src/CompletionProvider.php @@ -14,6 +14,7 @@ CompletionContext, CompletionTriggerKind }; +use function LanguageServer\Scope\getScopeAtNode; use Microsoft\PhpParser; use Microsoft\PhpParser\Node; use Generator; @@ -193,15 +194,22 @@ public function provideCompletion(PhpDocument $doc, Position $pos, CompletionCon // // $| // $a| + // + // TODO: Superglobals - // Find variables, parameters and use statements in the scope $namePrefix = $node->getName() ?? ''; - foreach ($this->suggestVariablesAtNode($node, $namePrefix) as $var) { + $prefixLen = strlen($namePrefix); + $scope = getScopeAtNode($this->definitionResolver, $node); + $variables = $scope->variables; + foreach ($variables as $name => $var) { + if (substr($name, 0, $prefixLen) !== $namePrefix) { + continue; + } $item = new CompletionItem; $item->kind = CompletionItemKind::VARIABLE; - $item->label = '$' . $var->getName(); - $item->documentation = $this->definitionResolver->getDocumentationFromNode($var); - $item->detail = (string)$this->definitionResolver->getTypeFromNode($var); + $item->label = '$' . $name; + $item->documentation = $this->definitionResolver->getDocumentationFromNode($var->definitionNode); + $item->detail = (string)$var->type; $item->textEdit = new TextEdit( new Range($pos, $pos), stripStringOverlap($doc->getRange(new Range(new Position(0, 0), $pos)), $item->label) @@ -408,111 +416,4 @@ private function expandParentFqns(array $fqns) : Generator } } } - - /** - * Will walk the AST upwards until a function-like node is met - * and at each level walk all previous siblings and their children to search for definitions - * of that variable - * - * @param Node $node - * @param string $namePrefix Prefix to filter - * @return array - */ - private function suggestVariablesAtNode(Node $node, string $namePrefix = ''): array - { - $vars = []; - - // Find variables in the node itself - // When getting completion in the middle of a function, $node will be the function node - // so we need to search it - foreach ($this->findVariableDefinitionsInNode($node, $namePrefix) as $var) { - // Only use the first definition - if (!isset($vars[$var->name])) { - $vars[$var->name] = $var; - } - } - - // Walk the AST upwards until a scope boundary is met - $level = $node; - while ($level && !($level instanceof PhpParser\FunctionLike)) { - // Walk siblings before the node - $sibling = $level; - while ($sibling = $sibling->getPreviousSibling()) { - // Collect all variables inside the sibling node - foreach ($this->findVariableDefinitionsInNode($sibling, $namePrefix) as $var) { - $vars[$var->getName()] = $var; - } - } - $level = $level->parent; - } - - // If the traversal ended because a function was met, - // also add its parameters and closure uses to the result list - if ($level && $level instanceof PhpParser\FunctionLike && $level->parameters !== null) { - foreach ($level->parameters->getValues() as $param) { - $paramName = $param->getName(); - if (empty($namePrefix) || strpos($paramName, $namePrefix) !== false) { - $vars[$paramName] = $param; - } - } - - if ($level instanceof Node\Expression\AnonymousFunctionCreationExpression && $level->anonymousFunctionUseClause !== null && - $level->anonymousFunctionUseClause->useVariableNameList !== null) { - foreach ($level->anonymousFunctionUseClause->useVariableNameList->getValues() as $use) { - $useName = $use->getName(); - if (empty($namePrefix) || strpos($useName, $namePrefix) !== false) { - $vars[$useName] = $use; - } - } - } - } - - return array_values($vars); - } - - /** - * Searches the subnodes of a node for variable assignments - * - * @param Node $node - * @param string $namePrefix Prefix to filter - * @return Node\Expression\Variable[] - */ - private function findVariableDefinitionsInNode(Node $node, string $namePrefix = ''): array - { - $vars = []; - // If the child node is a variable assignment, save it - - $isAssignmentToVariable = function ($node) { - return $node instanceof Node\Expression\AssignmentExpression; - }; - - if ($this->isAssignmentToVariableWithPrefix($node, $namePrefix)) { - $vars[] = $node->leftOperand; - } elseif ($node instanceof Node\ForeachKey || $node instanceof Node\ForeachValue) { - foreach ($node->getDescendantNodes() as $descendantNode) { - if ($descendantNode instanceof Node\Expression\Variable - && ($namePrefix === '' || strpos($descendantNode->getName(), $namePrefix) !== false) - ) { - $vars[] = $descendantNode; - } - } - } else { - // Get all descendent variables, then filter to ones that start with $namePrefix. - // Avoiding closure usage in tight loop - foreach ($node->getDescendantNodes($isAssignmentToVariable) as $descendantNode) { - if ($this->isAssignmentToVariableWithPrefix($descendantNode, $namePrefix)) { - $vars[] = $descendantNode->leftOperand; - } - } - } - - return $vars; - } - - private function isAssignmentToVariableWithPrefix(Node $node, string $namePrefix): bool - { - return $node instanceof Node\Expression\AssignmentExpression - && $node->leftOperand instanceof Node\Expression\Variable - && ($namePrefix === '' || strpos($node->leftOperand->getName(), $namePrefix) !== false); - } } diff --git a/src/DefinitionResolver.php b/src/DefinitionResolver.php index 990c1965..d4b9ffd8 100644 --- a/src/DefinitionResolver.php +++ b/src/DefinitionResolver.php @@ -5,6 +5,8 @@ use LanguageServer\Index\ReadableIndex; use LanguageServer\Protocol\SymbolInformation; +use LanguageServer\Scope\Scope; +use function LanguageServer\Scope\getScopeAtNode; use Microsoft\PhpParser; use Microsoft\PhpParser\Node; use Microsoft\PhpParser\FunctionLike; @@ -175,10 +177,15 @@ private function getDocBlock(Node $node) * * @param Node $node * @param string $fqn + * @param Scope|null $scope Scope at the point of Node. If not provided, will be computed from $node. * @return Definition */ - public function createDefinitionFromNode(Node $node, string $fqn = null): Definition + public function createDefinitionFromNode(Node $node, string $fqn = null, Scope $scope = null): Definition { + if ($scope === null) { + $scope = getScopeAtNode($this, $node); + } + $def = new Definition; $def->fqn = $fqn; @@ -221,7 +228,7 @@ public function createDefinitionFromNode(Node $node, string $fqn = null): Defini if ($node instanceof Node\Statement\ClassDeclaration && // TODO - this should be better represented in the parser API $node->classBaseClause !== null && $node->classBaseClause->baseClass !== null) { - $def->extends = [(string)$node->classBaseClause->baseClass->getResolvedName()]; + $def->extends = [$scope->getResolvedName($node->classBaseClause->baseClass)]; } elseif ( $node instanceof Node\Statement\InterfaceDeclaration && // TODO - this should be better represented in the parser API @@ -229,20 +236,20 @@ public function createDefinitionFromNode(Node $node, string $fqn = null): Defini ) { $def->extends = []; foreach ($node->interfaceBaseClause->interfaceNameList->getValues() as $n) { - $def->extends[] = (string)$n->getResolvedName(); + $def->extends[] = $scope->getResolvedName($n); } } $def->symbolInformation = SymbolInformation::fromNode($node, $fqn); if ($def->symbolInformation !== null) { - $def->type = $this->getTypeFromNode($node); + $def->type = $this->getTypeFromNode($node, $scope); $def->declarationLine = $this->getDeclarationLineFromNode($node); $def->documentation = $this->getDocumentationFromNode($node); } if ($node instanceof FunctionLike) { - $def->signatureInformation = $this->signatureInformationFactory->create($node); + $def->signatureInformation = $this->signatureInformationFactory->create($node, $scope); } return $def; @@ -252,56 +259,62 @@ public function createDefinitionFromNode(Node $node, string $fqn = null): Defini * Given any node, returns the Definition object of the symbol that is referenced * * @param Node $node Any reference node + * @param Scope|null $scope Scope at the point of Node. If not provided, will be computed from $node. * @return Definition|null */ - public function resolveReferenceNodeToDefinition(Node $node) + public function resolveReferenceNodeToDefinition(Node $node, Scope $scope = null) { + if ($scope === null) { + $scope = getScopeAtNode($this, $node); + } + $parent = $node->parent; // Variables are not indexed globally, as they stay in the file scope anyway. // Ignore variable nodes that are part of ScopedPropertyAccessExpression, // as the scoped property access expression node is handled separately. if ($node instanceof Node\Expression\Variable && !($parent instanceof Node\Expression\ScopedPropertyAccessExpression)) { + $name = $node->getName(); // Resolve $this to the containing class definition. - if ($node->getName() === 'this' && $fqn = $this->getContainingClassFqn($node)) { + if ($name === 'this') { + if ($scope->currentSelf === null) { + return null; + } + $fqn = substr((string)$scope->currentSelf->type->getFqsen(), 1); return $this->index->getDefinition($fqn, false); } // Resolve the variable to a definition node (assignment, param or closure use) - $defNode = $this->resolveVariableToNode($node); - if ($defNode === null) { + if (!isset($scope->variables[$name])) { return null; } - return $this->createDefinitionFromNode($defNode); + return $this->createDefinitionFromNode($scope->variables[$name]->definitionNode, null, $scope); } // Other references are references to a global symbol that have an FQN // Find out the FQN - $fqn = $this->resolveReferenceNodeToFqn($node); - if (!$fqn) { - return null; - } + $fqn = $this->resolveReferenceNodeToFqn($node, $scope); if ($fqn === 'self' || $fqn === 'static') { // Resolve self and static keywords to the containing class // (This is not 100% correct for static but better than nothing) - $classNode = $node->getFirstAncestor(Node\Statement\ClassDeclaration::class); - if (!$classNode) { - return; - } - $fqn = (string)$classNode->getNamespacedName(); - if (!$fqn) { - return; + if ($scope->currentSelf === null) { + return null; } + $fqn = substr((string)$scope->currentSelf->type->getFqsen(), 1); } else if ($fqn === 'parent') { - // Resolve parent keyword to the base class FQN - $classNode = $node->getFirstAncestor(Node\Statement\ClassDeclaration::class); - if (!$classNode || !$classNode->classBaseClause || !$classNode->classBaseClause->baseClass) { - return; + if ($scope->currentSelf === null) { + return null; } - $fqn = (string)$classNode->classBaseClause->baseClass->getResolvedName(); - if (!$fqn) { - return; + // Resolve parent keyword to the base class FQN + $classNode = $scope->currentSelf->definitionNode; + if (!$classNode->classBaseClause || !$classNode->classBaseClause->baseClass) { + return null; } + $fqn = $scope->getResolvedName($classNode->classBaseClause->baseClass); + } + + if (!$fqn) { + return; } // If the node is a function or constant, it could be namespaced, but PHP falls back to global @@ -319,15 +332,19 @@ public function resolveReferenceNodeToDefinition(Node $node) * May also return "static", "self" or "parent" * * @param Node $node + * @param Scope|null $scope Scope at the point of Node. If not provided, will be computed from $node. * @return string|null */ - public function resolveReferenceNodeToFqn(Node $node) + public function resolveReferenceNodeToFqn(Node $node, Scope $scope = null) { + if ($scope === null) { + $scope = getScopeAtNode($this, $node); + } // TODO all name tokens should be a part of a node if ($node instanceof Node\QualifiedName) { - return $this->resolveQualifiedNameNodeToFqn($node); + return $this->resolveQualifiedNameNodeToFqn($node, $scope); } else if ($node instanceof Node\Expression\MemberAccessExpression) { - return $this->resolveMemberAccessExpressionNodeToFqn($node); + return $this->resolveMemberAccessExpressionNodeToFqn($node, $scope); } else if (ParserHelpers\isConstantFetch($node)) { return (string)($node->getNamespacedName()); } else if ( @@ -335,18 +352,18 @@ public function resolveReferenceNodeToFqn(Node $node) $node instanceof Node\Expression\ScopedPropertyAccessExpression && !($node->memberName instanceof Node\Expression\Variable) ) { - return $this->resolveScopedPropertyAccessExpressionNodeToFqn($node); + return $this->resolveScopedPropertyAccessExpressionNodeToFqn($node, $scope); } else if ( // A\B::$c - static property access expression $node->parent instanceof Node\Expression\ScopedPropertyAccessExpression ) { - return $this->resolveScopedPropertyAccessExpressionNodeToFqn($node->parent); + return $this->resolveScopedPropertyAccessExpressionNodeToFqn($node->parent, $scope); } return null; } - private function resolveQualifiedNameNodeToFqn(Node\QualifiedName $node) + private function resolveQualifiedNameNodeToFqn(Node\QualifiedName $node, Scope $scope) { $parent = $node->parent; @@ -385,7 +402,7 @@ private function resolveQualifiedNameNodeToFqn(Node\QualifiedName $node) } // For extends, implements, type hints and classes of classes of static calls use the name directly - $name = (string) ($node->getResolvedName() ?? $node->getNamespacedName()); + $name = $scope->getResolvedName($node) ?? (string)$node->getNamespacedName(); if ($node->parent instanceof Node\Expression\CallExpression) { $name .= '()'; @@ -393,14 +410,14 @@ private function resolveQualifiedNameNodeToFqn(Node\QualifiedName $node) return $name; } - private function resolveMemberAccessExpressionNodeToFqn(Node\Expression\MemberAccessExpression $access) + private function resolveMemberAccessExpressionNodeToFqn(Node\Expression\MemberAccessExpression $access, Scope $scope) { if ($access->memberName instanceof Node\Expression) { // Cannot get definition if right-hand side is expression return null; } // Get the type of the left-hand expression - $varType = $this->resolveExpressionNodeToType($access->dereferencableExpression); + $varType = $this->resolveExpressionNodeToType($access->dereferencableExpression, $scope); if ($varType instanceof Types\Compound) { // For compound types, use the first FQN we find @@ -423,14 +440,18 @@ private function resolveMemberAccessExpressionNodeToFqn(Node\Expression\MemberAc || $varType instanceof Types\Self_ ) { // $this/static/self is resolved to the containing class - $classFqn = self::getContainingClassFqn($access); + if ($scope->currentSelf === null) { + return null; + } + $classFqn = substr((string)$scope->currentSelf->type->getFqsen(), 1); } 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); } - $memberSuffix = '->' . (string)($access->memberName->getText() ?? $access->memberName->getText($access->getFileContents())); + $memberSuffix = '->' . (string)($access->memberName->getText() + ?? $access->memberName->getText($access->getFileContents())); if ($access->parent instanceof Node\Expression\CallExpression) { $memberSuffix .= '()'; } @@ -460,23 +481,25 @@ private function resolveMemberAccessExpressionNodeToFqn(Node\Expression\MemberAc return $classFqn . $memberSuffix; } - private function resolveScopedPropertyAccessExpressionNodeToFqn(Node\Expression\ScopedPropertyAccessExpression $scoped) - { + private function resolveScopedPropertyAccessExpressionNodeToFqn( + Node\Expression\ScopedPropertyAccessExpression $scoped, + Scope $scope + ) { if ($scoped->scopeResolutionQualifier instanceof Node\Expression\Variable) { - $varType = $this->getTypeFromNode($scoped->scopeResolutionQualifier); + $varType = $this->getTypeFromNode($scoped->scopeResolutionQualifier, $scope); if ($varType === null) { return null; } $className = substr((string)$varType->getFqsen(), 1); } elseif ($scoped->scopeResolutionQualifier instanceof Node\QualifiedName) { - $className = (string)$scoped->scopeResolutionQualifier->getResolvedName(); + $className = $scope->getResolvedName($scoped->scopeResolutionQualifier); } else { return null; } if ($className === 'self' || $className === 'static' || $className === 'parent') { // self and static are resolved to the containing class - $classNode = $scoped->getFirstAncestor(Node\Statement\ClassDeclaration::class); + $classNode = $scope->currentSelf->definitionNode ?? null; if ($classNode === null) { return null; } @@ -485,12 +508,12 @@ private function resolveScopedPropertyAccessExpressionNodeToFqn(Node\Expression\ if (!isset($classNode->extends)) { return null; } - $className = (string)$classNode->extends->getResolvedName(); + $className = $scope->getResolvedName($classNode->extends); } else { - $className = (string)$classNode->getNamespacedName(); + $className = substr((string)$scope->currentSelf->type->getFqsen(), 1); } } elseif ($scoped->scopeResolutionQualifier instanceof Node\QualifiedName) { - $className = $scoped->scopeResolutionQualifier->getResolvedName(); + $className = $scope->getResolvedName($scoped->scopeResolutionQualifier); } if ($scoped->memberName instanceof Node\Expression\Variable) { if ($scoped->parent instanceof Node\Expression\CallExpression) { @@ -510,146 +533,20 @@ private function resolveScopedPropertyAccessExpressionNodeToFqn(Node\Expression\ 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 = $node->getFirstAncestor(Node\Statement\ClassDeclaration::class); - if ($classNode === null) { - return null; - } - return (string)$classNode->getNamespacedName(); - } - - /** - * Returns the type 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 The node used to find the containing class - * - * @return Types\Object_|null - */ - private function getContainingClassType(Node $node) - { - $classFqn = $this->getContainingClassFqn($node); - return $classFqn ? new Types\Object_(new Fqsen('\\' . $classFqn)) : null; - } - - /** - * Returns the assignment or parameter node where a variable was defined - * - * @param Node\Expression\Variable|Node\Expression\ClosureUse $var The variable access - * @return Node\Expression\Assign|Node\Expression\AssignOp|Node\Param|Node\Expression\ClosureUse|null - */ - public function resolveVariableToNode($var) - { - $n = $var; - // When a use is passed, start outside the closure to not return immediately - // Use variable vs variable parsing? - if ($var instanceof Node\UseVariableName) { - $n = $var->getFirstAncestor(Node\Expression\AnonymousFunctionCreationExpression::class)->parent; - $name = $var->getName(); - } else if ($var instanceof Node\Expression\Variable || $var instanceof Node\Parameter) { - $name = $var->getName(); - } else { - throw new \InvalidArgumentException('$var must be Variable, Param or ClosureUse, not ' . get_class($var)); - } - if (empty($name)) { - return null; - } - - $shouldDescend = function ($nodeToDescand) { - // Make sure not to decend into functions or classes (they represent a scope boundary) - return !($nodeToDescand instanceof PhpParser\FunctionLike || $nodeToDescand instanceof PhpParser\ClassLike); - }; - - // Traverse the AST up - do { - // If a function is met, check the parameters and use statements - if ($n instanceof PhpParser\FunctionLike) { - if ($n->parameters !== null) { - foreach ($n->parameters->getElements() as $param) { - if ($param->getName() === $name) { - return $param; - } - } - } - // If it is a closure, also check use statements - if ($n instanceof Node\Expression\AnonymousFunctionCreationExpression && - $n->anonymousFunctionUseClause !== null && - $n->anonymousFunctionUseClause->useVariableNameList !== null) { - foreach ($n->anonymousFunctionUseClause->useVariableNameList->getElements() as $use) { - if ($use->getName() === $name) { - return $use; - } - } - } - break; - } - - // Check each previous sibling node and their descendents for a variable assignment to that variable - // Each previous sibling could contain a declaration of the variable - while (($prevSibling = $n->getPreviousSibling()) !== null && $n = $prevSibling) { - - // Check the sibling itself - if (self::isVariableDeclaration($n, $name)) { - return $n; - } - - // Check descendant of this sibling (e.g. the children of a previous if block) - foreach ($n->getDescendantNodes($shouldDescend) as $descendant) { - if (self::isVariableDeclaration($descendant, $name)) { - return $descendant; - } - } - } - } while (isset($n) && $n = $n->parent); - // Return null if nothing was found - return null; - } - - /** - * Checks whether the given Node declares the given variable name - * - * @param Node $n The Node to check - * @param string $name The name of the wanted variable - * @return bool - */ - private static function isVariableDeclaration(Node $n, string $name) - { - if ( - // TODO - clean this up - ($n instanceof Node\Expression\AssignmentExpression && $n->operator->kind === PhpParser\TokenKind::EqualsToken) - && $n->leftOperand instanceof Node\Expression\Variable && $n->leftOperand->getName() === $name - ) { - return true; - } - - if ( - ($n instanceof Node\ForeachValue || $n instanceof Node\ForeachKey) - && $n->expression instanceof Node\Expression\Variable - && $n->expression->getName() === $name - ) { - return true; - } - - return false; - } - /** * Given an expression node, resolves that expression recursively to a type. * If the type could not be resolved, returns Types\Mixed_. * - * @param Node\Expression $expr + * @param Node|Token $expr + * @param Scope|null $scope Scope at the point of Node. If not provided, will be computed from $node. * @return \phpDocumentor\Reflection\Type|null */ - public function resolveExpressionNodeToType($expr) + public function resolveExpressionNodeToType($expr, Scope $scope = null) { + if ($scope === null) { + $scope = getScopeAtNode($this, $expr); + } + // PARENTHESIZED EXPRESSION // Retrieve inner expression from parenthesized expression while ($expr instanceof Node\Expression\ParenthesizedExpression) { @@ -666,20 +563,10 @@ public function resolveExpressionNodeToType($expr) // $this -> Type\this // $myVariable -> type of corresponding assignment expression if ($expr instanceof Node\Expression\Variable || $expr instanceof Node\UseVariableName) { - if ($expr->getName() === 'this') { - return new Types\Object_(new Fqsen('\\' . $this->getContainingClassFqn($expr))); - } - // Find variable definition (parameter or assignment expression) - $defNode = $this->resolveVariableToNode($expr); - if ($defNode instanceof Node\Expression\AssignmentExpression || $defNode instanceof Node\UseVariableName) { - return $this->resolveExpressionNodeToType($defNode); - } - if ($defNode instanceof Node\ForeachKey || $defNode instanceof Node\ForeachValue) { - return $this->getTypeFromNode($defNode); - } - if ($defNode instanceof Node\Parameter) { - return $this->getTypeFromNode($defNode); - } + $name = $expr->getName(); + return isset($scope->variables[$name]) + ? $scope->variables[$name]->type + : new Types\Mixed_; } // FUNCTION CALL @@ -691,18 +578,18 @@ public function resolveExpressionNodeToType($expr) ) { // Find the function definition if ($expr->callableExpression instanceof Node\Expression) { - // Cannot get type for dynamic function call return new Types\Mixed_; } if ($expr->callableExpression instanceof Node\QualifiedName) { - $fqn = $expr->callableExpression->getResolvedName() ?? $expr->callableExpression->getNamespacedName(); + $fqn = $scope->getResolvedName($expr->callableExpression) ?? $expr->callableExpression->getNamespacedName(); $fqn .= '()'; $def = $this->index->getDefinition($fqn, true); if ($def !== null) { return $def->type; } } + return new Types\Mixed_; } // TRUE / FALSE / NULL @@ -716,6 +603,7 @@ public function resolveExpressionNodeToType($expr) if ($token === PhpParser\TokenKind::NullReservedWord) { return new Types\Null_; } + return new Types\Mixed_; } // CONSTANT FETCH @@ -726,6 +614,7 @@ public function resolveExpressionNodeToType($expr) if ($def !== null) { return $def->type; } + return new Types\Mixed_; } // MEMBER CALL EXPRESSION/SCOPED PROPERTY CALL EXPRESSION @@ -735,7 +624,7 @@ public function resolveExpressionNodeToType($expr) $expr->callableExpression instanceof Node\Expression\MemberAccessExpression || $expr->callableExpression instanceof Node\Expression\ScopedPropertyAccessExpression) ) { - return $this->resolveExpressionNodeToType($expr->callableExpression); + return $this->resolveExpressionNodeToType($expr->callableExpression, $scope); } // MEMBER ACCESS EXPRESSION @@ -746,16 +635,19 @@ public function resolveExpressionNodeToType($expr) $var = $expr->dereferencableExpression; // Resolve object - $objType = $this->resolveExpressionNodeToType($var); + $objType = $this->resolveExpressionNodeToType($var, $scope); + if ($objType === null) { + return null; + } 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) { + if ($scope->currentSelf === null) { return new Types\Mixed_; } + $classFqn = substr((string)$scope->currentSelf->type->getFqsen(), 1); } else if (!($t instanceof Types\Object_) || $t->getFqsen() === null) { return new Types\Mixed_; } else { @@ -782,7 +674,7 @@ public function resolveExpressionNodeToType($expr) // SCOPED PROPERTY ACCESS EXPRESSION if ($expr instanceof Node\Expression\ScopedPropertyAccessExpression) { - $classType = $this->resolveClassNameToType($expr->scopeResolutionQualifier); + $classType = $this->resolveClassNameToType($expr->scopeResolutionQualifier, $scope); if (!($classType instanceof Types\Object_) || $classType->getFqsen() === null) { return new Types\Mixed_; } @@ -805,19 +697,19 @@ public function resolveExpressionNodeToType($expr) // new A() => resolves to the type of the class type designator (A) // TODO: new $this->a => resolves to the string represented by "a" if ($expr instanceof Node\Expression\ObjectCreationExpression) { - return $this->resolveClassNameToType($expr->classTypeDesignator); + return $this->resolveClassNameToType($expr->classTypeDesignator, $scope); } // CLONE EXPRESSION // clone($a) => resolves to the type of $a if ($expr instanceof Node\Expression\CloneExpression) { - return $this->resolveExpressionNodeToType($expr->expression); + return $this->resolveExpressionNodeToType($expr->expression, $scope); } // ASSIGNMENT EXPRESSION // $a = $myExpression => resolves to the type of the right-hand operand if ($expr instanceof Node\Expression\AssignmentExpression) { - return $this->resolveExpressionNodeToType($expr->rightOperand); + return $this->resolveExpressionNodeToType($expr->rightOperand, $scope); } // TERNARY EXPRESSION @@ -827,14 +719,14 @@ public function resolveExpressionNodeToType($expr) // ?: if ($expr->ifExpression === null) { return new Types\Compound([ - $this->resolveExpressionNodeToType($expr->condition), // TODO: why? - $this->resolveExpressionNodeToType($expr->elseExpression) + $this->resolveExpressionNodeToType($expr->condition, $scope), // TODO: why? + $this->resolveExpressionNodeToType($expr->elseExpression, $scope) ]); } // Ternary is a compound of the two possible values return new Types\Compound([ - $this->resolveExpressionNodeToType($expr->ifExpression), - $this->resolveExpressionNodeToType($expr->elseExpression) + $this->resolveExpressionNodeToType($expr->ifExpression, $scope), + $this->resolveExpressionNodeToType($expr->elseExpression, $scope) ]); } @@ -843,8 +735,8 @@ public function resolveExpressionNodeToType($expr) if ($expr instanceof Node\Expression\BinaryExpression && $expr->operator->kind === PhpParser\TokenKind::QuestionQuestionToken) { // ?? operator return new Types\Compound([ - $this->resolveExpressionNodeToType($expr->leftOperand), - $this->resolveExpressionNodeToType($expr->rightOperand) + $this->resolveExpressionNodeToType($expr->leftOperand, $scope), + $this->resolveExpressionNodeToType($expr->rightOperand, $scope) ]); } @@ -902,8 +794,8 @@ public function resolveExpressionNodeToType($expr) ) ) { if ( - $this->resolveExpressionNodeToType($expr->leftOperand) instanceof Types\Integer - && $this->resolveExpressionNodeToType($expr->rightOperand) instanceof Types\Integer + $this->resolveExpressionNodeToType($expr->leftOperand, $scope) instanceof Types\Integer + && $this->resolveExpressionNodeToType($expr->rightOperand, $scope) instanceof Types\Integer ) { return new Types\Integer; } @@ -954,8 +846,8 @@ public function resolveExpressionNodeToType($expr) $keyTypes = []; if ($expr->arrayElements !== null) { foreach ($expr->arrayElements->getElements() as $item) { - $valueTypes[] = $this->resolveExpressionNodeToType($item->elementValue); - $keyTypes[] = $item->elementKey ? $this->resolveExpressionNodeToType($item->elementKey) : new Types\Integer; + $valueTypes[] = $this->resolveExpressionNodeToType($item->elementValue, $scope); + $keyTypes[] = $item->elementKey ? $this->resolveExpressionNodeToType($item->elementKey, $scope) : new Types\Integer; } } $valueTypes = array_unique($valueTypes); @@ -981,7 +873,7 @@ public function resolveExpressionNodeToType($expr) // $myArray[3] // $myArray{"hello"} if ($expr instanceof Node\Expression\SubscriptExpression) { - $varType = $this->resolveExpressionNodeToType($expr->postfixExpression); + $varType = $this->resolveExpressionNodeToType($expr->postfixExpression, $scope); if (!($varType instanceof Types\Array_)) { return new Types\Mixed_; } @@ -996,7 +888,7 @@ public function resolveExpressionNodeToType($expr) } if ($expr instanceof Node\QualifiedName) { - return $this->resolveClassNameToType($expr); + return $this->resolveClassNameToType($expr, $scope); } return new Types\Mixed_; @@ -1010,9 +902,9 @@ public function resolveExpressionNodeToType($expr) * @param Node|PhpParser\Token $class * @return Type */ - public function resolveClassNameToType($class): Type + public function resolveClassNameToType($class, Scope $scope = null): Type { - if ($class instanceof Node\Expression) { + if ($class instanceof Node\Expression || $class instanceof PhpParser\MissingToken) { return new Types\Mixed_; } if ($class instanceof PhpParser\Token && $class->kind === PhpParser\TokenKind::ClassKeyword) { @@ -1023,23 +915,28 @@ public function resolveClassNameToType($class): Type // `new static` return new Types\Static_; } - $className = (string)$class->getResolvedName(); + if ($scope === null) { + $scope = getScopeAtNode($this, $class); + } + $className = $scope->getResolvedName($class); - if ($className === 'self' || $className === 'parent') { - $classNode = $class->getFirstAncestor(Node\Statement\ClassDeclaration::class); - if ($className === 'parent') { - if ($classNode === null || $classNode->classBaseClause === null) { - return new Types\Object_; - } - // parent is resolved to the parent class - $classFqn = (string)$classNode->classBaseClause->baseClass->getResolvedName(); - } else { - if ($classNode === null) { - return new Types\Self_; - } - // self is resolved to the containing class - $classFqn = (string)$classNode->getNamespacedName(); + if ($className === 'self') { + if ($scope->currentSelf === null) { + return new Types\Self_; } + return $scope->currentSelf->type; + } else if ($className === 'parent') { + if ($scope->currentSelf === null) { + return new Types\Object_; + } + $classNode = $scope->currentSelf->definitionNode; + if (empty($classNode->classBaseClause) + || !$classNode->classBaseClause->baseClass instanceof Node\QualifiedName + ) { + return new Types\Object_; + } + // parent is resolved to the parent class + $classFqn = $scope->getResolvedName($classNode->classBaseClause->baseClass); return new Types\Object_(new Fqsen('\\' . $classFqn)); } return new Types\Object_(new Fqsen('\\' . $className)); @@ -1057,14 +954,19 @@ public function resolveClassNameToType($class): Type * Returns null if the node does not have a type. * * @param Node $node + * @param Scope|null $scope Scope at the point of Node. If not provided, will be computed from $node. * @return \phpDocumentor\Reflection\Type|null */ - public function getTypeFromNode($node) + public function getTypeFromNode($node, Scope $scope = null) { + if ($scope === null) { + $scope = getScopeAtNode($this, $node); + } + if (ParserHelpers\isConstDefineExpression($node)) { // constants with define() like // define('TEST_DEFINE_CONSTANT', false); - return $this->resolveExpressionNodeToType($node->argumentExpressionList->children[2]->expression); + return $this->resolveExpressionNodeToType($node->argumentExpressionList->children[2]->expression, $scope); } // PARAMETERS @@ -1078,10 +980,12 @@ public function getTypeFromNode($node) // * @param MyClass $myParam // */ // function foo($a) - $functionLikeDeclaration = ParserHelpers\getFunctionLikeDeclarationFromParameter($node); $variableName = $node->getName(); + if (isset($scope->variables[$variableName])) { + return $scope->variables[$variableName]->type; + } + $functionLikeDeclaration = ParserHelpers\getFunctionLikeDeclarationFromParameter($node); $docBlock = $this->getDocBlock($functionLikeDeclaration); - $parameterDocBlockTag = $this->tryGetDocBlockTagForParameter($docBlock, $variableName); if ($parameterDocBlockTag !== null && ($type = $parameterDocBlockTag->getType())) { // Doc block comments supercede all other forms of type inference @@ -1095,12 +999,12 @@ public function getTypeFromNode($node) // Resolve a string like "bool" to a type object $type = $this->typeResolver->resolve($node->typeDeclaration->getText($node->getFileContents())); } else { - $type = new Types\Object_(new Fqsen('\\' . (string)$node->typeDeclaration->getResolvedName())); + $type = new Types\Object_(new Fqsen('\\' . $scope->getResolvedName($node->typeDeclaration))); } } // function foo($a = 3) if ($node->default !== null) { - $defaultType = $this->resolveExpressionNodeToType($node->default); + $defaultType = $this->resolveExpressionNodeToType($node->default, $scope); if (isset($type) && !is_a($type, get_class($defaultType))) { // TODO - verify it is worth creating a compound type return new Types\Compound([$type, $defaultType]); @@ -1121,15 +1025,11 @@ public function getTypeFromNode($node) if ( $docBlock !== null && !empty($returnTags = $docBlock->getTagsByName('return')) - && $returnTags[0]->getType() !== null + && ($returnType = $returnTags[0]->getType()) !== null ) { // Use @return tag - $returnType = $returnTags[0]->getType(); - if ($returnType instanceof Types\Self_) { - $selfType = $this->getContainingClassType($node); - if ($selfType) { - return $selfType; - } + if ($returnType instanceof Types\Self_ && null !== $scope->currentSelf) { + return $scope->currentSelf->type; } return $returnType; } @@ -1138,13 +1038,10 @@ public function getTypeFromNode($node) if ($node->returnType instanceof PhpParser\Token) { // Resolve a string like "bool" to a type object return $this->typeResolver->resolve($node->returnType->getText($node->getFileContents())); - } elseif ($node->returnType->getResolvedName() === 'self') { - $selfType = $this->getContainingClassType($node); - if ($selfType !== null) { - return $selfType; - } + } else if ($scope->currentSelf !== null && $scope->getResolvedName($node->returnType) === 'self') { + return $scope->currentSelf->type; } - return new Types\Object_(new Fqsen('\\' . (string)$node->returnType->getResolvedName())); + return new Types\Object_(new Fqsen('\\' . $scope->getResolvedName($node->returnType))); } // Unknown return type return new Types\Mixed_; @@ -1153,7 +1050,7 @@ public function getTypeFromNode($node) // FOREACH KEY/VARIABLE if ($node instanceof Node\ForeachKey || $node->parent instanceof Node\ForeachKey) { $foreach = $node->getFirstAncestor(Node\Statement\ForeachStatement::class); - $collectionType = $this->resolveExpressionNodeToType($foreach->forEachCollectionName); + $collectionType = $this->resolveExpressionNodeToType($foreach->forEachCollectionName, $scope); if ($collectionType instanceof Types\Array_) { return $collectionType->getKeyType(); } @@ -1165,7 +1062,7 @@ public function getTypeFromNode($node) || ($node instanceof Node\Expression\Variable && $node->parent instanceof Node\ForeachValue) ) { $foreach = $node->getFirstAncestor(Node\Statement\ForeachStatement::class); - $collectionType = $this->resolveExpressionNodeToType($foreach->forEachCollectionName); + $collectionType = $this->resolveExpressionNodeToType($foreach->forEachCollectionName, $scope); if ($collectionType instanceof Types\Array_) { return $collectionType->getValueType(); } @@ -1196,12 +1093,12 @@ public function getTypeFromNode($node) if ($declarationNode instanceof Node\PropertyDeclaration) { // TODO should have default if (isset($node->parent->rightOperand)) { - return $this->resolveExpressionNodeToType($node->parent->rightOperand); + return $this->resolveExpressionNodeToType($node->parent->rightOperand, $scope); } } else if ($node instanceof Node\ConstElement) { - return $this->resolveExpressionNodeToType($node->assignment); + return $this->resolveExpressionNodeToType($node->assignment, $scope); } else if ($node instanceof Node\Expression\AssignmentExpression) { - return $this->resolveExpressionNodeToType($node->rightOperand); + return $this->resolveExpressionNodeToType($node->rightOperand, $scope); } // TODO: read @property tags of class // TODO: Try to infer the type from default value / constant value @@ -1218,9 +1115,10 @@ public function getTypeFromNode($node) * Returns null if the node does not declare any symbol that can be referenced by an FQN * * @param Node $node + * @param Scope|null $scope Scope at the point of Node. If not provided, will be computed from $node. * @return string|null */ - public static function getDefinedFqn($node) + public function getDefinedFqn($node, Scope $scope = null) { $parent = $node->parent; // Anonymous classes don't count as a definition @@ -1251,6 +1149,10 @@ public static function getDefinedFqn($node) return $name === "" ? null : $name . '()'; } + if ($scope === null) { + $scope = getScopeAtNode($this, $node); + } + // INPUT OUTPUT // namespace A\B; // class C { @@ -1259,18 +1161,18 @@ public static function getDefinedFqn($node) // } if ($node instanceof Node\MethodDeclaration) { // Class method: use ClassName->methodName() as name - $class = $node->getFirstAncestor( - Node\Expression\ObjectCreationExpression::class, - PhpParser\ClassLike::class - ); - if (!isset($class->name)) { + if ($scope->currentSelf === null) { + return; + } + $className = substr((string)$scope->currentSelf->type->getFqsen(), 1); + if (!$className) { // Ignore anonymous classes return null; } if ($node->isStatic()) { - return (string)$class->getNamespacedName() . '::' . $node->getName() . '()'; + return $className . '::' . $node->getName() . '()'; } else { - return (string)$class->getNamespacedName() . '->' . $node->getName() . '()'; + return $className . '->' . $node->getName() . '()'; } } @@ -1282,20 +1184,18 @@ public static function getDefinedFqn($node) // } if ( ($propertyDeclaration = ParserHelpers\tryGetPropertyDeclaration($node)) !== null && - ($classDeclaration = - $node->getFirstAncestor( - Node\Expression\ObjectCreationExpression::class, - PhpParser\ClassLike::class - ) - ) !== null && isset($classDeclaration->name)) { + $scope->currentSelf !== null && + isset($scope->currentSelf->definitionNode->name) + ) { + $className = substr((string)$scope->currentSelf->type->getFqsen(), 1); $name = $node->getName(); if ($propertyDeclaration->isStatic()) { // Static Property: use ClassName::$propertyName as name - return (string)$classDeclaration->getNamespacedName() . '::$' . $name; + return $className . '::$' . $name; } // Instance Property: use ClassName->propertyName as name - return (string)$classDeclaration->getNamespacedName() . '->' . $name; + return $className . '->' . $name; } // INPUT OUTPUT @@ -1310,16 +1210,13 @@ public static function getDefinedFqn($node) return (string)$node->getNamespacedName(); } - // Class constant: use ClassName::CONSTANT_NAME as name - $classDeclaration = $constDeclaration->getFirstAncestor( - Node\Expression\ObjectCreationExpression::class, - PhpParser\ClassLike::class - ); - - if (!isset($classDeclaration->name)) { + if ($scope->currentSelf === null || !isset($scope->currentSelf->definitionNode->name) + ) { + // Class constant: use ClassName::CONSTANT_NAME as name return null; } - return (string)$classDeclaration->getNamespacedName() . '::' . $node->getName(); + $className = substr((string)$scope->currentSelf->type->getFqsen(), 1); + return $className . '::' . $node->getName(); } if (ParserHelpers\isConstDefineExpression($node)) { diff --git a/src/Scope/GetScopeAtNode.php b/src/Scope/GetScopeAtNode.php new file mode 100644 index 00000000..d7dc2da3 --- /dev/null +++ b/src/Scope/GetScopeAtNode.php @@ -0,0 +1,57 @@ +getRoot(); + /** @var FunctionDeclaration|null The first function declaration met, excluding anonymous functions. */ + $nearestFunctionDeclarationParent = $targetNode->getFirstAncestor(FunctionDeclaration::class); + /** @var ClassLike|null The first class met. */ + $nearestClassLike = $targetNode->getFirstAncestor(ClassLike::class); + + $traverser = new TreeTraverser($definitionResolver); + $resultScope = null; + $traverser->traverse( + $sourceFile, + function ( + $nodeOrToken, + Scope $scope + ) use ( + &$resultScope, + $targetNode, + $nearestFunctionDeclarationParent, + $nearestClassLike + ): int { + if ($nodeOrToken instanceof FunctionDeclaration && $nodeOrToken !== $nearestFunctionDeclarationParent) { + // Skip function declarations which do not contain the target node. + return TreeTraverser::ACTION_SKIP; + } + + if ($nodeOrToken instanceof ClassLike && $nodeOrToken !== $nearestClassLike) { + // Skip classes which are not the nearest parent class. + return TreeTraverser::ACTION_SKIP; + } + + if ($nodeOrToken === $targetNode) { + $resultScope = $scope; + return TreeTraverser::ACTION_END; + } + + return TreeTraverser::ACTION_CONTINUE; + } + ); + + return $resultScope; +} diff --git a/src/Scope/Scope.php b/src/Scope/Scope.php new file mode 100644 index 00000000..f3ad37de --- /dev/null +++ b/src/Scope/Scope.php @@ -0,0 +1,45 @@ +resolvedNameCache = []; + } + + /** + * @return string|null + */ + public function getResolvedName(QualifiedName $name) + { + $nameStr = (string)$name; + if (array_key_exists($nameStr, $this->resolvedNameCache)) { + return $this->resolvedNameCache[$nameStr]; + } + $resolvedName = $name->getResolvedName(); + return $this->resolvedNameCache[$nameStr] = $resolvedName ? (string)$resolvedName : null; + } +} diff --git a/src/Scope/TraversingEndedException.php b/src/Scope/TraversingEndedException.php new file mode 100644 index 00000000..10951a51 --- /dev/null +++ b/src/Scope/TraversingEndedException.php @@ -0,0 +1,8 @@ +definitionResolver = $definitionResolver; + } + + /** + * Calls visitor for each node or token with the node or token and the scope at that point. + * + * @param Node|Token $node Node or token to traverse. + * @param callable $visitor function(Node|Token, Scope). May return one of the ACTION_ constants. + */ + public function traverse($node, callable $visitor) + { + try { + $this->traverseRecursive($node, $visitor, new Scope); + } catch (TraversingEndedException $e) { + } + } + + private function traverseRecursive($node, callable $visitor, Scope $scope) + { + $visitorResult = $visitor($node, $scope); + if ($visitorResult === self::ACTION_END) { + throw new TraversingEndedException; + } + if (!$node instanceof Node || $visitorResult === self::ACTION_SKIP) { + return; + } + + foreach ($node::CHILD_NAMES as $childName) { + $child = $node->$childName; + + if ($child === null) { + continue; + } + + $childScope = $this->getScopeInChild($node, $childName, $scope); + + if (\is_array($child)) { + foreach ($child as $actualChild) { + $this->traverseRecursive($actualChild, $visitor, $childScope); + } + } else { + $this->traverseRecursive($child, $visitor, $childScope); + } + } + + $this->modifyScopeAfterNode($node, $scope); + } + + /** + * E.g. in function body, gets the scope consisting of parameters and used names. + * + * @return Scope + * The new scope, or the same scope instance if the child does not has its own scope. + */ + private function getScopeInChild(Node $node, string $childName, Scope $scope): Scope + { + if ($node instanceof FunctionLike + && $childName === 'compoundStatementOrSemicolon' + && $node->compoundStatementOrSemicolon instanceof Node\Statement\CompoundStatementNode + ) { + $childScope = new Scope; + $childScope->currentSelf = $scope->currentSelf; + $childScope->resolvedNameCache = $scope->resolvedNameCache; + $isStatic = $node instanceof Node\MethodDeclaration ? $node->isStatic() : !empty($node->staticModifier); + if (!$isStatic && isset($scope->variables['this'])) { + $childScope->variables['this'] = $scope->variables['this']; + } + + if ($node->parameters !== null) { + foreach ($node->parameters->getElements() as $param) { + $childScope->variables[$param->getName()] = new Variable( + // Pass the child scope when getting parameters - the outer scope cannot affect + // any parameters of the function declaration. + $this->definitionResolver->getTypeFromNode($param, $childScope), + $param + ); + } + } + + if ($node instanceof Node\Expression\AnonymousFunctionCreationExpression + && $node->anonymousFunctionUseClause !== null + && $node->anonymousFunctionUseClause->useVariableNameList !== null) { + foreach ($node->anonymousFunctionUseClause->useVariableNameList->getElements() as $use) { + $name = $use->getName(); + // Used variable in an anonymous function. Same as parent type, Mixed if not defined in parent. + $childScope->variables[$name] = new Variable( + isset($scope->variables[$name]) ? $scope->variables[$name]->type : new Types\Mixed_, + $use + ); + } + } + + return $childScope; + } + + if ($node instanceof ClassLike + && (in_array($childName, ['classMembers', 'interfaceMembers','traitMembers'], true)) + ) { + $childScope = new Scope; + $childScope->resolvedNameCache = $scope->resolvedNameCache; + $thisVar = new Variable( + new Types\Object_(new Fqsen('\\' . (string)$node->getNamespacedName())), + $node + ); + $childScope->variables['this'] = $thisVar; + $childScope->currentSelf = $thisVar; + return $childScope; + } + + return $scope; + } + + /** + * Adds any variables declared by $node to $scope. + * + * Note that functions like extract and parse_str are not handled. + * + * @return void + */ + private function modifyScopeAfterNode(Node $node, Scope $scope) + { + if ($node instanceof Expression\AssignmentExpression) { + if ($node->operator->kind !== TokenKind::EqualsToken + || !$node->leftOperand instanceof Expression\Variable + || $node->rightOperand === null + || $node->rightOperand instanceof MissingToken + ) { + return; + } + $scope->variables[$node->leftOperand->getName()] = new Variable( + $this->definitionResolver->resolveExpressionNodeToType($node->rightOperand, $scope), + $node + ); + } else if (($node instanceof Node\ForeachValue || $node instanceof Node\ForeachKey) + && $node->expression instanceof Node\Expression\Variable + ) { + $scope->variables[$node->expression->getName()] = new Variable( + $this->definitionResolver->getTypeFromNode($node, $scope), + $node + ); + } else if ($node instanceof Node\Statement\NamespaceDefinition) { + // After a new namespace A\B;, the current alias table is flushed. + $scope->clearResolvedNameCache(); + } + + + // TODO: Handle use (&$x) when $x is not defined in scope. + // TODO: Handle list(...) = $a; and [...] = $a; + // TODO: Handle foreach ($a as list(...)) and foreach ($a as [...]) + // TODO: Handle unset($var) + // TODO: Handle global $var + } +} diff --git a/src/Scope/Variable.php b/src/Scope/Variable.php new file mode 100644 index 00000000..19939714 --- /dev/null +++ b/src/Scope/Variable.php @@ -0,0 +1,29 @@ +type = $type; + $this->definitionNode = $definitionNode; + } +} diff --git a/src/Server/TextDocument.php b/src/Server/TextDocument.php index 371ad361..b5cbedc2 100644 --- a/src/Server/TextDocument.php +++ b/src/Server/TextDocument.php @@ -214,7 +214,7 @@ public function references( } } else { // Definition with a global FQN - $fqn = DefinitionResolver::getDefinedFqn($node); + $fqn = $this->definitionResolver->getDefinedFqn($node); // Wait until indexing finished if (!$this->index->isComplete()) { @@ -277,7 +277,7 @@ public function definition(TextDocumentIdentifier $textDocument, Position $posit return []; } // Handle definition nodes - $fqn = DefinitionResolver::getDefinedFqn($node); + $fqn = $this->definitionResolver->getDefinedFqn($node); while (true) { if ($fqn) { $def = $this->index->getDefinition($fqn); @@ -318,7 +318,7 @@ public function hover(TextDocumentIdentifier $textDocument, Position $position): if ($node === null) { return new Hover([]); } - $definedFqn = DefinitionResolver::getDefinedFqn($node); + $definedFqn = $this->definitionResolver->getDefinedFqn($node); while (true) { if ($definedFqn) { // Support hover for definitions @@ -392,7 +392,7 @@ public function xdefinition(TextDocumentIdentifier $textDocument, Position $posi return []; } // Handle definition nodes - $fqn = DefinitionResolver::getDefinedFqn($node); + $fqn = $this->definitionResolver->getDefinedFqn($node); while (true) { if ($fqn) { $def = $this->index->getDefinition($fqn); diff --git a/src/SignatureHelpProvider.php b/src/SignatureHelpProvider.php index aba02b4d..92728252 100644 --- a/src/SignatureHelpProvider.php +++ b/src/SignatureHelpProvider.php @@ -126,7 +126,7 @@ private function getCallingInfo(Node $node) } // Now find the definition of the call - $fqn = $fqn ?: DefinitionResolver::getDefinedFqn($callingNode); + $fqn = $fqn ?: $this->definitionResolver->getDefinedFqn($callingNode); while (true) { if ($fqn) { $def = $this->index->getDefinition($fqn); diff --git a/src/SignatureInformationFactory.php b/src/SignatureInformationFactory.php index 6b8a1f04..f6fa5a01 100644 --- a/src/SignatureInformationFactory.php +++ b/src/SignatureInformationFactory.php @@ -4,6 +4,7 @@ namespace LanguageServer; use LanguageServer\Protocol\{SignatureInformation, ParameterInformation}; +use LanguageServer\Scope\Scope; use Microsoft\PhpParser\FunctionLike; class SignatureInformationFactory @@ -28,9 +29,9 @@ public function __construct(DefinitionResolver $definitionResolver) * * @return SignatureInformation */ - public function create(FunctionLike $node): SignatureInformation + public function create(FunctionLike $node, Scope $scope = null): SignatureInformation { - $params = $this->createParameters($node); + $params = $this->createParameters($node, $scope); $label = $this->createLabel($params); return new SignatureInformation( $label, @@ -46,12 +47,12 @@ public function create(FunctionLike $node): SignatureInformation * * @return ParameterInformation[] */ - private function createParameters(FunctionLike $node): array + private function createParameters(FunctionLike $node, Scope $scope = null): array { $params = []; if ($node->parameters) { foreach ($node->parameters->getElements() as $element) { - $param = (string) $this->definitionResolver->getTypeFromNode($element); + $param = (string) $this->definitionResolver->getTypeFromNode($element, $scope); $param .= ' '; if ($element->dotDotDotToken) { $param .= '...'; diff --git a/src/TreeAnalyzer.php b/src/TreeAnalyzer.php index 465f5bb1..0ceda734 100644 --- a/src/TreeAnalyzer.php +++ b/src/TreeAnalyzer.php @@ -5,6 +5,8 @@ use LanguageServer\Protocol\{Diagnostic, DiagnosticSeverity, Range, Position, TextEdit}; use LanguageServer\Index\Index; +use LanguageServer\Scope\Scope; +use LanguageServer\Scope\TreeTraverser; use phpDocumentor\Reflection\DocBlockFactory; use Sabre\Uri; use Microsoft\PhpParser; @@ -56,7 +58,16 @@ public function __construct(PhpParser\Parser $parser, string $content, DocBlockF // TODO - docblock errors - $this->traverse($this->sourceFileNode); + $traverser = new TreeTraverser($definitionResolver); + $traverser->traverse( + $this->sourceFileNode, + function ($nodeOrToken, Scope $scope) { + $this->collectDiagnostics($nodeOrToken, $scope); + if ($nodeOrToken instanceof Node) { + $this->collectDefinitionsAndReferences($nodeOrToken, $scope); + } + } + ); } /** @@ -66,7 +77,7 @@ public function __construct(PhpParser\Parser $parser, string $content, DocBlockF * @param Node|Token $node * @return void */ - private function collectDiagnostics($node) + private function collectDiagnostics($node, Scope $scope) { // Get errors from the parser. if (($error = PhpParser\DiagnosticsProvider::checkDiagnostics($node)) !== null) { @@ -95,52 +106,17 @@ private function collectDiagnostics($node) } // Check for invalid usage of $this. - if ($node instanceof Node\Expression\Variable && $node->getName() === 'this') { - // Find the first ancestor that's a class method. Return an error - // if there is none, or if the method is static. - $method = $node->getFirstAncestor(Node\MethodDeclaration::class); - if ($method && $method->isStatic()) { - $this->diagnostics[] = new Diagnostic( - "\$this can not be used in static methods.", - Range::fromNode($node), - null, - DiagnosticSeverity::ERROR, - 'php' - ); - } - } - } - - /** - * Recursive AST traversal to collect definitions/references and diagnostics - * - * @param Node|Token $currentNode The node/token to process - */ - private function traverse($currentNode) - { - $this->collectDiagnostics($currentNode); - - // Only update/descend into Nodes, Tokens are leaves - if ($currentNode instanceof Node) { - $this->collectDefinitionsAndReferences($currentNode); - - foreach ($currentNode::CHILD_NAMES as $name) { - $child = $currentNode->$name; - - if ($child === null) { - continue; - } - - if (\is_array($child)) { - foreach ($child as $actualChild) { - if ($actualChild !== null) { - $this->traverse($actualChild); - } - } - } else { - $this->traverse($child); - } - } + if ($node instanceof Node\Expression\Variable && + !isset($scope->variables['this']) && + $node->getName() === 'this' + ) { + $this->diagnostics[] = new Diagnostic( + "\$this can not be used in static methods.", + Range::fromNode($node), + null, + DiagnosticSeverity::ERROR, + 'php' + ); } } @@ -149,13 +125,13 @@ private function traverse($currentNode) * * @param Node $node */ - private function collectDefinitionsAndReferences(Node $node) + private function collectDefinitionsAndReferences(Node $node, Scope $scope) { - $fqn = ($this->definitionResolver)::getDefinedFqn($node); + $fqn = $this->definitionResolver->getDefinedFqn($node, $scope); // Only index definitions with an FQN (no variables) if ($fqn !== null) { $this->definitionNodes[$fqn] = $node; - $this->definitions[$fqn] = $this->definitionResolver->createDefinitionFromNode($node, $fqn); + $this->definitions[$fqn] = $this->definitionResolver->createDefinitionFromNode($node, $fqn, $scope); } else { $parent = $node->parent; @@ -165,7 +141,7 @@ private function collectDefinitionsAndReferences(Node $node) ($node instanceof Node\Expression\ScopedPropertyAccessExpression || $node instanceof Node\Expression\MemberAccessExpression) && !( - $node->parent instanceof Node\Expression\CallExpression || + $parent instanceof Node\Expression\CallExpression || $node->memberName instanceof PhpParser\Token )) || ($parent instanceof Node\Statement\NamespaceDefinition && $parent->name !== null && $parent->name->getStart() === $node->getStart()) @@ -173,7 +149,7 @@ private function collectDefinitionsAndReferences(Node $node) return; } - $fqn = $this->definitionResolver->resolveReferenceNodeToFqn($node); + $fqn = $this->definitionResolver->resolveReferenceNodeToFqn($node, $scope); if (!$fqn) { return; } @@ -181,21 +157,22 @@ private function collectDefinitionsAndReferences(Node $node) if ($fqn === 'self' || $fqn === 'static') { // Resolve self and static keywords to the containing class // (This is not 100% correct for static but better than nothing) - $classNode = $node->getFirstAncestor(Node\Statement\ClassDeclaration::class); - if (!$classNode) { - return; - } - $fqn = (string)$classNode->getNamespacedName(); - if (!$fqn) { + if (!$scope->currentSelf) { return; } + $fqn = substr((string)$scope->currentSelf->type->getFqsen(), 1); } else if ($fqn === 'parent') { // Resolve parent keyword to the base class FQN - $classNode = $node->getFirstAncestor(Node\Statement\ClassDeclaration::class); - if (!$classNode || !$classNode->classBaseClause || !$classNode->classBaseClause->baseClass) { + if ($scope->currentSelf === null) { + return; + } + $classNode = $scope->currentSelf->definitionNode; + if (empty($classNode->classBaseClause) + || !$classNode->classBaseClause->baseClass instanceof Node\QualifiedName + ) { return; } - $fqn = (string)$classNode->classBaseClause->baseClass->getResolvedName(); + $fqn = $scope->getResolvedName($classNode->classBaseClause->baseClass); if (!$fqn) { return; } diff --git a/tests/Validation/cases/constantsInFunctionParamDefault.php.expected.json b/tests/Validation/cases/constantsInFunctionParamDefault.php.expected.json index 0a34c36e..468ccc94 100644 --- a/tests/Validation/cases/constantsInFunctionParamDefault.php.expected.json +++ b/tests/Validation/cases/constantsInFunctionParamDefault.php.expected.json @@ -45,11 +45,11 @@ "declarationLine": "function b ($a = MY_CONSTANT);", "documentation": null, "signatureInformation": { - "label": "(\\MY_CONSTANT $a = MY_CONSTANT)", + "label": "(mixed $a = MY_CONSTANT)", "documentation": null, "parameters": [ { - "label": "\\MY_CONSTANT $a = MY_CONSTANT", + "label": "mixed $a = MY_CONSTANT", "documentation": null } ] diff --git a/tests/Validation/cases/verifyFqsenOnClassProperty.php.expected.json b/tests/Validation/cases/verifyFqsenOnClassProperty.php.expected.json index a434cf25..314596b1 100644 --- a/tests/Validation/cases/verifyFqsenOnClassProperty.php.expected.json +++ b/tests/Validation/cases/verifyFqsenOnClassProperty.php.expected.json @@ -43,7 +43,7 @@ }, "containerName": "Foo" }, - "type__tostring": "\\", + "type__tostring": "mixed", "type": {}, "declarationLine": "protected $bar;", "documentation": null,