diff --git a/rules-tests/DeadCode/Rector/Expression/RemovePsr4AutoloadedIncludeRector/Fixture/remove_autoloaded_require.php.inc b/rules-tests/DeadCode/Rector/Expression/RemovePsr4AutoloadedIncludeRector/Fixture/remove_autoloaded_require.php.inc new file mode 100644 index 00000000000..de3cfacbd17 --- /dev/null +++ b/rules-tests/DeadCode/Rector/Expression/RemovePsr4AutoloadedIncludeRector/Fixture/remove_autoloaded_require.php.inc @@ -0,0 +1,13 @@ + +----- + diff --git a/rules-tests/DeadCode/Rector/Expression/RemovePsr4AutoloadedIncludeRector/Fixture/skip_assigned_include.php.inc b/rules-tests/DeadCode/Rector/Expression/RemovePsr4AutoloadedIncludeRector/Fixture/skip_assigned_include.php.inc new file mode 100644 index 00000000000..27470ac5684 --- /dev/null +++ b/rules-tests/DeadCode/Rector/Expression/RemovePsr4AutoloadedIncludeRector/Fixture/skip_assigned_include.php.inc @@ -0,0 +1,5 @@ + diff --git a/rules-tests/DeadCode/Rector/Expression/RemovePsr4AutoloadedIncludeRector/Fixture/skip_multiple_classes.php.inc b/rules-tests/DeadCode/Rector/Expression/RemovePsr4AutoloadedIncludeRector/Fixture/skip_multiple_classes.php.inc new file mode 100644 index 00000000000..6c98f9012fd --- /dev/null +++ b/rules-tests/DeadCode/Rector/Expression/RemovePsr4AutoloadedIncludeRector/Fixture/skip_multiple_classes.php.inc @@ -0,0 +1,7 @@ + diff --git a/rules-tests/DeadCode/Rector/Expression/RemovePsr4AutoloadedIncludeRector/Fixture/skip_not_autoloaded.php.inc b/rules-tests/DeadCode/Rector/Expression/RemovePsr4AutoloadedIncludeRector/Fixture/skip_not_autoloaded.php.inc new file mode 100644 index 00000000000..c49559198e4 --- /dev/null +++ b/rules-tests/DeadCode/Rector/Expression/RemovePsr4AutoloadedIncludeRector/Fixture/skip_not_autoloaded.php.inc @@ -0,0 +1,7 @@ + diff --git a/rules-tests/DeadCode/Rector/Expression/RemovePsr4AutoloadedIncludeRector/RemovePsr4AutoloadedIncludeRectorTest.php b/rules-tests/DeadCode/Rector/Expression/RemovePsr4AutoloadedIncludeRector/RemovePsr4AutoloadedIncludeRectorTest.php new file mode 100644 index 00000000000..4539f16ec4e --- /dev/null +++ b/rules-tests/DeadCode/Rector/Expression/RemovePsr4AutoloadedIncludeRector/RemovePsr4AutoloadedIncludeRectorTest.php @@ -0,0 +1,28 @@ +doTestFile($filePath); + } + + public static function provideData(): Iterator + { + return self::yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/rules-tests/DeadCode/Rector/Expression/RemovePsr4AutoloadedIncludeRector/Source/composer.json b/rules-tests/DeadCode/Rector/Expression/RemovePsr4AutoloadedIncludeRector/Source/composer.json new file mode 100644 index 00000000000..c94579132bf --- /dev/null +++ b/rules-tests/DeadCode/Rector/Expression/RemovePsr4AutoloadedIncludeRector/Source/composer.json @@ -0,0 +1,7 @@ +{ + "autoload": { + "psr-4": { + "App\\": "src" + } + } +} diff --git a/rules-tests/DeadCode/Rector/Expression/RemovePsr4AutoloadedIncludeRector/Source/outside/LooseClass.php b/rules-tests/DeadCode/Rector/Expression/RemovePsr4AutoloadedIncludeRector/Source/outside/LooseClass.php new file mode 100644 index 00000000000..7c82ea28841 --- /dev/null +++ b/rules-tests/DeadCode/Rector/Expression/RemovePsr4AutoloadedIncludeRector/Source/outside/LooseClass.php @@ -0,0 +1,9 @@ +withConfiguredRule(RemovePsr4AutoloadedIncludeRector::class, [ + RemovePsr4AutoloadedIncludeRector::COMPOSER_JSON_PATH => __DIR__ . '/../Source/composer.json', + ]); diff --git a/rules/DeadCode/Rector/Expression/RemovePsr4AutoloadedIncludeRector.php b/rules/DeadCode/Rector/Expression/RemovePsr4AutoloadedIncludeRector.php new file mode 100644 index 00000000000..22f638770d5 --- /dev/null +++ b/rules/DeadCode/Rector/Expression/RemovePsr4AutoloadedIncludeRector.php @@ -0,0 +1,214 @@ +|null list of [namespace prefix, absolute directory] pairs + */ + private ?array $psr4Prefixes = null; + + public function __construct( + private readonly RectorParser $rectorParser, + private readonly BetterNodeFinder $betterNodeFinder + ) { + } + + /** + * @param array $configuration + */ + public function configure(array $configuration): void + { + $composerJsonPath = $configuration[self::COMPOSER_JSON_PATH] ?? null; + $this->composerJsonPath = is_string($composerJsonPath) ? $composerJsonPath : null; + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition( + 'Remove include/require of a file that is already autoloaded via composer.json PSR-4', + [ + new ConfiguredCodeSample( + <<<'CODE_SAMPLE' +require __DIR__ . '/src/SomeClass.php'; + +$someClass = new App\SomeClass(); +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +$someClass = new App\SomeClass(); +CODE_SAMPLE + , + [ + self::COMPOSER_JSON_PATH => 'composer.json', + ] + ), + ] + ); + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [Expression::class]; + } + + /** + * @param Expression $node + */ + public function refactor(Node $node): int|null + { + if (! $node->expr instanceof Include_) { + return null; + } + + $includedFilePath = $this->resolveIncludedFilePath($node->expr); + if ($includedFilePath === null) { + return null; + } + + if (! $this->isAutoloadedViaPsr4($includedFilePath)) { + return null; + } + + return NodeVisitor::REMOVE_NODE; + } + + private function resolveIncludedFilePath(Include_ $include): ?string + { + $currentDirectory = dirname($this->file->getFilePath()); + + // __DIR__ . '/relative/path.php' + if ($include->expr instanceof Concat && $include->expr->left instanceof Dir && $include->expr->right instanceof String_) { + return $this->realPath($currentDirectory . $include->expr->right->value); + } + + if (! $include->expr instanceof String_) { + return null; + } + + $rawPath = $include->expr->value; + + // absolute path + if (str_starts_with($rawPath, '/')) { + return $this->realPath($rawPath); + } + + return $this->realPath($currentDirectory . '/' . $rawPath); + } + + private function realPath(string $path): ?string + { + if (! is_file($path)) { + return null; + } + + return realpath($path); + } + + private function isAutoloadedViaPsr4(string $includedFilePath): bool + { + $declaredClassName = $this->resolveSingleDeclaredClassName($includedFilePath); + if ($declaredClassName === null) { + return false; + } + + foreach ($this->providePsr4Prefixes() as [$namespacePrefix, $directory]) { + if (! str_starts_with($declaredClassName, $namespacePrefix)) { + continue; + } + + $relativeClassName = substr($declaredClassName, strlen($namespacePrefix)); + $expectedFilePath = $this->realPath($directory . '/' . str_replace('\\', '/', $relativeClassName) . '.php'); + + if ($expectedFilePath === $includedFilePath) { + return true; + } + } + + return false; + } + + private function resolveSingleDeclaredClassName(string $filePath): ?string + { + $stmts = $this->rectorParser->parseFile($filePath); + + $classLikes = $this->betterNodeFinder->findInstanceOf($stmts, ClassLike::class); + + // require must define exactly one class, or removing it would drop the other definitions + if (count($classLikes) !== 1) { + return null; + } + + // RichParser resolves names, so this is already the fully-qualified class name + return $this->getName($classLikes[0]); + } + + /** + * @return array list of [namespace prefix, absolute directory] pairs + */ + private function providePsr4Prefixes(): array + { + if ($this->psr4Prefixes !== null) { + return $this->psr4Prefixes; + } + + $composerJsonPath = $this->composerJsonPath ?? getcwd() . '/composer.json'; + if (! is_file($composerJsonPath)) { + return $this->psr4Prefixes = []; + } + + $composerJson = JsonFileSystem::readFilePath($composerJsonPath); + $rootDirectory = dirname($composerJsonPath); + + $psr4Prefixes = []; + foreach (['autoload', 'autoload-dev'] as $autoloadSection) { + $psr4 = $composerJson[$autoloadSection]['psr-4'] ?? []; + if (! is_array($psr4)) { + continue; + } + + foreach ($psr4 as $namespacePrefix => $directories) { + foreach ((array) $directories as $directory) { + $psr4Prefixes[] = [ + (string) $namespacePrefix, + $rootDirectory . '/' . rtrim((string) $directory, '/'), + ]; + } + } + } + + return $this->psr4Prefixes = $psr4Prefixes; + } +} diff --git a/structarmed.php b/structarmed.php index 61913483c8d..63d5f79c263 100644 --- a/structarmed.php +++ b/structarmed.php @@ -16,6 +16,9 @@ // no namespace on purpose __DIR__ . '/rules-tests/Php70/Rector/ClassMethod/Php4ConstructorRector/Source/ParentClass.php', + // multiple classes in one file on purpose, to test the skip + __DIR__ . '/rules-tests/DeadCode/Rector/Expression/RemovePsr4AutoloadedIncludeRector/Source/src/TwoClasses.php', + // simulate under phpstan.phar __DIR__ . '/rules-tests/Php71/Rector/FuncCall/RemoveExtraParametersRector/Source/phpstan.phar', ],