diff --git a/fixtures/completion/this_return_value.php b/fixtures/completion/this_return_value.php new file mode 100644 index 0000000..167d0b4 --- /dev/null +++ b/fixtures/completion/this_return_value.php @@ -0,0 +1,23 @@ +foo()->q + } + public function qux() + { + } +} diff --git a/src/CompletionProvider.php b/src/CompletionProvider.php index f4f091d..2414ead 100644 --- a/src/CompletionProvider.php +++ b/src/CompletionProvider.php @@ -14,6 +14,7 @@ use LanguageServer\Protocol\{ }; use Microsoft\PhpParser; use Microsoft\PhpParser\Node; +use Generator; class CompletionProvider { @@ -196,18 +197,15 @@ class CompletionProvider // $a->| // Multiple prefixes for all possible types - $prefixes = FqnUtilities\getFqnsFromType( + $fqns = FqnUtilities\getFqnsFromType( $this->definitionResolver->resolveExpressionNodeToType($node->dereferencableExpression) ); - // Include parent classes - $prefixes = $this->expandParentFqns($prefixes); - - // Add the object access operator to only get members - foreach ($prefixes as &$prefix) { - $prefix .= '->'; + // Add the object access operator to only get members of all parents + $prefixes = []; + foreach ($this->expandParentFqns($fqns) as $prefix) { + $prefixes[] = $prefix . '->'; } - unset($prefix); // Collect all definitions that match any of the prefixes foreach ($this->index->getDefinitions() as $fqn => $def) { @@ -232,18 +230,15 @@ class CompletionProvider // TODO: $a::| // Resolve all possible types to FQNs - $prefixes = FqnUtilities\getFqnsFromType( + $fqns = FqnUtilities\getFqnsFromType( $classType = $this->definitionResolver->resolveExpressionNodeToType($scoped->scopeResolutionQualifier) ); - // Add parent classes - $prefixes = $this->expandParentFqns($prefixes); - - // Append :: operator to only get static members - foreach ($prefixes as &$prefix) { - $prefix .= '::'; + // Append :: operator to only get static members of all parents + $prefixes = []; + foreach ($this->expandParentFqns($fqns) as $prefix) { + $prefixes[] = $prefix . '::'; } - unset($prefix); // Collect all definitions that match any of the prefixes foreach ($this->index->getDefinitions() as $fqn => $def) { @@ -377,23 +372,22 @@ class CompletionProvider } /** - * Adds the FQNs of all parent classes to an array of FQNs of classes + * Yields FQNs from an array along with the FQNs of all parent classes * * @param string[] $fqns - * @return string[] + * @return Generator */ - private function expandParentFqns(array $fqns): array + private function expandParentFqns(array $fqns) : Generator { - $expanded = $fqns; foreach ($fqns as $fqn) { + yield $fqn; $def = $this->index->getDefinition($fqn); - if ($def) { - foreach ($this->expandParentFqns($def->extends ?? []) as $parent) { - $expanded[] = $parent; + if ($def !== null) { + foreach ($def->getAncestorDefinitions($this->index) as $name => $def) { + yield $name; } } } - return $expanded; } /** diff --git a/src/Definition.php b/src/Definition.php index cf0f574..c03e852 100644 --- a/src/Definition.php +++ b/src/Definition.php @@ -3,9 +3,11 @@ declare(strict_types = 1); namespace LanguageServer; +use LanguageServer\Index\ReadableIndex; use phpDocumentor\Reflection\{Types, Type, Fqsen, TypeResolver}; use LanguageServer\Protocol\SymbolInformation; use Exception; +use Generator; /** * Class used to represent symbols @@ -95,4 +97,33 @@ class Definition * @var string */ public $documentation; + + /** + * Yields the definitons of all ancestor classes (the Definition fqn is yielded as key) + * + * @param ReadableIndex $index the index to search for needed definitions + * @param bool $includeSelf should the first yielded value be the current definition itself + * @return Generator + */ + public function getAncestorDefinitions(ReadableIndex $index, bool $includeSelf = false): Generator + { + if ($includeSelf) { + yield $this->fqn => $this; + } + if ($this->extends !== null) { + // iterating once, storing the references and iterating again + // guarantees that closest definitions are yielded first + $definitions = []; + foreach ($this->extends as $fqn) { + $def = $index->getDefinition($fqn); + if ($def !== null) { + yield $def->fqn => $def; + $definitions[] = $def; + } + } + foreach ($definitions as $def) { + yield from $def->getAncestorDefinitions($index); + } + } + } } diff --git a/src/DefinitionResolver.php b/src/DefinitionResolver.php index 713d868..51c5955 100644 --- a/src/DefinitionResolver.php +++ b/src/DefinitionResolver.php @@ -574,7 +574,6 @@ class DefinitionResolver // $this -> Type\this // $myVariable -> type of corresponding assignment expression if ($expr instanceof Node\Expression\Variable || $expr instanceof Node\UseVariableName) { - // TODO: this will need to change when fluent interfaces are supported if ($expr->getName() === 'this') { return new Types\Object_(new Fqsen('\\' . $this->getContainingClassFqn($expr))); } @@ -667,13 +666,21 @@ class DefinitionResolver } else { $classFqn = substr((string)$t->getFqsen(), 1); } - $fqn = $classFqn . '->' . $expr->memberName->getText($expr->getFileContents()); + $add = '->' . $expr->memberName->getText($expr->getFileContents()); if ($expr->parent instanceof Node\Expression\CallExpression) { - $fqn .= '()'; + $add .= '()'; } - $def = $this->index->getDefinition($fqn); - if ($def !== null) { - return $def->type; + $classDef = $this->index->getDefinition($classFqn); + if ($classDef !== null) { + foreach ($classDef->getAncestorDefinitions($this->index, true) as $fqn => $def) { + $def = $this->index->getDefinition($fqn . $add); + if ($def !== null) { + if ($def->type instanceof Types\This) { + return new Types\Object_(new Fqsen('\\' . $classFqn)); + } + return $def->type; + } + } } } } diff --git a/tests/Server/TextDocument/CompletionTest.php b/tests/Server/TextDocument/CompletionTest.php index 3cdb5f8..0d68ec3 100644 --- a/tests/Server/TextDocument/CompletionTest.php +++ b/tests/Server/TextDocument/CompletionTest.php @@ -653,4 +653,31 @@ class CompletionTest extends TestCase ) ], true), $items); } + + public function testThisReturnValue() + { + $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/this_return_value.php'); + $this->loader->open($completionUri, file_get_contents($completionUri)); + $items = $this->textDocument->completion( + new TextDocumentIdentifier($completionUri), + new Position(17, 23) + )->wait(); + $this->assertEquals(new CompletionList([ + new CompletionItem( + 'foo', + CompletionItemKind::METHOD, + '$this' // Return type of the method + ), + new CompletionItem( + 'bar', + CompletionItemKind::METHOD, + 'mixed' // Return type of the method + ), + new CompletionItem( + 'qux', + CompletionItemKind::METHOD, + 'mixed' // Return type of the method + ) + ], true), $items); + } }