Repository navigation
feat: add composer-based set binding every rule to an exact drupal/core version - #419
Conversation
…re version
Replaces `DrupalSetProvider` — Rector deprecated `SetProviderInterface` and
`ComposerTriggeredSet` in favour of binding rules to a package version
directly — with a single generated set, `config/composer-based.php`, exposed
as `DrupalSetList::COMPOSER_BASED`.
Every configurable rule is registered through Rector 2.6's
`RectorConfig::ruleWithConfigurationComposerVersionBound()` with the exact
version its deprecation was introduced in, taken from the value object it is
already configured with:
-$rectorConfig->ruleWithConfiguration(FileSystemBasenameToNativeRector::class, [
- new DrupalIntroducedVersionConfiguration('11.3.0'),
-]);
+$rectorConfig->ruleWithConfigurationComposerVersionBound(FileSystemBasenameToNativeRector::class, [
+ new DrupalIntroducedVersionConfiguration('11.3.0'),
+], 'drupal/core', '>=11.3.0');
Rules that take no configuration cannot use that API, so they are registered
behind a `$ruleSince()` helper that checks the same constraint against the
installed `drupal/core`:
-$rectorConfig->rule(LoadAllIncludesRector::class);
+$ruleSince(LoadAllIncludesRector::class, '>=11.3.0');
Rector then activates only the rules the installed core satisfies — 34 of the
124 bound registrations on core 10.3, 63 on 11.2, 124 on 12.0 — and
`vendor/bin/rector composer-based` lists them with their constraint. Because
the installed version is known exactly, the opt-in "breaking" renames are
included; they cannot fatal on a core guaranteed to have the replacement.
The per-minor `Drupal*SetList` constants are unchanged and remain the way to
look ahead at a Drupal version that is not installed yet.
The set is generated by `scripts/generate-composer-based.php` from the
per-minor configs, so a rule only has to be registered once;
`ComposerBasedSetTest` asserts the committed file is in sync, that every
registration carries an exact `>=X.Y.Z` constraint, and that no rule of the
per-minor sets is missing from it.
…rupal: true) Restores `DrupalSetProvider` so existing `withSetProviders(DrupalSetProvider::class)` configs keep working; its docblock now points at `DrupalSetList::COMPOSER_BASED` as the successor. Drops the explicit `withSets([DrupalSetList::COMPOSER_BASED])` from the README and from the generated set's docblock — `withComposerBased(drupal: true)` is the documented entry point.
Adds the `@deprecated` annotation and a CHANGELOG "Deprecated" entry for `DrupalSetProvider` and the `drupal` / `drupal (breaking)` set groups it provides. The class stays registered so existing configs keep working, and is removed in a later release.
…hConfigurationComposerVersionBound()
|
Still WIP... need a bit config tuning. But check the linked PRs in the description to get the idea ahead 👍 |
The generator rewrote the per-minor configs by walking their PHP tokens and patching the registration calls, which made the set a build artifact of a parser rather than a config you can read. Keeps the set as a plain config of real rules, in the shape rector-doctrine uses (rectorphp/rector-doctrine#497), and accepts that a rule is registered both here and in its per-minor config. `ComposerBasedSetTest` fails when a rule of a per-minor config is missing here, so the duplication cannot drift unnoticed.
…e shape Follows rectorphp/rector-doctrine#497: a rule that takes no configuration declares the version its target API landed in on the rule class, instead of being gated by the set that registers it. -$ruleSince(LoadAllIncludesRector::class, '>=11.3.0'); +$rectorConfig->rule(LoadAllIncludesRector::class); +final class LoadAllIncludesRector extends AbstractRector implements ComposerPackageConstraintInterface, DocumentedRuleInterface +{ + public function provideComposerPackageConstraint(): ComposerPackageConstraint + { + return new ComposerPackageConstraint('drupal/core', '>=11.3.0'); + } 76 rules are bonded this way, which drops the `$ruleSince()` helper and the `Semver` lookup from the set. The three rector-phpunit rules the Drupal 10.1 config registers are dropped from the set as well: they are already bonded to `phpunit/phpunit` by rector-phpunit's own composer-based set, which is the accurate constraint for them. Rector applies the constraint filter globally rather than per set, so these rules no longer run against a core older than the deprecation, including when a `Drupal*SetList` set is loaded by hand. Verified against a project on 11.2.3 and one on 11.3.0: `LoadAllIncludesRector` fires only on the latter. Rule tests point `provideComposerJsonFilePath()` at a stub composer.json that requires drupal/core, as this package does not — without it every bonded rule would be filtered out of its own test.
- `ComposerBasedSetTest` used `new ReflectionClass(…)->…`, which only parses on PHP 8.4; the phpunit and phpstan workflows run 8.3. - Both workflows installed `rector/rector:^2` before running, which can resolve below the 2.6 that ships `ruleWithConfigurationComposerVersionBound()` and `ComposerPackageConstraintInterface`. Bumped the matrix to `^2.6`, matching the constraint in composer.json. - `ShouldCallParentMethodsRector` and `AddSymfonyConstraintValidatorTypeDeclarationsRector` were each registered by two per-minor configs. A bonded rule carries one constraint, so each is bonded to the lower of its two versions and registered once, under that version. Verified on PHP 8.3 with `composer require rector/rector:^2.6 --dev` applied the way the workflows do it: php-cs-fixer 0/523, PHPStan no errors, PHPUnit 699 tests.
The checks are static properties of the config and the rule classes, so PHPStan is where they belong — they now report at the offending line rather than as a regex assertion over the file contents, and cover every rule class rather than only what the set file happens to reference. - BoundRuleConfigurationRule — a ruleWithConfigurationComposerVersionBound() call must name "drupal/core" and an exact ">=X.Y.Z" version. - ComposerPackageConstraintRule — the same for every ComposerPackageConstraint a rule class constructs. - PlainlyRegisteredRuleRule — a rule registered with a plain rule() call in the composer-based set must implement ComposerPackageConstraintInterface, otherwise it runs on every Drupal version. - ComposerBasedSetCoverageRule — with RegisteredRectorClassCollector, fails when a per-minor config registers a rule the composer-based set does not. Each rule is covered by a RuleTestCase, and all four were verified against a deliberately broken tree: a ">=8.0" constraint, a rule class stripped of the interface, a "drupal/coder" package name, and a registration deleted from the set each produce the expected error.
|
Ready now 👍 |
|
Ping @bbrala |
|
We should bump the required rector version and make So that Or all sets needs to check that the constant like |
* fix: conflict with rector/rector >=2.6.2 (#420) Rector 2.6.2 removed every version-specific set constant from its first-party extension packages in favour of the new composer-based sets (rectorphp/rector-phpunit#760). The Drupal 8, 9 and 10 configs reference those constants directly, so from 2.6.2 on they abort before any rule runs. Bisected against real installs; 2.6.1 is the last release on which every Drupal set loads: 2.5.9 / 2.6.0 / 2.6.1 ok 2.6.2 / 2.6.3 / 2.6.6 fail Breakage map on 2.6.6: DRUPAL_90, DRUPAL_100 Could not detect twig set. DRUPAL_91, DRUPAL_92 Undefined constant PHPUnitSetList::PHPUNIT_90 DRUPAL_101 Undefined constant SymfonySetList::SYMFONY_63 DRUPAL_102 Undefined constant SymfonySetList::SYMFONY_64 plus the DRUPAL_8 / DRUPAL_9 / DRUPAL_10 aggregates that include them. The Drupal 11 and 12 sets reference no third-party sets and are unaffected. A conflict rather than a narrowed require constraint, so the supported range stays documented as ^2 and composer reports the incompatibility by name. This is a stopgap to keep installs working. The real fix is porting the sets to the composer-based mechanism (#419), which Rector 2.6.6 already expects: withComposerBased(drupal: true) resolves DrupalRector\Set\DrupalSetList:: COMPOSER_BASED, and SetGroup::DRUPAL is now marked deprecated upstream in favour of a composer-based.php set. The conflict is lifted once that lands. Reported by ptmkenny in #420 * style: use ?int over null|int in RemoveToolkitArgFromImageToolkitOperationConstructor Unrelated to #420, but the codestyle job is failing on main's code and blocks this branch. The @symfony ruleset in php-cs-fixer 3.95.24 enforces nullable_type_declaration with the question_mark style. The dev constraint is ^3.95.1, so the rule arrived by a floating minor rather than by any change here — the file has been unchanged since it was merged green. Only the php-cs-fixer job would have caught it, and the scheduled runs cover phpstan, phpunit and functional tests only, so it went unnoticed. No behavior change; ?int and null|int are the same type.
|
Ok, back from vacation. I did a ninja release for this package to lock it out of >2.6.1 so it doesnt break while this is worked on. There is another path in this, and that is removing the specific rules for twig and phpunit maybe. Also kinda interested in how this works. A rule tells us: ">=10.1.0", does that mean it is selected on anything above, or anything above but belore next major? So does I wouldn't mind to much to drop the set references, but am a little worried to throw things around again with how the sets work. |
|
Welcome back :) many news in Rector. Just the >=. Checkout my PRs in Laravel Rector package, where I did the same upgrade. |
Every rule that declares a ComposerPackageConstraint stated only the version its deprecation was introduced in, so it stayed active forever: a Drupal 8 rule still ran against a Drupal 11 codebase. Each now retires one major after the API was removed. Removal versions come from the version matrix, which reads them from core's own deprecation notices or, where the symbol is already gone from an 11.x tree, from the commit that deleted it plus the first release tag containing that commit. The bound is the major *after* removal, never the removal itself. Rector reads source, not runtime: a codebase can still contain a removed call in dead code or an unported module, and those are exactly the calls worth rewriting. Bounding at the removal version would switch the rule off at the moment the code became broken. Five constraints stay open. Three have no removal version in core to bind to (ProtectedStaticModulesPropertyRector, ShouldCallParentMethodsRector, RemoveStateCacheSettingRector). The other two are part of the six rules whose constraint names drupal/core but whose deprecation belongs to PHPUnit or Symfony; they need the right package before a bound means anything. Depends on the preceding commit: without the per-major test pin these bounds fail 35 of the project's own rule tests, because the shared stub reported a core newer than a D8/D9 rule's upper bound.
RenameClassRector and RenameStaticMethodRector are Rector's own classes, but the registrations, the deprecations and the change records are all Drupal's, so they bound like any other rule. Six of the seven now close: L505 Bytes::toInt >=9.1.0 <11.0.0 removed 10.0.0 L792 MatchingRouteNotFoundException >=11.1.0 <13.0.0 removed 12.0.0 L875 AliasWhitelist >=11.1.0 <13.0.0 removed 12.0.0 L1162 pgsql QueryFactory, migrate >=11.2.0 <13.0.0 removed 12.0.0 L1460 WorkspaceAssociation >=11.3.0 <13.0.0 removed 12.0.0 L2121 NodeViewController >=11.4.0 <14.0.0 removed 13.0.0 Four of those removal versions were stated in the surrounding code comments rather than read from core. All four were re-checked against core's own notices; three held, one did not. The one that did not is the block_content Access group, and the comment was wrong three ways. Drupal\block_content\Access\* was @internal and was MOVED to Drupal\Core\Access\* in 2d8018be0a8, first released in 11.2.0 -- not deprecated in 11.3.0 and removed in 12.0.0. The commit adds no class_alias and no @deprecated marker, so no deprecation cycle ever existed, which is how it landed mid-major without breaching policy. That makes it a genuine open-ended case, but for the opposite reason to the one recorded: code on the old path breaks from 11.2.0 and stays broken, so the rewrite keeps its value indefinitely. Its floor was also a minor too late, withholding the rule from 11.2 sites that already need it, so it moves >=11.3.0 -> >=11.2.0. The comment and its stale TODO are corrected. Set registrations now 113 of 124 bounded.
…noyances. Officle Drupal 9 support ended 3 years ago
…ser-based docs The composer-based.php docblock and the README still described each rule as bound to the exact version its deprecation was introduced in, and the docblock still referred to Symfony/PHPUnit sets the per-minor configs no longer pull in. - describe the bound as a range: introduced version up to the major after the removal - say that class-bound rules are also dropped past their upper bound and on a dev-main checkout, while configurable rules are bound only in this set - warn that withComposerBased(symfony: true) applies Symfony 7 rules on a Drupal 11 site, which breaks code that still supports Drupal 10
Rector 2.6.5 made withSetProviders() a no-op and switched withComposerBased(drupal: true) to DrupalSetList::COMPOSER_BASED, which no palantirnet/drupal-rector release ships yet (palantirnet/drupal-rector#419). Since drupal-rector only requires rector/rector ^2, a fresh install resolved to 2.7.0 and the skill's config loaded no Drupal rules without any error. - Require rector/rector ">=2.5.0 <2.6.5" alongside drupal-rector, which also lifts projects stuck on 2.0-2.4 where the drupal: parameter is a fatal error - Check rector list-rules for DrupalRector rules before trusting an empty dry-run - Correct the breaking-sets claim: since drupal-rector 1.1.2 they sit in a separate group that drupal: true does not load - PHPStan 2 is required by every Rector 2.x, not 2.5 specifically
Rector 2.8 only prints nodes a rule returns. In the keep-both path the rule appended the attribute but returned null, so the attribute was silently dropped.
Inline NodeFactory::createLocalMethodCall() and ParentClassScopeResolver::resolveParentClassName(), and drop the HookConvertRector fallback to AbstractRector::$file; getFile() exists on every Rector release this branch allows.
Rector 2.8.0 deleted SetProviderInterface and ComposerTriggeredSet, so the provider can no longer load. Rules carry their drupal/core version themselves; DrupalSetList::COMPOSER_BASED replaces it.
Rector 2.8 deprecated RenameStaticMethodRector and no longer runs it. Bytes::toInt() was removed in Drupal 10.0.
Existing rector.php files call withSetProviders(DrupalSetProvider::class). Since Rector 2.6.5 that call is a no-op, but with the class gone static analysis of those files (upgrade_status scans a module's own rector.php) reports "Class not found". Keep an empty class until 2.0.0.
2.6.5 is the first release where withComposerBased(drupal: true) loads DrupalSetList::COMPOSER_BASED itself and withSetProviders() no longer needs a provider class; on 2.6.0-2.6.4 the documented setup loaded nothing.
With importNames(), Rector 2.8 re-runs rules on a changed file, so a later HookConvertRector pass over the .module file also sees the LegacyHook and hook-class imports added for the wrappers, and copied them into the hook class. The class then imports its own name, a fatal "Cannot declare class ... because the name is already in use". Found in project_analysis: every hook conversion on Rector 2.8.3 produced a broken hook class.
Rector 2.8 leaves some nodes without a scope. isInBackwardsCompatibleCall() called getFunctionCallStackWithParameters() through null, which aborted the whole file: 22 test files in the project_analysis test set on Rector 2.8.3. Without a scope there is no call stack to be inside, so treat it as not wrapped.
|
Ok this took a while, and i still need the 2.8 to release the fix for the includes the setlist fix. In some configurations we might break if you update to a higher rector version. But rather have this merged into master to start work on other tickets opened recently. |
Hey, I'm improving the way Rector find composer-based rules and sets. All you need is:
ComposerPackageConstraintInterfaceruleWithConfigurationComposerVersionBound- e.g. for generic rules like rename class:The set provider, SetList, versioned set list can be dropped (in one of next PRs, not now).
It's agent generated, but it works well on Doctrine/Symfony/PHPUnit sets, see e.g.
I'll open this for feedback if it's all done correctly :) do not merge yet.
I'll make sure it's part of next Rector release and everything works as smoothly as before.
Feedback welcomed 👍
I've also added custom PHPStan rule to make sure all the rules are registered in
composer-based.phpconfig and nothing is missed by accident 😉Replaces
DrupalSetProvider— Rector deprecatedSetProviderInterfaceandComposerTriggeredSetin favour of binding rules to a package version directly - with a single generated set,config/composer-based.php, exposed asDrupalSetList::COMPOSER_BASED.