From 8a1bc93ea790b7bcf37a295c3d70c8025097744b Mon Sep 17 00:00:00 2001 From: Camille Dejoye Date: Sat, 28 Nov 2020 16:34:36 +0100 Subject: [PATCH 1/6] resolve suggestions with table import --- .../TolerantParser/ChainTolerantCompletor.php | 56 +++++++- .../TolerantParser/TolerantCompletor.php | 2 +- lib/Core/Suggestion.php | 23 +++- .../ChainTolerantCompletorTest.php | 120 ++++++++++++++++++ tests/Unit/Core/SuggestionTest.php | 23 ++++ 5 files changed, 215 insertions(+), 9 deletions(-) diff --git a/lib/Bridge/TolerantParser/ChainTolerantCompletor.php b/lib/Bridge/TolerantParser/ChainTolerantCompletor.php index 21224bb9..7ef22a99 100644 --- a/lib/Bridge/TolerantParser/ChainTolerantCompletor.php +++ b/lib/Bridge/TolerantParser/ChainTolerantCompletor.php @@ -5,7 +5,9 @@ use Generator; use Microsoft\PhpParser\Node; use Microsoft\PhpParser\Parser; +use Microsoft\PhpParser\ResolvedName; use Phpactor\Completion\Core\Completor; +use Phpactor\Completion\Core\Suggestion; use Phpactor\Completion\Core\Util\OffsetHelper; use Phpactor\TextDocument\ByteOffset; use Phpactor\TextDocument\TextDocument; @@ -54,8 +56,9 @@ public function complete(TextDocument $source, ByteOffset $byteOffset): Generato } $suggestions = $tolerantCompletor->complete($completionNode, $source, $byteOffset); - - yield from $suggestions; + foreach ($suggestions as $suggestion) { + yield $this->resolveClassSuggestion($completionNode, $suggestion); + } $isComplete = $isComplete && $suggestions->getReturn(); } @@ -70,10 +73,10 @@ private function truncateSource(string $source, int $byteOffset): string // ` will evaluate the Variable node as an expression node with a // double variable `$\n $bar = ` $truncatedSource = substr($source, 0, $byteOffset); - + // determine the last non-whitespace _character_ offset $characterOffset = OffsetHelper::lastNonWhitespaceCharacterOffset($truncatedSource); - + // truncate the source at the character offset $truncatedSource = mb_substr($source, 0, $characterOffset); @@ -90,4 +93,49 @@ private function filterNonQualifyingClasses(Node $node): array return $completor->qualifier()->couldComplete($node); }); } + + private function resolveClassSuggestion(Node $completionNode, Suggestion $suggestion): Suggestion + { + if (Suggestion::TYPE_CLASS !== $suggestion->type()) { + return $suggestion; + } + + /** @var ResolvedName[] $importTable */ + [$importTable] = $completionNode->getImportTablesForCurrentScope(); + + // Prioritize import without alias + if (isset($importTable[$suggestion->name()])) { + return $suggestion->withoutNameImport(); + } + + $suggestionFqcn = $suggestion->classImport(); + $possibleMatches = []; + foreach ($importTable as $alias => $resolvedName) { + $importFqcn = $resolvedName->getFullyQualifiedNameText(); + + if ($suggestionFqcn === $importFqcn) { + return $suggestion->withoutNameImport()->withName($alias); + } + + if (0 === strpos($suggestionFqcn, $importFqcn)) { + $possibleMatches[$alias] = $importFqcn; + } + } + + if (!$possibleMatches) { + return $suggestion; + } + + // Sort the possible by matches by FQCN length + uasort($possibleMatches, function (string $firstFqcn, $secondFqcn) { + return strlen($firstFqcn) <=> strlen($secondFqcn); + }); + + // Keep the match with the longest FQCN (more accurate one) + $importFqcn = end($possibleMatches); + $alias = array_key_last($possibleMatches); + $name = $alias.substr($suggestionFqcn, strlen($importFqcn)); + + return $suggestion->withoutNameImport()->withName($name); + } } diff --git a/lib/Bridge/TolerantParser/TolerantCompletor.php b/lib/Bridge/TolerantParser/TolerantCompletor.php index 8b995d65..bfd554fb 100644 --- a/lib/Bridge/TolerantParser/TolerantCompletor.php +++ b/lib/Bridge/TolerantParser/TolerantCompletor.php @@ -11,7 +11,7 @@ interface TolerantCompletor { /** - * @return Generator & iterable + * @return Suggestion[]|Generator */ public function complete(Node $node, TextDocument $source, ByteOffset $offset): Generator; } diff --git a/lib/Core/Suggestion.php b/lib/Core/Suggestion.php index 1e15fb42..ddb39494 100644 --- a/lib/Core/Suggestion.php +++ b/lib/Core/Suggestion.php @@ -46,7 +46,7 @@ class Suggestion private $shortDescription; /** - * @var string + * @var string|null */ private $label; @@ -83,7 +83,7 @@ private function __construct( $this->type = $type; $this->name = $name; $this->shortDescription = $shortDescription; - $this->label = $label ?: $name; + $this->label = $label; $this->range = $range; $this->documentation = $documentation; $this->snippet = $snippet; @@ -130,6 +130,22 @@ public static function createWithOptions(string $name, array $options): self ); } + public function withName(string $name): self + { + $suggestion = clone $this; + $suggestion->name = $name; + + return $suggestion; + } + + public function withoutNameImport(): self + { + $suggestion = clone $this; + $suggestion->nameImport = null; + + return $suggestion; + } + public function toArray(): array { return [ @@ -185,10 +201,9 @@ public function nameImport(): ?string return $this->nameImport; } - public function label(): string { - return $this->label; + return $this->label ?: $this->name; } public function range(): ?Range diff --git a/tests/Unit/Bridge/TolerantParser/ChainTolerantCompletorTest.php b/tests/Unit/Bridge/TolerantParser/ChainTolerantCompletorTest.php index 77581088..c6a47c31 100644 --- a/tests/Unit/Bridge/TolerantParser/ChainTolerantCompletorTest.php +++ b/tests/Unit/Bridge/TolerantParser/ChainTolerantCompletorTest.php @@ -12,6 +12,7 @@ use Phpactor\Completion\Tests\TestCase; use Phpactor\TestUtils\ExtractOffset; use Phpactor\TextDocument\ByteOffset; +use Phpactor\TextDocument\TextDocument; use Phpactor\TextDocument\TextDocumentBuilder; use Prophecy\Argument; @@ -158,6 +159,125 @@ public function testExcludesNonQualifingClasses() $this->assertTrue($suggestions->getReturn()); } + /** + * @dataProvider provideSuggestionsToResolve + */ + public function testResolveImportName( + TextDocument $textDocument, + ByteOffset $byteOffset, + Suggestion $completorSuggestion, + Suggestion $expectedSuggestion + ): void { + $completor = $this->create([$this->completor1->reveal()]); + + $this->completor1->complete(Argument::type(Node::class), $textDocument, $byteOffset) + ->will(function () use ($completorSuggestion) { + yield $completorSuggestion; + + return false; + }) + ; + + $generator = $completor->complete($textDocument, $byteOffset); + $suggestions = iterator_to_array($generator); + + $this->assertCount(1, $suggestions); + $this->assertFalse($generator->getReturn()); + $this->assertEquals($expectedSuggestion, $suggestions[0]); + } + + public function provideSuggestionsToResolve(): iterable + { + $textDocumentFactory = function (array $data = []): TextDocument { + $useStatements = []; + foreach ($data as $alias => $fqcn) { + $alias = is_int($alias) ? null : $alias; + $useStatements[] = "use $fqcn".($alias ? " as $alias" : ''); + } + + $useStatements = implode(PHP_EOL, $useStatements); + + return TextDocumentBuilder::create( + <<build(); + }; + $computeByteOffset = function (TextDocument $textDocument): ByteOffset { + $matches = []; + preg_match('/Bar \$bar/u', (string) $textDocument, $matches, PREG_OFFSET_CAPTURE); + + return ByteOffset::fromInt($matches[0][1] + 2); + }; + $suggestion = Suggestion::createWithOptions('Bar', [ + 'short_description' => 'App\Foo\Bar', + 'type' => Suggestion::TYPE_CLASS, + 'name_import' => 'App\Foo\Bar', + ]); + + yield 'Not imported yet' => [ + $textDocument = $textDocumentFactory(), + $computeByteOffset($textDocument), + $suggestion, + $suggestion, + ]; + + yield 'Imported without an alias' => [ + $textDocument = $textDocumentFactory(['App\Foo\Bar']), + $computeByteOffset($textDocument), + $suggestion, + $suggestion->withoutNameImport(), + ]; + + yield 'Imported with an alias' => [ + $textDocument = $textDocumentFactory(['Foobar' => 'App\Foo\Bar']), + $computeByteOffset($textDocument), + $suggestion, + $suggestion->withoutNameImport()->withName('Foobar'), + ]; + + yield 'Imported with and without an alias' => [ + $textDocument = $textDocumentFactory([ + 'Foobar' => 'App\Foo\Bar', + 'App\Foo\Bar', + ]), + $computeByteOffset($textDocument), + $suggestion, + $suggestion->withoutNameImport(), + ]; + + yield 'Imported with an aliased namespace' => [ + $textDocument = $textDocumentFactory([ + 'FOO' => 'App\Foo', + 'APP' => 'App', + ]), + $computeByteOffset($textDocument), + $suggestion, + $suggestion->withoutNameImport()->withName('FOO\Bar'), + ]; + + yield 'Imported with and without an aliased namespace' => [ + $textDocument = $textDocumentFactory([ + 'App\Foo\Bar', + 'FOO' => 'App\Foo', + ]), + $computeByteOffset($textDocument), + $suggestion, + $suggestion->withoutNameImport(), + ]; + } + private function create(array $completors): ChainTolerantCompletor { return new ChainTolerantCompletor($completors); diff --git a/tests/Unit/Core/SuggestionTest.php b/tests/Unit/Core/SuggestionTest.php index af5b6309..98990838 100644 --- a/tests/Unit/Core/SuggestionTest.php +++ b/tests/Unit/Core/SuggestionTest.php @@ -66,4 +66,27 @@ public function testCastsToArray() 'name_import' => 'Namespace\\Foobar', ], $suggestion->toArray()); } + + public function testWithName() + { + $suggestion = Suggestion::create('name')->withName('test'); + $this->assertEquals('test', $suggestion->name()); + $this->assertEquals('test', $suggestion->label()); + + $suggestion = Suggestion::createWithOptions( + 'name', + ['label' => 'label'], + )->withName('test'); + $this->assertEquals('test', $suggestion->name()); + $this->assertEquals('label', $suggestion->label()); + } + + public function testWithoutNameImport() + { + $suggestion = Suggestion::createWithOptions( + 'name', + ['name_import' => 'previous'], + )->withoutNameImport(); + $this->assertNull($suggestion->nameImport()); + } } From 48fbc680d4d24c41ff9d83af3dbd9f599c28ec72 Mon Sep 17 00:00:00 2001 From: Camille Dejoye Date: Sat, 28 Nov 2020 17:39:12 +0100 Subject: [PATCH 2/6] add suggestions instead of replacing Before all I did was trying to find the most accurate name to use depending on the import table. It actually not the job of the completor but the responsibility of the user. This commit will instead resolve a class suggestion to a list of suggestions in order for the user to pick the one he wants. --- .../TolerantParser/ChainTolerantCompletor.php | 50 ++++++++----------- .../ChainTolerantCompletorTest.php | 33 ++++++++---- 2 files changed, 46 insertions(+), 37 deletions(-) diff --git a/lib/Bridge/TolerantParser/ChainTolerantCompletor.php b/lib/Bridge/TolerantParser/ChainTolerantCompletor.php index 7ef22a99..5fe6dde3 100644 --- a/lib/Bridge/TolerantParser/ChainTolerantCompletor.php +++ b/lib/Bridge/TolerantParser/ChainTolerantCompletor.php @@ -57,7 +57,11 @@ public function complete(TextDocument $source, ByteOffset $byteOffset): Generato $suggestions = $tolerantCompletor->complete($completionNode, $source, $byteOffset); foreach ($suggestions as $suggestion) { - yield $this->resolveClassSuggestion($completionNode, $suggestion); + // Trick to avoid any BC break when converting to an array + // https://www.php.net/manual/fr/language.generators.syntax.php#control-structures.yield.from + foreach ($this->resolveAliasSuggestions($completionNode, $suggestion) as $resolvedSuggestion) { + yield $resolvedSuggestion; + } } $isComplete = $isComplete && $suggestions->getReturn(); @@ -94,48 +98,38 @@ private function filterNonQualifyingClasses(Node $node): array }); } - private function resolveClassSuggestion(Node $completionNode, Suggestion $suggestion): Suggestion + /** + * Add suggestions when a class is already imported with an alias or when a relative name is abailable. + * + * Will update the suggestion to remove the import_name option if already imported. + * Will add a suggestion if the class is imported under an alias. + * Will add a suggestion if part of the namespace is imported (i.e. ORM\Column is a relative name). + * + * @return Suggestion[] + */ + private function resolveAliasSuggestions(Node $completionNode, Suggestion $suggestion): array { if (Suggestion::TYPE_CLASS !== $suggestion->type()) { - return $suggestion; + return [$suggestion]; } /** @var ResolvedName[] $importTable */ [$importTable] = $completionNode->getImportTablesForCurrentScope(); - // Prioritize import without alias - if (isset($importTable[$suggestion->name()])) { - return $suggestion->withoutNameImport(); - } - $suggestionFqcn = $suggestion->classImport(); - $possibleMatches = []; + $suggestions = [$suggestion->name() => $suggestion]; foreach ($importTable as $alias => $resolvedName) { $importFqcn = $resolvedName->getFullyQualifiedNameText(); - if ($suggestionFqcn === $importFqcn) { - return $suggestion->withoutNameImport()->withName($alias); + if (0 !== strpos($suggestionFqcn, $importFqcn)) { + continue; } - if (0 === strpos($suggestionFqcn, $importFqcn)) { - $possibleMatches[$alias] = $importFqcn; - } - } + $name = $alias.substr($suggestionFqcn, strlen($importFqcn)); - if (!$possibleMatches) { - return $suggestion; + $suggestions[$alias] = $suggestion->withoutNameImport()->withName($name); } - // Sort the possible by matches by FQCN length - uasort($possibleMatches, function (string $firstFqcn, $secondFqcn) { - return strlen($firstFqcn) <=> strlen($secondFqcn); - }); - - // Keep the match with the longest FQCN (more accurate one) - $importFqcn = end($possibleMatches); - $alias = array_key_last($possibleMatches); - $name = $alias.substr($suggestionFqcn, strlen($importFqcn)); - - return $suggestion->withoutNameImport()->withName($name); + return array_values($suggestions); } } diff --git a/tests/Unit/Bridge/TolerantParser/ChainTolerantCompletorTest.php b/tests/Unit/Bridge/TolerantParser/ChainTolerantCompletorTest.php index c6a47c31..5dad256e 100644 --- a/tests/Unit/Bridge/TolerantParser/ChainTolerantCompletorTest.php +++ b/tests/Unit/Bridge/TolerantParser/ChainTolerantCompletorTest.php @@ -161,12 +161,14 @@ public function testExcludesNonQualifingClasses() /** * @dataProvider provideSuggestionsToResolve + * + * @param Suggestion[] $expectedSuggestions */ public function testResolveImportName( TextDocument $textDocument, ByteOffset $byteOffset, Suggestion $completorSuggestion, - Suggestion $expectedSuggestion + array $expectedSuggestions ): void { $completor = $this->create([$this->completor1->reveal()]); @@ -181,9 +183,9 @@ public function testResolveImportName( $generator = $completor->complete($textDocument, $byteOffset); $suggestions = iterator_to_array($generator); - $this->assertCount(1, $suggestions); + $this->assertCount(count($expectedSuggestions), $suggestions); $this->assertFalse($generator->getReturn()); - $this->assertEquals($expectedSuggestion, $suggestions[0]); + $this->assertEqualsCanonicalizing($expectedSuggestions, $suggestions); } public function provideSuggestionsToResolve(): iterable @@ -230,21 +232,24 @@ public function methodName(Bar \$bar) $textDocument = $textDocumentFactory(), $computeByteOffset($textDocument), $suggestion, - $suggestion, + [$suggestion], ]; yield 'Imported without an alias' => [ $textDocument = $textDocumentFactory(['App\Foo\Bar']), $computeByteOffset($textDocument), $suggestion, - $suggestion->withoutNameImport(), + [$suggestion->withoutNameImport()], ]; yield 'Imported with an alias' => [ $textDocument = $textDocumentFactory(['Foobar' => 'App\Foo\Bar']), $computeByteOffset($textDocument), $suggestion, - $suggestion->withoutNameImport()->withName('Foobar'), + [ + $suggestion, + $suggestion->withoutNameImport()->withName('Foobar'), + ], ]; yield 'Imported with and without an alias' => [ @@ -254,7 +259,10 @@ public function methodName(Bar \$bar) ]), $computeByteOffset($textDocument), $suggestion, - $suggestion->withoutNameImport(), + [ + $suggestion->withoutNameImport(), + $suggestion->withoutNameImport()->withName('Foobar'), + ], ]; yield 'Imported with an aliased namespace' => [ @@ -264,7 +272,11 @@ public function methodName(Bar \$bar) ]), $computeByteOffset($textDocument), $suggestion, - $suggestion->withoutNameImport()->withName('FOO\Bar'), + [ + $suggestion, + $suggestion->withoutNameImport()->withName('FOO\Bar'), + $suggestion->withoutNameImport()->withName('APP\Foo\Bar'), + ], ]; yield 'Imported with and without an aliased namespace' => [ @@ -274,7 +286,10 @@ public function methodName(Bar \$bar) ]), $computeByteOffset($textDocument), $suggestion, - $suggestion->withoutNameImport(), + [ + $suggestion->withoutNameImport(), + $suggestion->withoutNameImport()->withName('FOO\Bar'), + ], ]; } From 010fe9dbf62f3f15cfdf6169c418598132134e2f Mon Sep 17 00:00:00 2001 From: Camille Dejoye Date: Sat, 28 Nov 2020 17:51:22 +0100 Subject: [PATCH 3/6] extract the logic into a trait I want to extract the logic in order to reuse it in DoctrineAnnotationCompletor Inheritance is not suited in this case, the only remaining options I can think of are: * Using a static method in an helper class * Injecting a new dependency I don't think injecting a dependency is a good solution here since I can't think of a different implementation I might want to inject. The helper class with a static method is a possibility but I think trait are also meant for scenarios like this one. So I kept the trait to open a new debate :) --- .../TolerantParser/ChainTolerantCompletor.php | 43 ++------------ .../ResolveAliasSuggestionsTrait.php | 58 +++++++++++++++++++ 2 files changed, 63 insertions(+), 38 deletions(-) create mode 100644 lib/Bridge/TolerantParser/ResolveAliasSuggestionsTrait.php diff --git a/lib/Bridge/TolerantParser/ChainTolerantCompletor.php b/lib/Bridge/TolerantParser/ChainTolerantCompletor.php index 5fe6dde3..60184a04 100644 --- a/lib/Bridge/TolerantParser/ChainTolerantCompletor.php +++ b/lib/Bridge/TolerantParser/ChainTolerantCompletor.php @@ -5,15 +5,15 @@ use Generator; use Microsoft\PhpParser\Node; use Microsoft\PhpParser\Parser; -use Microsoft\PhpParser\ResolvedName; use Phpactor\Completion\Core\Completor; -use Phpactor\Completion\Core\Suggestion; use Phpactor\Completion\Core\Util\OffsetHelper; use Phpactor\TextDocument\ByteOffset; use Phpactor\TextDocument\TextDocument; class ChainTolerantCompletor implements Completor { + use ResolveAliasSuggestionsTrait; + /** * @var Parser */ @@ -55,11 +55,13 @@ public function complete(TextDocument $source, ByteOffset $byteOffset): Generato continue; } + $importTable = $this->getClassImportTablesForNode($completionNode); $suggestions = $tolerantCompletor->complete($completionNode, $source, $byteOffset); + foreach ($suggestions as $suggestion) { // Trick to avoid any BC break when converting to an array // https://www.php.net/manual/fr/language.generators.syntax.php#control-structures.yield.from - foreach ($this->resolveAliasSuggestions($completionNode, $suggestion) as $resolvedSuggestion) { + foreach ($this->resolveAliasSuggestions($importTable, $suggestion) as $resolvedSuggestion) { yield $resolvedSuggestion; } } @@ -97,39 +99,4 @@ private function filterNonQualifyingClasses(Node $node): array return $completor->qualifier()->couldComplete($node); }); } - - /** - * Add suggestions when a class is already imported with an alias or when a relative name is abailable. - * - * Will update the suggestion to remove the import_name option if already imported. - * Will add a suggestion if the class is imported under an alias. - * Will add a suggestion if part of the namespace is imported (i.e. ORM\Column is a relative name). - * - * @return Suggestion[] - */ - private function resolveAliasSuggestions(Node $completionNode, Suggestion $suggestion): array - { - if (Suggestion::TYPE_CLASS !== $suggestion->type()) { - return [$suggestion]; - } - - /** @var ResolvedName[] $importTable */ - [$importTable] = $completionNode->getImportTablesForCurrentScope(); - - $suggestionFqcn = $suggestion->classImport(); - $suggestions = [$suggestion->name() => $suggestion]; - foreach ($importTable as $alias => $resolvedName) { - $importFqcn = $resolvedName->getFullyQualifiedNameText(); - - if (0 !== strpos($suggestionFqcn, $importFqcn)) { - continue; - } - - $name = $alias.substr($suggestionFqcn, strlen($importFqcn)); - - $suggestions[$alias] = $suggestion->withoutNameImport()->withName($name); - } - - return array_values($suggestions); - } } diff --git a/lib/Bridge/TolerantParser/ResolveAliasSuggestionsTrait.php b/lib/Bridge/TolerantParser/ResolveAliasSuggestionsTrait.php new file mode 100644 index 00000000..0506b0e9 --- /dev/null +++ b/lib/Bridge/TolerantParser/ResolveAliasSuggestionsTrait.php @@ -0,0 +1,58 @@ +type()) { + return [$suggestion]; + } + + $suggestionFqcn = $suggestion->classImport(); + $suggestions = [$suggestion->name() => $suggestion]; + foreach ($importTable as $alias => $resolvedName) { + $importFqcn = $resolvedName->getFullyQualifiedNameText(); + + if (0 !== strpos($suggestionFqcn, $importFqcn)) { + continue; + } + + $name = $alias.substr($suggestionFqcn, strlen($importFqcn)); + + $suggestions[$alias] = $suggestion->withoutNameImport()->withName($name); + } + + return array_values($suggestions); + } + + /** + * @return ResolvedName[] + */ + private function getClassImportTablesForNode(Node $node): array + { + try { + [$importTable] = $node->getImportTablesForCurrentScope(); + } catch (\Exception $e) { + $importTable = []; + } + + return $importTable; + } +} From 3eae72178853a271c62a009ef25e2f8fe2acc26a Mon Sep 17 00:00:00 2001 From: Camille Dejoye Date: Sat, 28 Nov 2020 18:37:06 +0100 Subject: [PATCH 4/6] add the feature to DoctrineAnnotationCompletor --- .../DoctrineAnnotationCompletor.php | 17 ++++---- lib/Core/Suggestion.php | 8 ++++ .../DoctrineAnnotationCompletorTest.php | 40 ++++++++++++++++++- tests/Unit/Core/SuggestionTest.php | 9 +++++ 4 files changed, 64 insertions(+), 10 deletions(-) diff --git a/lib/Bridge/TolerantParser/WorseReflection/DoctrineAnnotationCompletor.php b/lib/Bridge/TolerantParser/WorseReflection/DoctrineAnnotationCompletor.php index 15c8e81d..0dcb39b6 100644 --- a/lib/Bridge/TolerantParser/WorseReflection/DoctrineAnnotationCompletor.php +++ b/lib/Bridge/TolerantParser/WorseReflection/DoctrineAnnotationCompletor.php @@ -6,12 +6,12 @@ use Microsoft\PhpParser\Node; use Microsoft\PhpParser\Node\SourceFileNode; use Microsoft\PhpParser\Parser; +use Phpactor\Completion\Bridge\TolerantParser\ResolveAliasSuggestionsTrait; use Phpactor\Completion\Core\Completor; use Phpactor\Completion\Core\Completor\NameSearcherCompletor; use Phpactor\Completion\Core\Suggestion; use Phpactor\Completion\Core\Util\OffsetHelper; use Phpactor\ReferenceFinder\NameSearcher; -use Phpactor\ReferenceFinder\Search\NameSearchResult; use Phpactor\TextDocument\ByteOffset; use Phpactor\TextDocument\TextDocument; use Phpactor\WorseReflection\Core\Exception\NotFound; @@ -19,6 +19,8 @@ class DoctrineAnnotationCompletor extends NameSearcherCompletor implements Completor { + use ResolveAliasSuggestionsTrait; + /** * @var Reflector */ @@ -61,6 +63,7 @@ public function complete(TextDocument $source, ByteOffset $byteOffset): Generato return true; } + $importTable = $this->getClassImportTablesForNode($node); $suggestions = $this->completeName($annotation); foreach ($suggestions as $suggestion) { @@ -68,19 +71,15 @@ public function complete(TextDocument $source, ByteOffset $byteOffset): Generato continue; } - yield $suggestion; + $resolvedSuggestions = $this->resolveAliasSuggestions($importTable, $suggestion); + foreach ($resolvedSuggestions as $resolvedSuggestion) { + yield $resolvedSuggestion->withSnippet($resolvedSuggestion->name().'($1)$0'); + } } return $suggestions->getReturn(); } - protected function createSuggestionOptions(NameSearchResult $result): array - { - return array_merge(parent::createSuggestionOptions($result), [ - 'snippet' => (string) $result->name()->head() .'($1)$0', - ]); - } - private function truncateSource(string $source, int $byteOffset): string { // truncate source at byte offset - we don't want the rest of the source diff --git a/lib/Core/Suggestion.php b/lib/Core/Suggestion.php index ddb39494..215ee9d9 100644 --- a/lib/Core/Suggestion.php +++ b/lib/Core/Suggestion.php @@ -146,6 +146,14 @@ public function withoutNameImport(): self return $suggestion; } + public function withSnippet(string $snippet): self + { + $suggestion = clone $this; + $suggestion->snippet = $snippet; + + return $suggestion; + } + public function toArray(): array { return [ diff --git a/tests/Integration/Bridge/TolerantParser/DoctrineAnnotationCompletorTest.php b/tests/Integration/Bridge/TolerantParser/DoctrineAnnotationCompletorTest.php index 451145ba..53e3ddf2 100644 --- a/tests/Integration/Bridge/TolerantParser/DoctrineAnnotationCompletorTest.php +++ b/tests/Integration/Bridge/TolerantParser/DoctrineAnnotationCompletorTest.php @@ -104,7 +104,45 @@ class Foo {} 'type' => Suggestion::TYPE_CLASS, 'name' => 'Entity', 'short_description' => 'App\Annotation\Entity', - 'snippet' => 'Entity($1)$0' + 'snippet' => 'Entity($1)$0', + 'name_import' => 'App\Annotation\Entity', + ] + ]]; + + + yield 'in a namespace with an import' => [ + <<<'EOT' + + */ +class Foo {} +EOT + , [ + [ + 'type' => Suggestion::TYPE_CLASS, + 'name' => 'APP\Entity', + 'short_description' => 'App\Annotation\Entity', + 'snippet' => 'APP\Entity($1)$0', + 'name_import' => null, + ], [ + 'type' => Suggestion::TYPE_CLASS, + 'name' => 'Entity', + 'short_description' => 'App\Annotation\Entity', + 'snippet' => 'Entity($1)$0', + 'name_import' => 'App\Annotation\Entity', ] ]]; diff --git a/tests/Unit/Core/SuggestionTest.php b/tests/Unit/Core/SuggestionTest.php index 98990838..f809f85e 100644 --- a/tests/Unit/Core/SuggestionTest.php +++ b/tests/Unit/Core/SuggestionTest.php @@ -89,4 +89,13 @@ public function testWithoutNameImport() )->withoutNameImport(); $this->assertNull($suggestion->nameImport()); } + + public function testWithSnippet() + { + $suggestion = Suggestion::createWithOptions( + 'name', + ['snippet' => 'snippet'], + )->withSnippet('test'); + $this->assertEquals('test', $suggestion->snippet()); + } } From cebd3255c39bb7af8bb16b4716dfe3f011723290 Mon Sep 17 00:00:00 2001 From: Camille Dejoye Date: Sun, 29 Nov 2020 12:55:03 +0100 Subject: [PATCH 5/6] refactor to use ResolveAliasSuggestionCompletor decorator --- .../TolerantParser/ChainTolerantCompletor.php | 14 +- .../ResolveAliasSuggestionCompletor.php | 150 ++++++++++ .../ResolveAliasSuggestionsTrait.php | 58 ---- .../DoctrineAnnotationCompletor.php | 17 +- .../DoctrineAnnotationCompletorTest.php | 37 --- .../ResolveAliasSuggestionCompletorTest.php | 257 ++++++++++++++++++ .../ChainTolerantCompletorTest.php | 135 --------- 7 files changed, 419 insertions(+), 249 deletions(-) create mode 100644 lib/Bridge/TolerantParser/ResolveAliasSuggestionCompletor.php delete mode 100644 lib/Bridge/TolerantParser/ResolveAliasSuggestionsTrait.php create mode 100644 tests/Integration/Bridge/TolerantParser/ResolveAliasSuggestionCompletorTest.php diff --git a/lib/Bridge/TolerantParser/ChainTolerantCompletor.php b/lib/Bridge/TolerantParser/ChainTolerantCompletor.php index 60184a04..59fbcc09 100644 --- a/lib/Bridge/TolerantParser/ChainTolerantCompletor.php +++ b/lib/Bridge/TolerantParser/ChainTolerantCompletor.php @@ -12,8 +12,6 @@ class ChainTolerantCompletor implements Completor { - use ResolveAliasSuggestionsTrait; - /** * @var Parser */ @@ -38,7 +36,8 @@ public function complete(TextDocument $source, ByteOffset $byteOffset): Generato $truncatedSource = $this->truncateSource((string) $source, $byteOffset->toInt()); $node = $this->parser->parseSourceFile($truncatedSource)->getDescendantNodeAtPosition( - // the parser requires the byte offset, not the char offset + // use strlen because the parser requires the byte offset, not the char offset + // But we need to recalculate it because we removed trailing spaces when truncating strlen($truncatedSource) ); @@ -55,16 +54,9 @@ public function complete(TextDocument $source, ByteOffset $byteOffset): Generato continue; } - $importTable = $this->getClassImportTablesForNode($completionNode); $suggestions = $tolerantCompletor->complete($completionNode, $source, $byteOffset); - foreach ($suggestions as $suggestion) { - // Trick to avoid any BC break when converting to an array - // https://www.php.net/manual/fr/language.generators.syntax.php#control-structures.yield.from - foreach ($this->resolveAliasSuggestions($importTable, $suggestion) as $resolvedSuggestion) { - yield $resolvedSuggestion; - } - } + yield from $suggestions; $isComplete = $isComplete && $suggestions->getReturn(); } diff --git a/lib/Bridge/TolerantParser/ResolveAliasSuggestionCompletor.php b/lib/Bridge/TolerantParser/ResolveAliasSuggestionCompletor.php new file mode 100644 index 00000000..191615bc --- /dev/null +++ b/lib/Bridge/TolerantParser/ResolveAliasSuggestionCompletor.php @@ -0,0 +1,150 @@ +decorated = $decorated; + $this->parser = $parser ?: new Parser(); + } + + /** + * {@inheritDoc} + */ + public function complete(TextDocument $source, ByteOffset $byteOffset): Generator + { + $importTable = $this->getClassImportTableAtPosition($source, $byteOffset); + $suggestions = $this->decorated->complete($source, $byteOffset); + + foreach ($suggestions as $suggestion) { + $resolvedSuggestions = $this->resolveAliasSuggestions($importTable, $suggestion); + + // Trick to avoid any BC break when converting to an array + // https://www.php.net/manual/fr/language.generators.syntax.php#control-structures.yield.from + foreach ($resolvedSuggestions as $resolvedSuggestion) { + yield $resolvedSuggestion; + } + } + + return $suggestions->getReturn(); + } + + private function truncateSource(string $source, int $byteOffset): string + { + // truncate source at byte offset - we don't want the rest of the source + // file contaminating the completion (for example `$foo($<>\n $bar = + // ` will evaluate the Variable node as an expression node with a + // double variable `$\n $bar = ` + $truncatedSource = substr($source, 0, $byteOffset); + + // determine the last non-whitespace _character_ offset + $characterOffset = OffsetHelper::lastNonWhitespaceCharacterOffset($truncatedSource); + + // truncate the source at the character offset + $truncatedSource = mb_substr($source, 0, $characterOffset); + + return $truncatedSource; + } + + /** + * Add suggestions when a class is already imported with an alias or when a relative name is abailable. + * + * Will update the suggestion to remove the import_name option if already imported. + * Will add a suggestion if the class is imported under an alias. + * Will add a suggestion if part of the namespace is imported (i.e. ORM\Column is a relative name). + * + * @param ResolvedName[] $importTable + * + * @return Suggestion[] + */ + private function resolveAliasSuggestions(array $importTable, Suggestion $suggestion): array + { + if (Suggestion::TYPE_CLASS !== $suggestion->type()) { + return [$suggestion]; + } + + $suggestionFqcn = $suggestion->nameImport(); + $originalName = $suggestion->name(); + $originalSnippet = $suggestion->snippet(); + $suggestions = [$suggestion->name() => $suggestion]; + + foreach ($importTable as $alias => $resolvedName) { + $importFqcn = $resolvedName->getFullyQualifiedNameText(); + + if (0 !== strpos($suggestionFqcn, $importFqcn)) { + continue; + } + + $name = $alias.substr($suggestionFqcn, strlen($importFqcn)); + $suggestions[$alias] = $suggestion->withoutNameImport()->withName($name); + + if ($originalSnippet && $originalName !== $name) { + $snippet = str_replace($originalName, $name, $originalSnippet); + $suggestions[$alias] = $suggestions[$alias]->withSnippet($snippet); + } + } + + return array_values($suggestions); + } + + /** + * @return ResolvedName[] + */ + private function getClassImportTableAtPosition(TextDocument $source, ByteOffset $byteOffset): array + { + // We only need the closest node to retrieve the import table + // It's not a big deal if it's not the completed node as long as it has + // the same import table + $node = $this->getClosestNodeAtPosition( + $this->parser->parseSourceFile((string) $source), + $byteOffset->toInt(), + ); + + try { + [$importTable] = $node->getImportTablesForCurrentScope(); + } catch (\Exception $e) { + // If the node does not have an import table (SourceFileNode for example) + $importTable = []; + } + + return $importTable; + } + + private function getClosestNodeAtPosition(SourceFileNode $sourceFileNode, int $position): Node + { + $lastNode = $sourceFileNode; + /** @var Node $node */ + foreach ($sourceFileNode->getDescendantNodes() as $node) { + if ($position < $node->getFullStart()) { + return $lastNode; + } + + $lastNode = $node; + } + + return $lastNode; + } +} diff --git a/lib/Bridge/TolerantParser/ResolveAliasSuggestionsTrait.php b/lib/Bridge/TolerantParser/ResolveAliasSuggestionsTrait.php deleted file mode 100644 index 0506b0e9..00000000 --- a/lib/Bridge/TolerantParser/ResolveAliasSuggestionsTrait.php +++ /dev/null @@ -1,58 +0,0 @@ -type()) { - return [$suggestion]; - } - - $suggestionFqcn = $suggestion->classImport(); - $suggestions = [$suggestion->name() => $suggestion]; - foreach ($importTable as $alias => $resolvedName) { - $importFqcn = $resolvedName->getFullyQualifiedNameText(); - - if (0 !== strpos($suggestionFqcn, $importFqcn)) { - continue; - } - - $name = $alias.substr($suggestionFqcn, strlen($importFqcn)); - - $suggestions[$alias] = $suggestion->withoutNameImport()->withName($name); - } - - return array_values($suggestions); - } - - /** - * @return ResolvedName[] - */ - private function getClassImportTablesForNode(Node $node): array - { - try { - [$importTable] = $node->getImportTablesForCurrentScope(); - } catch (\Exception $e) { - $importTable = []; - } - - return $importTable; - } -} diff --git a/lib/Bridge/TolerantParser/WorseReflection/DoctrineAnnotationCompletor.php b/lib/Bridge/TolerantParser/WorseReflection/DoctrineAnnotationCompletor.php index 0dcb39b6..15c8e81d 100644 --- a/lib/Bridge/TolerantParser/WorseReflection/DoctrineAnnotationCompletor.php +++ b/lib/Bridge/TolerantParser/WorseReflection/DoctrineAnnotationCompletor.php @@ -6,12 +6,12 @@ use Microsoft\PhpParser\Node; use Microsoft\PhpParser\Node\SourceFileNode; use Microsoft\PhpParser\Parser; -use Phpactor\Completion\Bridge\TolerantParser\ResolveAliasSuggestionsTrait; use Phpactor\Completion\Core\Completor; use Phpactor\Completion\Core\Completor\NameSearcherCompletor; use Phpactor\Completion\Core\Suggestion; use Phpactor\Completion\Core\Util\OffsetHelper; use Phpactor\ReferenceFinder\NameSearcher; +use Phpactor\ReferenceFinder\Search\NameSearchResult; use Phpactor\TextDocument\ByteOffset; use Phpactor\TextDocument\TextDocument; use Phpactor\WorseReflection\Core\Exception\NotFound; @@ -19,8 +19,6 @@ class DoctrineAnnotationCompletor extends NameSearcherCompletor implements Completor { - use ResolveAliasSuggestionsTrait; - /** * @var Reflector */ @@ -63,7 +61,6 @@ public function complete(TextDocument $source, ByteOffset $byteOffset): Generato return true; } - $importTable = $this->getClassImportTablesForNode($node); $suggestions = $this->completeName($annotation); foreach ($suggestions as $suggestion) { @@ -71,15 +68,19 @@ public function complete(TextDocument $source, ByteOffset $byteOffset): Generato continue; } - $resolvedSuggestions = $this->resolveAliasSuggestions($importTable, $suggestion); - foreach ($resolvedSuggestions as $resolvedSuggestion) { - yield $resolvedSuggestion->withSnippet($resolvedSuggestion->name().'($1)$0'); - } + yield $suggestion; } return $suggestions->getReturn(); } + protected function createSuggestionOptions(NameSearchResult $result): array + { + return array_merge(parent::createSuggestionOptions($result), [ + 'snippet' => (string) $result->name()->head() .'($1)$0', + ]); + } + private function truncateSource(string $source, int $byteOffset): string { // truncate source at byte offset - we don't want the rest of the source diff --git a/tests/Integration/Bridge/TolerantParser/DoctrineAnnotationCompletorTest.php b/tests/Integration/Bridge/TolerantParser/DoctrineAnnotationCompletorTest.php index 53e3ddf2..c06e0009 100644 --- a/tests/Integration/Bridge/TolerantParser/DoctrineAnnotationCompletorTest.php +++ b/tests/Integration/Bridge/TolerantParser/DoctrineAnnotationCompletorTest.php @@ -109,43 +109,6 @@ class Foo {} ] ]]; - - yield 'in a namespace with an import' => [ - <<<'EOT' - - */ -class Foo {} -EOT - , [ - [ - 'type' => Suggestion::TYPE_CLASS, - 'name' => 'APP\Entity', - 'short_description' => 'App\Annotation\Entity', - 'snippet' => 'APP\Entity($1)$0', - 'name_import' => null, - ], [ - 'type' => Suggestion::TYPE_CLASS, - 'name' => 'Entity', - 'short_description' => 'App\Annotation\Entity', - 'snippet' => 'Entity($1)$0', - 'name_import' => 'App\Annotation\Entity', - ] - ]]; - yield 'annotation on a node in the middle of the AST' => [ <<<'EOT' prophesize(Completor::class); + $completor = new ResolveAliasSuggestionCompletor($decoratedCompletor->reveal()); + + $decoratedCompletor->complete($textDocument, $byteOffset) + ->will(function () use ($completorSuggestion) { + yield $completorSuggestion; + + return true; + }) + ; + + $generator = $completor->complete($textDocument, $byteOffset); + $suggestions = iterator_to_array($generator, false); + + $this->assertCount(count($expectedSuggestions), $suggestions); + $this->assertTrue($generator->getReturn()); + $this->assertEqualsCanonicalizing($expectedSuggestions, $suggestions); + } + + public function provideSuggestionsToResolve(): iterable + { + $textDocumentFactory = function (array $data = []): TextDocument { + $useStatements = []; + foreach ($data as $alias => $fqcn) { + $alias = is_int($alias) ? null : $alias; + $useStatements[] = "use $fqcn".($alias ? " as $alias" : ''); + } + + $useStatements = implode(PHP_EOL, $useStatements); + + return TextDocumentBuilder::create( + <<build(); + }; + $computeByteOffset = function (TextDocument $textDocument): ByteOffset { + $matches = []; + preg_match('/Bar \$bar/u', (string) $textDocument, $matches, PREG_OFFSET_CAPTURE); + + return ByteOffset::fromInt($matches[0][1] + 2); + }; + $suggestion = Suggestion::createWithOptions('Bar', [ + 'short_description' => 'App\Foo\Bar', + 'type' => Suggestion::TYPE_CLASS, + 'name_import' => 'App\Foo\Bar', + ]); + + yield 'Class not imported yet' => [ + $textDocument = $textDocumentFactory(), + $computeByteOffset($textDocument), + $suggestion, + [$suggestion], + ]; + + yield 'Class imported without an alias' => [ + $textDocument = $textDocumentFactory(['App\Foo\Bar']), + $computeByteOffset($textDocument), + $suggestion, + [$suggestion->withoutNameImport()], + ]; + + yield 'Class imported with an alias' => [ + $textDocument = $textDocumentFactory(['Foobar' => 'App\Foo\Bar']), + $computeByteOffset($textDocument), + $suggestion, + [ + $suggestion, + $suggestion->withoutNameImport()->withName('Foobar'), + ], + ]; + + yield 'Class imported with and without an alias' => [ + $textDocument = $textDocumentFactory([ + 'Foobar' => 'App\Foo\Bar', + 'App\Foo\Bar', + ]), + $computeByteOffset($textDocument), + $suggestion, + [ + $suggestion->withoutNameImport(), + $suggestion->withoutNameImport()->withName('Foobar'), + ], + ]; + + yield 'Class imported with an aliased namespace' => [ + $textDocument = $textDocumentFactory([ + 'FOO' => 'App\Foo', + 'APP' => 'App', + ]), + $computeByteOffset($textDocument), + $suggestion, + [ + $suggestion, + $suggestion->withoutNameImport()->withName('FOO\Bar'), + $suggestion->withoutNameImport()->withName('APP\Foo\Bar'), + ], + ]; + + yield 'Class imported with and without an aliased namespace' => [ + $textDocument = $textDocumentFactory([ + 'App\Foo\Bar', + 'FOO' => 'App\Foo', + ]), + $computeByteOffset($textDocument), + $suggestion, + [ + $suggestion->withoutNameImport(), + $suggestion->withoutNameImport()->withName('FOO\Bar'), + ], + ]; + } + + public function provideAnnotationToResolve(): iterable + { + $textDocumentFactory = function (array $data = []): TextDocument { + $useStatements = []; + foreach ($data as $alias => $fqcn) { + $alias = is_int($alias) ? null : $alias; + $useStatements[] = "use $fqcn".($alias ? " as $alias" : ''); + } + + $useStatements = implode(PHP_EOL, $useStatements); + + return TextDocumentBuilder::create( + <<build(); + }; + $computeByteOffset = function (TextDocument $textDocument): ByteOffset { + $matches = []; + preg_match('/\* @Ba/u', (string) $textDocument, $matches, PREG_OFFSET_CAPTURE); + + return ByteOffset::fromInt($matches[0][1] + 5); + }; + $suggestion = Suggestion::createWithOptions('Bar', [ + 'short_description' => 'App\Foo\Bar', + 'type' => Suggestion::TYPE_CLASS, + 'name_import' => 'App\Foo\Bar', + 'snippet' => 'Bar($1)$0', + ]); + + yield 'Annotation not imported yet' => [ + $textDocument = $textDocumentFactory(), + $computeByteOffset($textDocument), + $suggestion, + [$suggestion], + ]; + + yield 'Annotation imported without an alias' => [ + $textDocument = $textDocumentFactory(['App\Foo\Bar']), + $computeByteOffset($textDocument), + $suggestion, + [$suggestion->withoutNameImport()], + ]; + + yield 'Annotation imported with an alias' => [ + $textDocument = $textDocumentFactory(['Foobar' => 'App\Foo\Bar']), + $computeByteOffset($textDocument), + $suggestion, + [ + $suggestion, + $suggestion->withoutNameImport()->withName('Foobar')->withSnippet('Foobar($1)$0'), + ], + ]; + + yield 'Annotation imported with and without an alias' => [ + $textDocument = $textDocumentFactory([ + 'Foobar' => 'App\Foo\Bar', + 'App\Foo\Bar', + ]), + $computeByteOffset($textDocument), + $suggestion, + [ + $suggestion->withoutNameImport(), + $suggestion->withoutNameImport()->withName('Foobar')->withSnippet('Foobar($1)$0'), + ], + ]; + + yield 'Annotation imported with an aliased namespace' => [ + $textDocument = $textDocumentFactory([ + 'FOO' => 'App\Foo', + 'APP' => 'App', + ]), + $computeByteOffset($textDocument), + $suggestion, + [ + $suggestion, + $suggestion->withoutNameImport()->withName('FOO\Bar')->withSnippet('FOO\Bar($1)$0'), + $suggestion->withoutNameImport()->withName('APP\Foo\Bar')->withSnippet('APP\Foo\Bar($1)$0'), + ], + ]; + + yield 'Annotation imported with and without an aliased namespace' => [ + $textDocument = $textDocumentFactory([ + 'App\Foo\Bar', + 'FOO' => 'App\Foo', + ]), + $computeByteOffset($textDocument), + $suggestion, + [ + $suggestion->withoutNameImport(), + $suggestion->withoutNameImport()->withName('FOO\Bar')->withSnippet('FOO\Bar($1)$0'), + ], + ]; + } +} diff --git a/tests/Unit/Bridge/TolerantParser/ChainTolerantCompletorTest.php b/tests/Unit/Bridge/TolerantParser/ChainTolerantCompletorTest.php index 5dad256e..77581088 100644 --- a/tests/Unit/Bridge/TolerantParser/ChainTolerantCompletorTest.php +++ b/tests/Unit/Bridge/TolerantParser/ChainTolerantCompletorTest.php @@ -12,7 +12,6 @@ use Phpactor\Completion\Tests\TestCase; use Phpactor\TestUtils\ExtractOffset; use Phpactor\TextDocument\ByteOffset; -use Phpactor\TextDocument\TextDocument; use Phpactor\TextDocument\TextDocumentBuilder; use Prophecy\Argument; @@ -159,140 +158,6 @@ public function testExcludesNonQualifingClasses() $this->assertTrue($suggestions->getReturn()); } - /** - * @dataProvider provideSuggestionsToResolve - * - * @param Suggestion[] $expectedSuggestions - */ - public function testResolveImportName( - TextDocument $textDocument, - ByteOffset $byteOffset, - Suggestion $completorSuggestion, - array $expectedSuggestions - ): void { - $completor = $this->create([$this->completor1->reveal()]); - - $this->completor1->complete(Argument::type(Node::class), $textDocument, $byteOffset) - ->will(function () use ($completorSuggestion) { - yield $completorSuggestion; - - return false; - }) - ; - - $generator = $completor->complete($textDocument, $byteOffset); - $suggestions = iterator_to_array($generator); - - $this->assertCount(count($expectedSuggestions), $suggestions); - $this->assertFalse($generator->getReturn()); - $this->assertEqualsCanonicalizing($expectedSuggestions, $suggestions); - } - - public function provideSuggestionsToResolve(): iterable - { - $textDocumentFactory = function (array $data = []): TextDocument { - $useStatements = []; - foreach ($data as $alias => $fqcn) { - $alias = is_int($alias) ? null : $alias; - $useStatements[] = "use $fqcn".($alias ? " as $alias" : ''); - } - - $useStatements = implode(PHP_EOL, $useStatements); - - return TextDocumentBuilder::create( - <<build(); - }; - $computeByteOffset = function (TextDocument $textDocument): ByteOffset { - $matches = []; - preg_match('/Bar \$bar/u', (string) $textDocument, $matches, PREG_OFFSET_CAPTURE); - - return ByteOffset::fromInt($matches[0][1] + 2); - }; - $suggestion = Suggestion::createWithOptions('Bar', [ - 'short_description' => 'App\Foo\Bar', - 'type' => Suggestion::TYPE_CLASS, - 'name_import' => 'App\Foo\Bar', - ]); - - yield 'Not imported yet' => [ - $textDocument = $textDocumentFactory(), - $computeByteOffset($textDocument), - $suggestion, - [$suggestion], - ]; - - yield 'Imported without an alias' => [ - $textDocument = $textDocumentFactory(['App\Foo\Bar']), - $computeByteOffset($textDocument), - $suggestion, - [$suggestion->withoutNameImport()], - ]; - - yield 'Imported with an alias' => [ - $textDocument = $textDocumentFactory(['Foobar' => 'App\Foo\Bar']), - $computeByteOffset($textDocument), - $suggestion, - [ - $suggestion, - $suggestion->withoutNameImport()->withName('Foobar'), - ], - ]; - - yield 'Imported with and without an alias' => [ - $textDocument = $textDocumentFactory([ - 'Foobar' => 'App\Foo\Bar', - 'App\Foo\Bar', - ]), - $computeByteOffset($textDocument), - $suggestion, - [ - $suggestion->withoutNameImport(), - $suggestion->withoutNameImport()->withName('Foobar'), - ], - ]; - - yield 'Imported with an aliased namespace' => [ - $textDocument = $textDocumentFactory([ - 'FOO' => 'App\Foo', - 'APP' => 'App', - ]), - $computeByteOffset($textDocument), - $suggestion, - [ - $suggestion, - $suggestion->withoutNameImport()->withName('FOO\Bar'), - $suggestion->withoutNameImport()->withName('APP\Foo\Bar'), - ], - ]; - - yield 'Imported with and without an aliased namespace' => [ - $textDocument = $textDocumentFactory([ - 'App\Foo\Bar', - 'FOO' => 'App\Foo', - ]), - $computeByteOffset($textDocument), - $suggestion, - [ - $suggestion->withoutNameImport(), - $suggestion->withoutNameImport()->withName('FOO\Bar'), - ], - ]; - } - private function create(array $completors): ChainTolerantCompletor { return new ChainTolerantCompletor($completors); From 5a973422b7d045b6c37f7734cdcdeb40297f94fb Mon Sep 17 00:00:00 2001 From: Camille Dejoye Date: Sun, 29 Nov 2020 16:56:22 +0100 Subject: [PATCH 6/6] v2: add a dedicated completor for imported names We want to have a dedicated completor for the resolved import suggestions. The idea is to register multiple time the same completor but with different dependencies: NameSearcherCompletor => Find names with indexer and genrate the suggestions TolerantNameSearcherCompletor => complete class names using a NameSearcherCompletor DoctrineAnnotationCompletor => complete annotations using a NameSearcherCompletor ImportedNameSearcherCompletor => complete with import table from the suggestions of a NameSearcherCompletor Completors: TolerantNameSearcherCompletor(NameSearcherCompletor) DoctrineAnnotationCompletor(NameSearcherCompletor) TolerantNameSearcherCompletor(ImportedNameSearcherCompletor) DoctrineAnnotationCompletor(ImportedNameSearcherCompletor) --- ....php => ImportedNameSearcherCompletor.php} | 23 ++++-- .../ReferenceFinder/NameSearcherCompletor.php | 14 +++- .../DoctrineAnnotationCompletor.php | 29 +++---- .../Completor/CoreNameSearcherCompletor.php | 61 +++++++++++++++ lib/Core/Completor/NameSearcherCompletor.php | 75 +++---------------- .../DoctrineAnnotationCompletorTest.php | 47 ++++++++---- ... => ImportedNameSearcherCompletorTest.php} | 56 +++++--------- .../NameSearcherCompletorTest.php | 22 ++++-- .../CoreNameSearcherCompletorTest.php | 36 +++++++++ 9 files changed, 214 insertions(+), 149 deletions(-) rename lib/Bridge/TolerantParser/{ResolveAliasSuggestionCompletor.php => ImportedNameSearcherCompletor.php} (86%) create mode 100644 lib/Core/Completor/CoreNameSearcherCompletor.php rename tests/Integration/Bridge/TolerantParser/{ResolveAliasSuggestionCompletorTest.php => ImportedNameSearcherCompletorTest.php} (81%) create mode 100644 tests/Integration/Core/Completor/CoreNameSearcherCompletorTest.php diff --git a/lib/Bridge/TolerantParser/ResolveAliasSuggestionCompletor.php b/lib/Bridge/TolerantParser/ImportedNameSearcherCompletor.php similarity index 86% rename from lib/Bridge/TolerantParser/ResolveAliasSuggestionCompletor.php rename to lib/Bridge/TolerantParser/ImportedNameSearcherCompletor.php index 191615bc..5b353daa 100644 --- a/lib/Bridge/TolerantParser/ResolveAliasSuggestionCompletor.php +++ b/lib/Bridge/TolerantParser/ImportedNameSearcherCompletor.php @@ -7,16 +7,19 @@ use Microsoft\PhpParser\Node\SourceFileNode; use Microsoft\PhpParser\Parser; use Microsoft\PhpParser\ResolvedName; -use Phpactor\Completion\Core\Completor; +use Phpactor\Completion\Core\Completor\NameSearcherCompletor; use Phpactor\Completion\Core\Suggestion; use Phpactor\Completion\Core\Util\OffsetHelper; use Phpactor\TextDocument\ByteOffset; use Phpactor\TextDocument\TextDocument; -final class ResolveAliasSuggestionCompletor implements Completor +/** + * Replace the suggestions from the decorated decorator by new ones based on the impor table. + */ +final class ImportedNameSearcherCompletor implements NameSearcherCompletor { /** - * @var Completor + * @var NameSearcherCompletor */ private $decorated; @@ -25,7 +28,7 @@ final class ResolveAliasSuggestionCompletor implements Completor */ private $parser; - public function __construct(Completor $decorated, Parser $parser = null) + public function __construct(NameSearcherCompletor $decorated, Parser $parser = null) { $this->decorated = $decorated; $this->parser = $parser ?: new Parser(); @@ -34,10 +37,10 @@ public function __construct(Completor $decorated, Parser $parser = null) /** * {@inheritDoc} */ - public function complete(TextDocument $source, ByteOffset $byteOffset): Generator + public function complete(TextDocument $source, ByteOffset $byteOffset, string $name = null): Generator { $importTable = $this->getClassImportTableAtPosition($source, $byteOffset); - $suggestions = $this->decorated->complete($source, $byteOffset); + $suggestions = $this->decorated->complete($source, $byteOffset, $name); foreach ($suggestions as $suggestion) { $resolvedSuggestions = $this->resolveAliasSuggestions($importTable, $suggestion); @@ -89,12 +92,18 @@ private function resolveAliasSuggestions(array $importTable, Suggestion $suggest $suggestionFqcn = $suggestion->nameImport(); $originalName = $suggestion->name(); $originalSnippet = $suggestion->snippet(); - $suggestions = [$suggestion->name() => $suggestion]; + $suggestions = []; foreach ($importTable as $alias => $resolvedName) { $importFqcn = $resolvedName->getFullyQualifiedNameText(); + if ($suggestionFqcn === $importFqcn && $originalName === $alias) { + // Ignore the original suggestion, another completor already retruns it + continue; + } + if (0 !== strpos($suggestionFqcn, $importFqcn)) { + // Ignore imported name that are not part of the one from suggestion continue; } diff --git a/lib/Bridge/TolerantParser/ReferenceFinder/NameSearcherCompletor.php b/lib/Bridge/TolerantParser/ReferenceFinder/NameSearcherCompletor.php index 14230391..d665180e 100644 --- a/lib/Bridge/TolerantParser/ReferenceFinder/NameSearcherCompletor.php +++ b/lib/Bridge/TolerantParser/ReferenceFinder/NameSearcherCompletor.php @@ -9,14 +9,24 @@ use Phpactor\TextDocument\ByteOffset; use Phpactor\TextDocument\TextDocument; -class NameSearcherCompletor extends CoreNameSearcherCompletor implements TolerantCompletor +final class NameSearcherCompletor implements TolerantCompletor { + /** + * @var CoreNameSearcherCompletor + */ + private $nameCompletor; + + public function __construct(CoreNameSearcherCompletor $nameCompletor) + { + $this->nameCompletor = $nameCompletor; + } + /** * {@inheritDoc} */ public function complete(Node $node, TextDocument $source, ByteOffset $offset): Generator { - $suggestions = $this->completeName($node->getText()); + $suggestions = $this->nameCompletor->complete($source, $offset, $node->getText()); yield from $suggestions; diff --git a/lib/Bridge/TolerantParser/WorseReflection/DoctrineAnnotationCompletor.php b/lib/Bridge/TolerantParser/WorseReflection/DoctrineAnnotationCompletor.php index 15c8e81d..714994b4 100644 --- a/lib/Bridge/TolerantParser/WorseReflection/DoctrineAnnotationCompletor.php +++ b/lib/Bridge/TolerantParser/WorseReflection/DoctrineAnnotationCompletor.php @@ -10,15 +10,18 @@ use Phpactor\Completion\Core\Completor\NameSearcherCompletor; use Phpactor\Completion\Core\Suggestion; use Phpactor\Completion\Core\Util\OffsetHelper; -use Phpactor\ReferenceFinder\NameSearcher; -use Phpactor\ReferenceFinder\Search\NameSearchResult; use Phpactor\TextDocument\ByteOffset; use Phpactor\TextDocument\TextDocument; use Phpactor\WorseReflection\Core\Exception\NotFound; use Phpactor\WorseReflection\Reflector; -class DoctrineAnnotationCompletor extends NameSearcherCompletor implements Completor +final class DoctrineAnnotationCompletor implements Completor { + /** + * @var NameSearcherCompletor + */ + private $nameCompletor; + /** * @var Reflector */ @@ -30,12 +33,11 @@ class DoctrineAnnotationCompletor extends NameSearcherCompletor implements Compl private $parser; public function __construct( - NameSearcher $nameSearcher, + NameSearcherCompletor $nameCompletor, Reflector $reflector, Parser $parser = null ) { - parent::__construct($nameSearcher); - + $this->nameCompletor = $nameCompletor; $this->reflector = $reflector; $this->parser = $parser ?: new Parser(); } @@ -48,7 +50,7 @@ public function complete(TextDocument $source, ByteOffset $byteOffset): Generato $node = $this->findNodeForPhpdocAtPosition( $sourceNodeFile, // the parser requires the byte offset, not the char offset - strlen($truncatedSource) + strlen($truncatedSource), ); if (!$node) { @@ -61,26 +63,19 @@ public function complete(TextDocument $source, ByteOffset $byteOffset): Generato return true; } - $suggestions = $this->completeName($annotation); + $suggestions = $this->nameCompletor->complete($source, $byteOffset, $annotation); foreach ($suggestions as $suggestion) { if (!$this->isAnAnnotation($suggestion)) { continue; } - yield $suggestion; + yield $suggestion->withSnippet($suggestion->name().'($1)$0'); } return $suggestions->getReturn(); } - protected function createSuggestionOptions(NameSearchResult $result): array - { - return array_merge(parent::createSuggestionOptions($result), [ - 'snippet' => (string) $result->name()->head() .'($1)$0', - ]); - } - private function truncateSource(string $source, int $byteOffset): string { // truncate source at byte offset - we don't want the rest of the source @@ -117,7 +112,7 @@ private function findNodeForPhpdocAtPosition(SourceFileNode $sourceNodeFile, int private function isAnAnnotation(Suggestion $suggestion): bool { try { - $reflectionClass = $this->reflector->reflectClass($suggestion->classImport()); + $reflectionClass = $this->reflector->reflectClass($suggestion->shortDescription()); $docblock = $reflectionClass->docblock(); return false !== strpos($docblock->raw(), '@Annotation'); diff --git a/lib/Core/Completor/CoreNameSearcherCompletor.php b/lib/Core/Completor/CoreNameSearcherCompletor.php new file mode 100644 index 00000000..4c8d1348 --- /dev/null +++ b/lib/Core/Completor/CoreNameSearcherCompletor.php @@ -0,0 +1,61 @@ +nameSearcher = $nameSearcher; + } + + /** + * {@inheritDoc} + */ + public function complete( + TextDocument $source, + ByteOffset $byteOffset, + string $name = null + ): Generator { + if (!$name) { + return true; + } + + foreach ($this->nameSearcher->search($name) as $result) { + $fqcn = $result->name(); + + yield Suggestion::createWithOptions($fqcn->head(), [ + 'type' => $this->suggestionType($result), + 'short_description' => (string) $fqcn, + 'name_import' => (string) $fqcn, + ]); + } + + return true; + } + + protected function suggestionType(NameSearchResult $result): ?string + { + if ($result->type()->isClass()) { + return Suggestion::TYPE_CLASS; + } + + if ($result->type()->isFunction()) { + return Suggestion::TYPE_FUNCTION; + } + + return null; + } +} diff --git a/lib/Core/Completor/NameSearcherCompletor.php b/lib/Core/Completor/NameSearcherCompletor.php index 0c73145e..29dbb47b 100644 --- a/lib/Core/Completor/NameSearcherCompletor.php +++ b/lib/Core/Completor/NameSearcherCompletor.php @@ -3,73 +3,18 @@ namespace Phpactor\Completion\Core\Completor; use Generator; -use Phpactor\Completion\Core\Suggestion; -use Phpactor\ReferenceFinder\NameSearcher; -use Phpactor\ReferenceFinder\Search\NameSearchResult; +use Phpactor\Completion\Core\Completor; +use Phpactor\TextDocument\ByteOffset; +use Phpactor\TextDocument\TextDocument; -abstract class NameSearcherCompletor +interface NameSearcherCompletor extends Completor { /** - * @var NameSearcher + * {@inheritDoc} */ - protected $nameSearcher; - - public function __construct(NameSearcher $nameSearcher) - { - $this->nameSearcher = $nameSearcher; - } - - /** - * @return Generator - */ - protected function completeName(string $name): Generator - { - foreach ($this->nameSearcher->search($name) as $result) { - yield $this->createSuggestion( - $result, - $this->createSuggestionOptions($result), - ); - } - - return true; - } - - protected function createSuggestion(NameSearchResult $result, array $options = []): Suggestion - { - $options = array_merge($this->createSuggestionOptions($result), $options); - - return Suggestion::createWithOptions($result->name()->head(), $options); - } - - protected function createSuggestionOptions(NameSearchResult $result): array - { - return [ - 'short_description' => $result->name()->__toString(), - 'type' => $this->suggestionType($result), - 'class_import' => $this->classImport($result), - 'name_import' => $result->name()->__toString(), - ]; - } - - protected function suggestionType(NameSearchResult $result): ?string - { - if ($result->type()->isClass()) { - return Suggestion::TYPE_CLASS; - } - - if ($result->type()->isFunction()) { - return Suggestion::TYPE_FUNCTION; - } - - return null; - } - - protected function classImport(NameSearchResult $result): ?string - { - if ($result->type()->isClass()) { - return $result->name()->__toString(); - } - - return null; - } + public function complete( + TextDocument $source, + ByteOffset $byteOffset, + string $name = null + ): Generator; } diff --git a/tests/Integration/Bridge/TolerantParser/DoctrineAnnotationCompletorTest.php b/tests/Integration/Bridge/TolerantParser/DoctrineAnnotationCompletorTest.php index c06e0009..58040a8d 100644 --- a/tests/Integration/Bridge/TolerantParser/DoctrineAnnotationCompletorTest.php +++ b/tests/Integration/Bridge/TolerantParser/DoctrineAnnotationCompletorTest.php @@ -5,10 +5,11 @@ use Generator; use Phpactor\Completion\Bridge\TolerantParser\WorseReflection\DoctrineAnnotationCompletor; use Phpactor\Completion\Core\Completor; +use Phpactor\Completion\Core\Completor\NameSearcherCompletor; use Phpactor\Completion\Core\Suggestion; use Phpactor\Completion\Tests\Integration\CompletorTestCase; -use Phpactor\ReferenceFinder\NameSearcher; -use Phpactor\ReferenceFinder\Search\NameSearchResult; +use Phpactor\TextDocument\ByteOffset; +use Phpactor\TextDocument\TextDocument; use Phpactor\TextDocument\TextDocumentBuilder; use Phpactor\WorseReflection\Bridge\Phpactor\MemberProvider\DocblockMemberProvider; use Phpactor\WorseReflection\ReflectorBuilder; @@ -20,24 +21,42 @@ protected function createCompletor(string $source): Completor { $source = TextDocumentBuilder::create($source)->uri('file:///tmp/test')->build(); - $searcher = $this->prophesize(NameSearcher::class); - $searcher->search(Argument::any())->willYield([]); - $searcher->search('Ann')->willYield([ - NameSearchResult::create('class', 'Annotation') - ]); - $searcher->search('Ent')->willYield([ - NameSearchResult::create('class', 'App\Annotation\Entity') - ]); - $searcher->search('NotAnn')->willYield([ - NameSearchResult::create('class', 'NotAnnotation') - ]); + $nameSearcherCompletor = $this->prophesize(NameSearcherCompletor::class); + $nameSearcherCompletor->complete(Argument::any())->willYield([]); + $nameSearcherCompletor->complete(Argument::type(TextDocument::class), Argument::type(ByteOffset::class), 'Ann')->will(function () { + yield Suggestion::createWithOptions('Annotation', [ + 'type' => Suggestion::TYPE_CLASS, + 'short_description' => 'Annotation', + 'name_import' => 'Annotation', + ]); + + return true; + }); + $nameSearcherCompletor->complete(Argument::type(TextDocument::class), Argument::type(ByteOffset::class), 'Ent')->will(function () { + yield Suggestion::createWithOptions('Entity', [ + 'type' => Suggestion::TYPE_CLASS, + 'short_description' => 'App\Annotation\Entity', + 'name_import' => 'App\Annotation\Entity', + ]); + + return true; + }); + $nameSearcherCompletor->complete(Argument::type(TextDocument::class), Argument::type(ByteOffset::class), 'NotAnn')->will(function () { + yield Suggestion::createWithOptions('NotAnnotation', [ + 'type' => Suggestion::TYPE_CLASS, + 'short_description' => 'NotAnnotation', + 'name_import' => 'NotAnnotation', + ]); + + return true; + }); $reflector = ReflectorBuilder::create() ->addMemberProvider(new DocblockMemberProvider()) ->addSource($source)->build(); return new DoctrineAnnotationCompletor( - $searcher->reveal(), + $nameSearcherCompletor->reveal(), $reflector, ); } diff --git a/tests/Integration/Bridge/TolerantParser/ResolveAliasSuggestionCompletorTest.php b/tests/Integration/Bridge/TolerantParser/ImportedNameSearcherCompletorTest.php similarity index 81% rename from tests/Integration/Bridge/TolerantParser/ResolveAliasSuggestionCompletorTest.php rename to tests/Integration/Bridge/TolerantParser/ImportedNameSearcherCompletorTest.php index 701caaf9..780ebc9c 100644 --- a/tests/Integration/Bridge/TolerantParser/ResolveAliasSuggestionCompletorTest.php +++ b/tests/Integration/Bridge/TolerantParser/ImportedNameSearcherCompletorTest.php @@ -2,15 +2,15 @@ namespace Phpactor\Completion\Tests\Integration\Bridge\TolerantParser; -use Phpactor\Completion\Bridge\TolerantParser\ResolveAliasSuggestionCompletor; -use Phpactor\Completion\Core\Completor; +use Phpactor\Completion\Bridge\TolerantParser\ImportedNameSearcherCompletor; +use Phpactor\Completion\Core\Completor\NameSearcherCompletor; use Phpactor\Completion\Core\Suggestion; use Phpactor\Completion\Tests\TestCase; use Phpactor\TextDocument\ByteOffset; use Phpactor\TextDocument\TextDocument; use Phpactor\TextDocument\TextDocumentBuilder; -class ResolveAliasSuggestionCompletorTest extends TestCase +class ImportedNameSearcherCompletorTest extends TestCase { /** * @dataProvider provideSuggestionsToResolve @@ -24,10 +24,12 @@ public function testResolveClassAlias( Suggestion $completorSuggestion, array $expectedSuggestions ): void { - $decoratedCompletor = $this->prophesize(Completor::class); - $completor = new ResolveAliasSuggestionCompletor($decoratedCompletor->reveal()); + $nameSearcherCompletor = $this->prophesize(NameSearcherCompletor::class); + $completor = new ImportedNameSearcherCompletor( + $nameSearcherCompletor->reveal(), + ); - $decoratedCompletor->complete($textDocument, $byteOffset) + $nameSearcherCompletor->complete($textDocument, $byteOffset, 'something') ->will(function () use ($completorSuggestion) { yield $completorSuggestion; @@ -35,7 +37,7 @@ public function testResolveClassAlias( }) ; - $generator = $completor->complete($textDocument, $byteOffset); + $generator = $completor->complete($textDocument, $byteOffset, 'something'); $suggestions = iterator_to_array($generator, false); $this->assertCount(count($expectedSuggestions), $suggestions); @@ -87,24 +89,21 @@ public function methodName(Bar \$bar) $textDocument = $textDocumentFactory(), $computeByteOffset($textDocument), $suggestion, - [$suggestion], + [], ]; yield 'Class imported without an alias' => [ $textDocument = $textDocumentFactory(['App\Foo\Bar']), $computeByteOffset($textDocument), $suggestion, - [$suggestion->withoutNameImport()], + [], ]; yield 'Class imported with an alias' => [ $textDocument = $textDocumentFactory(['Foobar' => 'App\Foo\Bar']), $computeByteOffset($textDocument), $suggestion, - [ - $suggestion, - $suggestion->withoutNameImport()->withName('Foobar'), - ], + [$suggestion->withoutNameImport()->withName('Foobar')], ]; yield 'Class imported with and without an alias' => [ @@ -114,10 +113,7 @@ public function methodName(Bar \$bar) ]), $computeByteOffset($textDocument), $suggestion, - [ - $suggestion->withoutNameImport(), - $suggestion->withoutNameImport()->withName('Foobar'), - ], + [$suggestion->withoutNameImport()->withName('Foobar')], ]; yield 'Class imported with an aliased namespace' => [ @@ -128,7 +124,6 @@ public function methodName(Bar \$bar) $computeByteOffset($textDocument), $suggestion, [ - $suggestion, $suggestion->withoutNameImport()->withName('FOO\Bar'), $suggestion->withoutNameImport()->withName('APP\Foo\Bar'), ], @@ -141,10 +136,7 @@ public function methodName(Bar \$bar) ]), $computeByteOffset($textDocument), $suggestion, - [ - $suggestion->withoutNameImport(), - $suggestion->withoutNameImport()->withName('FOO\Bar'), - ], + [$suggestion->withoutNameImport()->withName('FOO\Bar')], ]; } @@ -194,24 +186,21 @@ class BarTest $textDocument = $textDocumentFactory(), $computeByteOffset($textDocument), $suggestion, - [$suggestion], + [], ]; yield 'Annotation imported without an alias' => [ $textDocument = $textDocumentFactory(['App\Foo\Bar']), $computeByteOffset($textDocument), $suggestion, - [$suggestion->withoutNameImport()], + [], ]; yield 'Annotation imported with an alias' => [ $textDocument = $textDocumentFactory(['Foobar' => 'App\Foo\Bar']), $computeByteOffset($textDocument), $suggestion, - [ - $suggestion, - $suggestion->withoutNameImport()->withName('Foobar')->withSnippet('Foobar($1)$0'), - ], + [$suggestion->withoutNameImport()->withName('Foobar')->withSnippet('Foobar($1)$0')], ]; yield 'Annotation imported with and without an alias' => [ @@ -221,10 +210,7 @@ class BarTest ]), $computeByteOffset($textDocument), $suggestion, - [ - $suggestion->withoutNameImport(), - $suggestion->withoutNameImport()->withName('Foobar')->withSnippet('Foobar($1)$0'), - ], + [$suggestion->withoutNameImport()->withName('Foobar')->withSnippet('Foobar($1)$0')], ]; yield 'Annotation imported with an aliased namespace' => [ @@ -235,7 +221,6 @@ class BarTest $computeByteOffset($textDocument), $suggestion, [ - $suggestion, $suggestion->withoutNameImport()->withName('FOO\Bar')->withSnippet('FOO\Bar($1)$0'), $suggestion->withoutNameImport()->withName('APP\Foo\Bar')->withSnippet('APP\Foo\Bar($1)$0'), ], @@ -248,10 +233,7 @@ class BarTest ]), $computeByteOffset($textDocument), $suggestion, - [ - $suggestion->withoutNameImport(), - $suggestion->withoutNameImport()->withName('FOO\Bar')->withSnippet('FOO\Bar($1)$0'), - ], + [$suggestion->withoutNameImport()->withName('FOO\Bar')->withSnippet('FOO\Bar($1)$0')], ]; } } diff --git a/tests/Integration/Bridge/TolerantParser/ReferenceFinder/NameSearcherCompletorTest.php b/tests/Integration/Bridge/TolerantParser/ReferenceFinder/NameSearcherCompletorTest.php index edcdeb54..bf5898c1 100644 --- a/tests/Integration/Bridge/TolerantParser/ReferenceFinder/NameSearcherCompletorTest.php +++ b/tests/Integration/Bridge/TolerantParser/ReferenceFinder/NameSearcherCompletorTest.php @@ -5,10 +5,9 @@ use Generator; use Phpactor\Completion\Bridge\TolerantParser\ReferenceFinder\NameSearcherCompletor; use Phpactor\Completion\Bridge\TolerantParser\TolerantCompletor; +use Phpactor\Completion\Core\Completor\NameSearcherCompletor as PhpactorNameSearcherCompletor; use Phpactor\Completion\Core\Suggestion; use Phpactor\Completion\Tests\Integration\Bridge\TolerantParser\TolerantCompletorTestCase; -use Phpactor\ReferenceFinder\NameSearcher; -use Phpactor\ReferenceFinder\Search\NameSearchResult; use Phpactor\TextDocument\TextDocument; use Prophecy\Argument; @@ -16,11 +15,20 @@ class NameSearcherCompletorTest extends TolerantCompletorTestCase { protected function createTolerantCompletor(TextDocument $source): TolerantCompletor { - $searcher = $this->prophesize(NameSearcher::class); - $searcher->search(Argument::any())->willYield([ - NameSearchResult::create('class', 'Foobar') - ]); - return new NameSearcherCompletor($searcher->reveal()); + $nameSearcherCompletor = $this->prophesize(PhpactorNameSearcherCompletor::class); + $nameSearcherCompletor->complete(Argument::cetera())->will(function () { + yield Suggestion::createWithOptions('Foobar', [ + 'type' => Suggestion::TYPE_CLASS, + 'short_description' => 'Foobar', + 'name_import' => 'Foobar', + ]); + + return true; + }); + + return new NameSearcherCompletor( + $nameSearcherCompletor->reveal(), + ); } /** diff --git a/tests/Integration/Core/Completor/CoreNameSearcherCompletorTest.php b/tests/Integration/Core/Completor/CoreNameSearcherCompletorTest.php new file mode 100644 index 00000000..56939224 --- /dev/null +++ b/tests/Integration/Core/Completor/CoreNameSearcherCompletorTest.php @@ -0,0 +1,36 @@ +prophesize(NameSearcher::class); + $searcher->search(Argument::any())->willYield([ + NameSearchResult::create('class', 'App\Foo\Bar') + ]); + $completor = new CoreNameSearcherCompletor($searcher->reveal()); + $expectedSuggestion = Suggestion::createWithOptions('Bar', [ + 'type' => Suggestion::TYPE_CLASS, + 'short_description' => 'App\Foo\Bar', + 'name_import' => 'App\Foo\Bar' + ]); + $textDocument = TextDocumentBuilder::create('')->build(); + + $generator = $completor->complete($textDocument, ByteOffset::fromInt(0), 'Foo'); + $suggestions = iterator_to_array($generator, false); + + $this->assertCount(1, $suggestions); + $this->assertEquals($expectedSuggestion, $suggestions[0]); + } +}