Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
208ff05
docs: fixup cr/issue links for rules
bbrala May 25, 2026
5a5a4f3
feat(Drupal11): Add ReplaceHideShowWithPrintedRector for issue #2258355
bbrala May 25, 2026
d64d28f
docs: fixup cr/issue links for rules
bbrala May 25, 2026
f1b8687
feat(Drupal11): Add ReplaceExpectDeprecationRector for issue #3550268
bbrala May 25, 2026
0c05c66
feat(Drupal11): Add block_content\Access class alias renames for issu…
bbrala May 25, 2026
9ce3ff3
feat: Add PHPStan-message coverage registry infrastructure
bbrala May 25, 2026
b681075
feat(Drupal11): Add ViewsBlockItemsPerPageNoneToNullRector for issue …
bbrala May 25, 2026
4d8347a
docs: fixup cr/issue links for rules
bbrala May 25, 2026
2c2f40d
feat(Drupal11): Add GetDrupalRootToRootPropertyRector for issue #3589047
bbrala May 25, 2026
b5064c9
feat(Drupal11): Add PHPSTAN_MESSAGES to GetDrupalRootToRootPropertyRe…
bbrala May 25, 2026
f080770
feat(Drupal11): Add ReplaceCommentPreviewConstantsRector for issue #3…
bbrala May 25, 2026
dd34080
feat(Drupal11): Add DrupalGetHeadersAssocArrayRector for issue #3440169
bbrala May 25, 2026
dd146c9
docs: changelog entry for DrupalGetHeadersAssocArrayRector
bbrala May 25, 2026
92b2956
feat(Drupal11): Add EntityFormModeEmptyDescriptionToNullRector for is…
bbrala May 25, 2026
1341e22
docs: changelog entry for EntityFormModeEmptyDescriptionToNullRector
bbrala May 25, 2026
74ce49b
feat(Drupal11): Add ViewsConfigUpdaterClassResolverToServiceRector fo…
bbrala May 25, 2026
bf7d2a8
docs: changelog entry for ViewsConfigUpdaterClassResolverToServiceRector
bbrala May 25, 2026
5beb443
feat(Drupal11): Add ReplaceLocaleTranslationPathConfigRector for issu…
bbrala May 26, 2026
e739792
docs: changelog entry for ReplaceLocaleTranslationPathConfigRector
bbrala May 26, 2026
2c5313e
feat(Drupal11): Add RemoveAliasManagerCacheMethodCallsRector for issu…
bbrala May 26, 2026
8d36be8
docs: changelog entry for RemoveAliasManagerCacheMethodCallsRector
bbrala May 26, 2026
f720298
feat(Drupal11): Add RemovePhpUnitCompatibilityTraitRector for issue #…
bbrala May 26, 2026
2fc699f
docs: changelog entry for RemovePhpUnitCompatibilityTraitRector
bbrala May 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 77 additions & 3 deletions .claude/skills/rector-live-test/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,81 @@ git -C ~/projects/drupal-rector-test checkout -- web/modules/contrib/
rm ~/projects/drupal-rector-test/rector-live-test.php
```

### 5. Report results
### 5. Capture the PHPStan deprecation message

While the contrib module is still installed and the pre-transform code is on
disk, run PHPStan against the file the rector matched and capture the
deprecation message PHPStan emits for the targeted symbol. This is the literal
string the rector "covers", and is what upgrade_status's
`DeprecationAnalyzer::isRectorCovered()` does an exact string match against
(after a small set of normalizations — see below).

```bash
cd ~/projects/drupal-rector-test
ddev exec vendor/bin/phpstan analyse \
web/modules/contrib/<module>/<file_the_rector_matched>.php \
--level=max --no-progress --error-format=raw 2>&1 \
| grep -i "deprecated.*<short_symbol_name>"
```

If no contrib file matched (or the symbol is already fully removed from
installed core so PHPStan emits "not found" rather than a deprecation), fall
back to a synthetic probe — see the `rector-extract-phpstan-error` skill's
"Synthetic probe" section for templates.

**Normalize and store.** Pipe the raw message through the normalizer (which
applies the three transforms upgrade_status applies before its `in_array()`
lookup — whitespace collapse, `: in` → `. Deprecated in`, leading `\Drupal`
strip):

```bash
ddev exec php scripts/normalize-phpstan-message.php "<raw multi-line message>"
# or:
printf '<raw message>' | ddev exec php scripts/normalize-phpstan-message.php
```

Add the normalized string to the rector source:

- **Custom rector class** (`src/Drupal*/Rector/Deprecation/<ClassName>.php`)
— add or extend the `public const PHPSTAN_MESSAGES` array. One element per
distinct call shape the rector handles.

```php
public const PHPSTAN_MESSAGES = [
'Call to deprecated method foo() of class Drupal\Bar. Deprecated in drupal:11.4.0 ...',
];
```

- **Config-only registration** (`config/drupal-*/drupal-*.N-deprecations.php`)
— add a `// PHPSTAN_MESSAGES <RectorShortName>:` comment block immediately
above the `ruleWithConfiguration(...)` call, one message per `//` line:

