From 67f0014b608f2f646bff295aef3fcf602a646e8b Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Mon, 13 Jul 2026 17:30:04 +0200 Subject: [PATCH 1/2] Extract ComposerRootLocator to deduplicate vendor dir and composer.json handling Three copies of the "single registered vendor dir" autodetection (TestsUsageExcluder, SymfonyUsageProvider, ComposerUsageProvider) and two copies of the composer.json read+decode block are replaced by a single ComposerRootLocator service. Co-Authored-By: Claude Code --- rules.neon | 2 + src/Composer/ComposerRootLocator.php | 78 ++++++++++++++++++ src/Excluder/TestsUsageExcluder.php | 85 +++++++------------- src/Provider/ComposerUsageProvider.php | 48 ++--------- src/Provider/SymfonyUsageProvider.php | 16 ++-- tests/Excluder/TestsUsageExcluderTest.php | 3 +- tests/Provider/ComposerUsageProviderTest.php | 11 +-- tests/Provider/SymfonyUsageProviderTest.php | 9 ++- tests/Rule/DeadCodeRuleTest.php | 4 + 9 files changed, 139 insertions(+), 117 deletions(-) create mode 100644 src/Composer/ComposerRootLocator.php diff --git a/rules.neon b/rules.neon index cead736f..8faa0bef 100644 --- a/rules.neon +++ b/rules.neon @@ -13,6 +13,8 @@ services: arguments: tmpDir: %tmpDir% offloadCollectorData: %shipmonkDeadCode.cache.offloadCollectorData% + - + class: ShipMonk\PHPStan\DeadCode\Composer\ComposerRootLocator - class: ShipMonk\PHPStan\DeadCode\Hierarchy\ClassHierarchy - diff --git a/src/Composer/ComposerRootLocator.php b/src/Composer/ComposerRootLocator.php new file mode 100644 index 00000000..5fc620e3 --- /dev/null +++ b/src/Composer/ComposerRootLocator.php @@ -0,0 +1,78 @@ +autodetectVendorDir(); + + if ($vendorDir === null) { + return null; + } + + $composerJsonPath = $vendorDir . '/../composer.json'; + + if (!is_file($composerJsonPath)) { + return null; + } + + return $composerJsonPath; + } + + /** + * @return array + */ + public function parseComposerJson(string $composerJsonPath): array + { + if (!is_file($composerJsonPath)) { + return []; + } + + $composerJsonRawData = file_get_contents($composerJsonPath); + + if ($composerJsonRawData === false) { + return []; + } + + $composerJsonData = json_decode($composerJsonRawData, associative: true); + + if (json_last_error() !== JSON_ERROR_NONE || !is_array($composerJsonData)) { + return []; + } + + return $composerJsonData; // @phpstan-ignore return.type (composer.json keys are strings) + } + +} diff --git a/src/Excluder/TestsUsageExcluder.php b/src/Excluder/TestsUsageExcluder.php index 76113304..285a3b23 100644 --- a/src/Excluder/TestsUsageExcluder.php +++ b/src/Excluder/TestsUsageExcluder.php @@ -2,34 +2,28 @@ namespace ShipMonk\PHPStan\DeadCode\Excluder; -use Composer\Autoload\ClassLoader; use LogicException; use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; +use ShipMonk\PHPStan\DeadCode\Composer\ComposerRootLocator; use ShipMonk\PHPStan\DeadCode\Graph\ClassMemberUsage; -use function array_filter; -use function array_keys; -use function count; use function dirname; -use function file_get_contents; use function glob; use function is_array; -use function is_file; -use function json_decode; -use function json_last_error; +use function is_string; use function preg_match; use function realpath; -use function reset; use function str_contains; use function str_starts_with; -use const JSON_ERROR_NONE; final class TestsUsageExcluder implements MemberUsageExcluder { private readonly ReflectionProvider $reflectionProvider; + private readonly ComposerRootLocator $composerRootLocator; + private readonly bool $enabled; /** @@ -42,11 +36,13 @@ final class TestsUsageExcluder implements MemberUsageExcluder */ public function __construct( ReflectionProvider $reflectionProvider, + ComposerRootLocator $composerRootLocator, bool $enabled, ?array $devPaths, ) { $this->reflectionProvider = $reflectionProvider; + $this->composerRootLocator = $composerRootLocator; $this->enabled = $enabled; if ($devPaths !== null) { @@ -120,78 +116,53 @@ private function getDeclarationFile(?string $className): ?string */ private function autodetectComposerDevPaths(): array { - $vendorDirs = array_filter(array_keys(ClassLoader::getRegisteredLoaders()), static function (string $vendorDir): bool { - return !str_starts_with($vendorDir, 'phar://'); - }); - - if (count($vendorDirs) !== 1) { - return []; - } - - $vendorDir = reset($vendorDirs); - $composerJsonPath = $vendorDir . '/../composer.json'; - - $composerJsonData = $this->parseComposerJson($composerJsonPath); - $basePath = dirname($composerJsonPath); + $composerJsonPath = $this->composerRootLocator->autodetectComposerJsonPath(); - return [ - ...$this->extractAutoloadPaths($basePath, $composerJsonData['autoload-dev']['psr-0'] ?? []), - ...$this->extractAutoloadPaths($basePath, $composerJsonData['autoload-dev']['psr-4'] ?? []), - ...$this->extractAutoloadPaths($basePath, $composerJsonData['autoload-dev']['files'] ?? []), - ...$this->extractAutoloadPaths($basePath, $composerJsonData['autoload-dev']['classmap'] ?? []), - ]; - } - - /** - * @return array{ - * autoload-dev?: array{ - * psr-0?: array, - * psr-4?: array, - * files?: string[], - * classmap?: string[], - * } - * } - */ - private function parseComposerJson(string $composerJsonPath): array - { - if (!is_file($composerJsonPath)) { + if ($composerJsonPath === null) { return []; } - $composerJsonRawData = file_get_contents($composerJsonPath); + $composerJsonData = $this->composerRootLocator->parseComposerJson($composerJsonPath); + $autoloadDev = $composerJsonData['autoload-dev'] ?? []; - if ($composerJsonRawData === false) { + if (!is_array($autoloadDev)) { return []; } - $composerJsonData = json_decode($composerJsonRawData, associative: true); - - $jsonError = json_last_error(); - - if ($jsonError !== JSON_ERROR_NONE) { - return []; - } + $basePath = dirname($composerJsonPath); - return $composerJsonData; // @phpstan-ignore-line ignore mixed returned + return [ + ...$this->extractAutoloadPaths($basePath, $autoloadDev['psr-0'] ?? []), + ...$this->extractAutoloadPaths($basePath, $autoloadDev['psr-4'] ?? []), + ...$this->extractAutoloadPaths($basePath, $autoloadDev['files'] ?? []), + ...$this->extractAutoloadPaths($basePath, $autoloadDev['classmap'] ?? []), + ]; } /** - * @param array> $autoload * @return list */ private function extractAutoloadPaths( string $basePath, - array $autoload, + mixed $autoload, ): array { + if (!is_array($autoload)) { + return []; + } + $result = []; foreach ($autoload as $paths) { if (!is_array($paths)) { - $paths = [$paths]; // @phpstan-ignore shipmonk.variableTypeOverwritten + $paths = [$paths]; } foreach ($paths as $path) { + if (!is_string($path)) { + continue; + } + $isAbsolute = preg_match('#([a-z]:)?[/\\\\]#Ai', $path); if ($isAbsolute === 1) { diff --git a/src/Provider/ComposerUsageProvider.php b/src/Provider/ComposerUsageProvider.php index ba5323e2..faf86498 100644 --- a/src/Provider/ComposerUsageProvider.php +++ b/src/Provider/ComposerUsageProvider.php @@ -2,26 +2,18 @@ namespace ShipMonk\PHPStan\DeadCode\Provider; -use Composer\Autoload\ClassLoader; use LogicException; use PHPStan\Reflection\ReflectionProvider; use ReflectionMethod; -use function array_filter; -use function array_keys; -use function count; +use ShipMonk\PHPStan\DeadCode\Composer\ComposerRootLocator; use function explode; -use function file_get_contents; use function is_array; use function is_file; use function is_string; -use function json_decode; -use function json_last_error; use function ltrim; -use function reset; use function sprintf; use function str_contains; use function str_starts_with; -use const JSON_ERROR_NONE; /** * Detects static methods referenced as PHP callbacks in the scripts section of composer.json, @@ -34,6 +26,8 @@ final class ComposerUsageProvider extends ReflectionBasedMemberUsageProvider private readonly ReflectionProvider $reflectionProvider; + private readonly ComposerRootLocator $composerRootLocator; + /** * declaring class => [method => note] * @@ -43,11 +37,13 @@ final class ComposerUsageProvider extends ReflectionBasedMemberUsageProvider public function __construct( ReflectionProvider $reflectionProvider, + ComposerRootLocator $composerRootLocator, bool $enabled, ?string $composerJsonPath, ) { $this->reflectionProvider = $reflectionProvider; + $this->composerRootLocator = $composerRootLocator; $this->loadScriptCallbacks($enabled, $composerJsonPath); } @@ -61,7 +57,7 @@ private function loadScriptCallbacks( } if ($composerJsonPath === null) { - $autodetectedPath = $this->autodetectComposerJsonPath(); + $autodetectedPath = $this->composerRootLocator->autodetectComposerJsonPath(); if ($autodetectedPath !== null) { $this->extractScriptCallbacks($autodetectedPath); @@ -88,18 +84,7 @@ protected function shouldMarkMethodAsUsed(ReflectionMethod $method): ?VirtualUsa private function extractScriptCallbacks(string $composerJsonPath): void { - $composerJsonRawData = file_get_contents($composerJsonPath); - - if ($composerJsonRawData === false) { - return; - } - - $composerJsonData = json_decode($composerJsonRawData, associative: true); - - if (json_last_error() !== JSON_ERROR_NONE || !is_array($composerJsonData)) { - return; - } - + $composerJsonData = $this->composerRootLocator->parseComposerJson($composerJsonPath); $scripts = $composerJsonData['scripts'] ?? []; if (!is_array($scripts)) { @@ -155,23 +140,4 @@ private function isPhpScript(string $listener): bool && str_contains($listener, '::'); } - private function autodetectComposerJsonPath(): ?string - { - $vendorDirs = array_filter(array_keys(ClassLoader::getRegisteredLoaders()), static function (string $vendorDir): bool { - return !str_starts_with($vendorDir, 'phar://'); - }); - - if (count($vendorDirs) !== 1) { - return null; - } - - $composerJsonPath = reset($vendorDirs) . '/../composer.json'; - - if (!is_file($composerJsonPath)) { - return null; - } - - return $composerJsonPath; - } - } diff --git a/src/Provider/SymfonyUsageProvider.php b/src/Provider/SymfonyUsageProvider.php index 1dba8444..94e19fbe 100644 --- a/src/Provider/SymfonyUsageProvider.php +++ b/src/Provider/SymfonyUsageProvider.php @@ -2,7 +2,6 @@ namespace ShipMonk\PHPStan\DeadCode\Provider; -use Composer\Autoload\ClassLoader; use Composer\InstalledVersions; use FilesystemIterator; use LogicException; @@ -33,6 +32,7 @@ use ReflectionAttribute; use ReflectionNamedType; use Reflector; +use ShipMonk\PHPStan\DeadCode\Composer\ComposerRootLocator; use ShipMonk\PHPStan\DeadCode\Enum\AccessType; use ShipMonk\PHPStan\DeadCode\Graph\ClassConstantRef; use ShipMonk\PHPStan\DeadCode\Graph\ClassConstantUsage; @@ -47,9 +47,7 @@ use SplFileInfo; use Symfony\UX\TwigComponent\Attribute\FromMethod; use UnexpectedValueException; -use function array_filter; use function array_key_first; -use function array_keys; use function count; use function explode; use function extension_loaded; @@ -59,7 +57,6 @@ use function is_dir; use function is_string; use function preg_match_all; -use function reset; use function simplexml_load_string; use function sprintf; use function str_ends_with; @@ -72,6 +69,8 @@ final class SymfonyUsageProvider implements MemberUsageProvider private readonly ReflectionProvider $reflectionProvider; + private readonly ComposerRootLocator $composerRootLocator; + private readonly TemplateViewDataTraverser $traverser; private readonly bool $enabled; @@ -112,6 +111,7 @@ final class SymfonyUsageProvider implements MemberUsageProvider public function __construct( Container $container, ReflectionProvider $reflectionProvider, + ComposerRootLocator $composerRootLocator, TemplateViewDataTraverser $traverser, ?bool $enabled, ?string $configDir, @@ -119,6 +119,7 @@ public function __construct( ) { $this->reflectionProvider = $reflectionProvider; + $this->composerRootLocator = $composerRootLocator; $this->traverser = $traverser; $this->enabled = $enabled ?? $this->isSymfonyInstalled(); $this->configDir = $configDir ?? $this->autodetectConfigDir(); @@ -1575,15 +1576,12 @@ private function createUsage( private function autodetectConfigDir(): ?string { - $vendorDirs = array_filter(array_keys(ClassLoader::getRegisteredLoaders()), static function (string $vendorDir): bool { - return !str_starts_with($vendorDir, 'phar://'); - }); + $vendorDir = $this->composerRootLocator->autodetectVendorDir(); - if (count($vendorDirs) !== 1) { + if ($vendorDir === null) { return null; } - $vendorDir = reset($vendorDirs); $configDir = $vendorDir . '/../config'; if (is_dir($configDir)) { diff --git a/tests/Excluder/TestsUsageExcluderTest.php b/tests/Excluder/TestsUsageExcluderTest.php index 8f12d2ff..a0f987e9 100644 --- a/tests/Excluder/TestsUsageExcluderTest.php +++ b/tests/Excluder/TestsUsageExcluderTest.php @@ -5,6 +5,7 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\Testing\PHPStanTestCase; use ReflectionClass; +use ShipMonk\PHPStan\DeadCode\Composer\ComposerRootLocator; use function realpath; final class TestsUsageExcluderTest extends PHPStanTestCase @@ -12,7 +13,7 @@ final class TestsUsageExcluderTest extends PHPStanTestCase public function testAutodetectComposerDevPaths(): void { - $excluder = new TestsUsageExcluder(self::getContainer()->getByType(ReflectionProvider::class), true, null); + $excluder = new TestsUsageExcluder(self::getContainer()->getByType(ReflectionProvider::class), new ComposerRootLocator(), true, null); $excluderReflection = new ReflectionClass(TestsUsageExcluder::class); $devPathsPropertyReflection = $excluderReflection->getProperty('devPaths'); diff --git a/tests/Provider/ComposerUsageProviderTest.php b/tests/Provider/ComposerUsageProviderTest.php index 50d280d4..dff960d1 100644 --- a/tests/Provider/ComposerUsageProviderTest.php +++ b/tests/Provider/ComposerUsageProviderTest.php @@ -7,6 +7,7 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\Testing\PHPStanTestCase; use ReflectionClass; +use ShipMonk\PHPStan\DeadCode\Composer\ComposerRootLocator; final class ComposerUsageProviderTest extends PHPStanTestCase { @@ -15,7 +16,7 @@ public function testComposerJsonPathCollectsScriptCallbacks(): void { $composerJsonPath = __DIR__ . '/../Rule/data/providers/composer/composer.json'; - $provider = new ComposerUsageProvider($this->getReflectionProviderFromContainer(), true, $composerJsonPath); + $provider = new ComposerUsageProvider($this->getReflectionProviderFromContainer(), new ComposerRootLocator(), true, $composerJsonPath); $scriptCalls = $this->getScriptCalls($provider); self::assertArrayHasKey('ComposerProvider\Scripts', $scriptCalls); @@ -40,7 +41,7 @@ public function testComposerJsonPathCollectsScriptCallbacks(): void public function testAutodetectsComposerJson(): void { - $provider = new ComposerUsageProvider($this->getReflectionProviderFromContainer(), true, null); + $provider = new ComposerUsageProvider($this->getReflectionProviderFromContainer(), new ComposerRootLocator(), true, null); // this repository has no PHP callbacks in its own composer.json scripts self::assertSame([], $this->getScriptCalls($provider)); @@ -52,7 +53,7 @@ public function testAutodetectionRequiresSingleVendorDir(): void $extraLoader->register(); try { - $provider = new ComposerUsageProvider($this->getReflectionProviderFromContainer(), true, null); + $provider = new ComposerUsageProvider($this->getReflectionProviderFromContainer(), new ComposerRootLocator(), true, null); self::assertSame([], $this->getScriptCalls($provider)); } finally { @@ -62,7 +63,7 @@ public function testAutodetectionRequiresSingleVendorDir(): void public function testDisabled(): void { - $provider = new ComposerUsageProvider($this->getReflectionProviderFromContainer(), false, __DIR__ . '/not-a-file.json'); + $provider = new ComposerUsageProvider($this->getReflectionProviderFromContainer(), new ComposerRootLocator(), false, __DIR__ . '/not-a-file.json'); self::assertSame([], $this->getScriptCalls($provider)); } @@ -71,7 +72,7 @@ public function testInvalidPathThrows(): void { self::expectException(LogicException::class); - new ComposerUsageProvider($this->getReflectionProviderFromContainer(), true, __DIR__ . '/not-a-file.json'); + new ComposerUsageProvider($this->getReflectionProviderFromContainer(), new ComposerRootLocator(), true, __DIR__ . '/not-a-file.json'); } private function getReflectionProviderFromContainer(): ReflectionProvider diff --git a/tests/Provider/SymfonyUsageProviderTest.php b/tests/Provider/SymfonyUsageProviderTest.php index 9e670275..88a486ec 100644 --- a/tests/Provider/SymfonyUsageProviderTest.php +++ b/tests/Provider/SymfonyUsageProviderTest.php @@ -5,6 +5,7 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\Testing\PHPStanTestCase; use ReflectionClass; +use ShipMonk\PHPStan\DeadCode\Composer\ComposerRootLocator; use function mkdir; use function realpath; use function rmdir; @@ -17,7 +18,7 @@ public function testAutodetectConfigDir(): void $configDir = __DIR__ . '/../../config'; @mkdir($configDir); - $provider = new SymfonyUsageProvider(self::getContainer(), self::getContainer()->getByType(ReflectionProvider::class), new TemplateViewDataTraverser(self::getContainer()->getByType(ReflectionProvider::class), []), true, null, []); + $provider = new SymfonyUsageProvider(self::getContainer(), self::getContainer()->getByType(ReflectionProvider::class), new ComposerRootLocator(), new TemplateViewDataTraverser(self::getContainer()->getByType(ReflectionProvider::class), []), true, null, []); $providerReflection = new ReflectionClass(SymfonyUsageProvider::class); $configDirPropertyReflection = $providerReflection->getProperty('configDir'); @@ -39,7 +40,7 @@ public function testExplicitContainerXmlPaths(): void { $containerXmlPath = __DIR__ . '/../Rule/data/providers/symfony/services.xml'; - $provider = new SymfonyUsageProvider(self::getContainer(), self::getContainer()->getByType(ReflectionProvider::class), new TemplateViewDataTraverser(self::getContainer()->getByType(ReflectionProvider::class), []), true, null, [$containerXmlPath]); + $provider = new SymfonyUsageProvider(self::getContainer(), self::getContainer()->getByType(ReflectionProvider::class), new ComposerRootLocator(), new TemplateViewDataTraverser(self::getContainer()->getByType(ReflectionProvider::class), []), true, null, [$containerXmlPath]); $providerReflection = new ReflectionClass(SymfonyUsageProvider::class); $dicCallsReflection = $providerReflection->getProperty('dicCalls'); @@ -62,7 +63,7 @@ public function testExplicitContainerXmlPathsTakesPrecedenceOverContainer(): voi $containerXmlPath = __DIR__ . '/../Rule/data/providers/symfony/services.xml'; // Even though self::getContainer() has no symfony config, the explicit paths are used - $provider = new SymfonyUsageProvider(self::getContainer(), self::getContainer()->getByType(ReflectionProvider::class), new TemplateViewDataTraverser(self::getContainer()->getByType(ReflectionProvider::class), []), true, null, [$containerXmlPath]); + $provider = new SymfonyUsageProvider(self::getContainer(), self::getContainer()->getByType(ReflectionProvider::class), new ComposerRootLocator(), new TemplateViewDataTraverser(self::getContainer()->getByType(ReflectionProvider::class), []), true, null, [$containerXmlPath]); $providerReflection = new ReflectionClass(SymfonyUsageProvider::class); $dicCallsReflection = $providerReflection->getProperty('dicCalls'); @@ -77,7 +78,7 @@ public function testEmptyContainerXmlPathsFallsBackToContainer(): void { // When containerXmlPaths is empty, it falls back to getContainerXmlPath(container) // self::getContainer() has no symfony parameter, so no DIC classes are loaded - $provider = new SymfonyUsageProvider(self::getContainer(), self::getContainer()->getByType(ReflectionProvider::class), new TemplateViewDataTraverser(self::getContainer()->getByType(ReflectionProvider::class), []), true, null, []); + $provider = new SymfonyUsageProvider(self::getContainer(), self::getContainer()->getByType(ReflectionProvider::class), new ComposerRootLocator(), new TemplateViewDataTraverser(self::getContainer()->getByType(ReflectionProvider::class), []), true, null, []); $providerReflection = new ReflectionClass(SymfonyUsageProvider::class); $dicCallsReflection = $providerReflection->getProperty('dicCalls'); diff --git a/tests/Rule/DeadCodeRuleTest.php b/tests/Rule/DeadCodeRuleTest.php index 9d88c430..b0982e95 100644 --- a/tests/Rule/DeadCodeRuleTest.php +++ b/tests/Rule/DeadCodeRuleTest.php @@ -37,6 +37,7 @@ use ShipMonk\PHPStan\DeadCode\Collector\PropertyAccessCollector; use ShipMonk\PHPStan\DeadCode\Collector\ProvidedUsagesCollector; use ShipMonk\PHPStan\DeadCode\Compatibility\BackwardCompatibilityChecker; +use ShipMonk\PHPStan\DeadCode\Composer\ComposerRootLocator; use ShipMonk\PHPStan\DeadCode\Debug\DebugUsagePrinter; use ShipMonk\PHPStan\DeadCode\Error\BlackMember; use ShipMonk\PHPStan\DeadCode\Excluder\MemberUsageExcluder; @@ -1262,12 +1263,14 @@ private function getMemberUsageProviders(): array ), new ComposerUsageProvider( self::getContainer()->getByType(ReflectionProvider::class), + new ComposerRootLocator(), $this->providersEnabled, __DIR__ . '/data/providers/composer/composer.json', ), new SymfonyUsageProvider( $this->createContainerMockWithSymfonyConfig(), self::getContainer()->getByType(ReflectionProvider::class), + new ComposerRootLocator(), $templateViewDataTraverser, $this->providersEnabled, __DIR__ . '/data/providers/symfony/', @@ -1333,6 +1336,7 @@ private function getMemberUsageExcluders(): array $excluders = [ new TestsUsageExcluder( self::createReflectionProvider(), + new ComposerRootLocator(), true, [__DIR__ . '/data/excluders/../excluders/tests/tests'], // tests path normalization ), From 8f99fcf173bfc87d898419eaae1d963701b57fda Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Mon, 13 Jul 2026 21:11:24 +0200 Subject: [PATCH 2/2] Rename ComposerRootLocator to ComposerIntrospector Co-Authored-By: Claude Code --- rules.neon | 2 +- ...poserRootLocator.php => ComposerIntrospector.php} | 2 +- src/Excluder/TestsUsageExcluder.php | 12 ++++++------ src/Provider/ComposerUsageProvider.php | 12 ++++++------ src/Provider/SymfonyUsageProvider.php | 10 +++++----- tests/Excluder/TestsUsageExcluderTest.php | 4 ++-- tests/Provider/ComposerUsageProviderTest.php | 12 ++++++------ tests/Provider/SymfonyUsageProviderTest.php | 10 +++++----- tests/Rule/DeadCodeRuleTest.php | 8 ++++---- 9 files changed, 36 insertions(+), 36 deletions(-) rename src/Composer/{ComposerRootLocator.php => ComposerIntrospector.php} (98%) diff --git a/rules.neon b/rules.neon index 8faa0bef..b8ed1c41 100644 --- a/rules.neon +++ b/rules.neon @@ -14,7 +14,7 @@ services: tmpDir: %tmpDir% offloadCollectorData: %shipmonkDeadCode.cache.offloadCollectorData% - - class: ShipMonk\PHPStan\DeadCode\Composer\ComposerRootLocator + class: ShipMonk\PHPStan\DeadCode\Composer\ComposerIntrospector - class: ShipMonk\PHPStan\DeadCode\Hierarchy\ClassHierarchy - diff --git a/src/Composer/ComposerRootLocator.php b/src/Composer/ComposerIntrospector.php similarity index 98% rename from src/Composer/ComposerRootLocator.php rename to src/Composer/ComposerIntrospector.php index 5fc620e3..46259fa2 100644 --- a/src/Composer/ComposerRootLocator.php +++ b/src/Composer/ComposerIntrospector.php @@ -15,7 +15,7 @@ use function str_starts_with; use const JSON_ERROR_NONE; -final class ComposerRootLocator +final class ComposerIntrospector { /** diff --git a/src/Excluder/TestsUsageExcluder.php b/src/Excluder/TestsUsageExcluder.php index 285a3b23..97cc09ff 100644 --- a/src/Excluder/TestsUsageExcluder.php +++ b/src/Excluder/TestsUsageExcluder.php @@ -6,7 +6,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; -use ShipMonk\PHPStan\DeadCode\Composer\ComposerRootLocator; +use ShipMonk\PHPStan\DeadCode\Composer\ComposerIntrospector; use ShipMonk\PHPStan\DeadCode\Graph\ClassMemberUsage; use function dirname; use function glob; @@ -22,7 +22,7 @@ final class TestsUsageExcluder implements MemberUsageExcluder private readonly ReflectionProvider $reflectionProvider; - private readonly ComposerRootLocator $composerRootLocator; + private readonly ComposerIntrospector $composerIntrospector; private readonly bool $enabled; @@ -36,13 +36,13 @@ final class TestsUsageExcluder implements MemberUsageExcluder */ public function __construct( ReflectionProvider $reflectionProvider, - ComposerRootLocator $composerRootLocator, + ComposerIntrospector $composerIntrospector, bool $enabled, ?array $devPaths, ) { $this->reflectionProvider = $reflectionProvider; - $this->composerRootLocator = $composerRootLocator; + $this->composerIntrospector = $composerIntrospector; $this->enabled = $enabled; if ($devPaths !== null) { @@ -116,13 +116,13 @@ private function getDeclarationFile(?string $className): ?string */ private function autodetectComposerDevPaths(): array { - $composerJsonPath = $this->composerRootLocator->autodetectComposerJsonPath(); + $composerJsonPath = $this->composerIntrospector->autodetectComposerJsonPath(); if ($composerJsonPath === null) { return []; } - $composerJsonData = $this->composerRootLocator->parseComposerJson($composerJsonPath); + $composerJsonData = $this->composerIntrospector->parseComposerJson($composerJsonPath); $autoloadDev = $composerJsonData['autoload-dev'] ?? []; if (!is_array($autoloadDev)) { diff --git a/src/Provider/ComposerUsageProvider.php b/src/Provider/ComposerUsageProvider.php index faf86498..1c3693dc 100644 --- a/src/Provider/ComposerUsageProvider.php +++ b/src/Provider/ComposerUsageProvider.php @@ -5,7 +5,7 @@ use LogicException; use PHPStan\Reflection\ReflectionProvider; use ReflectionMethod; -use ShipMonk\PHPStan\DeadCode\Composer\ComposerRootLocator; +use ShipMonk\PHPStan\DeadCode\Composer\ComposerIntrospector; use function explode; use function is_array; use function is_file; @@ -26,7 +26,7 @@ final class ComposerUsageProvider extends ReflectionBasedMemberUsageProvider private readonly ReflectionProvider $reflectionProvider; - private readonly ComposerRootLocator $composerRootLocator; + private readonly ComposerIntrospector $composerIntrospector; /** * declaring class => [method => note] @@ -37,13 +37,13 @@ final class ComposerUsageProvider extends ReflectionBasedMemberUsageProvider public function __construct( ReflectionProvider $reflectionProvider, - ComposerRootLocator $composerRootLocator, + ComposerIntrospector $composerIntrospector, bool $enabled, ?string $composerJsonPath, ) { $this->reflectionProvider = $reflectionProvider; - $this->composerRootLocator = $composerRootLocator; + $this->composerIntrospector = $composerIntrospector; $this->loadScriptCallbacks($enabled, $composerJsonPath); } @@ -57,7 +57,7 @@ private function loadScriptCallbacks( } if ($composerJsonPath === null) { - $autodetectedPath = $this->composerRootLocator->autodetectComposerJsonPath(); + $autodetectedPath = $this->composerIntrospector->autodetectComposerJsonPath(); if ($autodetectedPath !== null) { $this->extractScriptCallbacks($autodetectedPath); @@ -84,7 +84,7 @@ protected function shouldMarkMethodAsUsed(ReflectionMethod $method): ?VirtualUsa private function extractScriptCallbacks(string $composerJsonPath): void { - $composerJsonData = $this->composerRootLocator->parseComposerJson($composerJsonPath); + $composerJsonData = $this->composerIntrospector->parseComposerJson($composerJsonPath); $scripts = $composerJsonData['scripts'] ?? []; if (!is_array($scripts)) { diff --git a/src/Provider/SymfonyUsageProvider.php b/src/Provider/SymfonyUsageProvider.php index 94e19fbe..2ce7a047 100644 --- a/src/Provider/SymfonyUsageProvider.php +++ b/src/Provider/SymfonyUsageProvider.php @@ -32,7 +32,7 @@ use ReflectionAttribute; use ReflectionNamedType; use Reflector; -use ShipMonk\PHPStan\DeadCode\Composer\ComposerRootLocator; +use ShipMonk\PHPStan\DeadCode\Composer\ComposerIntrospector; use ShipMonk\PHPStan\DeadCode\Enum\AccessType; use ShipMonk\PHPStan\DeadCode\Graph\ClassConstantRef; use ShipMonk\PHPStan\DeadCode\Graph\ClassConstantUsage; @@ -69,7 +69,7 @@ final class SymfonyUsageProvider implements MemberUsageProvider private readonly ReflectionProvider $reflectionProvider; - private readonly ComposerRootLocator $composerRootLocator; + private readonly ComposerIntrospector $composerIntrospector; private readonly TemplateViewDataTraverser $traverser; @@ -111,7 +111,7 @@ final class SymfonyUsageProvider implements MemberUsageProvider public function __construct( Container $container, ReflectionProvider $reflectionProvider, - ComposerRootLocator $composerRootLocator, + ComposerIntrospector $composerIntrospector, TemplateViewDataTraverser $traverser, ?bool $enabled, ?string $configDir, @@ -119,7 +119,7 @@ public function __construct( ) { $this->reflectionProvider = $reflectionProvider; - $this->composerRootLocator = $composerRootLocator; + $this->composerIntrospector = $composerIntrospector; $this->traverser = $traverser; $this->enabled = $enabled ?? $this->isSymfonyInstalled(); $this->configDir = $configDir ?? $this->autodetectConfigDir(); @@ -1576,7 +1576,7 @@ private function createUsage( private function autodetectConfigDir(): ?string { - $vendorDir = $this->composerRootLocator->autodetectVendorDir(); + $vendorDir = $this->composerIntrospector->autodetectVendorDir(); if ($vendorDir === null) { return null; diff --git a/tests/Excluder/TestsUsageExcluderTest.php b/tests/Excluder/TestsUsageExcluderTest.php index a0f987e9..fd1cf773 100644 --- a/tests/Excluder/TestsUsageExcluderTest.php +++ b/tests/Excluder/TestsUsageExcluderTest.php @@ -5,7 +5,7 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\Testing\PHPStanTestCase; use ReflectionClass; -use ShipMonk\PHPStan\DeadCode\Composer\ComposerRootLocator; +use ShipMonk\PHPStan\DeadCode\Composer\ComposerIntrospector; use function realpath; final class TestsUsageExcluderTest extends PHPStanTestCase @@ -13,7 +13,7 @@ final class TestsUsageExcluderTest extends PHPStanTestCase public function testAutodetectComposerDevPaths(): void { - $excluder = new TestsUsageExcluder(self::getContainer()->getByType(ReflectionProvider::class), new ComposerRootLocator(), true, null); + $excluder = new TestsUsageExcluder(self::getContainer()->getByType(ReflectionProvider::class), new ComposerIntrospector(), true, null); $excluderReflection = new ReflectionClass(TestsUsageExcluder::class); $devPathsPropertyReflection = $excluderReflection->getProperty('devPaths'); diff --git a/tests/Provider/ComposerUsageProviderTest.php b/tests/Provider/ComposerUsageProviderTest.php index dff960d1..0368e05a 100644 --- a/tests/Provider/ComposerUsageProviderTest.php +++ b/tests/Provider/ComposerUsageProviderTest.php @@ -7,7 +7,7 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\Testing\PHPStanTestCase; use ReflectionClass; -use ShipMonk\PHPStan\DeadCode\Composer\ComposerRootLocator; +use ShipMonk\PHPStan\DeadCode\Composer\ComposerIntrospector; final class ComposerUsageProviderTest extends PHPStanTestCase { @@ -16,7 +16,7 @@ public function testComposerJsonPathCollectsScriptCallbacks(): void { $composerJsonPath = __DIR__ . '/../Rule/data/providers/composer/composer.json'; - $provider = new ComposerUsageProvider($this->getReflectionProviderFromContainer(), new ComposerRootLocator(), true, $composerJsonPath); + $provider = new ComposerUsageProvider($this->getReflectionProviderFromContainer(), new ComposerIntrospector(), true, $composerJsonPath); $scriptCalls = $this->getScriptCalls($provider); self::assertArrayHasKey('ComposerProvider\Scripts', $scriptCalls); @@ -41,7 +41,7 @@ public function testComposerJsonPathCollectsScriptCallbacks(): void public function testAutodetectsComposerJson(): void { - $provider = new ComposerUsageProvider($this->getReflectionProviderFromContainer(), new ComposerRootLocator(), true, null); + $provider = new ComposerUsageProvider($this->getReflectionProviderFromContainer(), new ComposerIntrospector(), true, null); // this repository has no PHP callbacks in its own composer.json scripts self::assertSame([], $this->getScriptCalls($provider)); @@ -53,7 +53,7 @@ public function testAutodetectionRequiresSingleVendorDir(): void $extraLoader->register(); try { - $provider = new ComposerUsageProvider($this->getReflectionProviderFromContainer(), new ComposerRootLocator(), true, null); + $provider = new ComposerUsageProvider($this->getReflectionProviderFromContainer(), new ComposerIntrospector(), true, null); self::assertSame([], $this->getScriptCalls($provider)); } finally { @@ -63,7 +63,7 @@ public function testAutodetectionRequiresSingleVendorDir(): void public function testDisabled(): void { - $provider = new ComposerUsageProvider($this->getReflectionProviderFromContainer(), new ComposerRootLocator(), false, __DIR__ . '/not-a-file.json'); + $provider = new ComposerUsageProvider($this->getReflectionProviderFromContainer(), new ComposerIntrospector(), false, __DIR__ . '/not-a-file.json'); self::assertSame([], $this->getScriptCalls($provider)); } @@ -72,7 +72,7 @@ public function testInvalidPathThrows(): void { self::expectException(LogicException::class); - new ComposerUsageProvider($this->getReflectionProviderFromContainer(), new ComposerRootLocator(), true, __DIR__ . '/not-a-file.json'); + new ComposerUsageProvider($this->getReflectionProviderFromContainer(), new ComposerIntrospector(), true, __DIR__ . '/not-a-file.json'); } private function getReflectionProviderFromContainer(): ReflectionProvider diff --git a/tests/Provider/SymfonyUsageProviderTest.php b/tests/Provider/SymfonyUsageProviderTest.php index 88a486ec..e3c1fe25 100644 --- a/tests/Provider/SymfonyUsageProviderTest.php +++ b/tests/Provider/SymfonyUsageProviderTest.php @@ -5,7 +5,7 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\Testing\PHPStanTestCase; use ReflectionClass; -use ShipMonk\PHPStan\DeadCode\Composer\ComposerRootLocator; +use ShipMonk\PHPStan\DeadCode\Composer\ComposerIntrospector; use function mkdir; use function realpath; use function rmdir; @@ -18,7 +18,7 @@ public function testAutodetectConfigDir(): void $configDir = __DIR__ . '/../../config'; @mkdir($configDir); - $provider = new SymfonyUsageProvider(self::getContainer(), self::getContainer()->getByType(ReflectionProvider::class), new ComposerRootLocator(), new TemplateViewDataTraverser(self::getContainer()->getByType(ReflectionProvider::class), []), true, null, []); + $provider = new SymfonyUsageProvider(self::getContainer(), self::getContainer()->getByType(ReflectionProvider::class), new ComposerIntrospector(), new TemplateViewDataTraverser(self::getContainer()->getByType(ReflectionProvider::class), []), true, null, []); $providerReflection = new ReflectionClass(SymfonyUsageProvider::class); $configDirPropertyReflection = $providerReflection->getProperty('configDir'); @@ -40,7 +40,7 @@ public function testExplicitContainerXmlPaths(): void { $containerXmlPath = __DIR__ . '/../Rule/data/providers/symfony/services.xml'; - $provider = new SymfonyUsageProvider(self::getContainer(), self::getContainer()->getByType(ReflectionProvider::class), new ComposerRootLocator(), new TemplateViewDataTraverser(self::getContainer()->getByType(ReflectionProvider::class), []), true, null, [$containerXmlPath]); + $provider = new SymfonyUsageProvider(self::getContainer(), self::getContainer()->getByType(ReflectionProvider::class), new ComposerIntrospector(), new TemplateViewDataTraverser(self::getContainer()->getByType(ReflectionProvider::class), []), true, null, [$containerXmlPath]); $providerReflection = new ReflectionClass(SymfonyUsageProvider::class); $dicCallsReflection = $providerReflection->getProperty('dicCalls'); @@ -63,7 +63,7 @@ public function testExplicitContainerXmlPathsTakesPrecedenceOverContainer(): voi $containerXmlPath = __DIR__ . '/../Rule/data/providers/symfony/services.xml'; // Even though self::getContainer() has no symfony config, the explicit paths are used - $provider = new SymfonyUsageProvider(self::getContainer(), self::getContainer()->getByType(ReflectionProvider::class), new ComposerRootLocator(), new TemplateViewDataTraverser(self::getContainer()->getByType(ReflectionProvider::class), []), true, null, [$containerXmlPath]); + $provider = new SymfonyUsageProvider(self::getContainer(), self::getContainer()->getByType(ReflectionProvider::class), new ComposerIntrospector(), new TemplateViewDataTraverser(self::getContainer()->getByType(ReflectionProvider::class), []), true, null, [$containerXmlPath]); $providerReflection = new ReflectionClass(SymfonyUsageProvider::class); $dicCallsReflection = $providerReflection->getProperty('dicCalls'); @@ -78,7 +78,7 @@ public function testEmptyContainerXmlPathsFallsBackToContainer(): void { // When containerXmlPaths is empty, it falls back to getContainerXmlPath(container) // self::getContainer() has no symfony parameter, so no DIC classes are loaded - $provider = new SymfonyUsageProvider(self::getContainer(), self::getContainer()->getByType(ReflectionProvider::class), new ComposerRootLocator(), new TemplateViewDataTraverser(self::getContainer()->getByType(ReflectionProvider::class), []), true, null, []); + $provider = new SymfonyUsageProvider(self::getContainer(), self::getContainer()->getByType(ReflectionProvider::class), new ComposerIntrospector(), new TemplateViewDataTraverser(self::getContainer()->getByType(ReflectionProvider::class), []), true, null, []); $providerReflection = new ReflectionClass(SymfonyUsageProvider::class); $dicCallsReflection = $providerReflection->getProperty('dicCalls'); diff --git a/tests/Rule/DeadCodeRuleTest.php b/tests/Rule/DeadCodeRuleTest.php index b0982e95..54ec27b6 100644 --- a/tests/Rule/DeadCodeRuleTest.php +++ b/tests/Rule/DeadCodeRuleTest.php @@ -37,7 +37,7 @@ use ShipMonk\PHPStan\DeadCode\Collector\PropertyAccessCollector; use ShipMonk\PHPStan\DeadCode\Collector\ProvidedUsagesCollector; use ShipMonk\PHPStan\DeadCode\Compatibility\BackwardCompatibilityChecker; -use ShipMonk\PHPStan\DeadCode\Composer\ComposerRootLocator; +use ShipMonk\PHPStan\DeadCode\Composer\ComposerIntrospector; use ShipMonk\PHPStan\DeadCode\Debug\DebugUsagePrinter; use ShipMonk\PHPStan\DeadCode\Error\BlackMember; use ShipMonk\PHPStan\DeadCode\Excluder\MemberUsageExcluder; @@ -1263,14 +1263,14 @@ private function getMemberUsageProviders(): array ), new ComposerUsageProvider( self::getContainer()->getByType(ReflectionProvider::class), - new ComposerRootLocator(), + new ComposerIntrospector(), $this->providersEnabled, __DIR__ . '/data/providers/composer/composer.json', ), new SymfonyUsageProvider( $this->createContainerMockWithSymfonyConfig(), self::getContainer()->getByType(ReflectionProvider::class), - new ComposerRootLocator(), + new ComposerIntrospector(), $templateViewDataTraverser, $this->providersEnabled, __DIR__ . '/data/providers/symfony/', @@ -1336,7 +1336,7 @@ private function getMemberUsageExcluders(): array $excluders = [ new TestsUsageExcluder( self::createReflectionProvider(), - new ComposerRootLocator(), + new ComposerIntrospector(), true, [__DIR__ . '/data/excluders/../excluders/tests/tests'], // tests path normalization ),