1
0
Fork 0
pull/609/merge
Declspeck 2018-02-28 07:23:57 +00:00 committed by GitHub
commit a8504d9116
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 577 additions and 468 deletions

View File

@ -44,7 +44,8 @@
"files" : [ "files" : [
"src/utils.php", "src/utils.php",
"src/FqnUtilities.php", "src/FqnUtilities.php",
"src/ParserHelpers.php" "src/ParserHelpers.php",
"src/Scope/GetScopeAtNode.php"
] ]
}, },
"autoload-dev": { "autoload-dev": {

View File

@ -14,6 +14,7 @@ use LanguageServer\Protocol\{
CompletionContext, CompletionContext,
CompletionTriggerKind CompletionTriggerKind
}; };
use function LanguageServer\Scope\getScopeAtNode;
use Microsoft\PhpParser; use Microsoft\PhpParser;
use Microsoft\PhpParser\Node; use Microsoft\PhpParser\Node;
use Generator; use Generator;
@ -193,15 +194,22 @@ class CompletionProvider
// //
// $| // $|
// $a| // $a|
//
// TODO: Superglobals
// Find variables, parameters and use statements in the scope
$namePrefix = $node->getName() ?? ''; $namePrefix = $node->getName() ?? '';
foreach ($this->suggestVariablesAtNode($node, $namePrefix) as $var) { $prefixLen = strlen($namePrefix);
$scope = getScopeAtNode($this->definitionResolver, $node);
$variables = $scope->variables;
foreach ($variables as $name => $var) {
if (substr($name, 0, $prefixLen) !== $namePrefix) {
continue;
}
$item = new CompletionItem; $item = new CompletionItem;
$item->kind = CompletionItemKind::VARIABLE; $item->kind = CompletionItemKind::VARIABLE;
$item->label = '$' . $var->getName(); $item->label = '$' . $name;
$item->documentation = $this->definitionResolver->getDocumentationFromNode($var); $item->documentation = $this->definitionResolver->getDocumentationFromNode($var->definitionNode);
$item->detail = (string)$this->definitionResolver->getTypeFromNode($var); $item->detail = (string)$var->type;
$item->textEdit = new TextEdit( $item->textEdit = new TextEdit(
new Range($pos, $pos), new Range($pos, $pos),
stripStringOverlap($doc->getRange(new Range(new Position(0, 0), $pos)), $item->label) stripStringOverlap($doc->getRange(new Range(new Position(0, 0), $pos)), $item->label)
@ -408,111 +416,4 @@ class CompletionProvider
} }
} }
} }
/**
* 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 <Node\Expr\Variable|Node\Param|Node\Expr\ClosureUse>
*/
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);
}
} }

View File

