From a12372cbf34be37277852ad352a87da5a1b793e6 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sat, 29 Aug 2026 19:32:13 +0200 Subject: [PATCH 1/9] Deprecate SetList::PHP_* constants in favor of withPhpSets()/withPhpLevel() The per-version PHP set constants are superseded by withPhpSets() and withPhpLevel(), which resolve the target PHP version automatically. Mark them @deprecated to steer users toward the version-driven API. Internal resolution (PhpLevelSetResolver, config/set/level, their tests) still fetches the constants until the single version-driven set replaces them, so those paths are excluded from the deprecation check. Claude-Session: https://claude.ai/code/session_014Q25TQ4b7b13UQQXScu3Fg --- phpstan.neon | 9 ++++++ src/Set/ValueObject/SetList.php | 51 +++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/phpstan.neon b/phpstan.neon index 3a0fd261b44..d156796a28d 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -76,6 +76,15 @@ parameters: paths: - src/Configuration/RectorConfigBuilder.php + # the deprecated SetList::PHP_* constants are still resolved internally, until withPhpSets()/withPhpLevel() replace them + - + identifier: classConstant.deprecated + paths: + - src/Configuration/PhpLevelSetResolver.php + - config/set/level + - tests/Configuration/PhpLevelSetResolverTest.php + - tests/Bridge/SetRectorsResolverTest.php + # the deprecated github/gitlab output formatters are still tested until removed in next minor version - identifier: new.deprecatedClass diff --git a/src/Set/ValueObject/SetList.php b/src/Set/ValueObject/SetList.php index 209a06a8cc7..c48b553f43c 100644 --- a/src/Set/ValueObject/SetList.php +++ b/src/Set/ValueObject/SetList.php @@ -35,38 +35,89 @@ final class SetList */ public const string RECTOR_PRESET = __DIR__ . '/../../../config/set/rector-preset.php'; + /** + * @deprecated Use withPhpSets() or withPhpLevel() instead + */ public const string PHP_52 = __DIR__ . '/../../../config/set/php52.php'; + /** + * @deprecated Use withPhpSets() or withPhpLevel() instead + */ public const string PHP_53 = __DIR__ . '/../../../config/set/php53.php'; + /** + * @deprecated Use withPhpSets() or withPhpLevel() instead + */ public const string PHP_54 = __DIR__ . '/../../../config/set/php54.php'; + /** + * @deprecated Use withPhpSets() or withPhpLevel() instead + */ public const string PHP_55 = __DIR__ . '/../../../config/set/php55.php'; + /** + * @deprecated Use withPhpSets() or withPhpLevel() instead + */ public const string PHP_56 = __DIR__ . '/../../../config/set/php56.php'; + /** + * @deprecated Use withPhpSets() or withPhpLevel() instead + */ public const string PHP_70 = __DIR__ . '/../../../config/set/php70.php'; + /** + * @deprecated Use withPhpSets() or withPhpLevel() instead + */ public const string PHP_71 = __DIR__ . '/../../../config/set/php71.php'; + /** + * @deprecated Use withPhpSets() or withPhpLevel() instead + */ public const string PHP_72 = __DIR__ . '/../../../config/set/php72.php'; + /** + * @deprecated Use withPhpSets() or withPhpLevel() instead + */ public const string PHP_73 = __DIR__ . '/../../../config/set/php73.php'; + /** + * @deprecated Use withPhpSets() or withPhpLevel() instead + */ public const string PHP_74 = __DIR__ . '/../../../config/set/php74.php'; + /** + * @deprecated Use withPhpSets() or withPhpLevel() instead + */ public const string PHP_80 = __DIR__ . '/../../../config/set/php80.php'; + /** + * @deprecated Use withPhpSets() or withPhpLevel() instead + */ public const string PHP_81 = __DIR__ . '/../../../config/set/php81.php'; + /** + * @deprecated Use withPhpSets() or withPhpLevel() instead + */ public const string PHP_82 = __DIR__ . '/../../../config/set/php82.php'; + /** + * @deprecated Use withPhpSets() or withPhpLevel() instead + */ public const string PHP_83 = __DIR__ . '/../../../config/set/php83.php'; + /** + * @deprecated Use withPhpSets() or withPhpLevel() instead + */ public const string PHP_84 = __DIR__ . '/../../../config/set/php84.php'; + /** + * @deprecated Use withPhpSets() or withPhpLevel() instead + */ public const string PHP_85 = __DIR__ . '/../../../config/set/php85.php'; + /** + * @deprecated Use withPhpSets() or withPhpLevel() instead + */ public const string PHP_86 = __DIR__ . '/../../../config/set/php86.php'; public const string PRIVATIZATION = __DIR__ . '/../../../config/set/privatization.php'; From cf70ee045a28017001bb13afd63c179c2e97b620 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sat, 29 Aug 2026 19:47:40 +0200 Subject: [PATCH 2/9] Collapse per-version PHP sets into single php-version-based.php, drop PhpLevelSetResolver withPhpSets() and withPhpLevel() now load one config/set/php-version-based.php set that lists every PHP version rule. The runtime PhpVersionedFilter already gates each rule by MinPhpVersionInterface, so cumulative per-version file selection is redundant - PhpLevelSetResolver is removed. An explicitly picked withPhpSets(phpXX: true) version still caps the set: the ceiling now applies to all rules in PhpVersionedFilter (previously only to polyfilled ones, the rest were capped by file selection), preserving behavior. Claude-Session: https://claude.ai/code/session_014Q25TQ4b7b13UQQXScu3Fg --- config/set/php-version-based.php | 29 +++++++++ phpstan.neon | 4 +- src/Configuration/PhpLevelSetResolver.php | 59 ------------------- src/Configuration/RectorConfigBuilder.php | 27 +++++---- src/VersionBonding/PhpVersionedFilter.php | 12 +++- tests/Bridge/SetRectorsResolverTest.php | 38 +++++------- .../Configuration/PhpLevelSetResolverTest.php | 27 --------- 7 files changed, 67 insertions(+), 129 deletions(-) create mode 100644 config/set/php-version-based.php delete mode 100644 src/Configuration/PhpLevelSetResolver.php delete mode 100644 tests/Configuration/PhpLevelSetResolverTest.php diff --git a/config/set/php-version-based.php b/config/set/php-version-based.php new file mode 100644 index 00000000000..a9a100288f0 --- /dev/null +++ b/config/set/php-version-based.php @@ -0,0 +1,29 @@ +sets([ + __DIR__ . '/php52.php', + __DIR__ . '/php53.php', + __DIR__ . '/php54.php', + __DIR__ . '/php55.php', + __DIR__ . '/php56.php', + __DIR__ . '/php70.php', + __DIR__ . '/php71.php', + __DIR__ . '/php72.php', + __DIR__ . '/php73.php', + __DIR__ . '/php74.php', + __DIR__ . '/php80.php', + __DIR__ . '/php81.php', + __DIR__ . '/php82.php', + __DIR__ . '/php83.php', + __DIR__ . '/php84.php', + __DIR__ . '/php85.php', + __DIR__ . '/php86.php', + ]); +}; diff --git a/phpstan.neon b/phpstan.neon index d156796a28d..6923a5ea250 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -76,13 +76,11 @@ parameters: paths: - src/Configuration/RectorConfigBuilder.php - # the deprecated SetList::PHP_* constants are still resolved internally, until withPhpSets()/withPhpLevel() replace them + # the deprecated SetList::PHP_* constants are still resolved internally by the level sets and their tests - identifier: classConstant.deprecated paths: - - src/Configuration/PhpLevelSetResolver.php - config/set/level - - tests/Configuration/PhpLevelSetResolverTest.php - tests/Bridge/SetRectorsResolverTest.php # the deprecated github/gitlab output formatters are still tested until removed in next minor version diff --git a/src/Configuration/PhpLevelSetResolver.php b/src/Configuration/PhpLevelSetResolver.php deleted file mode 100644 index 001806d2088..00000000000 --- a/src/Configuration/PhpLevelSetResolver.php +++ /dev/null @@ -1,59 +0,0 @@ - - */ - private const array VERSION_LOWER_BOUND_CONFIGS = [ - PhpVersion::PHP_52 => SetList::PHP_52, - PhpVersion::PHP_53 => SetList::PHP_53, - PhpVersion::PHP_54 => SetList::PHP_54, - PhpVersion::PHP_55 => SetList::PHP_55, - PhpVersion::PHP_56 => SetList::PHP_56, - PhpVersion::PHP_70 => SetList::PHP_70, - PhpVersion::PHP_71 => SetList::PHP_71, - PhpVersion::PHP_72 => SetList::PHP_72, - PhpVersion::PHP_73 => SetList::PHP_73, - PhpVersion::PHP_74 => SetList::PHP_74, - PhpVersion::PHP_80 => SetList::PHP_80, - PhpVersion::PHP_81 => SetList::PHP_81, - PhpVersion::PHP_82 => SetList::PHP_82, - PhpVersion::PHP_83 => SetList::PHP_83, - PhpVersion::PHP_84 => SetList::PHP_84, - PhpVersion::PHP_85 => SetList::PHP_85, - PhpVersion::PHP_86 => SetList::PHP_86, - ]; - - /** - * @param PhpVersion::* $phpVersion - * @return string[] - */ - public static function resolveFromPhpVersion(int $phpVersion): array - { - $configFilePaths = []; - - foreach (self::VERSION_LOWER_BOUND_CONFIGS as $versionLowerBound => $phpSetFilePath) { - if ($versionLowerBound <= $phpVersion) { - $configFilePaths[] = $phpSetFilePath; - } - } - - Assert::allFileExists($configFilePaths); - - return $configFilePaths; - } -} diff --git a/src/Configuration/RectorConfigBuilder.php b/src/Configuration/RectorConfigBuilder.php index e11f2105a0f..8d38f75b8b1 100644 --- a/src/Configuration/RectorConfigBuilder.php +++ b/src/Configuration/RectorConfigBuilder.php @@ -166,6 +166,11 @@ final class RectorConfigBuilder private ?string $editorUrl = null; + /** + * All PHP version rules in one set; each rule gates itself by PHP version at runtime + */ + private const string PHP_VERSION_BASED_SET = __DIR__ . '/../../config/set/php-version-based.php'; + private ?bool $isWithPhpSetsUsed = null; private ?bool $isWithPhpLevelUsed = null; @@ -551,15 +556,16 @@ public function withPhpSets( ); } - // no version picked, resolve it from the project composer.json + // no version picked, target the project composer.json PHP version; validated here, gated at runtime if ($pickedPhpVersions === []) { - return $this->addPhpLevelSets(ComposerJsonPhpVersionResolver::resolveFromCwdOrFail()); + ComposerJsonPhpVersionResolver::resolveFromCwdOrFail(); + return $this->addPhpLevelSets(); } // explicitly picked version is a ceiling, even for polyfilled rules $this->pickedPhpSetsVersion = $pickedPhpVersions[0]; - return $this->addPhpLevelSets($pickedPhpVersions[0]); + return $this->addPhpLevelSets(); } #[Deprecated(message: 'Use "withPhpLevel()" instead, it raises PHP level one rule at a time.')] @@ -962,12 +968,10 @@ public function withPhpLevel(int $level): self $this->isWithPhpLevelUsed = true; - $phpVersion = ComposerJsonPhpVersionResolver::resolveFromCwdOrFail(); - $setRectorsResolver = new SetRectorsResolver(); - $setFilePaths = PhpLevelSetResolver::resolveFromPhpVersion($phpVersion); - - $rectorRulesWithConfiguration = $setRectorsResolver->resolveFromFilePathsIncludingConfiguration($setFilePaths); + $rectorRulesWithConfiguration = $setRectorsResolver->resolveFromFilePathIncludingConfiguration( + self::PHP_VERSION_BASED_SET + ); foreach ($rectorRulesWithConfiguration as $position => $rectorRuleWithConfiguration) { // add rules until level is reached @@ -1137,14 +1141,11 @@ public function withSetProviders(): self return $this; } - /** - * @param PhpVersion::* $phpVersion - */ - private function addPhpLevelSets(int $phpVersion): self + private function addPhpLevelSets(): self { $this->isWithPhpSetsUsed = true; - $this->sets = array_merge($this->sets, PhpLevelSetResolver::resolveFromPhpVersion($phpVersion)); + $this->sets[] = self::PHP_VERSION_BASED_SET; return $this; } diff --git a/src/VersionBonding/PhpVersionedFilter.php b/src/VersionBonding/PhpVersionedFilter.php index 478812b2037..c387fa16c6f 100644 --- a/src/VersionBonding/PhpVersionedFilter.php +++ b/src/VersionBonding/PhpVersionedFilter.php @@ -50,9 +50,15 @@ public function filter(array $rectors): array continue; } - $maxPhpVersion = $rector instanceof RelatedPolyfillInterface && $ceilingPhpVersion !== null - ? $ceilingPhpVersion - : $minProjectPhpVersion; + // an explicitly picked withPhpSets() version caps the whole set: + // polyfilled rules up to the ceiling, the rest up to the lower of ceiling and project version + if ($ceilingPhpVersion !== null) { + $maxPhpVersion = $rector instanceof RelatedPolyfillInterface + ? $ceilingPhpVersion + : min($ceilingPhpVersion, $minProjectPhpVersion); + } else { + $maxPhpVersion = $minProjectPhpVersion; + } // does satisfy version? → include if ($rector->provideMinPhpVersion() <= $maxPhpVersion) { diff --git a/tests/Bridge/SetRectorsResolverTest.php b/tests/Bridge/SetRectorsResolverTest.php index 32cf9a64420..24fb0508187 100644 --- a/tests/Bridge/SetRectorsResolverTest.php +++ b/tests/Bridge/SetRectorsResolverTest.php @@ -6,11 +6,8 @@ use PHPUnit\Framework\TestCase; use Rector\Bridge\SetRectorsResolver; -use Rector\Configuration\PhpLevelSetResolver; use Rector\Contract\Rector\RectorInterface; -use Rector\Php\PhpVersionResolver\ComposerJsonPhpVersionResolver; use Rector\Set\ValueObject\SetList; -use Rector\ValueObject\PhpVersion; final class SetRectorsResolverTest extends TestCase { @@ -21,31 +18,16 @@ protected function setUp(): void $this->setRectorsResolver = new SetRectorsResolver(); } - public function testResolveFromFilePathForPhpVersion(): void + public function testResolvePhpVersionBasedSet(): void { - $configFilePaths = PhpLevelSetResolver::resolveFromPhpVersion(PhpVersion::PHP_70); - $this->assertCount(6, $configFilePaths); - $this->assertContainsOnlyString($configFilePaths); - - foreach ($configFilePaths as $configFilePath) { - $this->assertFileExists($configFilePath); - } - } - - public function testResolveFromFilePathForPhpLevel(): void - { - $projectPhpVersion = ComposerJsonPhpVersionResolver::resolve(__DIR__ . '/Fixture/some-composer.json'); - - $this->assertIsInt($projectPhpVersion); - $this->assertSame(PhpVersion::PHP_73, $projectPhpVersion); - - $configFilePaths = PhpLevelSetResolver::resolveFromPhpVersion($projectPhpVersion); - $this->assertCount(9, $configFilePaths); + $phpVersionBasedSetFilePath = dirname(__DIR__, 2) . '/config/set/php-version-based.php'; $rectorRulesWithConfiguration = $this->setRectorsResolver->resolveFromFilePathsIncludingConfiguration( - $configFilePaths + [$phpVersionBasedSetFilePath] ); - $this->assertCount(63, $rectorRulesWithConfiguration); + + $this->assertNotEmpty($rectorRulesWithConfiguration); + $this->assertContainsOnlyRules($rectorRulesWithConfiguration); } public function testResolveWithConfiguration(): void @@ -58,6 +40,14 @@ public function testResolveWithConfiguration(): void $this->assertArrayHasKey(0, $rectorRulesWithConfiguration); $this->assertArrayHasKey(8, $rectorRulesWithConfiguration); + $this->assertContainsOnlyRules($rectorRulesWithConfiguration); + } + + /** + * @param array|array, mixed[]>> $rectorRulesWithConfiguration + */ + private function assertContainsOnlyRules(array $rectorRulesWithConfiguration): void + { foreach ($rectorRulesWithConfiguration as $rectorRuleWithConfiguration) { if (is_string($rectorRuleWithConfiguration)) { $this->assertTrue(is_a($rectorRuleWithConfiguration, RectorInterface::class, true)); diff --git a/tests/Configuration/PhpLevelSetResolverTest.php b/tests/Configuration/PhpLevelSetResolverTest.php deleted file mode 100644 index af129f2fe30..00000000000 --- a/tests/Configuration/PhpLevelSetResolverTest.php +++ /dev/null @@ -1,27 +0,0 @@ -assertCount(5, $phpSetFiles); - - $this->assertSame([ - SetList::PHP_52, - SetList::PHP_53, - SetList::PHP_54, - SetList::PHP_55, - SetList::PHP_56, - ], $phpSetFiles); - } -} From 97661abd1569d6edee67361c64c3e7b3056c49e0 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sat, 29 Aug 2026 20:26:07 +0200 Subject: [PATCH 3/9] List all rules directly in php-version-based.php, add ruleWithConfigurationPhpVersionBound() The single set now enumerates every PHP version rule in one rules() call with per-version comment separators, mirroring rector-symfony composer-based.php. Configured rules that span multiple versions (RenameFunctionRector, RemoveFuncCallArgRector, RenameCastRector) are registered via the new RectorConfig::ruleWithConfigurationPhpVersionBound(), which applies a configuration only when the target PHP version is high enough - mirroring ruleWithConfigurationComposerVersionBound() for package versions. An explicitly picked withPhpSets(phpXX) version is the target, otherwise the project composer.json PHP version is used. Claude-Session: https://claude.ai/code/session_014Q25TQ4b7b13UQQXScu3Fg --- config/set/php-version-based.php | 608 ++++++++++++++++++++++++++++++- src/Config/RectorConfig.php | 39 ++ 2 files changed, 627 insertions(+), 20 deletions(-) diff --git a/config/set/php-version-based.php b/config/set/php-version-based.php index a9a100288f0..37cf0f579ed 100644 --- a/config/set/php-version-based.php +++ b/config/set/php-version-based.php @@ -2,28 +2,596 @@ declare(strict_types=1); +use PhpParser\Node\Expr\Cast\Bool_; +use PhpParser\Node\Expr\Cast\Double; +use PhpParser\Node\Expr\Cast\Int_; +use PhpParser\Node\Expr\Cast\String_; +use Rector\Arguments\Rector\ClassMethod\ArgumentAdderRector; +use Rector\Arguments\Rector\FuncCall\FunctionArgumentDefaultValueReplacerRector; +use Rector\Arguments\ValueObject\ArgumentAdder; +use Rector\Arguments\ValueObject\ReplaceFuncCallArgumentDefaultValue; +use Rector\CodeQuality\Rector\ClassMethod\OptionalParametersAfterRequiredRector; +use Rector\CodingStyle\Rector\ArrowFunction\ArrowFunctionDelegatingCallToFirstClassCallableRector; +use Rector\CodingStyle\Rector\Closure\ClosureDelegatingCallToFirstClassCallableRector; +use Rector\CodingStyle\Rector\FuncCall\ClosureFromCallableToFirstClassCallableRector; +use Rector\CodingStyle\Rector\FuncCall\ConsistentImplodeRector; +use Rector\CodingStyle\Rector\FuncCall\FunctionFirstClassCallableRector; use Rector\Config\RectorConfig; +use Rector\DeadCode\Rector\StaticCall\RemoveParentCallWithoutParentRector; +use Rector\Php52\Rector\Property\VarToPublicPropertyRector; +use Rector\Php52\Rector\Switch_\ContinueToBreakInSwitchRector; +use Rector\Php53\Rector\FuncCall\DirNameFileConstantToDirConstantRector; +use Rector\Php53\Rector\Ternary\TernaryToElvisRector; +use Rector\Php53\Rector\Variable\ReplaceHttpServerVarsByServerRector; +use Rector\Php54\Rector\Array_\LongArrayToShortArrayRector; +use Rector\Php54\Rector\Break_\RemoveZeroBreakContinueRector; +use Rector\Php54\Rector\FuncCall\RemoveReferenceFromCallRector; +use Rector\Php55\Rector\Class_\ClassConstantToSelfClassRector; +use Rector\Php55\Rector\ClassConstFetch\StaticToSelfOnFinalClassRector; +use Rector\Php55\Rector\FuncCall\GetCalledClassToSelfClassRector; +use Rector\Php55\Rector\FuncCall\GetCalledClassToStaticClassRector; +use Rector\Php55\Rector\FuncCall\PregReplaceEModifierRector; +use Rector\Php55\Rector\String_\StringClassNameToClassConstantRector; +use Rector\Php56\Rector\FuncCall\PowToExpRector; +use Rector\Php70\Rector\Assign\ListSplitStringRector; +use Rector\Php70\Rector\Assign\ListSwapArrayOrderRector; +use Rector\Php70\Rector\Break_\BreakNotInLoopOrSwitchToReturnRector; +use Rector\Php70\Rector\ClassMethod\Php4ConstructorRector; +use Rector\Php70\Rector\FuncCall\CallUserMethodRector; +use Rector\Php70\Rector\FuncCall\EregToPregMatchRector; +use Rector\Php70\Rector\FuncCall\MultiDirnameRector; +use Rector\Php70\Rector\FuncCall\RandomFunctionRector; +use Rector\Php70\Rector\FuncCall\RenameMktimeWithoutArgsToTimeRector; +use Rector\Php70\Rector\FunctionLike\ExceptionHandlerTypehintRector; +use Rector\Php70\Rector\If_\IfToSpaceshipRector; +use Rector\Php70\Rector\List_\EmptyListRector; +use Rector\Php70\Rector\MethodCall\ThisCallOnStaticMethodToStaticCallRector; +use Rector\Php70\Rector\StmtsAwareInterface\IfIssetToCoalescingRector; +use Rector\Php70\Rector\Switch_\ReduceMultipleDefaultSwitchRector; +use Rector\Php70\Rector\Ternary\TernaryToNullCoalescingRector; +use Rector\Php70\Rector\Ternary\TernaryToSpaceshipRector; +use Rector\Php70\Rector\Variable\WrapVariableVariableNameInCurlyBracesRector; +use Rector\Php71\Rector\Assign\AssignArrayToStringRector; +use Rector\Php71\Rector\BinaryOp\BinaryOpBetweenNumberAndStringRector; +use Rector\Php71\Rector\BooleanOr\IsIterableRector; +use Rector\Php71\Rector\FuncCall\RemoveExtraParametersRector; +use Rector\Php71\Rector\List_\ListToArrayDestructRector; +use Rector\Php71\Rector\TryCatch\MultiExceptionCatchRector; +use Rector\Php72\Rector\Assign\ListEachRector; +use Rector\Php72\Rector\Assign\ReplaceEachAssignmentWithKeyCurrentRector; +use Rector\Php72\Rector\FuncCall\CreateFunctionToAnonymousFunctionRector; +use Rector\Php72\Rector\FuncCall\GetClassOnNullRector; +use Rector\Php72\Rector\FuncCall\ParseStrWithResultArgumentRector; +use Rector\Php72\Rector\FuncCall\StringifyDefineRector; +use Rector\Php72\Rector\FuncCall\StringsAssertNakedRector; +use Rector\Php72\Rector\Unset_\UnsetCastRector; +use Rector\Php72\Rector\While_\WhileEachToForeachRector; +use Rector\Php73\Rector\BooleanOr\IsCountableRector; +use Rector\Php73\Rector\ConstFetch\SensitiveConstantNameRector; +use Rector\Php73\Rector\FuncCall\ArrayKeyFirstLastRector; +use Rector\Php73\Rector\FuncCall\ArrayKeysToArrayKeyFirstLastRector; +use Rector\Php73\Rector\FuncCall\RegexDashEscapeRector; +use Rector\Php73\Rector\FuncCall\SensitiveDefineRector; +use Rector\Php73\Rector\FuncCall\SetCookieRector; +use Rector\Php73\Rector\FuncCall\StringifyStrNeedlesRector; +use Rector\Php73\Rector\String_\SensitiveHereNowDocRector; +use Rector\Php74\Rector\ArrayDimFetch\CurlyToSquareBracketArrayStringRector; +use Rector\Php74\Rector\Assign\NullCoalescingOperatorRector; +use Rector\Php74\Rector\Closure\ClosureToArrowFunctionRector; +use Rector\Php74\Rector\FuncCall\ArrayKeyExistsOnPropertyRector; +use Rector\Php74\Rector\FuncCall\FilterVarToAddSlashesRector; +use Rector\Php74\Rector\FuncCall\HebrevcToNl2brHebrevRector; +use Rector\Php74\Rector\FuncCall\MbStrrposEncodingArgumentPositionRector; +use Rector\Php74\Rector\FuncCall\MoneyFormatToNumberFormatRector; +use Rector\Php74\Rector\FuncCall\RestoreIncludePathToIniRestoreRector; +use Rector\Php74\Rector\If_\IfToNullCoalescingAssignRector; +use Rector\Php74\Rector\Property\RestoreDefaultNullToNullableTypePropertyRector; +use Rector\Php74\Rector\StaticCall\ExportToReflectionFunctionRector; +use Rector\Php74\Rector\Ternary\ParenthesizeNestedTernaryRector; +use Rector\Php80\Rector\Catch_\RemoveUnusedVariableInCatchRector; +use Rector\Php80\Rector\Class_\ClassPropertyAssignToConstructorPromotionRector; +use Rector\Php80\Rector\Class_\StringableForToStringRector; +use Rector\Php80\Rector\ClassConstFetch\ClassOnThisVariableObjectRector; +use Rector\Php80\Rector\ClassMethod\AddParamBasedOnParentClassMethodRector; +use Rector\Php80\Rector\ClassMethod\FinalPrivateToPrivateVisibilityRector; +use Rector\Php80\Rector\ClassMethod\SetStateToStaticRector; +use Rector\Php80\Rector\FuncCall\ClassOnObjectRector; +use Rector\Php80\Rector\Identical\StrEndsWithRector; +use Rector\Php80\Rector\Identical\StrStartsWithRector; +use Rector\Php80\Rector\NotIdentical\StrContainsRector; +use Rector\Php80\Rector\Switch_\ChangeSwitchToMatchRector; +use Rector\Php80\Rector\Ternary\GetDebugTypeRector; +use Rector\Php81\Rector\Array_\ArrayToFirstClassCallableRector; +use Rector\Php81\Rector\Class_\MyCLabsClassToEnumRector; +use Rector\Php81\Rector\Class_\SpatieEnumClassToEnumRector; +use Rector\Php81\Rector\FuncCall\NullToStrictIntPregSlitFuncCallLimitArgRector; +use Rector\Php81\Rector\MethodCall\MyCLabsMethodCallToEnumConstRector; +use Rector\Php81\Rector\MethodCall\RemoveReflectionSetAccessibleCallsRector; +use Rector\Php81\Rector\MethodCall\SpatieEnumMethodCallToEnumConstRector; +use Rector\Php81\Rector\New_\MyCLabsConstructorCallToEnumFromRector; +use Rector\Php81\Rector\Property\ReadOnlyPropertyRector; +use Rector\Php82\Rector\Class_\ReadOnlyClassRector; +use Rector\Php82\Rector\Encapsed\VariableInStringInterpolationFixerRector; +use Rector\Php82\Rector\FuncCall\Utf8DecodeEncodeToMbConvertEncodingRector; +use Rector\Php82\Rector\New_\FilesystemIteratorSkipDotsRector; +use Rector\Php83\Rector\BooleanAnd\JsonValidateRector; +use Rector\Php83\Rector\Class_\ReadOnlyAnonymousClassRector; +use Rector\Php83\Rector\ClassConst\AddTypeToConstRector; +use Rector\Php83\Rector\FuncCall\CombineHostPortLdapUriRector; +use Rector\Php83\Rector\FuncCall\DynamicClassConstFetchRector; +use Rector\Php83\Rector\FuncCall\RemoveGetClassGetParentClassNoArgsRector; +use Rector\Php84\Rector\Foreach_\ForeachToArrayAllRector; +use Rector\Php84\Rector\Foreach_\ForeachToArrayAnyRector; +use Rector\Php84\Rector\Foreach_\ForeachToArrayFindKeyRector; +use Rector\Php84\Rector\Foreach_\ForeachToArrayFindRector; +use Rector\Php84\Rector\FuncCall\AddEscapeArgumentRector; +use Rector\Php84\Rector\FuncCall\RoundingModeEnumRector; +use Rector\Php84\Rector\MethodCall\NewMethodCallWithoutParenthesesRector; +use Rector\Php84\Rector\Param\ExplicitNullableParamTypeRector; +use Rector\Php85\Rector\ArrayDimFetch\ArrayFirstLastRector; +use Rector\Php85\Rector\Class_\SleepToSerializeRector; +use Rector\Php85\Rector\Class_\WakeupToUnserializeRector; +use Rector\Php85\Rector\ClassMethod\NullDebugInfoReturnRector; +use Rector\Php85\Rector\FuncCall\ArrayKeyExistsNullToEmptyStringRector; +use Rector\Php85\Rector\FuncCall\ChrArgModuloRector; +use Rector\Php85\Rector\FuncCall\OrdSingleByteRector; +use Rector\Php85\Rector\FuncCall\RemoveFinfoBufferContextArgRector; +use Rector\Php85\Rector\Property\AddOverrideAttributeToOverriddenPropertiesRector; +use Rector\Php85\Rector\ShellExec\ShellExecFunctionCallOverBackticksRector; +use Rector\Php85\Rector\Switch_\ColonAfterSwitchCaseRector; +use Rector\Php86\Rector\FuncCall\MinMaxToClampRector; +use Rector\Removing\Rector\FuncCall\RemoveFuncCallArgRector; +use Rector\Removing\Rector\FuncCall\RemoveFuncCallRector; +use Rector\Removing\ValueObject\RemoveFuncCallArg; +use Rector\Renaming\Rector\Cast\RenameCastRector; +use Rector\Renaming\Rector\ClassConstFetch\RenameClassConstFetchRector; +use Rector\Renaming\Rector\ConstFetch\RenameConstantRector; +use Rector\Renaming\Rector\FuncCall\RenameFunctionRector; +use Rector\Renaming\Rector\MethodCall\RenameMethodRector; +use Rector\Renaming\ValueObject\MethodCallRename; +use Rector\Renaming\ValueObject\RenameCast; +use Rector\Renaming\ValueObject\RenameClassAndConstFetch; +use Rector\Transform\Rector\StaticCall\StaticCallToFuncCallRector; +use Rector\Transform\ValueObject\StaticCallToFuncCall; +use Rector\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector; +use Rector\ValueObject\PhpVersion; +use Rector\Visibility\Rector\ClassMethod\ExplicitPublicClassMethodRector; -// all PHP version rules, oldest to newest; each rule gates itself at runtime -// via MinPhpVersionInterface, so only rules up to the target PHP version apply +// all PHP version rules, oldest to newest; each rule gates itself at runtime by PHP version, +// so only rules up to the target PHP version apply return static function (RectorConfig $rectorConfig): void { - $rectorConfig->sets([ - __DIR__ . '/php52.php', - __DIR__ . '/php53.php', - __DIR__ . '/php54.php', - __DIR__ . '/php55.php', - __DIR__ . '/php56.php', - __DIR__ . '/php70.php', - __DIR__ . '/php71.php', - __DIR__ . '/php72.php', - __DIR__ . '/php73.php', - __DIR__ . '/php74.php', - __DIR__ . '/php80.php', - __DIR__ . '/php81.php', - __DIR__ . '/php82.php', - __DIR__ . '/php83.php', - __DIR__ . '/php84.php', - __DIR__ . '/php85.php', - __DIR__ . '/php86.php', + $rectorConfig->rules([ + // PHP 5.2 + VarToPublicPropertyRector::class, + ContinueToBreakInSwitchRector::class, + + // PHP 5.3 + ExplicitPublicClassMethodRector::class, + TernaryToElvisRector::class, + DirNameFileConstantToDirConstantRector::class, + ReplaceHttpServerVarsByServerRector::class, + + // PHP 5.4 + LongArrayToShortArrayRector::class, + RemoveReferenceFromCallRector::class, + RemoveZeroBreakContinueRector::class, + + // PHP 5.5 + StringClassNameToClassConstantRector::class, + ClassConstantToSelfClassRector::class, + PregReplaceEModifierRector::class, + GetCalledClassToSelfClassRector::class, + GetCalledClassToStaticClassRector::class, + StaticToSelfOnFinalClassRector::class, + + // PHP 5.6 + PowToExpRector::class, + + // PHP 7.0 + Php4ConstructorRector::class, + TernaryToNullCoalescingRector::class, + RandomFunctionRector::class, + ExceptionHandlerTypehintRector::class, + MultiDirnameRector::class, + ListSplitStringRector::class, + EmptyListRector::class, + ListSwapArrayOrderRector::class, + CallUserMethodRector::class, + EregToPregMatchRector::class, + ReduceMultipleDefaultSwitchRector::class, + TernaryToSpaceshipRector::class, + WrapVariableVariableNameInCurlyBracesRector::class, + IfToSpaceshipRector::class, + ThisCallOnStaticMethodToStaticCallRector::class, + BreakNotInLoopOrSwitchToReturnRector::class, + RenameMktimeWithoutArgsToTimeRector::class, + IfIssetToCoalescingRector::class, + + // PHP 7.1 + IsIterableRector::class, + MultiExceptionCatchRector::class, + AssignArrayToStringRector::class, + RemoveExtraParametersRector::class, + BinaryOpBetweenNumberAndStringRector::class, + ListToArrayDestructRector::class, + + // PHP 7.2 + GetClassOnNullRector::class, + ParseStrWithResultArgumentRector::class, + StringsAssertNakedRector::class, + CreateFunctionToAnonymousFunctionRector::class, + StringifyDefineRector::class, + WhileEachToForeachRector::class, + ListEachRector::class, + ReplaceEachAssignmentWithKeyCurrentRector::class, + UnsetCastRector::class, + + // PHP 7.3 + StringifyStrNeedlesRector::class, + RegexDashEscapeRector::class, + SetCookieRector::class, + IsCountableRector::class, + ArrayKeyFirstLastRector::class, + ArrayKeysToArrayKeyFirstLastRector::class, + SensitiveDefineRector::class, + SensitiveConstantNameRector::class, + SensitiveHereNowDocRector::class, + + // PHP 7.4 + ArrayKeyExistsOnPropertyRector::class, + FilterVarToAddSlashesRector::class, + ExportToReflectionFunctionRector::class, + MbStrrposEncodingArgumentPositionRector::class, + NullCoalescingOperatorRector::class, + IfToNullCoalescingAssignRector::class, + ClosureToArrowFunctionRector::class, + RestoreDefaultNullToNullableTypePropertyRector::class, + CurlyToSquareBracketArrayStringRector::class, + MoneyFormatToNumberFormatRector::class, + ParenthesizeNestedTernaryRector::class, + RestoreIncludePathToIniRestoreRector::class, + HebrevcToNl2brHebrevRector::class, + + // PHP 8.0 + StrContainsRector::class, + StrStartsWithRector::class, + StrEndsWithRector::class, + StringableForToStringRector::class, + ClassOnObjectRector::class, + GetDebugTypeRector::class, + RemoveUnusedVariableInCatchRector::class, + ClassPropertyAssignToConstructorPromotionRector::class, + ChangeSwitchToMatchRector::class, + RemoveParentCallWithoutParentRector::class, + SetStateToStaticRector::class, + FinalPrivateToPrivateVisibilityRector::class, + AddParamBasedOnParentClassMethodRector::class, + ClassOnThisVariableObjectRector::class, + ConsistentImplodeRector::class, + OptionalParametersAfterRequiredRector::class, + + // PHP 8.1 + ReturnNeverTypeRector::class, + MyCLabsClassToEnumRector::class, + MyCLabsMethodCallToEnumConstRector::class, + MyCLabsConstructorCallToEnumFromRector::class, + ReadOnlyPropertyRector::class, + SpatieEnumClassToEnumRector::class, + SpatieEnumMethodCallToEnumConstRector::class, + NullToStrictIntPregSlitFuncCallLimitArgRector::class, + ArrayToFirstClassCallableRector::class, + ArrowFunctionDelegatingCallToFirstClassCallableRector::class, + ClosureDelegatingCallToFirstClassCallableRector::class, + ClosureFromCallableToFirstClassCallableRector::class, + FunctionFirstClassCallableRector::class, + RemoveReflectionSetAccessibleCallsRector::class, + + // PHP 8.2 + ReadOnlyClassRector::class, + Utf8DecodeEncodeToMbConvertEncodingRector::class, + FilesystemIteratorSkipDotsRector::class, + VariableInStringInterpolationFixerRector::class, + + // PHP 8.3 + AddTypeToConstRector::class, + CombineHostPortLdapUriRector::class, + RemoveGetClassGetParentClassNoArgsRector::class, + ReadOnlyAnonymousClassRector::class, + DynamicClassConstFetchRector::class, + JsonValidateRector::class, + + // PHP 8.4 + ExplicitNullableParamTypeRector::class, + RoundingModeEnumRector::class, + AddEscapeArgumentRector::class, + NewMethodCallWithoutParenthesesRector::class, + ForeachToArrayFindRector::class, + ForeachToArrayFindKeyRector::class, + ForeachToArrayAllRector::class, + ForeachToArrayAnyRector::class, + + // PHP 8.5 + ArrayFirstLastRector::class, + RemoveFinfoBufferContextArgRector::class, + NullDebugInfoReturnRector::class, + ColonAfterSwitchCaseRector::class, + ArrayKeyExistsNullToEmptyStringRector::class, + ChrArgModuloRector::class, + SleepToSerializeRector::class, + OrdSingleByteRector::class, + WakeupToUnserializeRector::class, + ShellExecFunctionCallOverBackticksRector::class, + AddOverrideAttributeToOverriddenPropertiesRector::class, + + // PHP 8.6 + MinMaxToClampRector::class, ]); + + // configured rules, each bound to the PHP version its configuration targets + + // PHP 5.2 + $rectorConfig->ruleWithConfigurationPhpVersionBound(RemoveFuncCallArgRector::class, [ + // see https://www.php.net/manual/en/function.ldap-first-attribute.php + new RemoveFuncCallArg('ldap_first_attribute', 2), + ], PhpVersion::PHP_52); + + // PHP 5.4 + $rectorConfig->ruleWithConfigurationPhpVersionBound(RenameFunctionRector::class, [ + 'mysqli_param_count' => 'mysqli_stmt_param_count', + ], PhpVersion::PHP_54); + + // PHP 5.6 + $rectorConfig->ruleWithConfigurationPhpVersionBound(RenameFunctionRector::class, [ + 'mcrypt_generic_end' => 'mcrypt_generic_deinit', + 'set_socket_blocking' => 'stream_set_blocking', + 'ocibindbyname' => 'oci_bind_by_name', + 'ocicancel' => 'oci_cancel', + 'ocicolumnisnull' => 'oci_field_is_null', + 'ocicolumnname' => 'oci_field_name', + 'ocicolumnprecision' => 'oci_field_precision', + 'ocicolumnscale' => 'oci_field_scale', + 'ocicolumnsize' => 'oci_field_size', + 'ocicolumntype' => 'oci_field_type', + 'ocicolumntyperaw' => 'oci_field_type_raw', + 'ocicommit' => 'oci_commit', + 'ocidefinebyname' => 'oci_define_by_name', + 'ocierror' => 'oci_error', + 'ociexecute' => 'oci_execute', + 'ocifetch' => 'oci_fetch', + 'ocifetchstatement' => 'oci_fetch_all', + 'ocifreecursor' => 'oci_free_statement', + 'ocifreestatement' => 'oci_free_statement', + 'ociinternaldebug' => 'oci_internal_debug', + 'ocilogoff' => 'oci_close', + 'ocilogon' => 'oci_connect', + 'ocinewcollection' => 'oci_new_collection', + 'ocinewcursor' => 'oci_new_cursor', + 'ocinewdescriptor' => 'oci_new_descriptor', + 'ocinlogon' => 'oci_new_connect', + 'ocinumcols' => 'oci_num_fields', + 'ociparse' => 'oci_parse', + 'ociplogon' => 'oci_pconnect', + 'ociresult' => 'oci_result', + 'ocirollback' => 'oci_rollback', + 'ocirowcount' => 'oci_num_rows', + 'ociserverversion' => 'oci_server_version', + 'ocisetprefetch' => 'oci_set_prefetch', + 'ocistatementtype' => 'oci_statement_type', + ], PhpVersion::PHP_56); + + // PHP 7.2 + $rectorConfig->ruleWithConfigurationPhpVersionBound(RenameFunctionRector::class, [ + # and imagewbmp + 'jpeg2wbmp' => 'imagecreatefromjpeg', + # or imagewbmp + 'png2wbmp' => 'imagecreatefrompng', + # migration72.deprecated.gmp_random-function + # http://php.net/manual/en/migration72.deprecated.php + # or gmp_random_range + 'gmp_random' => 'gmp_random_bits', + 'read_exif_data' => 'exif_read_data', + ], PhpVersion::PHP_72); + + // PHP 7.3 + $rectorConfig->ruleWithConfigurationPhpVersionBound(RenameFunctionRector::class, [ + # https://wiki.php.net/rfc/deprecations_php_7_3 + 'image2wbmp' => 'imagewbmp', + 'mbregex_encoding' => 'mb_regex_encoding', + 'mbereg' => 'mb_ereg', + 'mberegi' => 'mb_eregi', + 'mbereg_replace' => 'mb_ereg_replace', + 'mberegi_replace' => 'mb_eregi_replace', + 'mbsplit' => 'mb_split', + 'mbereg_match' => 'mb_ereg_match', + 'mbereg_search' => 'mb_ereg_search', + 'mbereg_search_pos' => 'mb_ereg_search_pos', + 'mbereg_search_regs' => 'mb_ereg_search_regs', + 'mbereg_search_init' => 'mb_ereg_search_init', + 'mbereg_search_getregs' => 'mb_ereg_search_getregs', + 'mbereg_search_getpos' => 'mb_ereg_search_getpos', + ], PhpVersion::PHP_73); + + // PHP 7.4 + $rectorConfig->ruleWithConfigurationPhpVersionBound(RenameFunctionRector::class, [ + # the_real_type + # https://wiki.php.net/rfc/deprecations_php_7_4 + 'is_real' => 'is_float', + ], PhpVersion::PHP_74); + + $rectorConfig->ruleWithConfigurationPhpVersionBound( + RenameCastRector::class, + [new RenameCast(Double::class, Double::KIND_REAL, Double::KIND_FLOAT)], + PhpVersion::PHP_74 + ); + + // PHP 8.0 + $rectorConfig->ruleWithConfigurationPhpVersionBound(StaticCallToFuncCallRector::class, [ + new StaticCallToFuncCall('Nette\Utils\Strings', 'startsWith', 'str_starts_with'), + new StaticCallToFuncCall('Nette\Utils\Strings', 'endsWith', 'str_ends_with'), + new StaticCallToFuncCall('Nette\Utils\Strings', 'contains', 'str_contains'), + ], PhpVersion::PHP_80); + + // nette\utils and Strings::replace() + $rectorConfig->ruleWithConfigurationPhpVersionBound( + ArgumentAdderRector::class, + [new ArgumentAdder('Nette\Utils\Strings', 'replace', 2, 'replacement', '')], + PhpVersion::PHP_80 + ); + + // @see https://php.watch/versions/8.0/pgsql-aliases-deprecated + $rectorConfig->ruleWithConfigurationPhpVersionBound(RenameFunctionRector::class, [ + 'pg_clientencoding' => 'pg_client_encoding', + 'pg_cmdtuples' => 'pg_affected_rows', + 'pg_errormessage' => 'pg_last_error', + 'pg_fieldisnull' => 'pg_field_is_null', + 'pg_fieldname' => 'pg_field_name', + 'pg_fieldnum' => 'pg_field_num', + 'pg_fieldprtlen' => 'pg_field_prtlen', + 'pg_fieldsize' => 'pg_field_size', + 'pg_fieldtype' => 'pg_field_type', + 'pg_freeresult' => 'pg_free_result', + 'pg_getlastoid' => 'pg_last_oid', + 'pg_loclose' => 'pg_lo_close', + 'pg_locreate' => 'pg_lo_create', + 'pg_loexport' => 'pg_lo_export', + 'pg_loimport' => 'pg_lo_import', + 'pg_loopen' => 'pg_lo_open', + 'pg_loread' => 'pg_lo_read', + 'pg_loreadall' => 'pg_lo_read_all', + 'pg_lounlink' => 'pg_lo_unlink', + 'pg_lowrite' => 'pg_lo_write', + 'pg_numfields' => 'pg_num_fields', + 'pg_numrows' => 'pg_num_rows', + 'pg_result' => 'pg_fetch_result', + 'pg_setclientencoding' => 'pg_set_client_encoding', + ], PhpVersion::PHP_80); + + $rectorConfig->ruleWithConfigurationPhpVersionBound(FunctionArgumentDefaultValueReplacerRector::class, [ + new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, 'gte', 'ge'), + new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, 'lte', 'le'), + new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, '', '!='), + new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, '!', '!='), + new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, 'g', 'gt'), + new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, 'l', 'lt'), + new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, 'n', 'ne'), + new ReplaceFuncCallArgumentDefaultValue('get_headers', 1, 0, false), + new ReplaceFuncCallArgumentDefaultValue('get_headers', 1, 1, true), + ], PhpVersion::PHP_80); + + // PHP 8.5 + $rectorConfig->ruleWithConfigurationPhpVersionBound(RemoveFuncCallArgRector::class, [ + // https://wiki.php.net/rfc/deprecations_php_8_5#deprecate_key_length_parameter_of_openssl_pkey_derive + new RemoveFuncCallArg('openssl_pkey_derive', 2), + // https://wiki.php.net/rfc/deprecations_php_8_5#deprecate_the_exclude_disabled_parameter_of_get_defined_functions + new RemoveFuncCallArg('get_defined_functions', 0), + ], PhpVersion::PHP_85); + + $rectorConfig->ruleWithConfigurationPhpVersionBound(RenameMethodRector::class, [ + // https://wiki.php.net/rfc/deprecations_php_8_5#deprecate_splobjectstoragecontains_splobjectstorageattach_and_splobjectstoragedetach + new MethodCallRename('SplObjectStorage', 'contains', 'offsetExists'), + new MethodCallRename('SplObjectStorage', 'attach', 'offsetSet'), + new MethodCallRename('SplObjectStorage', 'detach', 'offsetUnset'), + + // https://wiki.php.net/rfc/deprecations_php_8_5#deprecate_driver_specific_pdo_constants_and_methods + new MethodCallRename('PDO', 'pgsqlCopyFromArray', 'copyFromArray'), + new MethodCallRename('PDO', 'pgsqlCopyFromFile', 'copyFromFile'), + new MethodCallRename('PDO', 'pgsqlCopyToArray', 'copyToArray'), + new MethodCallRename('PDO', 'pgsqlCopyToFile', 'copyToFile'), + new MethodCallRename('PDO', 'pgsqlGetNotify', 'getNotify'), + new MethodCallRename('PDO', 'pgsqlGetPid', 'getPid'), + new MethodCallRename('PDO', 'pgsqlLOBCreate', 'lobCreate'), + new MethodCallRename('PDO', 'pgsqlLOBOpen', 'lobOpen'), + new MethodCallRename('PDO', 'pgsqlLOBUnlink', 'lobUnlink'), + new MethodCallRename('PDO', 'sqliteCreateAggregate', 'createAggregate'), + new MethodCallRename('PDO', 'sqliteCreateCollation', 'createCollation'), + new MethodCallRename('PDO', 'sqliteCreateFunction', 'createFunction'), + ], PhpVersion::PHP_85); + + $rectorConfig->ruleWithConfigurationPhpVersionBound(RenameFunctionRector::class, [ + // https://wiki.php.net/rfc/deprecations_php_8_5#formally_deprecate_socket_set_timeout + 'socket_set_timeout' => 'stream_set_timeout', + + // https://wiki.php.net/rfc/deprecations_php_8_5#formally_deprecate_mysqli_execute + 'mysqli_execute' => 'mysqli_stmt_execute', + ], PhpVersion::PHP_85); + + // https://wiki.php.net/rfc/deprecations_php_8_5#deprecate_driver_specific_pdo_constants_and_methods + $rectorConfig->ruleWithConfigurationPhpVersionBound(RenameClassConstFetchRector::class, [ + new RenameClassAndConstFetch('PDO', 'DBLIB_ATTR_CONNECTION_TIMEOUT', 'Pdo\Dblib', 'ATTR_CONNECTION_TIMEOUT'), + new RenameClassAndConstFetch('PDO', 'DBLIB_ATTR_QUERY_TIMEOUT', 'Pdo\Dblib', 'ATTR_QUERY_TIMEOUT'), + new RenameClassAndConstFetch( + 'PDO', + 'DBLIB_ATTR_STRINGIFY_UNIQUEIDENTIFIER', + 'Pdo\Dblib', + 'ATTR_STRINGIFY_UNIQUEIDENTIFIER' + ), + new RenameClassAndConstFetch('PDO', 'DBLIB_ATTR_VERSION', 'Pdo\Dblib', 'ATTR_VERSION'), + new RenameClassAndConstFetch('PDO', 'DBLIB_ATTR_TDS_VERSION', 'Pdo\Dblib', 'ATTR_TDS_VERSION'), + new RenameClassAndConstFetch('PDO', 'DBLIB_ATTR_SKIP_EMPTY_ROWSETS', 'Pdo\Dblib', 'ATTR_SKIP_EMPTY_ROWSETS'), + new RenameClassAndConstFetch('PDO', 'DBLIB_ATTR_DATETIME_CONVERT', 'Pdo\Dblib', 'ATTR_DATETIME_CONVERT'), + new RenameClassAndConstFetch('PDO', 'FB_ATTR_DATE_FORMAT', 'Pdo\Firebird', 'ATTR_DATE_FORMAT'), + new RenameClassAndConstFetch('PDO', 'FB_ATTR_TIME_FORMAT', 'Pdo\Firebird', 'ATTR_TIME_FORMAT'), + new RenameClassAndConstFetch('PDO', 'FB_ATTR_TIMESTAMP_FORMAT', 'Pdo\Firebird', 'ATTR_TIMESTAMP_FORMAT'), + new RenameClassAndConstFetch('PDO', 'MYSQL_ATTR_USE_BUFFERED_QUERY', 'Pdo\Mysql', 'ATTR_USE_BUFFERED_QUERY'), + new RenameClassAndConstFetch('PDO', 'MYSQL_ATTR_LOCAL_INFILE', 'Pdo\Mysql', 'ATTR_LOCAL_INFILE'), + new RenameClassAndConstFetch( + 'PDO', + 'MYSQL_ATTR_LOCAL_INFILE_DIRECTORY', + 'Pdo\Mysql', + 'ATTR_LOCAL_INFILE_DIRECTORY' + ), + new RenameClassAndConstFetch('PDO', 'MYSQL_ATTR_INIT_COMMAND', 'Pdo\Mysql', 'ATTR_INIT_COMMAND'), + new RenameClassAndConstFetch('PDO', 'MYSQL_ATTR_MAX_BUFFER_SIZE', 'Pdo\Mysql', 'ATTR_MAX_BUFFER_SIZE'), + new RenameClassAndConstFetch('PDO', 'MYSQL_ATTR_READ_DEFAULT_FILE', 'Pdo\Mysql', 'ATTR_READ_DEFAULT_FILE'), + new RenameClassAndConstFetch('PDO', 'MYSQL_ATTR_READ_DEFAULT_GROUP', 'Pdo\Mysql', 'ATTR_READ_DEFAULT_GROUP'), + new RenameClassAndConstFetch('PDO', 'MYSQL_ATTR_COMPRESS', 'Pdo\Mysql', 'ATTR_COMPRESS'), + new RenameClassAndConstFetch('PDO', 'MYSQL_ATTR_DIRECT_QUERY', 'Pdo\Mysql', 'ATTR_DIRECT_QUERY'), + new RenameClassAndConstFetch('PDO', 'MYSQL_ATTR_FOUND_ROWS', 'Pdo\Mysql', 'ATTR_FOUND_ROWS'), + new RenameClassAndConstFetch('PDO', 'MYSQL_ATTR_IGNORE_SPACE', 'Pdo\Mysql', 'ATTR_IGNORE_SPACE'), + new RenameClassAndConstFetch('PDO', 'MYSQL_ATTR_SSL_KEY', 'Pdo\Mysql', 'ATTR_SSL_KEY'), + new RenameClassAndConstFetch('PDO', 'MYSQL_ATTR_SSL_CERT', 'Pdo\Mysql', 'ATTR_SSL_CERT'), + new RenameClassAndConstFetch('PDO', 'MYSQL_ATTR_SSL_CA', 'Pdo\Mysql', 'ATTR_SSL_CA'), + new RenameClassAndConstFetch('PDO', 'MYSQL_ATTR_SSL_CAPATH', 'Pdo\Mysql', 'ATTR_SSL_CAPATH'), + new RenameClassAndConstFetch('PDO', 'MYSQL_ATTR_SSL_CIPHER', 'Pdo\Mysql', 'ATTR_SSL_CIPHER'), + new RenameClassAndConstFetch( + 'PDO', + 'MYSQL_ATTR_SSL_VERIFY_SERVER_CERT', + 'Pdo\Mysql', + 'ATTR_SSL_VERIFY_SERVER_CERT' + ), + new RenameClassAndConstFetch('PDO', 'MYSQL_ATTR_SERVER_PUBLIC_KEY', 'Pdo\Mysql', 'ATTR_SERVER_PUBLIC_KEY'), + new RenameClassAndConstFetch('PDO', 'MYSQL_ATTR_MULTI_STATEMENTS', 'Pdo\Mysql', 'ATTR_MULTI_STATEMENTS'), + new RenameClassAndConstFetch('PDO', 'ODBC_ATTR_USE_CURSOR_LIBRARY', 'Pdo\Odbc', 'ATTR_USE_CURSOR_LIBRARY'), + new RenameClassAndConstFetch('PDO', 'ODBC_ATTR_ASSUME_UTF8', 'Pdo\Odbc', 'ATTR_ASSUME_UTF8'), + new RenameClassAndConstFetch('PDO', 'ODBC_SQL_USE_IF_NEEDED', 'Pdo\Odbc', 'SQL_USE_IF_NEEDED'), + new RenameClassAndConstFetch('PDO', 'ODBC_SQL_USE_DRIVER', 'Pdo\Odbc', 'SQL_USE_DRIVER'), + new RenameClassAndConstFetch('PDO', 'ODBC_SQL_USE_ODBC', 'Pdo\Odbc', 'SQL_USE_ODBC'), + new RenameClassAndConstFetch('PDO', 'PGSQL_ATTR_DISABLE_PREPARES', 'Pdo\Pgsql', 'ATTR_DISABLE_PREPARES'), + new RenameClassAndConstFetch('PDO', 'SQLITE_ATTR_EXTENDED_RESULT_CODES', 'Pdo\Sqlite', 'ATTR_EXTENDED_RESULT_CODES'), + new RenameClassAndConstFetch('PDO', 'SQLITE_ATTR_OPEN_FLAGS', 'Pdo\Sqlite', 'OPEN_FLAGS'), + new RenameClassAndConstFetch('PDO', 'SQLITE_ATTR_READONLY_STATEMENT', 'Pdo\Sqlite', 'ATTR_READONLY_STATEMENT'), + new RenameClassAndConstFetch('PDO', 'SQLITE_DETERMINISTIC', 'Pdo\Sqlite', 'DETERMINISTIC'), + new RenameClassAndConstFetch('PDO', 'SQLITE_OPEN_READONLY', 'Pdo\Sqlite', 'OPEN_READONLY'), + new RenameClassAndConstFetch('PDO', 'SQLITE_OPEN_READWRITE', 'Pdo\Sqlite', 'OPEN_READWRITE'), + new RenameClassAndConstFetch('PDO', 'SQLITE_OPEN_CREATE', 'Pdo\Sqlite', 'OPEN_CREATE'), + ], PhpVersion::PHP_85); + + // https://wiki.php.net/rfc/deprecations_php_8_5#deprecate_non-standard_cast_names + $rectorConfig->ruleWithConfigurationPhpVersionBound(RenameCastRector::class, [ + new RenameCast(Int_::class, Int_::KIND_INTEGER, Int_::KIND_INT), + new RenameCast(Bool_::class, Bool_::KIND_BOOLEAN, Bool_::KIND_BOOL), + new RenameCast(Double::class, Double::KIND_DOUBLE, Double::KIND_FLOAT), + new RenameCast(String_::class, String_::KIND_BINARY, String_::KIND_STRING), + ], PhpVersion::PHP_85); + + // https://wiki.php.net/rfc/deprecations_php_8_5#deprecate_no-op_functions_from_the_resource_to_object_conversion + // these function have no effect when use + $rectorConfig->ruleWithConfigurationPhpVersionBound(RemoveFuncCallRector::class, [ + 'curl_close', 'curl_share_close', 'finfo_close', 'imagedestroy', 'xml_parser_free', + ], PhpVersion::PHP_85); + + // https://wiki.php.net/rfc/deprecations_php_8_5#deprecate_filter_default_constant + $rectorConfig->ruleWithConfigurationPhpVersionBound(RenameConstantRector::class, [ + 'FILTER_DEFAULT' => 'FILTER_UNSAFE_RAW', + ], PhpVersion::PHP_85); }; diff --git a/src/Config/RectorConfig.php b/src/Config/RectorConfig.php index b57fb7e4129..384869e3faa 100644 --- a/src/Config/RectorConfig.php +++ b/src/Config/RectorConfig.php @@ -16,6 +16,7 @@ use Rector\Contract\Rector\RectorInterface; use Rector\Enum\Config\Defaults; use Rector\Exception\ShouldNotHappenException; +use Rector\Php\PhpVersionResolver\ComposerJsonPhpVersionResolver; use Rector\Skipper\SkipCriteriaResolver\SkippedClassResolver; use Rector\Validation\RectorConfigValidator; use Rector\ValueObject\Configuration\LevelOverflow; @@ -277,6 +278,26 @@ public function ruleWithConfigurationComposerVersionBound( $this->ruleWithConfiguration($rectorClass, $configuration); } + /** + * Register the rule configuration only if the target PHP version is at least $phpVersion. + * Useful for a configuration valid from a specific PHP version, e.g. a function renamed in PHP 8.0. + * + * @param class-string $rectorClass + * @param mixed[] $configuration + * @param PhpVersion::* $phpVersion + */ + public function ruleWithConfigurationPhpVersionBound( + string $rectorClass, + array $configuration, + int $phpVersion + ): void { + if ($this->resolveTargetPhpVersion() < $phpVersion) { + return; + } + + $this->ruleWithConfiguration($rectorClass, $configuration); + } + /** * @param class-string $rectorClass */ @@ -626,4 +647,22 @@ private function resolveInstalledPackageVersion(string $packageName): ?string return $this->installedPackageResolver->resolvePackageVersion($packageName); } + + private function resolveTargetPhpVersion(): int + { + // an explicitly picked withPhpSets(phpXX: true) version is the target for its sets, + // even when it is above the project composer.json PHP version + if (SimpleParameterProvider::hasParameter(Option::POLYFILL_CEILING_PHP_VERSION)) { + $ceilingPhpVersion = SimpleParameterProvider::provideIntParameter(Option::POLYFILL_CEILING_PHP_VERSION); + if ($ceilingPhpVersion > 0) { + return $ceilingPhpVersion; + } + } + + if (SimpleParameterProvider::hasParameter(Option::PHP_VERSION_FEATURES)) { + return SimpleParameterProvider::provideIntParameter(Option::PHP_VERSION_FEATURES); + } + + return ComposerJsonPhpVersionResolver::resolve(getcwd() . '/composer.json') ?? PHP_VERSION_ID; + } } From 130af89fc3ab17f027005bc0e3eac9bcf970d5c4 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sat, 29 Aug 2026 22:25:11 +0200 Subject: [PATCH 4/9] move set link to SetList --- src/Configuration/RectorConfigBuilder.php | 9 ++------- src/Set/ValueObject/SetList.php | 5 +++++ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Configuration/RectorConfigBuilder.php b/src/Configuration/RectorConfigBuilder.php index 8d38f75b8b1..511e132b286 100644 --- a/src/Configuration/RectorConfigBuilder.php +++ b/src/Configuration/RectorConfigBuilder.php @@ -166,11 +166,6 @@ final class RectorConfigBuilder private ?string $editorUrl = null; - /** - * All PHP version rules in one set; each rule gates itself by PHP version at runtime - */ - private const string PHP_VERSION_BASED_SET = __DIR__ . '/../../config/set/php-version-based.php'; - private ?bool $isWithPhpSetsUsed = null; private ?bool $isWithPhpLevelUsed = null; @@ -970,7 +965,7 @@ public function withPhpLevel(int $level): self $setRectorsResolver = new SetRectorsResolver(); $rectorRulesWithConfiguration = $setRectorsResolver->resolveFromFilePathIncludingConfiguration( - self::PHP_VERSION_BASED_SET + SetList::PHP_VERSION_BASED_SET ); foreach ($rectorRulesWithConfiguration as $position => $rectorRuleWithConfiguration) { @@ -1145,7 +1140,7 @@ private function addPhpLevelSets(): self { $this->isWithPhpSetsUsed = true; - $this->sets[] = self::PHP_VERSION_BASED_SET; + $this->sets[] = SetList::PHP_VERSION_BASED_SET; return $this; } diff --git a/src/Set/ValueObject/SetList.php b/src/Set/ValueObject/SetList.php index c48b553f43c..8c5331c9374 100644 --- a/src/Set/ValueObject/SetList.php +++ b/src/Set/ValueObject/SetList.php @@ -138,4 +138,9 @@ final class SetList public const string CARBON = __DIR__ . '/../../../config/set/datetime-to-carbon.php'; public const string BEHAT_ANNOTATIONS_TO_ATTRIBUTES = __DIR__ . '/../../../config/set/behat-annotations-to-attributes.php'; + + /** + * All PHP version rules in one set; each rule gates itself by PHP version at runtime + */ + public const string PHP_VERSION_BASED_SET = __DIR__ . '/../../config/set/php-version-based.php'; } From 7172fd0ab1a32c5566a31adb01d65ffcf8f379df Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sat, 29 Aug 2026 22:32:52 +0200 Subject: [PATCH 5/9] Restore addPhpLevelSets() version arg, fix PHP_VERSION_BASED_SET path addPhpLevelSets() takes the picked PHP version again (nullable): an explicit withPhpSets(phpXX) version acts as a ceiling, null keeps composer.json gating and polyfill package support. Also fix SetList::PHP_VERSION_BASED_SET path depth - it sits three levels deep in src/Set/ValueObject, so it needs ../../../ to reach config/set. Claude-Session: https://claude.ai/code/session_014Q25TQ4b7b13UQQXScu3Fg --- src/Configuration/RectorConfigBuilder.php | 16 ++++++++++------ src/Set/ValueObject/SetList.php | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Configuration/RectorConfigBuilder.php b/src/Configuration/RectorConfigBuilder.php index 511e132b286..fe1a0e708c8 100644 --- a/src/Configuration/RectorConfigBuilder.php +++ b/src/Configuration/RectorConfigBuilder.php @@ -4,6 +4,7 @@ namespace Rector\Configuration; +use DrupalRector\Set\DrupalSetList; use Deprecated; use PhpParser\NodeVisitor; use Rector\Bridge\SetRectorsResolver; @@ -554,13 +555,11 @@ public function withPhpSets( // no version picked, target the project composer.json PHP version; validated here, gated at runtime if ($pickedPhpVersions === []) { ComposerJsonPhpVersionResolver::resolveFromCwdOrFail(); - return $this->addPhpLevelSets(); + return $this->addPhpLevelSets(null); } // explicitly picked version is a ceiling, even for polyfilled rules - $this->pickedPhpSetsVersion = $pickedPhpVersions[0]; - - return $this->addPhpLevelSets(); + return $this->addPhpLevelSets($pickedPhpVersions[0]); } #[Deprecated(message: 'Use "withPhpLevel()" instead, it raises PHP level one rule at a time.')] @@ -689,7 +688,7 @@ public function withComposerBased( if ($drupal && class_exists('DrupalRector\Set\DrupalSetList') && constant('DrupalRector\Set\DrupalSetList::COMPOSER_BASED')) { // waits on https://github.com/palantirnet/drupal-rector/pull/419/files#diff-c6bd4ee854830efc1363a7d99c1b6a2e7e64f2499a51e503174ab777de7e64e5 - $this->sets[] = \DrupalRector\Set\DrupalSetList::COMPOSER_BASED; + $this->sets[] = DrupalSetList::COMPOSER_BASED; } if ($phpunit) { @@ -1136,9 +1135,14 @@ public function withSetProviders(): self return $this; } - private function addPhpLevelSets(): self + /** + * @param PhpVersion::*|null $phpVersion an explicitly picked version acts as a ceiling; + * null keeps the composer.json PHP version gating and polyfill package support + */ + private function addPhpLevelSets(?int $phpVersion): self { $this->isWithPhpSetsUsed = true; + $this->pickedPhpSetsVersion = $phpVersion; $this->sets[] = SetList::PHP_VERSION_BASED_SET; diff --git a/src/Set/ValueObject/SetList.php b/src/Set/ValueObject/SetList.php index 8c5331c9374..5683b300cda 100644 --- a/src/Set/ValueObject/SetList.php +++ b/src/Set/ValueObject/SetList.php @@ -142,5 +142,5 @@ final class SetList /** * All PHP version rules in one set; each rule gates itself by PHP version at runtime */ - public const string PHP_VERSION_BASED_SET = __DIR__ . '/../../config/set/php-version-based.php'; + public const string PHP_VERSION_BASED_SET = __DIR__ . '/../../../config/set/php-version-based.php'; } From b5deec4ca964ba899d6ea354fafa8fb62789778d Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sat, 29 Aug 2026 22:46:08 +0200 Subject: [PATCH 6/9] Make addPhpLevelSets() take non-nullable PHP version again Restore the original non-nullable int signature. The picked-version ceiling is set in the explicit-pick branch only, so the no-arg case keeps composer.json gating and polyfill package support. Claude-Session: https://claude.ai/code/session_014Q25TQ4b7b13UQQXScu3Fg --- src/Configuration/RectorConfigBuilder.php | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Configuration/RectorConfigBuilder.php b/src/Configuration/RectorConfigBuilder.php index fe1a0e708c8..20990880499 100644 --- a/src/Configuration/RectorConfigBuilder.php +++ b/src/Configuration/RectorConfigBuilder.php @@ -552,13 +552,14 @@ public function withPhpSets( ); } - // no version picked, target the project composer.json PHP version; validated here, gated at runtime + // no version picked, target the project composer.json PHP version if ($pickedPhpVersions === []) { - ComposerJsonPhpVersionResolver::resolveFromCwdOrFail(); - return $this->addPhpLevelSets(null); + return $this->addPhpLevelSets(ComposerJsonPhpVersionResolver::resolveFromCwdOrFail()); } // explicitly picked version is a ceiling, even for polyfilled rules + $this->pickedPhpSetsVersion = $pickedPhpVersions[0]; + return $this->addPhpLevelSets($pickedPhpVersions[0]); } @@ -1136,13 +1137,11 @@ public function withSetProviders(): self } /** - * @param PhpVersion::*|null $phpVersion an explicitly picked version acts as a ceiling; - * null keeps the composer.json PHP version gating and polyfill package support + * @param PhpVersion::* $phpVersion */ - private function addPhpLevelSets(?int $phpVersion): self + private function addPhpLevelSets(int $phpVersion): self { $this->isWithPhpSetsUsed = true; - $this->pickedPhpSetsVersion = $phpVersion; $this->sets[] = SetList::PHP_VERSION_BASED_SET; From 33e9182392ac7707757910542bb7078028f92d47 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sat, 29 Aug 2026 22:47:04 +0200 Subject: [PATCH 7/9] Fix import order Claude-Session: https://claude.ai/code/session_014Q25TQ4b7b13UQQXScu3Fg --- src/Configuration/RectorConfigBuilder.php | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/Configuration/RectorConfigBuilder.php b/src/Configuration/RectorConfigBuilder.php index 20990880499..13f06afdf6d 100644 --- a/src/Configuration/RectorConfigBuilder.php +++ b/src/Configuration/RectorConfigBuilder.php @@ -4,8 +4,8 @@ namespace Rector\Configuration; -use DrupalRector\Set\DrupalSetList; use Deprecated; +use DrupalRector\Set\DrupalSetList; use PhpParser\NodeVisitor; use Rector\Bridge\SetRectorsResolver; use Rector\Caching\Contract\ValueObject\Storage\CacheStorageInterface; @@ -23,7 +23,6 @@ use Rector\Doctrine\Set\DoctrineSetList; use Rector\Enum\Config\Defaults; use Rector\Exception\Configuration\InvalidConfigurationException; -use Rector\Php\PhpVersionResolver\ComposerJsonPhpVersionResolver; use Rector\PHPUnit\Set\PHPUnitSetList; use Rector\Set\ValueObject\DowngradeLevelSetList; use Rector\Set\ValueObject\SetList; @@ -554,13 +553,13 @@ public function withPhpSets( // no version picked, target the project composer.json PHP version if ($pickedPhpVersions === []) { - return $this->addPhpLevelSets(ComposerJsonPhpVersionResolver::resolveFromCwdOrFail()); + return $this->addPhpLevelSets(); } // explicitly picked version is a ceiling, even for polyfilled rules $this->pickedPhpSetsVersion = $pickedPhpVersions[0]; - return $this->addPhpLevelSets($pickedPhpVersions[0]); + return $this->addPhpLevelSets(); } #[Deprecated(message: 'Use "withPhpLevel()" instead, it raises PHP level one rule at a time.')] @@ -1136,15 +1135,10 @@ public function withSetProviders(): self return $this; } - /** - * @param PhpVersion::* $phpVersion - */ - private function addPhpLevelSets(int $phpVersion): self + private function addPhpLevelSets(): self { $this->isWithPhpSetsUsed = true; - $this->sets[] = SetList::PHP_VERSION_BASED_SET; - return $this; } From 33fa2f9d9d6b28d7c9a44a58af63ddf2e50b852a Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sat, 29 Aug 2026 22:57:15 +0200 Subject: [PATCH 8/9] Warn on deprecated per-version PHP set in withSets(), keep addPhpLevelSets() version arg withSets() emits an E_USER_DEPRECATED notice when a SetList::PHP_* set file is passed, pointing to withPhpSets()/withPhpLevel(). Detected by file path, so the deprecated constants are not referenced internally. addPhpLevelSets() keeps the non-nullable picked version and stores it; an explicit withPhpSets(phpXX) version acts as a ceiling via isPhpSetsVersionPicked, while the composer.json fallback does not, preserving polyfill package support. Storing the version also keeps the argument used, so it is not stripped as dead. Claude-Session: https://claude.ai/code/session_014Q25TQ4b7b13UQQXScu3Fg --- phpstan.neon | 1 + src/Configuration/RectorConfigBuilder.php | 42 ++++++++++++++-- .../Configuration/RectorConfigBuilderTest.php | 48 +++++++++++++++++++ 3 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 tests/Configuration/RectorConfigBuilderTest.php diff --git a/phpstan.neon b/phpstan.neon index 6923a5ea250..21a5c194658 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -82,6 +82,7 @@ parameters: paths: - config/set/level - tests/Bridge/SetRectorsResolverTest.php + - tests/Configuration/RectorConfigBuilderTest.php # the deprecated github/gitlab output formatters are still tested until removed in next minor version - diff --git a/src/Configuration/RectorConfigBuilder.php b/src/Configuration/RectorConfigBuilder.php index 13f06afdf6d..360a3d49803 100644 --- a/src/Configuration/RectorConfigBuilder.php +++ b/src/Configuration/RectorConfigBuilder.php @@ -6,6 +6,7 @@ use Deprecated; use DrupalRector\Set\DrupalSetList; +use Nette\Utils\Strings; use PhpParser\NodeVisitor; use Rector\Bridge\SetRectorsResolver; use Rector\Caching\Contract\ValueObject\Storage\CacheStorageInterface; @@ -23,6 +24,7 @@ use Rector\Doctrine\Set\DoctrineSetList; use Rector\Enum\Config\Defaults; use Rector\Exception\Configuration\InvalidConfigurationException; +use Rector\Php\PhpVersionResolver\ComposerJsonPhpVersionResolver; use Rector\PHPUnit\Set\PHPUnitSetList; use Rector\Set\ValueObject\DowngradeLevelSetList; use Rector\Set\ValueObject\SetList; @@ -37,11 +39,17 @@ /** * @api + * @see \Rector\Tests\Configuration\RectorConfigBuilderTest */ final class RectorConfigBuilder { private const int MAX_LEVEL_GAP = 10; + /** + * Matches the deprecated per-version PHP set files, e.g. .../config/set/php82.php + */ + private const string DEPRECATED_PHP_SET_REGEX = '#/config/set/php\d+\.php$#'; + /** * A level method and the set that contains the very same rules, * so they are never enabled both at once @@ -172,6 +180,12 @@ final class RectorConfigBuilder private ?int $pickedPhpSetsVersion = null; + /** + * Only an explicitly picked withPhpSets(phpXX) version acts as a ceiling; the composer.json + * fallback must not, so polyfilled rules can still be raised above the project PHP version + */ + private bool $isPhpSetsVersionPicked = false; + /** * @var LevelOverflow[] */ @@ -184,7 +198,7 @@ public function __invoke(RectorConfig $rectorConfig): void $this->sets[] = SetList::PHP_POLYFILLS; } - if ($this->pickedPhpSetsVersion !== null) { + if ($this->isPhpSetsVersionPicked && $this->pickedPhpSetsVersion !== null) { SimpleParameterProvider::setParameter( Option::POLYFILL_CEILING_PHP_VERSION, $this->pickedPhpSetsVersion @@ -408,6 +422,18 @@ public function withRootFiles(): self */ public function withSets(array $sets): self { + foreach ($sets as $set) { + if (Strings::match($set, self::DEPRECATED_PHP_SET_REGEX) === null) { + continue; + } + + trigger_error(sprintf( + 'The per-version PHP set "%s" is deprecated. Use "withPhpSets()" or "withPhpLevel()" instead, ' + . 'they pick the rules by your PHP version automatically.', + $set + ), E_USER_DEPRECATED); + } + $this->sets = array_merge($this->sets, $sets); return $this; @@ -553,13 +579,13 @@ public function withPhpSets( // no version picked, target the project composer.json PHP version if ($pickedPhpVersions === []) { - return $this->addPhpLevelSets(); + return $this->addPhpLevelSets(ComposerJsonPhpVersionResolver::resolveFromCwdOrFail()); } // explicitly picked version is a ceiling, even for polyfilled rules - $this->pickedPhpSetsVersion = $pickedPhpVersions[0]; + $this->isPhpSetsVersionPicked = true; - return $this->addPhpLevelSets(); + return $this->addPhpLevelSets($pickedPhpVersions[0]); } #[Deprecated(message: 'Use "withPhpLevel()" instead, it raises PHP level one rule at a time.')] @@ -1135,10 +1161,16 @@ public function withSetProviders(): self return $this; } - private function addPhpLevelSets(): self + /** + * @param PhpVersion::* $phpVersion + */ + private function addPhpLevelSets(int $phpVersion): self { $this->isWithPhpSetsUsed = true; + $this->pickedPhpSetsVersion = $phpVersion; + $this->sets[] = SetList::PHP_VERSION_BASED_SET; + return $this; } diff --git a/tests/Configuration/RectorConfigBuilderTest.php b/tests/Configuration/RectorConfigBuilderTest.php new file mode 100644 index 00000000000..b8fe1dc944e --- /dev/null +++ b/tests/Configuration/RectorConfigBuilderTest.php @@ -0,0 +1,48 @@ +deprecations = []; + set_error_handler(function (int $errorNumber, string $errorMessage): bool { + $this->deprecations[] = $errorMessage; + return true; + }, E_USER_DEPRECATED); + } + + protected function tearDown(): void + { + restore_error_handler(); + } + + public function testWithSetsWarnsOnDeprecatedPhpSet(): void + { + RectorConfig::configure() + ->withSets([SetList::PHP_82]); + + $this->assertCount(1, $this->deprecations); + $this->assertStringContainsString('withPhpSets()', $this->deprecations[0]); + } + + public function testWithSetsDoesNotWarnOnNonPhpSet(): void + { + RectorConfig::configure() + ->withSets([SetList::CODE_QUALITY]); + + $this->assertSame([], $this->deprecations); + } +} From c5e730cc12481fa2935a557ebf32e9b51b116e33 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sat, 29 Aug 2026 23:04:14 +0200 Subject: [PATCH 9/9] Use SymfonyStyle warning for deprecated PHP set notice in withSets() Replace the trigger_error() deprecation with a SymfonyStyle ->warning() via a new Notifier::notifyDeprecatedPhpSet(), matching the other withSets/withPhpSets notifications. Drop the console-output unit test, in line with the untested Notifier notifications. Claude-Session: https://claude.ai/code/session_014Q25TQ4b7b13UQQXScu3Fg --- phpstan.neon | 1 - src/Configuration/RectorConfigBuilder.php | 7 +-- src/Console/Notifier.php | 12 +++++ .../Configuration/RectorConfigBuilderTest.php | 48 ------------------- 4 files changed, 13 insertions(+), 55 deletions(-) delete mode 100644 tests/Configuration/RectorConfigBuilderTest.php diff --git a/phpstan.neon b/phpstan.neon index 21a5c194658..6923a5ea250 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -82,7 +82,6 @@ parameters: paths: - config/set/level - tests/Bridge/SetRectorsResolverTest.php - - tests/Configuration/RectorConfigBuilderTest.php # the deprecated github/gitlab output formatters are still tested until removed in next minor version - diff --git a/src/Configuration/RectorConfigBuilder.php b/src/Configuration/RectorConfigBuilder.php index 360a3d49803..edd972ba24c 100644 --- a/src/Configuration/RectorConfigBuilder.php +++ b/src/Configuration/RectorConfigBuilder.php @@ -39,7 +39,6 @@ /** * @api - * @see \Rector\Tests\Configuration\RectorConfigBuilderTest */ final class RectorConfigBuilder { @@ -427,11 +426,7 @@ public function withSets(array $sets): self continue; } - trigger_error(sprintf( - 'The per-version PHP set "%s" is deprecated. Use "withPhpSets()" or "withPhpLevel()" instead, ' - . 'they pick the rules by your PHP version automatically.', - $set - ), E_USER_DEPRECATED); + Notifier::notifyDeprecatedPhpSet($set); } $this->sets = array_merge($this->sets, $sets); diff --git a/src/Console/Notifier.php b/src/Console/Notifier.php index 76fd77bcd12..06539f12f5b 100644 --- a/src/Console/Notifier.php +++ b/src/Console/Notifier.php @@ -28,6 +28,18 @@ public static function notifyNotSuitableMethodForPHP74(string $calledMethod): vo sleep(3); } + public static function notifyDeprecatedPhpSet(string $set): void + { + $message = sprintf( + 'The per-version PHP set "%s" is deprecated. Use "withPhpSets()" or "withPhpLevel()" instead, ' + . 'they pick the rules by your PHP version automatically.', + $set + ); + + $symfonyStyle = new SymfonyStyle(new ArgvInput(), new ConsoleOutput()); + $symfonyStyle->warning($message); + } + public static function errorWithPhpSetsNotSuitableForPHP74AndLower(): void { if (PHP_VERSION_ID >= 80000) { diff --git a/tests/Configuration/RectorConfigBuilderTest.php b/tests/Configuration/RectorConfigBuilderTest.php deleted file mode 100644 index b8fe1dc944e..00000000000 --- a/tests/Configuration/RectorConfigBuilderTest.php +++ /dev/null @@ -1,48 +0,0 @@ -deprecations = []; - set_error_handler(function (int $errorNumber, string $errorMessage): bool { - $this->deprecations[] = $errorMessage; - return true; - }, E_USER_DEPRECATED); - } - - protected function tearDown(): void - { - restore_error_handler(); - } - - public function testWithSetsWarnsOnDeprecatedPhpSet(): void - { - RectorConfig::configure() - ->withSets([SetList::PHP_82]); - - $this->assertCount(1, $this->deprecations); - $this->assertStringContainsString('withPhpSets()', $this->deprecations[0]); - } - - public function testWithSetsDoesNotWarnOnNonPhpSet(): void - { - RectorConfig::configure() - ->withSets([SetList::CODE_QUALITY]); - - $this->assertSame([], $this->deprecations); - } -}