```php
// PHPSTAN_MESSAGES FunctionToServiceRector:
// Call to deprecated function foo(). Deprecated in drupal:11.4.0 and is removed from drupal:12.0.0. Use Drupal\Bar::baz() instead.
$rectorConfig->ruleWithConfiguration(FunctionToServiceRector::class, [ /* ... */ ]);
```

Then regenerate the flat registry:

```bash
ddev exec php scripts/generate-coverage-registry.php
```

This writes `config/coverage-registry.php` (a `return [...]` file mapping
rector short name → list of normalized messages). The registry is the
artifact a future upgrade_status PR will consume to replace its hardcoded
`$rector_covered` array.

If PHPStan emits no deprecation for the symbol — symbol present but not
annotated `@deprecated`, or already fully removed — record a `TODO
PHPSTAN_MESSAGES <RectorShortName>:` comment with the reason instead of
guessing the message text. Do **not** synthesize the string from the
`@deprecated` docblock by hand: PHPStan's exact wording differs between
"Call to deprecated method", "Instantiation of deprecated class", "Class X
implements deprecated interface", etc.

### 6. Report results

For each tested module, report:
```
Expand All @@ -192,9 +266,9 @@ For each tested module, report:
```

For every module with **zero changes**, do not just say "no match" — always show the actual
code and explain why. See step 6.
code and explain why. See step 7.

### 6. Diagnose zero-match results
### 7. Diagnose zero-match results

For **every** module that produced no changes, you must:

Expand Down
136 changes: 136 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,142 @@ Historical entries (≤ 0.21.2) are reproduced from the
they were originally published; their format and level of detail varies
release-by-release.

## [Unreleased]

### Added