@ -5,6 +5,8 @@ namespace LanguageServer;
use LanguageServer\Index\ReadableIndex; use LanguageServer\Index\ReadableIndex;
use LanguageServer\Protocol\SymbolInformation; use LanguageServer\Protocol\SymbolInformation;
use LanguageServer\Scope\Scope;
use function LanguageServer\Scope\getScopeAtNode;
use Microsoft\PhpParser; use Microsoft\PhpParser;
use Microsoft\PhpParser\Node; use Microsoft\PhpParser\Node;
use Microsoft\PhpParser\FunctionLike; use Microsoft\PhpParser\FunctionLike;
@ -175,10 +177,15 @@ class DefinitionResolver
* *
* @param Node $node * @param Node $node
* @param string $fqn * @param string $fqn
* @param Scope|null $scope Scope at the point of Node. If not provided, will be computed from $node.
* @return Definition * @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 = new Definition;
$def->fqn = $fqn; $def->fqn = $fqn;
@ -221,7 +228,7 @@ class DefinitionResolver
if ($node instanceof Node\Statement\ClassDeclaration && if ($node instanceof Node\Statement\ClassDeclaration &&
// TODO - this should be better represented in the parser API // TODO - this should be better represented in the parser API
$node->classBaseClause !== null && $node->classBaseClause->baseClass !== null) { $node->classBaseClause !== null && $node->classBaseClause->baseClass !== null) {
$def->extends = [(string)$node->classBaseClause->baseClass->getResolvedName()]; $def->extends = [$scope->getResolvedName($node->classBaseClause->baseClass)];
} elseif ( } elseif (
$node instanceof Node\Statement\InterfaceDeclaration && $node instanceof Node\Statement\InterfaceDeclaration &&
// TODO - this should be better represented in the parser API // TODO - this should be better represented in the parser API
@ -229,20 +236,20 @@ class DefinitionResolver
) { ) {
$def->extends = []; $def->extends = [];
foreach ($node->interfaceBaseClause->interfaceNameList->getValues() as $n) { foreach ($node->interfaceBaseClause->interfaceNameList->getValues() as $n) {
$def->extends[] = (string)$n->getResolvedName(); $def->extends[] = $scope->getResolvedName($n);
} }
} }
$def->symbolInformation = SymbolInformation::fromNode($node, $fqn); $def->symbolInformation = SymbolInformation::fromNode($node, $fqn);
if ($def->symbolInformation !== null) { if ($def->symbolInformation !== null) {
$def->type = $this->getTypeFromNode($node); $def->type = $this->getTypeFromNode($node, $scope);
$def->declarationLine = $this->getDeclarationLineFromNode($node); $def->declarationLine = $this->getDeclarationLineFromNode($node);
$def->documentation = $this->getDocumentationFromNode($node); $def->documentation = $this->getDocumentationFromNode($node);
} }
if ($node instanceof FunctionLike) { if ($node instanceof FunctionLike) {
$def->signatureInformation = $this->signatureInformationFactory->create($node); $def->signatureInformation = $this->signatureInformationFactory->create($node, $scope);
} }
return $def; return $def;
@ -252,56 +259,62 @@ class DefinitionResolver
* Given any node, returns the Definition object of the symbol that is referenced * Given any node, returns the Definition object of the symbol that is referenced
* *
* @param Node $node Any reference node * @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 * @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; $parent = $node->parent;
// Variables are not indexed globally, as they stay in the file scope anyway. // Variables are not indexed globally, as they stay in the file scope anyway.
// Ignore variable nodes that are part of ScopedPropertyAccessExpression, // Ignore variable nodes that are part of ScopedPropertyAccessExpression,
// as the scoped property access expression node is handled separately. // as the scoped property access expression node is handled separately.
if ($node instanceof Node\Expression\Variable && if ($node instanceof Node\Expression\Variable &&
!($parent instanceof Node\Expression\ScopedPropertyAccessExpression)) { !($parent instanceof Node\Expression\ScopedPropertyAccessExpression)) {
$name = $node->getName();
// Resolve $this to the containing class definition. // Resolve $this to the containing class definition.
if ($node->getName() === 'this' && $fqn = $this->getContainingClassFqn($node)) { if ($name === 'this') {
if ($scope->currentSelf === null) {
return null;
}
$fqn = substr((string)$scope->currentSelf->type->getFqsen(), 1);
return $this->index->getDefinition($fqn, false); return $this->index->getDefinition($fqn, false);
} }
// Resolve the variable to a definition node (assignment, param or closure use) // Resolve the variable to a definition node (assignment, param or closure use)
$defNode = $this->resolveVariableToNode($node); if (!isset($scope->variables[$name])) {
if ($defNode === null) {
return null; 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 // Other references are references to a global symbol that have an FQN
// Find out the FQN // Find out the FQN
$fqn = $this->resolveReferenceNodeToFqn($node); $fqn = $this->resolveReferenceNodeToFqn($node, $scope);
if (!$fqn) {
return null;
}
if ($fqn === 'self' || $fqn === 'static') { if ($fqn === 'self' || $fqn === 'static') {
// Resolve self and static keywords to the containing class // Resolve self and static keywords to the containing class
// (This is not 100% correct for static but better than nothing) // (This is not 100% correct for static but better than nothing)
$classNode = $node->getFirstAncestor(Node\Statement\ClassDeclaration::class); if ($scope->currentSelf === null) {
if (!$classNode) { return null;
return;
}
$fqn = (string)$classNode->getNamespacedName();
if (!$fqn) {
return;
} }
$fqn = substr((string)$scope->currentSelf->type->getFqsen(), 1);
} else if ($fqn === 'parent') { } else if ($fqn === 'parent') {
if ($scope->currentSelf === null) {
return null;
}
// Resolve parent keyword to the base class FQN // Resolve parent keyword to the base class FQN
$classNode = $node->getFirstAncestor(Node\Statement\ClassDeclaration::class); $classNode = $scope->currentSelf->definitionNode;
if (!$classNode || !$classNode->classBaseClause || !$classNode->classBaseClause->baseClass) { if (!$classNode->classBaseClause || !$classNode->classBaseClause->baseClass) {
return; return null;
}
$fqn = (string)$classNode->classBaseClause->baseClass->getResolvedName();
if (!$fqn) {
return;
} }
$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 // If the node is a function or constant, it could be namespaced, but PHP falls back to global
@ -319,15 +332,19 @@ class DefinitionResolver
* May also return "static", "self" or "parent" * May also return "static", "self" or "parent"
* *
* @param Node $node * @param Node $node
* @param Scope|null $scope Scope at the point of Node. If not provided, will be computed from $node.
* @return string|null * @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 // TODO all name tokens should be a part of a node
if ($node instanceof Node\QualifiedName) { if ($node instanceof Node\QualifiedName) {
return $this->resolveQualifiedNameNodeToFqn($node); return $this->resolveQualifiedNameNodeToFqn($node, $scope);
} else if ($node instanceof Node\Expression\MemberAccessExpression) { } else if ($node instanceof Node\Expression\MemberAccessExpression) {
return $this->resolveMemberAccessExpressionNodeToFqn($node); return $this->resolveMemberAccessExpressionNodeToFqn($node, $scope);
} else if (ParserHelpers\isConstantFetch($node)) { } else if (ParserHelpers\isConstantFetch($node)) {
return (string)($node->getNamespacedName()); return (string)($node->getNamespacedName());
} else if ( } else if (
@ -335,18 +352,18 @@ class DefinitionResolver
$node instanceof Node\Expression\ScopedPropertyAccessExpression $node instanceof Node\Expression\ScopedPropertyAccessExpression
&& !($node->memberName instanceof Node\Expression\Variable) && !($node->memberName instanceof Node\Expression\Variable)
) { ) {
return $this->resolveScopedPropertyAccessExpressionNodeToFqn($node); return $this->resolveScopedPropertyAccessExpressionNodeToFqn($node, $scope);
} else if ( } else if (
// A\B::$c - static property access expression // A\B::$c - static property access expression
$node->parent instanceof Node\Expression\ScopedPropertyAccessExpression $node->parent instanceof Node\Expression\ScopedPropertyAccessExpression
) { ) {
return $this->resolveScopedPropertyAccessExpressionNodeToFqn($node->parent); return $this->resolveScopedPropertyAccessExpressionNodeToFqn($node->parent, $scope);
} }
return null; return null;
} }
private function resolveQualifiedNameNodeToFqn(Node\QualifiedName $node) private function resolveQualifiedNameNodeToFqn(Node\QualifiedName $node, Scope $scope)
{ {
$parent = $node->parent; $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 // 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) { if ($node->parent instanceof Node\Expression\CallExpression) {
$name .= '()'; $name .= '()';
@ -393,14 +410,14 @@ class DefinitionResolver
return $name; return $name;
} }
private function resolveMemberAccessExpressionNodeToFqn(Node\Expression\MemberAccessExpression $access) private function resolveMemberAccessExpressionNodeToFqn(Node\Expression\MemberAccessExpression $access, Scope $scope)
{ {
if ($access->memberName instanceof Node\Expression) { if ($access->memberName instanceof Node\Expression) {
// Cannot get definition if right-hand side is expression // Cannot get definition if right-hand side is expression
return null; return null;
} }
// Get the type of the left-hand expression // Get the type of the left-hand expression
$varType = $this->resolveExpressionNodeToType($access->dereferencableExpression); $varType = $this->resolveExpressionNodeToType($access->dereferencableExpression, $scope);
if ($varType instanceof Types\Compound) { if ($varType instanceof Types\Compound) {
// For compound types, use the first FQN we find // For compound types, use the first FQN we find
@ -423,14 +440,18 @@ class DefinitionResolver
|| $varType instanceof Types\Self_ || $varType instanceof Types\Self_
) { ) {
// $this/static/self is resolved to the containing class // $this/static/self is resolved to the containing class
$classFqn = self::getContainingClassFqn($access); if ($scope->currentSelf === null) {
return null;
}
$classFqn = substr((string)$scope->currentSelf->type->getFqsen(), 1);
} else if (!($varType instanceof Types\Object_) || $varType->getFqsen() === null) { } else if (!($varType instanceof Types\Object_) || $varType->getFqsen() === null) {
// Left-hand expression could not be resolved to a class // Left-hand expression could not be resolved to a class
return null; return null;
} else { } else {
$classFqn = substr((string)$varType->getFqsen(), 1); $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) { if ($access->parent instanceof Node\Expression\CallExpression) {
$memberSuffix .= '()'; $memberSuffix .= '()';
} }
@ -460,23 +481,25 @@ class DefinitionResolver
return $classFqn . $memberSuffix; 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) { if ($scoped->scopeResolutionQualifier instanceof Node\Expression\Variable) {
$varType = $this->getTypeFromNode($scoped->scopeResolutionQualifier); $varType = $this->getTypeFromNode($scoped->scopeResolutionQualifier, $scope);
if ($varType === null) { if ($varType === null) {
return null; return null;
} }
$className = substr((string)$varType->getFqsen(), 1); $className = substr((string)$varType->getFqsen(), 1);
} elseif ($scoped->scopeResolutionQualifier instanceof Node\QualifiedName) { } elseif ($scoped->scopeResolutionQualifier instanceof Node\QualifiedName) {
$className = (string)$scoped->scopeResolutionQualifier->getResolvedName(); $className = $scope->getResolvedName($scoped->scopeResolutionQualifier);
} else { } else {
return null; return null;
} }
if ($className === 'self' || $className === 'static' || $className === 'parent') { if ($className === 'self' || $className === 'static' || $className === 'parent') {
// self and static are resolved to the containing class // self and static are resolved to the containing class
$classNode = $scoped->getFirstAncestor(Node\Statement\ClassDeclaration::class); $classNode = $scope->currentSelf->definitionNode ?? null;
if ($classNode === null) { if ($classNode === null) {
return null; return null;
} }
@ -485,12 +508,12 @@ class DefinitionResolver
if (!isset($classNode->extends)) { if (!isset($classNode->extends)) {
return null; return null;
} }
$className = (string)$classNode->extends->getResolvedName(); $className = $scope->getResolvedName($classNode->extends);
} else { } else {
$className = (string)$classNode->getNamespacedName(); $className = substr((string)$scope->currentSelf->type->getFqsen(), 1);
} }
} elseif ($scoped->scopeResolutionQualifier instanceof Node\QualifiedName) { } elseif ($scoped->scopeResolutionQualifier instanceof Node\QualifiedName) {
$className = $scoped->scopeResolutionQualifier->getResolvedName(); $className = $scope->getResolvedName($scoped->scopeResolutionQualifier);
} }
if ($scoped->memberName instanceof Node\Expression\Variable) { if ($scoped->memberName instanceof Node\Expression\Variable) {
if ($scoped->parent instanceof Node\Expression\CallExpression) { if ($scoped->parent instanceof Node\Expression\CallExpression) {
@ -510,146 +533,20 @@ class DefinitionResolver
return $name; 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. * Given an expression node, resolves that expression recursively to a type.
* If the type could not be resolved, returns Types\Mixed_. * 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 * @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 // PARENTHESIZED EXPRESSION
// Retrieve inner expression from parenthesized expression // Retrieve inner expression from parenthesized expression
while ($expr instanceof Node\Expression\ParenthesizedExpression) { while ($expr instanceof Node\Expression\ParenthesizedExpression) {
@ -666,20 +563,10 @@ class DefinitionResolver
// $this -> Type\this // $this -> Type\this
// $myVariable -> type of corresponding assignment expression // $myVariable -> type of corresponding assignment expression
if ($expr instanceof Node\Expression\Variable || $expr instanceof Node\UseVariableName) { if ($expr instanceof Node\Expression\Variable || $expr instanceof Node\UseVariableName) {
if ($expr->getName() === 'this') { $name = $expr->getName();
return new Types\Object_(new Fqsen('\\' . $this->getContainingClassFqn($expr))); return isset($scope->variables[$name])
} ? $scope->variables[$name]->type
// Find variable definition (parameter or assignment expression) : new Types\Mixed_;
$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);
}
} }
// FUNCTION CALL // FUNCTION CALL
@ -691,18 +578,18 @@ class DefinitionResolver
) { ) {
// Find the function definition // Find the function definition
if ($expr->callableExpression instanceof Node\Expression) { if ($expr->callableExpression instanceof Node\Expression) {
// Cannot get type for dynamic function call
return new Types\Mixed_; return new Types\Mixed_;
} }
if ($expr->callableExpression instanceof Node\QualifiedName) { if ($expr->callableExpression instanceof Node\QualifiedName) {
$fqn = $expr->callableExpression->getResolvedName() ?? $expr->callableExpression->getNamespacedName(); $fqn = $scope->getResolvedName($expr->callableExpression) ?? $expr->callableExpression->getNamespacedName();
$fqn .= '()'; $fqn .= '()';
$def = $this->index->getDefinition($fqn, true); $def = $this->index->getDefinition($fqn, true);
if ($def !== null) { if ($def !== null) {
return $def->type; return $def->type;
} }
} }
return new Types\Mixed_;
} }
// TRUE / FALSE / NULL // TRUE / FALSE / NULL
@ -716,6 +603,7 @@ class DefinitionResolver
if ($token === PhpParser\TokenKind::NullReservedWord) { if ($token === PhpParser\TokenKind::NullReservedWord) {
return new Types\Null_; return new Types\Null_;
} }
return new Types\Mixed_;
} }
// CONSTANT FETCH // CONSTANT FETCH
@ -726,6 +614,7 @@ class DefinitionResolver
if ($def !== null) { if ($def !== null) {
return $def->type; return $def->type;
} }
return new Types\Mixed_;
} }
// MEMBER CALL EXPRESSION/SCOPED PROPERTY CALL EXPRESSION // MEMBER CALL EXPRESSION/SCOPED PROPERTY CALL EXPRESSION
@ -735,7 +624,7 @@ class DefinitionResolver
$expr->callableExpression instanceof Node\Expression\MemberAccessExpression || $expr->callableExpression instanceof Node\Expression\MemberAccessExpression ||
$expr->callableExpression instanceof Node\Expression\ScopedPropertyAccessExpression) $expr->callableExpression instanceof Node\Expression\ScopedPropertyAccessExpression)
) { ) {
return $this->resolveExpressionNodeToType($expr->callableExpression); return $this->resolveExpressionNodeToType($expr->callableExpression, $scope);
} }
// MEMBER ACCESS EXPRESSION // MEMBER ACCESS EXPRESSION
@ -746,16 +635,19 @@ class DefinitionResolver
$var = $expr->dereferencableExpression; $var = $expr->dereferencableExpression;
// Resolve object // Resolve object
$objType = $this->resolveExpressionNodeToType($var); $objType = $this->resolveExpressionNodeToType($var, $scope);
if ($objType === null) {
return null;
}
if (!($objType instanceof Types\Compound)) { if (!($objType instanceof Types\Compound)) {
$objType = new Types\Compound([$objType]); $objType = new Types\Compound([$objType]);
} }
for ($i = 0; $t = $objType->get($i); $i++) { for ($i = 0; $t = $objType->get($i); $i++) {
if ($t instanceof Types\This) { if ($t instanceof Types\This) {
$classFqn = self::getContainingClassFqn($expr); if ($scope->currentSelf === null) {
if ($classFqn === null) {
return new Types\Mixed_; return new Types\Mixed_;
} }
$classFqn = substr((string)$scope->currentSelf->type->getFqsen(), 1);
} else if (!($t instanceof Types\Object_) || $t->getFqsen() === null) { } else if (!($t instanceof Types\Object_) || $t->getFqsen() === null) {
return new Types\Mixed_; return new Types\Mixed_;
} else { } else {
@ -782,7 +674,7 @@ class DefinitionResolver
// SCOPED PROPERTY ACCESS EXPRESSION // SCOPED PROPERTY ACCESS EXPRESSION
if ($expr instanceof Node\Expression\ScopedPropertyAccessExpression) { 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) { if (!($classType instanceof Types\Object_) || $classType->getFqsen() === null) {
return new Types\Mixed_; return new Types\Mixed_;
} }
@ -805,19 +697,19 @@ class DefinitionResolver
// new A() => resolves to the type of the class type designator (A) // new A() => resolves to the type of the class type designator (A)
// TODO: new $this->a => resolves to the string represented by "a" // TODO: new $this->a => resolves to the string represented by "a"
if ($expr instanceof Node\Expression\ObjectCreationExpression) { if ($expr instanceof Node\Expression\ObjectCreationExpression) {
return $this->resolveClassNameToType($expr->classTypeDesignator); return $this->resolveClassNameToType($expr->classTypeDesignator, $scope);
} }
// CLONE EXPRESSION // CLONE EXPRESSION
// clone($a) => resolves to the type of $a // clone($a) => resolves to the type of $a
if ($expr instanceof Node\Expression\CloneExpression) { if ($expr instanceof Node\Expression\CloneExpression) {
return $this->resolveExpressionNodeToType($expr->expression); return $this->resolveExpressionNodeToType($expr->expression, $scope);
} }
// ASSIGNMENT EXPRESSION // ASSIGNMENT EXPRESSION
// $a = $myExpression => resolves to the type of the right-hand operand // $a = $myExpression => resolves to the type of the right-hand operand
if ($expr instanceof Node\Expression\AssignmentExpression) { if ($expr instanceof Node\Expression\AssignmentExpression) {
return $this->resolveExpressionNodeToType($expr->rightOperand); return $this->resolveExpressionNodeToType($expr->rightOperand, $scope);
} }
// TERNARY EXPRESSION // TERNARY EXPRESSION
@ -827,14 +719,14 @@ class DefinitionResolver
// ?: // ?:
if ($expr->ifExpression === null) { if ($expr->ifExpression === null) {
return new Types\Compound([ return new Types\Compound([
$this->resolveExpressionNodeToType($expr->condition), // TODO: why? $this->resolveExpressionNodeToType($expr->condition, $scope), // TODO: why?
$this->resolveExpressionNodeToType($expr->elseExpression) $this->resolveExpressionNodeToType($expr->elseExpression, $scope)
]); ]);
} }
// Ternary is a compound of the two possible values // Ternary is a compound of the two possible values
return new Types\Compound([ return new Types\Compound([
$this->resolveExpressionNodeToType($expr->ifExpression), $this->resolveExpressionNodeToType($expr->ifExpression, $scope),
$this->resolveExpressionNodeToType($expr->elseExpression) $this->resolveExpressionNodeToType($expr->elseExpression, $scope)
]); ]);
} }
@ -843,8 +735,8 @@ class DefinitionResolver
if ($expr instanceof Node\Expression\BinaryExpression && $expr->operator->kind === PhpParser\TokenKind::QuestionQuestionToken) { if ($expr instanceof Node\Expression\BinaryExpression && $expr->operator->kind === PhpParser\TokenKind::QuestionQuestionToken) {
// ?? operator // ?? operator
return new Types\Compound([ return new Types\Compound([
$this->resolveExpressionNodeToType($expr->leftOperand), $this->resolveExpressionNodeToType($expr->leftOperand, $scope),
$this->resolveExpressionNodeToType($expr->rightOperand) $this->resolveExpressionNodeToType($expr->rightOperand, $scope)
]); ]);
} }
@ -902,8 +794,8 @@ class DefinitionResolver
) )
) { ) {
if ( if (
$this->resolveExpressionNodeToType($expr->leftOperand) instanceof Types\Integer $this->resolveExpressionNodeToType($expr->leftOperand, $scope) instanceof Types\Integer
&& $this->resolveExpressionNodeToType($expr->rightOperand) instanceof Types\Integer && $this->resolveExpressionNodeToType($expr->rightOperand, $scope) instanceof Types\Integer
) { ) {
return new Types\Integer; return new Types\Integer;
} }
@ -954,8 +846,8 @@ class DefinitionResolver
$keyTypes = []; $keyTypes = [];
if ($expr->arrayElements !== null) { if ($expr->arrayElements !== null) {
foreach ($expr->arrayElements->getElements() as $item) { foreach ($expr->arrayElements->getElements() as $item) {
$valueTypes[] = $this->resolveExpressionNodeToType($item->elementValue); $valueTypes[] = $this->resolveExpressionNodeToType($item->elementValue, $scope);
$keyTypes[] = $item->elementKey ? $this->resolveExpressionNodeToType($item->elementKey) : new Types\Integer; $keyTypes[] = $item->elementKey ? $this->resolveExpressionNodeToType($item->elementKey, $scope) : new Types\Integer;
} }
} }
$valueTypes = array_unique($valueTypes); $valueTypes = array_unique($valueTypes);
@ -981,7 +873,7 @@ class DefinitionResolver
// $myArray[3] // $myArray[3]
// $myArray{"hello"} // $myArray{"hello"}
if ($expr instanceof Node\Expression\SubscriptExpression) { if ($expr instanceof Node\Expression\SubscriptExpression) {
$varType = $this->resolveExpressionNodeToType($expr->postfixExpression); $varType = $this->resolveExpressionNodeToType($expr->postfixExpression, $scope);
if (!($varType instanceof Types\Array_)) { if (!($varType instanceof Types\Array_)) {
return new Types\Mixed_; return new Types\Mixed_;
} }
@ -996,7 +888,7 @@ class DefinitionResolver
} }
if ($expr instanceof Node\QualifiedName) { if ($expr instanceof Node\QualifiedName) {
return $this->resolveClassNameToType($expr); return $this->resolveClassNameToType($expr, $scope);
} }
return new Types\Mixed_; return new Types\Mixed_;
@ -1010,9 +902,9 @@ class DefinitionResolver
* @param Node|PhpParser\Token $class * @param Node|PhpParser\Token $class
* @return Type * @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_; return new Types\Mixed_;
} }
if ($class instanceof PhpParser\Token && $class->kind === PhpParser\TokenKind::ClassKeyword) { if ($class instanceof PhpParser\Token && $class->kind === PhpParser\TokenKind::ClassKeyword) {
@ -1023,23 +915,28 @@ class DefinitionResolver
// `new static` // `new static`
return new Types\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') { if ($className === 'self') {
$classNode = $class->getFirstAncestor(Node\Statement\ClassDeclaration::class); if ($scope->currentSelf === null) {
if ($className === 'parent') { return new Types\Self_;
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();
} }
return $scope->currentSelf->type;
} else if ($className === 'parent') {
if ($scope->currentSelf === null) {
return new Types\Object_;
}
$classNode = $scope->currentSelf->definitionNode;
if (empty($classNode->classBaseClause)
|| !$classNode->classBaseClause->baseClass instanceof Node\QualifiedName
) {
return new Types\Object_;
}
// parent is resolved to the parent class
$classFqn = $scope->getResolvedName($classNode->classBaseClause->baseClass);
return new Types\Object_(new Fqsen('\\' . $classFqn)); return new Types\Object_(new Fqsen('\\' . $classFqn));
} }
return new Types\Object_(new Fqsen('\\' . $className)); return new Types\Object_(new Fqsen('\\' . $className));
@ -1057,14 +954,19 @@ class DefinitionResolver
* Returns null if the node does not have a type. * Returns null if the node does not have a type.
* *
* @param Node $node * @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 * @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)) { if (ParserHelpers\isConstDefineExpression($node)) {
// constants with define() like // constants with define() like
// define('TEST_DEFINE_CONSTANT', false); // define('TEST_DEFINE_CONSTANT', false);
return $this->resolveExpressionNodeToType($node->argumentExpressionList->children[2]->expression); return $this->resolveExpressionNodeToType($node->argumentExpressionList->children[2]->expression, $scope);
} }
// PARAMETERS // PARAMETERS
@ -1078,10 +980,12 @@ class DefinitionResolver
// * @param MyClass $myParam // * @param MyClass $myParam
// */ // */
// function foo($a) // function foo($a)
$functionLikeDeclaration = ParserHelpers\getFunctionLikeDeclarationFromParameter($node);
$variableName = $node->getName(); $variableName = $node->getName();
if (isset($scope->variables[$variableName])) {
return $scope->variables[$variableName]->type;
}
$functionLikeDeclaration = ParserHelpers\getFunctionLikeDeclarationFromParameter($node);
$docBlock = $this->getDocBlock($functionLikeDeclaration); $docBlock = $this->getDocBlock($functionLikeDeclaration);
$parameterDocBlockTag = $this->tryGetDocBlockTagForParameter($docBlock, $variableName); $parameterDocBlockTag = $this->tryGetDocBlockTagForParameter($docBlock, $variableName);
if ($parameterDocBlockTag !== null && ($type = $parameterDocBlockTag->getType())) { if ($parameterDocBlockTag !== null && ($type = $parameterDocBlockTag->getType())) {
// Doc block comments supercede all other forms of type inference // Doc block comments supercede all other forms of type inference
@ -1095,12 +999,12 @@ class DefinitionResolver
// Resolve a string like "bool" to a type object // Resolve a string like "bool" to a type object
$type = $this->typeResolver->resolve($node->typeDeclaration->getText($node->getFileContents())); $type = $this->typeResolver->resolve($node->typeDeclaration->getText($node->getFileContents()));
} else { } 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) // function foo($a = 3)
if ($node->default !== null) { if ($node->default !== null) {
$defaultType = $this->resolveExpressionNodeToType($node->default); $defaultType = $this->resolveExpressionNodeToType($node->default, $scope);
if (isset($type) && !is_a($type, get_class($defaultType))) { if (isset($type) && !is_a($type, get_class($defaultType))) {
// TODO - verify it is worth creating a compound type // TODO - verify it is worth creating a compound type
return new Types\Compound([$type, $defaultType]); return new Types\Compound([$type, $defaultType]);
@ -1121,15 +1025,11 @@ class DefinitionResolver
if ( if (
$docBlock !== null $docBlock !== null
&& !empty($returnTags = $docBlock->getTagsByName('return')) && !empty($returnTags = $docBlock->getTagsByName('return'))
&& $returnTags[0]->getType() !== null && ($returnType = $returnTags[0]->getType()) !== null
) { ) {
// Use @return tag // Use @return tag
$returnType = $returnTags[0]->getType(); if ($returnType instanceof Types\Self_ && null !== $scope->currentSelf) {
if ($returnType instanceof Types\Self_) { return $scope->currentSelf->type;
$selfType = $this->getContainingClassType($node);
if ($selfType) {
return $selfType;
}
} }
return $returnType; return $returnType;
} }
@ -1138,13 +1038,10 @@ class DefinitionResolver
if ($node->returnType instanceof PhpParser\Token) { if ($node->returnType instanceof PhpParser\Token) {
// Resolve a string like "bool" to a type object // Resolve a string like "bool" to a type object
return $this->typeResolver->resolve($node->returnType->getText($node->getFileContents())); return $this->typeResolver->resolve($node->returnType->getText($node->getFileContents()));
} elseif ($node->returnType->getResolvedName() === 'self') { } else if ($scope->currentSelf !== null && $scope->getResolvedName($node->returnType) === 'self') {
$selfType = $this->getContainingClassType($node); return $scope->currentSelf->type;
if ($selfType !== null) {
return $selfType;
}
} }
return new Types\Object_(new Fqsen('\\' . (string)$node->returnType->getResolvedName())); return new Types\Object_(new Fqsen('\\' . $scope->getResolvedName($node->returnType)));
} }
// Unknown return type // Unknown return type
return new Types\Mixed_; return new Types\Mixed_;
@ -1153,7 +1050,7 @@ class DefinitionResolver
// FOREACH KEY/VARIABLE // FOREACH KEY/VARIABLE
if ($node instanceof Node\ForeachKey || $node->parent instanceof Node\ForeachKey) { if ($node instanceof Node\ForeachKey || $node->parent instanceof Node\ForeachKey) {
$foreach = $node->getFirstAncestor(Node\Statement\ForeachStatement::class); $foreach = $node->getFirstAncestor(Node\Statement\ForeachStatement::class);
$collectionType = $this->resolveExpressionNodeToType($foreach->forEachCollectionName); $collectionType = $this->resolveExpressionNodeToType($foreach->forEachCollectionName, $scope);
if ($collectionType instanceof Types\Array_) { if ($collectionType instanceof Types\Array_) {
return $collectionType->getKeyType(); return $collectionType->getKeyType();
} }
@ -1165,7 +1062,7 @@ class DefinitionResolver
|| ($node instanceof Node\Expression\Variable && $node->parent instanceof Node\ForeachValue) || ($node instanceof Node\Expression\Variable && $node->parent instanceof Node\ForeachValue)
) { ) {
$foreach = $node->getFirstAncestor(Node\Statement\ForeachStatement::class); $foreach = $node->getFirstAncestor(Node\Statement\ForeachStatement::class);
$collectionType = $this->resolveExpressionNodeToType($foreach->forEachCollectionName); $collectionType = $this->resolveExpressionNodeToType($foreach->forEachCollectionName, $scope);
if ($collectionType instanceof Types\Array_) { if ($collectionType instanceof Types\Array_) {
return $collectionType->getValueType(); return $collectionType->getValueType();
} }
@ -1196,12 +1093,12 @@ class DefinitionResolver
if ($declarationNode instanceof Node\PropertyDeclaration) { if ($declarationNode instanceof Node\PropertyDeclaration) {
// TODO should have default // TODO should have default
if (isset($node->parent->rightOperand)) { if (isset($node->parent->rightOperand)) {
return $this->resolveExpressionNodeToType($node->parent->rightOperand); return $this->resolveExpressionNodeToType($node->parent->rightOperand, $scope);
} }
} else if ($node instanceof Node\ConstElement) { } else if ($node instanceof Node\ConstElement) {
return $this->resolveExpressionNodeToType($node->assignment); return $this->resolveExpressionNodeToType($node->assignment, $scope);
} else if ($node instanceof Node\Expression\AssignmentExpression) { } 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: read @property tags of class
// TODO: Try to infer the type from default value / constant value // TODO: Try to infer the type from default value / constant value
@ -1218,9 +1115,10 @@ class DefinitionResolver
* Returns null if the node does not declare any symbol that can be referenced by an FQN * Returns null if the node does not declare any symbol that can be referenced by an FQN
* *
* @param Node $node * @param Node $node
* @param Scope|null $scope Scope at the point of Node. If not provided, will be computed from $node.
* @return string|null * @return string|null
*/ */
public static function getDefinedFqn($node) public function getDefinedFqn($node, Scope $scope = null)
{ {
$parent = $node->parent; $parent = $node->parent;
// Anonymous classes don't count as a definition // Anonymous classes don't count as a definition
@ -1251,6 +1149,10 @@ class DefinitionResolver
return $name === "" ? null : $name . '()'; return $name === "" ? null : $name . '()';
} }
if ($scope === null) {
$scope = getScopeAtNode($this, $node);
}
// INPUT OUTPUT // INPUT OUTPUT
// namespace A\B; // namespace A\B;
// class C { // class C {
@ -1259,18 +1161,18 @@ class DefinitionResolver
// } // }
if ($node instanceof Node\MethodDeclaration) { if ($node instanceof Node\MethodDeclaration) {
// Class method: use ClassName->methodName() as name // Class method: use ClassName->methodName() as name
$class = $node->getFirstAncestor( if ($scope->currentSelf === null) {
Node\Expression\ObjectCreationExpression::class, return;
PhpParser\ClassLike::class }
); $className = substr((string)$scope->currentSelf->type->getFqsen(), 1);
if (!isset($class->name)) { if (!$className) {
// Ignore anonymous classes // Ignore anonymous classes
return null; return null;
} }
if ($node->isStatic()) { if ($node->isStatic()) {
return (string)$class->getNamespacedName() . '::' . $node->getName() . '()'; return $className . '::' . $node->getName() . '()';
} else { } else {
return (string)$class->getNamespacedName() . '->' . $node->getName() . '()'; return $className . '->' . $node->getName() . '()';
} }
} }
@ -1282,20 +1184,18 @@ class DefinitionResolver
// } // }
if ( if (
($propertyDeclaration = ParserHelpers\tryGetPropertyDeclaration($node)) !== null && ($propertyDeclaration = ParserHelpers\tryGetPropertyDeclaration($node)) !== null &&
($classDeclaration = $scope->currentSelf !== null &&
$node->getFirstAncestor( isset($scope->currentSelf->definitionNode->name)
Node\Expression\ObjectCreationExpression::class, ) {
PhpParser\ClassLike::class $className = substr((string)$scope->currentSelf->type->getFqsen(), 1);
)
) !== null && isset($classDeclaration->name)) {
$name = $node->getName(); $name = $node->getName();
if ($propertyDeclaration->isStatic()) { if ($propertyDeclaration->isStatic()) {
// Static Property: use ClassName::$propertyName as name // Static Property: use ClassName::$propertyName as name
return (string)$classDeclaration->getNamespacedName() . '::$' . $name; return $className . '::$' . $name;
} }
// Instance Property: use ClassName->propertyName as name // Instance Property: use ClassName->propertyName as name
return (string)$classDeclaration->getNamespacedName() . '->' . $name; return $className . '->' . $name;
} }
// INPUT OUTPUT // INPUT OUTPUT
@ -1310,16 +1210,13 @@ class DefinitionResolver
return (string)$node->getNamespacedName(); return (string)$node->getNamespacedName();
} }
// Class constant: use ClassName::CONSTANT_NAME as name if ($scope->currentSelf === null || !isset($scope->currentSelf->definitionNode->name)
$classDeclaration = $constDeclaration->getFirstAncestor( ) {
Node\Expression\ObjectCreationExpression::class, // Class constant: use ClassName::CONSTANT_NAME as name
PhpParser\ClassLike::class
);
if (!isset($classDeclaration->name)) {
return null; return null;
} }
return (string)$classDeclaration->getNamespacedName() . '::' . $node->getName(); $className = substr((string)$scope->currentSelf->type->getFqsen(), 1);
return $className . '::' . $node->getName();
} }
if (ParserHelpers\isConstDefineExpression($node)) { if (ParserHelpers\isConstDefineExpression($node)) {

View File

@ -0,0 +1,57 @@
<?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;
}

45
src/Scope/Scope.php Normal file
View File

@ -0,0 +1,45 @@
<?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 "Variable" representing this/self
*/
public $currentSelf;
/**
* @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;
}
}

View File

@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
namespace LanguageServer\Scope;
class TraversingEndedException extends \Exception
{
}

193
src/Scope/TreeTraverser.php Normal file
View File

@ -0,0 +1,193 @@
<?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.
*/
const ACTION_CONTINUE = 0;
/**
* Do not descend into the node being parsed. Traversal will continue after the node.
*/
const ACTION_SKIP = 1;
/**
* Stop parsing entirely. `traverse` will return immediately.
*/
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->currentSelf = $scope->currentSelf;
$childScope->resolvedNameCache = $scope->resolvedNameCache;
$isStatic = $node instanceof Node\MethodDeclaration ? $node->isStatic() : !empty($node->staticModifier);
if (!$isStatic && isset($scope->variables['this'])) {
$childScope->variables['this'] = $scope->variables['this'];
}
if ($node->parameters !== null) {
foreach ($node->parameters->getElements() as $param) {
$childScope->variables[$param->getName()] = new Variable(
// Pass the child scope when getting parameters - the outer scope cannot affect
// any parameters of the function declaration.
$this->definitionResolver->getTypeFromNode($param, $childScope),
$param
);
}
}
if ($node instanceof Node\Expression\AnonymousFunctionCreationExpression
&& $node->anonymousFunctionUseClause !== null
&& $node->anonymousFunctionUseClause->useVariableNameList !== null) {
foreach ($node->anonymousFunctionUseClause->useVariableNameList->getElements() as $use) {
$name = $use->getName();
// Used variable in an anonymous function. Same as parent type, Mixed if not defined in parent.
$childScope->variables[$name] = new Variable(
isset($scope->variables[$name]) ? $scope->variables[$name]->type : new Types\Mixed_,
$use
);
}
}
return $childScope;
}
if ($node instanceof ClassLike
&& (in_array($childName, ['classMembers', 'interfaceMembers','traitMembers'], true))
) {
$childScope = new Scope;
$childScope->resolvedNameCache = $scope->resolvedNameCache;
$thisVar = new Variable(
new Types\Object_(new Fqsen('\\' . (string)$node->getNamespacedName())),
$node
);
$childScope->variables['this'] = $thisVar;
$childScope->currentSelf = $thisVar;
return $childScope;
}
return $scope;
}
/**
* Adds any variables declared by $node to $scope.
*
* Note that functions like extract and parse_str are not handled.
*
* @return void
*/
private function modifyScopeAfterNode(Node $node, Scope $scope)
{
if ($node instanceof Expression\AssignmentExpression) {
if ($node->operator->kind !== TokenKind::EqualsToken
|| !$node->leftOperand instanceof Expression\Variable
|| $node->rightOperand === null
|| $node->rightOperand instanceof MissingToken
) {
return;
}
$scope->variables[$node->leftOperand->getName()] = new Variable(
$this->definitionResolver->resolveExpressionNodeToType($node->rightOperand, $scope),
$node
);
} else if (($node instanceof Node\ForeachValue || $node instanceof Node\ForeachKey)
&& $node->expression instanceof Node\Expression\Variable
) {
$scope->variables[$node->expression->getName()] = new Variable(
$this->definitionResolver->getTypeFromNode($node, $scope),
$node
);
} else if ($node instanceof Node\Statement\NamespaceDefinition) {
// After a new namespace A\B;, the current alias table is flushed.
$scope->clearResolvedNameCache();
}
// TODO: Handle use (&$x) when $x is not defined in scope.
// TODO: Handle list(...) = $a; and [...] = $a;
// TODO: Handle foreach ($a as list(...)) and foreach ($a as [...])
// TODO: Handle unset($var)
// TODO: Handle global $var
}
}

