From d78b99b23dc16ec9f222d454873b9577b3bcfb97 Mon Sep 17 00:00:00 2001 From: Declspeck Date: Sat, 24 Feb 2018 18:25:48 +0200 Subject: [PATCH 1/8] feat(parsing): Keep track of scope when parsing --- composer.json | 3 +- src/DefinitionResolver.php | 462 +++++++----------- src/Scope/GetScopeAtNode.php | 54 ++ src/Scope/Scope.php | 49 ++ src/Scope/TraversingEndedException.php | 8 + src/Scope/TreeTraverser.php | 192 ++++++++ src/Scope/Variable.php | 29 ++ src/Server/TextDocument.php | 8 +- src/SignatureHelpProvider.php | 2 +- src/SignatureInformationFactory.php | 9 +- src/TreeAnalyzer.php | 101 ++-- ...tsInFunctionParamDefault.php.expected.json | 4 +- ...rifyFqsenOnClassProperty.php.expected.json | 2 +- 13 files changed, 567 insertions(+), 356 deletions(-) create mode 100644 src/Scope/GetScopeAtNode.php create mode 100644 src/Scope/Scope.php create mode 100644 src/Scope/TraversingEndedException.php create mode 100644 src/Scope/TreeTraverser.php create mode 100644 src/Scope/Variable.php 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/DefinitionResolver.php b/src/DefinitionResolver.php index 990c1965..812e6648 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->currentClassLikeVariable === null) { + return null; + } + $fqn = substr((string)$scope->currentClassLikeVariable->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->currentClassLikeVariable === null) { + return null; } + $fqn = substr((string)$scope->currentClassLikeVariable->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->currentClassLikeVariable === null) { + return null; } - $fqn = (string)$classNode->classBaseClause->baseClass->getResolvedName(); - if (!$fqn) { - return; + // Resolve parent keyword to the base class FQN + $classNode = $scope->currentClassLikeVariable->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,13 @@ 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 +439,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->currentClassLikeVariable === null) { + return null; + } + $classFqn = substr((string)$scope->currentClassLikeVariable->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 +480,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->currentClassLikeVariable->definitionNode ?? null; if ($classNode === null) { return null; } @@ -485,12 +507,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->currentClassLikeVariable->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 +532,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 +562,13 @@ 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(); + if ($name === 'this') { + return $scope->thisVariable === null ? new Types\Mixed_ : $scope->thisVariable->type; } + return isset($scope->variables[$name]) + ? $scope->variables[$name]->type + : new Types\Mixed_; } // FUNCTION CALL @@ -691,18 +580,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 +605,7 @@ public function resolveExpressionNodeToType($expr) if ($token === PhpParser\TokenKind::NullReservedWord) { return new Types\Null_; } + return new Types\Mixed_; } // CONSTANT FETCH @@ -726,6 +616,7 @@ public function resolveExpressionNodeToType($expr) if ($def !== null) { return $def->type; } + return new Types\Mixed_; } // MEMBER CALL EXPRESSION/SCOPED PROPERTY CALL EXPRESSION @@ -735,7 +626,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 +637,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->currentClassLikeVariable === null) { return new Types\Mixed_; } + $classFqn = substr((string)$scope->currentClassLikeVariable->type->getFqsen(), 1); } else if (!($t instanceof Types\Object_) || $t->getFqsen() === null) { return new Types\Mixed_; } else { @@ -782,7 +676,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 +699,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 +721,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 +737,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 +796,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 +848,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 +875,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 +890,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 +904,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 +917,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->currentClassLikeVariable === null) { + return new Types\Self_; } + return $scope->currentClassLikeVariable->type; + } else if ($className === 'parent') { + if ($scope->currentClassLikeVariable === null) { + return new Types\Object_; + } + $classNode = $scope->currentClassLikeVariable->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 +956,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 +982,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 +1001,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 +1027,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->currentClassLikeVariable) { + return $scope->currentClassLikeVariable->type; } return $returnType; } @@ -1138,13 +1040,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->currentClassLikeVariable !== null && $scope->getResolvedName($node->returnType) === 'self') { + return $scope->currentClassLikeVariable->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 +1052,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 +1064,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 +1095,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 +1117,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 +1151,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 +1163,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->currentClassLikeVariable === null) { + return; + } + $className = substr((string)$scope->currentClassLikeVariable->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 +1186,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->currentClassLikeVariable !== null && + isset($scope->currentClassLikeVariable->definitionNode->name) + ) { + $className = substr((string)$scope->currentClassLikeVariable->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 +1212,14 @@ 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->currentClassLikeVariable === null + || !isset($scope->currentClassLikeVariable->definitionNode->name) + ) { + // Class constant: use ClassName::CONSTANT_NAME as name return null; } - return (string)$classDeclaration->getNamespacedName() . '::' . $node->getName(); + $className = substr((string)$scope->currentClassLikeVariable->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..68f0968b --- /dev/null +++ b/src/Scope/GetScopeAtNode.php @@ -0,0 +1,54 @@ +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..d43abcf9 --- /dev/null +++ b/src/Scope/Scope.php @@ -0,0 +1,49 @@ +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->currentClassLikeVariable = $scope->currentClassLikeVariable; + $childScope->resolvedNameCache = $scope->resolvedNameCache; + $isStatic = $node instanceof Node\MethodDeclaration ? $node->isStatic() : !empty($node->staticModifier); + if (!$isStatic) { + $childScope->thisVariable = $scope->thisVariable; + } + + 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; + $childScope->thisVariable = new Variable( + new Types\Object_(new Fqsen('\\' . (string)$node->getNamespacedName())), + $node + ); + $childScope->currentClassLikeVariable = $childScope->thisVariable; + 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 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; + // TODO: Handle foreach ($a as list(...)) + // 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..c7c6e9f7 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 ($scope->thisVariable === null && + $node instanceof Node\Expression\Variable && + $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->currentClassLikeVariable) { return; } + $fqn = substr((string)$scope->currentClassLikeVariable->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->currentClassLikeVariable === null) { + return; + } + $classNode = $scope->currentClassLikeVariable->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, From 7dd0f105643214963cdc03788e149d610854a436 Mon Sep 17 00:00:00 2001 From: Declspeck Date: Sat, 24 Feb 2018 20:12:03 +0200 Subject: [PATCH 2/8] refactor(completion): use scope to suggest local variables --- src/CompletionProvider.php | 128 +++++-------------------------------- 1 file changed, 16 insertions(+), 112 deletions(-) diff --git a/src/CompletionProvider.php b/src/CompletionProvider.php index 8b5b5c00..5ec9c974 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,25 @@ 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; + if ($scope->thisVariable !== null) { + $variables['this'] = $scope->thisVariable; + } + 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 +419,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); - } } From a8829a9b44b7a67273d99ce6dc33e98f9be1c978 Mon Sep 17 00:00:00 2001 From: Declspeck Date: Sat, 24 Feb 2018 20:38:49 +0200 Subject: [PATCH 3/8] refactor(scope): remove special-case handling of $this --- src/CompletionProvider.php | 3 --- src/DefinitionResolver.php | 3 --- src/Scope/Scope.php | 7 ------- src/Scope/TreeTraverser.php | 9 +++++---- src/TreeAnalyzer.php | 4 ++-- 5 files changed, 7 insertions(+), 19 deletions(-) diff --git a/src/CompletionProvider.php b/src/CompletionProvider.php index 5ec9c974..8c39c776 100644 --- a/src/CompletionProvider.php +++ b/src/CompletionProvider.php @@ -201,9 +201,6 @@ public function provideCompletion(PhpDocument $doc, Position $pos, CompletionCon $prefixLen = strlen($namePrefix); $scope = getScopeAtNode($this->definitionResolver, $node); $variables = $scope->variables; - if ($scope->thisVariable !== null) { - $variables['this'] = $scope->thisVariable; - } foreach ($variables as $name => $var) { if (substr($name, 0, $prefixLen) !== $namePrefix) { continue; diff --git a/src/DefinitionResolver.php b/src/DefinitionResolver.php index 812e6648..52d85a37 100644 --- a/src/DefinitionResolver.php +++ b/src/DefinitionResolver.php @@ -563,9 +563,6 @@ public function resolveExpressionNodeToType($expr, Scope $scope = null) // $myVariable -> type of corresponding assignment expression if ($expr instanceof Node\Expression\Variable || $expr instanceof Node\UseVariableName) { $name = $expr->getName(); - if ($name === 'this') { - return $scope->thisVariable === null ? new Types\Mixed_ : $scope->thisVariable->type; - } return isset($scope->variables[$name]) ? $scope->variables[$name]->type : new Types\Mixed_; diff --git a/src/Scope/Scope.php b/src/Scope/Scope.php index d43abcf9..5c137af6 100644 --- a/src/Scope/Scope.php +++ b/src/Scope/Scope.php @@ -9,13 +9,6 @@ */ class Scope { - /** - * @var Variable|null $this - * - * Note that this will be set when a class is entered. It is unset again when entering a static function. - */ - public $thisVariable; - /** * @var Variable|null $this, except also set in static contexts. */ diff --git a/src/Scope/TreeTraverser.php b/src/Scope/TreeTraverser.php index 4413f476..79358718 100644 --- a/src/Scope/TreeTraverser.php +++ b/src/Scope/TreeTraverser.php @@ -102,8 +102,8 @@ private function getScopeInChild(Node $node, string $childName, Scope $scope): S $childScope->currentClassLikeVariable = $scope->currentClassLikeVariable; $childScope->resolvedNameCache = $scope->resolvedNameCache; $isStatic = $node instanceof Node\MethodDeclaration ? $node->isStatic() : !empty($node->staticModifier); - if (!$isStatic) { - $childScope->thisVariable = $scope->thisVariable; + if (!$isStatic && isset($scope->variables['this'])) { + $childScope->variables['this'] = $scope->variables['this']; } if ($node->parameters !== null) { @@ -138,11 +138,12 @@ private function getScopeInChild(Node $node, string $childName, Scope $scope): S ) { $childScope = new Scope; $childScope->resolvedNameCache = $scope->resolvedNameCache; - $childScope->thisVariable = new Variable( + $thisVar = new Variable( new Types\Object_(new Fqsen('\\' . (string)$node->getNamespacedName())), $node ); - $childScope->currentClassLikeVariable = $childScope->thisVariable; + $childScope->variables['this'] = $thisVar; + $childScope->currentClassLikeVariable = $thisVar; return $childScope; } diff --git a/src/TreeAnalyzer.php b/src/TreeAnalyzer.php index c7c6e9f7..176dc5e5 100644 --- a/src/TreeAnalyzer.php +++ b/src/TreeAnalyzer.php @@ -106,8 +106,8 @@ private function collectDiagnostics($node, Scope $scope) } // Check for invalid usage of $this. - if ($scope->thisVariable === null && - $node instanceof Node\Expression\Variable && + if ($node instanceof Node\Expression\Variable && + !isset($scope->variables['this']) && $node->getName() === 'this' ) { $this->diagnostics[] = new Diagnostic( From 77345799f50d95048b81c63939f7f2d4b260cd24 Mon Sep 17 00:00:00 2001 From: Declspeck Date: Sun, 25 Feb 2018 00:34:36 +0200 Subject: [PATCH 4/8] refactor(scope): rename currentClassLikeVariable to currentSelf --- src/DefinitionResolver.php | 55 ++++++++++++++++++------------------- src/Scope/Scope.php | 4 +-- src/Scope/TreeTraverser.php | 4 +-- src/TreeAnalyzer.php | 8 +++--- 4 files changed, 35 insertions(+), 36 deletions(-) diff --git a/src/DefinitionResolver.php b/src/DefinitionResolver.php index 52d85a37..8c9dd0fc 100644 --- a/src/DefinitionResolver.php +++ b/src/DefinitionResolver.php @@ -277,10 +277,10 @@ public function resolveReferenceNodeToDefinition(Node $node, Scope $scope = null $name = $node->getName(); // Resolve $this to the containing class definition. if ($name === 'this') { - if ($scope->currentClassLikeVariable === null) { + if ($scope->currentSelf === null) { return null; } - $fqn = substr((string)$scope->currentClassLikeVariable->type->getFqsen(), 1); + $fqn = substr((string)$scope->currentSelf->type->getFqsen(), 1); return $this->index->getDefinition($fqn, false); } @@ -297,16 +297,16 @@ public function resolveReferenceNodeToDefinition(Node $node, Scope $scope = null 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) - if ($scope->currentClassLikeVariable === null) { + if ($scope->currentSelf === null) { return null; } - $fqn = substr((string)$scope->currentClassLikeVariable->type->getFqsen(), 1); + $fqn = substr((string)$scope->currentSelf->type->getFqsen(), 1); } else if ($fqn === 'parent') { - if ($scope->currentClassLikeVariable === null) { + if ($scope->currentSelf === null) { return null; } // Resolve parent keyword to the base class FQN - $classNode = $scope->currentClassLikeVariable->definitionNode; + $classNode = $scope->currentSelf->definitionNode; if (!$classNode->classBaseClause || !$classNode->classBaseClause->baseClass) { return null; } @@ -439,10 +439,10 @@ private function resolveMemberAccessExpressionNodeToFqn(Node\Expression\MemberAc || $varType instanceof Types\Self_ ) { // $this/static/self is resolved to the containing class - if ($scope->currentClassLikeVariable === null) { + if ($scope->currentSelf === null) { return null; } - $classFqn = substr((string)$scope->currentClassLikeVariable->type->getFqsen(), 1); + $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; @@ -498,7 +498,7 @@ private function resolveScopedPropertyAccessExpressionNodeToFqn( if ($className === 'self' || $className === 'static' || $className === 'parent') { // self and static are resolved to the containing class - $classNode = $scope->currentClassLikeVariable->definitionNode ?? null; + $classNode = $scope->currentSelf->definitionNode ?? null; if ($classNode === null) { return null; } @@ -509,7 +509,7 @@ private function resolveScopedPropertyAccessExpressionNodeToFqn( } $className = $scope->getResolvedName($classNode->extends); } else { - $className = substr((string)$scope->currentClassLikeVariable->type->getFqsen(), 1); + $className = substr((string)$scope->currentSelf->type->getFqsen(), 1); } } elseif ($scoped->scopeResolutionQualifier instanceof Node\QualifiedName) { $className = $scope->getResolvedName($scoped->scopeResolutionQualifier); @@ -643,10 +643,10 @@ public function resolveExpressionNodeToType($expr, Scope $scope = null) } for ($i = 0; $t = $objType->get($i); $i++) { if ($t instanceof Types\This) { - if ($scope->currentClassLikeVariable === null) { + if ($scope->currentSelf === null) { return new Types\Mixed_; } - $classFqn = substr((string)$scope->currentClassLikeVariable->type->getFqsen(), 1); + $classFqn = substr((string)$scope->currentSelf->type->getFqsen(), 1); } else if (!($t instanceof Types\Object_) || $t->getFqsen() === null) { return new Types\Mixed_; } else { @@ -920,15 +920,15 @@ public function resolveClassNameToType($class, Scope $scope = null): Type $className = $scope->getResolvedName($class); if ($className === 'self') { - if ($scope->currentClassLikeVariable === null) { + if ($scope->currentSelf === null) { return new Types\Self_; } - return $scope->currentClassLikeVariable->type; + return $scope->currentSelf->type; } else if ($className === 'parent') { - if ($scope->currentClassLikeVariable === null) { + if ($scope->currentSelf === null) { return new Types\Object_; } - $classNode = $scope->currentClassLikeVariable->definitionNode; + $classNode = $scope->currentSelf->definitionNode; if (empty($classNode->classBaseClause) || !$classNode->classBaseClause->baseClass instanceof Node\QualifiedName ) { @@ -1027,8 +1027,8 @@ public function getTypeFromNode($node, Scope $scope = null) && ($returnType = $returnTags[0]->getType()) !== null ) { // Use @return tag - if ($returnType instanceof Types\Self_ && null !== $scope->currentClassLikeVariable) { - return $scope->currentClassLikeVariable->type; + if ($returnType instanceof Types\Self_ && null !== $scope->currentSelf) { + return $scope->currentSelf->type; } return $returnType; } @@ -1037,8 +1037,8 @@ public function getTypeFromNode($node, Scope $scope = null) if ($node->returnType instanceof PhpParser\Token) { // Resolve a string like "bool" to a type object return $this->typeResolver->resolve($node->returnType->getText($node->getFileContents())); - } else if ($scope->currentClassLikeVariable !== null && $scope->getResolvedName($node->returnType) === 'self') { - return $scope->currentClassLikeVariable->type; + } else if ($scope->currentSelf !== null && $scope->getResolvedName($node->returnType) === 'self') { + return $scope->currentSelf->type; } return new Types\Object_(new Fqsen('\\' . $scope->getResolvedName($node->returnType))); } @@ -1160,10 +1160,10 @@ public function getDefinedFqn($node, Scope $scope = null) // } if ($node instanceof Node\MethodDeclaration) { // Class method: use ClassName->methodName() as name - if ($scope->currentClassLikeVariable === null) { + if ($scope->currentSelf === null) { return; } - $className = substr((string)$scope->currentClassLikeVariable->type->getFqsen(), 1); + $className = substr((string)$scope->currentSelf->type->getFqsen(), 1); if (!$className) { // Ignore anonymous classes return null; @@ -1183,10 +1183,10 @@ public function getDefinedFqn($node, Scope $scope = null) // } if ( ($propertyDeclaration = ParserHelpers\tryGetPropertyDeclaration($node)) !== null && - $scope->currentClassLikeVariable !== null && - isset($scope->currentClassLikeVariable->definitionNode->name) + $scope->currentSelf !== null && + isset($scope->currentSelf->definitionNode->name) ) { - $className = substr((string)$scope->currentClassLikeVariable->type->getFqsen(), 1); + $className = substr((string)$scope->currentSelf->type->getFqsen(), 1); $name = $node->getName(); if ($propertyDeclaration->isStatic()) { // Static Property: use ClassName::$propertyName as name @@ -1209,13 +1209,12 @@ public function getDefinedFqn($node, Scope $scope = null) return (string)$node->getNamespacedName(); } - if ($scope->currentClassLikeVariable === null - || !isset($scope->currentClassLikeVariable->definitionNode->name) + if ($scope->currentSelf === null || !isset($scope->currentSelf->definitionNode->name) ) { // Class constant: use ClassName::CONSTANT_NAME as name return null; } - $className = substr((string)$scope->currentClassLikeVariable->type->getFqsen(), 1); + $className = substr((string)$scope->currentSelf->type->getFqsen(), 1); return $className . '::' . $node->getName(); } diff --git a/src/Scope/Scope.php b/src/Scope/Scope.php index 5c137af6..be7c7beb 100644 --- a/src/Scope/Scope.php +++ b/src/Scope/Scope.php @@ -10,9 +10,9 @@ class Scope { /** - * @var Variable|null $this, except also set in static contexts. + * @var Variable|null "Variable" representing this/self */ - public $currentClassLikeVariable; + public $currentSelf; /** * @var Variable[] Variables in the scope, indexed by their names (without the dollar) and excluding $this. diff --git a/src/Scope/TreeTraverser.php b/src/Scope/TreeTraverser.php index 79358718..99f4f731 100644 --- a/src/Scope/TreeTraverser.php +++ b/src/Scope/TreeTraverser.php @@ -99,7 +99,7 @@ private function getScopeInChild(Node $node, string $childName, Scope $scope): S && $node->compoundStatementOrSemicolon instanceof Node\Statement\CompoundStatementNode ) { $childScope = new Scope; - $childScope->currentClassLikeVariable = $scope->currentClassLikeVariable; + $childScope->currentSelf = $scope->currentSelf; $childScope->resolvedNameCache = $scope->resolvedNameCache; $isStatic = $node instanceof Node\MethodDeclaration ? $node->isStatic() : !empty($node->staticModifier); if (!$isStatic && isset($scope->variables['this'])) { @@ -143,7 +143,7 @@ private function getScopeInChild(Node $node, string $childName, Scope $scope): S $node ); $childScope->variables['this'] = $thisVar; - $childScope->currentClassLikeVariable = $thisVar; + $childScope->currentSelf = $thisVar; return $childScope; } diff --git a/src/TreeAnalyzer.php b/src/TreeAnalyzer.php index 176dc5e5..0ceda734 100644 --- a/src/TreeAnalyzer.php +++ b/src/TreeAnalyzer.php @@ -157,16 +157,16 @@ private function collectDefinitionsAndReferences(Node $node, Scope $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) - if (!$scope->currentClassLikeVariable) { + if (!$scope->currentSelf) { return; } - $fqn = substr((string)$scope->currentClassLikeVariable->type->getFqsen(), 1); + $fqn = substr((string)$scope->currentSelf->type->getFqsen(), 1); } else if ($fqn === 'parent') { // Resolve parent keyword to the base class FQN - if ($scope->currentClassLikeVariable === null) { + if ($scope->currentSelf === null) { return; } - $classNode = $scope->currentClassLikeVariable->definitionNode; + $classNode = $scope->currentSelf->definitionNode; if (empty($classNode->classBaseClause) || !$classNode->classBaseClause->baseClass instanceof Node\QualifiedName ) { From 29fd70a5bcdadf758d8d494501c22720ebd79f3f Mon Sep 17 00:00:00 2001 From: Declspeck Date: Sun, 25 Feb 2018 00:59:20 +0200 Subject: [PATCH 5/8] PHP 7.0 compatibility --- src/Scope/TreeTraverser.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Scope/TreeTraverser.php b/src/Scope/TreeTraverser.php index 99f4f731..8e21c1ad 100644 --- a/src/Scope/TreeTraverser.php +++ b/src/Scope/TreeTraverser.php @@ -22,17 +22,17 @@ class TreeTraverser /** * Descend into the node being parsed. The default action. */ - public const ACTION_CONTINUE = 0; + const ACTION_CONTINUE = 0; /** * Do not descend into the node being parsed. Traversal will continue after the node. */ - public const ACTION_SKIP = 1; + const ACTION_SKIP = 1; /** * Stop parsing entirely. `traverse` will return immediately. */ - public const ACTION_END = 2; + const ACTION_END = 2; private $definitionResolver; From 3a2bba7986fa585879710b7f605aa2078abb02be Mon Sep 17 00:00:00 2001 From: Declspeck Date: Sun, 25 Feb 2018 01:02:34 +0200 Subject: [PATCH 6/8] fix(style): fix phpcs errors --- src/DefinitionResolver.php | 3 ++- src/Scope/GetScopeAtNode.php | 5 ++++- src/Scope/Scope.php | 7 +++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/DefinitionResolver.php b/src/DefinitionResolver.php index 8c9dd0fc..d4b9ffd8 100644 --- a/src/DefinitionResolver.php +++ b/src/DefinitionResolver.php @@ -410,7 +410,8 @@ private function resolveQualifiedNameNodeToFqn(Node\QualifiedName $node, Scope $ return $name; } - private function resolveMemberAccessExpressionNodeToFqn(Node\Expression\MemberAccessExpression $access, Scope $scope) { + 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; diff --git a/src/Scope/GetScopeAtNode.php b/src/Scope/GetScopeAtNode.php index 68f0968b..d7dc2da3 100644 --- a/src/Scope/GetScopeAtNode.php +++ b/src/Scope/GetScopeAtNode.php @@ -25,7 +25,10 @@ function getScopeAtNode(DefinitionResolver $definitionResolver, Node $targetNode $resultScope = null; $traverser->traverse( $sourceFile, - function ($nodeOrToken, Scope $scope) use ( + function ( + $nodeOrToken, + Scope $scope + ) use ( &$resultScope, $targetNode, $nearestFunctionDeclarationParent, diff --git a/src/Scope/Scope.php b/src/Scope/Scope.php index be7c7beb..f3ad37de 100644 --- a/src/Scope/Scope.php +++ b/src/Scope/Scope.php @@ -2,6 +2,7 @@ declare(strict_types=1); namespace LanguageServer\Scope; + use Microsoft\PhpParser\Node\QualifiedName; /** @@ -24,14 +25,16 @@ class Scope */ public $resolvedNameCache = []; - public function clearResolvedNameCache() { + public function clearResolvedNameCache() + { $this->resolvedNameCache = []; } /** * @return string|null */ - public function getResolvedName(QualifiedName $name) { + public function getResolvedName(QualifiedName $name) + { $nameStr = (string)$name; if (array_key_exists($nameStr, $this->resolvedNameCache)) { return $this->resolvedNameCache[$nameStr]; From f4db997632bca94dd33d0ef03aae62a610bdb1ad Mon Sep 17 00:00:00 2001 From: Declspeck Date: Sun, 25 Feb 2018 01:05:23 +0200 Subject: [PATCH 7/8] fix(scope): reset on namespace declaration --- src/Scope/TreeTraverser.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Scope/TreeTraverser.php b/src/Scope/TreeTraverser.php index 8e21c1ad..30568c74 100644 --- a/src/Scope/TreeTraverser.php +++ b/src/Scope/TreeTraverser.php @@ -178,7 +178,7 @@ private function modifyScopeAfterNode(Node $node, Scope $scope) $this->definitionResolver->getTypeFromNode($node, $scope), $node ); - } else if ($node instanceof Statement\NamespaceDefinition) { + } else if ($node instanceof Node\Statement\NamespaceDefinition) { // After a new namespace A\B;, the current alias table is flushed. $scope->clearResolvedNameCache(); } From f14478f795d13f63adc6f3ab650d0a888dedbb05 Mon Sep 17 00:00:00 2001 From: Declspeck Date: Sun, 25 Feb 2018 10:11:57 +0200 Subject: [PATCH 8/8] documentation(scope): add more todo comments --- src/Scope/TreeTraverser.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Scope/TreeTraverser.php b/src/Scope/TreeTraverser.php index 30568c74..bafc4768 100644 --- a/src/Scope/TreeTraverser.php +++ b/src/Scope/TreeTraverser.php @@ -185,8 +185,8 @@ private function modifyScopeAfterNode(Node $node, Scope $scope) // TODO: Handle use (&$x) when $x is not defined in scope. - // TODO: Handle list(...) = $a; - // TODO: Handle foreach ($a as list(...)) + // TODO: Handle list(...) = $a; and [...] = $a; + // TODO: Handle foreach ($a as list(...)) and foreach ($a as [...]) // TODO: Handle unset($var) // TODO: Handle global $var }