From 3dfcb98dc267acd0abc9323f38033eb26e927bc5 Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Wed, 7 Oct 2026 11:45:39 -0700 Subject: [PATCH 1/9] Unit-test ReferenceKind priority Co-Authored-By: Claude Opus 5.5 --- tests/Resolution/ReferenceKindTest.php | 43 ++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 tests/Resolution/ReferenceKindTest.php 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', + ); + } +} From 0d1c449de0ace90f56cffc12dc1e4a8e367e9c41 Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Wed, 7 Oct 2026 11:46:57 -0700 Subject: [PATCH 2/9] Add the ReportsIncomplete completion expectation Co-Authored-By: Claude Opus 5.5 --- .../Expectation/ReportsIncomplete.php | 20 +++++++++++++++++++ tests/EndToEnd/README.md | 1 + 2 files changed, 21 insertions(+) create mode 100644 tests/EndToEnd/Expectation/ReportsIncomplete.php 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. From 18eb9f18c0fa76d0e0cc912442df4e156c6bab6b Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Wed, 7 Oct 2026 11:46:57 -0700 Subject: [PATCH 3/9] Add response cap completion script Co-Authored-By: Claude Opus 5.5 --- .../scripts/completion-response-cap.php | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 tests/EndToEnd/scripts/completion-response-cap.php diff --git a/tests/EndToEnd/scripts/completion-response-cap.php b/tests/EndToEnd/scripts/completion-response-cap.php new file mode 100644 index 00000000..21e8e64b --- /dev/null +++ b/tests/EndToEnd/scripts/completion-response-cap.php @@ -0,0 +1,23 @@ + Date: Wed, 7 Oct 2026 11:49:25 -0700 Subject: [PATCH 4/9] Remove response cap integration tests Co-Authored-By: Claude Opus 5.5 --- tests/Integration/CompletionCapFloodTest.php | 124 ------------------- 1 file changed, 124 deletions(-) delete mode 100644 tests/Integration/CompletionCapFloodTest.php diff --git a/tests/Integration/CompletionCapFloodTest.php b/tests/Integration/CompletionCapFloodTest.php deleted file mode 100644 index addedcaf..00000000 --- a/tests/Integration/CompletionCapFloodTest.php +++ /dev/null @@ -1,124 +0,0 @@ -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), - )); - } -} From b8d851a963b69d90c11153c569c9c06e06e32851 Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Wed, 7 Oct 2026 11:49:26 -0700 Subject: [PATCH 5/9] Remove unused flood navigation fixture method Co-Authored-By: Claude Opus 5.5 --- tests/Fixtures/Namespacing/AbsoluteNavigation.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/Fixtures/Namespacing/AbsoluteNavigation.php b/tests/Fixtures/Namespacing/AbsoluteNavigation.php index 10283960..4aa0aea7 100644 --- a/tests/Fixtures/Namespacing/AbsoluteNavigation.php +++ b/tests/Fixtures/Namespacing/AbsoluteNavigation.php @@ -26,11 +26,6 @@ public function withParam(\Ps/*|param_nav*/): void public function withReturn(): \Ps/*|return_nav*/ { } - - public function flood(): void - { - new \Flood\/*|flood_nav*/ - } } class ExtendsAbsoluteNavigation extends \Ps/*|extends_nav*/ From 22cae3845de93c3f0e86560334e83f326f7dc924 Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Wed, 7 Oct 2026 11:49:42 -0700 Subject: [PATCH 6/9] Remove cap tests from CompletionHandlerIntegrationTest Co-Authored-By: Claude Opus 5.5 --- .../CompletionHandlerIntegrationTest.php | 103 ------------------ 1 file changed, 103 deletions(-) diff --git a/tests/Integration/CompletionHandlerIntegrationTest.php b/tests/Integration/CompletionHandlerIntegrationTest.php index eff6d161..0c9f7dd6 100644 --- a/tests/Integration/CompletionHandlerIntegrationTest.php +++ b/tests/Integration/CompletionHandlerIntegrationTest.php @@ -17,15 +17,6 @@ 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; @@ -94,17 +85,6 @@ protected function setUp(): void $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); @@ -116,89 +96,6 @@ private function makeHandler(SymbolSourceInterface $symbolSource): CompletionHan ); } - 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'); - } - /** * One parse per handled message. Completion is the fan-out * that made this matter — its sources each call a different CodeResolverInterface From 655e10179319d13c4d78b1ed65b1ec8a7a1a6bde Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Wed, 7 Oct 2026 11:59:08 -0700 Subject: [PATCH 7/9] Build the completion handler inline in its integration test Co-Authored-By: Claude Opus 5.5 --- .../CompletionHandlerIntegrationTest.php | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/tests/Integration/CompletionHandlerIntegrationTest.php b/tests/Integration/CompletionHandlerIntegrationTest.php index 0c9f7dd6..d40eab41 100644 --- a/tests/Integration/CompletionHandlerIntegrationTest.php +++ b/tests/Integration/CompletionHandlerIntegrationTest.php @@ -19,7 +19,6 @@ use Firehed\PhpLsp\Document\DocumentManager; 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; @@ -57,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; @@ -71,29 +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 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), ); + $this->syncHandler = new TextDocumentSyncHandler($this->documents, $knowledge->sink, $knowledge->invalidator); } /** From 3525b949580d4f33af6e6d756891b0f874941017 Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Wed, 7 Oct 2026 12:01:46 -0700 Subject: [PATCH 8/9] Add a response cap completion fixture Co-Authored-By: Claude Opus 5.5 --- tests/Fixtures/src/Completion/ResponseCap.php | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 tests/Fixtures/src/Completion/ResponseCap.php diff --git a/tests/Fixtures/src/Completion/ResponseCap.php b/tests/Fixtures/src/Completion/ResponseCap.php new file mode 100644 index 00000000..8d838e02 --- /dev/null +++ b/tests/Fixtures/src/Completion/ResponseCap.php @@ -0,0 +1,20 @@ + Date: Wed, 7 Oct 2026 12:01:46 -0700 Subject: [PATCH 9/9] Rank the response cap script on core functions only Co-Authored-By: Claude Opus 5.5 --- tests/EndToEnd/scripts/completion-response-cap.php | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/tests/EndToEnd/scripts/completion-response-cap.php b/tests/EndToEnd/scripts/completion-response-cap.php index 21e8e64b..bfd90cce 100644 --- a/tests/EndToEnd/scripts/completion-response-cap.php +++ b/tests/EndToEnd/scripts/completion-response-cap.php @@ -4,20 +4,15 @@ namespace Firehed\PhpLsp\Tests\EndToEnd; -$file = 'src/Completion/FunctionCompletion.php'; -$cursor = new Marker\CursorMarker('user_function'); +$file = 'src/Completion/ResponseCap.php'; -// A one-letter prefix matches more built-in functions and constants than the -// response holds. The file's own functions rank ahead of them, so they survive -// the cap even though built-in constant names sort first alphabetically. return new Session\Script( project: 'tests/Fixtures', steps: [ new Step\Open($file), - new Step\Type($file, $cursor, ' + c'), - new Step\Complete($file, $cursor, expect: [ + new Step\Complete($file, new Marker\CursorMarker('response_cap'), expect: [ new Expectation\ReportsIncomplete(), - new Expectation\Offers('calculateSum', 'calculateProduct'), + new Expectation\Offers('summarizeTotals'), ]), ], );