From f36b14da7e033d8369859fb3b0656ee82fc0d55b Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sat, 8 Aug 2026 20:48:14 +0200 Subject: [PATCH] warn on skipped classes that are not Rector rules --- src/Configuration/Option.php | 5 +++ src/Console/Command/ProcessCommand.php | 1 + src/Reporting/MissConfigurationReporter.php | 21 +++++++++++ src/Validation/RectorConfigValidator.php | 26 ++++++++++++++ .../MissConfigurationReporterTest.php | 35 +++++++++++++++++++ 5 files changed, 88 insertions(+) diff --git a/src/Configuration/Option.php b/src/Configuration/Option.php index 7f22ca35e33..0ff8efb5925 100644 --- a/src/Configuration/Option.php +++ b/src/Configuration/Option.php @@ -240,6 +240,11 @@ final class Option */ public const string SKIPPED_RECTOR_RULES = 'skipped_rector_rules'; + /** + * @internal For reporting skipped classes that are not Rector rules + */ + public const string SKIPPED_NON_RECTOR_CLASSES = 'skipped_non_rector_classes'; + /** * @internal For reporting deprecated cache meta extensions */ diff --git a/src/Console/Command/ProcessCommand.php b/src/Console/Command/ProcessCommand.php index d285ddd1d92..8d85b358272 100644 --- a/src/Console/Command/ProcessCommand.php +++ b/src/Console/Command/ProcessCommand.php @@ -189,6 +189,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->deprecatedRulesReporter->reportDeprecatedAttributesSetsArgs(); $this->missConfigurationReporter->reportSkippedNeverRegisteredRules(); + $this->missConfigurationReporter->reportSkippedNonRectorClasses(); $this->missConfigurationReporter->reportUnusedSkips($processResult); return $this->resolveReturnCode($processResult, $configuration); diff --git a/src/Reporting/MissConfigurationReporter.php b/src/Reporting/MissConfigurationReporter.php index 0b011de9b80..8b9327e2e6d 100644 --- a/src/Reporting/MissConfigurationReporter.php +++ b/src/Reporting/MissConfigurationReporter.php @@ -7,6 +7,7 @@ use Rector\Configuration\Option; use Rector\Configuration\Parameter\SimpleParameterProvider; use Rector\Configuration\VendorMissAnalyseGuard; +use Rector\Contract\Rector\RectorInterface; use Rector\PostRector\Contract\Rector\PostRectorInterface; use Rector\ValueObject\ProcessResult; use Symfony\Component\Console\Style\SymfonyStyle; @@ -72,6 +73,26 @@ public function reportSkippedNeverRegisteredRules(): void $this->symfonyStyle->listing($neverRegisteredSkippedRules); } + public function reportSkippedNonRectorClasses(): void + { + $skippedNonRectorClasses = SimpleParameterProvider::provideArrayParameter( + Option::SKIPPED_NON_RECTOR_CLASSES + ); + + if ($skippedNonRectorClasses === []) { + return; + } + + $this->symfonyStyle->warning(sprintf( + '%s not a Rector rule, so %s never be skipped. Only classes that implement "%s" can be used in "->withSkip()"', + count($skippedNonRectorClasses) > 1 ? 'These skipped classes are' : 'This skipped class is', + count($skippedNonRectorClasses) > 1 ? 'they can' : 'it can', + RectorInterface::class + )); + + $this->symfonyStyle->listing($skippedNonRectorClasses); + } + /** * @param string[] $filePaths */ diff --git a/src/Validation/RectorConfigValidator.php b/src/Validation/RectorConfigValidator.php index 6afea507819..02ae64b0f7f 100644 --- a/src/Validation/RectorConfigValidator.php +++ b/src/Validation/RectorConfigValidator.php @@ -6,7 +6,9 @@ use Rector\Configuration\Option; use Rector\Configuration\Parameter\SimpleParameterProvider; +use Rector\Contract\Rector\RectorInterface; use Rector\Exception\ShouldNotHappenException; +use Rector\PostRector\Contract\Rector\PostRectorInterface; final class RectorConfigValidator { @@ -33,8 +35,14 @@ public static function ensureRectorRulesExist(array $skip): void { $nonExistingRules = []; $skippedRectorRules = []; + $skippedNonRectorClasses = []; foreach ($skip as $key => $value) { + if (is_string($key) && self::isNonRectorClass($key)) { + $skippedNonRectorClasses[] = $key; + continue; + } + if (self::isRectorClassValue($key)) { if (class_exists($key)) { $skippedRectorRules[] = $key; @@ -58,6 +66,7 @@ public static function ensureRectorRulesExist(array $skip): void } SimpleParameterProvider::addParameter(Option::SKIPPED_RECTOR_RULES, $skippedRectorRules); + SimpleParameterProvider::addParameter(Option::SKIPPED_NON_RECTOR_CLASSES, $skippedNonRectorClasses); if ($nonExistingRules === []) { return; @@ -73,6 +82,23 @@ public static function ensureRectorRulesExist(array $skip): void ); } + /** + * Only Rector rules are matched against skipped classes, so any other class can never be skipped + */ + private static function isNonRectorClass(string $key): bool + { + // interfaces are allowed, as they can mark a group of Rector rules + if (! class_exists($key)) { + return false; + } + + if (is_a($key, RectorInterface::class, true)) { + return false; + } + + return ! is_a($key, PostRectorInterface::class, true); + } + private static function isRectorClassValue(mixed $value): bool { // only validate string diff --git a/tests/Reporting/MissConfigurationReporterTest.php b/tests/Reporting/MissConfigurationReporterTest.php index 02732bb3a66..e05cb978d77 100644 --- a/tests/Reporting/MissConfigurationReporterTest.php +++ b/tests/Reporting/MissConfigurationReporterTest.php @@ -7,12 +7,15 @@ use Rector\Configuration\Option; use Rector\Configuration\Parameter\SimpleParameterProvider; use Rector\Configuration\VendorMissAnalyseGuard; +use Rector\Php85\Rector\FuncCall\OrdSingleByteRector; +use Rector\PostRector\Rector\NameImportingPostRector; use Rector\Reporting\MissConfigurationReporter; use Rector\Reporting\UnusedSkipResolver; use Rector\Testing\PHPUnit\AbstractLazyTestCase; use Rector\Tests\Skipper\Skipper\Fixture\Element\FifthElement; use Rector\Tests\Skipper\Skipper\Fixture\Element\ThreeMan; use Rector\Tests\Skipper\Skipper\Source\AnotherClassToSkip; +use Rector\Validation\RectorConfigValidator; use Rector\ValueObject\ProcessResult; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Output\BufferedOutput; @@ -59,6 +62,38 @@ protected function tearDown(): void { SimpleParameterProvider::setParameter(Option::SKIP, []); SimpleParameterProvider::setParameter(Option::REPORT_UNUSED_SKIPS, false); + SimpleParameterProvider::setParameter(Option::SKIPPED_NON_RECTOR_CLASSES, []); + } + + public function testReportsSkippedNonRectorClass(): void + { + RectorConfigValidator::ensureRectorRulesExist([ + // not a Rector rule, can never be skipped + AnotherClassToSkip::class => ['some/path'], + // Rector rule, must not be reported + OrdSingleByteRector::class => ['some/path'], + // post rector rule, must not be reported + NameImportingPostRector::class => ['some/path'], + ]); + + $this->missConfigurationReporter->reportSkippedNonRectorClasses(); + + $output = $this->bufferedOutput->fetch(); + + $this->assertStringContainsString('AnotherClassToSkip', $output); + $this->assertStringNotContainsString('OrdSingleByteRector', $output); + $this->assertStringNotContainsString('NameImportingPostRector', $output); + } + + public function testReportsNothingOnRectorClassesOnly(): void + { + RectorConfigValidator::ensureRectorRulesExist([ + OrdSingleByteRector::class => ['some/path'], + ]); + + $this->missConfigurationReporter->reportSkippedNonRectorClasses(); + + $this->assertSame('', $this->bufferedOutput->fetch()); } public function testReportsOnlyTrackableUnusedSkips(): void