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
5 changes: 5 additions & 0 deletions src/Knowledge/DelegatingSymbolSource.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ public function closeDocument(string $uri): void
$this->classes->removeDocument($uri);
}

public function isSubclassOf(ClassName $class, ClassName $potentialParent): bool
{
return $this->classes->isSubclassOf($class, $potentialParent);
}

public function lookupClassLike(ClassName $name): ?ClassInfo
{
return $this->classes->get($name);
Expand Down
11 changes: 11 additions & 0 deletions src/Knowledge/SymbolSource.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@ interface SymbolSource
*/
public function childrenOf(NamespaceName $namespace): NamespaceContents;

/**
* Whether $class is a subtype of $potentialParent somewhere along the type graph
* (extends, implements, and interface-extends-interface alike). Not reflexive: a
* class is not a subclass of itself, mirroring PHP's `is_subclass_of` — a caller
* that also wants the identity case tests it separately. A subtype relationship
* the caller cannot answer from a single lookup — it needs the whole graph — so it
* is a knowledge query in its own right (Plan 0002 §5.2: the surface a migrated
* feature actually needs).
*/
public function isSubclassOf(ClassName $class, ClassName $potentialParent): bool;

/**
* Full metadata for a class-like by its exact name, or null when nothing the
* source can reach declares it (RFC 1 §5.3: absence is a bare null).
Expand Down
26 changes: 13 additions & 13 deletions src/Resolution/SymbolResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
use Firehed\PhpLsp\Domain\MethodName;
use Firehed\PhpLsp\Domain\Visibility;
use Firehed\PhpLsp\Index\NodeAtPosition;
use Firehed\PhpLsp\Knowledge\SymbolSource;
use Firehed\PhpLsp\Parser\ParserService;
use Firehed\PhpLsp\Repository\ClassRepository;
use Firehed\PhpLsp\Repository\FunctionRepository;
use Firehed\PhpLsp\Repository\MemberResolver;
use Firehed\PhpLsp\TypeInference\TypeResolverInterface;
Expand Down Expand Up @@ -75,7 +75,7 @@ final class SymbolResolver implements CodeResolver

public function __construct(
private readonly ParserService $parser,
private readonly ClassRepository $classRepository,
private readonly SymbolSource $symbolSource,
private readonly MemberResolver $memberResolver,
private readonly TypeResolverInterface $typeResolver,
private readonly FunctionRepository $functionRepository,
Expand Down Expand Up @@ -194,7 +194,7 @@ private function getMembersForClass(
*/
public function isClassLike(ClassName $className): bool
{
return $this->classRepository->get($className) !== null;
return $this->symbolSource->lookupClassLike($className) !== null;
}

/**
Expand All @@ -203,7 +203,7 @@ public function isClassLike(ClassName $className): bool
*/
public function isInstantiable(ClassName $className): bool
{
$classInfo = $this->classRepository->get($className);
$classInfo = $this->symbolSource->lookupClassLike($className);
if ($classInfo === null) {
return true;
}
Expand All @@ -217,7 +217,7 @@ public function isInstantiable(ClassName $className): bool
*/
public function isValidTypeHint(ClassName $className): bool
{
$classInfo = $this->classRepository->get($className);
$classInfo = $this->symbolSource->lookupClassLike($className);
if ($classInfo === null) {
return true;
}
Expand All @@ -231,7 +231,7 @@ public function isValidTypeHint(ClassName $className): bool
*/
public function isInterface(ClassName $className): bool
{
$classInfo = $this->classRepository->get($className);
$classInfo = $this->symbolSource->lookupClassLike($className);
if ($classInfo === null) {
return false;
}
Expand All @@ -246,7 +246,7 @@ public function isInterface(ClassName $className): bool
*/
public function isExtendableClass(ClassName $className): bool
{
$classInfo = $this->classRepository->get($className);
$classInfo = $this->symbolSource->lookupClassLike($className);
if ($classInfo === null) {
return false;
}
Expand All @@ -261,7 +261,7 @@ public function isExtendableClass(ClassName $className): bool
*/
public function isThrowable(ClassName $className): bool
{
$classInfo = $this->classRepository->get($className);
$classInfo = $this->symbolSource->lookupClassLike($className);
if ($classInfo === null) {
return false;
}
Expand All @@ -270,7 +270,7 @@ public function isThrowable(ClassName $className): bool
if ($classInfo->name->equals($throwable)) {
return true;
}
return $this->classRepository->isSubclassOf($className, $throwable);
return $this->symbolSource->isSubclassOf($className, $throwable);
}

/**
Expand All @@ -280,7 +280,7 @@ public function isThrowable(ClassName $className): bool
*/
public function isAttribute(ClassName $className): bool
{
$classInfo = $this->classRepository->get($className);
$classInfo = $this->symbolSource->lookupClassLike($className);
if ($classInfo === null) {
return false;
}
Expand Down Expand Up @@ -522,7 +522,7 @@ private function getMinVisibilityForStaticAccess(
}

if (
$this->classRepository->isSubclassOf(
$this->symbolSource->isSubclassOf(
new ClassName($enclosingClassName),
new ClassName($targetClassName),
)
Expand Down Expand Up @@ -1421,7 +1421,7 @@ private function resolveName(Name $node, array $ast): ?ResolvedSymbol
// Class reference (new, instanceof, static call, type hint, etc.)
$classNameStr = ScopeFinder::resolveClassName($node);

$classInfo = $this->classRepository->get(new ClassName($classNameStr));
$classInfo = $this->symbolSource->lookupClassLike(new ClassName($classNameStr));
if ($classInfo === null) {
return null;
}
Expand Down Expand Up @@ -1499,7 +1499,7 @@ private function resolveParameter(Node\Param $param): ResolvedParameter
throw new LogicException('ClassMethod always has enclosing class');
}
// @codeCoverageIgnoreEnd
$classInfo = $this->classRepository->get(new ClassName($selfContext));
$classInfo = $this->symbolSource->lookupClassLike(new ClassName($selfContext));
$parentContext = $classInfo?->parent?->fqn;
}

Expand Down
17 changes: 9 additions & 8 deletions src/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,19 +97,12 @@ public static function forProject(
$functionRepository = new DefaultFunctionRepository();
$memberResolver = new MemberResolver($classRepository);
$typeResolver = new BasicTypeResolver($memberResolver, $functionRepository);
$symbolResolver = new SymbolResolver(
$parser,
$classRepository,
$memberResolver,
$typeResolver,
$functionRepository,
);

$catalog = NamespaceCatalogFactory::forProject($symbolIndex, $projectRoot);

// The read/write knowledge seam (RFC 1 §4.2, §4.3). Consumers migrate onto
// this one facade instance across Step 2's slices (Plan 0002 §5.5); today
// ClassCandidates reads class-like prefix search through it.
// ClassCandidates, NamespaceCandidates, and SymbolResolver read through it.
$symbolSource = new DelegatingSymbolSource(
$classRepository,
$symbolIndex,
Expand All @@ -119,6 +112,14 @@ public static function forProject(
$parser,
);

$symbolResolver = new SymbolResolver(
$parser,
$symbolSource,
$memberResolver,
$typeResolver,
$functionRepository,
);

$negotiator = new CapabilityNegotiator($serverInfo);
$lifecycleHandler = new LifecycleHandler($negotiator);

Expand Down
44 changes: 44 additions & 0 deletions tests/BuildsSymbolSourceTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace Firehed\PhpLsp\Tests;

use Firehed\PhpLsp\Index\DocumentIndexer;
use Firehed\PhpLsp\Index\NamespaceCatalog;
use Firehed\PhpLsp\Index\SymbolExtractor;
use Firehed\PhpLsp\Index\SymbolIndex;
use Firehed\PhpLsp\Knowledge\DelegatingSymbolSource;
use Firehed\PhpLsp\Parser\ParserService;
use Firehed\PhpLsp\Repository\ClassRepository;
use Firehed\PhpLsp\Repository\DefaultClassInfoFactory;

/**
* Builds the production {@see DelegatingSymbolSource} facade over a test's existing
* class repository, so a consumer that reads through the {@see \Firehed\PhpLsp\Knowledge\SymbolSource}
* seam (e.g. {@see \Firehed\PhpLsp\Resolution\SymbolResolver}) can be wired without
* every test class re-assembling the six collaborators by hand.
*
* The enumeration collaborators (index, indexer, catalog) are inert here: a consumer
* that only needs class-like lookup or the subtype query never reaches them, so a
* throwaway index and a stubbed catalog are sufficient. A test exercising namespace
* enumeration or the write path builds its own facade with real ones instead.
*/
trait BuildsSymbolSourceTrait
{
protected function symbolSourceFor(
ClassRepository $repository,
ParserService $parser,
): DelegatingSymbolSource {
$index = new SymbolIndex();

return new DelegatingSymbolSource(
$repository,
$index,
self::createStub(NamespaceCatalog::class),
new DocumentIndexer($parser, new SymbolExtractor(), $index),
new DefaultClassInfoFactory(),
$parser,
);
}
}
4 changes: 3 additions & 1 deletion tests/Handler/CompletionHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
use Firehed\PhpLsp\Repository\DefaultFunctionRepository;
use Firehed\PhpLsp\Repository\MemberResolver;
use Firehed\PhpLsp\Resolution\SymbolResolver;
use Firehed\PhpLsp\Tests\BuildsSymbolSourceTrait;
use Firehed\PhpLsp\TypeInference\BasicTypeResolver;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
Expand All @@ -58,6 +59,7 @@
#[CoversClass(VariableCandidates::class)]
class CompletionHandlerTest extends TestCase
{
use BuildsSymbolSourceTrait;
use OpensDocumentsTrait;

private DocumentManager $documents;
Expand Down Expand Up @@ -88,7 +90,7 @@ protected function setUp(): void
$typeResolver = new BasicTypeResolver($this->memberResolver, new DefaultFunctionRepository());
$this->symbolResolver = new SymbolResolver(
$this->parser,
$this->classRepository,
$this->symbolSourceFor($this->classRepository, $this->parser),
$this->memberResolver,
$typeResolver,
new DefaultFunctionRepository(),
Expand Down
4 changes: 3 additions & 1 deletion tests/Handler/DefinitionHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@
use Firehed\PhpLsp\Repository\DefaultFunctionRepository;
use Firehed\PhpLsp\Repository\MemberResolver;
use Firehed\PhpLsp\Resolution\SymbolResolver;
use Firehed\PhpLsp\Tests\BuildsSymbolSourceTrait;
use Firehed\PhpLsp\TypeInference\BasicTypeResolver;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;

#[CoversClass(DefinitionHandler::class)]
class DefinitionHandlerTest extends TestCase
{
use BuildsSymbolSourceTrait;
use OpensDocumentsTrait;

private DocumentManager $documents;
Expand All @@ -48,7 +50,7 @@ protected function setUp(): void
$typeResolver = new BasicTypeResolver($memberResolver, new DefaultFunctionRepository());
$symbolResolver = new SymbolResolver(
$this->parser,
$this->classRepository,
$this->symbolSourceFor($this->classRepository, $this->parser),
$memberResolver,
$typeResolver,
new DefaultFunctionRepository(),
Expand Down
4 changes: 3 additions & 1 deletion tests/Handler/HoverHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@
use Firehed\PhpLsp\Repository\DefaultFunctionRepository;
use Firehed\PhpLsp\Repository\MemberResolver;
use Firehed\PhpLsp\Resolution\SymbolResolver;
use Firehed\PhpLsp\Tests\BuildsSymbolSourceTrait;
use Firehed\PhpLsp\TypeInference\BasicTypeResolver;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;

#[CoversClass(HoverHandler::class)]
class HoverHandlerTest extends TestCase
{
use BuildsSymbolSourceTrait;
use OpensDocumentsTrait;

private DocumentManager $documents;
Expand All @@ -54,7 +56,7 @@ protected function setUp(): void
$typeResolver = new BasicTypeResolver($memberResolver, new DefaultFunctionRepository());
$this->symbolResolver = new SymbolResolver(
$this->parser,
$this->classRepository,
$this->symbolSourceFor($this->classRepository, $this->parser),
$memberResolver,
$typeResolver,
new DefaultFunctionRepository(),
Expand Down
4 changes: 3 additions & 1 deletion tests/Handler/SignatureHelpHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@
use Firehed\PhpLsp\Repository\DefaultFunctionRepository;
use Firehed\PhpLsp\Repository\MemberResolver;
use Firehed\PhpLsp\Resolution\SymbolResolver;
use Firehed\PhpLsp\Tests\BuildsSymbolSourceTrait;
use Firehed\PhpLsp\TypeInference\BasicTypeResolver;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;

#[CoversClass(SignatureHelpHandler::class)]
class SignatureHelpHandlerTest extends TestCase
{
use BuildsSymbolSourceTrait;
use OpensDocumentsTrait;

private DocumentManager $documents;
Expand All @@ -49,7 +51,7 @@ protected function setUp(): void
$typeResolver = new BasicTypeResolver($this->memberResolver, new DefaultFunctionRepository());
$symbolResolver = new SymbolResolver(
$this->parser,
$this->classRepository,
$this->symbolSourceFor($this->classRepository, $this->parser),
$this->memberResolver,
$typeResolver,
new DefaultFunctionRepository(),
Expand Down
20 changes: 20 additions & 0 deletions tests/Knowledge/DelegatingSymbolSourceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,26 @@ public function testSearchClassLikesFiltersByPrefix(): void
);
}

public function testIsSubclassOfForwardsToTheRepository(): void
{
// Pure forwarding, both branches: a real subclass relationship and an
// unrelated pair. The two directions also prove the arguments reach the
// repository in order rather than swapped (the transitive graph walk itself
// is frozen by ClassLikeLookupParityTest, not re-proven here).
$source = $this->facade($this->unusedCatalog());
$descendant = self::className('Fixtures\Inheritance\FinalDescendant');
$ancestor = self::className('Fixtures\Inheritance\ParentClass');

self::assertTrue(
$source->isSubclassOf($descendant, $ancestor),
'isSubclassOf must return the repository result for a real subclass',
);
self::assertFalse(
$source->isSubclassOf($ancestor, $descendant),
'isSubclassOf must forward its arguments in order, not swapped',
);
}

public function testChildrenOfForwardsTheNamespacePath(): void
{
$expected = new NamespaceContents(['Fixtures\Domain\Sub'], []);
Expand Down
4 changes: 3 additions & 1 deletion tests/Resolution/SymbolResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
use Firehed\PhpLsp\Resolution\SymbolResolver;
use Firehed\PhpLsp\Resolution\TextFallbackHelper;
use Firehed\PhpLsp\TypeInference\BasicTypeResolver;
use Firehed\PhpLsp\Tests\BuildsSymbolSourceTrait;
use Firehed\PhpLsp\Tests\Handler\OpensDocumentsTrait;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
Expand All @@ -49,6 +50,7 @@
#[CoversClass(TextFallbackHelper::class)]
final class SymbolResolverTest extends TestCase
{
use BuildsSymbolSourceTrait;
use OpensDocumentsTrait;

private SymbolResolver $resolver;
Expand All @@ -74,7 +76,7 @@ protected function setUp(): void

$this->resolver = new SymbolResolver(
parser: $this->parser,
classRepository: $this->classRepository,
symbolSource: $this->symbolSourceFor($this->classRepository, $this->parser),
memberResolver: $memberResolver,
typeResolver: $typeResolver,
functionRepository: new DefaultFunctionRepository(),
Expand Down