29
src/Scope/Variable.php Normal file
View File

@ -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;
}
}

View File

@ -214,7 +214,7 @@ class TextDocument
} }
} else { } else {
// Definition with a global FQN // Definition with a global FQN
$fqn = DefinitionResolver::getDefinedFqn($node); $fqn = $this->definitionResolver->getDefinedFqn($node);
// Wait until indexing finished // Wait until indexing finished
if (!$this->index->isComplete()) { if (!$this->index->isComplete()) {
@ -277,7 +277,7 @@ class TextDocument
return []; return [];
} }
// Handle definition nodes // Handle definition nodes
$fqn = DefinitionResolver::getDefinedFqn($node); $fqn = $this->definitionResolver->getDefinedFqn($node);
while (true) { while (true) {
if ($fqn) { if ($fqn) {
$def = $this->index->getDefinition($fqn); $def = $this->index->getDefinition($fqn);
@ -318,7 +318,7 @@ class TextDocument
if ($node === null) { if ($node === null) {
return new Hover([]); return new Hover([]);
} }
$definedFqn = DefinitionResolver::getDefinedFqn($node); $definedFqn = $this->definitionResolver->getDefinedFqn($node);
while (true) { while (true) {
if ($definedFqn) { if ($definedFqn) {
// Support hover for definitions // Support hover for definitions
@ -392,7 +392,7 @@ class TextDocument
return []; return [];
} }
// Handle definition nodes // Handle definition nodes
$fqn = DefinitionResolver::getDefinedFqn($node); $fqn = $this->definitionResolver->getDefinedFqn($node);
while (true) { while (true) {
if ($fqn) { if ($fqn) {
$def = $this->index->getDefinition($fqn); $def = $this->index->getDefinition($fqn);

View File

@ -126,7 +126,7 @@ class SignatureHelpProvider
} }
// Now find the definition of the call // Now find the definition of the call
$fqn = $fqn ?: DefinitionResolver::getDefinedFqn($callingNode); $fqn = $fqn ?: $this->definitionResolver->getDefinedFqn($callingNode);
while (true) { while (true) {
if ($fqn) { if ($fqn) {
$def = $this->index->getDefinition($fqn); $def = $this->index->getDefinition($fqn);

View File

@ -4,6 +4,7 @@ declare(strict_types = 1);
namespace LanguageServer; namespace LanguageServer;
use LanguageServer\Protocol\{SignatureInformation, ParameterInformation}; use LanguageServer\Protocol\{SignatureInformation, ParameterInformation};
use LanguageServer\Scope\Scope;
use Microsoft\PhpParser\FunctionLike; use Microsoft\PhpParser\FunctionLike;
class SignatureInformationFactory class SignatureInformationFactory
@ -28,9 +29,9 @@ class SignatureInformationFactory
* *
* @return SignatureInformation * @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); $label = $this->createLabel($params);
return new SignatureInformation( return new SignatureInformation(
$label, $label,
@ -46,12 +47,12 @@ class SignatureInformationFactory
* *
* @return ParameterInformation[] * @return ParameterInformation[]
*/ */
private function createParameters(FunctionLike $node): array private function createParameters(FunctionLike $node, Scope $scope = null): array
{ {
$params = []; $params = [];
if ($node->parameters) { if ($node->parameters) {
foreach ($node->parameters->getElements() as $element) { foreach ($node->parameters->getElements() as $element) {
$param = (string) $this->definitionResolver->getTypeFromNode($element); $param = (string) $this->definitionResolver->getTypeFromNode($element, $scope);
$param .= ' '; $param .= ' ';
if ($element->dotDotDotToken) { if ($element->dotDotDotToken) {
$param .= '...'; $param .= '...';

View File

@ -5,6 +5,8 @@ namespace LanguageServer;
use LanguageServer\Protocol\{Diagnostic, DiagnosticSeverity, Range, Position, TextEdit}; use LanguageServer\Protocol\{Diagnostic, DiagnosticSeverity, Range, Position, TextEdit};
use LanguageServer\Index\Index; use LanguageServer\Index\Index;
use LanguageServer\Scope\Scope;
use LanguageServer\Scope\TreeTraverser;
use phpDocumentor\Reflection\DocBlockFactory; use phpDocumentor\Reflection\DocBlockFactory;
use Sabre\Uri; use Sabre\Uri;
use Microsoft\PhpParser; use Microsoft\PhpParser;
@ -56,7 +58,16 @@ class TreeAnalyzer
// TODO - docblock errors // 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 * @param Node|Token $node
* @return void * @return void
*/ */
private function collectDiagnostics($node) private function collectDiagnostics($node, Scope $scope)
{ {
// Get errors from the parser. // Get errors from the parser.
if (($error = PhpParser\DiagnosticsProvider::checkDiagnostics($node)) !== null) { if (($error = PhpParser\DiagnosticsProvider::checkDiagnostics($node)) !== null) {
@ -95,52 +106,17 @@ class TreeAnalyzer
} }
// Check for invalid usage of $this. // Check for invalid usage of $this.
if ($node instanceof Node\Expression\Variable && $node->getName() === 'this') { if ($node instanceof Node\Expression\Variable &&
// Find the first ancestor that's a class method. Return an error !isset($scope->variables['this']) &&
// if there is none, or if the method is static. $node->getName() === 'this'
$method = $node->getFirstAncestor(Node\MethodDeclaration::class); ) {
if ($method && $method->isStatic()) { $this->diagnostics[] = new Diagnostic(
$this->diagnostics[] = new Diagnostic( "\$this can not be used in static methods.",
"\$this can not be used in static methods.", Range::fromNode($node),
Range::fromNode($node), null,
null, DiagnosticSeverity::ERROR,
DiagnosticSeverity::ERROR, 'php'
'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);
}
}
} }
} }
@ -149,13 +125,13 @@ class TreeAnalyzer
* *
* @param Node $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) // Only index definitions with an FQN (no variables)
if ($fqn !== null) { if ($fqn !== null) {
$this->definitionNodes[$fqn] = $node; $this->definitionNodes[$fqn] = $node;
$this->definitions[$fqn] = $this->definitionResolver->createDefinitionFromNode($node, $fqn); $this->definitions[$fqn] = $this->definitionResolver->createDefinitionFromNode($node, $fqn, $scope);
} else { } else {
$parent = $node->parent; $parent = $node->parent;
@ -165,7 +141,7 @@ class TreeAnalyzer
($node instanceof Node\Expression\ScopedPropertyAccessExpression || ($node instanceof Node\Expression\ScopedPropertyAccessExpression ||
$node instanceof Node\Expression\MemberAccessExpression) $node instanceof Node\Expression\MemberAccessExpression)
&& !( && !(
$node->parent instanceof Node\Expression\CallExpression || $parent instanceof Node\Expression\CallExpression ||
$node->memberName instanceof PhpParser\Token $node->memberName instanceof PhpParser\Token
)) ))
|| ($parent instanceof Node\Statement\NamespaceDefinition && $parent->name !== null && $parent->name->getStart() === $node->getStart()) || ($parent instanceof Node\Statement\NamespaceDefinition && $parent->name !== null && $parent->name->getStart() === $node->getStart())
@ -173,7 +149,7 @@ class TreeAnalyzer
return; return;
} }
$fqn = $this->definitionResolver->resolveReferenceNodeToFqn($node); $fqn = $this->definitionResolver->resolveReferenceNodeToFqn($node, $scope);
if (!$fqn) { if (!$fqn) {
return; return;
} }
@ -181,21 +157,22 @@ class TreeAnalyzer
if ($fqn === 'self' || $fqn === 'static') { if ($fqn === 'self' || $fqn === 'static') {
// Resolve self and static keywords to the containing class // Resolve self and static keywords to the containing class
// (This is not 100% correct for static but better than nothing) // (This is not 100% correct for static but better than nothing)
$classNode = $node->getFirstAncestor(Node\Statement\ClassDeclaration::class); if (!$scope->currentSelf) {
if (!$classNode) {
return;
}
$fqn = (string)$classNode->getNamespacedName();
if (!$fqn) {
return; return;
} }
$fqn = substr((string)$scope->currentSelf->type->getFqsen(), 1);
} else if ($fqn === 'parent') { } else if ($fqn === 'parent') {
// Resolve parent keyword to the base class FQN // Resolve parent keyword to the base class FQN
$classNode = $node->getFirstAncestor(Node\Statement\ClassDeclaration::class); if ($scope->currentSelf === null) {
if (!$classNode || !$classNode->classBaseClause || !$classNode->classBaseClause->baseClass) {
return; return;
} }
$fqn = (string)$classNode->classBaseClause->baseClass->getResolvedName(); $classNode = $scope->currentSelf->definitionNode;
if (empty($classNode->classBaseClause)
|| !$classNode->classBaseClause->baseClass instanceof Node\QualifiedName
) {
return;
}
$fqn = $scope->getResolvedName($classNode->classBaseClause->baseClass);
if (!$fqn) { if (!$fqn) {
return; return;
} }

View File

@ -45,11 +45,11 @@
"declarationLine": "function b ($a = MY_CONSTANT);", "declarationLine": "function b ($a = MY_CONSTANT);",
"documentation": null, "documentation": null,
"signatureInformation": { "signatureInformation": {
"label": "(\\MY_CONSTANT $a = MY_CONSTANT)", "label": "(mixed $a = MY_CONSTANT)",
"documentation": null, "documentation": null,
"parameters": [ "parameters": [
{ {
"label": "\\MY_CONSTANT $a = MY_CONSTANT", "label": "mixed $a = MY_CONSTANT",
"documentation": null "documentation": null
} }
] ]

View File

@ -43,7 +43,7 @@
}, },
"containerName": "Foo" "containerName": "Foo"
}, },
"type__tostring": "\\", "type__tostring": "mixed",
"type": {}, "type": {},
"declarationLine": "protected $bar;", "declarationLine": "protected $bar;",
"documentation": null, "documentation": null,