diff --git a/lib/Bridge/TolerantParser/ChainTolerantCompletor.php b/lib/Bridge/TolerantParser/ChainTolerantCompletor.php index 21224bb9..59fbcc09 100644 --- a/lib/Bridge/TolerantParser/ChainTolerantCompletor.php +++ b/lib/Bridge/TolerantParser/ChainTolerantCompletor.php @@ -36,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) ); @@ -70,10 +71,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); diff --git a/lib/Bridge/TolerantParser/ImportedNameSearcherCompletor.php b/lib/Bridge/TolerantParser/ImportedNameSearcherCompletor.php new file mode 100644 index 00000000..5b353daa --- /dev/null +++ b/lib/Bridge/TolerantParser/ImportedNameSearcherCompletor.php @@ -0,0 +1,159 @@ +decorated = $decorated; + $this->parser = $parser ?: new Parser(); + } + + /** + * {@inheritDoc} + */ + public function complete(TextDocument $source, ByteOffset $byteOffset, string $name = null): Generator + { + $importTable = $this->getClassImportTableAtPosition($source, $byteOffset); + $suggestions = $this->decorated->complete($source, $byteOffset, $name); + + 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 = []; + + 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; + } + + $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/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/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/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/lib/Core/Suggestion.php b/lib/Core/Suggestion.php index 1e15fb42..215ee9d9 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,30 @@ 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 withSnippet(string $snippet): self + { + $suggestion = clone $this; + $suggestion->snippet = $snippet; + + return $suggestion; + } + public function toArray(): array { return [ @@ -185,10 +209,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/Integration/Bridge/TolerantParser/DoctrineAnnotationCompletorTest.php b/tests/Integration/Bridge/TolerantParser/DoctrineAnnotationCompletorTest.php index 451145ba..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, ); } @@ -104,7 +123,8 @@ 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', ] ]]; diff --git a/tests/Integration/Bridge/TolerantParser/ImportedNameSearcherCompletorTest.php b/tests/Integration/Bridge/TolerantParser/ImportedNameSearcherCompletorTest.php new file mode 100644 index 00000000..780ebc9c --- /dev/null +++ b/tests/Integration/Bridge/TolerantParser/ImportedNameSearcherCompletorTest.php @@ -0,0 +1,239 @@ +prophesize(NameSearcherCompletor::class); + $completor = new ImportedNameSearcherCompletor( + $nameSearcherCompletor->reveal(), + ); + + $nameSearcherCompletor->complete($textDocument, $byteOffset, 'something') + ->will(function () use ($completorSuggestion) { + yield $completorSuggestion; + + return true; + }) + ; + + $generator = $completor->complete($textDocument, $byteOffset, 'something'); + $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, + [], + ]; + + yield 'Class imported without an alias' => [ + $textDocument = $textDocumentFactory(['App\Foo\Bar']), + $computeByteOffset($textDocument), + $suggestion, + [], + ]; + + yield 'Class imported with an alias' => [ + $textDocument = $textDocumentFactory(['Foobar' => 'App\Foo\Bar']), + $computeByteOffset($textDocument), + $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()->withName('Foobar')], + ]; + + yield 'Class imported with an aliased namespace' => [ + $textDocument = $textDocumentFactory([ + 'FOO' => 'App\Foo', + 'APP' => 'App', + ]), + $computeByteOffset($textDocument), + $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()->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, + [], + ]; + + yield 'Annotation imported without an alias' => [ + $textDocument = $textDocumentFactory(['App\Foo\Bar']), + $computeByteOffset($textDocument), + $suggestion, + [], + ]; + + yield 'Annotation imported with an alias' => [ + $textDocument = $textDocumentFactory(['Foobar' => 'App\Foo\Bar']), + $computeByteOffset($textDocument), + $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()->withName('Foobar')->withSnippet('Foobar($1)$0')], + ]; + + yield 'Annotation imported with an aliased namespace' => [ + $textDocument = $textDocumentFactory([ + 'FOO' => 'App\Foo', + 'APP' => 'App', + ]), + $computeByteOffset($textDocument), + $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()->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]); + } +} diff --git a/tests/Unit/Core/SuggestionTest.php b/tests/Unit/Core/SuggestionTest.php index af5b6309..f809f85e 100644 --- a/tests/Unit/Core/SuggestionTest.php +++ b/tests/Unit/Core/SuggestionTest.php @@ -66,4 +66,36 @@ 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()); + } + + public function testWithSnippet() + { + $suggestion = Suggestion::createWithOptions( + 'name', + ['snippet' => 'snippet'], + )->withSnippet('test'); + $this->assertEquals('test', $suggestion->snippet()); + } }