diff --git a/Performance.php b/Performance.php index 8ed002f..3a04bc7 100644 --- a/Performance.php +++ b/Performance.php @@ -7,7 +7,7 @@ use Exception; use LanguageServer\Index\Index; use LanguageServer\ParserKind; use LanguageServer\PhpDocument; -use LanguageServer\TolerantDefinitionResolver; +use LanguageServer\DefinitionResolver; use Microsoft\PhpParser as Tolerant; use phpDocumentor\Reflection\DocBlockFactory; use RecursiveDirectoryIterator; @@ -57,7 +57,7 @@ foreach($frameworks as $framework) { $maxRecursion = []; $definitions = []; - $definitionResolver = new TolerantDefinitionResolver($index); + $definitionResolver = new DefinitionResolver($index); $parser = new Tolerant\Parser(); try { diff --git a/src/CompletionProvider.php b/src/CompletionProvider.php index 4c42ff4..bdb52f9 100644 --- a/src/CompletionProvider.php +++ b/src/CompletionProvider.php @@ -90,7 +90,7 @@ class CompletionProvider ]; /** - * @var TolerantDefinitionResolver + * @var DefinitionResolver */ private $definitionResolver; @@ -105,10 +105,10 @@ class CompletionProvider private $index; /** - * @param TolerantDefinitionResolver $definitionResolver + * @param DefinitionResolver $definitionResolver * @param ReadableIndex $index */ - public function __construct(TolerantDefinitionResolver $definitionResolver, ReadableIndex $index) + public function __construct(DefinitionResolver $definitionResolver, ReadableIndex $index) { $this->definitionResolver = $definitionResolver; $this->index = $index; @@ -228,7 +228,7 @@ class CompletionProvider } } } - } elseif (TolerantParserHelpers::isConstantFetch($node) || + } elseif (ParserHelpers::isConstantFetch($node) || ($creation = $node->parent) instanceof Tolerant\Node\Expression\ObjectCreationExpression || (($creation = $node) instanceof Tolerant\Node\Expression\ObjectCreationExpression)) { @@ -298,7 +298,7 @@ class CompletionProvider $list->items[] = $item; } } - } elseif (TolerantParserHelpers::isConstantFetch($node)) { + } elseif (ParserHelpers::isConstantFetch($node)) { $prefix = (string) ($node->getResolvedName() ?? Tolerant\ResolvedName::buildName($node->nameParts, $node->getFileContents())); foreach (self::KEYWORDS as $keyword) { $item = new CompletionItem($keyword, CompletionItemKind::KEYWORD); @@ -355,7 +355,7 @@ class CompletionProvider // Walk the AST upwards until a scope boundary is met $level = $node; - while ($level && !TolerantParserHelpers::isFunctionLike($level)) { + while ($level && !ParserHelpers::isFunctionLike($level)) { // Walk siblings before the node $sibling = $level; while ($sibling = $sibling->getPreviousSibling()) { @@ -369,7 +369,7 @@ class CompletionProvider // If the traversal ended because a function was met, // also add its parameters and closure uses to the result list - if ($level && TolerantParserHelpers::isFunctionLike($level) && $level->parameters !== null) { + if ($level && ParserHelpers::isFunctionLike($level) && $level->parameters !== null) { foreach ($level->parameters->getValues() as $param) { $paramName = $param->getName(); if (empty($namePrefix) || strpos($paramName, $namePrefix) !== false) { @@ -409,7 +409,7 @@ class CompletionProvider }; $isNotFunctionLike = function($node) { return !( - TolerantParserHelpers::isFunctionLike($node) || + ParserHelpers::isFunctionLike($node) || $node instanceof Tolerant\Node\Statement\ClassDeclaration || $node instanceof Tolerant\Node\Statement\InterfaceDeclaration || $node instanceof Tolerant\Node\Statement\TraitDeclaration diff --git a/src/ComposerScripts.php b/src/ComposerScripts.php index c8c7a16..11f0433 100644 --- a/src/ComposerScripts.php +++ b/src/ComposerScripts.php @@ -31,7 +31,7 @@ class ComposerScripts $contentRetriever = new FileSystemContentRetriever; $docBlockFactory = DocBlockFactory::createInstance(); $parser = new Tolerant\Parser(); - $definitionResolver = new TolerantDefinitionResolver($index); + $definitionResolver = new DefinitionResolver($index); $stubsLocation = null; foreach ([__DIR__ . '/../../../jetbrains/phpstorm-stubs', __DIR__ . '/../vendor/jetbrains/phpstorm-stubs'] as $dir) { diff --git a/src/DefinitionResolver.php b/src/DefinitionResolver.php index 3bc7b0c..4843df0 100644 --- a/src/DefinitionResolver.php +++ b/src/DefinitionResolver.php @@ -3,28 +3,36 @@ declare(strict_types = 1); namespace LanguageServer; -use PhpParser\Node; -use PhpParser\PrettyPrinter\Standard as PrettyPrinter; -use phpDocumentor\Reflection\{Types, Type, Fqsen, TypeResolver}; -use LanguageServer\Protocol\SymbolInformation; use LanguageServer\Index\ReadableIndex; +use LanguageServer\Protocol\SymbolInformation; +use Microsoft\PhpParser as Tolerant; +use phpDocumentor\Reflection\{ + DocBlock, DocBlockFactory, Fqsen, Type, TypeResolver, Types +}; +use PhpParser\Node; class DefinitionResolver { /** + * The current project index (for retrieving existing definitions) + * * @var \LanguageServer\Index\ReadableIndex */ - private $index; + protected $index; /** + * Resolves strings to a type object. + * * @var \phpDocumentor\Reflection\TypeResolver */ private $typeResolver; /** - * @var \PhpParser\PrettyPrinterAbstract + * Parses Doc Block comments given the DocBlock text and import tables at a position. + * + * @var DocBlockFactory */ - private $prettyPrinter; + private $docBlockFactory; /** * @param ReadableIndex $index @@ -33,125 +41,203 @@ class DefinitionResolver { $this->index = $index; $this->typeResolver = new TypeResolver; - $this->prettyPrinter = new PrettyPrinter; + $this->docBlockFactory = DocBlockFactory::createInstance(); } /** - * Builds the declaration line for a given node + * Builds the declaration line for a given node. Declarations with multiple lines are trimmed. * - * @param Node $node + * @param Tolerant\Node $node * @return string */ public function getDeclarationLineFromNode($node): string { - if ($node instanceof Node\Stmt\PropertyProperty || $node instanceof Node\Const_) { - // Properties and constants can have multiple declarations - // Use the parent node (that includes the modifiers), but only render the requested declaration - $child = $node; - /** @var Node */ - $node = $node->getAttribute('parentNode'); - $defLine = clone $node; - $defLine->props = [$child]; + // 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;" + if ( + ($declaration = ParserHelpers::tryGetPropertyDeclaration($node)) && ($elements = $declaration->propertyElements) || + ($declaration = ParserHelpers::tryGetConstOrClassConstDeclaration($node)) && ($elements = $declaration->constElements) + ) { + $defLine = $declaration->getText(); + $defLineStart = $declaration->getStart(); + + $defLine = \substr_replace( + $defLine, + $node->getFullText(), + $elements->getFullStart() - $defLineStart, + $elements->getFullWidth() + ); } else { - $defLine = clone $node; + $defLine = $node->getText(); } - // Don't include the docblock in the declaration string - $defLine->setAttribute('comments', []); - if (isset($defLine->stmts)) { - $defLine->stmts = []; - } - $defText = $this->prettyPrinter->prettyPrint([$defLine]); - return strstr($defText, "\n", true) ?: $defText; + + // Trim string to only include first line + $defLine = \rtrim(\strtok($defLine, "\n"), "\r"); + + // TODO - pretty print rather than getting text + + return $defLine; } /** * Gets the documentation string for a node, if it has one * - * @param Node $node + * @param Tolerant\Node $node * @return string|null */ public function getDocumentationFromNode($node) { - if ($node instanceof Node\Stmt\PropertyProperty || $node instanceof Node\Const_) { - $node = $node->getAttribute('parentNode'); + // Any NamespaceDefinition comments likely apply to the file, not the declaration itself. + if ($node instanceof Tolerant\Node\Statement\NamespaceDefinition) { + return null; } - if ($node instanceof Node\Param) { - $fn = $node->getAttribute('parentNode'); - $docBlock = $fn->getAttribute('docBlock'); - if ($docBlock !== null) { - $tags = $docBlock->getTagsByName('param'); - foreach ($tags as $tag) { - if ($tag->getVariableName() === $node->name) { - return $tag->getDescription()->render(); - } - } + + // 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. + $constOrPropertyDeclaration = ParserHelpers::tryGetPropertyDeclaration($node) ?? ParserHelpers::tryGetConstOrClassConstDeclaration($node); + if ($constOrPropertyDeclaration !== null) { + $node = $constOrPropertyDeclaration; + } + + // For parameters, parse the function-like declaration to get documentation for a parameter + if ($node instanceof Tolerant\Node\Parameter) { + $variableName = $node->getName(); + + $functionLikeDeclaration = ParserHelpers::getFunctionLikeDeclarationFromParameter($node); + $docBlock = $this->getDocBlock($functionLikeDeclaration); + + $parameterDocBlockTag = $this->tryGetDocBlockTagForParameter($docBlock, $variableName); + return $parameterDocBlockTag !== null ? $parameterDocBlockTag->getDescription()->render() : null; + } + + // For everything else, get the doc block summary corresponding to the current node. + $docBlock = $this->getDocBlock($node); + if ($docBlock !== null) { + return $docBlock->getSummary(); + } + return null; + } + + /** + * 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(); + foreach ($namespaceImportTable as $alias=>$name) { + $namespaceImportTable[$alias] = (string)$name; } - } else { - $docBlock = $node->getAttribute('docBlock'); - if ($docBlock !== null) { - return $docBlock->getSummary(); + $namespaceDefinition = $node->getNamespaceDefinition(); + if ($namespaceDefinition !== null && $namespaceDefinition->name !== null) { + $namespaceName = (string)$namespaceDefinition->name->getNamespacedName(); + } else { + $namespaceName = 'global'; + } + $context = new Types\Context($namespaceName, $namespaceImportTable); + + try { + return $this->docBlockFactory->create($docCommentText, $context); + } catch (\InvalidArgumentException $e) { + return null; } } + return null; } /** * Create a Definition for a definition node * - * @param Node $node + * @param Tolerant\Node $node * @param string $fqn * @return Definition */ public function createDefinitionFromNode($node, string $fqn = null): Definition { - $parent = $node->getAttribute('parentNode'); $def = new Definition; - $def->canBeInstantiated = $node instanceof Node\Stmt\Class_; - $def->isGlobal = ( - $node instanceof Node\Stmt\ClassLike - || ($node instanceof Node\Name && $parent instanceof Node\Stmt\Namespace_) - || $node instanceof Node\Stmt\Function_ - || $parent instanceof Node\Stmt\Const_ - ); - $def->isStatic = ( - ($node instanceof Node\Stmt\ClassMethod && $node->isStatic()) - || ($node instanceof Node\Stmt\PropertyProperty && $parent->isStatic()) - ); $def->fqn = $fqn; - if ($node instanceof Node\Stmt\Class_) { + + // Determines whether the suggestion will show after "new" + $def->canBeInstantiated = $node instanceof Tolerant\Node\Statement\ClassDeclaration; + + // Interfaces, classes, traits, namespaces, functions, and global const elements + $def->isGlobal = ( + $node instanceof Tolerant\Node\Statement\InterfaceDeclaration || + $node instanceof Tolerant\Node\Statement\ClassDeclaration || + $node instanceof Tolerant\Node\Statement\TraitDeclaration || + + ($node instanceof Tolerant\Node\Statement\NamespaceDefinition && $node->name !== null) || + + $node instanceof Tolerant\Node\Statement\FunctionDeclaration || + + ($node instanceof Tolerant\Node\ConstElement && $node->parent->parent instanceof Tolerant\Node\Statement\ConstDeclaration) + ); + + // Static methods and static property declarations + $def->isStatic = ( + ($node instanceof Tolerant\Node\MethodDeclaration && $node->isStatic()) || + + (($propertyDeclaration = ParserHelpers::tryGetPropertyDeclaration($node)) !== null + && $propertyDeclaration->isStatic()) + ); + + 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 should be better represented in the parser API + $node->interfaceBaseClause !== null && $node->interfaceBaseClause->interfaceNameList !== null + ) { $def->extends = []; - if ($node->extends) { - $def->extends[] = (string)$node->extends; - } - } else if ($node instanceof Node\Stmt\Interface_) { - $def->extends = []; - foreach ($node->extends as $n) { - $def->extends[] = (string)$n; + foreach ($node->interfaceBaseClause->interfaceNameList->getValues() as $n) { + $def->extends[] = (string)$n->getResolvedName(); } } + $def->symbolInformation = SymbolInformation::fromNode($node, $fqn); - $def->type = $this->getTypeFromNode($node); - $def->declarationLine = $this->getDeclarationLineFromNode($node); - $def->documentation = $this->getDocumentationFromNode($node); -// var_dump($def); + + if ($def->symbolInformation !== null) { + $def->type = $this->getTypeFromNode($node); + $def->declarationLine = $this->getDeclarationLineFromNode($node); + $def->documentation = $this->getDocumentationFromNode($node); + } + return $def; } /** * Given any node, returns the Definition object of the symbol that is referenced * - * @param Node $node Any reference node + * @param Tolerant\Node $node Any reference node * @return Definition|null */ public function resolveReferenceNodeToDefinition($node) { - // Variables are not indexed globally, as they stay in the file scope anyway - if ($node instanceof Node\Expr\Variable) { - // Resolve $this - if ($node->name === 'this' && $fqn = $this->getContainingClassFqn($node)) { + $parent = $node->parent; + // Variables are not indexed globally, as they stay in the file scope anyway. + // Ignore variable nodes that are part of ScopedPropertyAccessExpression, + // as the scoped property access expression node is handled separately. + if ($node instanceof Tolerant\Node\Expression\Variable && + !($parent instanceof Tolerant\Node\Expression\ScopedPropertyAccessExpression)) + { + // Resolve $this to the containing class definition. + if ($node->getName() === 'this' && $fqn = $this->getContainingClassFqn($node)) { return $this->index->getDefinition($fqn, false); } + // Resolve the variable to a definition node (assignment, param or closure use) - $defNode = self::resolveVariableToNode($node); + $defNode = $this->resolveVariableToNode($node); if ($defNode === null) { return null; } @@ -165,8 +251,8 @@ class DefinitionResolver } // 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 - $parent = $node->getAttribute('parentNode'); - $globalFallback = $parent instanceof Node\Expr\ConstFetch || $parent instanceof Node\Expr\FuncCall; + // TODO - verify that this is not a method + $globalFallback = ParserHelpers::isConstantFetch($node) || $parent instanceof Tolerant\Node\Expression\CallExpression; // Return the Definition object from the index index return $this->index->getDefinition($fqn, $globalFallback); } @@ -178,143 +264,191 @@ class DefinitionResolver * @param Node $node * @return string|null */ - public function resolveReferenceNodeToFqn($node) - { - $parent = $node->getAttribute('parentNode'); + public function resolveReferenceNodeToFqn($node) { + // TODO all name tokens should be a part of a node + if ($node instanceof Tolerant\Node\QualifiedName) { + return $this->resolveQualifiedNameNodeToFqn($node); + } - if ( - $node instanceof Node\Name && ( - $parent instanceof Node\Stmt\ClassLike - || $parent instanceof Node\Param - || $parent instanceof Node\FunctionLike - || $parent instanceof Node\Stmt\GroupUse - || $parent instanceof Node\Expr\New_ - || $parent instanceof Node\Expr\StaticCall - || $parent instanceof Node\Expr\ClassConstFetch - || $parent instanceof Node\Expr\StaticPropertyFetch - || $parent instanceof Node\Expr\Instanceof_ - ) + else if ($node instanceof Tolerant\Node\Expression\MemberAccessExpression) { + return $this->resolveMemberAccessExpressionNodeToFqn($node); + } + else if (ParserHelpers::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) ) { - // For extends, implements, type hints and classes of classes of static calls use the name directly - $name = (string)$node; - // Only the name node should be considered a reference, not the UseUse node itself - } else if ($parent instanceof Node\Stmt\UseUse) { - $name = (string)$parent->name; - $grandParent = $parent->getAttribute('parentNode'); - if ($grandParent instanceof Node\Stmt\GroupUse) { - $name = $grandParent->prefix . '\\' . $name; - } else if ($grandParent instanceof Node\Stmt\Use_ && $grandParent->type === Node\Stmt\Use_::TYPE_FUNCTION) { - $name .= '()'; - } - } else if ($node instanceof Node\Expr\MethodCall || $node instanceof Node\Expr\PropertyFetch) { - if ($node->name instanceof Node\Expr) { - // Cannot get definition if right-hand side is expression - return null; - } - // Get the type of the left-hand expression - $varType = $this->resolveExpressionNodeToType($node->var); - 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; - break; - } - } - } - 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($node); - } 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)$node->name; - if ($node instanceof Node\Expr\MethodCall) { - $memberSuffix .= '()'; - } - // Find the right class that implements the member - $implementorFqns = [$classFqn]; - 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; - } - } - } - return $classFqn . $memberSuffix; - } else if ($parent instanceof Node\Expr\FuncCall && $node instanceof Node\Name) { - if ($parent->name instanceof Node\Expr) { - return null; - } - $name = (string)($node->getAttribute('namespacedName') ?? $parent->name); - } else if ($parent instanceof Node\Expr\ConstFetch && $node instanceof Node\Name) { - $name = (string)($node->getAttribute('namespacedName') ?? $parent->name); + return $this->resolveScopedPropertyAccessExpressionNodeToFqn($node); } else if ( - $node instanceof Node\Expr\ClassConstFetch - || $node instanceof Node\Expr\StaticPropertyFetch - || $node instanceof Node\Expr\StaticCall + // A\B::$c - static property access expression + $node->parent instanceof Tolerant\Node\Expression\ScopedPropertyAccessExpression ) { - if ($node->class instanceof Node\Expr || $node->name instanceof Node\Expr) { - // Cannot get definition of dynamic names - return null; - } - $className = (string)$node->class; - if ($className === 'self' || $className === 'static' || $className === 'parent') { - // self and static are resolved to the containing class - $classNode = getClosestNode($node, Node\Stmt\Class_::class); - if ($classNode === null) { + return $this->resolveScopedPropertyAccessExpressionNodeToFqn($node->parent); + } + + return null; + } + + private function resolveQualifiedNameNodeToFqn(Tolerant\Node\QualifiedName $node) { + $parent = $node->parent; + + if ($parent instanceof Tolerant\Node\TraitSelectOrAliasClause) { + return null; + } + // 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; } - if ($className === 'parent') { - // parent is resolved to the parent class - if (!isset($n->extends)) { - 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 .= '()'; } - $className = (string)$classNode->extends; - } else { - $className = (string)$classNode->namespacedName; + } + 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) { + $name .= '()'; } } - if ($node instanceof Node\Expr\StaticPropertyFetch) { - $name = (string)$className . '::$' . $node->name; - } else { - $name = (string)$className . '::' . $node->name; + + return $name; + } + + // For extends, implements, type hints and classes of classes of static calls use the name directly + $name = (string) ($node->getResolvedName() ?? $node->getNamespacedName()); + + if ($node->parent instanceof Tolerant\Node\Expression\CallExpression) { + $name .= '()'; + } + return $name; + } + + 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); + + 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; + break; + } } + } + 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]; + + 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; + } + } + } + + return $classFqn . $memberSuffix; + } + + private function resolveScopedPropertyAccessExpressionNodeToFqn(Tolerant\Node\Expression\ScopedPropertyAccessExpression $scoped) { + 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; } - if (!isset($name)) { - return null; + + 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) { + return null; + } + if ($className === 'parent') { + // parent is resolved to the parent class + if (!isset($classNode->extends)) { + return null; + } + $className = (string)$classNode->extends->getResolvedName(); + } else { + $className = (string)$classNode->getNamespacedName(); + } + } elseif ($scoped->scopeResolutionQualifier instanceof Tolerant\Node\QualifiedName) { + $className = $scoped->scopeResolutionQualifier->getResolvedName(); } - if ( - $node instanceof Node\Expr\MethodCall - || $node instanceof Node\Expr\StaticCall - || $parent instanceof Node\Expr\FuncCall - ) { + if ($scoped->memberName instanceof Tolerant\Node\Expression\Variable) { + if ($scoped->parent instanceof Tolerant\Node\Expression\CallExpression) { + return null; + } + $memberName = $scoped->memberName->getName(); + if (empty($memberName)) { + return null; + } + $name = (string)$className . '::$' . $memberName; + } else { + $name = (string)$className . '::' . $scoped->memberName->getText($scoped->getFileContents()); + } + if ($scoped->parent instanceof Tolerant\Node\Expression\CallExpression) { $name .= '()'; } return $name; @@ -324,16 +458,16 @@ class DefinitionResolver * 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 + * @param Tolerant\Node $node * @return string|null */ - private static function getContainingClassFqn(Node $node) + private static function getContainingClassFqn(Tolerant\Node $node) { - $classNode = getClosestNode($node, Node\Stmt\Class_::class); - if ($classNode === null || $classNode->isAnonymous()) { + $classNode = $node->getFirstAncestor(Tolerant\Node\Statement\ClassDeclaration::class); + if ($classNode === null) { return null; } - return (string)$classNode->namespacedName; + return (string)$classNode->getNamespacedName(); } /** @@ -345,28 +479,35 @@ class DefinitionResolver public function resolveVariableToNode($var) { $n = $var; - // When a use is passed, start outside the closure to not return immediatly - if ($var instanceof Node\Expr\ClosureUse) { - $n = $var->getAttribute('parentNode')->getAttribute('parentNode'); - $name = $var->var; - } else if ($var instanceof Node\Expr\Variable || $var instanceof Node\Param) { - $name = $var->name; + // When a use is passed, start outside the closure to not return immediately + // Use variable vs variable parsing? + if ($var instanceof Tolerant\Node\UseVariableName) { + $n = $var->getFirstAncestor(Tolerant\Node\Expression\AnonymousFunctionCreationExpression::class)->parent; + $name = $var->getName(); + } else if ($var instanceof Tolerant\Node\Expression\Variable || $var instanceof Tolerant\Node\Parameter) { + $name = $var->getName(); } 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 - if ($n instanceof Node\FunctionLike) { - foreach ($n->getParams() as $param) { - if ($param->name === $name) { - return $param; + if (ParserHelpers::isFunctionLike($n)) { + 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\Expr\Closure) { - foreach ($n->uses as $use) { - if ($use->var === $name) { + if ($n instanceof Tolerant\Node\Expression\AnonymousFunctionCreationExpression && + $n->anonymousFunctionUseClause !== null && + $n->anonymousFunctionUseClause->useVariableNameList !== null) { + foreach ($n->anonymousFunctionUseClause->useVariableNameList->getElements() as $use + ) { + if ($use->getName() === $name) { return $use; } } @@ -374,15 +515,19 @@ class DefinitionResolver break; } // Check each previous sibling node for a variable assignment to that variable - while ($n->getAttribute('previousSibling') && $n = $n->getAttribute('previousSibling')) { + while (($prevSibling = $n->getPreviousSibling()) !== null && $n = $prevSibling) { + if ($n instanceof Tolerant\Node\Statement\ExpressionStatement) { + $n = $n->expression; + } if ( - ($n instanceof Node\Expr\Assign || $n instanceof Node\Expr\AssignOp) - && $n->var instanceof Node\Expr\Variable && $n->var->name === $name + // TODO - clean this up + ($n instanceof Tolerant\Node\Expression\AssignmentExpression && $n->operator->kind === Tolerant\TokenKind::EqualsToken) + && $n->leftOperand instanceof Tolerant\Node\Expression\Variable && $n->leftOperand->getName() === $name ) { return $n; } } - } while (isset($n) && $n = $n->getAttribute('parentNode')); + } while (isset($n) && $n = $n->parent); // Return null if nothing was found return null; } @@ -391,224 +536,298 @@ class DefinitionResolver * Given an expression node, resolves that expression recursively to a type. * If the type could not be resolved, returns Types\Mixed. * - * @param \PhpParser\Node\Expr $expr - * @return \phpDocumentor\Reflection\Type + * @param Tolerant\Node\Expression $expr + * @return \phpDocumentor\Reflection\Type|null */ - public function resolveExpressionNodeToType($expr): Type + public function resolveExpressionNodeToType($expr) { - if ($expr instanceof Node\Expr\Variable || $expr instanceof Node\Expr\ClosureUse) { - if ($expr instanceof Node\Expr\Variable && $expr->name === 'this') { + if ($expr == null || $expr instanceof Tolerant\MissingToken || $expr instanceof Tolerant\SkippedToken) { + // TODO some members are null or Missing/SkippedToken + // How do we handle this more generally? + return new Types\Mixed; + } + + // PARENTHESIZED EXPRESSION + // Retrieve inner expression from parenthesized expression + while ($expr instanceof Tolerant\Node\Expression\ParenthesizedExpression) { + $expr = $expr->expression; + } + + // VARIABLE + // $this -> Type\this + // $myVariable -> type of corresponding assignment expression + if ($expr instanceof Tolerant\Node\Expression\Variable || $expr instanceof Tolerant\Node\UseVariableName) { + if ($expr->getName() === 'this') { return new Types\This; } - // Find variable definition - + // Find variable definition (parameter or assignment expression) $defNode = $this->resolveVariableToNode($expr); - if ($defNode instanceof Node\Expr) { + if ($defNode instanceof Tolerant\Node\Expression\AssignmentExpression || $defNode instanceof Tolerant\Node\UseVariableName) { return $this->resolveExpressionNodeToType($defNode); } - if ($defNode instanceof Node\Param) { + if ($defNode instanceof Tolerant\Node\Parameter) { return $this->getTypeFromNode($defNode); } } - if ($expr instanceof Node\Expr\FuncCall) { + + // FUNCTION CALL + // Function calls are resolved to type corresponding to their FQN + if ($expr instanceof Tolerant\Node\Expression\CallExpression && + !( + $expr->callableExpression instanceof Tolerant\Node\Expression\ScopedPropertyAccessExpression || + $expr->callableExpression instanceof Tolerant\Node\Expression\MemberAccessExpression) + ) { + // Find the function definition - if ($expr->name instanceof Node\Expr) { + if ($expr->callableExpression instanceof Tolerant\Node\Expression) { // Cannot get type for dynamic function call return new Types\Mixed; } - $fqn = (string)($expr->getAttribute('namespacedName') ?? $expr->name); - $def = $this->index->getDefinition($fqn, true); - if ($def !== null) { - return $def->type; - } - } - if ($expr instanceof Node\Expr\ConstFetch) { - if (strtolower((string)$expr->name) === 'true' || strtolower((string)$expr->name) === 'false') { - return new Types\Boolean; - } - if (strtolower((string)$expr->name) === 'null') { - return new Types\Null_; - } - // Resolve constant - $fqn = (string)($expr->getAttribute('namespacedName') ?? $expr->name); - $def = $this->index->getDefinition($fqn, true); - if ($def !== null) { - return $def->type; - } - } - if ($expr instanceof Node\Expr\MethodCall || $expr instanceof Node\Expr\PropertyFetch) { - if ($expr->name instanceof Node\Expr) { - return new Types\Mixed; - } - // Resolve object - $objType = $this->resolveExpressionNodeToType($expr->var); - // var_dump((string)$expr->var->name); -// var_dump($objType); - 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); - // var_dump($classFqn); - } - $fqn = $classFqn . '->' . $expr->name; - if ($expr instanceof Node\Expr\MethodCall) { - $fqn .= '()'; - } - $def = $this->index->getDefinition($fqn); + + if ($expr->callableExpression instanceof Tolerant\Node\QualifiedName) { + $fqn = $expr->callableExpression->getResolvedName() ?? $expr->callableExpression->getNamespacedName(); + $fqn .= '()'; + $def = $this->index->getDefinition($fqn, true); if ($def !== null) { return $def->type; } } } - if ( - $expr instanceof Node\Expr\StaticCall - || $expr instanceof Node\Expr\StaticPropertyFetch - || $expr instanceof Node\Expr\ClassConstFetch - ) { - $classType = self::resolveClassNameToType($expr->class); - if (!($classType instanceof Types\Object_) || $classType->getFqsen() === null || $expr->name instanceof Node\Expr) { + + // 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_; + } + } + + // CONSTANT FETCH + // Resolve constants by retrieving corresponding definition type from FQN + if (ParserHelpers::isConstantFetch($expr)) { + $fqn = (string)$expr->getNamespacedName(); + $def = $this->index->getDefinition($fqn, true); + if ($def !== null) { + return $def->type; + } + } + + // MEMBER ACCESS EXPRESSION + if ($expr instanceof Tolerant\Node\Expression\MemberAccessExpression) { + if ($expr->memberName instanceof Tolerant\Node\Expression) { + return new Types\Mixed; + } + $var = $expr->dereferencableExpression; + + // 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); + } + $fqn = $classFqn . '->' . $expr->memberName->getText($expr->getFileContents()); + if ($expr->parent instanceof Tolerant\Node\Expression\CallExpression) { + $fqn .= '()'; + } + $def = $this->index->getDefinition($fqn); + if ($def !== null) { + return $def->type; + } + } + } + + // SCOPED PROPERTY ACCESS EXPRESSION + if ($expr instanceof Tolerant\Node\Expression\ScopedPropertyAccessExpression) { + $classType = $this->resolveClassNameToType($expr->scopeResolutionQualifier); + if (!($classType instanceof Types\Object_) || $classType->getFqsen() === null) { return new Types\Mixed; } $fqn = substr((string)$classType->getFqsen(), 1) . '::'; - if ($expr instanceof Node\Expr\StaticPropertyFetch) { - $fqn .= '$'; - } - $fqn .= $expr->name; - if ($expr instanceof Node\Expr\StaticCall) { + + // TODO is there a cleaner way to do this? + $fqn .= $expr->memberName->getText() ?? $expr->memberName->getText($expr->getFileContents()); + if ($expr->parent instanceof Tolerant\Node\Expression\CallExpression) { $fqn .= '()'; } + $def = $this->index->getDefinition($fqn); if ($def === null) { return new Types\Mixed; } return $def->type; } - if ($expr instanceof Node\Expr\New_) { - return self::resolveClassNameToType($expr->class); + + // 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" + if ($expr instanceof Tolerant\Node\Expression\ObjectCreationExpression) { + return $this->resolveClassNameToType($expr->classTypeDesignator); } - if ($expr instanceof Node\Expr\Clone_ || $expr instanceof Node\Expr\Assign) { - return $this->resolveExpressionNodeToType($expr->expr); + + // CLONE EXPRESSION + // clone($a) => resolves to the type of $a + if ($expr instanceof Tolerant\Node\Expression\CloneExpression) { + return $this->resolveExpressionNodeToType($expr->expression); } - if ($expr instanceof Node\Expr\Ternary) { + + // ASSIGNMENT EXPRESSION + // $a = $myExpression => resolves to the type of the right-hand operand + if ($expr instanceof Tolerant\Node\Expression\AssignmentExpression) { + return $this->resolveExpressionNodeToType($expr->rightOperand); + } + + // TERNARY EXPRESSION + // $condition ? $ifExpression : $elseExpression => reslves to type of $ifCondition or $elseExpression + // $condition ?: $elseExpression => resolves to type of $condition or $elseExpression + if ($expr instanceof Tolerant\Node\Expression\TernaryExpression) { // ?: - if ($expr->if === null) { + if ($expr->ifExpression === null) { return new Types\Compound([ - $this->resolveExpressionNodeToType($expr->cond), - $this->resolveExpressionNodeToType($expr->else) + $this->resolveExpressionNodeToType($expr->condition), // TODO: why? + $this->resolveExpressionNodeToType($expr->elseExpression) ]); } // Ternary is a compound of the two possible values return new Types\Compound([ - $this->resolveExpressionNodeToType($expr->if), - $this->resolveExpressionNodeToType($expr->else) + $this->resolveExpressionNodeToType($expr->ifExpression), + $this->resolveExpressionNodeToType($expr->elseExpression) ]); } - if ($expr instanceof Node\Expr\BinaryOp\Coalesce) { + + // NULL COALLESCE + // $rightOperand ?? $leftOperand => resolves to type of $rightOperand or $leftOperand + if ($expr instanceof Tolerant\Node\Expression\BinaryExpression && $expr->operator->kind === Tolerant\TokenKind::QuestionQuestionToken) { // ?? operator return new Types\Compound([ - $this->resolveExpressionNodeToType($expr->left), - $this->resolveExpressionNodeToType($expr->right) + $this->resolveExpressionNodeToType($expr->leftOperand), + $this->resolveExpressionNodeToType($expr->rightOperand) ]); } + + // BOOLEAN EXPRESSIONS: resolve to Types\Boolean + // (bool) $expression + // !$expression + // empty($var) + // isset($var) + // >, >=, <, <=, &&, ||, AND, OR, XOR, ==, ===, !=, !== if ( - $expr instanceof Node\Expr\Instanceof_ - || $expr instanceof Node\Expr\Cast\Bool_ - || $expr instanceof Node\Expr\BooleanNot - || $expr instanceof Node\Expr\Empty_ - || $expr instanceof Node\Expr\Isset_ - || $expr instanceof Node\Expr\BinaryOp\Greater - || $expr instanceof Node\Expr\BinaryOp\GreaterOrEqual - || $expr instanceof Node\Expr\BinaryOp\Smaller - || $expr instanceof Node\Expr\BinaryOp\SmallerOrEqual - || $expr instanceof Node\Expr\BinaryOp\BooleanAnd - || $expr instanceof Node\Expr\BinaryOp\BooleanOr - || $expr instanceof Node\Expr\BinaryOp\LogicalAnd - || $expr instanceof Node\Expr\BinaryOp\LogicalOr - || $expr instanceof Node\Expr\BinaryOp\LogicalXor - || $expr instanceof Node\Expr\BinaryOp\NotEqual - || $expr instanceof Node\Expr\BinaryOp\NotIdentical + ParserHelpers::isBooleanExpression($expr) + + || ($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 ) { return new Types\Boolean; } + + // 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__) if ( - $expr instanceof Node\Expr\Cast\String_ - || $expr instanceof Node\Expr\BinaryOp\Concat - || $expr instanceof Node\Expr\AssignOp\Concat - || $expr instanceof Node\Scalar\String_ - || $expr instanceof Node\Scalar\Encapsed - || $expr instanceof Node\Scalar\EncapsedStringPart - || $expr instanceof Node\Scalar\MagicConst\Class_ - || $expr instanceof Node\Scalar\MagicConst\Dir - || $expr instanceof Node\Scalar\MagicConst\Function_ - || $expr instanceof Node\Scalar\MagicConst\Method - || $expr instanceof Node\Scalar\MagicConst\Namespace_ - || $expr instanceof Node\Scalar\MagicConst\Trait_ + ($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) ) { return new Types\String_; } + + // 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] /= if ( - $expr instanceof Node\Expr\BinaryOp\Minus - || $expr instanceof Node\Expr\BinaryOp\Plus - || $expr instanceof Node\Expr\BinaryOp\Pow - || $expr instanceof Node\Expr\BinaryOp\Mul + $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 || + + // Assignment expressions (TODO: consider making this a type of AssignmentExpression rather than kind of BinaryExpression) + $operator === Tolerant\TokenKind::AsteriskEqualsToken|| + $operator === Tolerant\TokenKind::AsteriskAsteriskEqualsToken || + $operator === Tolerant\TokenKind::MinusEqualsToken || + $operator === Tolerant\TokenKind::PlusEqualsToken + ) ) { if ( - $this->resolveExpressionNodeToType($expr->left) instanceof Types\Integer - && $this->resolveExpressionNodeToType($expr->right) instanceof Types\Integer + $this->resolveExpressionNodeToType($expr->leftOperand) instanceof Types\Integer + && $this->resolveExpressionNodeToType($expr->rightOperand) instanceof Types\Integer ) { return new Types\Integer; } return new Types\Float_; - } - - if ( - $expr instanceof Node\Expr\AssignOp\Minus - || $expr instanceof Node\Expr\AssignOp\Plus - || $expr instanceof Node\Expr\AssignOp\Pow - || $expr instanceof Node\Expr\AssignOp\Mul + } else if ( + $expr instanceof Tolerant\Node\Expression\BinaryExpression && + $expr->operator->kind === Tolerant\TokenKind::SlashEqualsToken ) { - if ( - $this->resolveExpressionNodeToType($expr->var) instanceof Types\Integer - && $this->resolveExpressionNodeToType($expr->expr) instanceof Types\Integer - ) { - return new Types\Integer; - } return new Types\Float_; } + // INTEGER EXPRESSIONS: resolve to Types\Integer + // [literal] 1 + // [operator] <=>, &, ^, | + // TODO: Magic constants (__LINE__) if ( - $expr instanceof Node\Scalar\LNumber - || $expr instanceof Node\Expr\Cast\Int_ - || $expr instanceof Node\Scalar\MagicConst\Line - || $expr instanceof Node\Expr\BinaryOp\Spaceship - || $expr instanceof Node\Expr\BinaryOp\BitwiseAnd - || $expr instanceof Node\Expr\BinaryOp\BitwiseOr - || $expr instanceof Node\Expr\BinaryOp\BitwiseXor + // TODO: consider different Node types of float/int, also better property name (not "children") + ($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) + ) ) { return new Types\Integer; } + + // FLOAT EXPRESSIONS: resolve to Types\Float + // [literal] 1.5 + // [operator] / + // [cast] (double) if ( - $expr instanceof Node\Expr\BinaryOp\Div - || $expr instanceof Node\Scalar\DNumber - || $expr instanceof Node\Expr\Cast\Double + $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) ) { return new Types\Float_; } - if ($expr instanceof Node\Expr\Array_) { + + // ARRAY CREATION EXPRESSION: + // Resolve to Types\Array (Types\Compound of value and key types) + // [a, b, c] + // [1=>"hello", "hi"=>1, 4=>[]]s + if ($expr instanceof Tolerant\Node\Expression\ArrayCreationExpression) { $valueTypes = []; $keyTypes = []; - foreach ($expr->items as $item) { - $valueTypes[] = $this->resolveExpressionNodeToType($item->value); - $keyTypes[] = $item->key ? $this->resolveExpressionNodeToType($item->key) : new Types\Integer; + if ($expr->arrayElements !== null) { + foreach ($expr->arrayElements->getElements() as $item) { + $valueTypes[] = $this->resolveExpressionNodeToType($item->elementValue); + $keyTypes[] = $item->elementKey ? $this->resolveExpressionNodeToType($item->elementKey) : new Types\Integer; + } } $valueTypes = array_unique($keyTypes); $keyTypes = array_unique($keyTypes); @@ -628,54 +847,68 @@ class DefinitionResolver } return new Types\Array_($valueType, $keyType); } - if ($expr instanceof Node\Expr\ArrayDimFetch) { - $varType = $this->resolveExpressionNodeToType($expr->var); + + // SUBSCRIPT EXPRESSION + // $myArray[3] + // $myArray{"hello"} + if ($expr instanceof Tolerant\Node\Expression\SubscriptExpression) { + $varType = $this->resolveExpressionNodeToType($expr->postfixExpression); if (!($varType instanceof Types\Array_)) { return new Types\Mixed; } return $varType->getValueType(); } - if ($expr instanceof Node\Expr\Include_) { + + // SCRIPT INCLUSION EXPRESSION + // include, require, include_once, require_once + if ($expr instanceof Tolerant\Node\Expression\ScriptInclusionExpression) { // TODO: resolve path to PhpDocument and find return statement return new Types\Mixed; } + + if ($expr instanceof Tolerant\Node\QualifiedName) { + return $this->resolveClassNameToType($expr); + } + return new Types\Mixed; } + /** * 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 * - * @param Node $class + * @param Tolerant\Node || Tolerant\Token $class * @return Type */ - private function resolveClassNameToType(Node $class): Type + public function resolveClassNameToType($class): Type { - if ($class instanceof Node\Expr) { + if ($class instanceof Tolerant\Node\Expression) { return new Types\Mixed; } - if ($class instanceof Node\Stmt\Class_) { + if ($class instanceof Tolerant\Token && $class->kind === Tolerant\TokenKind::ClassKeyword) { // Anonymous class return new Types\Object_; } - $className = (string)$class; + $className = (string)$class->getResolvedName(); + if ($className === 'static') { return new Types\Static_; } if ($className === 'self' || $className === 'parent') { - $classNode = getClosestNode($class, Node\Stmt\Class_::class); + $classNode = $class->getFirstAncestor(Tolerant\Node\Statement\ClassDeclaration::class); if ($className === 'parent') { - if ($classNode === null || $classNode->extends === null) { + if ($classNode === null || $classNode->classBaseClause === null) { return new Types\Object_; } // parent is resolved to the parent class - $classFqn = (string)$classNode->extends; + $classFqn = (string)$classNode->classBaseClause->baseClass->getResolvedName(); } else { if ($classNode === null) { return new Types\Self_; } // self is resolved to the containing class - $classFqn = (string)$classNode->namespacedName; + $classFqn = (string)$classNode->getNamespacedName(); } return new Types\Object_(new Fqsen('\\' . $classFqn)); } @@ -693,48 +926,62 @@ class DefinitionResolver * If it is unknown, will be Types\Mixed. * Returns null if the node does not have a type. * - * @param Node $node + * @param Tolerant\Node $node * @return \phpDocumentor\Reflection\Type|null */ public function getTypeFromNode($node) { - if ($node instanceof Node\Param) { + // PARAMETERS + // Get the type of the parameter: + // 1. Doc block + // 2. Parameter type and default + if ($node instanceof Tolerant\Node\Parameter) { // Parameters - $docBlock = $node->getAttribute('parentNode')->getAttribute('docBlock'); - if ($docBlock !== null) { - // Use @param tag - foreach ($docBlock->getTagsByName('param') as $paramTag) { - if ($paramTag->getVariableName() === $node->name) { - if ($paramTag->getType() === null) { - break; - } - return $paramTag->getType(); - } - } + // Get the doc block for the the function call + // /** + // * @param MyClass $myParam + // */ + // function foo($a) + $functionLikeDeclaration = ParserHelpers::getFunctionLikeDeclarationFromParameter($node); + $variableName = $node->getName(); + $docBlock = $this->getDocBlock($functionLikeDeclaration); + + $parameterDocBlockTag = $this->tryGetDocBlockTagForParameter($docBlock, $variableName); + if ($parameterDocBlockTag !== null && ($type = $parameterDocBlockTag->getType())) { + // Doc block comments supercede all other forms of type inference + return $type; } - $type = null; - if ($node->type !== null) { + + // function foo(MyClass $a) + if ($node->typeDeclaration !== null) { // Use PHP7 return type hint - if (is_string($node->type)) { + if ($node->typeDeclaration instanceof Tolerant\Token) { // Resolve a string like "bool" to a type object - $type = $this->typeResolver->resolve($node->type); + $type = $this->typeResolver->resolve($node->typeDeclaration->getText($node->getFileContents())); } else { - $type = new Types\Object_(new Fqsen('\\' . (string)$node->type)); + $type = new Types\Object_(new Fqsen('\\' . (string)$node->typeDeclaration->getResolvedName())); } } + // function foo($a = 3) if ($node->default !== null) { $defaultType = $this->resolveExpressionNodeToType($node->default); if (isset($type) && !is_a($type, get_class($defaultType))) { - $type = new Types\Compound([$type, $defaultType]); - } else { - $type = $defaultType; + // TODO - verify it is worth creating a compound type + return new Types\Compound([$type, $defaultType]); } + $type = $defaultType; } return $type ?? new Types\Mixed; } - if ($node instanceof Node\FunctionLike) { + + // FUNCTIONS AND METHODS + // Get the return type + // 1. doc block + // 2. return type hint + // 3. TODO: infer from return statements + if (ParserHelpers::isFunctionLike($node)) { // Functions/methods - $docBlock = $node->getAttribute('docBlock'); + $docBlock = $this->getDocBlock($node); if ( $docBlock !== null && !empty($returnTags = $docBlock->getTagsByName('return')) @@ -743,57 +990,57 @@ class DefinitionResolver // Use @return tag return $returnTags[0]->getType(); } - if ($node->returnType !== null) { + if ($node->returnType !== null && !($node->returnType instanceof Tolerant\MissingToken)) { // Use PHP7 return type hint - if (is_string($node->returnType)) { + if ($node->returnType instanceof Tolerant\Token) { // Resolve a string like "bool" to a type object - return $this->typeResolver->resolve($node->returnType); + return $this->typeResolver->resolve($node->returnType->getText($node->getFileContents())); } - return new Types\Mixed; -// return new Types\Object_(new Fqsen('\\' . (string)$node->returnType)); + return new Types\Object_(new Fqsen('\\' . (string)$node->returnType->getResolvedName())); } // Unknown return type return new Types\Mixed; } - if ($node instanceof Node\Expr\Variable) { - $node = $node->getAttribute('parentNode'); - } + + // PROPERTIES, CONSTS, CLASS CONSTS, ASSIGNMENT EXPRESSIONS + // Get the documented type the assignment resolves to. if ( - $node instanceof Node\Stmt\PropertyProperty - || $node instanceof Node\Const_ - || $node instanceof Node\Expr\Assign - || $node instanceof Node\Expr\AssignOp - ) { - if ($node instanceof Node\Stmt\PropertyProperty || $node instanceof Node\Const_) { - $docBlockHolder = $node->getAttribute('parentNode'); - } else { - $docBlockHolder = $node; - } + ($declarationNode = + ParserHelpers::tryGetPropertyDeclaration($node) ?? + ParserHelpers::tryGetConstOrClassConstDeclaration($node) + ) !== null || + ($node = $node->parent) instanceof Tolerant\Node\Expression\AssignmentExpression) + { + $declarationNode = $declarationNode ?? $node; + // Property, constant or variable // Use @var tag if ( - isset($docBlockHolder) - && ($docBlock = $docBlockHolder->getAttribute('docBlock')) + ($docBlock = $this->getDocBlock($declarationNode)) && !empty($varTags = $docBlock->getTagsByName('var')) && ($type = $varTags[0]->getType()) ) { return $type; } + // Resolve the expression - if ($node instanceof Node\Stmt\PropertyProperty) { - if ($node->default) { - return $this->resolveExpressionNodeToType($node->default); + if ($declarationNode instanceof Tolerant\Node\PropertyDeclaration) { + // TODO should have default + if (isset($node->parent->rightOperand)) { + return $this->resolveExpressionNodeToType($node->parent->rightOperand); } - } else if ($node instanceof Node\Const_) { - return $this->resolveExpressionNodeToType($node->value); - } else { - return $this->resolveExpressionNodeToType($node); + } else if ($node instanceof Tolerant\Node\ConstElement) { + return $this->resolveExpressionNodeToType($node->assignment); + } else if ($node instanceof Tolerant\Node\Expression\AssignmentExpression) { + return $this->resolveExpressionNodeToType($node->rightOperand); } // TODO: read @property tags of class // TODO: Try to infer the type from default value / constant value // Unknown return new Types\Mixed; } + + // The node does not have a type return null; } @@ -801,60 +1048,134 @@ class DefinitionResolver * 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 * - * @param Node $node + * @param Tolerant\Node $node * @return string|null */ public static function getDefinedFqn($node) { - $parent = $node->getAttribute('parentNode'); + $parent = $node->parent; // Anonymous classes don't count as a definition - if ($node instanceof Node\Stmt\ClassLike && isset($node->name)) { - // Class, interface or trait declaration - return (string)$node->namespacedName; - } else if ($node instanceof Node\Name && $parent instanceof Node\Stmt\Namespace_) { - return (string)$node; - } else if ($node instanceof Node\Stmt\Function_) { + // 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) { + $name = (string) Tolerant\ResolvedName::buildName($node->name->nameParts, $node->getFileContents()); + return \count($name) > 0 ? $name : null; + } + // INPUT OUTPUT: + // namespace A\B; + // function a(); A\B\a(); + else if ($node instanceof Tolerant\Node\Statement\FunctionDeclaration) { // Function: use functionName() as the name - return (string)$node->namespacedName . '()'; - } else if ($node instanceof Node\Stmt\ClassMethod) { + $name = (string)$node->getNamespacedName(); + return \count($name) > 0 ? $name . '()' : null; + } + // 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) { // Class method: use ClassName->methodName() as name - $class = $node->getAttribute('parentNode'); + $class = $node->getFirstAncestor( + Tolerant\Node\Expression\ObjectCreationExpression::class, + Tolerant\Node\Statement\ClassDeclaration::class, + Tolerant\Node\Statement\InterfaceDeclaration::class, + Tolerant\Node\Statement\TraitDeclaration::class + ); if (!isset($class->name)) { // Ignore anonymous classes return null; } if ($node->isStatic()) { - return (string)$class->namespacedName . '::' . (string)$node->name . '()'; + return (string)$class->getNamespacedName() . '::' . $node->getName() . '()'; } else { - return (string)$class->namespacedName . '->' . (string)$node->name . '()'; + return (string)$class->getNamespacedName() . '->' . $node->getName() . '()'; } - } else if ($node instanceof Node\Stmt\PropertyProperty) { - $property = $node->getAttribute('parentNode'); - $class = $property->getAttribute('parentNode'); - if (!isset($class->name)) { - // Ignore anonymous classes + } + + // 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 // TODO verify variable name + // } + else if ( + ($propertyDeclaration = ParserHelpers::tryGetPropertyDeclaration($node)) !== null && + ($classDeclaration = + $node->getFirstAncestor( + Tolerant\Node\Expression\ObjectCreationExpression::class, + Tolerant\Node\Statement\ClassDeclaration::class, + Tolerant\Node\Statement\InterfaceDeclaration::class, + Tolerant\Node\Statement\TraitDeclaration::class + ) + ) !== null && isset($classDeclaration->name)) + { + $name = $node->getName(); + if ($propertyDeclaration->isStatic()) { + // Static Property: use ClassName::$propertyName as name + return (string)$classDeclaration->getNamespacedName() . '::$' . $name; + } + + // Instance Property: use ClassName->propertyName as name + return (string)$classDeclaration->getNamespacedName() . '->' . $name; + } + + // 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 + // } + else if (($constDeclaration = ParserHelpers::tryGetConstOrClassConstDeclaration($node)) !== null) { + if ($constDeclaration instanceof Tolerant\Node\Statement\ConstDeclaration) { + // Basic constant: use CONSTANT_NAME as name + return (string)$node->getNamespacedName(); + } + + // 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; } - if ($property->isStatic()) { - // Static Property: use ClassName::$propertyName as name - return (string)$class->namespacedName . '::$' . (string)$node->name; - } else { - // Instance Property: use ClassName->propertyName as name - return (string)$class->namespacedName . '->' . (string)$node->name; - } - } else if ($node instanceof Node\Const_) { - $parent = $node->getAttribute('parentNode'); - if ($parent instanceof Node\Stmt\Const_) { - // Basic constant: use CONSTANT_NAME as name - return (string)$node->namespacedName; - } - if ($parent instanceof Node\Stmt\ClassConst) { - // Class constant: use ClassName::CONSTANT_NAME as name - $class = $parent->getAttribute('parentNode'); - if (!isset($class->name) || $class->name instanceof Node\Expr) { - return null; - } - return (string)$class->namespacedName . '::' . $node->name; + 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; } } } diff --git a/src/FqnUtilities.php b/src/FqnUtilities.php index 40cb487..3e24819 100644 --- a/src/FqnUtilities.php +++ b/src/FqnUtilities.php @@ -20,7 +20,7 @@ class FqnUtilities if ($node instanceof Node) { return DefinitionResolver::getDefinedFqn($node); } elseif ($node instanceof Tolerant\Node) { - return TolerantDefinitionResolver::getDefinedFqn($node); + return DefinitionResolver::getDefinedFqn($node); } throw new \TypeError("Unspported Node class"); diff --git a/src/LanguageServer.php b/src/LanguageServer.php index 091651a..31348f9 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -101,7 +101,7 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher protected $projectIndex; /** - * @var TolerantDefinitionResolver + * @var DefinitionResolver */ protected $definitionResolver; @@ -187,7 +187,7 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher $this->globalIndex = new GlobalIndex($stubsIndex, $this->projectIndex); // The DefinitionResolver should look in stubs, the project source and dependencies - $this->definitionResolver = new TolerantDefinitionResolver($this->globalIndex); + $this->definitionResolver = new DefinitionResolver($this->globalIndex); $this->documentLoader = new PhpDocumentLoader( $this->contentRetriever, diff --git a/src/NodeVisitor/DefinitionCollector.php b/src/NodeVisitor/DefinitionCollector.php index 37eeebe..ead2d88 100644 --- a/src/NodeVisitor/DefinitionCollector.php +++ b/src/NodeVisitor/DefinitionCollector.php @@ -5,7 +5,7 @@ namespace LanguageServer\NodeVisitor; use PhpParser\{NodeVisitorAbstract, Node}; use LanguageServer\{ - Definition, FqnUtilities, TolerantDefinitionResolver + Definition, FqnUtilities, DefinitionResolver }; /** @@ -30,7 +30,7 @@ class DefinitionCollector extends NodeVisitorAbstract private $definitionResolver; - public function __construct(TolerantDefinitionResolver $definitionResolver) + public function __construct(DefinitionResolver $definitionResolver) { $this->definitionResolver = $definitionResolver; } diff --git a/src/NodeVisitor/ReferencesCollector.php b/src/NodeVisitor/ReferencesCollector.php index a64e633..cc20ffd 100644 --- a/src/NodeVisitor/ReferencesCollector.php +++ b/src/NodeVisitor/ReferencesCollector.php @@ -22,14 +22,14 @@ class ReferencesCollector extends NodeVisitorAbstract public $nodes = []; /** - * @var TolerantDefinitionResolver + * @var DefinitionResolver */ private $definitionResolver; /** - * @param TolerantDefinitionResolver $definitionResolver The definition resolver to resolve reference nodes to definitions + * @param DefinitionResolver $definitionResolver The definition resolver to resolve reference nodes to definitions */ - public function __construct(TolerantDefinitionResolver $definitionResolver) + public function __construct(DefinitionResolver $definitionResolver) { $this->definitionResolver = $definitionResolver; } diff --git a/src/TolerantParserHelpers.php b/src/ParserHelpers.php similarity index 99% rename from src/TolerantParserHelpers.php rename to src/ParserHelpers.php index 3390202..bd9c6e1 100644 --- a/src/TolerantParserHelpers.php +++ b/src/ParserHelpers.php @@ -5,7 +5,7 @@ namespace LanguageServer; use Microsoft\PhpParser as Tolerant; -class TolerantParserHelpers { +class ParserHelpers { public static function isConstantFetch(Tolerant\Node $node) : bool { $parent = $node->parent; return @@ -70,7 +70,7 @@ class TolerantParserHelpers { return false; } - + /** * Tries to get the parent property declaration given a Node * @param Tolerant\Node $node diff --git a/src/PhpDocument.php b/src/PhpDocument.php index 69b6a9b..878cfe9 100644 --- a/src/PhpDocument.php +++ b/src/PhpDocument.php @@ -35,7 +35,7 @@ class PhpDocument /** * The DefinitionResolver instance to resolve reference nodes to definitions * - * @var TolerantDefinitionResolver + * @var DefinitionResolver */ private $definitionResolver; @@ -99,7 +99,7 @@ class PhpDocument * @param Index $index The Index to register definitions and references to * @param Parser $parser The PHPParser instance * @param DocBlockFactory $docBlockFactory The DocBlockFactory instance to parse docblocks - * @param TolerantDefinitionResolver $definitionResolver The DefinitionResolver to resolve definitions to symbols in the workspace + * @param DefinitionResolver $definitionResolver The DefinitionResolver to resolve definitions to symbols in the workspace */ public function __construct( string $uri, @@ -107,7 +107,7 @@ class PhpDocument Index $index, $parser, DocBlockFactory $docBlockFactory, - TolerantDefinitionResolver $definitionResolver + DefinitionResolver $definitionResolver ) { $this->uri = $uri; $this->index = $index; @@ -158,7 +158,7 @@ class PhpDocument $this->definitions = null; $this->definitionNodes = null; - $treeAnalyzer = new TolerantTreeAnalyzer($this->parser, $content, $this->docBlockFactory, $this->definitionResolver, $this->uri); + $treeAnalyzer = new TreeAnalyzer($this->parser, $content, $this->docBlockFactory, $this->definitionResolver, $this->uri); $this->diagnostics = $treeAnalyzer->getDiagnostics(); diff --git a/src/PhpDocumentLoader.php b/src/PhpDocumentLoader.php index 8f94943..0c5bbca 100644 --- a/src/PhpDocumentLoader.php +++ b/src/PhpDocumentLoader.php @@ -48,20 +48,20 @@ class PhpDocumentLoader private $docBlockFactory; /** - * @var TolerantDefinitionResolver + * @var DefinitionResolver */ private $definitionResolver; /** * @param ContentRetriever $contentRetriever * @param ProjectIndex $projectIndex - * @param TolerantDefinitionResolver $definitionResolver + * @param DefinitionResolver $definitionResolver * @internal param ProjectIndex $project */ public function __construct( ContentRetriever $contentRetriever, ProjectIndex $projectIndex, - TolerantDefinitionResolver $definitionResolver + DefinitionResolver $definitionResolver ) { $this->contentRetriever = $contentRetriever; $this->projectIndex = $projectIndex; diff --git a/src/Protocol/SymbolInformation.php b/src/Protocol/SymbolInformation.php index a608ebd..7e6f489 100644 --- a/src/Protocol/SymbolInformation.php +++ b/src/Protocol/SymbolInformation.php @@ -3,6 +3,7 @@ namespace LanguageServer\Protocol; use PhpParser\Node; +use Microsoft\PhpParser as Tolerant; use Exception; /** @@ -42,53 +43,65 @@ class SymbolInformation /** * Converts a Node to a SymbolInformation * - * @param Node $node + * @param Tolerant\Node $node * @param string $fqn If given, $containerName will be extracted from it - * @return self|null + * @return SymbolInformation|null */ public static function fromNode($node, string $fqn = null) { - $parent = $node->getAttribute('parentNode'); $symbol = new self; - if ($node instanceof Node\Stmt\Class_) { + if ($node instanceof Tolerant\Node\Statement\ClassDeclaration) { $symbol->kind = SymbolKind::CLASS_; - } else if ($node instanceof Node\Stmt\Trait_) { + } else if ($node instanceof Tolerant\Node\Statement\TraitDeclaration) { $symbol->kind = SymbolKind::CLASS_; - } else if ($node instanceof Node\Stmt\Interface_) { + } else if ($node instanceof Tolerant\Node\Statement\InterfaceDeclaration) { $symbol->kind = SymbolKind::INTERFACE; - } else if ($node instanceof Node\Name && $parent instanceof Node\Stmt\Namespace_) { + } else if ($node instanceof Tolerant\Node\Statement\NamespaceDefinition) { $symbol->kind = SymbolKind::NAMESPACE; - } else if ($node instanceof Node\Stmt\Function_) { + } else if ($node instanceof Tolerant\Node\Statement\FunctionDeclaration) { $symbol->kind = SymbolKind::FUNCTION; - } else if ($node instanceof Node\Stmt\ClassMethod) { + } else if ($node instanceof Tolerant\Node\MethodDeclaration) { $symbol->kind = SymbolKind::METHOD; - } else if ($node instanceof Node\Stmt\PropertyProperty) { + } else if ($node instanceof Tolerant\Node\Expression\Variable && $node->getFirstAncestor(Tolerant\Node\PropertyDeclaration::class) !== null) { $symbol->kind = SymbolKind::PROPERTY; - } else if ($node instanceof Node\Const_) { + } else if ($node instanceof Tolerant\Node\ConstElement) { $symbol->kind = SymbolKind::CONSTANT; - } else if ( + } + + else if ( ( - ($node instanceof Node\Expr\Assign || $node instanceof Node\Expr\AssignOp) - && $node->var instanceof Node\Expr\Variable + ($node instanceof Tolerant\Node\Expression\AssignmentExpression) + && $node->leftOperand instanceof Tolerant\Node\Expression\Variable ) - || $node instanceof Node\Expr\ClosureUse - || $node instanceof Node\Param + || $node instanceof Tolerant\Node\UseVariableName + || $node instanceof Tolerant\Node\Parameter ) { $symbol->kind = SymbolKind::VARIABLE; } else { return null; } - if ($node instanceof Node\Name) { - $symbol->name = (string)$node; - } else if ($node instanceof Node\Expr\Assign || $node instanceof Node\Expr\AssignOp) { - $symbol->name = $node->var->name; - } else if ($node instanceof Node\Expr\ClosureUse) { - $symbol->name = $node->var; + + if ($node instanceof Tolerant\Node\Expression\AssignmentExpression) { + if ($node->leftOperand instanceof Tolerant\Node\Expression\Variable) { + $symbol->name = $node->leftOperand->getName(); + } elseif ($node->leftOperand instanceof Tolerant\Token) { + $symbol->name = trim($node->leftOperand->getText($node->getFileContents()), "$"); + } + + } else if ($node instanceof Tolerant\Node\UseVariableName) { + $symbol->name = $node->getName(); } else if (isset($node->name)) { - $symbol->name = (string)$node->name; + if ($node->name instanceof Tolerant\Node\QualifiedName) { + $symbol->name = (string)Tolerant\ResolvedName::buildName($node->name->nameParts, $node->getFileContents()); + } else { + $symbol->name = ltrim((string)$node->name->getText($node->getFileContents()), "$"); + } + } else if (isset($node->variableName)) { + $symbol->name = $node->variableName->getText($node); } else { return null; } + $symbol->location = Location::fromNode($node); if ($fqn !== null) { $parts = preg_split('/(::|->|\\\\)/', $fqn); diff --git a/src/Protocol/TolerantSymbolInformation.php b/src/Protocol/TolerantSymbolInformation.php deleted file mode 100644 index 769f1af..0000000 --- a/src/Protocol/TolerantSymbolInformation.php +++ /dev/null @@ -1,85 +0,0 @@ -kind = SymbolKind::CLASS_; - } else if ($node instanceof Tolerant\Node\Statement\TraitDeclaration) { - $symbol->kind = SymbolKind::CLASS_; - } else if ($node instanceof Tolerant\Node\Statement\InterfaceDeclaration) { - $symbol->kind = SymbolKind::INTERFACE; - } else if ($node instanceof Tolerant\Node\Statement\NamespaceDefinition) { - $symbol->kind = SymbolKind::NAMESPACE; - } else if ($node instanceof Tolerant\Node\Statement\FunctionDeclaration) { - $symbol->kind = SymbolKind::FUNCTION; - } else if ($node instanceof Tolerant\Node\MethodDeclaration) { - $symbol->kind = SymbolKind::METHOD; - } else if ($node instanceof Tolerant\Node\Expression\Variable && $node->getFirstAncestor(Tolerant\Node\PropertyDeclaration::class) !== null) { - $symbol->kind = SymbolKind::PROPERTY; - } else if ($node instanceof Tolerant\Node\ConstElement) { - $symbol->kind = SymbolKind::CONSTANT; - } - - else if ( - ( - ($node instanceof Tolerant\Node\Expression\AssignmentExpression) - && $node->leftOperand instanceof Tolerant\Node\Expression\Variable - ) - || $node instanceof Tolerant\Node\UseVariableName - || $node instanceof Tolerant\Node\Parameter - ) { - $symbol->kind = SymbolKind::VARIABLE; - } else { - return null; - } - - if ($node instanceof Tolerant\Node\Expression\AssignmentExpression) { - if ($node->leftOperand instanceof Tolerant\Node\Expression\Variable) { - $symbol->name = $node->leftOperand->getName(); - } elseif ($node->leftOperand instanceof Tolerant\Token) { - $symbol->name = trim($node->leftOperand->getText($node->getFileContents()), "$"); - } - - } else if ($node instanceof Tolerant\Node\UseVariableName) { - $symbol->name = $node->getName(); - } else if (isset($node->name)) { - if ($node->name instanceof Tolerant\Node\QualifiedName) { - $symbol->name = (string)Tolerant\ResolvedName::buildName($node->name->nameParts, $node->getFileContents()); - } else { - $symbol->name = ltrim((string)$node->name->getText($node->getFileContents()), "$"); - } - } else if (isset($node->variableName)) { - $symbol->name = $node->variableName->getText($node); - } else { - return null; - } - - $symbol->location = Location::fromNode($node); - if ($fqn !== null) { - $parts = preg_split('/(::|->|\\\\)/', $fqn); - array_pop($parts); - $symbol->containerName = implode('\\', $parts); - } - return $symbol; - } -} diff --git a/src/Server/TextDocument.php b/src/Server/TextDocument.php index 3679950..7a2b062 100644 --- a/src/Server/TextDocument.php +++ b/src/Server/TextDocument.php @@ -4,7 +4,7 @@ declare(strict_types = 1); namespace LanguageServer\Server; use LanguageServer\{ - CompletionProvider, FqnUtilities, LanguageClient, PhpDocument, PhpDocumentLoader, TolerantDefinitionResolver + CompletionProvider, FqnUtilities, LanguageClient, PhpDocument, PhpDocumentLoader, DefinitionResolver }; use LanguageServer\Index\ReadableIndex; use LanguageServer\Protocol\{ @@ -36,7 +36,7 @@ class TextDocument protected $project; /** - * @var TolerantDefinitionResolver + * @var DefinitionResolver */ protected $definitionResolver; @@ -62,7 +62,7 @@ class TextDocument /** * @param PhpDocumentLoader $documentLoader - * @param TolerantDefinitionResolver $definitionResolver + * @param DefinitionResolver $definitionResolver * @param LanguageClient $client * @param ReadableIndex $index * @param \stdClass $composerJson @@ -70,7 +70,7 @@ class TextDocument */ public function __construct( PhpDocumentLoader $documentLoader, - TolerantDefinitionResolver $definitionResolver, + DefinitionResolver $definitionResolver, LanguageClient $client, ReadableIndex $index, \stdClass $composerJson = null, diff --git a/src/TolerantDefinitionResolver.php b/src/TolerantDefinitionResolver.php deleted file mode 100644 index 810eb9f..0000000 --- a/src/TolerantDefinitionResolver.php +++ /dev/null @@ -1,1182 +0,0 @@ -index = $index; - $this->typeResolver = new TypeResolver; - $this->docBlockFactory = DocBlockFactory::createInstance(); - } - - /** - * Builds the declaration line for a given node. Declarations with multiple lines are trimmed. - * - * @param Tolerant\Node $node - * @return string - */ - public function getDeclarationLineFromNode($node): string - { - // 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;" - if ( - ($declaration = TolerantParserHelpers::tryGetPropertyDeclaration($node)) && ($elements = $declaration->propertyElements) || - ($declaration = TolerantParserHelpers::tryGetConstOrClassConstDeclaration($node)) && ($elements = $declaration->constElements) - ) { - $defLine = $declaration->getText(); - $defLineStart = $declaration->getStart(); - - $defLine = \substr_replace( - $defLine, - $node->getFullText(), - $elements->getFullStart() - $defLineStart, - $elements->getFullWidth() - ); - } else { - $defLine = $node->getText(); - } - - // Trim string to only include first line - $defLine = \rtrim(\strtok($defLine, "\n"), "\r"); - - // TODO - pretty print rather than getting text - - return $defLine; - } - - /** - * Gets the documentation string for a node, if it has one - * - * @param Tolerant\Node $node - * @return string|null - */ - public function getDocumentationFromNode($node) - { - // Any NamespaceDefinition comments likely apply to the file, not the declaration itself. - if ($node instanceof Tolerant\Node\Statement\NamespaceDefinition) { - return null; - } - - // 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. - $constOrPropertyDeclaration = TolerantParserHelpers::tryGetPropertyDeclaration($node) ?? TolerantParserHelpers::tryGetConstOrClassConstDeclaration($node); - if ($constOrPropertyDeclaration !== null) { - $node = $constOrPropertyDeclaration; - } - - // For parameters, parse the function-like declaration to get documentation for a parameter - if ($node instanceof Tolerant\Node\Parameter) { - $variableName = $node->getName(); - - $functionLikeDeclaration = TolerantParserHelpers::getFunctionLikeDeclarationFromParameter($node); - $docBlock = $this->getDocBlock($functionLikeDeclaration); - - $parameterDocBlockTag = $this->tryGetDocBlockTagForParameter($docBlock, $variableName); - return $parameterDocBlockTag !== null ? $parameterDocBlockTag->getDescription()->render() : null; - } - - // For everything else, get the doc block summary corresponding to the current node. - $docBlock = $this->getDocBlock($node); - if ($docBlock !== null) { - return $docBlock->getSummary(); - } - return null; - } - - /** - * 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(); - foreach ($namespaceImportTable as $alias=>$name) { - $namespaceImportTable[$alias] = (string)$name; - } - $namespaceDefinition = $node->getNamespaceDefinition(); - if ($namespaceDefinition !== null && $namespaceDefinition->name !== null) { - $namespaceName = (string)$namespaceDefinition->name->getNamespacedName(); - } else { - $namespaceName = 'global'; - } - $context = new Types\Context($namespaceName, $namespaceImportTable); - - try { - return $this->docBlockFactory->create($docCommentText, $context); - } catch (\InvalidArgumentException $e) { - return null; - } - } - return null; - } - - /** - * Create a Definition for a definition node - * - * @param Tolerant\Node $node - * @param string $fqn - * @return Definition - */ - public function createDefinitionFromNode($node, string $fqn = null): Definition - { - $def = new Definition; - $def->fqn = $fqn; - - // Determines whether the suggestion will show after "new" - $def->canBeInstantiated = $node instanceof Tolerant\Node\Statement\ClassDeclaration; - - // Interfaces, classes, traits, namespaces, functions, and global const elements - $def->isGlobal = ( - $node instanceof Tolerant\Node\Statement\InterfaceDeclaration || - $node instanceof Tolerant\Node\Statement\ClassDeclaration || - $node instanceof Tolerant\Node\Statement\TraitDeclaration || - - ($node instanceof Tolerant\Node\Statement\NamespaceDefinition && $node->name !== null) || - - $node instanceof Tolerant\Node\Statement\FunctionDeclaration || - - ($node instanceof Tolerant\Node\ConstElement && $node->parent->parent instanceof Tolerant\Node\Statement\ConstDeclaration) - ); - - // Static methods and static property declarations - $def->isStatic = ( - ($node instanceof Tolerant\Node\MethodDeclaration && $node->isStatic()) || - - (($propertyDeclaration = TolerantParserHelpers::tryGetPropertyDeclaration($node)) !== null - && $propertyDeclaration->isStatic()) - ); - - 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 should be better represented in the parser API - $node->interfaceBaseClause !== null && $node->interfaceBaseClause->interfaceNameList !== null - ) { - $def->extends = []; - foreach ($node->interfaceBaseClause->interfaceNameList->getValues() as $n) { - $def->extends[] = (string)$n->getResolvedName(); - } - } - - $def->symbolInformation = TolerantSymbolInformation::fromNode($node, $fqn); - - if ($def->symbolInformation !== null) { - $def->type = $this->getTypeFromNode($node); - $def->declarationLine = $this->getDeclarationLineFromNode($node); - $def->documentation = $this->getDocumentationFromNode($node); - } - - return $def; - } - - /** - * Given any node, returns the Definition object of the symbol that is referenced - * - * @param Tolerant\Node $node Any reference node - * @return Definition|null - */ - public function resolveReferenceNodeToDefinition($node) - { - $parent = $node->parent; - // Variables are not indexed globally, as they stay in the file scope anyway. - // Ignore variable nodes that are part of ScopedPropertyAccessExpression, - // as the scoped property access expression node is handled separately. - if ($node instanceof Tolerant\Node\Expression\Variable && - !($parent instanceof Tolerant\Node\Expression\ScopedPropertyAccessExpression)) - { - // Resolve $this to the containing class definition. - if ($node->getName() === 'this' && $fqn = $this->getContainingClassFqn($node)) { - return $this->index->getDefinition($fqn, false); - } - - // Resolve the variable to a definition node (assignment, param or closure use) - $defNode = $this->resolveVariableToNode($node); - if ($defNode === null) { - 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 - // TODO - verify that this is not a method - $globalFallback = TolerantParserHelpers::isConstantFetch($node) || $parent instanceof Tolerant\Node\Expression\CallExpression; - // 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 - */ - public function resolveReferenceNodeToFqn($node) { - // TODO all name tokens should be a part of a node - if ($node instanceof Tolerant\Node\QualifiedName) { - 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; - } - - private function resolveQualifiedNameNodeToFqn(Tolerant\Node\QualifiedName $node) { - $parent = $node->parent; - - if ($parent instanceof Tolerant\Node\TraitSelectOrAliasClause) { - return null; - } - // 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 .= '()'; - } - } - 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) { - $name .= '()'; - } - } - - return $name; - } - - // For extends, implements, type hints and classes of classes of static calls use the name directly - $name = (string) ($node->getResolvedName() ?? $node->getNamespacedName()); - - if ($node->parent instanceof Tolerant\Node\Expression\CallExpression) { - $name .= '()'; - } - return $name; - } - - 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); - - 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; - break; - } - } - } - 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]; - - 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; - } - } - } - - return $classFqn . $memberSuffix; - } - - private function resolveScopedPropertyAccessExpressionNodeToFqn(Tolerant\Node\Expression\ScopedPropertyAccessExpression $scoped) { - 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; - } - - 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) { - return null; - } - if ($className === 'parent') { - // parent is resolved to the parent class - if (!isset($classNode->extends)) { - return null; - } - $className = (string)$classNode->extends->getResolvedName(); - } else { - $className = (string)$classNode->getNamespacedName(); - } - } elseif ($scoped->scopeResolutionQualifier instanceof Tolerant\Node\QualifiedName) { - $className = $scoped->scopeResolutionQualifier->getResolvedName(); - } - if ($scoped->memberName instanceof Tolerant\Node\Expression\Variable) { - if ($scoped->parent instanceof Tolerant\Node\Expression\CallExpression) { - return null; - } - $memberName = $scoped->memberName->getName(); - if (empty($memberName)) { - return null; - } - $name = (string)$className . '::$' . $memberName; - } else { - $name = (string)$className . '::' . $scoped->memberName->getText($scoped->getFileContents()); - } - if ($scoped->parent instanceof Tolerant\Node\Expression\CallExpression) { - $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 Tolerant\Node $node - * @return string|null - */ - private static function getContainingClassFqn(Tolerant\Node $node) - { - $classNode = $node->getFirstAncestor(Tolerant\Node\Statement\ClassDeclaration::class); - if ($classNode === null) { - return null; - } - return (string)$classNode->getNamespacedName(); - } - - /** - * 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 - */ - 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 Tolerant\Node\UseVariableName) { - $n = $var->getFirstAncestor(Tolerant\Node\Expression\AnonymousFunctionCreationExpression::class)->parent; - $name = $var->getName(); - } else if ($var instanceof Tolerant\Node\Expression\Variable || $var instanceof Tolerant\Node\Parameter) { - $name = $var->getName(); - } 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 - if (TolerantParserHelpers::isFunctionLike($n)) { - 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 Tolerant\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 for a variable assignment to that variable - while (($prevSibling = $n->getPreviousSibling()) !== null && $n = $prevSibling) { - if ($n instanceof Tolerant\Node\Statement\ExpressionStatement) { - $n = $n->expression; - } - if ( - // TODO - clean this up - ($n instanceof Tolerant\Node\Expression\AssignmentExpression && $n->operator->kind === Tolerant\TokenKind::EqualsToken) - && $n->leftOperand instanceof Tolerant\Node\Expression\Variable && $n->leftOperand->getName() === $name - ) { - return $n; - } - } - } while (isset($n) && $n = $n->parent); - // 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. - * - * @param Tolerant\Node\Expression $expr - * @return \phpDocumentor\Reflection\Type|null - */ - public function resolveExpressionNodeToType($expr) - { - if ($expr == null || $expr instanceof Tolerant\MissingToken || $expr instanceof Tolerant\SkippedToken) { - // TODO some members are null or Missing/SkippedToken - // How do we handle this more generally? - return new Types\Mixed; - } - - // PARENTHESIZED EXPRESSION - // Retrieve inner expression from parenthesized expression - while ($expr instanceof Tolerant\Node\Expression\ParenthesizedExpression) { - $expr = $expr->expression; - } - - // VARIABLE - // $this -> Type\this - // $myVariable -> type of corresponding assignment expression - if ($expr instanceof Tolerant\Node\Expression\Variable || $expr instanceof Tolerant\Node\UseVariableName) { - if ($expr->getName() === 'this') { - return new Types\This; - } - // Find variable definition (parameter or assignment expression) - $defNode = $this->resolveVariableToNode($expr); - if ($defNode instanceof Tolerant\Node\Expression\AssignmentExpression || $defNode instanceof Tolerant\Node\UseVariableName) { - return $this->resolveExpressionNodeToType($defNode); - } - if ($defNode instanceof Tolerant\Node\Parameter) { - return $this->getTypeFromNode($defNode); - } - } - - // FUNCTION CALL - // Function calls are resolved to type corresponding to their FQN - if ($expr instanceof Tolerant\Node\Expression\CallExpression && - !( - $expr->callableExpression instanceof Tolerant\Node\Expression\ScopedPropertyAccessExpression || - $expr->callableExpression instanceof Tolerant\Node\Expression\MemberAccessExpression) - ) { - - // Find the function definition - if ($expr->callableExpression instanceof Tolerant\Node\Expression) { - // Cannot get type for dynamic function call - return new Types\Mixed; - } - - if ($expr->callableExpression instanceof Tolerant\Node\QualifiedName) { - $fqn = $expr->callableExpression->getResolvedName() ?? $expr->callableExpression->getNamespacedName(); - $fqn .= '()'; - $def = $this->index->getDefinition($fqn, true); - if ($def !== null) { - return $def->type; - } - } - } - - // 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_; - } - } - - // CONSTANT FETCH - // Resolve constants by retrieving corresponding definition type from FQN - if (TolerantParserHelpers::isConstantFetch($expr)) { - $fqn = (string)$expr->getNamespacedName(); - $def = $this->index->getDefinition($fqn, true); - if ($def !== null) { - return $def->type; - } - } - - // MEMBER ACCESS EXPRESSION - if ($expr instanceof Tolerant\Node\Expression\MemberAccessExpression) { - if ($expr->memberName instanceof Tolerant\Node\Expression) { - return new Types\Mixed; - } - $var = $expr->dereferencableExpression; - - // 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); - } - $fqn = $classFqn . '->' . $expr->memberName->getText($expr->getFileContents()); - if ($expr->parent instanceof Tolerant\Node\Expression\CallExpression) { - $fqn .= '()'; - } - $def = $this->index->getDefinition($fqn); - if ($def !== null) { - return $def->type; - } - } - } - - // SCOPED PROPERTY ACCESS EXPRESSION - if ($expr instanceof Tolerant\Node\Expression\ScopedPropertyAccessExpression) { - $classType = $this->resolveClassNameToType($expr->scopeResolutionQualifier); - if (!($classType instanceof Types\Object_) || $classType->getFqsen() === null) { - return new Types\Mixed; - } - $fqn = substr((string)$classType->getFqsen(), 1) . '::'; - - // TODO is there a cleaner way to do this? - $fqn .= $expr->memberName->getText() ?? $expr->memberName->getText($expr->getFileContents()); - if ($expr->parent instanceof Tolerant\Node\Expression\CallExpression) { - $fqn .= '()'; - } - - $def = $this->index->getDefinition($fqn); - if ($def === null) { - return new Types\Mixed; - } - return $def->type; - } - - // 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" - if ($expr instanceof Tolerant\Node\Expression\ObjectCreationExpression) { - return $this->resolveClassNameToType($expr->classTypeDesignator); - } - - // CLONE EXPRESSION - // clone($a) => resolves to the type of $a - if ($expr instanceof Tolerant\Node\Expression\CloneExpression) { - return $this->resolveExpressionNodeToType($expr->expression); - } - - // ASSIGNMENT EXPRESSION - // $a = $myExpression => resolves to the type of the right-hand operand - if ($expr instanceof Tolerant\Node\Expression\AssignmentExpression) { - return $this->resolveExpressionNodeToType($expr->rightOperand); - } - - // TERNARY EXPRESSION - // $condition ? $ifExpression : $elseExpression => reslves to type of $ifCondition or $elseExpression - // $condition ?: $elseExpression => resolves to type of $condition or $elseExpression - if ($expr instanceof Tolerant\Node\Expression\TernaryExpression) { - // ?: - if ($expr->ifExpression === null) { - return new Types\Compound([ - $this->resolveExpressionNodeToType($expr->condition), // TODO: why? - $this->resolveExpressionNodeToType($expr->elseExpression) - ]); - } - // Ternary is a compound of the two possible values - return new Types\Compound([ - $this->resolveExpressionNodeToType($expr->ifExpression), - $this->resolveExpressionNodeToType($expr->elseExpression) - ]); - } - - // NULL COALLESCE - // $rightOperand ?? $leftOperand => resolves to type of $rightOperand or $leftOperand - if ($expr instanceof Tolerant\Node\Expression\BinaryExpression && $expr->operator->kind === Tolerant\TokenKind::QuestionQuestionToken) { - // ?? operator - return new Types\Compound([ - $this->resolveExpressionNodeToType($expr->leftOperand), - $this->resolveExpressionNodeToType($expr->rightOperand) - ]); - } - - // BOOLEAN EXPRESSIONS: resolve to Types\Boolean - // (bool) $expression - // !$expression - // empty($var) - // isset($var) - // >, >=, <, <=, &&, ||, AND, OR, XOR, ==, ===, !=, !== - if ( - TolerantParserHelpers::isBooleanExpression($expr) - - || ($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 - ) { - return new Types\Boolean; - } - - // 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__) - if ( - ($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) - ) { - return new Types\String_; - } - - // 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] /= - if ( - $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 || - - // Assignment expressions (TODO: consider making this a type of AssignmentExpression rather than kind of BinaryExpression) - $operator === Tolerant\TokenKind::AsteriskEqualsToken|| - $operator === Tolerant\TokenKind::AsteriskAsteriskEqualsToken || - $operator === Tolerant\TokenKind::MinusEqualsToken || - $operator === Tolerant\TokenKind::PlusEqualsToken - ) - ) { - if ( - $this->resolveExpressionNodeToType($expr->leftOperand) instanceof Types\Integer - && $this->resolveExpressionNodeToType($expr->rightOperand) instanceof Types\Integer - ) { - return new Types\Integer; - } - return new Types\Float_; - } else if ( - $expr instanceof Tolerant\Node\Expression\BinaryExpression && - $expr->operator->kind === Tolerant\TokenKind::SlashEqualsToken - ) { - return new Types\Float_; - } - - // INTEGER EXPRESSIONS: resolve to Types\Integer - // [literal] 1 - // [operator] <=>, &, ^, | - // TODO: Magic constants (__LINE__) - if ( - // TODO: consider different Node types of float/int, also better property name (not "children") - ($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) - ) - ) { - return new Types\Integer; - } - - // FLOAT EXPRESSIONS: resolve to Types\Float - // [literal] 1.5 - // [operator] / - // [cast] (double) - if ( - $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) - ) { - return new Types\Float_; - } - - // ARRAY CREATION EXPRESSION: - // Resolve to Types\Array (Types\Compound of value and key types) - // [a, b, c] - // [1=>"hello", "hi"=>1, 4=>[]]s - if ($expr instanceof Tolerant\Node\Expression\ArrayCreationExpression) { - $valueTypes = []; - $keyTypes = []; - if ($expr->arrayElements !== null) { - foreach ($expr->arrayElements->getElements() as $item) { - $valueTypes[] = $this->resolveExpressionNodeToType($item->elementValue); - $keyTypes[] = $item->elementKey ? $this->resolveExpressionNodeToType($item->elementKey) : new Types\Integer; - } - } - $valueTypes = 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); - } - - // SUBSCRIPT EXPRESSION - // $myArray[3] - // $myArray{"hello"} - if ($expr instanceof Tolerant\Node\Expression\SubscriptExpression) { - $varType = $this->resolveExpressionNodeToType($expr->postfixExpression); - if (!($varType instanceof Types\Array_)) { - return new Types\Mixed; - } - return $varType->getValueType(); - } - - // SCRIPT INCLUSION EXPRESSION - // include, require, include_once, require_once - if ($expr instanceof Tolerant\Node\Expression\ScriptInclusionExpression) { - // TODO: resolve path to PhpDocument and find return statement - return new Types\Mixed; - } - - if ($expr instanceof Tolerant\Node\QualifiedName) { - return $this->resolveClassNameToType($expr); - } - - return new Types\Mixed; - } - - - /** - * 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 - * - * @param Tolerant\Node || Tolerant\Token $class - * @return Type - */ - public function resolveClassNameToType($class): Type - { - if ($class instanceof Tolerant\Node\Expression) { - return new Types\Mixed; - } - if ($class instanceof Tolerant\Token && $class->kind === Tolerant\TokenKind::ClassKeyword) { - // Anonymous class - return new Types\Object_; - } - $className = (string)$class->getResolvedName(); - - if ($className === 'static') { - return new Types\Static_; - } - if ($className === 'self' || $className === 'parent') { - $classNode = $class->getFirstAncestor(Tolerant\Node\Statement\ClassDeclaration::class); - if ($className === 'parent') { - 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 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. - * - * @param Tolerant\Node $node - * @return \phpDocumentor\Reflection\Type|null - */ - public function getTypeFromNode($node) - { - // PARAMETERS - // Get the type of the parameter: - // 1. Doc block - // 2. Parameter type and default - if ($node instanceof Tolerant\Node\Parameter) { - // Parameters - // Get the doc block for the the function call - // /** - // * @param MyClass $myParam - // */ - // function foo($a) - $functionLikeDeclaration = TolerantParserHelpers::getFunctionLikeDeclarationFromParameter($node); - $variableName = $node->getName(); - $docBlock = $this->getDocBlock($functionLikeDeclaration); - - $parameterDocBlockTag = $this->tryGetDocBlockTagForParameter($docBlock, $variableName); - if ($parameterDocBlockTag !== null && ($type = $parameterDocBlockTag->getType())) { - // Doc block comments supercede all other forms of type inference - return $type; - } - - // function foo(MyClass $a) - if ($node->typeDeclaration !== null) { - // Use PHP7 return type hint - if ($node->typeDeclaration instanceof Tolerant\Token) { - // Resolve a string like "bool" to a type object - $type = $this->typeResolver->resolve($node->typeDeclaration->getText($node->getFileContents())); - } else { - $type = new Types\Object_(new Fqsen('\\' . (string)$node->typeDeclaration->getResolvedName())); - } - } - // function foo($a = 3) - if ($node->default !== null) { - $defaultType = $this->resolveExpressionNodeToType($node->default); - if (isset($type) && !is_a($type, get_class($defaultType))) { - // TODO - verify it is worth creating a compound type - return new Types\Compound([$type, $defaultType]); - } - $type = $defaultType; - } - return $type ?? new Types\Mixed; - } - - // FUNCTIONS AND METHODS - // Get the return type - // 1. doc block - // 2. return type hint - // 3. TODO: infer from return statements - if (TolerantParserHelpers::isFunctionLike($node)) { - // Functions/methods - $docBlock = $this->getDocBlock($node); - if ( - $docBlock !== null - && !empty($returnTags = $docBlock->getTagsByName('return')) - && $returnTags[0]->getType() !== null - ) { - // Use @return tag - return $returnTags[0]->getType(); - } - if ($node->returnType !== null && !($node->returnType instanceof Tolerant\MissingToken)) { - // Use PHP7 return type hint - if ($node->returnType instanceof Tolerant\Token) { - // Resolve a string like "bool" to a type object - return $this->typeResolver->resolve($node->returnType->getText($node->getFileContents())); - } - return new Types\Object_(new Fqsen('\\' . (string)$node->returnType->getResolvedName())); - } - // Unknown return type - return new Types\Mixed; - } - - // PROPERTIES, CONSTS, CLASS CONSTS, ASSIGNMENT EXPRESSIONS - // Get the documented type the assignment resolves to. - if ( - ($declarationNode = - TolerantParserHelpers::tryGetPropertyDeclaration($node) ?? - TolerantParserHelpers::tryGetConstOrClassConstDeclaration($node) - ) !== null || - ($node = $node->parent) instanceof Tolerant\Node\Expression\AssignmentExpression) - { - $declarationNode = $declarationNode ?? $node; - - // Property, constant or variable - // Use @var tag - if ( - ($docBlock = $this->getDocBlock($declarationNode)) - && !empty($varTags = $docBlock->getTagsByName('var')) - && ($type = $varTags[0]->getType()) - ) { - return $type; - } - - // Resolve the expression - if ($declarationNode instanceof Tolerant\Node\PropertyDeclaration) { - // TODO should have default - if (isset($node->parent->rightOperand)) { - return $this->resolveExpressionNodeToType($node->parent->rightOperand); - } - } else if ($node instanceof Tolerant\Node\ConstElement) { - return $this->resolveExpressionNodeToType($node->assignment); - } else if ($node instanceof Tolerant\Node\Expression\AssignmentExpression) { - return $this->resolveExpressionNodeToType($node->rightOperand); - } - // TODO: read @property tags of class - // TODO: Try to infer the type from default value / constant value - // Unknown - return new Types\Mixed; - } - - // The node does not have a type - 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 - * - * @param Tolerant\Node $node - * @return string|null - */ - public static function getDefinedFqn($node) - { - $parent = $node->parent; - // Anonymous classes don't count as a definition - // 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) { - $name = (string) Tolerant\ResolvedName::buildName($node->name->nameParts, $node->getFileContents()); - return \count($name) > 0 ? $name : null; - } - // INPUT OUTPUT: - // namespace A\B; - // function a(); A\B\a(); - else if ($node instanceof Tolerant\Node\Statement\FunctionDeclaration) { - // Function: use functionName() as the name - $name = (string)$node->getNamespacedName(); - return \count($name) > 0 ? $name . '()' : null; - } - // 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) { - // Class method: use ClassName->methodName() as name - $class = $node->getFirstAncestor( - Tolerant\Node\Expression\ObjectCreationExpression::class, - Tolerant\Node\Statement\ClassDeclaration::class, - Tolerant\Node\Statement\InterfaceDeclaration::class, - Tolerant\Node\Statement\TraitDeclaration::class - ); - if (!isset($class->name)) { - // Ignore anonymous classes - return null; - } - if ($node->isStatic()) { - return (string)$class->getNamespacedName() . '::' . $node->getName() . '()'; - } else { - return (string)$class->getNamespacedName() . '->' . $node->getName() . '()'; - } - } - - // 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 // TODO verify variable name - // } - else if ( - ($propertyDeclaration = TolerantParserHelpers::tryGetPropertyDeclaration($node)) !== null && - ($classDeclaration = - $node->getFirstAncestor( - Tolerant\Node\Expression\ObjectCreationExpression::class, - Tolerant\Node\Statement\ClassDeclaration::class, - Tolerant\Node\Statement\InterfaceDeclaration::class, - Tolerant\Node\Statement\TraitDeclaration::class - ) - ) !== null && isset($classDeclaration->name)) - { - $name = $node->getName(); - if ($propertyDeclaration->isStatic()) { - // Static Property: use ClassName::$propertyName as name - return (string)$classDeclaration->getNamespacedName() . '::$' . $name; - } - - // Instance Property: use ClassName->propertyName as name - return (string)$classDeclaration->getNamespacedName() . '->' . $name; - } - - // 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 - // } - else if (($constDeclaration = TolerantParserHelpers::tryGetConstOrClassConstDeclaration($node)) !== null) { - if ($constDeclaration instanceof Tolerant\Node\Statement\ConstDeclaration) { - // Basic constant: use CONSTANT_NAME as name - return (string)$node->getNamespacedName(); - } - - // 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; - } - } - } -} diff --git a/src/TolerantTreeAnalyzer.php b/src/TolerantTreeAnalyzer.php deleted file mode 100644 index 1b5bd0d..0000000 --- a/src/TolerantTreeAnalyzer.php +++ /dev/null @@ -1,186 +0,0 @@ -uri = $uri; - $this->parser = $parser; - $this->docBlockFactory = $docBlockFactory; - $this->definitionResolver = $definitionResolver; - $this->content = $content; - $this->stmts = $this->parser->parseSourceFile($content, $uri); - - // TODO - docblock errors - - $this->collectDefinitionsAndReferences($this->stmts); - } - - public function collectDefinitionsAndReferences(Tolerant\Node $stmts) { - foreach ($stmts::CHILD_NAMES as $name) { - $node = $stmts->$name; - - if ($node === null) { - continue; - } - - if (\is_array($node)) { - foreach ($node as $child) { - if ($child instanceof Tolerant\Node) { - $this->update($child); - } - } - continue; - } - - if ($node instanceof Tolerant\Node) { - $this->update($node); - } - - if (($_error = Tolerant\DiagnosticsProvider::checkDiagnostics($node)) !== null) { - $range = Tolerant\PositionUtilities::getRangeFromPosition($_error->start, $_error->length, $this->content); - - $this->diagnostics[] = new Diagnostic( - $_error->message, - new Range( - new Position($range->start->line, $range->start->character), - new Position($range->end->line, $range->start->character) - ), - null, - DiagnosticSeverity::ERROR, - 'php' - ); - } - } - } - - public function update($node) { - $fqn = ($this->definitionResolver)::getDefinedFqn($node); - // Only index definitions with an FQN (no variables) - if ($fqn !== null) { - $this->definitionNodes[$fqn] = $node; - $this->definitions[$fqn] = $this->definitionResolver->createDefinitionFromNode($node, $fqn); - } else { - $parent = $node->parent; - if (!( - ( - // $node->parent instanceof Tolerant\Node\Expression\ScopedPropertyAccessExpression || - ($node instanceof Tolerant\Node\Expression\ScopedPropertyAccessExpression || - $node instanceof Tolerant\Node\Expression\MemberAccessExpression) - && !( - $node->parent instanceof Tolerant\Node\Expression\CallExpression || - $node->memberName instanceof Tolerant\Token - )) - || ($parent instanceof Tolerant\Node\Statement\NamespaceDefinition && $parent->name !== null && $parent->name->getStart() === $node->getStart())) - ) { - - $fqn = $this->definitionResolver->resolveReferenceNodeToFqn($node); - if ($fqn !== null) { - $this->addReference($fqn, $node); - - if ( - $node instanceof Tolerant\Node\QualifiedName - && ($node->isQualifiedName() || $node->parent instanceof Tolerant\Node\NamespaceUseClause) - && !($parent instanceof Tolerant\Node\Statement\NamespaceDefinition && $parent->name->getStart() === $node->getStart() - ) - ) { - // Add references for each referenced namespace - $ns = $fqn; - while (($pos = strrpos($ns, '\\')) !== false) { - $ns = substr($ns, 0, $pos); - $this->addReference($ns, $node); - } - } - - // Namespaced constant access and function calls also need to register a reference - // to the global version because PHP falls back to global at runtime - // http://php.net/manual/en/language.namespaces.fallback.php - if (TolerantParserHelpers::isConstantFetch($node) || - ($parent instanceof Tolerant\Node\Expression\CallExpression - && !( - $node instanceof Tolerant\Node\Expression\ScopedPropertyAccessExpression || - $node instanceof Tolerant\Node\Expression\MemberAccessExpression - ))) { - $parts = explode('\\', $fqn); - if (count($parts) > 1) { - $globalFqn = end($parts); - $this->addReference($globalFqn, $node); - } - } - } - } - } - $this->collectDefinitionsAndReferences($node); - } - - public function getDiagnostics() { - return $this->diagnostics ?? []; - } - - private function addReference(string $fqn, Tolerant\Node $node) - { - if (!isset($this->referenceNodes[$fqn])) { - $this->referenceNodes[$fqn] = []; - } - $this->referenceNodes[$fqn][] = $node; - } - - public function getDefinitions() { - return $this->definitions ?? []; - } - - public function getDefinitionNodes() { - return $this->definitionNodes ?? []; - } - - public function getReferenceNodes() { - return $this->referenceNodes ?? []; - } - - public function getStmts() { - return $this->stmts; - } - /** - * Returns the URI of the document - * - * @return string - */ - public function getUri(): string - { - return $this->uri; - } -} diff --git a/src/TreeAnalyzer.php b/src/TreeAnalyzer.php index ddb78ab..82e5870 100644 --- a/src/TreeAnalyzer.php +++ b/src/TreeAnalyzer.php @@ -19,83 +19,144 @@ use phpDocumentor\Reflection\DocBlockFactory; use Sabre\Uri; use Microsoft\PhpParser as Tolerant; -class TreeAnalyzer implements TreeAnalyzerInterface { +class TreeAnalyzer { private $parser; + /** @var Tolerant\Node */ private $stmts; - private $errorHandler; - private $diagnostics; + private $content; + + /** + * TreeAnalyzer constructor. + * @param Tolerant\Parser $parser + * @param $content + * @param $docBlockFactory + * @param DefinitionResolver $definitionResolver + * @param $uri + */ public function __construct($parser, $content, $docBlockFactory, $definitionResolver, $uri) { $this->uri = $uri; $this->parser = $parser; $this->docBlockFactory = $docBlockFactory; $this->definitionResolver = $definitionResolver; $this->content = $content; - $errorHandler = new ErrorHandler\Collecting; - $stmts = $this->parser->parse($content, $errorHandler); + $this->stmts = $this->parser->parseSourceFile($content, $uri); - $this->diagnostics = []; - foreach ($errorHandler->getErrors() as $error) { - $this->diagnostics[] = Diagnostic::fromError($error, $this->content, DiagnosticSeverity::ERROR, 'php'); - } + // TODO - docblock errors - // $stmts can be null in case of a fatal parsing error <- Interesting. When do fatal parsing errors occur? - if ($stmts) { - $traverser = new NodeTraverser; + $this->collectDefinitionsAndReferences($this->stmts); + } - // Resolve aliased names to FQNs - $traverser->addVisitor(new NameResolver($errorHandler)); + public function collectDefinitionsAndReferences(Tolerant\Node $stmts) { + foreach ($stmts::CHILD_NAMES as $name) { + $node = $stmts->$name; - // Add parentNode, previousSibling, nextSibling attributes - $traverser->addVisitor(new ReferencesAdder($this)); - - // Add column attributes to nodes - $traverser->addVisitor(new ColumnCalculator($content)); - - // Parse docblocks and add docBlock attributes to nodes - $docBlockParser = new DocBlockParser($this->docBlockFactory); - $traverser->addVisitor($docBlockParser); - - $traverser->traverse($stmts); - - // Report errors from parsing docblocks - foreach ($docBlockParser->errors as $error) { - $this->diagnostics[] = Diagnostic::fromError($error, $this->content, DiagnosticSeverity::WARNING, 'php'); + if ($node === null) { + continue; } - $traverser = new NodeTraverser; - - // Collect all definitions - $definitionCollector = new DefinitionCollector($this->definitionResolver); - $traverser->addVisitor($definitionCollector); - - // Collect all references - $referencesCollector = new ReferencesCollector($this->definitionResolver); - $traverser->addVisitor($referencesCollector); - - $traverser->traverse($stmts); - - // Register this document on the project for all the symbols defined in it - $this->definitions = $definitionCollector->definitions; - $this->definitionNodes = $definitionCollector->nodes; - foreach ($definitionCollector->definitions as $fqn => $definition) { - // $this->index->setDefinition($fqn, $definition); - } - // Register this document on the project for references - $this->referenceNodes = $referencesCollector->nodes; - foreach ($referencesCollector->nodes as $fqn => $nodes) { - // $this->index->addReferenceUri($fqn, $this->uri); + if (\is_array($node)) { + foreach ($node as $child) { + if ($child instanceof Tolerant\Node) { + $this->update($child); + } + } + continue; } - $this->stmts = $stmts; + if ($node instanceof Tolerant\Node) { + $this->update($node); + } + + if (($_error = Tolerant\DiagnosticsProvider::checkDiagnostics($node)) !== null) { + $range = Tolerant\PositionUtilities::getRangeFromPosition($_error->start, $_error->length, $this->content); + + $this->diagnostics[] = new Diagnostic( + $_error->message, + new Range( + new Position($range->start->line, $range->start->character), + new Position($range->end->line, $range->start->character) + ), + null, + DiagnosticSeverity::ERROR, + 'php' + ); + } } } + public function update($node) { + $fqn = ($this->definitionResolver)::getDefinedFqn($node); + // Only index definitions with an FQN (no variables) + if ($fqn !== null) { + $this->definitionNodes[$fqn] = $node; + $this->definitions[$fqn] = $this->definitionResolver->createDefinitionFromNode($node, $fqn); + } else { + $parent = $node->parent; + if (!( + ( + // $node->parent instanceof Tolerant\Node\Expression\ScopedPropertyAccessExpression || + ($node instanceof Tolerant\Node\Expression\ScopedPropertyAccessExpression || + $node instanceof Tolerant\Node\Expression\MemberAccessExpression) + && !( + $node->parent instanceof Tolerant\Node\Expression\CallExpression || + $node->memberName instanceof Tolerant\Token + )) + || ($parent instanceof Tolerant\Node\Statement\NamespaceDefinition && $parent->name !== null && $parent->name->getStart() === $node->getStart())) + ) { + + $fqn = $this->definitionResolver->resolveReferenceNodeToFqn($node); + if ($fqn !== null) { + $this->addReference($fqn, $node); + + if ( + $node instanceof Tolerant\Node\QualifiedName + && ($node->isQualifiedName() || $node->parent instanceof Tolerant\Node\NamespaceUseClause) + && !($parent instanceof Tolerant\Node\Statement\NamespaceDefinition && $parent->name->getStart() === $node->getStart() + ) + ) { + // Add references for each referenced namespace + $ns = $fqn; + while (($pos = strrpos($ns, '\\')) !== false) { + $ns = substr($ns, 0, $pos); + $this->addReference($ns, $node); + } + } + + // Namespaced constant access and function calls also need to register a reference + // to the global version because PHP falls back to global at runtime + // http://php.net/manual/en/language.namespaces.fallback.php + if (ParserHelpers::isConstantFetch($node) || + ($parent instanceof Tolerant\Node\Expression\CallExpression + && !( + $node instanceof Tolerant\Node\Expression\ScopedPropertyAccessExpression || + $node instanceof Tolerant\Node\Expression\MemberAccessExpression + ))) { + $parts = explode('\\', $fqn); + if (count($parts) > 1) { + $globalFqn = end($parts); + $this->addReference($globalFqn, $node); + } + } + } + } + } + $this->collectDefinitionsAndReferences($node); + } + public function getDiagnostics() { - return $this->diagnostics; + return $this->diagnostics ?? []; + } + + private function addReference(string $fqn, Tolerant\Node $node) + { + if (!isset($this->referenceNodes[$fqn])) { + $this->referenceNodes[$fqn] = []; + } + $this->referenceNodes[$fqn][] = $node; } public function getDefinitions() { diff --git a/src/TreeAnalyzerInterface.php b/src/TreeAnalyzerInterface.php deleted file mode 100644 index a534a72..0000000 --- a/src/TreeAnalyzerInterface.php +++ /dev/null @@ -1,34 +0,0 @@ -getDefinitionNodes(); } } diff --git a/tests/PhpDocumentLoaderTest.php b/tests/PhpDocumentLoaderTest.php index 18e8373..348f23f 100644 --- a/tests/PhpDocumentLoaderTest.php +++ b/tests/PhpDocumentLoaderTest.php @@ -4,7 +4,7 @@ declare(strict_types = 1); namespace LanguageServer\Tests\Server; use LanguageServer\{ - PhpDocument, PhpDocumentLoader, Project, TolerantDefinitionResolver + PhpDocument, PhpDocumentLoader, Project, DefinitionResolver }; use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\Index\{ @@ -26,7 +26,7 @@ class PhpDocumentLoaderTest extends TestCase $this->loader = new PhpDocumentLoader( new FileSystemContentRetriever, $projectIndex, - new TolerantDefinitionResolver($projectIndex) + new DefinitionResolver($projectIndex) ); } diff --git a/tests/PhpDocumentTest.php b/tests/PhpDocumentTest.php index 19081af..ca36d79 100644 --- a/tests/PhpDocumentTest.php +++ b/tests/PhpDocumentTest.php @@ -4,7 +4,7 @@ declare(strict_types = 1); namespace LanguageServer\Tests\Server; use LanguageServer\{ - PhpDocument, TolerantDefinitionResolver + PhpDocument, DefinitionResolver }; use LanguageServer\Index\{ Index @@ -25,7 +25,7 @@ class PhpDocumentTest extends TestCase $parser = new Tolerant\Parser(); $docBlockFactory = DocBlockFactory::createInstance(); $index = new Index; - $definitionResolver = new TolerantDefinitionResolver($index); + $definitionResolver = new DefinitionResolver($index); return new PhpDocument($uri, $content, $index, $parser, $docBlockFactory, $definitionResolver); } diff --git a/tests/Server/ServerTestCase.php b/tests/Server/ServerTestCase.php index a93e4c1..bbef3ea 100644 --- a/tests/Server/ServerTestCase.php +++ b/tests/Server/ServerTestCase.php @@ -6,7 +6,7 @@ namespace LanguageServer\Tests\Server; use PHPUnit\Framework\TestCase; use LanguageServer\Tests\MockProtocolStream; use LanguageServer\{ - Server, LanguageClient, PhpDocumentLoader, TolerantDefinitionResolver + Server, LanguageClient, PhpDocumentLoader, DefinitionResolver }; use LanguageServer\Index\{ProjectIndex, DependenciesIndex, Index}; use LanguageServer\ContentRetriever\FileSystemContentRetriever; @@ -51,7 +51,7 @@ abstract class ServerTestCase extends TestCase $projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex); $projectIndex->setComplete(); - $definitionResolver = new TolerantDefinitionResolver($projectIndex); + $definitionResolver = new DefinitionResolver($projectIndex); $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); $this->documentLoader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver); $this->textDocument = new Server\TextDocument($this->documentLoader, $definitionResolver, $client, $projectIndex); diff --git a/tests/Server/TextDocument/CompletionTest.php b/tests/Server/TextDocument/CompletionTest.php index e8b417d..2056953 100644 --- a/tests/Server/TextDocument/CompletionTest.php +++ b/tests/Server/TextDocument/CompletionTest.php @@ -6,7 +6,7 @@ namespace LanguageServer\Tests\Server\TextDocument; use PHPUnit\Framework\TestCase; use LanguageServer\Tests\MockProtocolStream; use LanguageServer\{ - Server, LanguageClient, PhpDocumentLoader, TolerantDefinitionResolver + Server, LanguageClient, PhpDocumentLoader, DefinitionResolver }; use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex}; use LanguageServer\ContentRetriever\FileSystemContentRetriever; @@ -37,7 +37,7 @@ class CompletionTest extends TestCase { $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); $projectIndex = new ProjectIndex(new Index, new DependenciesIndex); - $definitionResolver = new TolerantDefinitionResolver($projectIndex); + $definitionResolver = new DefinitionResolver($projectIndex); $contentRetriever = new FileSystemContentRetriever; $this->loader = new PhpDocumentLoader($contentRetriever, $projectIndex, $definitionResolver); $this->loader->load(pathToUri(__DIR__ . '/../../../fixtures/global_symbols.php'))->wait(); diff --git a/tests/Server/TextDocument/Definition/GlobalFallbackTest.php b/tests/Server/TextDocument/Definition/GlobalFallbackTest.php index aa9d7b9..4e45f9e 100644 --- a/tests/Server/TextDocument/Definition/GlobalFallbackTest.php +++ b/tests/Server/TextDocument/Definition/GlobalFallbackTest.php @@ -6,7 +6,7 @@ namespace LanguageServer\Tests\Server\TextDocument\Definition; use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\Server\ServerTestCase; use LanguageServer\{ - Server, LanguageClient, PhpDocumentLoader, TolerantDefinitionResolver + Server, LanguageClient, PhpDocumentLoader, DefinitionResolver }; use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex}; use LanguageServer\ContentRetriever\FileSystemContentRetriever; @@ -19,7 +19,7 @@ class GlobalFallbackTest extends ServerTestCase $projectIndex = new ProjectIndex(new Index, new DependenciesIndex); $projectIndex->setComplete(); $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $definitionResolver = new TolerantDefinitionResolver($projectIndex); + $definitionResolver = new DefinitionResolver($projectIndex); $contentRetriever = new FileSystemContentRetriever; $loader = new PhpDocumentLoader($contentRetriever, $projectIndex, $definitionResolver); $this->textDocument = new Server\TextDocument($loader, $definitionResolver, $client, $projectIndex); diff --git a/tests/Server/TextDocument/DidChangeTest.php b/tests/Server/TextDocument/DidChangeTest.php index d46067b..4d26ed8 100644 --- a/tests/Server/TextDocument/DidChangeTest.php +++ b/tests/Server/TextDocument/DidChangeTest.php @@ -6,7 +6,7 @@ namespace LanguageServer\Tests\Server\TextDocument; use PHPUnit\Framework\TestCase; use LanguageServer\Tests\MockProtocolStream; use LanguageServer\{ - Server, LanguageClient, PhpDocumentLoader, TolerantDefinitionResolver + Server, LanguageClient, PhpDocumentLoader, DefinitionResolver }; use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex}; @@ -23,7 +23,7 @@ class DidChangeTest extends TestCase { $projectIndex = new ProjectIndex(new Index, new DependenciesIndex); $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $definitionResolver = new TolerantDefinitionResolver($projectIndex); + $definitionResolver = new DefinitionResolver($projectIndex); $loader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver); $textDocument = new Server\TextDocument($loader, $definitionResolver, $client, $projectIndex); $phpDocument = $loader->open('whatever', "open('whatever', "textDocument = new Server\TextDocument($loader, $definitionResolver, $client, $projectIndex); } diff --git a/tests/Server/TextDocument/References/GlobalFallbackTest.php b/tests/Server/TextDocument/References/GlobalFallbackTest.php index 684be18..abfefce 100644 --- a/tests/Server/TextDocument/References/GlobalFallbackTest.php +++ b/tests/Server/TextDocument/References/GlobalFallbackTest.php @@ -4,7 +4,7 @@ declare(strict_types = 1); namespace LanguageServer\Tests\Server\TextDocument\References; use LanguageServer\{ - LanguageClient, PhpDocumentLoader, Server, TolerantDefinitionResolver + LanguageClient, PhpDocumentLoader, Server, DefinitionResolver }; use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\Index\{ @@ -22,7 +22,7 @@ class GlobalFallbackTest extends ServerTestCase { $projectIndex = new ProjectIndex(new Index, new DependenciesIndex); $projectIndex->setComplete(); - $definitionResolver = new TolerantDefinitionResolver($projectIndex); + $definitionResolver = new DefinitionResolver($projectIndex); $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); $this->documentLoader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver); $this->textDocument = new Server\TextDocument($this->documentLoader, $definitionResolver, $client, $projectIndex); diff --git a/tests/Validation/ValidationTest.php b/tests/Validation/ValidationTest.php index a66e82e..06e97ed 100644 --- a/tests/Validation/ValidationTest.php +++ b/tests/Validation/ValidationTest.php @@ -9,7 +9,7 @@ use LanguageServer\Definition; use LanguageServer\Index\Index; use LanguageServer\ParserKind; use LanguageServer\PhpDocument; -use LanguageServer\TolerantDefinitionResolver; +use LanguageServer\DefinitionResolver; use phpDocumentor\Reflection\DocBlock; use phpDocumentor\Reflection\DocBlockFactory; use PHPUnit\Framework\TestCase; @@ -96,7 +96,7 @@ class ValidationTest extends TestCase $index = new Index(); $parser = new Tolerant\Parser(); $docBlockFactory = DocBlockFactory::createInstance(); - $definitionResolver = new TolerantDefinitionResolver($index); + $definitionResolver = new DefinitionResolver($index); $document = new PhpDocument($filename, $fileContents, $index, $parser, $docBlockFactory, $definitionResolver);