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;
use Microsoft\PhpParser\Node; use Microsoft\PhpParser\Node;
use Generator;
class CompletionProvider class CompletionProvider
{ {
@ -196,18 +197,15 @@ class CompletionProvider
// $a->| // $a->|
// Multiple prefixes for all possible types // Multiple prefixes for all possible types
$prefixes = FqnUtilities\getFqnsFromType( $fqns = FqnUtilities\getFqnsFromType(
$this->definitionResolver->resolveExpressionNodeToType($node->dereferencableExpression) $this->definitionResolver->resolveExpressionNodeToType($node->dereferencableExpression)
); );
// Include parent classes // Add the object access operator to only get members of all parents
$prefixes = $this->expandParentFqns($prefixes); $prefixes = [];
foreach ($this->expandParentFqns($fqns) as $prefix) {
// Add the object access operator to only get members $prefixes[] = $prefix . '->';
foreach ($prefixes as &$prefix) {
$prefix .= '->';
} }
unset($prefix);
// Collect all definitions that match any of the prefixes // Collect all definitions that match any of the prefixes
foreach ($this->index->getDefinitions() as $fqn => $def) { foreach ($this->index->getDefinitions() as $fqn => $def) {
@ -232,18 +230,15 @@ class CompletionProvider
// TODO: $a::| // TODO: $a::|
// Resolve all possible types to FQNs // Resolve all possible types to FQNs
$prefixes = FqnUtilities\getFqnsFromType( $fqns = FqnUtilities\getFqnsFromType(
$classType = $this->definitionResolver->resolveExpressionNodeToType($scoped->scopeResolutionQualifier) $classType = $this->definitionResolver->resolveExpressionNodeToType($scoped->scopeResolutionQualifier)
); );
// Add parent classes // Append :: operator to only get static members of all parents
$prefixes = $this->expandParentFqns($prefixes); $prefixes = [];
foreach ($this->expandParentFqns($fqns) as $prefix) {
// Append :: operator to only get static members $prefixes[] = $prefix . '::';
foreach ($prefixes as &$prefix) {
$prefix .= '::';
} }
unset($prefix);
// Collect all definitions that match any of the prefixes // Collect all definitions that match any of the prefixes
foreach ($this->index->getDefinitions() as $fqn => $def) { 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 * @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) { foreach ($fqns as $fqn) {
yield $fqn;
$def = $this->index->getDefinition($fqn); $def = $this->index->getDefinition($fqn);
if ($def) { if ($def !== null) {
foreach ($this->expandParentFqns($def->extends ?? []) as $parent) { foreach ($def->getAncestorDefinitions($this->index) as $name => $def) {
$expanded[] = $parent; yield $name;
} }
} }
} }
return $expanded;
} }
/** /**

View File

@ -3,9 +3,11 @@ declare(strict_types = 1);
namespace LanguageServer; namespace LanguageServer;
use LanguageServer\Index\ReadableIndex;
use phpDocumentor\Reflection\{Types, Type, Fqsen, TypeResolver}; use phpDocumentor\Reflection\{Types, Type, Fqsen, TypeResolver};
use LanguageServer\Protocol\SymbolInformation; use LanguageServer\Protocol\SymbolInformation;
use Exception; use Exception;
use Generator;
/** /**
* Class used to represent symbols * Class used to represent symbols
@ -95,4 +97,33 @@ class Definition
* @var string * @var string
*/ */
public $documentation; 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 // $this -> Type\this
// $myVariable -> type of corresponding assignment expression // $myVariable -> type of corresponding assignment expression
if ($expr instanceof Node\Expression\Variable || $expr instanceof Node\UseVariableName) { if ($expr instanceof Node\Expression\Variable || $expr instanceof Node\UseVariableName) {
// TODO: this will need to change when fluent interfaces are supported
if ($expr->getName() === 'this') { if ($expr->getName() === 'this') {
return new Types\Object_(new Fqsen('\\' . $this->getContainingClassFqn($expr))); return new Types\Object_(new Fqsen('\\' . $this->getContainingClassFqn($expr)));
} }
@ -667,16 +666,24 @@ class DefinitionResolver
} else { } else {
$classFqn = substr((string)$t->getFqsen(), 1); $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) { if ($expr->parent instanceof Node\Expression\CallExpression) {
$fqn .= '()'; $add .= '()';
} }
$def = $this->index->getDefinition($fqn); $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 !== null) {
if ($def->type instanceof Types\This) {
return new Types\Object_(new Fqsen('\\' . $classFqn));
}
return $def->type; return $def->type;
} }
} }
} }
}
}
// SCOPED PROPERTY ACCESS EXPRESSION // SCOPED PROPERTY ACCESS EXPRESSION
if ($expr instanceof Node\Expression\ScopedPropertyAccessExpression) { if ($expr instanceof Node\Expression\ScopedPropertyAccessExpression) {

View File

@ -653,4 +653,31 @@ class CompletionTest extends TestCase
) )
], true), $items); ], 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);
}
} }