2017-03-01 23:41:18 +00:00
|
|
|
<?php
|
|
|
|
declare(strict_types = 1);
|
|
|
|
|
|
|
|
namespace LanguageServer;
|
|
|
|
|
2017-03-02 00:14:13 +00:00
|
|
|
use LanguageServer\Protocol\TolerantSymbolInformation;
|
2017-03-01 23:41:18 +00:00
|
|
|
use PhpParser\Node;
|
2017-03-05 04:56:01 +00:00
|
|
|
use phpDocumentor\Reflection\{
|
2017-03-06 04:22:13 +00:00
|
|
|
DocBlock, DocBlockFactory, Types, Type, Fqsen, TypeResolver
|
2017-03-05 04:56:01 +00:00
|
|
|
};
|
2017-03-01 23:41:18 +00:00
|
|
|
use LanguageServer\Protocol\SymbolInformation;
|
|
|
|
use LanguageServer\Index\ReadableIndex;
|
2017-03-05 04:56:01 +00:00
|
|
|
use Microsoft\PhpParser as Tolerant;
|
2017-03-01 23:41:18 +00:00
|
|
|
|
|
|
|
class TolerantDefinitionResolver implements DefinitionResolverInterface
|
|
|
|
{
|
|
|
|
/**
|
2017-04-19 05:48:26 +00:00
|
|
|
* The current project index (for retrieving existing definitions)
|
|
|
|
*
|
2017-03-01 23:41:18 +00:00
|
|
|
* @var \LanguageServer\Index\ReadableIndex
|
|
|
|
*/
|
2017-03-07 00:08:06 +00:00
|
|
|
protected $index;
|
2017-03-01 23:41:18 +00:00
|
|
|
|
|
|
|
/**
|
2017-04-19 05:48:26 +00:00
|
|
|
* Resolves strings to a type object.
|
|
|
|
*
|
2017-03-01 23:41:18 +00:00
|
|
|
* @var \phpDocumentor\Reflection\TypeResolver
|
|
|
|
*/
|
|
|
|
private $typeResolver;
|
|
|
|
|
|
|
|
/**
|
2017-04-19 05:48:26 +00:00
|
|
|
* Parses Doc Block comments given the DocBlock text and import tables at a position.
|
|
|
|
*
|
2017-03-07 05:31:13 +00:00
|
|
|
* @var DocBlockFactory
|
|
|
|
*/
|
|
|
|
private $docBlockFactory;
|
|
|
|
|
2017-03-01 23:41:18 +00:00
|
|
|
/**
|
|
|
|
* @param ReadableIndex $index
|
|
|
|
*/
|
|
|
|
public function __construct(ReadableIndex $index)
|
|
|
|
{
|
|
|
|
$this->index = $index;
|
|
|
|
$this->typeResolver = new TypeResolver;
|
2017-03-07 05:31:13 +00:00
|
|
|
$this->docBlockFactory = DocBlockFactory::createInstance();
|
2017-03-01 23:41:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-04-19 05:48:26 +00:00
|
|
|
* Builds the declaration line for a given node. Declarations with multiple lines are trimmed.
|
2017-03-05 04:56:01 +00:00
|
|
|
*
|
|
|
|
* @param Tolerant\Node $node
|
2017-03-01 23:41:18 +00:00
|
|
|
* @return string
|
|
|
|
*/
|
2017-03-02 00:01:06 +00:00
|
|
|
public function getDeclarationLineFromNode($node): string
|
2017-03-01 23:41:18 +00:00
|
|
|
{
|
2017-04-19 05:48:26 +00:00
|
|
|
// If node is part of a declaration list, build a declaration line that discludes other elements in the list
|
|
|
|
// - [PropertyDeclaration] // public $a, [$b = 3], $c; => public $b = 3;
|
|
|
|
// - [ConstDeclaration | ClassConstDeclaration] // "const A = 3, [B = 4];" => "const B = 4;"
|
2017-03-05 04:56:01 +00:00
|
|
|
if (
|
2017-04-19 05:48:26 +00:00
|
|
|
($declaration = TolerantParserHelpers::tryGetPropertyDeclaration($node)) && ($elements = $declaration->propertyElements) ||
|
|
|
|
($declaration = TolerantParserHelpers::tryGetConstOrClassConstDeclaration($node)) && ($elements = $declaration->constElements)
|
2017-03-05 04:56:01 +00:00
|
|
|
) {
|
2017-04-19 05:48:26 +00:00
|
|
|
$defLine = $declaration->getText();
|
|
|
|
$defLineStart = $declaration->getStart();
|
2017-03-05 04:56:01 +00:00
|
|
|
|
|
|
|
$defLine = \substr_replace(
|
|
|
|
$defLine,
|
|
|
|
$node->getFullText(),
|
2017-04-19 05:48:26 +00:00
|
|
|
$elements->getFullStart() - $defLineStart,
|
|
|
|
$elements->getFullWidth()
|
2017-03-05 04:56:01 +00:00
|
|
|
);
|
2017-04-19 05:48:26 +00:00
|
|
|
} else {
|
2017-03-05 04:56:01 +00:00
|
|
|
$defLine = $node->getText();
|
2017-03-01 23:41:18 +00:00
|
|
|
}
|
2017-03-05 04:56:01 +00:00
|
|
|
|
2017-04-19 05:48:26 +00:00
|
|
|
// Trim string to only include first line
|
|
|
|
$defLine = \rtrim(\strtok($defLine, "\n"), "\r");
|
2017-03-05 04:56:01 +00:00
|
|
|
|
2017-04-19 05:48:26 +00:00
|
|
|
// TODO - pretty print rather than getting text
|
|
|
|
|
2017-03-05 04:56:01 +00:00
|
|
|
return $defLine;
|
2017-03-01 23:41:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets the documentation string for a node, if it has one
|
|
|
|
*
|
2017-03-05 04:56:01 +00:00
|
|
|
* @param Tolerant\Node $node
|
2017-03-01 23:41:18 +00:00
|
|
|
* @return string|null
|
|
|
|
*/
|
2017-03-02 00:01:06 +00:00
|
|
|
public function getDocumentationFromNode($node)
|
2017-03-01 23:41:18 +00:00
|
|
|
{
|
2017-04-19 05:48:26 +00:00
|
|
|
// Any NamespaceDefinition comments likely apply to the file, not the declaration itself.
|
|
|
|
if ($node instanceof Tolerant\Node\Statement\NamespaceDefinition) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2017-03-05 04:56:01 +00:00
|
|
|
// For properties and constants, set the node to the declaration node, rather than the individual property.
|
|
|
|
// This is because they get defined as part of a list.
|
2017-04-19 05:48:26 +00:00
|
|
|
$constOrPropertyDeclaration = TolerantParserHelpers::tryGetPropertyDeclaration($node) ?? TolerantParserHelpers::tryGetConstOrClassConstDeclaration($node);
|
2017-03-05 04:56:01 +00:00
|
|
|
if ($constOrPropertyDeclaration !== null) {
|
|
|
|
$node = $constOrPropertyDeclaration;
|
2017-03-01 23:41:18 +00:00
|
|
|
}
|
2017-03-05 04:56:01 +00:00
|
|
|
|
2017-04-19 05:48:26 +00:00
|
|
|
// For parameters, parse the function-like declaration to get documentation for a parameter
|
2017-03-05 04:56:01 +00:00
|
|
|
if ($node instanceof Tolerant\Node\Parameter) {
|
2017-04-19 05:48:26 +00:00
|
|
|
$variableName = $node->getName();
|
2017-03-05 04:56:01 +00:00
|
|
|
|
2017-04-19 05:48:26 +00:00
|
|
|
$functionLikeDeclaration = TolerantParserHelpers::getFunctionLikeDeclarationFromParameter($node);
|
|
|
|
$docBlock = $this->getDocBlock($functionLikeDeclaration);
|
2017-03-05 04:56:01 +00:00
|
|
|
|
2017-04-19 05:48:26 +00:00
|
|
|
$parameterDocBlockTag = $this->tryGetDocBlockTagForParameter($docBlock, $variableName);
|
|
|
|
return $parameterDocBlockTag !== null ? $parameterDocBlockTag->getDescription()->render() : null;
|
2017-03-05 04:56:01 +00:00
|
|
|
}
|
2017-04-19 05:48:26 +00:00
|
|
|
|
|
|
|
// For everything else, get the doc block summary corresponding to the current node.
|
|
|
|
$docBlock = $this->getDocBlock($node);
|
|
|
|
if ($docBlock !== null) {
|
|
|
|
return $docBlock->getSummary();
|
2017-03-01 23:41:18 +00:00
|
|
|
}
|
2017-04-11 19:52:24 +00:00
|
|
|
return null;
|
2017-03-01 23:41:18 +00:00
|
|
|
}
|
|
|
|
|
2017-04-19 05:48:26 +00:00
|
|
|
/**
|
|
|
|
* Gets Doc Block with resolved names for a Node
|
|
|
|
*
|
|
|
|
* @param Tolerant\Node $node
|
|
|
|
* @return DocBlock | null
|
|
|
|
*/
|
|
|
|
private function getDocBlock(Tolerant\Node $node)
|
|
|
|
{
|
|
|
|
// TODO make more efficient (caching, ensure import table is in right format to begin with)
|
|
|
|
$docCommentText = $node->getDocCommentText();
|
|
|
|
if ($docCommentText !== null) {
|
|
|
|
list($namespaceImportTable,,) = $node->getImportTablesForCurrentScope();
|
2017-03-07 05:31:13 +00:00
|
|
|
foreach ($namespaceImportTable as $alias=>$name) {
|
|
|
|
$namespaceImportTable[$alias] = (string)$name;
|
|
|
|
}
|
2017-04-19 05:48:26 +00:00
|
|
|
$namespaceDefinition = $node->getNamespaceDefinition();
|
2017-04-20 00:52:37 +00:00
|
|
|
if ($namespaceDefinition !== null && $namespaceDefinition->name !== null) {
|
2017-04-19 05:48:26 +00:00
|
|
|
$namespaceName = (string)$namespaceDefinition->name->getNamespacedName();
|
|
|
|
} else {
|
|
|
|
$namespaceName = 'global';
|
|
|
|
}
|
2017-04-11 19:52:24 +00:00
|
|
|
$context = new Types\Context($namespaceName, $namespaceImportTable);
|
2017-03-07 05:31:13 +00:00
|
|
|
|
2017-03-15 21:10:52 +00:00
|
|
|
try {
|
|
|
|
return $this->docBlockFactory->create($docCommentText, $context);
|
|
|
|
} catch (\InvalidArgumentException $e) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return null;
|
2017-03-05 04:56:01 +00:00
|
|
|
}
|
|
|
|
|
2017-03-01 23:41:18 +00:00
|
|
|
/**
|
|
|
|
* Create a Definition for a definition node
|
|
|
|
*
|
2017-03-05 04:56:01 +00:00
|
|
|
* @param Tolerant\Node $node
|
2017-03-01 23:41:18 +00:00
|
|
|
* @param string $fqn
|
|
|
|
* @return Definition
|
|
|
|
*/
|
2017-03-02 00:01:06 +00:00
|
|
|
public function createDefinitionFromNode($node, string $fqn = null): Definition
|
2017-03-01 23:41:18 +00:00
|
|
|
{
|
|
|
|
$def = new Definition;
|
2017-04-19 05:48:26 +00:00
|
|
|
$def->fqn = $fqn;
|
2017-03-05 04:56:01 +00:00
|
|
|
|
2017-04-19 05:48:26 +00:00
|
|
|
// Determines whether the suggestion will show after "new"
|
2017-04-10 19:29:58 +00:00
|
|
|
$def->canBeInstantiated = $node instanceof Tolerant\Node\Statement\ClassDeclaration;
|
2017-03-05 04:56:01 +00:00
|
|
|
|
2017-04-19 05:48:26 +00:00
|
|
|
// Interfaces, classes, traits, namespaces, functions, and global const elements
|
2017-03-01 23:41:18 +00:00
|
|
|
$def->isGlobal = (
|
2017-04-19 05:48:26 +00:00
|
|
|
$node instanceof Tolerant\Node\Statement\InterfaceDeclaration ||
|
|
|
|
$node instanceof Tolerant\Node\Statement\ClassDeclaration ||
|
|
|
|
$node instanceof Tolerant\Node\Statement\TraitDeclaration ||
|
2017-03-05 04:56:01 +00:00
|
|
|
|
2017-04-19 05:48:26 +00:00
|
|
|
($node instanceof Tolerant\Node\Statement\NamespaceDefinition && $node->name !== null) ||
|
2017-03-05 04:56:01 +00:00
|
|
|
|
2017-04-19 05:48:26 +00:00
|
|
|
$node instanceof Tolerant\Node\Statement\FunctionDeclaration ||
|
2017-03-05 04:56:01 +00:00
|
|
|
|
2017-04-19 05:48:26 +00:00
|
|
|
($node instanceof Tolerant\Node\ConstElement && $node->parent->parent instanceof Tolerant\Node\Statement\ConstDeclaration)
|
2017-03-01 23:41:18 +00:00
|
|
|
);
|
2017-03-05 04:56:01 +00:00
|
|
|
|
2017-04-19 05:48:26 +00:00
|
|
|
// Static methods and static property declarations
|
2017-03-01 23:41:18 +00:00
|
|
|
$def->isStatic = (
|
2017-04-19 05:48:26 +00:00
|
|
|
($node instanceof Tolerant\Node\MethodDeclaration && $node->isStatic()) ||
|
|
|
|
|
|
|
|
(($propertyDeclaration = TolerantParserHelpers::tryGetPropertyDeclaration($node)) !== null
|
|
|
|
&& $propertyDeclaration->isStatic())
|
2017-03-01 23:41:18 +00:00
|
|
|
);
|
2017-04-19 05:48:26 +00:00
|
|
|
|
|
|
|
if ($node instanceof Tolerant\Node\Statement\ClassDeclaration &&
|
|
|
|
// TODO - this should be bette rrpreented in the parser API
|
|
|
|
$node->classBaseClause !== null && $node->classBaseClause->baseClass !== null)
|
|
|
|
{
|
|
|
|
$def->extends = [(string)$node->classBaseClause->baseClass->getResolvedName()];
|
|
|
|
// TODO - why is this represented as an array?
|
|
|
|
// TODO interface implementations.
|
|
|
|
} elseif (
|
|
|
|
$node instanceof Tolerant\Node\Statement\InterfaceDeclaration &&
|
|
|
|
// TODO - this hould be better represented in the parser API
|
|
|
|
$node->interfaceBaseClause !== null && $node->interfaceBaseClause->interfaceNameList !== null
|
|
|
|
) {
|
2017-04-11 19:52:24 +00:00
|
|
|
$def->extends = [];
|
2017-04-19 05:48:26 +00:00
|
|
|
foreach ($node->interfaceBaseClause->interfaceNameList->getValues() as $n) {
|
|
|
|
$def->extends[] = (string)$n->getResolvedName();
|
2017-03-01 23:41:18 +00:00
|
|
|
}
|
|
|
|
}
|
2017-03-15 21:10:52 +00:00
|
|
|
|
2017-03-02 00:14:13 +00:00
|
|
|
$def->symbolInformation = TolerantSymbolInformation::fromNode($node, $fqn);
|
2017-03-15 21:10:52 +00:00
|
|
|
|
|
|
|
if ($def->symbolInformation !== null) {
|
2017-04-19 05:48:26 +00:00
|
|
|
$def->type = $this->getTypeFromNode($node);
|
2017-03-15 21:10:52 +00:00
|
|
|
$def->declarationLine = $this->getDeclarationLineFromNode($node);
|
|
|
|
$def->documentation = $this->getDocumentationFromNode($node);
|
|
|
|
}
|
|
|
|
|
2017-03-01 23:41:18 +00:00
|
|
|
return $def;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Given any node, returns the Definition object of the symbol that is referenced
|
|
|
|
*
|
2017-03-06 04:22:13 +00:00
|
|
|
* @param Tolerant\Node $node Any reference node
|
2017-03-01 23:41:18 +00:00
|
|
|
* @return Definition|null
|
|
|
|
*/
|
2017-03-02 00:01:06 +00:00
|
|
|
public function resolveReferenceNodeToDefinition($node)
|
2017-03-01 23:41:18 +00:00
|
|
|
{
|
2017-03-15 21:10:52 +00:00
|
|
|
$parent = $node->parent;
|
2017-04-19 05:48:26 +00:00
|
|
|
// 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 Tolerant\Node\Expression\Variable &&
|
|
|
|
!($parent instanceof Tolerant\Node\Expression\ScopedPropertyAccessExpression))
|
|
|
|
{
|
|
|
|
// Resolve $this to the containing class definition.
|
2017-03-06 04:22:13 +00:00
|
|
|
if ($node->getName() === 'this' && $fqn = $this->getContainingClassFqn($node)) {
|
2017-03-01 23:41:18 +00:00
|
|
|
return $this->index->getDefinition($fqn, false);
|
|
|
|
}
|
2017-03-06 07:23:33 +00:00
|
|
|
|
2017-03-01 23:41:18 +00:00
|
|
|
// Resolve the variable to a definition node (assignment, param or closure use)
|
2017-03-06 07:23:33 +00:00
|
|
|
$defNode = $this->resolveVariableToNode($node);
|
2017-03-01 23:41:18 +00:00
|
|
|
if ($defNode === null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return $this->createDefinitionFromNode($defNode);
|
|
|
|
}
|
|
|
|
// Other references are references to a global symbol that have an FQN
|
|
|
|
// Find out the FQN
|
|
|
|
$fqn = $this->resolveReferenceNodeToFqn($node);
|
|
|
|
if ($fqn === null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
// 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
|
2017-04-19 05:48:26 +00:00
|
|
|
$globalFallback = TolerantParserHelpers::isConstantFetch($node) || $parent instanceof Tolerant\Node\Expression\CallExpression;
|
2017-03-01 23:41:18 +00:00
|
|
|
// Return the Definition object from the index index
|
|
|
|
return $this->index->getDefinition($fqn, $globalFallback);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Given any node, returns the FQN of the symbol that is referenced
|
|
|
|
* Returns null if the FQN could not be resolved or the reference node references a variable
|
|
|
|
*
|
|
|
|
* @param Node $node
|
|
|
|
* @return string|null
|
|
|
|
*/
|
2017-04-19 05:48:26 +00:00
|
|
|
public function resolveReferenceNodeToFqn($node) {
|
|
|
|
// TODO all name tokens should be a part of a node
|
2017-03-06 04:22:13 +00:00
|
|
|
if ($node instanceof Tolerant\Node\QualifiedName) {
|
2017-04-19 05:48:26 +00:00
|
|
|
return $this->resolveQualifiedNameNodeToFqn($node);
|
|
|
|
}
|
|
|
|
|
|
|
|
else if ($node instanceof Tolerant\Node\Expression\MemberAccessExpression) {
|
|
|
|
return $this->resolveMemberAccessExpressionNodeToFqn($node);
|
|
|
|
}
|
|
|
|
else if (TolerantParserHelpers::isConstantFetch($node)) {
|
|
|
|
return (string)($node->getNamespacedName());
|
|
|
|
}
|
|
|
|
else if (
|
|
|
|
// A\B::C - constant access expression
|
|
|
|
$node instanceof Tolerant\Node\Expression\ScopedPropertyAccessExpression
|
|
|
|
&& !($node->memberName instanceof Tolerant\Node\Expression\Variable)
|
|
|
|
) {
|
|
|
|
return $this->resolveScopedPropertyAccessExpressionNodeToFqn($node);
|
|
|
|
} else if (
|
|
|
|
// A\B::$c - static property access expression
|
|
|
|
$node->parent instanceof Tolerant\Node\Expression\ScopedPropertyAccessExpression
|
|
|
|
) {
|
|
|
|
return $this->resolveScopedPropertyAccessExpressionNodeToFqn($node->parent);
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
2017-03-06 04:22:13 +00:00
|
|
|
|
2017-04-19 05:48:26 +00:00
|
|
|
private function resolveQualifiedNameNodeToFqn(Tolerant\Node\QualifiedName $node) {
|
|
|
|
$parent = $node->parent;
|
2017-04-20 08:33:45 +00:00
|
|
|
|
|
|
|
if ($parent instanceof Tolerant\Node\TraitSelectOrAliasClause) {
|
|
|
|
return null;
|
|
|
|
}
|
2017-04-19 05:48:26 +00:00
|
|
|
// Add use clause references
|
|
|
|
if (($useClause = $parent) instanceof Tolerant\Node\NamespaceUseGroupClause
|
|
|
|
|| $useClause instanceof Tolerant\Node\NamespaceUseClause
|
|
|
|
) {
|
|
|
|
$contents = $node->getFileContents();
|
|
|
|
if ($useClause instanceof Tolerant\Node\NamespaceUseGroupClause) {
|
|
|
|
$prefix = $useClause->parent->parent->namespaceName;
|
|
|
|
if ($prefix === null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
$name = Tolerant\ResolvedName::buildName($prefix->nameParts, $contents);
|
|
|
|
$name->addNameParts($node->nameParts, $contents);
|
|
|
|
$name = (string)$name;
|
|
|
|
|
|
|
|
if ($useClause->functionOrConst === null) {
|
|
|
|
$useClause = $node->getFirstAncestor(Tolerant\Node\Statement\NamespaceUseDeclaration::class);
|
|
|
|
if ($useClause->functionOrConst !== null && $useClause->functionOrConst->kind === Tolerant\TokenKind::FunctionKeyword) {
|
|
|
|
$name .= '()';
|
2017-03-06 04:22:13 +00:00
|
|
|
}
|
|
|
|
}
|
2017-04-19 05:48:26 +00:00
|
|
|
return $name;
|
|
|
|
} else {
|
|
|
|
$name = (string) Tolerant\ResolvedName::buildName($node->nameParts, $contents);
|
|
|
|
if ($useClause->groupClauses === null && $useClause->parent->parent->functionOrConst !== null && $useClause->parent->parent->functionOrConst->kind === Tolerant\TokenKind::FunctionKeyword) {
|
2017-03-06 07:23:33 +00:00
|
|
|
$name .= '()';
|
2017-03-06 04:22:13 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $name;
|
|
|
|
}
|
|
|
|
|
2017-04-19 05:48:26 +00:00
|
|
|
// For extends, implements, type hints and classes of classes of static calls use the name directly
|
|
|
|
$name = (string) ($node->getResolvedName() ?? $node->getNamespacedName());
|
2017-04-20 08:33:45 +00:00
|
|
|
|
2017-04-19 05:48:26 +00:00
|
|
|
if ($node->parent instanceof Tolerant\Node\Expression\CallExpression) {
|
|
|
|
$name .= '()';
|
|
|
|
}
|
|
|
|
return $name;
|
|
|
|
}
|
2017-03-06 23:34:43 +00:00
|
|
|
|
2017-04-19 05:48:26 +00:00
|
|
|
private function resolveMemberAccessExpressionNodeToFqn(Tolerant\Node\Expression\MemberAccessExpression $access) {
|
|
|
|
if ($access->memberName instanceof Tolerant\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);
|
2017-03-06 23:34:43 +00:00
|
|
|
|
2017-04-19 05:48:26 +00:00
|
|
|
if ($varType instanceof Types\Compound) {
|
|
|
|
// For compound types, use the first FQN we find
|
|
|
|
// (popular use case is ClassName|null)
|
|
|
|
for ($i = 0; $t = $varType->get($i); $i++) {
|
|
|
|
if (
|
|
|
|
$t instanceof Types\This
|
|
|
|
|| $t instanceof Types\Object_
|
|
|
|
|| $t instanceof Types\Static_
|
|
|
|
|| $t instanceof Types\Self_
|
|
|
|
) {
|
|
|
|
$varType = $t;
|
2017-03-01 23:41:18 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2017-04-19 05:48:26 +00:00
|
|
|
}
|
|
|
|
if (
|
|
|
|
$varType instanceof Types\This
|
|
|
|
|| $varType instanceof Types\Static_
|
|
|
|
|| $varType instanceof Types\Self_
|
|
|
|
) {
|
|
|
|
// $this/static/self is resolved to the containing class
|
|
|
|
$classFqn = self::getContainingClassFqn($access);
|
|
|
|
} 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()));
|
|
|
|
if ($access->parent instanceof Tolerant\Node\Expression\CallExpression) {
|
|
|
|
$memberSuffix .= '()';
|
|
|
|
}
|
|
|
|
|
|
|
|
// Find the right class that implements the member
|
|
|
|
$implementorFqns = [$classFqn];
|
2017-03-06 23:34:43 +00:00
|
|
|
|
2017-04-19 05:48:26 +00:00
|
|
|
while ($implementorFqn = array_shift($implementorFqns)) {
|
|
|
|
// If the member FQN exists, return it
|
|
|
|
if ($this->index->getDefinition($implementorFqn . $memberSuffix)) {
|
|
|
|
|
|
|
|
return $implementorFqn . $memberSuffix;
|
|
|
|
}
|
|
|
|
// Get Definition of implementor class
|
|
|
|
$implementorDef = $this->index->getDefinition($implementorFqn);
|
|
|
|
// If it doesn't exist, return the initial guess
|
|
|
|
if ($implementorDef === null) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
// Repeat for parent class
|
|
|
|
if ($implementorDef->extends) {
|
|
|
|
foreach ($implementorDef->extends as $extends) {
|
|
|
|
$implementorFqns[] = $extends;
|
|
|
|
}
|
|
|
|
}
|
2017-03-06 04:22:13 +00:00
|
|
|
}
|
2017-04-19 05:48:26 +00:00
|
|
|
|
|
|
|
return $classFqn . $memberSuffix;
|
|
|
|
}
|
|
|
|
|
|
|
|
private function resolveScopedPropertyAccessExpressionNodeToFqn(Tolerant\Node\Expression\ScopedPropertyAccessExpression $scoped) {
|
2017-04-20 07:20:47 +00:00
|
|
|
if ($scoped->scopeResolutionQualifier instanceof Tolerant\Node\Expression\Variable) {
|
|
|
|
$varType = $this->getTypeFromNode($scoped->scopeResolutionQualifier);
|
|
|
|
if ($varType === null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
$className = substr((string)$varType->getFqsen(), 1);
|
|
|
|
} elseif ($scoped->scopeResolutionQualifier instanceof Tolerant\Node\QualifiedName) {
|
|
|
|
$className = (string)$scoped->scopeResolutionQualifier->getResolvedName();
|
|
|
|
} else {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2017-04-19 05:48:26 +00:00
|
|
|
if ($className === 'self' || $className === 'static' || $className === 'parent') {
|
|
|
|
// self and static are resolved to the containing class
|
|
|
|
$classNode = $scoped->getFirstAncestor(Tolerant\Node\Statement\ClassDeclaration::class);
|
|
|
|
if ($classNode === null) {
|
2017-03-01 23:41:18 +00:00
|
|
|
return null;
|
|
|
|
}
|
2017-04-19 05:48:26 +00:00
|
|
|
if ($className === 'parent') {
|
|
|
|
// parent is resolved to the parent class
|
|
|
|
if (!isset($classNode->extends)) {
|
2017-03-01 23:41:18 +00:00
|
|
|
return null;
|
|
|
|
}
|
2017-04-19 05:48:26 +00:00
|
|
|
$className = (string)$classNode->extends->getResolvedName();
|
2017-03-01 23:41:18 +00:00
|
|
|
} else {
|
2017-04-19 05:48:26 +00:00
|
|
|
$className = (string)$classNode->getNamespacedName();
|
2017-03-06 19:01:10 +00:00
|
|
|
}
|
2017-04-19 05:48:26 +00:00
|
|
|
} elseif ($scoped->scopeResolutionQualifier instanceof Tolerant\Node\QualifiedName) {
|
|
|
|
$className = $scoped->scopeResolutionQualifier->getResolvedName();
|
2017-03-01 23:41:18 +00:00
|
|
|
}
|
2017-04-19 05:48:26 +00:00
|
|
|
if ($scoped->memberName instanceof Tolerant\Node\Expression\Variable) {
|
2017-04-20 08:33:45 +00:00
|
|
|
if ($scoped->parent instanceof Tolerant\Node\Expression\CallExpression) {
|
|
|
|
return null;
|
|
|
|
}
|
2017-04-20 07:20:47 +00:00
|
|
|
$memberName = $scoped->memberName->getName();
|
|
|
|
if (empty($memberName)) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
$name = (string)$className . '::$' . $memberName;
|
2017-04-19 05:48:26 +00:00
|
|
|
} else {
|
|
|
|
$name = (string)$className . '::' . $scoped->memberName->getText($scoped->getFileContents());
|
2017-03-01 23:41:18 +00:00
|
|
|
}
|
2017-04-19 05:48:26 +00:00
|
|
|
if ($scoped->parent instanceof Tolerant\Node\Expression\CallExpression) {
|
2017-03-01 23:41:18 +00:00
|
|
|
$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
|
|
|
|
*
|
2017-03-06 04:22:13 +00:00
|
|
|
* @param Tolerant\Node $node
|
2017-03-01 23:41:18 +00:00
|
|
|
* @return string|null
|
|
|
|
*/
|
2017-03-06 04:22:13 +00:00
|
|
|
private static function getContainingClassFqn(Tolerant\Node $node)
|
2017-03-01 23:41:18 +00:00
|
|
|
{
|
2017-03-06 04:22:13 +00:00
|
|
|
$classNode = $node->getFirstAncestor(Tolerant\Node\Statement\ClassDeclaration::class);
|
|
|
|
if ($classNode === null) {
|
2017-03-01 23:41:18 +00:00
|
|
|
return null;
|
|
|
|
}
|
2017-03-06 04:22:13 +00:00
|
|
|
return (string)$classNode->getNamespacedName();
|
2017-03-01 23:41:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the assignment or parameter node where a variable was defined
|
|
|
|
*
|
|
|
|
* @param Node\Expr\Variable|Node\Expr\ClosureUse $var The variable access
|
|
|
|
* @return Node\Expr\Assign|Node\Expr\AssignOp|Node\Param|Node\Expr\ClosureUse|null
|
|
|
|
*/
|
2017-04-11 23:24:35 +00:00
|
|
|
public function resolveVariableToNode($var)
|
2017-03-01 23:41:18 +00:00
|
|
|
{
|
|
|
|
$n = $var;
|
2017-03-06 04:22:13 +00:00
|
|
|
// When a use is passed, start outside the closure to not return immediately
|
2017-03-06 07:23:33 +00:00
|
|
|
// Use variable vs variable parsing?
|
2017-03-06 04:22:13 +00:00
|
|
|
if ($var instanceof Tolerant\Node\UseVariableName) {
|
2017-04-16 23:32:38 +00:00
|
|
|
$n = $var->getFirstAncestor(Tolerant\Node\Expression\AnonymousFunctionCreationExpression::class)->parent;
|
2017-03-06 04:22:13 +00:00
|
|
|
$name = $var->getName();
|
|
|
|
} else if ($var instanceof Tolerant\Node\Expression\Variable || $var instanceof Tolerant\Node\Parameter) {
|
|
|
|
$name = $var->getName();
|
2017-03-01 23:41:18 +00:00
|
|
|
} else {
|
|
|
|
throw new \InvalidArgumentException('$var must be Variable, Param or ClosureUse, not ' . get_class($var));
|
|
|
|
}
|
|
|
|
// Traverse the AST up
|
|
|
|
do {
|
|
|
|
// If a function is met, check the parameters and use statements
|
2017-04-19 05:48:26 +00:00
|
|
|
if (TolerantParserHelpers::isFunctionLike($n)) {
|
2017-03-06 04:22:13 +00:00
|
|
|
if ($n->parameters !== null) {
|
|
|
|
|
|
|
|
foreach ($n->parameters->getElements() as $param) {
|
|
|
|
if ($param->getName() === $name) {
|
|
|
|
return $param;
|
|
|
|
}
|
2017-03-01 23:41:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
// If it is a closure, also check use statements
|
2017-04-16 23:32:38 +00:00
|
|
|
if ($n instanceof Tolerant\Node\Expression\AnonymousFunctionCreationExpression &&
|
|
|
|
$n->anonymousFunctionUseClause !== null &&
|
|
|
|
$n->anonymousFunctionUseClause->useVariableNameList !== null) {
|
|
|
|
foreach ($n->anonymousFunctionUseClause->useVariableNameList->getElements() as $use
|
|
|
|
) {
|
2017-03-06 04:22:13 +00:00
|
|
|
if ($use->getName() === $name) {
|
2017-03-01 23:41:18 +00:00
|
|
|
return $use;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
// Check each previous sibling node for a variable assignment to that variable
|
2017-03-21 22:25:49 +00:00
|
|
|
while (($prevSibling = $n->getPreviousSibling()) !== null && $n = $prevSibling) {
|
2017-03-06 04:22:13 +00:00
|
|
|
if ($n instanceof Tolerant\Node\Statement\ExpressionStatement) {
|
|
|
|
$n = $n->expression;
|
|
|
|
}
|
2017-03-01 23:41:18 +00:00
|
|
|
if (
|
2017-04-26 00:09:52 +00:00
|
|
|
// TODO - clean this up
|
2017-03-06 04:22:13 +00:00
|
|
|
($n instanceof Tolerant\Node\Expression\AssignmentExpression && $n->operator->kind === Tolerant\TokenKind::EqualsToken)
|
|
|
|
&& $n->leftOperand instanceof Tolerant\Node\Expression\Variable && $n->leftOperand->getName() === $name
|
2017-03-01 23:41:18 +00:00
|
|
|
) {
|
|
|
|
return $n;
|
|
|
|
}
|
|
|
|
}
|
2017-03-15 21:10:52 +00:00
|
|
|
} while (isset($n) && $n = $n->parent);
|
2017-03-01 23:41:18 +00:00
|
|
|
// Return null if nothing was found
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Given an expression node, resolves that expression recursively to a type.
|
|
|
|
* If the type could not be resolved, returns Types\Mixed.
|
|
|
|
*
|
2017-04-19 05:48:26 +00:00
|
|
|
* @param Tolerant\Node\Expression $expr
|
2017-03-01 23:41:18 +00:00
|
|
|
* @return \phpDocumentor\Reflection\Type
|
|
|
|
*/
|
2017-03-02 00:01:06 +00:00
|
|
|
public function resolveExpressionNodeToType($expr): Type
|
2017-03-01 23:41:18 +00:00
|
|
|
{
|
2017-04-20 00:52:37 +00:00
|
|
|
if ($expr == null || $expr instanceof Tolerant\MissingToken || $expr instanceof Tolerant\SkippedToken) {
|
2017-03-24 17:30:34 +00:00
|
|
|
// TODO some members are null or Missing/SkippedToken
|
|
|
|
// How do we handle this more generally?
|
|
|
|
return new Types\Mixed;
|
|
|
|
}
|
2017-04-13 18:51:10 +00:00
|
|
|
|
2017-04-20 07:20:47 +00:00
|
|
|
// PARENTHESIZED EXPRESSION
|
|
|
|
// Retrieve inner expression from parenthesized expression
|
2017-04-19 05:48:26 +00:00
|
|
|
while ($expr instanceof Tolerant\Node\Expression\ParenthesizedExpression) {
|
2017-04-13 18:51:10 +00:00
|
|
|
$expr = $expr->expression;
|
|
|
|
}
|
|
|
|
|
2017-04-20 07:20:47 +00:00
|
|
|
// VARIABLE
|
|
|
|
// $this -> Type\this
|
|
|
|
// $myVariable -> type of corresponding assignment expression
|
2017-03-06 04:22:13 +00:00
|
|
|
if ($expr instanceof Tolerant\Node\Expression\Variable || $expr instanceof Tolerant\Node\UseVariableName) {
|
2017-04-20 00:52:37 +00:00
|
|
|
if ($expr->getName() === 'this') {
|
2017-03-01 23:41:18 +00:00
|
|
|
return new Types\This;
|
|
|
|
}
|
2017-04-20 00:52:37 +00:00
|
|
|
// Find variable definition (parameter or assignment expression)
|
2017-03-01 23:41:18 +00:00
|
|
|
$defNode = $this->resolveVariableToNode($expr);
|
2017-04-20 00:52:37 +00:00
|
|
|
if ($defNode instanceof Tolerant\Node\Expression\AssignmentExpression || $defNode instanceof Tolerant\Node\UseVariableName) {
|
2017-03-01 23:41:18 +00:00
|
|
|
return $this->resolveExpressionNodeToType($defNode);
|
|
|
|
}
|
2017-03-06 04:22:13 +00:00
|
|
|
if ($defNode instanceof Tolerant\Node\Parameter) {
|
2017-03-01 23:41:18 +00:00
|
|
|
return $this->getTypeFromNode($defNode);
|
|
|
|
}
|
|
|
|
}
|
2017-04-20 00:52:37 +00:00
|
|
|
|
2017-04-20 07:20:47 +00:00
|
|
|
// FUNCTION CALL
|
|
|
|
// Function calls are resolved to type corresponding to their FQN
|
2017-03-06 04:22:13 +00:00
|
|
|
if ($expr instanceof Tolerant\Node\Expression\CallExpression &&
|
2017-04-12 23:53:03 +00:00
|
|
|
!(
|
|
|
|
$expr->callableExpression instanceof Tolerant\Node\Expression\ScopedPropertyAccessExpression ||
|
|
|
|
$expr->callableExpression instanceof Tolerant\Node\Expression\MemberAccessExpression)
|
2017-04-20 07:20:47 +00:00
|
|
|
) {
|
2017-03-28 21:02:26 +00:00
|
|
|
|
2017-03-01 23:41:18 +00:00
|
|
|
// Find the function definition
|
2017-03-06 04:22:13 +00:00
|
|
|
if ($expr->callableExpression instanceof Tolerant\Node\Expression) {
|
2017-03-01 23:41:18 +00:00
|
|
|
// Cannot get type for dynamic function call
|
|
|
|
return new Types\Mixed;
|
|
|
|
}
|
2017-03-06 04:22:13 +00:00
|
|
|
|
|
|
|
if ($expr->callableExpression instanceof Tolerant\Node\QualifiedName) {
|
|
|
|
$fqn = $expr->callableExpression->getResolvedName() ?? $expr->callableExpression->getNamespacedName();
|
2017-03-06 07:23:33 +00:00
|
|
|
$fqn .= '()';
|
2017-03-06 04:22:13 +00:00
|
|
|
$def = $this->index->getDefinition($fqn, true);
|
|
|
|
if ($def !== null) {
|
|
|
|
return $def->type;
|
|
|
|
}
|
2017-03-01 23:41:18 +00:00
|
|
|
}
|
2017-03-06 04:22:13 +00:00
|
|
|
}
|
2017-04-20 00:52:37 +00:00
|
|
|
|
2017-04-20 07:20:47 +00:00
|
|
|
// TRUE / FALSE / NULL
|
|
|
|
// Resolve true and false reserved words to Types\Boolean
|
|
|
|
if ($expr instanceof Tolerant\Node\ReservedWord) {
|
|
|
|
$token = $expr->children->kind;
|
|
|
|
if ($token === Tolerant\TokenKind::TrueReservedWord || $token === Tolerant\TokenKind::FalseReservedWord) {
|
|
|
|
return new Types\Boolean;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($token === Tolerant\TokenKind::NullReservedWord) {
|
|
|
|
return new Types\Null_;
|
|
|
|
}
|
2017-03-06 04:22:13 +00:00
|
|
|
}
|
|
|
|
|
2017-04-20 07:20:47 +00:00
|
|
|
// CONSTANT FETCH
|
|
|
|
// Resolve constants by retrieving corresponding definition type from FQN
|
2017-04-19 05:48:26 +00:00
|
|
|
if (TolerantParserHelpers::isConstantFetch($expr)) {
|
2017-03-06 07:23:33 +00:00
|
|
|
$fqn = (string)$expr->getNamespacedName();
|
|
|
|
$def = $this->index->getDefinition($fqn, true);
|
|
|
|
if ($def !== null) {
|
|
|
|
return $def->type;
|
2017-03-01 23:41:18 +00:00
|
|
|
}
|
|
|
|
}
|
2017-04-20 00:52:37 +00:00
|
|
|
|
2017-04-20 07:20:47 +00:00
|
|
|
// MEMBER ACCESS EXPRESSION
|
|
|
|
if ($expr instanceof Tolerant\Node\Expression\MemberAccessExpression) {
|
|
|
|
if ($expr->memberName instanceof Tolerant\Node\Expression) {
|
2017-04-13 18:51:10 +00:00
|
|
|
return new Types\Mixed;
|
|
|
|
}
|
2017-04-20 07:20:47 +00:00
|
|
|
$var = $expr->dereferencableExpression;
|
2017-03-06 04:22:13 +00:00
|
|
|
|
2017-04-13 18:51:10 +00:00
|
|
|
// Resolve object
|
|
|
|
$objType = $this->resolveExpressionNodeToType($var);
|
|
|
|
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) {
|
|
|
|
return new Types\Mixed;
|
|
|
|
}
|
|
|
|
} else if (!($t instanceof Types\Object_) || $t->getFqsen() === null) {
|
|
|
|
return new Types\Mixed;
|
|
|
|
} else {
|
|
|
|
$classFqn = substr((string)$t->getFqsen(), 1);
|
|
|
|
}
|
2017-04-20 07:20:47 +00:00
|
|
|
$fqn = $classFqn . '->' . $expr->memberName->getText($expr->getFileContents());
|
2017-04-13 18:51:10 +00:00
|
|
|
if ($expr->parent instanceof Tolerant\Node\Expression\CallExpression) {
|
|
|
|
$fqn .= '()';
|
|
|
|
}
|
|
|
|
$def = $this->index->getDefinition($fqn);
|
|
|
|
if ($def !== null) {
|
|
|
|
return $def->type;
|
|
|
|
}
|
|
|
|
}
|
2017-04-20 07:20:47 +00:00
|
|
|
}
|
2017-04-20 00:52:37 +00:00
|
|
|
|
2017-04-20 07:20:47 +00:00
|
|
|
// SCOPED PROPERTY ACCESS EXPRESSION
|
|
|
|
if ($expr instanceof Tolerant\Node\Expression\ScopedPropertyAccessExpression) {
|
|
|
|
$classType = $this->resolveClassNameToType($expr->scopeResolutionQualifier);
|
|
|
|
if (!($classType instanceof Types\Object_) || $classType->getFqsen() === null) {
|
2017-03-01 23:41:18 +00:00
|
|
|
return new Types\Mixed;
|
|
|
|
}
|
|
|
|
$fqn = substr((string)$classType->getFqsen(), 1) . '::';
|
2017-04-19 05:48:26 +00:00
|
|
|
|
2017-04-20 07:20:47 +00:00
|
|
|
// TODO is there a cleaner way to do this?
|
|
|
|
$fqn .= $expr->memberName->getText() ?? $expr->memberName->getText($expr->getFileContents());
|
2017-04-12 23:53:03 +00:00
|
|
|
if ($expr->parent instanceof Tolerant\Node\Expression\CallExpression) {
|
2017-03-01 23:41:18 +00:00
|
|
|
$fqn .= '()';
|
|
|
|
}
|
2017-04-19 05:48:26 +00:00
|
|
|
|
2017-03-01 23:41:18 +00:00
|
|
|
$def = $this->index->getDefinition($fqn);
|
|
|
|
if ($def === null) {
|
|
|
|
return new Types\Mixed;
|
|
|
|
}
|
|
|
|
return $def->type;
|
|
|
|
}
|
2017-04-20 00:52:37 +00:00
|
|
|
|
2017-04-20 07:20:47 +00:00
|
|
|
// OBJECT CREATION EXPRESSION
|
|
|
|
// new A() => resolves to the type of the class type designator (A)
|
|
|
|
// TODO: new $this->a => resolves to the string represented by "a"
|
2017-03-06 04:22:13 +00:00
|
|
|
if ($expr instanceof Tolerant\Node\Expression\ObjectCreationExpression) {
|
2017-03-06 07:23:33 +00:00
|
|
|
return $this->resolveClassNameToType($expr->classTypeDesignator);
|
2017-03-01 23:41:18 +00:00
|
|
|
}
|
2017-04-20 00:52:37 +00:00
|
|
|
|
2017-04-20 07:20:47 +00:00
|
|
|
// CLONE EXPRESSION
|
|
|
|
// clone($a) => resolves to the type of $a
|
2017-03-06 04:22:13 +00:00
|
|
|
if ($expr instanceof Tolerant\Node\Expression\CloneExpression) {
|
|
|
|
return $this->resolveExpressionNodeToType($expr->expression);
|
2017-03-01 23:41:18 +00:00
|
|
|
}
|
2017-04-20 00:52:37 +00:00
|
|
|
|
2017-04-20 07:20:47 +00:00
|
|
|
// ASSIGNMENT EXPRESSION
|
|
|
|
// $a = $myExpression => resolves to the type of the right-hand operand
|
2017-03-06 04:22:13 +00:00
|
|
|
if ($expr instanceof Tolerant\Node\Expression\AssignmentExpression) {
|
|
|
|
return $this->resolveExpressionNodeToType($expr->rightOperand);
|
|
|
|
}
|
2017-04-20 00:52:37 +00:00
|
|
|
|
2017-04-20 07:20:47 +00:00
|
|
|
// TERNARY EXPRESSION
|
|
|
|
// $condition ? $ifExpression : $elseExpression => reslves to type of $ifCondition or $elseExpression
|
|
|
|
// $condition ?: $elseExpression => resolves to type of $condition or $elseExpression
|
2017-03-06 04:22:13 +00:00
|
|
|
if ($expr instanceof Tolerant\Node\Expression\TernaryExpression) {
|
2017-03-01 23:41:18 +00:00
|
|
|
// ?:
|
2017-03-06 04:22:13 +00:00
|
|
|
if ($expr->ifExpression === null) {
|
2017-03-01 23:41:18 +00:00
|
|
|
return new Types\Compound([
|
2017-04-20 07:20:47 +00:00
|
|
|
$this->resolveExpressionNodeToType($expr->condition), // TODO: why?
|
2017-03-06 04:22:13 +00:00
|
|
|
$this->resolveExpressionNodeToType($expr->elseExpression)
|
2017-03-01 23:41:18 +00:00
|
|
|
]);
|
|
|
|
}
|
|
|
|
// Ternary is a compound of the two possible values
|
|
|
|
return new Types\Compound([
|
2017-03-06 04:22:13 +00:00
|
|
|
$this->resolveExpressionNodeToType($expr->ifExpression),
|
|
|
|
$this->resolveExpressionNodeToType($expr->elseExpression)
|
2017-03-01 23:41:18 +00:00
|
|
|
]);
|
|
|
|
}
|
2017-04-20 00:52:37 +00:00
|
|
|
|
2017-04-20 07:20:47 +00:00
|
|
|
// NULL COALLESCE
|
|
|
|
// $rightOperand ?? $leftOperand => resolves to type of $rightOperand or $leftOperand
|
2017-03-06 04:22:13 +00:00
|
|
|
if ($expr instanceof Tolerant\Node\Expression\BinaryExpression && $expr->operator->kind === Tolerant\TokenKind::QuestionQuestionToken) {
|
2017-03-01 23:41:18 +00:00
|
|
|
// ?? operator
|
|
|
|
return new Types\Compound([
|
2017-03-06 04:22:13 +00:00
|
|
|
$this->resolveExpressionNodeToType($expr->leftOperand),
|
|
|
|
$this->resolveExpressionNodeToType($expr->rightOperand)
|
2017-03-01 23:41:18 +00:00
|
|
|
]);
|
|
|
|
}
|
2017-04-20 00:52:37 +00:00
|
|
|
|
2017-04-20 07:20:47 +00:00
|
|
|
// BOOLEAN EXPRESSIONS: resolve to Types\Boolean
|
|
|
|
// (bool) $expression
|
|
|
|
// !$expression
|
|
|
|
// empty($var)
|
|
|
|
// isset($var)
|
|
|
|
// >, >=, <, <=, &&, ||, AND, OR, XOR, ==, ===, !=, !==
|
2017-03-01 23:41:18 +00:00
|
|
|
if (
|
2017-04-19 05:48:26 +00:00
|
|
|
TolerantParserHelpers::isBooleanExpression($expr)
|
2017-03-06 04:22:13 +00:00
|
|
|
|
|
|
|
|| ($expr instanceof Tolerant\Node\Expression\CastExpression && $expr->castType->kind === Tolerant\TokenKind::BoolCastToken)
|
|
|
|
|| ($expr instanceof Tolerant\Node\Expression\UnaryOpExpression && $expr->operator->kind === Tolerant\TokenKind::ExclamationToken)
|
|
|
|
|| $expr instanceof Tolerant\Node\Expression\EmptyIntrinsicExpression
|
|
|
|
|| $expr instanceof Tolerant\Node\Expression\IssetIntrinsicExpression
|
2017-03-01 23:41:18 +00:00
|
|
|
) {
|
|
|
|
return new Types\Boolean;
|
|
|
|
}
|
2017-04-20 00:52:37 +00:00
|
|
|
|
2017-04-20 07:20:47 +00:00
|
|
|
// STRING EXPRESSIONS: resolve to Types\String
|
|
|
|
// [concatenation] .=, .
|
|
|
|
// [literals] "hello", \b"hello", \B"hello", 'hello', \b'hello', HEREDOC, NOWDOC
|
|
|
|
// [cast] (string) "hello"
|
|
|
|
//
|
|
|
|
// TODO: Magic constants (__CLASS__, __DIR__, __FUNCTION__, __METHOD__, __NAMESPACE__, __TRAIT__, __FILE__)
|
2017-03-01 23:41:18 +00:00
|
|
|
if (
|
2017-03-06 04:22:13 +00:00
|
|
|
($expr instanceof Tolerant\Node\Expression\BinaryExpression &&
|
|
|
|
($expr->operator->kind === Tolerant\TokenKind::DotToken || $expr->operator->kind === Tolerant\TokenKind::DotEqualsToken)) ||
|
|
|
|
$expr instanceof Tolerant\Node\StringLiteral ||
|
|
|
|
($expr instanceof Tolerant\Node\Expression\CastExpression && $expr->castType->kind === Tolerant\TokenKind::StringCastToken)
|
2017-03-01 23:41:18 +00:00
|
|
|
) {
|
|
|
|
return new Types\String_;
|
|
|
|
}
|
2017-04-20 00:52:37 +00:00
|
|
|
|
2017-04-20 07:20:47 +00:00
|
|
|
// BINARY EXPRESSIONS:
|
|
|
|
// Resolve to Types\Integer if both left and right operands are integer types, otherwise Types\Float
|
|
|
|
// [operator] +, -, *, **
|
|
|
|
// [assignment] *=, **=, -=, +=
|
|
|
|
// Resolve to Types\Float
|
|
|
|
// [assignment] /=
|
2017-03-01 23:41:18 +00:00
|
|
|
if (
|
2017-03-06 04:22:13 +00:00
|
|
|
$expr instanceof Tolerant\Node\Expression\BinaryExpression &&
|
|
|
|
($operator = $expr->operator->kind)
|
|
|
|
&& ($operator === Tolerant\TokenKind::PlusToken ||
|
|
|
|
$operator === Tolerant\TokenKind::AsteriskAsteriskToken ||
|
|
|
|
$operator === Tolerant\TokenKind::AsteriskToken ||
|
|
|
|
$operator === Tolerant\TokenKind::MinusToken ||
|
2017-04-20 07:20:47 +00:00
|
|
|
|
|
|
|
// Assignment expressions (TODO: consider making this a type of AssignmentExpression rather than kind of BinaryExpression)
|
2017-03-06 04:22:13 +00:00
|
|
|
$operator === Tolerant\TokenKind::AsteriskEqualsToken||
|
|
|
|
$operator === Tolerant\TokenKind::AsteriskAsteriskEqualsToken ||
|
|
|
|
$operator === Tolerant\TokenKind::MinusEqualsToken ||
|
2017-04-20 07:20:47 +00:00
|
|
|
$operator === Tolerant\TokenKind::PlusEqualsToken
|
2017-03-06 04:22:13 +00:00
|
|
|
)
|
2017-03-01 23:41:18 +00:00
|
|
|
) {
|
|
|
|
if (
|
2017-04-20 07:20:47 +00:00
|
|
|
$this->resolveExpressionNodeToType($expr->leftOperand) instanceof Types\Integer
|
|
|
|
&& $this->resolveExpressionNodeToType($expr->rightOperand) instanceof Types\Integer
|
2017-03-01 23:41:18 +00:00
|
|
|
) {
|
|
|
|
return new Types\Integer;
|
|
|
|
}
|
|
|
|
return new Types\Float_;
|
2017-04-20 07:20:47 +00:00
|
|
|
} else if (
|
|
|
|
$expr instanceof Tolerant\Node\Expression\BinaryExpression &&
|
|
|
|
$expr->operator->kind === Tolerant\TokenKind::SlashEqualsToken
|
|
|
|
) {
|
|
|
|
return new Types\Float_;
|
2017-03-01 23:41:18 +00:00
|
|
|
}
|
2017-04-20 00:52:37 +00:00
|
|
|
|
2017-04-20 07:20:47 +00:00
|
|
|
// INTEGER EXPRESSIONS: resolve to Types\Integer
|
|
|
|
// [literal] 1
|
|
|
|
// [operator] <=>, &, ^, |
|
|
|
|
// TODO: Magic constants (__LINE__)
|
2017-03-01 23:41:18 +00:00
|
|
|
if (
|
2017-04-20 07:20:47 +00:00
|
|
|
// TODO: consider different Node types of float/int, also better property name (not "children")
|
2017-03-06 04:22:13 +00:00
|
|
|
($expr instanceof Tolerant\Node\NumericLiteral && $expr->children->kind === Tolerant\TokenKind::IntegerLiteralToken) ||
|
|
|
|
$expr instanceof Tolerant\Node\Expression\BinaryExpression && (
|
|
|
|
($operator = $expr->operator->kind)
|
|
|
|
&& ($operator === Tolerant\TokenKind::LessThanEqualsGreaterThanToken ||
|
|
|
|
$operator === Tolerant\TokenKind::AmpersandToken ||
|
|
|
|
$operator === Tolerant\TokenKind::CaretToken ||
|
|
|
|
$operator === Tolerant\TokenKind::BarToken)
|
|
|
|
)
|
2017-03-01 23:41:18 +00:00
|
|
|
) {
|
|
|
|
return new Types\Integer;
|
|
|
|
}
|
2017-04-20 00:52:37 +00:00
|
|
|
|
2017-04-20 07:20:47 +00:00
|
|
|
// FLOAT EXPRESSIONS: resolve to Types\Float
|
|
|
|
// [literal] 1.5
|
|
|
|
// [operator] /
|
|
|
|
// [cast] (double)
|
2017-03-01 23:41:18 +00:00
|
|
|
if (
|
2017-04-20 07:20:47 +00:00
|
|
|
$expr instanceof Tolerant\Node\NumericLiteral && $expr->children->kind === Tolerant\TokenKind::FloatingLiteralToken ||
|
|
|
|
($expr instanceof Tolerant\Node\Expression\CastExpression && $expr->castType->kind === Tolerant\TokenKind::DoubleCastToken) ||
|
|
|
|
($expr instanceof Tolerant\Node\Expression\BinaryExpression && $expr->operator->kind === Tolerant\TokenKind::SlashToken)
|
2017-03-01 23:41:18 +00:00
|
|
|
) {
|
|
|
|
return new Types\Float_;
|
|
|
|
}
|
2017-04-20 00:52:37 +00:00
|
|
|
|
2017-04-20 07:20:47 +00:00
|
|
|
// ARRAY CREATION EXPRESSION:
|
|
|
|
// Resolve to Types\Array (Types\Compound of value and key types)
|
|
|
|
// [a, b, c]
|
|
|
|
// [1=>"hello", "hi"=>1, 4=>[]]s
|
2017-03-06 04:22:13 +00:00
|
|
|
if ($expr instanceof Tolerant\Node\Expression\ArrayCreationExpression) {
|
2017-03-01 23:41:18 +00:00
|
|
|
$valueTypes = [];
|
|
|
|
$keyTypes = [];
|
2017-03-15 21:10:52 +00:00
|
|
|
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;
|
|
|
|
}
|
2017-03-01 23:41:18 +00:00
|
|
|
}
|
|
|
|
$valueTypes = array_unique($keyTypes);
|
|
|
|
$keyTypes = array_unique($keyTypes);
|
|
|
|
if (empty($valueTypes)) {
|
|
|
|
$valueType = null;
|
|
|
|
} else if (count($valueTypes) === 1) {
|
|
|
|
$valueType = $valueTypes[0];
|
|
|
|
} else {
|
|
|
|
$valueType = new Types\Compound($valueTypes);
|
|
|
|
}
|
|
|
|
if (empty($keyTypes)) {
|
|
|
|
$keyType = null;
|
|
|
|
} else if (count($keyTypes) === 1) {
|
|
|
|
$keyType = $keyTypes[0];
|
|
|
|
} else {
|
|
|
|
$keyType = new Types\Compound($keyTypes);
|
|
|
|
}
|
|
|
|
return new Types\Array_($valueType, $keyType);
|
|
|
|
}
|
2017-04-20 00:52:37 +00:00
|
|
|
|
2017-04-20 07:20:47 +00:00
|
|
|
// SUBSCRIPT EXPRESSION
|
|
|
|
// $myArray[3]
|
|
|
|
// $myArray{"hello"}
|
2017-03-06 04:22:13 +00:00
|
|
|
if ($expr instanceof Tolerant\Node\Expression\SubscriptExpression) {
|
|
|
|
$varType = $this->resolveExpressionNodeToType($expr->postfixExpression);
|
2017-03-01 23:41:18 +00:00
|
|
|
if (!($varType instanceof Types\Array_)) {
|
|
|
|
return new Types\Mixed;
|
|
|
|
}
|
|
|
|
return $varType->getValueType();
|
|
|
|
}
|
2017-04-20 00:52:37 +00:00
|
|
|
|
2017-04-20 07:20:47 +00:00
|
|
|
// SCRIPT INCLUSION EXPRESSION
|
|
|
|
// include, require, include_once, require_once
|
2017-03-06 04:22:13 +00:00
|
|
|
if ($expr instanceof Tolerant\Node\Expression\ScriptInclusionExpression) {
|
2017-03-01 23:41:18 +00:00
|
|
|
// TODO: resolve path to PhpDocument and find return statement
|
|
|
|
return new Types\Mixed;
|
|
|
|
}
|
2017-04-20 00:52:37 +00:00
|
|
|
|
2017-04-26 00:09:52 +00:00
|
|
|
if ($expr instanceof Tolerant\Node\QualifiedName) {
|
|
|
|
return $this->resolveClassNameToType($expr);
|
|
|
|
}
|
|
|
|
|
2017-03-01 23:41:18 +00:00
|
|
|
return new Types\Mixed;
|
|
|
|
}
|
|
|
|
|
2017-03-06 04:22:13 +00:00
|
|
|
|
2017-03-01 23:41:18 +00:00
|
|
|
/**
|
|
|
|
* Takes any class name node (from a static method call, or new node) and returns a Type object
|
|
|
|
* Resolves keywords like self, static and parent
|
|
|
|
*
|
2017-03-06 04:22:13 +00:00
|
|
|
* @param Tolerant\Node || Tolerant\Token $class
|
2017-03-01 23:41:18 +00:00
|
|
|
* @return Type
|
|
|
|
*/
|
2017-03-06 07:23:33 +00:00
|
|
|
public function resolveClassNameToType($class): Type
|
2017-03-01 23:41:18 +00:00
|
|
|
{
|
2017-03-06 04:22:13 +00:00
|
|
|
if ($class instanceof Tolerant\Node\Expression) {
|
2017-03-01 23:41:18 +00:00
|
|
|
return new Types\Mixed;
|
|
|
|
}
|
2017-03-06 04:22:13 +00:00
|
|
|
if ($class instanceof Tolerant\Token && $class->kind === Tolerant\TokenKind::ClassKeyword) {
|
2017-03-01 23:41:18 +00:00
|
|
|
// Anonymous class
|
|
|
|
return new Types\Object_;
|
|
|
|
}
|
2017-03-06 04:22:13 +00:00
|
|
|
$className = (string)$class->getResolvedName();
|
2017-04-16 23:32:38 +00:00
|
|
|
|
2017-04-20 07:20:47 +00:00
|
|
|
if ($className === 'static') {
|
2017-03-01 23:41:18 +00:00
|
|
|
return new Types\Static_;
|
|
|
|
}
|
2017-04-20 07:20:47 +00:00
|
|
|
if ($className === 'self' || $className === 'parent') {
|
2017-03-06 04:22:13 +00:00
|
|
|
$classNode = $class->getFirstAncestor(Tolerant\Node\Statement\ClassDeclaration::class);
|
2017-04-20 07:20:47 +00:00
|
|
|
if ($className === 'parent') {
|
2017-04-20 00:52:37 +00:00
|
|
|
if ($classNode === null || $classNode->classBaseClause === null) {
|
2017-03-01 23:41:18 +00:00
|
|
|
return new Types\Object_;
|
|
|
|
}
|
|
|
|
// parent is resolved to the parent class
|
2017-03-06 04:22:13 +00:00
|
|
|
$classFqn = (string)$classNode->classBaseClause->baseClass->getResolvedName();
|
2017-03-01 23:41:18 +00:00
|
|
|
} else {
|
|
|
|
if ($classNode === null) {
|
|
|
|
return new Types\Self_;
|
|
|
|
}
|
|
|
|
// self is resolved to the containing class
|
2017-03-15 21:10:52 +00:00
|
|
|
$classFqn = (string)$classNode->getNamespacedName();
|
2017-03-01 23:41:18 +00:00
|
|
|
}
|
|
|
|
return new Types\Object_(new Fqsen('\\' . $classFqn));
|
|
|
|
}
|
|
|
|
return new Types\Object_(new Fqsen('\\' . $className));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the type a reference to this symbol will resolve to.
|
|
|
|
* For properties and constants, this is the type of the property/constant.
|
|
|
|
* For functions and methods, this is the return type.
|
|
|
|
* For parameters, this is the type of the parameter.
|
|
|
|
* For classes and interfaces, this is the class type (object).
|
|
|
|
* For variables / assignments, this is the documented type or type the assignment resolves to.
|
|
|
|
* Can also be a compound type.
|
|
|
|
* If it is unknown, will be Types\Mixed.
|
|
|
|
* Returns null if the node does not have a type.
|
|
|
|
*
|
2017-03-06 04:22:13 +00:00
|
|
|
* @param Tolerant\Node $node
|
2017-03-01 23:41:18 +00:00
|
|
|
* @return \phpDocumentor\Reflection\Type|null
|
|
|
|
*/
|
2017-03-02 00:01:06 +00:00
|
|
|
public function getTypeFromNode($node)
|
2017-03-01 23:41:18 +00:00
|
|
|
{
|
2017-04-20 07:20:47 +00:00
|
|
|
// PARAMETERS
|
|
|
|
// Get the type of the parameter:
|
|
|
|
// 1. Doc block
|
|
|
|
// 2. Parameter type and default
|
2017-03-06 04:22:13 +00:00
|
|
|
if ($node instanceof Tolerant\Node\Parameter) {
|
2017-03-01 23:41:18 +00:00
|
|
|
// Parameters
|
2017-03-06 04:22:13 +00:00
|
|
|
// Get the doc block for the the function call
|
2017-04-20 00:52:37 +00:00
|
|
|
// /**
|
|
|
|
// * @param MyClass $myParam
|
|
|
|
// */
|
|
|
|
// function foo($a)
|
2017-04-19 05:48:26 +00:00
|
|
|
$functionLikeDeclaration = TolerantParserHelpers::getFunctionLikeDeclarationFromParameter($node);
|
2017-04-20 00:52:37 +00:00
|
|
|
$variableName = $node->getName();
|
2017-03-06 04:22:13 +00:00
|
|
|
$docBlock = $this->getDocBlock($functionLikeDeclaration);
|
|
|
|
|
2017-04-19 05:48:26 +00:00
|
|
|
$parameterDocBlockTag = $this->tryGetDocBlockTagForParameter($docBlock, $variableName);
|
2017-04-20 00:52:37 +00:00
|
|
|
if ($parameterDocBlockTag !== null && ($type = $parameterDocBlockTag->getType())) {
|
|
|
|
// Doc block comments supercede all other forms of type inference
|
|
|
|
return $type;
|
2017-03-01 23:41:18 +00:00
|
|
|
}
|
2017-03-06 04:22:13 +00:00
|
|
|
|
2017-04-20 00:52:37 +00:00
|
|
|
// function foo(MyClass $a)
|
2017-03-06 04:22:13 +00:00
|
|
|
if ($node->typeDeclaration !== null) {
|
2017-03-01 23:41:18 +00:00
|
|
|
// Use PHP7 return type hint
|
2017-03-06 04:22:13 +00:00
|
|
|
if ($node->typeDeclaration instanceof Tolerant\Token) {
|
2017-03-01 23:41:18 +00:00
|
|
|
// Resolve a string like "bool" to a type object
|
2017-03-06 04:22:13 +00:00
|
|
|
$type = $this->typeResolver->resolve($node->typeDeclaration->getText($node->getFileContents()));
|
2017-03-01 23:41:18 +00:00
|
|
|
} else {
|
2017-03-06 04:22:13 +00:00
|
|
|
$type = new Types\Object_(new Fqsen('\\' . (string)$node->typeDeclaration->getResolvedName()));
|
2017-03-01 23:41:18 +00:00
|
|
|
}
|
|
|
|
}
|
2017-04-20 00:52:37 +00:00
|
|
|
// function foo($a = 3)
|
2017-03-01 23:41:18 +00:00
|
|
|
if ($node->default !== null) {
|
|
|
|
$defaultType = $this->resolveExpressionNodeToType($node->default);
|
|
|
|
if (isset($type) && !is_a($type, get_class($defaultType))) {
|
2017-04-20 00:52:37 +00:00
|
|
|
// TODO - verify it is worth creating a compound type
|
|
|
|
return new Types\Compound([$type, $defaultType]);
|
2017-03-01 23:41:18 +00:00
|
|
|
}
|
2017-04-20 00:52:37 +00:00
|
|
|
$type = $defaultType;
|
2017-03-01 23:41:18 +00:00
|
|
|
}
|
|
|
|
return $type ?? new Types\Mixed;
|
|
|
|
}
|
2017-04-20 07:20:47 +00:00
|
|
|
|
|
|
|
// FUNCTIONS AND METHODS
|
|
|
|
// Get the return type
|
|
|
|
// 1. doc block
|
|
|
|
// 2. return type hint
|
|
|
|
// 3. TODO: infer from return statements
|
2017-04-19 05:48:26 +00:00
|
|
|
if (TolerantParserHelpers::isFunctionLike($node)) {
|
2017-03-01 23:41:18 +00:00
|
|
|
// Functions/methods
|
2017-03-06 04:22:13 +00:00
|
|
|
$docBlock = $this->getDocBlock($node);
|
2017-03-01 23:41:18 +00:00
|
|
|
if (
|
|
|
|
$docBlock !== null
|
|
|
|
&& !empty($returnTags = $docBlock->getTagsByName('return'))
|
|
|
|
&& $returnTags[0]->getType() !== null
|
|
|
|
) {
|
|
|
|
// Use @return tag
|
|
|
|
return $returnTags[0]->getType();
|
|
|
|
}
|
2017-04-20 00:52:37 +00:00
|
|
|
if ($node->returnType !== null && !($node->returnType instanceof Tolerant\MissingToken)) {
|
2017-03-01 23:41:18 +00:00
|
|
|
// Use PHP7 return type hint
|
2017-03-06 04:22:13 +00:00
|
|
|
if ($node->returnType instanceof Tolerant\Token) {
|
2017-03-01 23:41:18 +00:00
|
|
|
// Resolve a string like "bool" to a type object
|
2017-03-06 04:22:13 +00:00
|
|
|
return $this->typeResolver->resolve($node->returnType->getText($node->getFileContents()));
|
2017-03-01 23:41:18 +00:00
|
|
|
}
|
2017-03-06 04:22:13 +00:00
|
|
|
return new Types\Object_(new Fqsen('\\' . (string)$node->returnType->getResolvedName()));
|
2017-03-01 23:41:18 +00:00
|
|
|
}
|
|
|
|
// Unknown return type
|
|
|
|
return new Types\Mixed;
|
|
|
|
}
|
2017-03-06 04:22:13 +00:00
|
|
|
|
2017-04-20 07:20:47 +00:00
|
|
|
// PROPERTIES, CONSTS, CLASS CONSTS, ASSIGNMENT EXPRESSIONS
|
|
|
|
// Get the documented type the assignment resolves to.
|
2017-03-01 23:41:18 +00:00
|
|
|
if (
|
2017-04-19 05:48:26 +00:00
|
|
|
($declarationNode =
|
|
|
|
TolerantParserHelpers::tryGetPropertyDeclaration($node) ??
|
|
|
|
TolerantParserHelpers::tryGetConstOrClassConstDeclaration($node)
|
|
|
|
) !== null ||
|
2017-04-20 00:52:37 +00:00
|
|
|
($node = $node->parent) instanceof Tolerant\Node\Expression\AssignmentExpression)
|
2017-03-06 04:22:13 +00:00
|
|
|
{
|
|
|
|
$declarationNode = $declarationNode ?? $node;
|
|
|
|
|
2017-03-01 23:41:18 +00:00
|
|
|
// Property, constant or variable
|
|
|
|
// Use @var tag
|
|
|
|
if (
|
2017-03-06 04:22:13 +00:00
|
|
|
($docBlock = $this->getDocBlock($declarationNode))
|
2017-03-01 23:41:18 +00:00
|
|
|
&& !empty($varTags = $docBlock->getTagsByName('var'))
|
|
|
|
&& ($type = $varTags[0]->getType())
|
|
|
|
) {
|
|
|
|
return $type;
|
|
|
|
}
|
2017-04-20 00:52:37 +00:00
|
|
|
|
2017-03-01 23:41:18 +00:00
|
|
|
// Resolve the expression
|
2017-03-06 04:22:13 +00:00
|
|
|
if ($declarationNode instanceof Tolerant\Node\PropertyDeclaration) {
|
|
|
|
// TODO should have default
|
2017-04-20 00:52:37 +00:00
|
|
|
if (isset($node->parent->rightOperand)) {
|
|
|
|
return $this->resolveExpressionNodeToType($node->parent->rightOperand);
|
2017-03-01 23:41:18 +00:00
|
|
|
}
|
2017-03-06 04:22:13 +00:00
|
|
|
} else if ($node instanceof Tolerant\Node\ConstElement) {
|
|
|
|
return $this->resolveExpressionNodeToType($node->assignment);
|
|
|
|
} else if ($node instanceof Tolerant\Node\Expression\AssignmentExpression) {
|
2017-04-20 00:52:37 +00:00
|
|
|
return $this->resolveExpressionNodeToType($node->rightOperand);
|
2017-03-01 23:41:18 +00:00
|
|
|
}
|
|
|
|
// TODO: read @property tags of class
|
|
|
|
// TODO: Try to infer the type from default value / constant value
|
|
|
|
// Unknown
|
|
|
|
return new Types\Mixed;
|
|
|
|
}
|
2017-04-20 00:52:37 +00:00
|
|
|
|
|
|
|
// The node does not have a type
|
2017-03-01 23:41:18 +00:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the fully qualified name (FQN) that is defined by a node
|
|
|
|
* Returns null if the node does not declare any symbol that can be referenced by an FQN
|
|
|
|
*
|
2017-03-05 04:56:01 +00:00
|
|
|
* @param Tolerant\Node $node
|
2017-03-01 23:41:18 +00:00
|
|
|
* @return string|null
|
|
|
|
*/
|
2017-03-02 00:01:06 +00:00
|
|
|
public static function getDefinedFqn($node)
|
2017-03-01 23:41:18 +00:00
|
|
|
{
|
2017-03-15 21:10:52 +00:00
|
|
|
$parent = $node->parent;
|
2017-03-01 23:41:18 +00:00
|
|
|
// Anonymous classes don't count as a definition
|
2017-03-05 04:56:01 +00:00
|
|
|
// INPUT OUTPUT:
|
|
|
|
// namespace A\B;
|
|
|
|
// class C { } A\B\C
|
|
|
|
// interface C { } A\B\C
|
|
|
|
// trait C { } A\B\C
|
|
|
|
if (
|
|
|
|
$node instanceof Tolerant\Node\Statement\ClassDeclaration ||
|
|
|
|
$node instanceof Tolerant\Node\Statement\InterfaceDeclaration ||
|
|
|
|
$node instanceof Tolerant\Node\Statement\TraitDeclaration
|
|
|
|
) {
|
|
|
|
return (string) $node->getNamespacedName();
|
|
|
|
}
|
|
|
|
|
|
|
|
// INPUT OUTPUT:
|
|
|
|
// namespace A\B; A\B
|
|
|
|
else if ($node instanceof Tolerant\Node\Statement\NamespaceDefinition && $node->name instanceof Tolerant\Node\QualifiedName) {
|
2017-04-19 05:48:26 +00:00
|
|
|
$name = (string) Tolerant\ResolvedName::buildName($node->name->nameParts, $node->getFileContents());
|
|
|
|
return \count($name) > 0 ? $name : null;
|
2017-03-05 04:56:01 +00:00
|
|
|
}
|
|
|
|
// INPUT OUTPUT:
|
|
|
|
// namespace A\B;
|
|
|
|
// function a(); A\B\a();
|
|
|
|
else if ($node instanceof Tolerant\Node\Statement\FunctionDeclaration) {
|
2017-03-01 23:41:18 +00:00
|
|
|
// Function: use functionName() as the name
|
2017-04-19 05:48:26 +00:00
|
|
|
$name = (string)$node->getNamespacedName();
|
|
|
|
return \count($name) > 0 ? $name . '()' : null;
|
2017-03-05 04:56:01 +00:00
|
|
|
}
|
|
|
|
// INPUT OUTPUT
|
|
|
|
// namespace A\B;
|
|
|
|
// class C {
|
|
|
|
// function a () {} A\B\C::a()
|
|
|
|
// static function b() {} A\B\C->b()
|
|
|
|
// }
|
|
|
|
else if ($node instanceof Tolerant\Node\MethodDeclaration) {
|
2017-03-01 23:41:18 +00:00
|
|
|
// Class method: use ClassName->methodName() as name
|
2017-04-10 20:13:47 +00:00
|
|
|
$class = $node->getFirstAncestor(
|
2017-04-11 04:58:08 +00:00
|
|
|
Tolerant\Node\Expression\ObjectCreationExpression::class,
|
2017-04-10 20:13:47 +00:00
|
|
|
Tolerant\Node\Statement\ClassDeclaration::class,
|
|
|
|
Tolerant\Node\Statement\InterfaceDeclaration::class,
|
|
|
|
Tolerant\Node\Statement\TraitDeclaration::class
|
|
|
|
);
|
2017-03-01 23:41:18 +00:00
|
|
|
if (!isset($class->name)) {
|
|
|
|
// Ignore anonymous classes
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
if ($node->isStatic()) {
|
2017-03-05 04:56:01 +00:00
|
|
|
return (string)$class->getNamespacedName() . '::' . $node->getName() . '()';
|
2017-03-01 23:41:18 +00:00
|
|
|
} else {
|
2017-03-05 04:56:01 +00:00
|
|
|
return (string)$class->getNamespacedName() . '->' . $node->getName() . '()';
|
2017-03-01 23:41:18 +00:00
|
|
|
}
|
2017-03-05 04:56:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// INPUT OUTPUT
|
|
|
|
// namespace A\B;
|
|
|
|
// class C {
|
|
|
|
// static $a = 4, $b = 4 A\B\C::$a, A\B\C::$b
|
|
|
|
// $a = 4, $b = 4 A\B\C->$a, A\B\C->$b
|
|
|
|
// }
|
|
|
|
else if (
|
2017-04-19 05:48:26 +00:00
|
|
|
($propertyDeclaration = TolerantParserHelpers::tryGetPropertyDeclaration($node)) !== null &&
|
2017-04-10 19:42:49 +00:00
|
|
|
($classDeclaration =
|
2017-04-10 20:13:47 +00:00
|
|
|
$node->getFirstAncestor(
|
2017-04-19 05:48:26 +00:00
|
|
|
Tolerant\Node\Expression\ObjectCreationExpression::class,
|
2017-04-10 20:13:47 +00:00
|
|
|
Tolerant\Node\Statement\ClassDeclaration::class,
|
|
|
|
Tolerant\Node\Statement\InterfaceDeclaration::class,
|
|
|
|
Tolerant\Node\Statement\TraitDeclaration::class
|
|
|
|
)
|
2017-04-19 05:48:26 +00:00
|
|
|
) !== null && isset($classDeclaration->name))
|
2017-03-05 04:56:01 +00:00
|
|
|
{
|
2017-04-19 05:48:26 +00:00
|
|
|
$name = $node->getName();
|
2017-03-05 04:56:01 +00:00
|
|
|
if ($propertyDeclaration->isStatic()) {
|
2017-03-01 23:41:18 +00:00
|
|
|
// Static Property: use ClassName::$propertyName as name
|
2017-04-19 05:48:26 +00:00
|
|
|
return (string)$classDeclaration->getNamespacedName() . '::$' . $name;
|
2017-03-01 23:41:18 +00:00
|
|
|
}
|
2017-04-19 05:48:26 +00:00
|
|
|
|
|
|
|
// Instance Property: use ClassName->propertyName as name
|
|
|
|
return (string)$classDeclaration->getNamespacedName() . '->' . $name;
|
2017-03-05 04:56:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// INPUT OUTPUT
|
|
|
|
// namespace A\B;
|
|
|
|
// const FOO = 5; A\B\FOO
|
|
|
|
// class C {
|
|
|
|
// const $a, $b = 4 A\B\C::$a(), A\B\C::$b
|
|
|
|
// }
|
2017-04-19 05:48:26 +00:00
|
|
|
else if (($constDeclaration = TolerantParserHelpers::tryGetConstOrClassConstDeclaration($node)) !== null) {
|
2017-03-05 04:56:01 +00:00
|
|
|
if ($constDeclaration instanceof Tolerant\Node\Statement\ConstDeclaration) {
|
2017-03-01 23:41:18 +00:00
|
|
|
// Basic constant: use CONSTANT_NAME as name
|
2017-03-05 04:56:01 +00:00
|
|
|
return (string)$node->getNamespacedName();
|
2017-03-01 23:41:18 +00:00
|
|
|
}
|
2017-04-19 05:48:26 +00:00
|
|
|
|
|
|
|
// Class constant: use ClassName::CONSTANT_NAME as name
|
|
|
|
$classDeclaration = $constDeclaration->getFirstAncestor(
|
|
|
|
Tolerant\Node\Expression\ObjectCreationExpression::class,
|
|
|
|
Tolerant\Node\Statement\ClassDeclaration::class,
|
|
|
|
Tolerant\Node\Statement\InterfaceDeclaration::class,
|
|
|
|
Tolerant\Node\Statement\TraitDeclaration::class
|
|
|
|
);
|
|
|
|
|
|
|
|
if (!isset($classDeclaration->name)) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return (string)$classDeclaration->getNamespacedName() . '::' . $node->getName();
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param DocBlock | null $docBlock
|
|
|
|
* @param $variableName
|
|
|
|
* @return DocBlock\Tags\Param | null
|
|
|
|
*/
|
|
|
|
private function tryGetDocBlockTagForParameter($docBlock, $variableName) {
|
|
|
|
if ($docBlock === null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
$tags = $docBlock->getTagsByName('param');
|
|
|
|
foreach ($tags as $tag) {
|
|
|
|
if ($tag->getVariableName() === \ltrim($variableName, "$")) {
|
|
|
|
return $tag;
|
2017-03-01 23:41:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|