Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions tests/EndToEnd/Expectation/ReportsIncomplete.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace Firehed\PhpLsp\Tests\EndToEnd\Expectation;

use Firehed\PhpLsp\Tests\EndToEnd\Result\CompletionList;
use PHPUnit\Framework\Assert;

/**
* The list says more items match than it holds, so the client asks again as
* the prefix narrows.
*/
final readonly class ReportsIncomplete implements CompletionExpectationInterface
{
public function checkCompletion(CompletionList $completions): void
{
Assert::assertTrue($completions->isIncomplete, 'the list is reported incomplete');
}
}
1 change: 1 addition & 0 deletions tests/EndToEnd/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
18 changes: 18 additions & 0 deletions tests/EndToEnd/scripts/completion-response-cap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Firehed\PhpLsp\Tests\EndToEnd;

$file = 'src/Completion/ResponseCap.php';

return new Session\Script(
project: 'tests/Fixtures',
steps: [
new Step\Open($file),
new Step\Complete($file, new Marker\CursorMarker('response_cap'), expect: [
new Expectation\ReportsIncomplete(),
new Expectation\Offers('summarizeTotals'),
]),
],
);
5 changes: 0 additions & 5 deletions tests/Fixtures/Namespacing/AbsoluteNavigation.php
Original file line number Diff line number Diff line change
Expand Up @@ -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*/
Expand Down
20 changes: 20 additions & 0 deletions tests/Fixtures/src/Completion/ResponseCap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace Fixtures\Completion;

// More core PHP functions start with `s` than a completion response holds, and
// most sort before this one by name, so it reaches the response only by ranking
// as a current-namespace symbol.
function summarizeTotals(): void
{
}

class ResponseCap
{
public function trigger(): void
{
$x = s/*|response_cap*/
}
}
124 changes: 0 additions & 124 deletions tests/Integration/CompletionCapFloodTest.php

This file was deleted.

121 changes: 4 additions & 117 deletions tests/Integration/CompletionHandlerIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand All @@ -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 === ''
? "<?php\nclass {$short} {}\n"
: "<?php\nnamespace {$namespace};\nclass {$short} {}\n";
$uri = 'file:///seed/' . str_replace('\\', '_', $fqn) . '.php';
$this->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<string> $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', '<?php new FloodClass');

$request = RequestMessage::fromArray([
'jsonrpc' => '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);
}

/**
Expand Down
Loading
Loading