- **`RemovePhpUnitCompatibilityTraitRector`** — removes
`use Drupal\Tests\PhpUnitCompatibilityTrait;` from test class
declarations. The trait was a forward-compatibility shim for PHPUnit
API differences across versions; it is **deleted from Drupal core in
Drupal 12** via [#3582118](https://www.drupal.org/i/3582118), at which
point any test class still composing the trait fatal-errors at
autoload time because the trait class no longer exists.

**Gated to Drupal 12 only — and deliberately off by default.** The
trait still exists on Drupal 10 (and may still hold shim methods that
tests depend on) and is an empty no-op on Drupal 11. Because the
trait composition is a structural `Class_` change, not an Expr → Expr
rewrite, it cannot be BC-wrapped with `DeprecationHelper`. Running
the rule prematurely on a D10-only codebase risks silently stripping
a composition that the tests still rely on. The rector therefore only
fires when the consumer explicitly sets the target Drupal version to
`12.0.0` or higher via
`DrupalRectorSettings::setDrupalVersion('12.0.0')`; the stub default
(`11.99.x-dev`) keeps it inert for normal D11-focused runs. The
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.
- **`RemoveAliasManagerCacheMethodCallsRector`** — deletes calls to
`AliasManager::setCacheKey()` and `AliasManager::writeCache()`. Both
methods were deprecated in drupal:11.3.0 and are removed in
drupal:13.0.0 with no replacement — they became no-ops when the path
alias preload cache was replaced by a Fiber-based bulk-lookup strategy.
The receiver must be typed as `\Drupal\path_alias\AliasManager` or
`AliasManagerInterface`; this guard prevents accidentally removing
unrelated methods that share the name (notably
`ModuleHandler::writeCache()`). Removes the entire expression
statement, leaving surrounding code intact. No BC wrapping is needed
since dropping a no-op call is safe on every Drupal version.
[#3496369](https://www.drupal.org/i/3496369) /
[CR](https://www.drupal.org/node/3532412).
- **`ReplaceLocaleTranslationPathConfigRector`** — rewrites chained
`\Drupal::config('locale.settings')->get('translation.path')` (and
equivalents via `configFactory()->get('locale.settings')->get(...)`,
`$this->config('locale.settings')->get(...)`, and similar) to
`\Drupal\Core\Site\Settings::get('locale_translation_path', 'public://translations')`.
The `locale.settings:translation.path` config key was deprecated in
drupal:11.4.0 and is removed in drupal:13.0.0; the interface
translations directory path must now be set as
`$settings['locale_translation_path']` in `settings.php`. On older
Drupal the value still lives in config, so the replacement is
BC-wrapped with `DeprecationHelper::backwardsCompatibleCall()`.
Matching is purely structural — two literal keys
(`'locale.settings'` and `'translation.path'`) must both appear in the
expected positions, so unrelated config reads and standalone
`$config->get('translation.path')` calls are left untouched.
**Caveat:** the BC wrapper gates on `\Drupal::VERSION`, not on where
the value is stored. Before running this rule, confirm that any
customised translation path has been moved to
`$settings['locale_translation_path']` in `settings.php`; otherwise
the new branch silently returns the default
`'public://translations'` even when the config still holds the
customised value. PHPStan / upgrade_status cannot detect this
deprecation — the deprecated symbol is the config key, not a PHP API
with `@deprecated` or `trigger_error()`, so this rule must be applied
proactively as part of an 11.4 → 13 migration plan.
[#3571593](https://www.drupal.org/i/3571593) /
[CR](https://www.drupal.org/node/3571594).
- **`ViewsConfigUpdaterClassResolverToServiceRector`** — rewrites
`\Drupal::classResolver(\Drupal\views\ViewsConfigUpdater::class)` to
`\Drupal::service(\Drupal\views\ViewsConfigUpdater::class)`. In
drupal:11.3.0 `ViewsConfigUpdater` was registered as a service;
`classResolver()` returns a fresh instance on each call, so state set via
`setDeprecationsEnabled(FALSE)` was lost across hook invocations. The new
call only resolves on Drupal ≥ 11.3.0 (the service isn't registered on
older versions), so the replacement is BC-wrapped with
`DeprecationHelper::backwardsCompatibleCall()`. Three layered guards
ensure only the targeted call shape is touched: receiver must be
`\Drupal`, method must be `classResolver`, and the single argument must be
`\Drupal\views\ViewsConfigUpdater::class`.
[#3529274](https://www.drupal.org/i/3529274) /
[CR](https://www.drupal.org/node/3530638).
- **`EntityFormModeEmptyDescriptionToNullRector`** — rewrites
`EntityFormMode::create([..., 'description' => '', ...])` to use `NULL`
instead of the empty string. Setting the description property of an
`EntityFormMode` to `''` was deprecated in drupal:11.2.0 and must be `NULL`
in drupal:12.0.0. Matches both the short class name (`use`-imported) and
the fully-qualified `\Drupal\Core\Entity\Entity\EntityFormMode::create()`
form, and leaves unrelated classes (e.g. `EntityViewMode`), non-empty
descriptions, and already-migrated NULL values untouched. The replacement
is plain PHP, so no BC wrapping is needed.
[#3448457](https://www.drupal.org/i/3448457) /
[CR](https://www.drupal.org/node/3452144).
- **`DrupalGetHeadersAssocArrayRector`** — converts the two deprecated
`UiHelperTrait::drupalGet()` `$headers` argument shapes to the documented
associative format: integer-keyed colon-separated strings
(`['X-Requested-With: XMLHttpRequest']`) are split to
`['X-Requested-With' => 'XMLHttpRequest']`, and `null` values
(`['Accept-Language' => NULL]`) become empty strings
(`['Accept-Language' => '']`). Guarded against `Drupal\Tests\BrowserTestBase`
so `KernelTestBase` (which uses `HttpKernelUiHelperTrait` and does not emit
this deprecation) is left alone. Deprecated in drupal:11.1.0, removed in
drupal:12.0.0; replacement is plain PHP so no BC wrapping is needed.
Live-tested against `pager_serializer`.
[#3440169](https://www.drupal.org/i/3440169) /
[CR (indexed headers)](https://www.drupal.org/node/3456178) /
[CR (null values)](https://www.drupal.org/node/3456233).
- **`ReplaceHideShowWithPrintedRector`** — replaces statement-level calls to the
deprecated global `hide()` and `show()` functions (deprecated in drupal:11.4.0,
removed in drupal:13.0.0) with direct `$element['#printed'] = TRUE/FALSE`
assignment. Expression-context uses (where the return value is captured) are
intentionally skipped because the original returns the element while the
rewrite would not. Live-tested against `fpa`, `saml_sp`, `vertical_tabs_config`,
and `field_group_background_image`.
[#2258355](https://www.drupal.org/i/2258355) /
[CR](https://www.drupal.org/node/3261271).
- **`ReplaceExpectDeprecationRector`** — migrates removed test framework methods
to their PHPUnit 11+ replacements. Renames are BC-wrapped with
`DeprecationHelper::backwardsCompatibleCall()` so tests keep passing on both
pre-11.4 (old methods) and 11.4+ (new methods). Covers:
`$this->expectDeprecation($msg)` and `$this->expectDeprecationMessage($msg)` →
`$this->expectUserDeprecationMessage($msg)`;
`$this->expectDeprecationMessageMatches($p)` →
`$this->expectUserDeprecationMessageMatches($p)`; bare
`$this->expectDeprecation()` (no-arg PHPUnit form) → removed.
`ExpectDeprecationTrait` is deprecated in drupal:11.4.0 and removed in
drupal:12.0.0.
[#3550268](https://www.drupal.org/i/3550268) /
[CR](https://www.drupal.org/node/3545276).
- Class-rename entries for the four `Drupal\block_content\Access\*` aliases
(`AccessGroupAnd`, `DependentAccessInterface`,
`RefinableDependentAccessInterface`, `RefinableDependentAccessTrait`) →
their canonical `Drupal\Core\Access\*` homes. Deprecated in drupal:11.3.0,
removed in drupal:12.0.0; registered via Rector's built-in
`RenameClassRector` in `drupal-11.3-deprecations.php`.
[#3571874](https://www.drupal.org/i/3571874) /
[CR](https://www.drupal.org/node/3527501).

## [1.0.0-beta1] — 2026-05-25

First beta of the 1.0 line. Adds full Drupal 11 deprecation coverage (versions 11.0
Expand Down
38 changes: 38 additions & 0 deletions config/coverage-registry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

/**
* Auto-generated by scripts/generate-coverage-registry.php.
*
* Do not edit by hand. Source of truth:
* - public const PHPSTAN_MESSAGES on each rector class
* - // PHPSTAN_MESSAGES: comment blocks above config-only registrations
*
* Messages are stored AFTER applying the three normalization transforms
* upgrade_status's DeprecationAnalyzer::categorizeMessage() applies before
* isRectorCovered(): whitespace collapse, ":\s+(in|as of)" → ". Deprecated
* \1", and "Use \Drupal" → "Use Drupal".
*
* Consumers can require this file and either iterate by rector class, or
* flatten with array_merge(...array_values($registry)) to get a flat list
* of all covered deprecation strings.
*/

return array (
'GetDrupalRootToRootPropertyRector' =>
array (
0 => 'Call to deprecated method getDrupalRoot() of class Drupal\\KernelTests\\KernelTestBase. Deprecated in drupal:11.4.0 and is removed from drupal:13.0.0. Access $this->root directly.',
1 => 'Call to deprecated method getDrupalRoot() of class Drupal\\Tests\\BrowserTestBase. Deprecated in drupal:11.4.0 and is removed from drupal:13.0.0. Access $this->root directly.',
2 => 'Call to deprecated method getDrupalRoot() of class Drupal\\Tests\\UnitTestCase. Deprecated in drupal:11.4.0 and is removed from drupal:13.0.0. Access $this->root directly.',
),
'RemoveAliasManagerCacheMethodCallsRector' =>
array (
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.',
),
'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.',
),
);
1 change: 1 addition & 0 deletions config/drupal-10/drupal-10.3-deprecations.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
]);

// https://www.drupal.org/node/3426517
// https://www.drupal.org/node/3575575
// FileSystemInterface::EXISTS_* deprecated in drupal:10.3.0, removed in drupal:12.0.0.
// Replaced by \Drupal\Core\File\FileExists enum cases.
$rectorConfig->ruleWithConfiguration(ClassConstantToClassConstantRector::class, [
Expand Down
4 changes: 3 additions & 1 deletion config/drupal-11/drupal-11.0-deprecations.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@
$rectorConfig->rule(GetNameToNameRector::class);

// https://www.drupal.org/node/3436954
// https://www.drupal.org/node/2575105 (change record)
// https://www.drupal.org/node/3443018 (change record)
// https://www.drupal.org/node/2575105 (related)
// $settings['state_cache'] deprecated in drupal:11.0.0.
// State caching is now permanently enabled and the setting has no effect.
$rectorConfig->rule(RemoveStateCacheSettingRector::class);

// https://www.drupal.org/node/3395986
// https://www.drupal.org/node/3395991 (change record)
// REQUEST_TIME constant deprecated in drupal:8.3.0, removed in drupal:11.0.0.
// Replaced by \Drupal::time()->getRequestTime().
$rectorConfig->ruleWithConfiguration(ReplaceRequestTimeConstantRector::class, [
Expand Down
23 changes: 19 additions & 4 deletions config/drupal-11/drupal-11.1-deprecations.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
declare(strict_types=1);

use DrupalRector\Drupal11\Rector\Deprecation\BlockContentTestBaseStringToArrayRector;
use DrupalRector\Drupal11\Rector\Deprecation\DrupalGetHeadersAssocArrayRector;
use DrupalRector\Drupal11\Rector\Deprecation\MovePointerToMouseOverRector;
use DrupalRector\Drupal11\Rector\Deprecation\PluginBaseIsConfigurableRector;
use DrupalRector\Drupal11\Rector\Deprecation\RemoveModuleHandlerDeprecatedMethodsRector;
Expand All @@ -19,14 +20,16 @@

return static function (RectorConfig $rectorConfig): void {
// https://www.drupal.org/node/3459533
// https://www.drupal.org/node/2946122 (change record)
// https://www.drupal.org/node/3459535 (change record)
// https://www.drupal.org/node/2946122 (related)
// PluginBase::isConfigurable() deprecated in drupal:11.1.0, removed in drupal:12.0.0.
// Replaced by instanceof \Drupal\Component\Plugin\ConfigurableInterface.
$rectorConfig->ruleWithConfiguration(PluginBaseIsConfigurableRector::class, [
new DrupalIntroducedVersionConfiguration('11.1.0'),
]);

// https://www.drupal.org/node/3467559
// https://www.drupal.org/node/3151086
// https://www.drupal.org/node/3467559 (change record)
// AliasWhitelist and AliasWhitelistInterface deprecated in drupal:11.1.0, removed in drupal:12.0.0.
// Replaced by AliasPrefixList and AliasPrefixListInterface.
// AliasManager::pathAliasWhitelistRebuild() deprecated in drupal:11.1.0, removed in drupal:12.0.0.
Expand All @@ -41,7 +44,8 @@
]);

// https://www.drupal.org/node/3442009
// https://www.drupal.org/node/3368812 (change record)
// https://www.drupal.org/node/3442349 (change record)
// https://www.drupal.org/node/3368812 (related)
// ModuleHandlerInterface::writeCache() deprecated in drupal:11.1.0, removed in drupal:12.0.0. No replacement needed.
// ModuleHandlerInterface::getHookInfo() deprecated in drupal:11.1.0, removed in drupal:12.0.0. Replaced by [].
$rectorConfig->rule(RemoveModuleHandlerDeprecatedMethodsRector::class);
Expand Down Expand Up @@ -81,13 +85,24 @@
]);

// https://www.drupal.org/node/3488176
// https://www.drupal.org/node/3488470 (change record)
// drupal_common_theme() removed in drupal:11.1.0.
// Replaced by \Drupal\Core\Theme\ThemeCommonElements::commonElements().
// https://www.drupal.org/node/3268441
// https://www.drupal.org/node/2350849
// https://www.drupal.org/node/3268441 (change record)
// https://www.drupal.org/node/3574424
// image_filter_keyword() deprecated in drupal:11.1.0, removed in drupal:12.0.0.
// Replaced by \Drupal\Component\Utility\Image::getKeywordOffset().
$rectorConfig->ruleWithConfiguration(FunctionToStaticRector::class, [
new FunctionToStaticConfiguration('11.1.0', 'drupal_common_theme', 'Drupal\Core\Theme\ThemeCommonElements', 'commonElements'),
new FunctionToStaticConfiguration('11.1.0', 'image_filter_keyword', 'Drupal\Component\Utility\Image', 'getKeywordOffset'),
]);

// https://www.drupal.org/node/3440169
// https://www.drupal.org/node/3456178 (change record: integer-keyed headers)
// https://www.drupal.org/node/3456233 (change record: null header values)
// UiHelperTrait::drupalGet() $headers as indexed colon-separated strings or null values
// deprecated in drupal:11.1.0, removed in drupal:12.0.0. Replaced by the associative array
// format ['Header-Name' => 'value'], with empty strings in place of null.
$rectorConfig->rule(DrupalGetHeadersAssocArrayRector::class);
};
Loading
Loading