1
0
Fork 0

Fluent interfaces support (#421)

pull/441/head
Ivan Bozhanov 2017-07-07 14:18:19 +03:00 committed by Felix Becker
parent 94fc0405fd
commit 35f33c8c91
5 changed files with 112 additions and 30 deletions

View File

@ -0,0 +1,23 @@
<?php
class Grand
{
/** @return $this */
public function foo()
{
return $this;
}
}
class Parent1 extends Grand
{
}
class Child extends Parent1
{
public function bar()
{
$this->foo()->q
}
public function qux()
{
}
}

View File

@ -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;
}
/**

View File

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

View File

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

View File

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