diff --git a/tests/EndToEnd/Expectation/ReportsIncomplete.php b/tests/EndToEnd/Expectation/ReportsIncomplete.php new file mode 100644 index 00000000..55bd5e6a --- /dev/null +++ b/tests/EndToEnd/Expectation/ReportsIncomplete.php @@ -0,0 +1,20 @@ +isIncomplete, 'the list is reported incomplete'); + } +} diff --git a/tests/EndToEnd/README.md b/tests/EndToEnd/README.md index c0b95c7d..10ccae12 100644 --- a/tests/EndToEnd/README.md +++ b/tests/EndToEnd/README.md @@ -49,6 +49,7 @@ A script file declares `namespace Firehed\PhpLsp\Tests\EndToEnd;` and names type - `Documents(label, text)` and `Details(label, text)`: one item has the label, with exactly that documentation or detail. - `Inserts(label, text, format)`: one item has the label, and accepting it inserts exactly that text, plain text unless a snippet format is given. - `RanksAbove(higher, lower)`: the client sorts one above the other. + - `ReportsIncomplete()`: the list says more items match than it holds. - `NoAnswer()`: nothing is offered. - Steps name files relative to the project, and places in them by marker. - `CursorMarker` is the position just before a `/*|name*/` marker. diff --git a/tests/EndToEnd/scripts/completion-response-cap.php b/tests/EndToEnd/scripts/completion-response-cap.php new file mode 100644 index 00000000..bfd90cce --- /dev/null +++ b/tests/EndToEnd/scripts/completion-response-cap.php @@ -0,0 +1,18 @@ +knowledgeStackForMap(new ComposerAutoloadMap(classMap: $classmap), $production); - - $memberResolver = new MemberResolver($knowledge->source); - $typeSource = new NativeTypeSource($knowledge->source, $memberResolver); - $resolver = new SymbolResolver( - $production->source, - $knowledge->source, - $memberResolver, - $typeSource, - ); - - $capabilities = self::createStub(SessionCapabilitiesProviderInterface::class); - $capabilities->method('getSessionCapabilities') - ->willReturn(new SessionCapabilities(snippetSupport: false)); - - $this->documents = new DocumentManager(); - $this->handler = new CompletionHandler( - $this->documents, - self::completionSourceFor($knowledge->source, $resolver, $capabilities), - ); - $this->syncHandler = new TextDocumentSyncHandler($this->documents, $knowledge->sink, $knowledge->invalidator); - } - - /** - * @return iterable - */ - public static function scopes(): iterable - { - yield 'global scope' => [" [" ["openDocument(self::PROBE_URI, $source); - $result = $this->handler->handle($this->completionRequestAt([ - 'uri' => self::PROBE_URI, - 'line' => $line, - 'character' => $character, - ])); - self::assertIsArray($result); - self::assertArrayHasKey('items', $result); - $labels = array_column($result['items'], 'label'); - - self::assertGreaterThanOrEqual( - 5, - count(self::stringFunctionsIn($labels)), - 'the str_* family must reach the response under the class-like flood', - ); - self::assertContains( - 'str_contains', - $labels, - 'str_contains is a long-stable member of the family; its absence signals a total collapse', - ); - } - - /** - * @param list $labels - * @return list - */ - private static function stringFunctionsIn(array $labels): array - { - return array_values(array_filter( - $labels, - static fn(string $label): bool => str_starts_with($label, 'str_') - && function_exists($label), - )); - } -} diff --git a/tests/Integration/CompletionHandlerIntegrationTest.php b/tests/Integration/CompletionHandlerIntegrationTest.php index eff6d161..d40eab41 100644 --- a/tests/Integration/CompletionHandlerIntegrationTest.php +++ b/tests/Integration/CompletionHandlerIntegrationTest.php @@ -17,18 +17,8 @@ use Firehed\PhpLsp\Completion\SymbolCandidates; use Firehed\PhpLsp\Completion\VariableCandidates; use Firehed\PhpLsp\Document\DocumentManager; -use Firehed\PhpLsp\Domain\ClassInfo; -use Firehed\PhpLsp\Domain\ClasslikeName; -use Firehed\PhpLsp\Domain\ConstantInfo; -use Firehed\PhpLsp\Domain\ConstantName; -use Firehed\PhpLsp\Domain\FunctionInfo; -use Firehed\PhpLsp\Domain\FunctionName; -use Firehed\PhpLsp\Domain\NameKind; -use Firehed\PhpLsp\Domain\NamespaceContents; -use Firehed\PhpLsp\Domain\NamespaceName; use Firehed\PhpLsp\Handler\CompletionHandler; use Firehed\PhpLsp\Handler\TextDocumentSyncHandler; -use Firehed\PhpLsp\Knowledge\SymbolSourceInterface; use Firehed\PhpLsp\Parser\SyntaxSource\MemoizingSyntaxSource; use Firehed\PhpLsp\Protocol\RequestMessage; use Firehed\PhpLsp\Repository\MemberResolver; @@ -66,8 +56,6 @@ class CompletionHandlerIntegrationTest extends TestCase private DocumentManager $documents; private MemoizingSyntaxSource $parser; private CountingSyntaxSource $counter; - private SymbolSourceInterface $symbolSource; - private SymbolResolver $symbolResolver; private CompletionHandler $handler; private TextDocumentSyncHandler $syncHandler; @@ -80,123 +68,22 @@ protected function setUp(): void $fixturesRoot = __DIR__ . '/../Fixtures'; $knowledge = $this->knowledgeStackForProjectRoot($fixturesRoot, $production); - $this->symbolSource = $knowledge->source; $memberResolver = new MemberResolver($knowledge->source); $typeSource = new NativeTypeSource($knowledge->source, $memberResolver); - $this->symbolResolver = new SymbolResolver( + $symbolResolver = new SymbolResolver( $this->parser, $knowledge->source, $memberResolver, $typeSource, ); - $this->handler = $this->makeHandler($this->symbolSource); - $this->syncHandler = new TextDocumentSyncHandler($this->documents, $knowledge->sink, $knowledge->invalidator); - } - - private function seedClass(string $fqn): void - { - $namespace = NamespaceName::namespaceOf($fqn); - $short = NamespaceName::shortNameOf($fqn); - $source = $namespace === '' - ? "openDocument($uri, $source); - } - - private function makeHandler(SymbolSourceInterface $symbolSource): CompletionHandler - { $capabilities = self::createStub(SessionCapabilitiesProviderInterface::class); $capabilities->method('getSessionCapabilities')->willReturn(new SessionCapabilities()); - - return new CompletionHandler( + $this->handler = new CompletionHandler( $this->documents, - self::completionSourceFor($symbolSource, $this->symbolResolver, $capabilities), + self::completionSourceFor($knowledge->source, $symbolResolver, $capabilities), ); - } - - public function testCapsResultsAndReportsIncompleteWhenNavigationOverflows(): void - { - // A namespace with far more children than the cap floods navigation. The - // response is capped and flagged incomplete; ranking runs before the cap, - // so it keeps the first-sorted nodes rather than raw source order. - $children = array_map( - static fn(int $i): string => sprintf('Flood\\N%03d', $i), - range(150, 1, -1), - ); - $source = new class ($children) implements SymbolSourceInterface { - /** @param list $children */ - public function __construct(private readonly array $children) - { - } - - public function childrenOf(NamespaceName $namespace): NamespaceContents - { - return $namespace->path === 'Flood' - ? new NamespaceContents($this->children, []) - : new NamespaceContents([], []); - } - - public function lookupClassLike(ClasslikeName $name): ?ClassInfo - { - return null; - } - - public function lookupConstant(ConstantName $name): ?ConstantInfo - { - return null; - } - - public function lookupFunction(FunctionName $name): ?FunctionInfo - { - return null; - } - - public function search(string $prefix, NameKind $kind): array - { - return []; - } - }; - $handler = $this->makeHandler($source); - $cursor = $this->openFixtureAtCursor('Namespacing/AbsoluteNavigation.php', 'flood_nav'); - - $result = $handler->handle($this->completionRequestAt($cursor)); - - self::assertIsArray($result); - self::assertTrue($result['isIncomplete'], 'An overflowing result set is reported incomplete'); - self::assertCount(100, $result['items'], 'The result set is capped at the limit'); - $labels = array_column($result['items'], 'label'); - self::assertContains('N001\\', $labels, 'Ranking runs before the cap, keeping the first-sorted node'); - self::assertNotContains('N150\\', $labels, 'A node sorted past the cap is dropped'); - } - - public function testCapAppliesToUnrankedCompletions(): void - { - // Flat class candidates (like members, variables, and functions) carry no - // sortText. When more than the cap match, the response is still truncated - // and flagged incomplete — the cap is a response-level limit, not one - // special to navigation — via the sentinel that sorts unranked items last. - foreach (range(0, 100) as $i) { - $this->seedClass(sprintf('FloodClass%03d', $i)); - } - $this->openDocument('file:///flood.php', ' '2.0', - 'id' => 1, - 'method' => 'textDocument/completion', - 'params' => [ - 'textDocument' => ['uri' => 'file:///flood.php'], - 'position' => ['line' => 0, 'character' => 20], - ], - ]); - - $result = $this->handler->handle($request); - - self::assertIsArray($result); - self::assertTrue($result['isIncomplete'], 'An overflowing unranked result set is reported incomplete'); - self::assertCount(100, $result['items'], 'Unranked completions are capped at the limit'); + $this->syncHandler = new TextDocumentSyncHandler($this->documents, $knowledge->sink, $knowledge->invalidator); } /** diff --git a/tests/Resolution/ReferenceKindTest.php b/tests/Resolution/ReferenceKindTest.php new file mode 100644 index 00000000..2aedff19 --- /dev/null +++ b/tests/Resolution/ReferenceKindTest.php @@ -0,0 +1,43 @@ + $a->priority() <=> $b->priority()); + + self::assertSame( + [ + ReferenceKind::CurrentNamespace, + ReferenceKind::Import, + ReferenceKind::PrefixImport, + ReferenceKind::GlobalFallback, + ReferenceKind::SubNamespace, + ReferenceKind::Unreachable, + ], + $kinds, + 'a bare name ranks ahead of a qualified one, and a global fallback ahead of a sub-namespace path', + ); + } + + public function testEachKindHasItsOwnPriority(): void + { + $priorities = array_map(static fn (ReferenceKind $kind): int => $kind->priority(), ReferenceKind::cases()); + + self::assertSame( + $priorities, + array_values(array_unique($priorities)), + 'no two kinds tie, so the order is total', + ); + } +}