feat(parsing): Keep track of scope when parsing
parent
a8f60c9cf6
commit
d78b99b23d
|
@ -44,7 +44,8 @@
|
|||
"files" : [
|
||||
"src/utils.php",
|
||||
"src/FqnUtilities.php",
|
||||
"src/ParserHelpers.php"
|
||||
"src/ParserHelpers.php",
|
||||
"src/Scope/GetScopeAtNode.php"
|
||||
]
|
||||
},
|
||||
"autoload-dev": {
|
||||
|
|
|
@ -5,6 +5,8 @@ namespace LanguageServer;
|
|||
|
||||
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 @@ class DefinitionResolver
|
|||
*
|
||||
* @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 @@ class DefinitionResolver
|
|||
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 @@ class DefinitionResolver
|
|||
) {
|
||||
$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,57 +259,63 @@ class DefinitionResolver
|
|||
* 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();
|
||||
// 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
|
||||
// http://php.net/manual/en/language.namespaces.fallback.php
|
||||
|
@ -319,15 +332,19 @@ class DefinitionResolver
|
|||
* 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 @@ class DefinitionResolver
|
|||
$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 @@ class DefinitionResolver
|
|||
}
|
||||
|
||||
// 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 @@ class DefinitionResolver
|
|||
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 @@ class DefinitionResolver
|
|||
|| $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 @@ class DefinitionResolver
|
|||
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 @@ class DefinitionResolver
|
|||
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 @@ class DefinitionResolver
|
|||
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 @@ class DefinitionResolver
|
|||
// $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 @@ class DefinitionResolver
|
|||
) {
|
||||
// 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 @@ class DefinitionResolver
|
|||
if ($token === PhpParser\TokenKind::NullReservedWord) {
|
||||
return new Types\Null_;
|
||||
}
|
||||
return new Types\Mixed_;
|
||||
}
|
||||
|
||||
// CONSTANT FETCH
|
||||
|
@ -726,6 +616,7 @@ class DefinitionResolver
|
|||
if ($def !== null) {
|
||||
return $def->type;
|
||||
}
|
||||
return new Types\Mixed_;
|
||||
}
|
||||
|
||||
// MEMBER CALL EXPRESSION/SCOPED PROPERTY CALL EXPRESSION
|
||||
|
@ -735,7 +626,7 @@ class DefinitionResolver
|
|||
$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 @@ class DefinitionResolver
|
|||
$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 @@ class DefinitionResolver
|
|||
|
||||
// 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 @@ class DefinitionResolver
|
|||
// 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 @@ class DefinitionResolver
|
|||
// ?:
|
||||
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 @@ class DefinitionResolver
|
|||
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 @@ class DefinitionResolver
|
|||
)
|
||||
) {
|
||||
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 @@ class DefinitionResolver
|
|||
$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 @@ class DefinitionResolver
|
|||
// $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 @@ class DefinitionResolver
|
|||
}
|
||||
|
||||
if ($expr instanceof Node\QualifiedName) {
|
||||
return $this->resolveClassNameToType($expr);
|
||||
return $this->resolveClassNameToType($expr, $scope);
|
||||
}
|
||||
|
||||
return new Types\Mixed_;
|
||||
|
@ -1010,9 +904,9 @@ class DefinitionResolver
|
|||
* @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 @@ class DefinitionResolver
|
|||
// `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) {
|
||||
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 = (string)$classNode->classBaseClause->baseClass->getResolvedName();
|
||||
} else {
|
||||
if ($classNode === null) {
|
||||
return new Types\Self_;
|
||||
}
|
||||
// self is resolved to the containing class
|
||||
$classFqn = (string)$classNode->getNamespacedName();
|
||||
}
|
||||
$classFqn = $scope->getResolvedName($classNode->classBaseClause->baseClass);
|
||||
return new Types\Object_(new Fqsen('\\' . $classFqn));
|
||||
}
|
||||
return new Types\Object_(new Fqsen('\\' . $className));
|
||||
|
@ -1057,14 +956,19 @@ class DefinitionResolver
|
|||
* 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 @@ class DefinitionResolver
|
|||
// * @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 @@ class DefinitionResolver
|
|||
// 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 @@ class DefinitionResolver
|
|||
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 @@ class DefinitionResolver
|
|||
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 @@ class DefinitionResolver
|
|||
// 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 @@ class DefinitionResolver
|
|||
|| ($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 @@ class DefinitionResolver
|
|||
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 @@ class DefinitionResolver
|
|||
* 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 @@ class DefinitionResolver
|
|||
return $name === "" ? null : $name . '()';
|
||||
}
|
||||
|
||||
if ($scope === null) {
|
||||
$scope = getScopeAtNode($this, $node);
|
||||
}
|
||||
|
||||
// INPUT OUTPUT
|
||||
// namespace A\B;
|
||||
// class C {
|
||||
|
@ -1259,18 +1163,18 @@ class DefinitionResolver
|
|||
// }
|
||||
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 @@ class DefinitionResolver
|
|||
// }
|
||||
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 @@ class DefinitionResolver
|
|||
return (string)$node->getNamespacedName();
|
||||
}
|
||||
|
||||
if ($scope->currentClassLikeVariable === null
|
||||
|| !isset($scope->currentClassLikeVariable->definitionNode->name)
|
||||
) {
|
||||
// Class constant: use ClassName::CONSTANT_NAME as name
|
||||
$classDeclaration = $constDeclaration->getFirstAncestor(
|
||||
Node\Expression\ObjectCreationExpression::class,
|
||||
PhpParser\ClassLike::class
|
||||
);
|
||||
|
||||
if (!isset($classDeclaration->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)) {
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LanguageServer\Scope;
|
||||
|
||||
use LanguageServer\DefinitionResolver;
|
||||
use Microsoft\PhpParser\ClassLike;
|
||||
use Microsoft\PhpParser\Node;
|
||||
use Microsoft\PhpParser\Node\SourceFileNode;
|
||||
use Microsoft\PhpParser\Node\Statement\FunctionDeclaration;
|
||||
|
||||
/**
|
||||
* Returns the scope at the start of $node.
|
||||
*/
|
||||
function getScopeAtNode(DefinitionResolver $definitionResolver, Node $targetNode): Scope
|
||||
{
|
||||
/** @var SourceFileNode The source file. */
|
||||
$sourceFile = $targetNode->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;
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LanguageServer\Scope;
|
||||
use Microsoft\PhpParser\Node\QualifiedName;
|
||||
|
||||
/**
|
||||
* Contains information about variables at a point.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
public $currentClassLikeVariable;
|
||||
|
||||
/**
|
||||
* @var Variable[] Variables in the scope, indexed by their names (without the dollar) and excluding $this.
|
||||
*/
|
||||
public $variables = [];
|
||||
|
||||
/**
|
||||
* @var string[] Maps unqualified names to fully qualified names.
|
||||
*/
|
||||
public $resolvedNameCache = [];
|
||||
|
||||
public function clearResolvedNameCache() {
|
||||
$this->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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LanguageServer\Scope;
|
||||
|
||||
class TraversingEndedException extends \Exception
|
||||
{
|
||||
}
|
|
@ -0,0 +1,192 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LanguageServer\Scope;
|
||||
|
||||
use LanguageServer\DefinitionResolver;
|
||||
use Microsoft\PhpParser\ClassLike;
|
||||
use Microsoft\PhpParser\FunctionLike;
|
||||
use Microsoft\PhpParser\MissingToken;
|
||||
use Microsoft\PhpParser\Node;
|
||||
use Microsoft\PhpParser\Node\Expression;
|
||||
use Microsoft\PhpParser\Token;
|
||||
use Microsoft\PhpParser\TokenKind;
|
||||
use phpDocumentor\Reflection\Fqsen;
|
||||
use phpDocumentor\Reflection\Types;
|
||||
|
||||
/**
|
||||
* Traversers AST with Scope information.
|
||||
*/
|
||||
class TreeTraverser
|
||||
{
|
||||
/**
|
||||
* Descend into the node being parsed. The default action.
|
||||
*/
|
||||
public const ACTION_CONTINUE = 0;
|
||||
|
||||
/**
|
||||
* Do not descend into the node being parsed. Traversal will continue after the node.
|
||||
*/
|
||||
public const ACTION_SKIP = 1;
|
||||
|
||||
/**
|
||||
* Stop parsing entirely. `traverse` will return immediately.
|
||||
*/
|
||||
public const ACTION_END = 2;
|
||||
|
||||
private $definitionResolver;
|
||||
|
||||
public function __construct(DefinitionResolver $definitionResolver)
|
||||
{
|
||||
$this->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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LanguageServer\Scope;
|
||||
|
||||
use phpDocumentor\Reflection\Type;
|
||||
use Microsoft\PhpParser\Node;
|
||||
|
||||
/**
|
||||
* Contains information about a single variable.
|
||||
*/
|
||||
class Variable
|
||||
{
|
||||
/**
|
||||
* @var Type
|
||||
*/
|
||||
public $type;
|
||||
|
||||
/**
|
||||
* @var Node
|
||||
*/
|
||||
public $definitionNode;
|
||||
|
||||
public function __construct(Type $type, Node $definitionNode)
|
||||
{
|
||||
$this->type = $type;
|
||||
$this->definitionNode = $definitionNode;
|
||||
}
|
||||
}
|
|
@ -214,7 +214,7 @@ class TextDocument
|
|||
}
|
||||
} 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 @@ class TextDocument
|
|||
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 @@ class TextDocument
|
|||
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 @@ class TextDocument
|
|||
return [];
|
||||
}
|
||||
// Handle definition nodes
|
||||
$fqn = DefinitionResolver::getDefinedFqn($node);
|
||||
$fqn = $this->definitionResolver->getDefinedFqn($node);
|
||||
while (true) {
|
||||
if ($fqn) {
|
||||
$def = $this->index->getDefinition($fqn);
|
||||
|
|
|
@ -126,7 +126,7 @@ class SignatureHelpProvider
|
|||
}
|
||||
|
||||
// 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);
|
||||
|
|
|
@ -4,6 +4,7 @@ declare(strict_types = 1);
|
|||
namespace LanguageServer;
|
||||
|
||||
use LanguageServer\Protocol\{SignatureInformation, ParameterInformation};
|
||||
use LanguageServer\Scope\Scope;
|
||||
use Microsoft\PhpParser\FunctionLike;
|
||||
|
||||
class SignatureInformationFactory
|
||||
|
@ -28,9 +29,9 @@ class SignatureInformationFactory
|
|||
*
|
||||
* @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 @@ class SignatureInformationFactory
|
|||
*
|
||||
* @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 .= '...';
|
||||
|
|
|
@ -5,6 +5,8 @@ namespace LanguageServer;
|
|||
|
||||
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 @@ class TreeAnalyzer
|
|||
|
||||
// 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 @@ class TreeAnalyzer
|
|||
* @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,11 +106,10 @@ class TreeAnalyzer
|
|||
}
|
||||
|
||||
// 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()) {
|
||||
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),
|
||||
|
@ -109,53 +119,19 @@ class TreeAnalyzer
|
|||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect definitions and references for the given node
|
||||
*
|
||||
* @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 @@ class TreeAnalyzer
|
|||
($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 @@ class TreeAnalyzer
|
|||
return;
|
||||
}
|
||||
|
||||
$fqn = $this->definitionResolver->resolveReferenceNodeToFqn($node);
|
||||
$fqn = $this->definitionResolver->resolveReferenceNodeToFqn($node, $scope);
|
||||
if (!$fqn) {
|
||||
return;
|
||||
}
|
||||
|
@ -181,21 +157,22 @@ class TreeAnalyzer
|
|||
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;
|
||||
}
|
||||
$fqn = (string)$classNode->classBaseClause->baseClass->getResolvedName();
|
||||
$classNode = $scope->currentClassLikeVariable->definitionNode;
|
||||
if (empty($classNode->classBaseClause)
|
||||
|| !$classNode->classBaseClause->baseClass instanceof Node\QualifiedName
|
||||
) {
|
||||
return;
|
||||
}
|
||||
$fqn = $scope->getResolvedName($classNode->classBaseClause->baseClass);
|
||||
if (!$fqn) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
]
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
},
|
||||
"containerName": "Foo"
|
||||
},
|
||||
"type__tostring": "\\",
|
||||
"type__tostring": "mixed",
|
||||
"type": {},
|
||||
"declarationLine": "protected $bar;",
|
||||
"documentation": null,
|
||||
|
|
Loading…
Reference in New Issue