From bd7bb2ada96caab1f9231692d48a6133320a6adc Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 9 Dec 2025 11:35:28 +0100 Subject: [PATCH 1/2] [dx] warn early about deprecated skipped rules, as not neccessary to skip anymore --- rector.php | 4 +++ src/Console/Command/ListRulesCommand.php | 15 +++++----- src/Console/Command/ProcessCommand.php | 11 +++++++ .../SkippedClassResolver.php | 30 ++++++++++++++----- .../Skipper/SkippedClassResolverTest.php | 18 +++++++++++ 5 files changed, 64 insertions(+), 14 deletions(-) create mode 100644 tests/Skipper/Skipper/SkippedClassResolverTest.php diff --git a/rector.php b/rector.php index 20e9a368c50..933843a13c6 100644 --- a/rector.php +++ b/rector.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use Rector\CodingStyle\Rector\FunctionLike\FunctionLikeToFirstClassCallableRector; use Rector\CodingStyle\Rector\String_\UseClassKeywordForClassNameResolutionRector; use Rector\Config\RectorConfig; use Rector\DeadCode\Rector\ConstFetch\RemovePhpVersionIdCheckRector; @@ -41,6 +42,9 @@ ->withImportNames(removeUnusedImports: true) ->withRules([RemoveRefactorDuplicatedNodeInstanceCheckRector::class, AddSeeTestAnnotationRector::class]) ->withSkip([ + // testing skip of deprecated class + FunctionLikeToFirstClassCallableRector::class, + StringClassNameToClassConstantRector::class, // tests '*/Fixture*', diff --git a/src/Console/Command/ListRulesCommand.php b/src/Console/Command/ListRulesCommand.php index 237a90733e7..965e21de21a 100644 --- a/src/Console/Command/ListRulesCommand.php +++ b/src/Console/Command/ListRulesCommand.php @@ -51,7 +51,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $rectorClasses = $this->resolveRectorClasses(); - $skippedClasses = $this->getSkippedCheckers(); + $skippedClasses = $this->getSkippedRectorClasses(); $outputFormat = $input->getOption(Option::OUTPUT_FORMAT); if ($outputFormat === 'json') { @@ -95,20 +95,21 @@ private function resolveRectorClasses(): array } /** - * @return string[] + * @return array */ - private function getSkippedCheckers(): array + private function getSkippedRectorClasses(): array { - $skippedCheckers = []; - foreach ($this->skippedClassResolver->resolve() as $checkerClass => $fileList) { + $skippedRectorClasses = []; + + foreach ($this->skippedClassResolver->resolve() as $rectorClass => $fileList) { // ignore specific skips if ($fileList !== null) { continue; } - $skippedCheckers[] = $checkerClass; + $skippedRectorClasses[] = $rectorClass; } - return $skippedCheckers; + return $skippedRectorClasses; } } diff --git a/src/Console/Command/ProcessCommand.php b/src/Console/Command/ProcessCommand.php index e6521fdf246..18047df392d 100644 --- a/src/Console/Command/ProcessCommand.php +++ b/src/Console/Command/ProcessCommand.php @@ -19,6 +19,7 @@ use Rector\Exception\ShouldNotHappenException; use Rector\Reporting\DeprecatedRulesReporter; use Rector\Reporting\MissConfigurationReporter; +use Rector\Skipper\SkipCriteriaResolver\SkippedClassResolver; use Rector\StaticReflection\DynamicSourceLocatorDecorator; use Rector\Util\MemoryLimiter; use Rector\ValueObject\Configuration; @@ -45,6 +46,7 @@ public function __construct( private readonly DeprecatedRulesReporter $deprecatedRulesReporter, private readonly MissConfigurationReporter $missConfigurationReporter, private readonly ConfigurationRuleFilter $configurationRuleFilter, + private readonly SkippedClassResolver $skippedClassResolver, ) { parent::__construct(); } @@ -105,6 +107,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->reportLevelOverflow($levelOverflow); } + // 0. warn about skipped rules that are deprecated + if ($this->skippedClassResolver->resolveDeprecatedSkippedClasses() !== []) { + $this->symfonyStyle->warning(sprintf( + 'These rules are skipped, but are deprecated. Most likely you do not need to skip them anymore as not part of any set and remove them: %s* %s', + "\n\n", + implode(' * ', $this->skippedClassResolver->resolveDeprecatedSkippedClasses()) . "\n" + )); + } + // 1. warn about rules registered in both withRules() and sets to avoid bloated rector.php configs $setAndRulesDuplicatedRegistrations = $configuration->getBothSetAndRulesDuplicatedRegistrations(); if ($setAndRulesDuplicatedRegistrations !== []) { diff --git a/src/Skipper/SkipCriteriaResolver/SkippedClassResolver.php b/src/Skipper/SkipCriteriaResolver/SkippedClassResolver.php index 8410da2f2d4..190affbb300 100644 --- a/src/Skipper/SkipCriteriaResolver/SkippedClassResolver.php +++ b/src/Skipper/SkipCriteriaResolver/SkippedClassResolver.php @@ -4,16 +4,32 @@ namespace Rector\Skipper\SkipCriteriaResolver; +use Rector\Configuration\Deprecation\Contract\DeprecatedInterface; use Rector\Configuration\Option; use Rector\Configuration\Parameter\SimpleParameterProvider; use Rector\Testing\PHPUnit\StaticPHPUnitEnvironment; +/** + * @see \Rector\Tests\Skipper\Skipper\SkippedClassResolverTest + */ final class SkippedClassResolver { /** * @var null|array */ - private null|array $skippedClasses = null; + private null|array $skippedClassesToFiles = null; + + /** + * @return array + */ + public function resolveDeprecatedSkippedClasses(): array + { + $skippedClassNames = array_keys($this->resolve()); + + return array_filter($skippedClassNames, function (string $class): bool { + return is_a($class, DeprecatedInterface::class, true); + }); + } /** * @return array @@ -22,16 +38,16 @@ public function resolve(): array { // disable cache in tests if (StaticPHPUnitEnvironment::isPHPUnitRun()) { - $this->skippedClasses = null; + $this->skippedClassesToFiles = null; } // already cached, even only empty array - if ($this->skippedClasses !== null) { - return $this->skippedClasses; + if ($this->skippedClassesToFiles !== null) { + return $this->skippedClassesToFiles; } $skip = SimpleParameterProvider::provideArrayParameter(Option::SKIP); - $this->skippedClasses = []; + $this->skippedClassesToFiles = []; foreach ($skip as $key => $value) { // e.g. [SomeClass::class] → shift values to [SomeClass::class => null] @@ -49,9 +65,9 @@ public function resolve(): array continue; } - $this->skippedClasses[$key] = $value; + $this->skippedClassesToFiles[$key] = $value; } - return $this->skippedClasses; + return $this->skippedClassesToFiles; } } diff --git a/tests/Skipper/Skipper/SkippedClassResolverTest.php b/tests/Skipper/Skipper/SkippedClassResolverTest.php new file mode 100644 index 00000000000..c8980975d00 --- /dev/null +++ b/tests/Skipper/Skipper/SkippedClassResolverTest.php @@ -0,0 +1,18 @@ +make(SkippedClassResolver::class); + + $this->assertSame([], $skippedClassResolver->resolveDeprecatedSkippedClasses()); + } +} From 9350af1055bf80450bc076c99a67a9af60eb191f Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 9 Dec 2025 11:44:43 +0100 Subject: [PATCH 2/2] [comp] Restore FirstClassCallableRector and make it explicitly deprecated, to ease transition --- phpstan.neon | 9 +++++++ rector.php | 4 --- .../ArrayToFirstClassCallableRector.php | 2 +- .../Array_/FirstClassCallableRector.php | 26 +++++++++++++++++++ .../SkippedClassResolver.php | 13 +++++----- 5 files changed, 43 insertions(+), 11 deletions(-) create mode 100644 rules/Php81/Rector/Array_/FirstClassCallableRector.php diff --git a/phpstan.neon b/phpstan.neon index c718ab070bf..4f70a86cf5f 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -433,3 +433,12 @@ parameters: - rules/Php55/Rector/String_/StringClassNameToClassConstantRector.php - rules/Php81/Enum/AttributeName.php + # deprecated rule + - '#Rule Rector\\Php81\\Rector\\Array_\\FirstClassCallableRector must implements Rector\\VersionBonding\\Contract\\MinPhpVersionInterface#' + - '#Register "Rector\\Php81\\Rector\\Array_\\FirstClassCallableRector" service to "php81\.php" config set#' + - '#Access to constant on deprecated class Rector\\Php81\\Rector\\Array_\\FirstClassCallableRector#' + - + message: '#Only abstract classes can be extended#' + path: rules/Php81/Rector/Array_/FirstClassCallableRector.php + + diff --git a/rector.php b/rector.php index 933843a13c6..20e9a368c50 100644 --- a/rector.php +++ b/rector.php @@ -2,7 +2,6 @@ declare(strict_types=1); -use Rector\CodingStyle\Rector\FunctionLike\FunctionLikeToFirstClassCallableRector; use Rector\CodingStyle\Rector\String_\UseClassKeywordForClassNameResolutionRector; use Rector\Config\RectorConfig; use Rector\DeadCode\Rector\ConstFetch\RemovePhpVersionIdCheckRector; @@ -42,9 +41,6 @@ ->withImportNames(removeUnusedImports: true) ->withRules([RemoveRefactorDuplicatedNodeInstanceCheckRector::class, AddSeeTestAnnotationRector::class]) ->withSkip([ - // testing skip of deprecated class - FunctionLikeToFirstClassCallableRector::class, - StringClassNameToClassConstantRector::class, // tests '*/Fixture*', diff --git a/rules/Php81/Rector/Array_/ArrayToFirstClassCallableRector.php b/rules/Php81/Rector/Array_/ArrayToFirstClassCallableRector.php index 6af80bbb1f6..12c8e180cd9 100644 --- a/rules/Php81/Rector/Array_/ArrayToFirstClassCallableRector.php +++ b/rules/Php81/Rector/Array_/ArrayToFirstClassCallableRector.php @@ -31,7 +31,7 @@ * @see RFC https://wiki.php.net/rfc/first_class_callable_syntax * @see \Rector\Tests\Php81\Rector\Array_\ArrayToFirstClassCallableRector\ArrayToFirstClassCallableRectorTest */ -final class ArrayToFirstClassCallableRector extends AbstractRector implements MinPhpVersionInterface +class ArrayToFirstClassCallableRector extends AbstractRector implements MinPhpVersionInterface { public function __construct( private readonly ArrayCallableMethodMatcher $arrayCallableMethodMatcher, diff --git a/rules/Php81/Rector/Array_/FirstClassCallableRector.php b/rules/Php81/Rector/Array_/FirstClassCallableRector.php new file mode 100644 index 00000000000..acd663ad5e5 --- /dev/null +++ b/rules/Php81/Rector/Array_/FirstClassCallableRector.php @@ -0,0 +1,26 @@ + + * @var null|array */ private null|array $skippedClassesToFiles = null; /** - * @return array + * @return array> */ public function resolveDeprecatedSkippedClasses(): array { $skippedClassNames = array_keys($this->resolve()); - return array_filter($skippedClassNames, function (string $class): bool { - return is_a($class, DeprecatedInterface::class, true); - }); + return array_filter( + $skippedClassNames, + fn (string $class): bool => is_a($class, DeprecatedInterface::class, true) + ); } /** - * @return array + * @return array */ public function resolve(): array {