Faster runs, single-pass rules, and a leaner rule set 🚀
The Rector core engine is ~26 % faster (1.36x) with ~8 % less memory than 2.7.0.
If you run PHP 8.3+, it's 3x faster using PHPStan Turbo, the native parsing extension shipped with phpstan/phpstan:
| Rev | Core engine (time / peak RSS) |
|---|---|
| 2.7.0 | 32.0s / 351 MB |
| 2.8.0 | 23.6s / 324 MB |
| 2.8.0 + PHPStan Turbo (PHP 8.3+) | ~10.0s / ~317 MB |
Turbo factor measured on this release via an A/B run on Rector's own rules/ (24-core host, PHP 8.4, cold, identical changed-file fingerprint): ~2.4x wall and ~2.6x worker CPU. Turbo speeds up PHPStan parsing in the workers, so the wall-time gain scales with the available core count.
This release is mostly about speed and reliability. A large performance campaign hoists cheap node guards in front of expensive reflection, scope and type lookups across hundreds of rules, replaces per-node config scans in configurable rules with keyed lookups, and resolves short names and unused imports in a single AST walk.
Rules now converge in a single pass: instead of a re-processing guard, the engine iterates rules to a fixed point and tests enforce idempotency, so a rule that changes a file produces the same result whether it runs once or many times.
We also trimmed the rule set: many niche, context-dependent rules that are not part of any set and rarely used are now deprecated. Deprecated rules still run and print a warning; they will be removed in a future major release. The goal is a smaller set of valuable, reliable rules for everyday upgrades.
New features 🥳
- [CodeQuality] Add
ExplicitNullableObjectInstanceRector, replacing 4 nullable-object instanceof rules (#8636) - [Console] Add
--alloption tolist-rulesto list every rule in core andrector-*packages (#8637) - [Console] Undeprecate the
list-rulescommand (#8533) - Respect cgroup/CFS CPU quota when picking the parallel worker count (#8559)
- Bump to PHPStan 2.3
Performance Improvements 🚀
- Run the parallel workers with PHPStan Turbo, the native parsing extension shipped with
phpstan/phpstan, on PHP 8.3+ - no main-process restart, the workers do the analysis (#8812) - Retarget class-level rules to a single
Class_/ClassLikenode and addgetClassReflection()toAbstractRector:RemoveParentDelegatingClassMethodRector,ReturnTypeFromMockObjectRector,AddVoidReturnTypeWhereNoReturnRectorandChangeMethodVisibilityRector(#8824, #8827, #8828, #8830, #8831) - Hoist the repeated fully qualified name build in
UseImportNameMatcher::resolveName()and resolve the parent class reflection by name inPhp4ConstructorRector(#8841, #8838) - Drop scope attachment on pre/post increment expressions and remove an unneeded
array_values()inBetterStandardPrinter::pArray()(#8814, #8821) - Hoist cheap name and structural guards before reflection, scope and type lookups across CodeQuality, DeadCode, CodingStyle, TypeDeclaration, Transform, EarlyReturn and legacy Php rules (#8664, #8666, #8667, #8668, #8669, #8670, #8671, #8672, #8661, #8663)
- Replace per-node config scans with keyed lookups in configurable rules - method, property, rename, transform, arguments and removing rules (#8734, #8735, #8736, #8739, #8742, #8743, #8744, #8745)
- Iterate rules to a fixed point with a content-cycle guard instead of a per-node re-processing guard, and enforce single-pass idempotency in tests (#8679, #8719, #8715, #8722)
- Skip rules whose work depends on a docblock when the node has none (#8624, #8625, #8626, #8627, #8628, #8629, #8630, #8631, #8632)
- Resolve short names and used names in a single AST walk, and cache resolved names per file version (#8697, #8700, #8701, #8702, #8704)
- Reuse the rebuilt
ClassReflectionby class content hash to stop memory growth on class refresh (#8731, #8728, #8729) - Cache per-file import facts in
ClassNameImportContext(#8687) - Cache
realpath()of skip patterns inFileInfoMatcher(#8699) - Memoize
StaticPHPUnitEnvironment::isPHPUnitRunand replace theisStringNamewildcard loop with a singlestrpbrkscan (#8732, #8733) - Only process the catch statements PHPStan skipped and run the decorating visitors once (#8738)
- Avoid nested traversals in
ContextNodeVisitorby tracking loop/try/attribute depth (#8741) - Short-circuit
NodeComparator::areNodesEqualbefore printing (#8752) - Move skipping from
enterNode()toFileProcessorso it runs once per file (#8593) - Parallel LPT buckets for a more even worker load (#8496)
- Add a single-process hotspot profiler and A/B benchmark script with a performance methodology (#8718, #8536)
- Speed up
ConvertStaticToSelfRector,ArrayToFirstClassCallableRectorandSimplifyEmptyCheckOnEmptyArrayRectorwith cheap guards before scope fetching (#8801, #8796, #8795) - Hoist loop-invariant resolve+continue guards above
foreach(#8761) - Stop attaching scope to class
extends/implementsnames and drop the unused process property on the node scope (#8788, #8792) - Remove the infinite-loop guard from
FileProcessornow that rules converge in a single pass (#8811)
Bugfixes 🐛
NewlineBeforeNewAssignSetRector: fix a false positive on a local var and$thisproperty name collision (#8535)SimplifyQuoteEscapeRector: handle strings with accented characters (#8532)TernaryToNullsafeCoalesceRector: skip mixed and untyped calls (#8531)TernaryToNullCoalescingRector: preserve fallback parentheses on theIdentical/NotIdenticalpath (#8674)AddArrowFunctionReturnTypeRector: fix a crash onNonexistentParentClassType(#8568)- Fix scope on an enum when combining
DowngradeEnumToConstantListClassRectorandRenameMethodRector(#8727) - Fix class scope refresh losing method scope when the class node is already in a class (#8725)
StrContainsRector/MbStrContainsRector: skip a non-zerostrpos()/mb_strpos()offset (#8640, #8642)SplitGroupedClassConstantsRector: preserve the attribute and type (#8650)RemoveUselessUnionReturnDocblockRector: skip astatic/$thisunion@return(#8649)StrictArrayParamDimFetchRector: skip a private method param called with anArrayAccessarg (#8648)ReadOnlyAnonymousClassRector: skip an anonymous class with attributes (#8665)NullToStrictStringFuncCallArgRector: skip an arg already cast to the target type (#8688)AddNameToBooleanArgumentRector: skipsession_set_save_handler(#8750)RemoveDuplicatedCaseInSwitchRector: compare switch case statements node-by-node (#8754)MergePhpstanDocTagIntoNativeRector: keep the native@param/@var/@returndescription (#8781)- Record in-place node mutations as changes, so a rule that mutates a node and returns
nullis still reported, acrossrector-doctrine,rector-symfony,rector-phpunitandrector-downgrade-php(doctrine #539, symfony #1087, phpunit #810, downgrade #418) fix(parallel): report the error limit only when it is actually reached (#8844)- Lazy-load rule class aliases to fix the PHP 7.4 scoped-build lint (#8845)
- Fix Windows CI error on
ChangedFilesDetectorPortableCacheTest(#8537)
Deprecations 💀
Deprecated rules still run and print a warning; they will be removed in a future major release. These are niche, context-dependent or rarely used rules outside the main PHP upgrade path.
Transform / Arguments
FuncCallToNewRector(#8776),FuncCallToStaticCallRector(#8775),MethodCallToFuncCallRector(#8769),MethodCallToStaticCallRector(#8770),NewToStaticCallRector(#8768),StaticCallToMethodCallRector(#8759),ConstFetchToClassConstFetchRector(#8771),MergeInterfacesRector(#8772),ArrayDimFetchToMethodCallRector(#8762),StringToClassConstantRector(#8779),RemoveMethodCallParamRector(#8766)ReplaceArgumentDefaultValueRectorandFunctionArgumentDefaultValueReplacerRectorin favor of dedicated rules (#8686, #8692)
Renaming
Naming (and the SetList::NAMING set itself)
RenameParamToMatchTypeRector(#8553),RenamePropertyToMatchTypeRector(#8547),RenameVariableToMatchNewTypeRector(#8554),RenameVariableToMatchMethodCallReturnTypeRector(#8549),RenameForeachValueVariableToMatchExprVariableRector(#8548),RenameForeachValueVariableToMatchMethodCallReturnTypeRector(#8550), and the naming prepared set warning (#8555)
TypeDeclaration
ParamTypeByMethodCallTypeRectorand its Object/Scalar/Array splits (#8567),ParamTypeByParentCallTypeRector(#8813),AddPropertyTypeDeclarationRector(#8774),AddParamStringTypeFromSprintfUseRector(#8723),DeclareStrictTypesRectorin favor ofSafeDeclareStrictTypesRector(#8562),BinaryOpNullableToInstanceofRectorfolded intoExplicitNullableObjectInstanceRector(#8817)
CodeQuality / CodingStyle / DeadCode / EarlyReturn / Php
CompleteDynamicPropertiesRector(#8753),SwitchTrueToMatchRector(#8622),InnerFunctionToPrivateMethodRector(#8621),MoveInnerFunctionToTopLevelRector(#8616),ForRepeatedCountToOwnVariableRector(#8620),CallUserFuncWithArrowFunctionToInlineRector(#8615),RemoveUselessIsObjectCheckRector(#8614),SimplifyStrposLowerRector(#8611),SetTypeToCastRector(#8610),ForeachItemsAssignToEmptyArrayToAssignRector(#8577),PostIncDecToPreIncDecRector(#8583),TernaryFalseExpressionToIfRector(#8683),RemoveDeadReturnRectorin favor ofRemoveAlwaysTrueIfConditionRector(#8685),RemoveDuplicatedReturnSelfDocblockRector(#8634),RemoveParentDelegatingConstructorRector(#8755),ChangeConstantVisibilityRector(#8767),AddOverrideAttributeToOverriddenMethodsRector(#8773),RemoveTraitUseRector(#8778),ReturnEarlyIfVariableRector(#8818)
Internals ⚙️
- Replace
tomasvotruba/unused-publicwithshipmonk/dead-code-detector(#8530) - Decouple Rector rules from the PhpParser
NodeVisitorand moveenterNode()out ofAbstractRectorintoRectorRunner(#8541, #8560) - Add a standalone
RectorRegistryand merge the skipper services into a singleSkipper(#8600, #8601, #8604) - Inline the deprecated
symplify/rule-doc-generator-contractsunder theRector\RuleDocnamespace (#8586) - Add a PHPStan rule to flag Rector rules not registered in any set (#8760)
- Remove dead and deprecated public API:
@apiannotations, unused collectors, converters and interfaces, thecreated_by_ruleattribute andRectifiedAnalyzer, and the deprecatedwithPhp53Sets()..withPhp74Sets()and cache-meta methods (#8794, #8803, #8804, #8805, #8806, #8807, #8808, #8809, #8810, #8798, #8799) - Register
MergePhpstanDocTagIntoNativeRectorin the type-declaration docblock level (#8763) - Move
VariableNamingtorector-downgrade-php(#8797, downgrade #417) - Deprecate
PhpVersionFeatureand usePhpVersionconstants directly in rules (#8839) - Remove the deprecated
custom-ruleandsetup-cicommands, and the unuseddoctrine/inflectordependency (#8815, #8816) - Add a
NoGetClassReflectionOnScopeRulePHPStan rule and route class reflection through a saferfetchClassLikeReflection(), used inReadonlyClassManipulatorandObjectTypeSpecifier(#8820, #8829, #8832, #8835, #8840) - Remove
NullToStrictStringIntConverter, use an explicitnulltype in the null-to-strict rules (#8837) - Lock the fixture directory while a test uses its temp file (#8822)
rector-doctrine 🟠
- Deprecate
AddArrayResultColumnNamesRector(#533) - Retarget
RemoveNullFromNullableCollectionTypeRectorto a singleClass_node and make it idempotent (#535, #534) - Cap removed-API rules at an upper version bound (#532)
- Check the method name before type inference in DBAL/ORM method call rules (#536, #537)
rector-symfony 🎵
- [Symfony 8.2] Add rules for
Schedule::with()toadd()and Doctrine middleware renames (#1082) - [Symfony 8.0] Merge the
eraseCredentialsbody into an existing__serialize()instead of a deadserialize()(#1085) - Reimplement
ReplaceArgumentDefaultValueRectorusages as dedicated rules (#1079) - Make
LogoutHandlerToLogoutEventSubscriberRectoridempotent (#1080) - Cap legacy removed-API rules and
StringToArrayArgumentProcessRectorat upper version bounds (#1076, #1075) - Move the nullable
ServiceMapout of theServiceMapProviderconstructor (#1073) - Check the method name before type inference in Intl and WebLink rules, and bail before reflection when a node has no attributes (#1083, #1084)
- Deprecate the JMS
AccessType/Accessorannotation-to-attribute rules and drop the orphaned enum (#1086, #1088)
rector-phpunit 🟢
- [PHPUnit 7.0] Add
ScenarioToTestAnnotationRectorto replace thescenarioannotation withtest(#809) - Match the rename map case-insensitively in
IdentifierManipulator(#808) WillReturnCallbackFallbackToReturnFalseRector: skip never-returning fall-through and a trailing comment after return (#801, #800)AssertEmptyNullableObjectToAssertInstanceofRector: skip an anonymous class (#792)- Deprecate
SuffixMockObjectPropertyRectoras it can produce invalid variable names (#795) - Make
AddProphecyTraitRectoridempotent and skip a non-test class early (#806, #793) - Speed up
AssertRegExpRectorand the PHPUnit test-case call guard with a cheap method-name gate (#796, #807) - Inline the scenario-to-test doc tag replace in
ScenarioToTestAnnotationRectorand drop the deprecatedfindFirstNonAnonymousClass()call (#812, #811)
rector-downgrade-php ⬇️
- Fix an infinite loop on
DowngradeIteratorCountToArrayRector, and skipiterator_to_array()/iterator_count()already guarded byis_array()(#414, #415) - Match
array_key_first/array_key_lastin a single statement traversal, and convert nested arrow functions to anonymous functions in a single pass (#416, #413) - Drop JSON constants from a bitwise-or structurally in
JsonConstCleaner(#410) - Make the exposed downgrade rules idempotent on repeated application (#409)
- Deprecate
DowngradeParameterTypeWideningRector(#411) - Add a local
VariableNamingservice moved from core, and fix the missingCOVARIANT_RETURNconstant withPhpVersion::PHP_74(#417, #419)