From 71f0eaf8d2f2279ce82970300589da839d697137 Mon Sep 17 00:00:00 2001 From: bjorn Date: Tue, 26 May 2026 14:18:28 +0200 Subject: [PATCH 01/15] feat: RemoveInstallSchemaSystemSequencesRector --- CHANGELOG.md | 13 ++ config/drupal-11/drupal-11.4-deprecations.php | 9 ++ ...moveInstallSchemaSystemSequencesRector.php | 138 ++++++++++++++++++ ...InstallSchemaSystemSequencesRectorTest.php | 26 ++++ .../config/configured_rule.php | 11 ++ .../fixture/array_with_other_tables.php.inc | 31 ++++ .../fixture/basic_array.php.inc | 30 ++++ .../fixture/no_change_other_module.php.inc | 33 +++++ .../fixture/no_change_other_tables.php.inc | 33 +++++ .../fixture/no_change_unrelated.php.inc | 19 +++ .../fixture/string_form.php.inc | 30 ++++ 11 files changed, 373 insertions(+) create mode 100644 src/Drupal11/Rector/Deprecation/RemoveInstallSchemaSystemSequencesRector.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/RemoveInstallSchemaSystemSequencesRector/RemoveInstallSchemaSystemSequencesRectorTest.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/RemoveInstallSchemaSystemSequencesRector/config/configured_rule.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/RemoveInstallSchemaSystemSequencesRector/fixture/array_with_other_tables.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/RemoveInstallSchemaSystemSequencesRector/fixture/basic_array.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/RemoveInstallSchemaSystemSequencesRector/fixture/no_change_other_module.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/RemoveInstallSchemaSystemSequencesRector/fixture/no_change_other_tables.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/RemoveInstallSchemaSystemSequencesRector/fixture/no_change_unrelated.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/RemoveInstallSchemaSystemSequencesRector/fixture/string_form.php.inc diff --git a/CHANGELOG.md b/CHANGELOG.md index 349976df..bfbfe970 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,19 @@ release-by-release. ### Added +- **`RemoveInstallSchemaSystemSequencesRector`** — removes deprecated + `KernelTestBase::installSchema('system', 'sequences')` calls in test + classes. The `sequences` table was deprecated in drupal:10.2.0 and + fully removed in drupal:12.0.0 via [#3335756](https://www.drupal.org/i/3335756); + the call now throws a `LogicException` on Drupal 12. The rule removes + the entire statement when the second argument is the string + `'sequences'` or an array containing only `'sequences'`; when the + array also lists other tables, only the `'sequences'` entry is + stripped. Receiver is type-guarded to `Drupal\KernelTests\KernelTestBase`, + so unrelated `installSchema()` methods on other classes are left + untouched. See change record + [#3349345](https://www.drupal.org/node/3349345). + - **`RemovePhpUnitCompatibilityTraitRector`** — removes `use Drupal\Tests\PhpUnitCompatibilityTrait;` from test class declarations. The trait was a forward-compatibility shim for PHPUnit diff --git a/config/drupal-11/drupal-11.4-deprecations.php b/config/drupal-11/drupal-11.4-deprecations.php index 91186e68..05257145 100644 --- a/config/drupal-11/drupal-11.4-deprecations.php +++ b/config/drupal-11/drupal-11.4-deprecations.php @@ -14,6 +14,7 @@ use DrupalRector\Drupal11\Rector\Deprecation\RemoveCacheExpireOverrideRector; use DrupalRector\Drupal11\Rector\Deprecation\RemoveConfigSaveTrustedDataArgRector; use DrupalRector\Drupal11\Rector\Deprecation\RemoveFilterTipsLongParamRector; +use DrupalRector\Drupal11\Rector\Deprecation\RemoveInstallSchemaSystemSequencesRector; use DrupalRector\Drupal11\Rector\Deprecation\RemoveLinkWidgetValidateTitleElementRector; use DrupalRector\Drupal11\Rector\Deprecation\RemovePhpUnitCompatibilityTraitRector; use DrupalRector\Drupal11\Rector\Deprecation\RemoveSetUriCallbackRector; @@ -522,4 +523,12 @@ $rectorConfig->ruleWithConfiguration(RemovePhpUnitCompatibilityTraitRector::class, [ new DrupalIntroducedVersionConfiguration('12.0.0'), ]); + + // https://www.drupal.org/node/3335756 + // https://www.drupal.org/node/3349345 (change record) + // KernelTestBase::installSchema('system', 'sequences') deprecated in + // drupal:10.2.0 and removed in drupal:12.0.0. The sequences table no + // longer exists in core; the call throws a LogicException on D12 and + // must be removed (or have 'sequences' stripped from its array form). + $rectorConfig->rule(RemoveInstallSchemaSystemSequencesRector::class); }; diff --git a/src/Drupal11/Rector/Deprecation/RemoveInstallSchemaSystemSequencesRector.php b/src/Drupal11/Rector/Deprecation/RemoveInstallSchemaSystemSequencesRector.php new file mode 100644 index 00000000..48938c86 --- /dev/null +++ b/src/Drupal11/Rector/Deprecation/RemoveInstallSchemaSystemSequencesRector.php @@ -0,0 +1,138 @@ +installSchema('system', ['sequences']); +CODE_BEFORE, + <<<'CODE_AFTER' + +CODE_AFTER + ), + ] + ); + } + + /** @return array> */ + public function getNodeTypes(): array + { + return [Expression::class]; + } + + /** @param Expression $node */ + public function refactor(Node $node): int|Node|null + { + if (!$node->expr instanceof MethodCall) { + return null; + } + + $methodCall = $node->expr; + + if (!$this->isName($methodCall->name, 'installSchema')) { + return null; + } + + if (!$this->isObjectType($methodCall->var, new ObjectType('Drupal\KernelTests\KernelTestBase'))) { + return null; + } + + $args = $methodCall->args; + if (count($args) < 2) { + return null; + } + + $firstArg = $args[0]; + if (!$firstArg instanceof Arg) { + return null; + } + if (!$firstArg->value instanceof String_) { + return null; + } + if ($firstArg->value->value !== 'system') { + return null; + } + + $secondArg = $args[1]; + if (!$secondArg instanceof Arg) { + return null; + } + + $tablesExpr = $secondArg->value; + + // Case 1: single string 'sequences' — remove the whole statement. + if ($tablesExpr instanceof String_ && $tablesExpr->value === 'sequences') { + return NodeVisitor::REMOVE_NODE; + } + + // Case 2: array containing 'sequences'. + if ($tablesExpr instanceof Array_) { + $newItems = []; + $foundSequences = false; + + foreach ($tablesExpr->items as $item) { + if ($item->value instanceof String_ && $item->value->value === 'sequences') { + $foundSequences = true; + continue; + } + $newItems[] = $item; + } + + if (!$foundSequences) { + return null; + } + + // Array is now empty — remove the whole statement. + if (count($newItems) === 0) { + return NodeVisitor::REMOVE_NODE; + } + + $tablesExpr->items = $newItems; + + return $node; + } + + return null; + } +} diff --git a/tests/src/Drupal11/Rector/Deprecation/RemoveInstallSchemaSystemSequencesRector/RemoveInstallSchemaSystemSequencesRectorTest.php b/tests/src/Drupal11/Rector/Deprecation/RemoveInstallSchemaSystemSequencesRector/RemoveInstallSchemaSystemSequencesRectorTest.php new file mode 100644 index 00000000..e6978ed8 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/RemoveInstallSchemaSystemSequencesRector/RemoveInstallSchemaSystemSequencesRectorTest.php @@ -0,0 +1,26 @@ +doTestFile($filePath); + } + + public static function provideData(): \Iterator + { + return self::yieldFilesFromDirectory(__DIR__.'/fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__.'/config/configured_rule.php'; + } +} diff --git a/tests/src/Drupal11/Rector/Deprecation/RemoveInstallSchemaSystemSequencesRector/config/configured_rule.php b/tests/src/Drupal11/Rector/Deprecation/RemoveInstallSchemaSystemSequencesRector/config/configured_rule.php new file mode 100644 index 00000000..6ca323ac --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/RemoveInstallSchemaSystemSequencesRector/config/configured_rule.php @@ -0,0 +1,11 @@ +installSchema('system', ['sequences', 'router']); + } +} +?> +----- +installSchema('system', ['router']); + } +} +?> diff --git a/tests/src/Drupal11/Rector/Deprecation/RemoveInstallSchemaSystemSequencesRector/fixture/basic_array.php.inc b/tests/src/Drupal11/Rector/Deprecation/RemoveInstallSchemaSystemSequencesRector/fixture/basic_array.php.inc new file mode 100644 index 00000000..59c392d9 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/RemoveInstallSchemaSystemSequencesRector/fixture/basic_array.php.inc @@ -0,0 +1,30 @@ +installSchema('system', ['sequences']); + } +} +?> +----- + diff --git a/tests/src/Drupal11/Rector/Deprecation/RemoveInstallSchemaSystemSequencesRector/fixture/no_change_other_module.php.inc b/tests/src/Drupal11/Rector/Deprecation/RemoveInstallSchemaSystemSequencesRector/fixture/no_change_other_module.php.inc new file mode 100644 index 00000000..13f46832 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/RemoveInstallSchemaSystemSequencesRector/fixture/no_change_other_module.php.inc @@ -0,0 +1,33 @@ +installSchema('node', ['sequences']); + } +} +?> +----- +installSchema('node', ['sequences']); + } +} +?> diff --git a/tests/src/Drupal11/Rector/Deprecation/RemoveInstallSchemaSystemSequencesRector/fixture/no_change_other_tables.php.inc b/tests/src/Drupal11/Rector/Deprecation/RemoveInstallSchemaSystemSequencesRector/fixture/no_change_other_tables.php.inc new file mode 100644 index 00000000..2830caf6 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/RemoveInstallSchemaSystemSequencesRector/fixture/no_change_other_tables.php.inc @@ -0,0 +1,33 @@ +installSchema('system', ['router', 'queue']); + } +} +?> +----- +installSchema('system', ['router', 'queue']); + } +} +?> diff --git a/tests/src/Drupal11/Rector/Deprecation/RemoveInstallSchemaSystemSequencesRector/fixture/no_change_unrelated.php.inc b/tests/src/Drupal11/Rector/Deprecation/RemoveInstallSchemaSystemSequencesRector/fixture/no_change_unrelated.php.inc new file mode 100644 index 00000000..4a5457da --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/RemoveInstallSchemaSystemSequencesRector/fixture/no_change_unrelated.php.inc @@ -0,0 +1,19 @@ +installSchema('system', 'sequences'); + $obj->installSchema('system', ['sequences']); +} +?> +----- +installSchema('system', 'sequences'); + $obj->installSchema('system', ['sequences']); +} +?> diff --git a/tests/src/Drupal11/Rector/Deprecation/RemoveInstallSchemaSystemSequencesRector/fixture/string_form.php.inc b/tests/src/Drupal11/Rector/Deprecation/RemoveInstallSchemaSystemSequencesRector/fixture/string_form.php.inc new file mode 100644 index 00000000..03dd27dd --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/RemoveInstallSchemaSystemSequencesRector/fixture/string_form.php.inc @@ -0,0 +1,30 @@ +installSchema('system', 'sequences'); + } +} +?> +----- + From 04d7a5e72e719486b3c02c631f60fe583b9ec04b Mon Sep 17 00:00:00 2001 From: bjorn Date: Tue, 26 May 2026 14:19:14 +0200 Subject: [PATCH 02/15] docs: Update SKILL.md to include bootstrap file usage and contrib module restoration steps --- .claude/skills/rector-live-test/SKILL.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.claude/skills/rector-live-test/SKILL.md b/.claude/skills/rector-live-test/SKILL.md index 8a4ac94f..ba4e6ffe 100644 --- a/.claude/skills/rector-live-test/SKILL.md +++ b/.claude/skills/rector-live-test/SKILL.md @@ -159,6 +159,9 @@ use Rector\Config\RectorConfig; return static function (RectorConfig $rectorConfig): void { $rectorConfig->fileExtensions(['php', 'module', 'theme', 'install', 'profile', 'inc']); + $rectorConfig->bootstrapFiles([ + __DIR__.'/vendor/palantirnet/drupal-rector/config/drupal-phpunit-bootstrap-file.php', + ]); $rectorConfig->ruleWithConfiguration(::class, [ new DrupalIntroducedVersionConfiguration(''), ]); @@ -183,6 +186,9 @@ git -C ~/projects/drupal-rector-test checkout -- web/modules/contrib/ rm ~/projects/drupal-rector-test/rector-live-test.php ``` +If the contrib modules are not git-tracked in the test project, `git checkout` won't restore them. +Use `ddev composer reinstall drupal/ drupal/ --no-interaction` instead. + ### 5. Capture the PHPStan deprecation message While the contrib module is still installed and the pre-transform code is on From 3f7a5ebf8163d9684000d18fd4493914ace30756 Mon Sep 17 00:00:00 2001 From: bjorn Date: Tue, 26 May 2026 14:38:28 +0200 Subject: [PATCH 03/15] feat(Drupal11): Add RemoveRendererAddCacheableDependencyNonObjectRector for issue #3525388 --- config/coverage-registry.php | 4 + config/drupal-11/drupal-11.3-deprecations.php | 9 ++ ...rAddCacheableDependencyNonObjectRector.php | 83 +++++++++++++++++++ ...CacheableDependencyNonObjectRectorTest.php | 26 ++++++ .../config/configured_rule.php | 11 +++ .../fixture/basic.php.inc | 22 +++++ .../fixture/concrete_class.php.inc | 20 +++++ .../fixture/no_change_object_arg.php.inc | 23 +++++ .../fixture/no_change_unrelated.php.inc | 19 +++++ 9 files changed, 217 insertions(+) create mode 100644 src/Drupal11/Rector/Deprecation/RemoveRendererAddCacheableDependencyNonObjectRector.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/RemoveRendererAddCacheableDependencyNonObjectRector/RemoveRendererAddCacheableDependencyNonObjectRectorTest.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/RemoveRendererAddCacheableDependencyNonObjectRector/config/configured_rule.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/RemoveRendererAddCacheableDependencyNonObjectRector/fixture/basic.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/RemoveRendererAddCacheableDependencyNonObjectRector/fixture/concrete_class.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/RemoveRendererAddCacheableDependencyNonObjectRector/fixture/no_change_object_arg.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/RemoveRendererAddCacheableDependencyNonObjectRector/fixture/no_change_unrelated.php.inc diff --git a/config/coverage-registry.php b/config/coverage-registry.php index d64254c5..8db7e1df 100644 --- a/config/coverage-registry.php +++ b/config/coverage-registry.php @@ -31,6 +31,10 @@ 0 => 'Call to deprecated method setCacheKey() of class Drupal\\path_alias\\AliasManager. Deprecated in drupal:11.3.0 and is removed from drupal:13.0.0. There is no replacement.', 1 => 'Call to deprecated method writeCache() of class Drupal\\path_alias\\AliasManager. Deprecated in drupal:11.3.0 and is removed from drupal:13.0.0. There is no replacement.', ), + 'RemoveRendererAddCacheableDependencyNonObjectRector' => + array ( + 0 => 'Calling Drupal\\Core\\Render\\Renderer::addCacheableDependency() with an object that doesn\'t implement Drupal\\Core\\Cache\\CacheableDependencyInterface is deprecated in drupal:11.3.0 and will throw an error in drupal:13.0.0. See https://www.drupal.org/node/3525389', + ), 'ReplaceExpectDeprecationRector' => array ( 0 => 'Call to deprecated method expectDeprecation() of class Drupal\\KernelTests\\KernelTestBase. Deprecated in drupal:11.4.0 and is removed from drupal:12.0.0. Use $this->expectUserDeprecationMessage() or $this->expectUserDeprecationMessageMatches() instead.', diff --git a/config/drupal-11/drupal-11.3-deprecations.php b/config/drupal-11/drupal-11.3-deprecations.php index 894e3c2a..98ae3bea 100644 --- a/config/drupal-11/drupal-11.3-deprecations.php +++ b/config/drupal-11/drupal-11.3-deprecations.php @@ -8,6 +8,7 @@ use DrupalRector\Drupal11\Rector\Deprecation\LoadAllIncludesRector; use DrupalRector\Drupal11\Rector\Deprecation\NodeStorageDeprecatedMethodsRector; use DrupalRector\Drupal11\Rector\Deprecation\RemoveAliasManagerCacheMethodCallsRector; +use DrupalRector\Drupal11\Rector\Deprecation\RemoveRendererAddCacheableDependencyNonObjectRector; use DrupalRector\Drupal11\Rector\Deprecation\RemoveRootFromConvertDbUrlRector; use DrupalRector\Drupal11\Rector\Deprecation\ReplaceCommentManagerGetCountNewCommentsRector; use DrupalRector\Drupal11\Rector\Deprecation\ReplaceCommentPreviewConstantsRector; @@ -268,4 +269,12 @@ // AliasManager::setCacheKey() and AliasManager::writeCache() deprecated in drupal:11.3.0, // removed in drupal:13.0.0 with no replacement (they are no-ops). $rectorConfig->rule(RemoveAliasManagerCacheMethodCallsRector::class); + + // https://www.drupal.org/node/3525388 + // https://www.drupal.org/node/3525389 (change record) + // RendererInterface::addCacheableDependency() deprecated in drupal:11.3.0, + // throws in drupal:13.0.0, when the dependency cannot implement + // CacheableDependencyInterface. Removes calls whose dependency argument is + // provably a primitive/array (bool, int, float, string, null, array). + $rectorConfig->rule(RemoveRendererAddCacheableDependencyNonObjectRector::class); }; diff --git a/src/Drupal11/Rector/Deprecation/RemoveRendererAddCacheableDependencyNonObjectRector.php b/src/Drupal11/Rector/Deprecation/RemoveRendererAddCacheableDependencyNonObjectRector.php new file mode 100644 index 00000000..9fc7e346 --- /dev/null +++ b/src/Drupal11/Rector/Deprecation/RemoveRendererAddCacheableDependencyNonObjectRector.php @@ -0,0 +1,83 @@ +renderer->addCacheableDependency($build, false);', + '' + ), + ] + ); + } + + /** @return array> */ + public function getNodeTypes(): array + { + return [Expression::class]; + } + + public function refactor(Node $node): ?int + { + assert($node instanceof Expression); + if (!$node->expr instanceof MethodCall) { + return null; + } + + $methodCall = $node->expr; + if (!$this->isName($methodCall->name, 'addCacheableDependency')) { + return null; + } + + // RendererInterface::addCacheableDependency() takes exactly 2 arguments + // (array &$elements, $dependency). The 1-argument variant on + // RefinableCacheableDependencyInterface/BubbleableMetadata is unrelated. + if (count($methodCall->args) !== 2) { + return null; + } + + if (!$this->isObjectType($methodCall->var, new ObjectType('Drupal\Core\Render\RendererInterface'))) { + return null; + } + + // Only remove when PHPStan can prove the dependency is not an object. + $dependencyType = $this->getType($methodCall->args[1]->value); + if (!$dependencyType->isObject()->no()) { + return null; + } + + return NodeVisitor::REMOVE_NODE; + } +} diff --git a/tests/src/Drupal11/Rector/Deprecation/RemoveRendererAddCacheableDependencyNonObjectRector/RemoveRendererAddCacheableDependencyNonObjectRectorTest.php b/tests/src/Drupal11/Rector/Deprecation/RemoveRendererAddCacheableDependencyNonObjectRector/RemoveRendererAddCacheableDependencyNonObjectRectorTest.php new file mode 100644 index 00000000..88e679ce --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/RemoveRendererAddCacheableDependencyNonObjectRector/RemoveRendererAddCacheableDependencyNonObjectRectorTest.php @@ -0,0 +1,26 @@ +doTestFile($filePath); + } + + public static function provideData(): \Iterator + { + return self::yieldFilesFromDirectory(__DIR__.'/fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__.'/config/configured_rule.php'; + } +} diff --git a/tests/src/Drupal11/Rector/Deprecation/RemoveRendererAddCacheableDependencyNonObjectRector/config/configured_rule.php b/tests/src/Drupal11/Rector/Deprecation/RemoveRendererAddCacheableDependencyNonObjectRector/config/configured_rule.php new file mode 100644 index 00000000..1280abd2 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/RemoveRendererAddCacheableDependencyNonObjectRector/config/configured_rule.php @@ -0,0 +1,11 @@ +addCacheableDependency($build, false); + $renderer->addCacheableDependency($build, 42); + $renderer->addCacheableDependency($build, 'a string'); + $renderer->addCacheableDependency($build, null); + $renderer->addCacheableDependency($build, ['x' => 1]); + return $build; +} +?> +----- + diff --git a/tests/src/Drupal11/Rector/Deprecation/RemoveRendererAddCacheableDependencyNonObjectRector/fixture/concrete_class.php.inc b/tests/src/Drupal11/Rector/Deprecation/RemoveRendererAddCacheableDependencyNonObjectRector/fixture/concrete_class.php.inc new file mode 100644 index 00000000..2486c317 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/RemoveRendererAddCacheableDependencyNonObjectRector/fixture/concrete_class.php.inc @@ -0,0 +1,20 @@ +addCacheableDependency($build, true); +} +?> +----- + diff --git a/tests/src/Drupal11/Rector/Deprecation/RemoveRendererAddCacheableDependencyNonObjectRector/fixture/no_change_object_arg.php.inc b/tests/src/Drupal11/Rector/Deprecation/RemoveRendererAddCacheableDependencyNonObjectRector/fixture/no_change_object_arg.php.inc new file mode 100644 index 00000000..ca58b7c5 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/RemoveRendererAddCacheableDependencyNonObjectRector/fixture/no_change_object_arg.php.inc @@ -0,0 +1,23 @@ +addCacheableDependency($build, $dep); + $renderer->addCacheableDependency($build, new \stdClass()); +} +?> +----- +addCacheableDependency($build, $dep); + $renderer->addCacheableDependency($build, new \stdClass()); +} +?> diff --git a/tests/src/Drupal11/Rector/Deprecation/RemoveRendererAddCacheableDependencyNonObjectRector/fixture/no_change_unrelated.php.inc b/tests/src/Drupal11/Rector/Deprecation/RemoveRendererAddCacheableDependencyNonObjectRector/fixture/no_change_unrelated.php.inc new file mode 100644 index 00000000..0898a88f --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/RemoveRendererAddCacheableDependencyNonObjectRector/fixture/no_change_unrelated.php.inc @@ -0,0 +1,19 @@ +addCacheableDependency($build, false); +} +?> +----- +addCacheableDependency($build, false); +} +?> From 5fcf41e5171211141fd29376a76e000e9b0953ea Mon Sep 17 00:00:00 2001 From: bjorn Date: Tue, 26 May 2026 14:38:34 +0200 Subject: [PATCH 04/15] docs: changelog entry for RemoveRendererAddCacheableDependencyNonObjectRector --- CHANGELOG.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bfbfe970..249d0cf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,28 @@ release-by-release. ### Added +- **`RemoveRendererAddCacheableDependencyNonObjectRector`** — deletes calls + to `RendererInterface::addCacheableDependency($elements, $dependency)` + whose second argument is statically provable to be a non-object + (`bool`, `int`, `float`, `string`, `null`, or `array`). Passing such + values was deprecated in drupal:11.3.0 and will throw in + drupal:13.0.0; at runtime it silently sets `max-age = 0` on the + render array, making the page uncacheable for no useful gain. The + rector matches at statement level so the entire expression is + removed, never partially rewritten. The receiver is type-guarded to + `\Drupal\Core\Render\RendererInterface` (catching the concrete + `Renderer` and any other implementer), and the argument count must + be exactly two — this distinguishes the call from the single-argument + `addCacheableDependency()` defined on `BubbleableMetadata` and + `RefinableCacheableDependencyInterface`. The PHPStan type of the + dependency must satisfy `isObject()->no()`, so any call where the + argument might be an object at runtime (variables typed as configs, + entities, or `mixed`) is left untouched — the rector targets only + the unambiguous primitive-passing mistake the deprecation was added + to flag. No BC wrapping is needed because the removed call is a + silent uncacheability bug on every Drupal version. + [#3525388](https://www.drupal.org/i/3525388) / + [CR](https://www.drupal.org/node/3525389). - **`RemoveInstallSchemaSystemSequencesRector`** — removes deprecated `KernelTestBase::installSchema('system', 'sequences')` calls in test classes. The `sequences` table was deprecated in drupal:10.2.0 and From 3d58b19c1e7ba3c4c66a5c9ebe205ac656be306e Mon Sep 17 00:00:00 2001 From: bjorn Date: Wed, 27 May 2026 10:33:33 +0200 Subject: [PATCH 05/15] feat(Drupal11): Add GroupLegacyToIgnoreDeprecationsRector for issue #3417066 --- config/drupal-11/drupal-11.0-deprecations.php | 7 ++ .../GroupLegacyToIgnoreDeprecationsRector.php | 113 ++++++++++++++++++ ...upLegacyToIgnoreDeprecationsRectorTest.php | 26 ++++ .../config/configured_rule.php | 11 ++ .../fixture/basic.php.inc | 37 ++++++ .../fixture/class_level.php.inc | 34 ++++++ .../no_change_already_has_attribute.php.inc | 18 +++ .../fixture/no_change_no_legacy.php.inc | 18 +++ 8 files changed, 264 insertions(+) create mode 100644 src/Drupal11/Rector/Deprecation/GroupLegacyToIgnoreDeprecationsRector.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/GroupLegacyToIgnoreDeprecationsRector/GroupLegacyToIgnoreDeprecationsRectorTest.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/GroupLegacyToIgnoreDeprecationsRector/config/configured_rule.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/GroupLegacyToIgnoreDeprecationsRector/fixture/basic.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/GroupLegacyToIgnoreDeprecationsRector/fixture/class_level.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/GroupLegacyToIgnoreDeprecationsRector/fixture/no_change_already_has_attribute.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/GroupLegacyToIgnoreDeprecationsRector/fixture/no_change_no_legacy.php.inc diff --git a/config/drupal-11/drupal-11.0-deprecations.php b/config/drupal-11/drupal-11.0-deprecations.php index e85c68df..68a33925 100644 --- a/config/drupal-11/drupal-11.0-deprecations.php +++ b/config/drupal-11/drupal-11.0-deprecations.php @@ -4,6 +4,7 @@ use DrupalRector\Drupal10\Rector\Deprecation\ReplaceRequestTimeConstantRector; use DrupalRector\Drupal11\Rector\Deprecation\GetNameToNameRector; +use DrupalRector\Drupal11\Rector\Deprecation\GroupLegacyToIgnoreDeprecationsRector; use DrupalRector\Drupal11\Rector\Deprecation\MigrateSqlGetMigrationPluginManagerRector; use DrupalRector\Drupal11\Rector\Deprecation\RemoveStateCacheSettingRector; use DrupalRector\Drupal11\Rector\Deprecation\StripMigrationDependenciesExpandArgRector; @@ -46,4 +47,10 @@ $rectorConfig->ruleWithConfiguration(MigrateSqlGetMigrationPluginManagerRector::class, [ new DrupalIntroducedVersionConfiguration('11.0.0'), ]); + + // https://www.drupal.org/node/3417066 + // @group legacy docblock annotation deprecated in drupal:10.3.0, removed in drupal:11.0.0. + // Drupal 11 drops symfony/phpunit-bridge in favour of PHPUnit 10, whose native + // #[\PHPUnit\Framework\Attributes\IgnoreDeprecations] attribute supersedes it. + $rectorConfig->rule(GroupLegacyToIgnoreDeprecationsRector::class); }; diff --git a/src/Drupal11/Rector/Deprecation/GroupLegacyToIgnoreDeprecationsRector.php b/src/Drupal11/Rector/Deprecation/GroupLegacyToIgnoreDeprecationsRector.php new file mode 100644 index 00000000..ab790233 --- /dev/null +++ b/src/Drupal11/Rector/Deprecation/GroupLegacyToIgnoreDeprecationsRector.php @@ -0,0 +1,113 @@ +> */ + public function getNodeTypes(): array + { + return [ClassMethod::class, Class_::class]; + } + + /** @param ClassMethod|Class_ $node */ + public function refactor(Node $node): ?Node + { + $docComment = $node->getDocComment(); + if ($docComment === null) { + return null; + } + + $docText = $docComment->getText(); + if (!str_contains($docText, '@group legacy')) { + return null; + } + + foreach ($node->attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + $name = $this->getName($attr->name); + if ($name === 'PHPUnit\\Framework\\Attributes\\IgnoreDeprecations' + || $name === 'IgnoreDeprecations' + ) { + return null; + } + } + } + + $node->attrGroups[] = new AttributeGroup([ + new Attribute(new FullyQualified('PHPUnit\\Framework\\Attributes\\IgnoreDeprecations')), + ]); + + $newDocText = preg_replace('/^[ \t]*\*[ \t]*@group legacy[ \t]*\r?\n/m', '', $docText); + $newDocText = preg_replace('/\n[ \t]*\*[ \t]*\n([ \t]*\*\/)$/', "\n$1", $newDocText); + + if (preg_match('/^\/\*\*\s*\*\/\s*$/', trim($newDocText))) { + $node->setAttribute('comments', []); + } else { + $node->setAttribute('comments', [new Doc($newDocText)]); + } + + return $node; + } +} diff --git a/tests/src/Drupal11/Rector/Deprecation/GroupLegacyToIgnoreDeprecationsRector/GroupLegacyToIgnoreDeprecationsRectorTest.php b/tests/src/Drupal11/Rector/Deprecation/GroupLegacyToIgnoreDeprecationsRector/GroupLegacyToIgnoreDeprecationsRectorTest.php new file mode 100644 index 00000000..e8a0562a --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/GroupLegacyToIgnoreDeprecationsRector/GroupLegacyToIgnoreDeprecationsRectorTest.php @@ -0,0 +1,26 @@ +doTestFile($filePath); + } + + public static function provideData(): \Iterator + { + return self::yieldFilesFromDirectory(__DIR__.'/fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__.'/config/configured_rule.php'; + } +} diff --git a/tests/src/Drupal11/Rector/Deprecation/GroupLegacyToIgnoreDeprecationsRector/config/configured_rule.php b/tests/src/Drupal11/Rector/Deprecation/GroupLegacyToIgnoreDeprecationsRector/config/configured_rule.php new file mode 100644 index 00000000..9ea2b777 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/GroupLegacyToIgnoreDeprecationsRector/config/configured_rule.php @@ -0,0 +1,11 @@ + +----- + diff --git a/tests/src/Drupal11/Rector/Deprecation/GroupLegacyToIgnoreDeprecationsRector/fixture/class_level.php.inc b/tests/src/Drupal11/Rector/Deprecation/GroupLegacyToIgnoreDeprecationsRector/fixture/class_level.php.inc new file mode 100644 index 00000000..9cb2440f --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/GroupLegacyToIgnoreDeprecationsRector/fixture/class_level.php.inc @@ -0,0 +1,34 @@ + +----- + diff --git a/tests/src/Drupal11/Rector/Deprecation/GroupLegacyToIgnoreDeprecationsRector/fixture/no_change_already_has_attribute.php.inc b/tests/src/Drupal11/Rector/Deprecation/GroupLegacyToIgnoreDeprecationsRector/fixture/no_change_already_has_attribute.php.inc new file mode 100644 index 00000000..d819019d --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/GroupLegacyToIgnoreDeprecationsRector/fixture/no_change_already_has_attribute.php.inc @@ -0,0 +1,18 @@ + diff --git a/tests/src/Drupal11/Rector/Deprecation/GroupLegacyToIgnoreDeprecationsRector/fixture/no_change_no_legacy.php.inc b/tests/src/Drupal11/Rector/Deprecation/GroupLegacyToIgnoreDeprecationsRector/fixture/no_change_no_legacy.php.inc new file mode 100644 index 00000000..13182699 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/GroupLegacyToIgnoreDeprecationsRector/fixture/no_change_no_legacy.php.inc @@ -0,0 +1,18 @@ + From 8b04f5cf31095a8812d0401e92a14b3f6c4fdde3 Mon Sep 17 00:00:00 2001 From: bjorn Date: Wed, 27 May 2026 10:34:09 +0200 Subject: [PATCH 06/15] feat(Drupal11): Add I18nQueryTrait class rename for issue #3258581 --- CHANGELOG.md | 14 ++++++++++++++ config/drupal-11/drupal-11.2-deprecations.php | 5 +++++ 2 files changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 249d0cf4..70dac99d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -180,6 +180,19 @@ release-by-release. `RenameClassRector` in `drupal-11.3-deprecations.php`. [#3571874](https://www.drupal.org/i/3571874) / [CR](https://www.drupal.org/node/3527501). +- Class-rename entry for `I18nQueryTrait`: + `Drupal\content_translation\Plugin\migrate\source\I18nQueryTrait` → + `Drupal\migrate_drupal\Plugin\migrate\source\I18nQueryTrait`. The trait was + moved because it is only needed by `migrate_drupal` source plugins, not by + `content_translation` itself. Deprecated in drupal:11.2.0, removed in + drupal:12.0.0; registered via Rector's built-in `RenameClassRector` in + `drupal-11.2-deprecations.php`. When the trait is used via a short-name + `use I18nQueryTrait;` import, `RenameClassRector` rewrites the class-body + trait composition to the fully-qualified new name but leaves the old `use` + import statement as dead code — a separate dead-import pass (PHPStan or + PHP-CS-Fixer) can clean it up. + [#3258581](https://www.drupal.org/i/3258581) / + [CR](https://www.drupal.org/node/3439256). ## [1.0.0-beta1] — 2026-05-25 @@ -318,6 +331,7 @@ distinct deprecations in the same minor. | `ReplaceEntityOriginalPropertyRector` | `$entity->original` magic property → `$entity->getOriginal()` / `setOriginal()`; nullsafe-aware | [node/3571065](https://www.drupal.org/node/3571065) | | `RenameStopProceduralHookScanRector` | `#[StopProceduralHookScan]` → `#[ProceduralHookScanStop]` | [node/3495943](https://www.drupal.org/node/3495943) | | `RenameClassRector` | `Drupal\Core\Entity\Query\Sql\pgsql\*` → `Drupal\pgsql\Entity\Query\*`; `jsonapi` ResourceResponseValidator move | [node/3488572](https://www.drupal.org/node/3488572) | +| `RenameClassRector` | `content_translation\Plugin\migrate\source\I18nQueryTrait` → `migrate_drupal\Plugin\migrate\source\I18nQueryTrait` | [node/3439256](https://www.drupal.org/node/3439256) | | `RemoveCacheTagChecksumAssertionsRector` | Remove `CacheTagChecksumCount` / `CacheTagIsValidCount` performance-trait assertions | [node/3511149](https://www.drupal.org/node/3511149) | | `RemoveRootFromCreateConnectionOptionsFromUrlRector` | `Connection::createConnectionOptionsFromUrl()` — drop `$root` parameter | [node/3511287](https://www.drupal.org/node/3511287) | | `ClassConstantToClassConstantRector` | `SystemManager::REQUIREMENT_*` → `RequirementSeverity::*` | [node/3410939](https://www.drupal.org/node/3410939) | diff --git a/config/drupal-11/drupal-11.2-deprecations.php b/config/drupal-11/drupal-11.2-deprecations.php index 371c111f..a60b01b4 100644 --- a/config/drupal-11/drupal-11.2-deprecations.php +++ b/config/drupal-11/drupal-11.2-deprecations.php @@ -228,12 +228,17 @@ // https://www.drupal.org/node/3498916 (change record) // Drupal\migrate_drupal\Plugin\migrate\source\ContentEntity/ContentEntityDeriver deprecated in drupal:11.2.0, // removed in drupal:12.0.0. Moved to Drupal\migrate namespace. + // https://www.drupal.org/node/3258581 + // https://www.drupal.org/node/3439256 (change record) + // Drupal\content_translation\Plugin\migrate\source\I18nQueryTrait deprecated in drupal:11.2.0, + // removed in drupal:12.0.0. Moved to Drupal\migrate_drupal namespace. $rectorConfig->ruleWithConfiguration(RenameClassRector::class, [ 'Drupal\Core\Entity\Query\Sql\pgsql\QueryFactory' => 'Drupal\pgsql\EntityQuery\QueryFactory', 'Drupal\Core\Entity\Query\Sql\pgsql\Condition' => 'Drupal\pgsql\EntityQuery\Condition', 'Drupal\jsonapi\EventSubscriber\ResourceResponseValidator' => 'Drupal\jsonapi_response_validator\EventSubscriber\ResourceResponseValidator', 'Drupal\migrate_drupal\Plugin\migrate\source\ContentEntity' => 'Drupal\migrate\Plugin\migrate\source\ContentEntity', 'Drupal\migrate_drupal\Plugin\migrate\source\ContentEntityDeriver' => 'Drupal\migrate\Plugin\migrate\source\ContentEntityDeriver', + 'Drupal\content_translation\Plugin\migrate\source\I18nQueryTrait' => 'Drupal\migrate_drupal\Plugin\migrate\source\I18nQueryTrait', ]); // https://www.drupal.org/node/3511123 From 2d420f522ac47351a2ed74f595d6946d3e871f57 Mon Sep 17 00:00:00 2001 From: bjorn Date: Wed, 27 May 2026 10:43:50 +0200 Subject: [PATCH 07/15] feat(Drupal11): Add LibraryDiscovery class rename for issue #3571057 --- CHANGELOG.md | 11 +++++++++++ config/drupal-11/drupal-11.1-deprecations.php | 15 +++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70dac99d..aa15b2aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -193,6 +193,16 @@ release-by-release. PHP-CS-Fixer) can clean it up. [#3258581](https://www.drupal.org/i/3258581) / [CR](https://www.drupal.org/node/3439256). +- Class-rename entry for `LibraryDiscovery`: + `Drupal\Core\Asset\LibraryDiscovery` → `Drupal\Core\Asset\LibraryDiscoveryInterface`. + The concrete class was deprecated in drupal:11.1.0 and removed in drupal:12.0.0; + the `library.discovery` service is now backed by `LibraryDiscoveryCollector`, + so consumer code should type-hint the interface. Registered via Rector's + built-in `RenameClassRector` in `drupal-11.1-deprecations.php`. + `LibraryDiscoveryInterface` has existed since Drupal 10.0.x, so the rewrite is + safe across all supported Drupal 10 and 11 minors. + [#3571057](https://www.drupal.org/i/3571057) / + [CR](https://www.drupal.org/node/3462970). ## [1.0.0-beta1] — 2026-05-25 @@ -298,6 +308,7 @@ distinct deprecations in the same minor. |---|---|---| | `PluginBaseIsConfigurableRector` | `PluginBase::isConfigurable()` → `instanceof ConfigurableInterface` check | [node/2946122](https://www.drupal.org/node/2946122) | | `RenameClassRector` | `AliasWhitelist[Interface]` → `AliasPrefixList[Interface]`; `MatchingRouteNotFoundException` → `ResourceNotFoundException` | [node/3467559](https://www.drupal.org/node/3467559) | +| `RenameClassRector` | `Drupal\Core\Asset\LibraryDiscovery` → `LibraryDiscoveryInterface` | [node/3462970](https://www.drupal.org/node/3462970) | | `MethodToMethodWithCheckRector` | `AliasManager::pathAliasWhitelistRebuild()` → `pathAliasPrefixListRebuild()` | [node/3467559](https://www.drupal.org/node/3467559) | | `RemoveModuleHandlerDeprecatedMethodsRector` | `ModuleHandler::writeCache()` removed; `getHookInfo()` returns `[]` | [node/3368812](https://www.drupal.org/node/3368812) | | `ReplaceLocaleConfigBatchFunctionsRector` | Rename `locale_config_batch_set_config_langcodes()` and `locale_config_batch_refresh_name()` | [node/3575254](https://www.drupal.org/node/3575254) | diff --git a/config/drupal-11/drupal-11.1-deprecations.php b/config/drupal-11/drupal-11.1-deprecations.php index 37765b8f..c9c15351 100644 --- a/config/drupal-11/drupal-11.1-deprecations.php +++ b/config/drupal-11/drupal-11.1-deprecations.php @@ -34,10 +34,25 @@ // Replaced by AliasPrefixList and AliasPrefixListInterface. // AliasManager::pathAliasWhitelistRebuild() deprecated in drupal:11.1.0, removed in drupal:12.0.0. // Replaced by pathAliasPrefixListRebuild(). + // https://www.drupal.org/node/3571057 + // https://www.drupal.org/node/3462970 (change record) + // Drupal\Core\Asset\LibraryDiscovery deprecated in drupal:11.1.0, removed in drupal:12.0.0. + // Consumer code should type-hint LibraryDiscoveryInterface; the library.discovery + // service is now backed by LibraryDiscoveryCollector. + // + // TODO PHPSTAN_MESSAGES RenameClassRector: class-deprecation messages from + // phpstan-deprecation-rules include consumer-specific context, so they + // can't be exact-matched by upgrade_status. Captured shape against + // drupal/style_selector 2.0.x: + // "Parameter $library_discovery of method ::__construct() has + // typehint with deprecated class Drupal\Core\Asset\LibraryDiscovery: + // \Drupal\Core\Asset\LibraryDiscoveryCollector instead." + // The stable suffix is "deprecated class Drupal\Core\Asset\LibraryDiscovery: …". $rectorConfig->ruleWithConfiguration(RenameClassRector::class, [ 'Drupal\path_alias\AliasWhitelist' => 'Drupal\path_alias\AliasPrefixList', 'Drupal\path_alias\AliasWhitelistInterface' => 'Drupal\path_alias\AliasPrefixListInterface', 'Drupal\Core\Routing\MatchingRouteNotFoundException' => 'Symfony\Component\Routing\Exception\ResourceNotFoundException', + 'Drupal\Core\Asset\LibraryDiscovery' => 'Drupal\Core\Asset\LibraryDiscoveryInterface', ]); $rectorConfig->ruleWithConfiguration(MethodToMethodWithCheckRector::class, [ new MethodToMethodWithCheckConfiguration('Drupal\path_alias\AliasManager', 'pathAliasWhitelistRebuild', 'pathAliasPrefixListRebuild'), From fa5219c7425d20c678ff231f84e0ae13184f49ec Mon Sep 17 00:00:00 2001 From: bjorn Date: Wed, 27 May 2026 10:58:38 +0200 Subject: [PATCH 08/15] docs: fixup cr/issue links for rules --- CHANGELOG.md | 3 ++- config/drupal-11/drupal-11.1-deprecations.php | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa15b2aa..d4bbd1c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -201,7 +201,8 @@ release-by-release. built-in `RenameClassRector` in `drupal-11.1-deprecations.php`. `LibraryDiscoveryInterface` has existed since Drupal 10.0.x, so the rewrite is safe across all supported Drupal 10 and 11 minors. - [#3571057](https://www.drupal.org/i/3571057) / + [#3462871](https://www.drupal.org/i/3462871) (deprecation) / + [#3571057](https://www.drupal.org/i/3571057) (removal) / [CR](https://www.drupal.org/node/3462970). ## [1.0.0-beta1] — 2026-05-25 diff --git a/config/drupal-11/drupal-11.1-deprecations.php b/config/drupal-11/drupal-11.1-deprecations.php index c9c15351..0a407a51 100644 --- a/config/drupal-11/drupal-11.1-deprecations.php +++ b/config/drupal-11/drupal-11.1-deprecations.php @@ -34,7 +34,8 @@ // Replaced by AliasPrefixList and AliasPrefixListInterface. // AliasManager::pathAliasWhitelistRebuild() deprecated in drupal:11.1.0, removed in drupal:12.0.0. // Replaced by pathAliasPrefixListRebuild(). - // https://www.drupal.org/node/3571057 + // https://www.drupal.org/node/3462871 (deprecation issue) + // https://www.drupal.org/node/3571057 (removal issue, drupal:12.0.0) // https://www.drupal.org/node/3462970 (change record) // Drupal\Core\Asset\LibraryDiscovery deprecated in drupal:11.1.0, removed in drupal:12.0.0. // Consumer code should type-hint LibraryDiscoveryInterface; the library.discovery From 422cb4a548ccea7a97f44b7f13ebfc6e0218272c Mon Sep 17 00:00:00 2001 From: bjorn Date: Wed, 27 May 2026 11:40:10 +0200 Subject: [PATCH 09/15] docs: changelog entry for GroupLegacyToIgnoreDeprecationsRector --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4bbd1c7..158a062a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,24 @@ release-by-release. orphan top-of-file `use Drupal\Tests\PhpUnitCompatibilityTrait;` import is left in place — PHP never resolves an unused alias, so it remains harmless on D12; cleanup is optional and out of scope. +- **`GroupLegacyToIgnoreDeprecationsRector`** — replaces the + `@group legacy` PHPDoc annotation with PHPUnit 10's native + `#[\PHPUnit\Framework\Attributes\IgnoreDeprecations]` attribute on + both method- and class-level test declarations. Drupal 11 dropped the + `symfony/phpunit-bridge` dependency and adopted PHPUnit 10, whose + attribute supersedes the bridge's docblock annotation. A Drupal shim + preserves the annotation form for BC, so the rewrite is purely a + forward-compatibility cleanup rather than a hard requirement, but + test classes that already declare the attribute are skipped so the + rule is idempotent. The rector strips just the `@group legacy` line + from the docblock — surrounding annotations (`@covers`, `@dataProvider`, + description text) are preserved — and inserts the attribute + immediately above the method or class declaration. PHPStan cannot + surface this deprecation because `@group legacy` is a docblock + convention, not a code-level `@deprecated` symbol; this rule must be + applied proactively as part of a PHPUnit 10 migration. + [#3417066](https://www.drupal.org/i/3417066) / + [related: #3365413](https://www.drupal.org/i/3365413). - **`RemoveAliasManagerCacheMethodCallsRector`** — deletes calls to `AliasManager::setCacheKey()` and `AliasManager::writeCache()`. Both methods were deprecated in drupal:11.3.0 and are removed in From 089d7b5324eea7fc5f48928f93c7b43b0165676b Mon Sep 17 00:00:00 2001 From: bjorn Date: Wed, 27 May 2026 12:15:38 +0200 Subject: [PATCH 10/15] feat(Drupal11): Add RemoveToolkitArgFromImageToolkitOperationConstructorRector for issue #3559481 Removes the deprecated ImageToolkitInterface $toolkit 4th constructor argument from ImageToolkitOperationBase subclasses and strips it from the matching parent::__construct() call. Deprecated in drupal:11.4.0 and removed in drupal:13.0.0; the plugin manager now injects the toolkit via setToolkit() after instantiation to enable constructor autowiring. The rector only fires when the subclass constructor has at least five parameters, the 4th is typed exactly as \Drupal\Core\ImageToolkit\ImageToolkitInterface, and the $toolkit variable appears exactly once in the constructor body (as the 4th argument of parent::__construct()). Classes that reference $toolkit elsewhere in the constructor are left untouched. @see https://www.drupal.org/node/3559481 @see https://www.drupal.org/node/3562304 --- config/drupal-11/drupal-11.4-deprecations.php | 7 + ...ImageToolkitOperationConstructorRector.php | 147 ++++++++++++++++++ ...eToolkitOperationConstructorRectorTest.php | 29 ++++ .../config/configured_rule.php | 10 ++ .../fixture/basic.php.inc | 31 ++++ .../fixture/no_change_no_constructor.php.inc | 37 +++++ .../no_change_toolkit_used_in_body.php.inc | 33 ++++ .../fixture/no_change_unrelated_class.php.inc | 27 ++++ 8 files changed, 321 insertions(+) create mode 100644 src/Drupal11/Rector/Deprecation/RemoveToolkitArgFromImageToolkitOperationConstructorRector.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/RemoveToolkitArgFromImageToolkitOperationConstructorRector/RemoveToolkitArgFromImageToolkitOperationConstructorRectorTest.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/RemoveToolkitArgFromImageToolkitOperationConstructorRector/config/configured_rule.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/RemoveToolkitArgFromImageToolkitOperationConstructorRector/fixture/basic.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/RemoveToolkitArgFromImageToolkitOperationConstructorRector/fixture/no_change_no_constructor.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/RemoveToolkitArgFromImageToolkitOperationConstructorRector/fixture/no_change_toolkit_used_in_body.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/RemoveToolkitArgFromImageToolkitOperationConstructorRector/fixture/no_change_unrelated_class.php.inc diff --git a/config/drupal-11/drupal-11.4-deprecations.php b/config/drupal-11/drupal-11.4-deprecations.php index 05257145..aa440e8e 100644 --- a/config/drupal-11/drupal-11.4-deprecations.php +++ b/config/drupal-11/drupal-11.4-deprecations.php @@ -18,6 +18,7 @@ use DrupalRector\Drupal11\Rector\Deprecation\RemoveLinkWidgetValidateTitleElementRector; use DrupalRector\Drupal11\Rector\Deprecation\RemovePhpUnitCompatibilityTraitRector; use DrupalRector\Drupal11\Rector\Deprecation\RemoveSetUriCallbackRector; +use DrupalRector\Drupal11\Rector\Deprecation\RemoveToolkitArgFromImageToolkitOperationConstructorRector; use DrupalRector\Drupal11\Rector\Deprecation\RemoveTrustDataCallRector; use DrupalRector\Drupal11\Rector\Deprecation\RemoveViewsRowCacheKeysRector; use DrupalRector\Drupal11\Rector\Deprecation\ReplaceEntityReferenceRecursiveLimitRector; @@ -301,6 +302,12 @@ // Subclass overrides are dead code; remove them. $rectorConfig->rule(RemoveCacheExpireOverrideRector::class); + // https://www.drupal.org/node/3559481 + // https://www.drupal.org/node/3562304 (change record) + // ImageToolkitOperationBase::__construct() $toolkit argument deprecated in drupal:11.4.0, + // removed in drupal:13.0.0. Plugin manager now injects via setToolkit() for autowiring. + $rectorConfig->rule(RemoveToolkitArgFromImageToolkitOperationConstructorRector::class); + // https://www.drupal.org/node/3347842 // https://www.drupal.org/node/3348180 (change record) // trustData() deprecated in drupal:11.4.0, removed in drupal:13.0.0. Remove from fluent chains. diff --git a/src/Drupal11/Rector/Deprecation/RemoveToolkitArgFromImageToolkitOperationConstructorRector.php b/src/Drupal11/Rector/Deprecation/RemoveToolkitArgFromImageToolkitOperationConstructorRector.php new file mode 100644 index 00000000..b3ddc3f7 --- /dev/null +++ b/src/Drupal11/Rector/Deprecation/RemoveToolkitArgFromImageToolkitOperationConstructorRector.php @@ -0,0 +1,147 @@ +> */ + public function getNodeTypes(): array + { + return [Class_::class]; + } + + /** @param Class_ $node */ + public function refactor(Node $node): ?Node + { + $constructor = $node->getMethod('__construct'); + if ($constructor === null) { + return null; + } + + $params = $constructor->params; + + // We need at least 5 params (configuration, plugin_id, plugin_definition, toolkit, logger). + if (count($params) < 5) { + return null; + } + + // Verify the 4th param (index 3) is typed as ImageToolkitInterface. + $toolkitParam = $params[3]; + if ($toolkitParam->type === null) { + return null; + } + + $typeName = $this->getName($toolkitParam->type); + if ($typeName !== self::TOOLKIT_INTERFACE) { + return null; + } + + $toolkitVarName = $this->getName($toolkitParam->var); + if ($toolkitVarName === null) { + return null; + } + + // Count all usages of the $toolkit variable inside the constructor body + // to ensure it is only passed to parent::__construct(). + $toolkitUsageCount = 0; + $this->traverseNodesWithCallable($constructor->stmts ?? [], function (Node $innerNode) use ($toolkitVarName, &$toolkitUsageCount): ?Node { + if ($innerNode instanceof Variable && $this->isName($innerNode, $toolkitVarName)) { + ++$toolkitUsageCount; + } + + return null; + }); + + // If $toolkit is used more than once or not at all in body, skip. + if ($toolkitUsageCount !== 1) { + return null; + } + + // Locate parent::__construct() and confirm $toolkit is its 4th arg, + // then remove that arg. + $parentCallUpdated = false; + $this->traverseNodesWithCallable($constructor->stmts ?? [], function (Node $innerNode) use ($toolkitVarName, &$parentCallUpdated): ?Node { + if (!$innerNode instanceof StaticCall) { + return null; + } + if (!$this->isName($innerNode->class, 'parent') || !$this->isName($innerNode->name, '__construct')) { + return null; + } + if (!isset($innerNode->args[3]) || !$innerNode->args[3] instanceof Arg) { + return null; + } + $arg3Value = $innerNode->args[3]->value; + if (!$arg3Value instanceof Variable || !$this->isName($arg3Value, $toolkitVarName)) { + return null; + } + array_splice($innerNode->args, 3, 1); + $parentCallUpdated = true; + + return $innerNode; + }); + + if (!$parentCallUpdated) { + return null; + } + + // Remove the $toolkit parameter from the constructor signature. + array_splice($constructor->params, 3, 1); + + return $node; + } +} diff --git a/tests/src/Drupal11/Rector/Deprecation/RemoveToolkitArgFromImageToolkitOperationConstructorRector/RemoveToolkitArgFromImageToolkitOperationConstructorRectorTest.php b/tests/src/Drupal11/Rector/Deprecation/RemoveToolkitArgFromImageToolkitOperationConstructorRector/RemoveToolkitArgFromImageToolkitOperationConstructorRectorTest.php new file mode 100644 index 00000000..82a8919f --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/RemoveToolkitArgFromImageToolkitOperationConstructorRector/RemoveToolkitArgFromImageToolkitOperationConstructorRectorTest.php @@ -0,0 +1,29 @@ +doTestFile($filePath); + } + + /** + * @return \Iterator<> + */ + public static function provideData(): \Iterator + { + return self::yieldFilesFromDirectory(__DIR__.'/fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__.'/config/configured_rule.php'; + } +} diff --git a/tests/src/Drupal11/Rector/Deprecation/RemoveToolkitArgFromImageToolkitOperationConstructorRector/config/configured_rule.php b/tests/src/Drupal11/Rector/Deprecation/RemoveToolkitArgFromImageToolkitOperationConstructorRector/config/configured_rule.php new file mode 100644 index 00000000..52ca1127 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/RemoveToolkitArgFromImageToolkitOperationConstructorRector/config/configured_rule.php @@ -0,0 +1,10 @@ +rule(RemoveToolkitArgFromImageToolkitOperationConstructorRector::class); +}; diff --git a/tests/src/Drupal11/Rector/Deprecation/RemoveToolkitArgFromImageToolkitOperationConstructorRector/fixture/basic.php.inc b/tests/src/Drupal11/Rector/Deprecation/RemoveToolkitArgFromImageToolkitOperationConstructorRector/fixture/basic.php.inc new file mode 100644 index 00000000..6aa52672 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/RemoveToolkitArgFromImageToolkitOperationConstructorRector/fixture/basic.php.inc @@ -0,0 +1,31 @@ + +----- + diff --git a/tests/src/Drupal11/Rector/Deprecation/RemoveToolkitArgFromImageToolkitOperationConstructorRector/fixture/no_change_no_constructor.php.inc b/tests/src/Drupal11/Rector/Deprecation/RemoveToolkitArgFromImageToolkitOperationConstructorRector/fixture/no_change_no_constructor.php.inc new file mode 100644 index 00000000..2c00f67d --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/RemoveToolkitArgFromImageToolkitOperationConstructorRector/fixture/no_change_no_constructor.php.inc @@ -0,0 +1,37 @@ + +----- + diff --git a/tests/src/Drupal11/Rector/Deprecation/RemoveToolkitArgFromImageToolkitOperationConstructorRector/fixture/no_change_toolkit_used_in_body.php.inc b/tests/src/Drupal11/Rector/Deprecation/RemoveToolkitArgFromImageToolkitOperationConstructorRector/fixture/no_change_toolkit_used_in_body.php.inc new file mode 100644 index 00000000..59541c1d --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/RemoveToolkitArgFromImageToolkitOperationConstructorRector/fixture/no_change_toolkit_used_in_body.php.inc @@ -0,0 +1,33 @@ +customToolkit = $toolkit; + } +} + +?> +----- +customToolkit = $toolkit; + } +} + +?> diff --git a/tests/src/Drupal11/Rector/Deprecation/RemoveToolkitArgFromImageToolkitOperationConstructorRector/fixture/no_change_unrelated_class.php.inc b/tests/src/Drupal11/Rector/Deprecation/RemoveToolkitArgFromImageToolkitOperationConstructorRector/fixture/no_change_unrelated_class.php.inc new file mode 100644 index 00000000..469ff7c1 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/RemoveToolkitArgFromImageToolkitOperationConstructorRector/fixture/no_change_unrelated_class.php.inc @@ -0,0 +1,27 @@ + +----- + From 37ffde0b8c182c8069272995c18e2c9b8589e212 Mon Sep 17 00:00:00 2001 From: bjorn Date: Wed, 27 May 2026 12:15:43 +0200 Subject: [PATCH 11/15] docs: changelog entry for RemoveToolkitArgFromImageToolkitOperationConstructorRector --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 158a062a..b6f03d0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,24 @@ release-by-release. ### Added +- **`RemoveToolkitArgFromImageToolkitOperationConstructorRector`** — + removes the deprecated `ImageToolkitInterface $toolkit` 4th argument + from `ImageToolkitOperationBase` subclass constructors and strips it + from the matching `parent::__construct()` call. The parameter was + deprecated in drupal:11.4.0 and will be removed in drupal:13.0.0; the + plugin manager now injects the toolkit via `setToolkit()` after + instantiation, enabling constructor autowiring. The rector only fires + when the subclass constructor has at least five parameters, the 4th + is typed exactly as `\Drupal\Core\ImageToolkit\ImageToolkitInterface`, + and `$toolkit` appears exactly once in the constructor body (as the + 4th argument of `parent::__construct()`). Classes that reference + `$toolkit` anywhere else in the constructor body are left untouched + to avoid breaking code that uses the variable before `setToolkit()` + can be called. No BC wrapping is needed: the parent signature + accepts the union `LoggerInterface|ImageToolkitInterface`, so the + transformed code runs on every drupal:11.4.x+ version. + [#3559481](https://www.drupal.org/i/3559481) / + [CR](https://www.drupal.org/node/3562304). - **`RemoveRendererAddCacheableDependencyNonObjectRector`** — deletes calls to `RendererInterface::addCacheableDependency($elements, $dependency)` whose second argument is statically provable to be a non-object From 23dde628c42d2774c8935f49b13eaa2906561e30 Mon Sep 17 00:00:00 2001 From: bjorn Date: Wed, 27 May 2026 12:36:50 +0200 Subject: [PATCH 12/15] feat(Drupal11): Add EntityPermissionsRouteProviderWithCheck class rename for issue #3573870 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drupal\user\Entity\EntityPermissionsRouteProviderWithCheck → EntityPermissionsRouteProvider. Deprecated in drupal:11.1.0 and removed in drupal:12.0.0. The base provider has existed since 10.0.x, so the rewrite is safe across all supported D10 and D11 minors. Registered via Rector's built-in RenameClassRector in drupal-11.1-deprecations.php. Access-check semantics: WithCheck layered a _custom_access requirement (EntityPermissionsForm::access, also removed) on top of the base route to deny access when an entity type had no entity-specific permissions; the base provider already enforces _permission: administer permissions, so the security boundary is preserved. Only the "no permissions defined → deny" convenience check is dropped. Limitation: doctrine annotation-string references (the dominant real-world pattern) are not rewritten — RenameClassRector only touches PHP Name nodes (use/extends/implements/::class/typehints), not strings inside annotations. Contrib audit (api.tresbien.tech, 2026-05-27) found three modules using the class (content_entity_builder, aiprompt, knowledge) and zero PHP-code references, so this entry is mostly a safety net for use/extends/::class patterns. --- CHANGELOG.md | 31 +++++++++++++++++++ config/drupal-11/drupal-11.1-deprecations.php | 24 ++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6f03d0e..e9694984 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -240,6 +240,36 @@ release-by-release. [#3462871](https://www.drupal.org/i/3462871) (deprecation) / [#3571057](https://www.drupal.org/i/3571057) (removal) / [CR](https://www.drupal.org/node/3462970). +- Class-rename entry for `EntityPermissionsRouteProviderWithCheck`: + `Drupal\user\Entity\EntityPermissionsRouteProviderWithCheck` → + `Drupal\user\Entity\EntityPermissionsRouteProvider`. The `WithCheck` variant was + deprecated in drupal:11.1.0 and removed in drupal:12.0.0; the base provider has + existed since Drupal 10.0.x, so the rewrite is safe across all supported Drupal + 10 and 11 minors. Registered via Rector's built-in `RenameClassRector` in + `drupal-11.1-deprecations.php`. **Access-check semantics:** the `WithCheck` + variant layered a `_custom_access` requirement (`EntityPermissionsForm::access`, + also removed) on top of the base route to deny access when an entity type had + no entity-specific permissions; the base provider already enforces + `_permission: administer permissions`, so the security boundary is preserved + and only the "no permissions defined → deny" convenience check is dropped. + Subclass overrides that re-added the custom check are NOT rewritten — + `RenameClassRector` only updates the parent class reference, so owners of such + subclasses must port any remaining access logic into the route definition + (the new model adds permission requirements directly on the route). + **Limitation — doctrine annotation strings are not rewritten.** Real-world + contrib usage is almost exclusively the entity-annotation form + (`"permissions" = "Drupal\user\Entity\EntityPermissionsRouteProviderWithCheck"` + inside a `@ContentEntityType` / `@ConfigEntityType` docblock). + `RenameClassRector` only touches PHP `Name` nodes (`use`, `extends`, + `implements`, `::class`, typehints, `instanceof`) — it does **not** scan + string literals inside doctrine annotations. Audited against Drupal contrib + (api.tresbien.tech, 2026-05-27): three modules reference the class — all via + the annotation form — and zero contrib modules use it in PHP code. Owners of + those modules must hand-edit the annotation string; this rector is a safety + net for `use` / `extends` patterns and for entity types that switch to + PHP-attribute syntax. + [#3573870](https://www.drupal.org/i/3573870) / + [CR](https://www.drupal.org/node/3384745). ## [1.0.0-beta1] — 2026-05-25 @@ -346,6 +376,7 @@ distinct deprecations in the same minor. | `PluginBaseIsConfigurableRector` | `PluginBase::isConfigurable()` → `instanceof ConfigurableInterface` check | [node/2946122](https://www.drupal.org/node/2946122) | | `RenameClassRector` | `AliasWhitelist[Interface]` → `AliasPrefixList[Interface]`; `MatchingRouteNotFoundException` → `ResourceNotFoundException` | [node/3467559](https://www.drupal.org/node/3467559) | | `RenameClassRector` | `Drupal\Core\Asset\LibraryDiscovery` → `LibraryDiscoveryInterface` | [node/3462970](https://www.drupal.org/node/3462970) | +| `RenameClassRector` | `EntityPermissionsRouteProviderWithCheck` → `EntityPermissionsRouteProvider` | [node/3384745](https://www.drupal.org/node/3384745) | | `MethodToMethodWithCheckRector` | `AliasManager::pathAliasWhitelistRebuild()` → `pathAliasPrefixListRebuild()` | [node/3467559](https://www.drupal.org/node/3467559) | | `RemoveModuleHandlerDeprecatedMethodsRector` | `ModuleHandler::writeCache()` removed; `getHookInfo()` returns `[]` | [node/3368812](https://www.drupal.org/node/3368812) | | `ReplaceLocaleConfigBatchFunctionsRector` | Rename `locale_config_batch_set_config_langcodes()` and `locale_config_batch_refresh_name()` | [node/3575254](https://www.drupal.org/node/3575254) | diff --git a/config/drupal-11/drupal-11.1-deprecations.php b/config/drupal-11/drupal-11.1-deprecations.php index 0a407a51..3eaf0e13 100644 --- a/config/drupal-11/drupal-11.1-deprecations.php +++ b/config/drupal-11/drupal-11.1-deprecations.php @@ -49,11 +49,35 @@ // typehint with deprecated class Drupal\Core\Asset\LibraryDiscovery: // \Drupal\Core\Asset\LibraryDiscoveryCollector instead." // The stable suffix is "deprecated class Drupal\Core\Asset\LibraryDiscovery: …". + // + // https://www.drupal.org/node/3573870 + // https://www.drupal.org/node/3384745 (change record) + // Drupal\user\Entity\EntityPermissionsRouteProviderWithCheck deprecated in + // drupal:11.1.0, removed in drupal:12.0.0. Use EntityPermissionsRouteProvider + // instead. The WithCheck variant layered an `_custom_access` requirement + // (EntityPermissionsForm::access, also removed) on top of the base route to + // deny access when an entity type had no entity-specific permissions; the + // base provider already enforces `_permission: administer permissions`, so + // the security boundary is preserved and only the "no permissions defined + // → deny" convenience check is dropped. Subclass overrides that re-added + // the custom check are NOT rewritten — RenameClassRector only updates the + // parent class reference. Owners of such subclasses must port any required + // access logic into the new model (route requirement on the route + // definition). + // + // Limitation: doctrine annotation-string references (the dominant real-world + // pattern, e.g. `"permissions" = "Drupal\user\Entity\Entity…"` inside an + // entity-type docblock) are NOT rewritten — RenameClassRector only touches + // PHP Name nodes (use/extends/implements/::class/typehints), not strings + // inside annotations. Contrib audit (api.tresbien.tech, 2026-05-27) found + // three modules using the class and zero PHP-code references; this entry + // is therefore mostly a safety net for use/extends/::class patterns. $rectorConfig->ruleWithConfiguration(RenameClassRector::class, [ 'Drupal\path_alias\AliasWhitelist' => 'Drupal\path_alias\AliasPrefixList', 'Drupal\path_alias\AliasWhitelistInterface' => 'Drupal\path_alias\AliasPrefixListInterface', 'Drupal\Core\Routing\MatchingRouteNotFoundException' => 'Symfony\Component\Routing\Exception\ResourceNotFoundException', 'Drupal\Core\Asset\LibraryDiscovery' => 'Drupal\Core\Asset\LibraryDiscoveryInterface', + 'Drupal\user\Entity\EntityPermissionsRouteProviderWithCheck' => 'Drupal\user\Entity\EntityPermissionsRouteProvider', ]); $rectorConfig->ruleWithConfiguration(MethodToMethodWithCheckRector::class, [ new MethodToMethodWithCheckConfiguration('Drupal\path_alias\AliasManager', 'pathAliasWhitelistRebuild', 'pathAliasPrefixListRebuild'), From 7699665b83e7d0912e13e88632d1fa91bd8e31b7 Mon Sep 17 00:00:00 2001 From: bjorn Date: Wed, 27 May 2026 12:55:38 +0200 Subject: [PATCH 13/15] feat(Drupal11): Add ReplaceDialogClassOptionRector for issue #3571054 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrites the removed $dialog_options['dialogClass'] key to $dialog_options['classes']['ui-dialog'] in OpenDialogCommand, OpenModalDialogCommand, and OpenOffCanvasDialogCommand constructors. The 'dialogClass' option was deprecated in drupal:11.3.0 and removed in drupal:12.0.0. classes['ui-dialog'] has existed in core since 10.3.x, so the transformed output is safe across all drupal-rector–supported Drupal minors; no BC wrapping is needed. Handles three array shapes: - no existing 'classes' key → add 'classes' => ['ui-dialog' => $value] - 'classes' exists without 'ui-dialog' → insert 'ui-dialog' => $value - 'classes'['ui-dialog'] already present → concatenate string values Non-literal options (variables, builder method returns) are skipped to avoid guessing runtime values. Receiver narrowing is by FQCN match on the resolved New_->class, so unrelated constructors with similarly-shaped option arrays are left alone. OpenModalDialogCommand coverage was added after the live test surfaced wide contrib usage (more than OpenDialogCommand itself): it extends OpenDialogCommand and forwards $dialog_options to parent::__construct(), so the same deprecation fires from the parent. Live-tested against ~2,230 PHP files in 17 D11-compatible contrib modules: 4 correct transformations (entity_browser, ai/field_widget_actions, ai_seo — 3 sites in a .module file), zero false-positives. Non-matching dialogClass usages were JS-side data-dialog-options encoded strings, builder-method returns, and assignment patterns — all out of scope for a structural rector. --- CHANGELOG.md | 19 ++ config/drupal-11/drupal-11.3-deprecations.php | 10 + .../ReplaceDialogClassOptionRector.php | 203 ++++++++++++++++++ .../ReplaceDialogClassOptionRectorTest.php | 26 +++ .../config/configured_rule.php | 11 + .../append_to_existing_ui_dialog.php.inc | 23 ++ .../fixture/basic.php.inc | 23 ++ .../merge_into_existing_classes.php.inc | 23 ++ .../fixture/modal_dialog.php.inc | 23 ++ .../fixture/no_change_no_dialog_class.php.inc | 11 + .../no_change_non_literal_options.php.inc | 11 + .../fixture/no_change_unrelated_class.php.inc | 16 ++ .../fixture/off_canvas.php.inc | 23 ++ .../fixture/use_imported_class.php.inc | 27 +++ 14 files changed, 449 insertions(+) create mode 100644 src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector/ReplaceDialogClassOptionRectorTest.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector/config/configured_rule.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector/fixture/append_to_existing_ui_dialog.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector/fixture/basic.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector/fixture/merge_into_existing_classes.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector/fixture/modal_dialog.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector/fixture/no_change_no_dialog_class.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector/fixture/no_change_non_literal_options.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector/fixture/no_change_unrelated_class.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector/fixture/off_canvas.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector/fixture/use_imported_class.php.inc diff --git a/CHANGELOG.md b/CHANGELOG.md index e9694984..8627120b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,24 @@ release-by-release. ### Added +- **`ReplaceDialogClassOptionRector`** — rewrites the removed + `$dialog_options['dialogClass']` key to + `$dialog_options['classes']['ui-dialog']` in `new OpenDialogCommand(...)` and + `new OpenOffCanvasDialogCommand(...)` calls. `dialogClass` was deprecated in + drupal:11.3.0 and removed in drupal:12.0.0. Receiver narrowing is by FQCN + match on the resolved `New_->class` (`Drupal\Core\Ajax\OpenDialogCommand` or + `Drupal\Core\Ajax\OpenOffCanvasDialogCommand`), so unrelated constructors + with similarly-shaped option arrays are left alone. Handles three array + shapes: (a) no existing `classes` key → adds `'classes' => ['ui-dialog' => $value]`; + (b) `classes` exists without `ui-dialog` → adds `'ui-dialog' => $value` inside + it; (c) `classes['ui-dialog']` already present and both old/new values are + string literals → concatenates with a space. Non-literal values (variables, + function calls, dynamic arrays) at any position cause the rule to skip + rather than guess. The replacement form `classes['ui-dialog']` has existed + in core since 10.3.x, so the transformed output runs on every + drupal-rector–supported Drupal minor (D10.3+) — no BC wrapping needed. + [#3571054](https://www.drupal.org/i/3571054) / + [CR](https://www.drupal.org/node/3440844). - **`RemoveToolkitArgFromImageToolkitOperationConstructorRector`** — removes the deprecated `ImageToolkitInterface $toolkit` 4th argument from `ImageToolkitOperationBase` subclass constructors and strips it @@ -441,6 +459,7 @@ distinct deprecations in the same minor. | `ReplaceThemeGetSettingRector` | `theme_get_setting()` → `ThemeSettingsProvider` service; `_system_default_theme_features()` → `ThemeSettingsProvider::DEFAULT_THEME_FEATURES` | [node/3573896](https://www.drupal.org/node/3573896) | | `RemoveRootFromConvertDbUrlRector` | `Database::convertDbUrlToConnectionInfo($url, $root, …)` — drop the `$root` arg | [node/3511287](https://www.drupal.org/node/3511287) | | `RenameClassRector` | `workspaces.association` service / `WorkspaceAssociation*` → `workspaces.tracker` / `WorkspaceTracker*` | [node/3551450](https://www.drupal.org/node/3551450) | +| `ReplaceDialogClassOptionRector` | `OpenDialogCommand`/`OpenOffCanvasDialogCommand` `dialogClass` option → `classes['ui-dialog']` | [node/3440844](https://www.drupal.org/node/3440844) | ##### Drupal 11.4 diff --git a/config/drupal-11/drupal-11.3-deprecations.php b/config/drupal-11/drupal-11.3-deprecations.php index 98ae3bea..a577298b 100644 --- a/config/drupal-11/drupal-11.3-deprecations.php +++ b/config/drupal-11/drupal-11.3-deprecations.php @@ -12,6 +12,7 @@ use DrupalRector\Drupal11\Rector\Deprecation\RemoveRootFromConvertDbUrlRector; use DrupalRector\Drupal11\Rector\Deprecation\ReplaceCommentManagerGetCountNewCommentsRector; use DrupalRector\Drupal11\Rector\Deprecation\ReplaceCommentPreviewConstantsRector; +use DrupalRector\Drupal11\Rector\Deprecation\ReplaceDialogClassOptionRector; use DrupalRector\Drupal11\Rector\Deprecation\ReplaceNodeAccessViewAllNodesRector; use DrupalRector\Drupal11\Rector\Deprecation\ReplaceNodeAddBodyFieldRector; use DrupalRector\Drupal11\Rector\Deprecation\ReplaceNodeModuleProceduralFunctionsRector; @@ -277,4 +278,13 @@ // CacheableDependencyInterface. Removes calls whose dependency argument is // provably a primitive/array (bool, int, float, string, null, array). $rectorConfig->rule(RemoveRendererAddCacheableDependencyNonObjectRector::class); + + // https://www.drupal.org/node/3571054 + // https://www.drupal.org/node/3440844 (change record) + // OpenDialogCommand / OpenOffCanvasDialogCommand $dialog_options['dialogClass'] + // deprecated in drupal:11.3.0, removed in drupal:12.0.0. Replaced by + // $dialog_options['classes']['ui-dialog']. The replacement form has existed in + // core since 10.3.x, so the transformed output is safe on every drupal-rector– + // supported Drupal minor (D10.3+); no BC wrapper needed. + $rectorConfig->rule(ReplaceDialogClassOptionRector::class); }; diff --git a/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector.php b/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector.php new file mode 100644 index 00000000..50ee5cc4 --- /dev/null +++ b/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector.php @@ -0,0 +1,203 @@ + ...] array trigger the same deprecation + * notice from the parent constructor. + * + * @see https://www.drupal.org/node/3571054 + * @see https://www.drupal.org/node/3440844 + */ +final class ReplaceDialogClassOptionRector extends AbstractRector +{ + /** + * Map: FQCN => zero-based index of the $dialog_options argument. + */ + private const CLASS_ARG_INDEX = [ + 'Drupal\\Core\\Ajax\\OpenDialogCommand' => 3, + 'Drupal\\Core\\Ajax\\OpenModalDialogCommand' => 2, + 'Drupal\\Core\\Ajax\\OpenOffCanvasDialogCommand' => 2, + ]; + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition( + "Replace removed \$dialog_options['dialogClass'] with \$dialog_options['classes']['ui-dialog'] in OpenDialogCommand / OpenModalDialogCommand / OpenOffCanvasDialogCommand constructors (removed in drupal:12.0.0)", + [ + new CodeSample( + <<<'CODE_BEFORE' +new \Drupal\Core\Ajax\OpenDialogCommand('#my-dialog', 'Title', $content, ['dialogClass' => 'my-class', 'width' => 600]); +CODE_BEFORE, + <<<'CODE_AFTER' +new \Drupal\Core\Ajax\OpenDialogCommand('#my-dialog', 'Title', $content, ['width' => 600, 'classes' => ['ui-dialog' => 'my-class']]); +CODE_AFTER + ), + ] + ); + } + + /** @return array> */ + public function getNodeTypes(): array + { + return [New_::class]; + } + + /** @param New_ $node */ + public function refactor(Node $node): ?Node + { + $className = $this->getResolvedClassName($node); + if ($className === null || !isset(self::CLASS_ARG_INDEX[$className])) { + return null; + } + + $argIndex = self::CLASS_ARG_INDEX[$className]; + + if (!isset($node->args[$argIndex])) { + return null; + } + + $arg = $node->args[$argIndex]; + if (!$arg instanceof Node\Arg) { + return null; + } + + $optionsArray = $arg->value; + if (!$optionsArray instanceof Array_) { + return null; + } + + $dialogClassIdx = null; + $classesIdx = null; + $uiDialogIdx = null; + + foreach ($optionsArray->items as $idx => $item) { + if (!$item->key instanceof String_) { + continue; + } + + if ($item->key->value === 'dialogClass') { + $dialogClassIdx = $idx; + } elseif ($item->key->value === 'classes') { + $classesIdx = $idx; + if ($item->value instanceof Array_) { + foreach ($item->value->items as $subIdx => $subItem) { + if ($subItem->key instanceof String_ + && $subItem->key->value === 'ui-dialog' + ) { + $uiDialogIdx = $subIdx; + } + } + } + } + } + + if ($dialogClassIdx === null) { + return null; + } + + $dialogClassValue = $optionsArray->items[$dialogClassIdx]->value; + + unset($optionsArray->items[$dialogClassIdx]); + $optionsArray->items = array_values($optionsArray->items); + + if ($classesIdx === null) { + $optionsArray->items[] = new ArrayItem( + new Array_([ + new ArrayItem($dialogClassValue, new String_('ui-dialog')), + ]), + new String_('classes') + ); + } elseif ($uiDialogIdx !== null) { + // dialogClass deletion shifted the indexes — recompute the classes index. + $classesIdx = $this->findKeyIndex($optionsArray, 'classes'); + if ($classesIdx === null) { + return null; + } + $classesItem = $optionsArray->items[$classesIdx]; + if (!$classesItem->value instanceof Array_) { + return null; + } + + $uiDialogItem = $classesItem->value->items[$uiDialogIdx]; + + if (!($uiDialogItem->value instanceof String_) || !($dialogClassValue instanceof String_)) { + return null; + } + + $uiDialogItem->value = new String_( + $uiDialogItem->value->value.' '.$dialogClassValue->value + ); + } else { + $classesIdx = $this->findKeyIndex($optionsArray, 'classes'); + if ($classesIdx === null) { + return null; + } + $classesItem = $optionsArray->items[$classesIdx]; + if (!$classesItem->value instanceof Array_) { + return null; + } + + $classesItem->value->items[] = new ArrayItem( + $dialogClassValue, + new String_('ui-dialog') + ); + } + + return $node; + } + + private function getResolvedClassName(New_ $node): ?string + { + $class = $node->class; + + if ($class instanceof FullyQualified) { + return $class->toString(); + } + + if ($class instanceof Node\Name) { + $resolved = $class->getAttribute('resolvedName'); + if ($resolved instanceof FullyQualified) { + return $resolved->toString(); + } + } + + return null; + } + + private function findKeyIndex(Array_ $array, string $keyName): ?int + { + foreach ($array->items as $idx => $item) { + if ($item->key instanceof String_ && $item->key->value === $keyName) { + return $idx; + } + } + + return null; + } +} diff --git a/tests/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector/ReplaceDialogClassOptionRectorTest.php b/tests/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector/ReplaceDialogClassOptionRectorTest.php new file mode 100644 index 00000000..fd52e8a0 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector/ReplaceDialogClassOptionRectorTest.php @@ -0,0 +1,26 @@ +doTestFile($filePath); + } + + public static function provideData(): \Iterator + { + return self::yieldFilesFromDirectory(__DIR__.'/fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__.'/config/configured_rule.php'; + } +} diff --git a/tests/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector/config/configured_rule.php b/tests/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector/config/configured_rule.php new file mode 100644 index 00000000..2faa0f92 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector/config/configured_rule.php @@ -0,0 +1,11 @@ + 'extra', 'classes' => ['ui-dialog' => 'base']]); +} + +} +?> +----- + ['ui-dialog' => 'base extra']]); +} + +} +?> diff --git a/tests/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector/fixture/basic.php.inc b/tests/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector/fixture/basic.php.inc new file mode 100644 index 00000000..f805a892 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector/fixture/basic.php.inc @@ -0,0 +1,23 @@ + 'my-class', 'width' => 600]); +} + +} +?> +----- + 600, 'classes' => ['ui-dialog' => 'my-class']]); +} + +} +?> diff --git a/tests/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector/fixture/merge_into_existing_classes.php.inc b/tests/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector/fixture/merge_into_existing_classes.php.inc new file mode 100644 index 00000000..774b0ec7 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector/fixture/merge_into_existing_classes.php.inc @@ -0,0 +1,23 @@ + 'extra', 'classes' => ['ui-dialog-content' => 'pad']]); +} + +} +?> +----- + ['ui-dialog-content' => 'pad', 'ui-dialog' => 'extra']]); +} + +} +?> diff --git a/tests/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector/fixture/modal_dialog.php.inc b/tests/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector/fixture/modal_dialog.php.inc new file mode 100644 index 00000000..be4ded12 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector/fixture/modal_dialog.php.inc @@ -0,0 +1,23 @@ + 'modal-class', 'width' => 800]); +} + +} +?> +----- + 800, 'classes' => ['ui-dialog' => 'modal-class']]); +} + +} +?> diff --git a/tests/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector/fixture/no_change_no_dialog_class.php.inc b/tests/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector/fixture/no_change_no_dialog_class.php.inc new file mode 100644 index 00000000..a82a7a4f --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector/fixture/no_change_no_dialog_class.php.inc @@ -0,0 +1,11 @@ + 600, 'classes' => ['ui-dialog' => 'already-correct']]); +} + +} +?> diff --git a/tests/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector/fixture/no_change_non_literal_options.php.inc b/tests/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector/fixture/no_change_non_literal_options.php.inc new file mode 100644 index 00000000..fb87dedb --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector/fixture/no_change_non_literal_options.php.inc @@ -0,0 +1,11 @@ + diff --git a/tests/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector/fixture/no_change_unrelated_class.php.inc b/tests/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector/fixture/no_change_unrelated_class.php.inc new file mode 100644 index 00000000..4a8566fa --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector/fixture/no_change_unrelated_class.php.inc @@ -0,0 +1,16 @@ + 'should-not-change']); +} + +} +?> diff --git a/tests/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector/fixture/off_canvas.php.inc b/tests/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector/fixture/off_canvas.php.inc new file mode 100644 index 00000000..74785ba1 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector/fixture/off_canvas.php.inc @@ -0,0 +1,23 @@ + 'side-panel', 'width' => 400]); +} + +} +?> +----- + 400, 'classes' => ['ui-dialog' => 'side-panel']]); +} + +} +?> diff --git a/tests/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector/fixture/use_imported_class.php.inc b/tests/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector/fixture/use_imported_class.php.inc new file mode 100644 index 00000000..ca200705 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ReplaceDialogClassOptionRector/fixture/use_imported_class.php.inc @@ -0,0 +1,27 @@ + 'imported-class']); +} + +} +?> +----- + ['ui-dialog' => 'imported-class']]); +} + +} +?> From 9be6314f783b6e66f95bbc6fabd8993befa2105a Mon Sep 17 00:00:00 2001 From: bjorn Date: Wed, 27 May 2026 15:19:46 +0200 Subject: [PATCH 14/15] feat(Drupal11): Add ReplaceNonBoolAccessRector for issue #3526250 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrites integer-literal `#access` values in render-array literals to proper booleans: `1` (or any non-zero integer literal) becomes `true`, `0` becomes `false`. Passing non-boolean, non-AccessResultInterface values to `#access` was deprecated in drupal:11.4.0 and is removed in drupal:13.0.0 (change record https://www.drupal.org/node/3549344). The rule matches `ArrayItem` nodes whose key is the string literal `'#access'` and whose value is an `Int_` (integer literal). It deliberately leaves booleans, variables, ternaries, method calls, and any other expression untouched — the correct boolean replacement for those cannot be determined statically, and Drupal's runtime deprecation will still prompt manual review. The replacement is pure PHP (`true`/`false`), so no DeprecationHelper BC wrapping is needed — the transformed code runs on every Drupal version. Registered by default in config/drupal-11/drupal-11.4-deprecations.php. BC risk assessment against installed contrib (paragraphs, webform, ctools, field_group, metatag, address, commerce, custom_field, search_api, linkit, ai, paragraphs_table, etc., ~3K PHP files): zero integer-literal `#access` patterns remain in current contrib HEAD; modules have already migrated to dynamic boolean expressions. The 186 distinct `'#access' => ` patterns observed are all variables, comparisons, or method calls — all correctly skipped by the rector. --- CHANGELOG.md | 13 ++++ config/drupal-11/drupal-11.4-deprecations.php | 8 ++ .../ReplaceNonBoolAccessRector.php | 75 +++++++++++++++++++ .../ReplaceNonBoolAccessRectorTest.php | 26 +++++++ .../config/configured_rule.php | 11 +++ .../fixture/basic.php.inc | 17 +++++ .../fixture/no_change_non_integer.php.inc | 18 +++++ 7 files changed, 168 insertions(+) create mode 100644 src/Drupal11/Rector/Deprecation/ReplaceNonBoolAccessRector.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceNonBoolAccessRector/ReplaceNonBoolAccessRectorTest.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceNonBoolAccessRector/config/configured_rule.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceNonBoolAccessRector/fixture/basic.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceNonBoolAccessRector/fixture/no_change_non_integer.php.inc diff --git a/CHANGELOG.md b/CHANGELOG.md index 8627120b..98deb101 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,18 @@ release-by-release. ### Added +- **`ReplaceNonBoolAccessRector`** — rewrites integer-literal `#access` + values inside render arrays to proper booleans: `1` (or any non-zero + integer) becomes `true`, and `0` becomes `false`. Passing non-boolean, + non-`AccessResultInterface` values to `#access` was deprecated in + drupal:11.4.0 and will be removed in drupal:13.0.0. The rule matches + on `ArrayItem` nodes whose key is the string literal `'#access'` and + whose value is an integer literal — it deliberately ignores booleans, + variables, function/method calls, and any other expression, because + the correct boolean replacement for those cannot be determined + statically. The replacement is pure PHP (`true` / `false`), so no BC + wrapping is needed; the transformed code runs on every Drupal version. + [#3526250](https://www.drupal.org/i/3526250). - **`ReplaceDialogClassOptionRector`** — rewrites the removed `$dialog_options['dialogClass']` key to `$dialog_options['classes']['ui-dialog']` in `new OpenDialogCommand(...)` and @@ -499,6 +511,7 @@ distinct deprecations in the same minor. | `RemoveFilterTipsLongParamRector` | `FilterInterface::tips()` — drop the `$long` parameter | [node/3567879](https://www.drupal.org/node/3567879) | | `SystemSortThemesRector` | `'system_sort_themes'` string callback → `Closure` (closure callable) | [node/3566774](https://www.drupal.org/node/3566774) | | `LocaleCompareIncToServiceRector` | `locale_translation_flush_projects()` / `locale_translation_build_projects()` / `locale_translation_check_projects*()` etc. → `locale.project` and `LocaleSource` services | [node/3037031](https://www.drupal.org/node/3037031) | +| `ReplaceNonBoolAccessRector` | Integer-literal `#access` render-array values → `true` / `false` | [node/3526250](https://www.drupal.org/node/3526250) | #### Drupal 10.3 deprecation rules diff --git a/config/drupal-11/drupal-11.4-deprecations.php b/config/drupal-11/drupal-11.4-deprecations.php index aa440e8e..7fe53266 100644 --- a/config/drupal-11/drupal-11.4-deprecations.php +++ b/config/drupal-11/drupal-11.4-deprecations.php @@ -25,6 +25,7 @@ use DrupalRector\Drupal11\Rector\Deprecation\ReplaceExpectDeprecationRector; use DrupalRector\Drupal11\Rector\Deprecation\ReplaceHideShowWithPrintedRector; use DrupalRector\Drupal11\Rector\Deprecation\ReplaceLocaleTranslationPathConfigRector; +use DrupalRector\Drupal11\Rector\Deprecation\ReplaceNonBoolAccessRector; use DrupalRector\Drupal11\Rector\Deprecation\ReplaceRecipeRunnerInstallModuleRector; use DrupalRector\Drupal11\Rector\Deprecation\ReplaceSessionManagerDeleteRector; use DrupalRector\Drupal11\Rector\Deprecation\ReplaceSystemPerformanceGzipKeyRector; @@ -483,6 +484,13 @@ // Replaced by direct $element['#printed'] = TRUE/FALSE assignment. $rectorConfig->rule(ReplaceHideShowWithPrintedRector::class); + // https://www.drupal.org/node/3526250 + // Integer values for #access render array key deprecated in drupal:11.4.0, + // removed in drupal:13.0.0. Replaced by boolean or AccessResultInterface. + // Only integer literals are rewritten (1 → true, 0 → false); variables and + // typed expressions are left for manual review. + $rectorConfig->rule(ReplaceNonBoolAccessRector::class); + // https://www.drupal.org/node/3550268 // https://www.drupal.org/node/3545276 (change record) // ExpectDeprecationTrait deprecated in drupal:11.4.0, removed in drupal:12.0.0. diff --git a/src/Drupal11/Rector/Deprecation/ReplaceNonBoolAccessRector.php b/src/Drupal11/Rector/Deprecation/ReplaceNonBoolAccessRector.php new file mode 100644 index 00000000..0d37e858 --- /dev/null +++ b/src/Drupal11/Rector/Deprecation/ReplaceNonBoolAccessRector.php @@ -0,0 +1,75 @@ + 'foo', '#access' => 1]; +CODE_BEFORE, + <<<'CODE_AFTER' +$build = ['#markup' => 'foo', '#access' => true]; +CODE_AFTER + ), + ] + ); + } + + /** @return array> */ + public function getNodeTypes(): array + { + return [ArrayItem::class]; + } + + /** @param ArrayItem $node */ + public function refactor(Node $node): ?Node + { + if ($node->key === null) { + return null; + } + if (!$node->key instanceof String_) { + return null; + } + if ($node->key->value !== '#access') { + return null; + } + // Only act on integer literals; leave booleans, variables, and + // AccessResultInterface expressions untouched. + if (!$node->value instanceof Int_) { + return null; + } + $node->value = $node->value->value === 0 + ? $this->nodeFactory->createFalse() + : $this->nodeFactory->createTrue(); + + return $node; + } +} diff --git a/tests/src/Drupal11/Rector/Deprecation/ReplaceNonBoolAccessRector/ReplaceNonBoolAccessRectorTest.php b/tests/src/Drupal11/Rector/Deprecation/ReplaceNonBoolAccessRector/ReplaceNonBoolAccessRectorTest.php new file mode 100644 index 00000000..dcecedf1 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ReplaceNonBoolAccessRector/ReplaceNonBoolAccessRectorTest.php @@ -0,0 +1,26 @@ +doTestFile($filePath); + } + + public static function provideData(): \Iterator + { + return self::yieldFilesFromDirectory(__DIR__.'/fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__.'/config/configured_rule.php'; + } +} diff --git a/tests/src/Drupal11/Rector/Deprecation/ReplaceNonBoolAccessRector/config/configured_rule.php b/tests/src/Drupal11/Rector/Deprecation/ReplaceNonBoolAccessRector/config/configured_rule.php new file mode 100644 index 00000000..6f623c1d --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ReplaceNonBoolAccessRector/config/configured_rule.php @@ -0,0 +1,11 @@ + 'foo', '#access' => 1]; + $other = ['#markup' => 'bar', '#access' => 0]; + return [$build, $other]; +} +?> +----- + 'foo', '#access' => true]; + $other = ['#markup' => 'bar', '#access' => false]; + return [$build, $other]; +} +?> diff --git a/tests/src/Drupal11/Rector/Deprecation/ReplaceNonBoolAccessRector/fixture/no_change_non_integer.php.inc b/tests/src/Drupal11/Rector/Deprecation/ReplaceNonBoolAccessRector/fixture/no_change_non_integer.php.inc new file mode 100644 index 00000000..70002f77 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ReplaceNonBoolAccessRector/fixture/no_change_non_integer.php.inc @@ -0,0 +1,18 @@ + 'foo', '#access' => true]; + $b = ['#markup' => 'foo', '#access' => false]; + + // Variable — cannot determine boolean replacement statically. + $c = ['#markup' => 'foo', '#access' => $variable]; + + // AccessResultInterface expression — leave alone. + $d = ['#markup' => 'foo', '#access' => $access_result]; + + // Unrelated key — even integer values must be ignored. + $e = ['#weight' => 1, '#markup' => 'foo']; + + return [$a, $b, $c, $d, $e]; +} From 9836badc5e77a73159aa2d50aa9cf76de47c476b Mon Sep 17 00:00:00 2001 From: bjorn Date: Wed, 27 May 2026 15:26:08 +0200 Subject: [PATCH 15/15] test(HookConvertRector): fix construction after rector 2.4.5 ExprAnalyzer change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rector/rector 2.4.5 (PR rectorphp/rector-src#8006) added a NodeNameResolver constructor argument to Rector\NodeAnalyzer\ExprAnalyzer, breaking the manual instantiation of BetterStandardPrinter in HookConvertRectorTest::setUp(). getLegacyHookFunction() never touches the printer, so bypass the constructor with ReflectionClass::newInstanceWithoutConstructor() rather than tracking upstream's transitive dependency chain (NodeNameResolver → ClassNaming / CallAnalyzer → ReflectionProvider). --- .../Convert/HookConvertRector/HookConvertRectorTest.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/src/Rector/Convert/HookConvertRector/HookConvertRectorTest.php b/tests/src/Rector/Convert/HookConvertRector/HookConvertRectorTest.php index d05b5807..95390375 100644 --- a/tests/src/Rector/Convert/HookConvertRector/HookConvertRectorTest.php +++ b/tests/src/Rector/Convert/HookConvertRector/HookConvertRectorTest.php @@ -12,9 +12,7 @@ use PhpParser\Node\Stmt\Function_; use PhpParser\ParserFactory; use PHPUnit\Framework\TestCase; -use Rector\NodeAnalyzer\ExprAnalyzer; use Rector\PhpParser\Printer\BetterStandardPrinter; -use Rector\Util\Reflection\PrivatesAccessor; class HookConvertRectorTest extends TestCase { @@ -22,7 +20,11 @@ class HookConvertRectorTest extends TestCase protected function setUp(): void { - $printer = new BetterStandardPrinter(new ExprAnalyzer(), new PrivatesAccessor()); + // getLegacyHookFunction() doesn't use the printer; bypassing the + // constructor avoids rector's internal dependency chain (>=2.4.5 + // ExprAnalyzer now requires NodeNameResolver, which transitively + // needs ReflectionProvider). + $printer = (new \ReflectionClass(BetterStandardPrinter::class))->newInstanceWithoutConstructor(); $this->rector = new HookConvertRector($printer); $ref = new \ReflectionClass($this->rector);