Merge f14478f795
into fc6b069425
commit
a8504d9116
|
@ -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": {
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)) {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LanguageServer\Scope;
|
||||||
|
|
||||||
|
class TraversingEndedException extends \Exception
|
||||||
|
{
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LanguageServer\Scope;
|
||||||
|
|
||||||
|
use phpDocumentor\Reflection\Type;
|
||||||
|
use Microsoft\PhpParser\Node;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains information about a single variable.
|
||||||
|
*/
|
||||||
|
class Variable
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Type
|
||||||
|
*/
|
||||||
|
public $type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Node
|
||||||
|
*/
|
||||||
|
public $definitionNode;
|
||||||
|
|
||||||
|
public function __construct(Type $type, Node $definitionNode)
|
||||||
|
{
|
||||||
|
$this->type = $type;
|
||||||
|
$this->definitionNode = $definitionNode;
|
||||||
|
}
|
||||||
|
}
|
|
@ -214,7 +214,7 @@ class TextDocument
|
||||||
}
|
}
|
||||||
} else {
|
} 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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 .= '...';
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue