Skip to content

[Config] Only registered rules count as active rules - #8402

Closed
CybotTM wants to merge 1 commit into
rectorphp:mainfrom
CybotTM:fix/testcase-active-rules-from-registration
Closed

[Config] Only registered rules count as active rules#8402
CybotTM wants to merge 1 commit into
rectorphp:mainfrom
CybotTM:fix/testcase-active-rules-from-registration

Conversation

@CybotTM

@CybotTM CybotTM commented Aug 29, 2026

Copy link
Copy Markdown

findByContract() returns every instance the container cached, so a rule that another service takes as a constructor dependency counts as active although no config registered it. RectorNodeTraverser (autowired from @param RectorInterface[]) and AbstractRectorTestCase both read that list, so it hits bin/rector, not just tests:

bin/rector process <dir> --dry-run --config <config registering one rule>

-        return 'marker';
+        return 'rewritten by an unregistered rule';
Applied rules:
 * UnregisteredMarkerRector

Before 2.6.4 the same decision was tagged(RectorInterface::class), filled by rule() — registration decided what was active, not existence.

Fix: RectorConfig::findByContract() intersects with $registeredRectorClasses when the contract is a rule contract. Other and wider contracts are untouched.

Test in tests/Config/RectorConfigTest. Found in ssch/typo3-rector, where ValidateAttributeDecorator takes StringClassNameToClassConstantRector in its constructor.

Assisted by claude-code:claude-fable-5

@CybotTM
CybotTM force-pushed the fix/testcase-active-rules-from-registration branch from f9f4256 to 35a32fa Compare August 29, 2026 10:32
@CybotTM CybotTM changed the title [Testing] Run only registered rules in AbstractRectorTestCase [Config] Only registered rules count as active rules Aug 29, 2026
@CybotTM
CybotTM force-pushed the fix/testcase-active-rules-from-registration branch 4 times, most recently from eb913d8 to 78d0943 Compare August 29, 2026 12:51
findByContract() returns every instance the container has cached that implements
the contract. A rule that another service takes as a constructor dependency is
built and cached like any other object, so it comes back as an active rule even
though no config ever registered it.

Five services take an autowired "@PARAM RectorInterface[]": RectorNodeTraverser,
ConfigInitializer, OnlyRuleResolver, DeprecatedRulesReporter and
ComposerBasedCommand. Entropy resolves an array parameter through
findByContract() (Container::resolve(), ParameterTypesResolver), so this is not
a test-only concern -- bin/rector applies the rule:

    bin/rector process <dir> --dry-run --clear-cache --config <that config>

    -        return 'marker';
    +        return 'rewritten by an unregistered rule';
    Applied rules:
     * UnregisteredMarkerRector

AbstractRectorTestCase::setUp() reads the same list for refreshPhpRectors().

Narrowing in RectorConfig::findByContract() covers all of them, because they all
go through it. Filtering in the test case alone would have left bin/rector as it
is and made the test stricter than the product.

The registered set is already tracked: rule() records into
$registeredRectorClasses, and every registration path funnels through rule() --
rules(), ruleWithConfiguration(), and ruleWithConfigurationComposerVersionBound(),
which returns before rule() when the package constraint is not satisfied. The
filter can therefore only remove rules nobody registered; it cannot activate one
the composer-package filter excluded. warmUpInstanceServices() builds every
registered class of the contract first, so the filtered result is exactly the
registered set, whatever the container happened to build earlier.

Contracts that are not rule contracts are returned untouched, and so is a wider
one: findByContract(NodeVisitor::class) still sees unregistered rules. That
keeps registerDecoratingNodeVisitor() working, which registers through
singleton() rather than rule().

One behaviour change reaches users. A rule class registered through
RectorConfig::singleton() goes past rule() and is no longer active. Two public
builder entry points lead there: registerService(), which asserts nothing, and
registerDecoratingNodeVisitor(), whose Assert::isAOf(..., NodeVisitor::class) a
rule satisfies because RectorInterface extends NodeVisitor.

ssch/typo3-rector does exactly this, and it argues for the change. Its
config/v13/typo3-130.php registers two rules through singleton() with the
comment "The following rules exist only to render the diff for the
documentation. The actual logic is in THIS file!". Measured against 2.6.4,
loading that set yields 28 active rules against 26 registered -- the two extra
are those documentation-only ones, which therefore run against user code today.
Before 2.6.4 they did not, because singleton() set no tag. This restores what
the comment says was intended.

The emptiness assertion in AbstractRectorTestCase now asks through NodeVisitor
instead of RectorInterface. Read through the rule contract it could no longer
fail: resetRuleConfigurations() empties the registered list just above,
so the answer would be "empty" without looking at what forgetRectorsRules()
actually left behind. The wider contract still sees a surviving rule instance,
which is what the assertion is there to catch -- a stale instance is handed back
by Container::make() before the afterResolving() callback runs, so the next test
class registering the same rule would run it with the previous class's
configuration.

Before 2.6.4 this decision was made by tagged(RectorInterface::class), an
explicit tag list filled by rule(). Registration decided what was active; since
the container swap, existence does. This restores the former behaviour.

The test goes to tests/Config/RectorConfigTest.php, at the seam the fix sits on:
a rule is registered, its constructor dependency is another rule, and only the
registered one comes back as active. A fixture test under tests/Issues would
have gone through AbstractRectorTestCase and stayed green if the filter moved
back into the test case.

Found while fixing ssch/typo3-rector for 2.6.4, where ValidateAttributeDecorator
takes StringClassNameToClassConstantRector in its constructor: the Extbase test
resolved two active rules against one registered.

Assisted-by: claude-code:claude-fable-5
Agent-Session: https://claude.ai/code/session_019wx8ueHuqUidp5yybDQ2CN
Agent-Host: 0493f0
Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
@CybotTM
CybotTM force-pushed the fix/testcase-active-rules-from-registration branch from 78d0943 to 21f378c Compare August 29, 2026 13:00
@CybotTM
CybotTM marked this pull request as ready for review August 29, 2026 13:21
@TomasVotruba

Copy link
Copy Markdown
Member

Tl;Dr; Not accepting AI slop here. Description way too long to read.

@CybotTM

CybotTM commented Aug 29, 2026

Copy link
Copy Markdown
Author

Hi @TomasVotruba,

Tl;Dr; Not accepting AI slop here. Description way too long to read.

Thanks for the feedback, why do you not ask for improvement instead of just closing?
Would be nice if you can update your contribution rules, so it is easier to follow them.
If I missed them, please point me at them, would happy to follwo them.
I am sorry, but i just did not found them.

AI slop is a modern slang term for low-quality digital content—such as text, images, videos, or audio—that is mass-produced using generative artificial intelligence with little human effort, care, or meaning

Sorry, if it made you think this PR was drafted "with little human effort, care, or meaning"
I can undertsand if you do not want to read the full text, but judging it as AI slop without reading is the exact meaning of slop.

I have shortened the description for your convenience.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants