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" : [
"src/utils.php",
"src/FqnUtilities.php",
"src/ParserHelpers.php"
"src/ParserHelpers.php",
"src/Scope/GetScopeAtNode.php"
]
},
"autoload-dev": {

View File

@ -14,6 +14,7 @@ use LanguageServer\Protocol\{
CompletionContext,
CompletionTriggerKind
};
use function LanguageServer\Scope\getScopeAtNode;
use Microsoft\PhpParser;
use Microsoft\PhpParser\Node;
use Generator;
@ -193,15 +194,22 @@ class CompletionProvider
//
// $|
// $a|
//
// TODO: Superglobals
// Find variables, parameters and use statements in the scope
$namePrefix = $node->getName() ?? '';
foreach ($this->suggestVariablesAtNode($node, $namePrefix) as $var) {
$prefixLen = strlen($namePrefix);
$scope = getScopeAtNode($this->definitionResolver, $node);
$variables = $scope->variables;
foreach ($variables as $name => $var) {
if (substr($name, 0, $prefixLen) !== $namePrefix) {
continue;
}
$item = new CompletionItem;
$item->kind = CompletionItemKind::VARIABLE;
$item->label = '$' . $var->getName();
$item->documentation = $this->definitionResolver->getDocumentationFromNode($var);
$item->detail = (string)$this->definitionResolver->getTypeFromNode($var);
$item->label = '$' . $name;
$item->documentation = $this->definitionResolver->getDocumentationFromNode($var->definitionNode);
$item->detail = (string)$var->type;
$item->textEdit = new TextEdit(
new Range($pos, $pos),
stripStringOverlap($doc->getRange(new Range(new Position(0, 0), $pos)), $item->label)
@ -408,111 +416,4 @@ 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\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->currentSelf === null) {
return null;
}
$fqn = substr((string)$scope->currentSelf->type->getFqsen(), 1);
return $this->index->getDefinition($fqn, false);
}
// Resolve the variable to a definition node (assignment, param or closure use)
$defNode = $this->resolveVariableToNode($node);
if ($defNode === null) {
if (!isset($scope->variables[$name])) {
return null;
}
return $this->createDefinitionFromNode($defNode);
return $this->createDefinitionFromNode($scope->variables[$name]->definitionNode, null, $scope);
}
// Other references are references to a global symbol that have an FQN
// Find out the FQN
$fqn = $this->resolveReferenceNodeToFqn($node);
if (!$fqn) {
return null;
}
$fqn = $this->resolveReferenceNodeToFqn($node, $scope);
if ($fqn === 'self' || $fqn === 'static') {
// Resolve self and static keywords to the containing class
// (This is not 100% correct for static but better than nothing)
$classNode = $node->getFirstAncestor(Node\Statement\ClassDeclaration::class);
if (!$classNode) {
return;
}
$fqn = (string)$classNode->getNamespacedName();
if (!$fqn) {
return;
if ($scope->currentSelf === null) {
return null;
}
$fqn = substr((string)$scope->currentSelf->type->getFqsen(), 1);
} else if ($fqn === 'parent') {
// Resolve parent keyword to the base class FQN
$classNode = $node->getFirstAncestor(Node\Statement\ClassDeclaration::class);
if (!$classNode || !$classNode->classBaseClause || !$classNode->classBaseClause->baseClass) {
return;
if ($scope->currentSelf === null) {
return null;
}
$fqn = (string)$classNode->classBaseClause->baseClass->getResolvedName();
// Resolve parent keyword to the base class FQN
$classNode = $scope->currentSelf->definitionNode;
if (!$classNode->classBaseClause || !$classNode->classBaseClause->baseClass) {
return null;
}
$fqn = $scope->getResolvedName($classNode->classBaseClause->baseClass);
}
if (!$fqn) {
return;
}
}
// If the node is a function or constant, it could be namespaced, but PHP falls back to global
// 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,14 @@ 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 +440,18 @@ class DefinitionResolver
|| $varType instanceof Types\Self_
) {
// $this/static/self is resolved to the containing class
$classFqn = self::getContainingClassFqn($access);
if ($scope->currentSelf === null) {
return null;
}
$classFqn = substr((string)$scope->currentSelf->type->getFqsen(), 1);
} else if (!($varType instanceof Types\Object_) || $varType->getFqsen() === null) {
// Left-hand expression could not be resolved to a class
return null;
} else {
$classFqn = substr((string)$varType->getFqsen(), 1);
}
$memberSuffix = '->' . (string)($access->memberName->getText() ?? $access->memberName->getText($access->getFileContents()));
$memberSuffix = '->' . (string)($access->memberName->getText()
?? $access->memberName->getText($access->getFileContents()));
if ($access->parent instanceof Node\Expression\CallExpression) {
$memberSuffix .= '()';
}
@ -460,23 +481,25 @@ 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->currentSelf->definitionNode ?? null;
if ($classNode === null) {
return null;
}
@ -485,12 +508,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->currentSelf->type->getFqsen(), 1);
}
} elseif ($scoped->scopeResolutionQualifier instanceof Node\QualifiedName) {
$className = $scoped->scopeResolutionQualifier->getResolvedName();
$className = $scope->getResolvedName($scoped->scopeResolutionQualifier);
}
if ($scoped->memberName instanceof Node\Expression\Variable) {
if ($scoped->parent instanceof Node\Expression\CallExpression) {
@ -510,146 +533,20 @@ 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 +563,10 @@ 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();
return isset($scope->variables[$name])
? $scope->variables[$name]->type
: new Types\Mixed_;
}
// FUNCTION CALL
@ -691,18 +578,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 +603,7 @@ class DefinitionResolver
if ($token === PhpParser\TokenKind::NullReservedWord) {
return new Types\Null_;
}
return new Types\Mixed_;
}
// CONSTANT FETCH
@ -726,6 +614,7 @@ class DefinitionResolver
if ($def !== null) {
return $def->type;
}
return new Types\Mixed_;
}
// MEMBER CALL EXPRESSION/SCOPED PROPERTY CALL EXPRESSION
@ -735,7 +624,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 +635,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->currentSelf === null) {
return new Types\Mixed_;
}
$classFqn = substr((string)$scope->currentSelf->type->getFqsen(), 1);
} else if (!($t instanceof Types\Object_) || $t->getFqsen() === null) {
return new Types\Mixed_;
} else {
@ -782,7 +674,7 @@ 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 +697,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 +719,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 +735,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 +794,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 +846,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 +873,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 +888,7 @@ class DefinitionResolver
}
if ($expr instanceof Node\QualifiedName) {
return $this->resolveClassNameToType($expr);
return $this->resolveClassNameToType($expr, $scope);
}
return new Types\Mixed_;
@ -1010,9 +902,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 +915,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->currentSelf === null) {
return new Types\Self_;
}
return $scope->currentSelf->type;
} else if ($className === 'parent') {
if ($scope->currentSelf === null) {
return new Types\Object_;
}
$classNode = $scope->currentSelf->definitionNode;
if (empty($classNode->classBaseClause)
|| !$classNode->classBaseClause->baseClass instanceof Node\QualifiedName
) {
return new Types\Object_;
}
// parent is resolved to the parent class
$classFqn = (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 +954,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 +980,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 +999,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 +1025,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->currentSelf) {
return $scope->currentSelf->type;
}
return $returnType;
}
@ -1138,13 +1038,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->currentSelf !== null && $scope->getResolvedName($node->returnType) === 'self') {
return $scope->currentSelf->type;
}
}
return new Types\Object_(new Fqsen('\\' . (string)$node->returnType->getResolvedName()));
return new Types\Object_(new Fqsen('\\' . $scope->getResolvedName($node->returnType)));
}
// Unknown return type
return new Types\Mixed_;
@ -1153,7 +1050,7 @@ 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 +1062,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 +1093,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 +1115,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 +1149,10 @@ class DefinitionResolver
return $name === "" ? null : $name . '()';
}
if ($scope === null) {
$scope = getScopeAtNode($this, $node);
}
// INPUT OUTPUT
// namespace A\B;
// class C {
@ -1259,18 +1161,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->currentSelf === null) {
return;
}
$className = substr((string)$scope->currentSelf->type->getFqsen(), 1);
if (!$className) {
// Ignore anonymous classes
return null;
}
if ($node->isStatic()) {
return (string)$class->getNamespacedName() . '::' . $node->getName() . '()';
return $className . '::' . $node->getName() . '()';
} else {
return (string)$class->getNamespacedName() . '->' . $node->getName() . '()';
return $className . '->' . $node->getName() . '()';
}
}
@ -1282,20 +1184,18 @@ class DefinitionResolver
// }
if (
($propertyDeclaration = ParserHelpers\tryGetPropertyDeclaration($node)) !== null &&
($classDeclaration =
$node->getFirstAncestor(
Node\Expression\ObjectCreationExpression::class,
PhpParser\ClassLike::class
)
) !== null && isset($classDeclaration->name)) {
$scope->currentSelf !== null &&
isset($scope->currentSelf->definitionNode->name)
) {
$className = substr((string)$scope->currentSelf->type->getFqsen(), 1);
$name = $node->getName();
if ($propertyDeclaration->isStatic()) {
// Static Property: use ClassName::$propertyName as name
return (string)$classDeclaration->getNamespacedName() . '::$' . $name;
return $className . '::$' . $name;
}
// Instance Property: use ClassName->propertyName as name
return (string)$classDeclaration->getNamespacedName() . '->' . $name;
return $className . '->' . $name;
}
// INPUT OUTPUT
@ -1310,16 +1210,13 @@ class DefinitionResolver
return (string)$node->getNamespacedName();
}
if ($scope->currentSelf === null || !isset($scope->currentSelf->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->currentSelf->type->getFqsen(), 1);
return $className . '::' . $node->getName();
}
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 {
// 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);

View File

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

View File

@ -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 .= '...';

View File

@ -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 ($node instanceof Node\Expression\Variable &&
!isset($scope->variables['this']) &&
$node->getName() === 'this'
) {
$this->diagnostics[] = new Diagnostic(
"\$this can not be used in static methods.",
Range::fromNode($node),
@ -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->currentSelf) {
return;
}
$fqn = substr((string)$scope->currentSelf->type->getFqsen(), 1);
} else if ($fqn === 'parent') {
// Resolve parent keyword to the base class FQN
$classNode = $node->getFirstAncestor(Node\Statement\ClassDeclaration::class);
if (!$classNode || !$classNode->classBaseClause || !$classNode->classBaseClause->baseClass) {
if ($scope->currentSelf === null) {
return;
}
$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) {
return;
}

View File

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

View File

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