Skip to content

New rectors and small improvements#8

Open
bbrala wants to merge 69 commits into
feature/digest-rectorsfrom
feature/new-rectors-combined
Open

New rectors and small improvements#8
bbrala wants to merge 69 commits into
feature/digest-rectorsfrom
feature/new-rectors-combined

Conversation

@bbrala
Copy link
Copy Markdown
Owner

@bbrala bbrala commented May 28, 2026

Description

Adds 18 new Drupal 11 deprecation rectors and reorganises how non-BC class renames
ship. Highlights:

New BC-wrappable rectors (default DRUPAL_11X sets)

  • RemoveDrupalToStringTraitRector — strips ToStringTrait and inlines __toString() (D11.4 → D13)
  • TaxonomyTermPageVariableToViewModeRector$variables['page']$variables['view_mode'] === 'full' (D11.3 → D13)
  • ReplaceNonBoolAccessRector — integer-literal #access values → true/false (D11.4 → D13)
  • ReplaceDialogClassOptionRectordialogClassclasses['ui-dialog'] on Ajax dialog commands (D11.3 → D12)
  • RemoveToolkitArgFromImageToolkitOperationConstructorRector — drops the deprecated 4th ImageToolkitInterface ctor arg (D11.4 → D13)
  • RemoveRendererAddCacheableDependencyNonObjectRector — deletes addCacheableDependency() calls passing statically-provable non-objects (D11.3 → D13)
  • RemoveInstallSchemaSystemSequencesRector — strips installSchema('system', 'sequences') test setup (D10.2 → D12)
  • RemovePhpUnitCompatibilityTraitRector — strips the trait composition; gated to Drupal-version ≥ 12.0.0 because the trait still exists on D10/D11 (D12 deletion)
  • GroupLegacyToIgnoreDeprecationsRector@group legacy → PHPUnit 10 #[IgnoreDeprecations] attribute
  • RemoveAliasManagerCacheMethodCallsRector — deletes AliasManager::setCacheKey() / writeCache() no-op calls (D11.3 → D13)
  • ReplaceLocaleTranslationPathConfigRectorlocale.settings:translation.path config read → Settings::get('locale_translation_path', …); BC-wrapped (D11.4 → D13)
  • ViewsConfigUpdaterClassResolverToServiceRector\Drupal::classResolver(ViewsConfigUpdater::class)\Drupal::service(...); BC-wrapped (D11.3 → D13)
  • EntityFormModeEmptyDescriptionToNullRector'description' => ''NULL on EntityFormMode::create() (D11.2 → D12)
  • DrupalGetHeadersAssocArrayRector — converts the two deprecated $headers shapes on drupalGet() to associative (D11.1 → D12)
  • ReplaceHideShowWithPrintedRector — statement-level hide() / show()$element['#printed'] = TRUE/FALSE (D11.4 → D13)
  • ReplaceExpectDeprecationRectorexpectDeprecation* test methods → PHPUnit 11 expectUserDeprecation*; BC-wrapped (D11.4 → D12)
  • GetDrupalRootToRootPropertyRectorDrupalKernelInterface::getDrupalRoot()->root property
  • ViewsBlockItemsPerPageNoneToNullRector — Views block items_per_page: 'none'null

Plus three new RenameClassRector entries (BC-safe across all supported minors):
block_content\Access\*Core\Access\*, LibraryDiscoveryLibraryDiscoveryInterface,
EntityPermissionsRouteProviderWithCheckEntityPermissionsRouteProvider.

New opt-in "breaking" sets

Four new per-minor sets — DRUPAL_111_BREAKING, DRUPAL_112_BREAKING,
DRUPAL_113_BREAKING, DRUPAL_114_BREAKING — hold RenameClassRector entries
whose replacement symbol was introduced together with the deprecation and
does not exist on older minors. None of these is included in DRUPAL_11X /
DRUPAL_11; consumers must load each one explicitly after committing to drop
the minor named in that file's docblock. Targets verified missing on Drupal
10.6.x: AliasWhitelistAliasPrefixList (11.1); pgsql EntityQuery,
migrate-source moves, I18nQueryTrait move (11.2); WorkspaceAssociation
WorkspaceTracker, block_content\Access moves (11.3); menu_link_content
migrate-process classes (11.4).

The jsonapi\ResourceResponseValidator rename (#3472008) was deliberately
not shipped — the replacement lives in a core test module, so rewriting
production FQCNs to it would fatal on any site that does not enable that
module.

Refactor: auto-BC for two existing rectors

ClassConstantToClassConstantRector and MethodToMethodWithCheckRector now
extend AbstractDrupalCoreRector and auto-wrap their Expr → Expr rewrites
via DeprecationHelper::backwardsCompatibleCall(). Their configuration value
objects gain a required introducedVersion argument and implement
VersionedConfigurationInterface. Closes three D11 → D10 regressions
(Comment* enums in 11.4, RequirementSeverity in 11.2, AliasManager
method rename in 11.1) without moving anything to a -breaking.php set.
The MethodToMethodWithCheckRector TODO-comment fallback for the maybe
inference case is dropped — the BC wrap makes both branches runtime-safe.

Infrastructure

  • PHPStan-message coverage registry (config/coverage-registry.php,
    scripts/generate-coverage-registry.php, scripts/normalize-phpstan-message.php):
    links each rector to the verbatim PHPStan deprecation messages it covers,
    so coverage gaps can be audited mechanically.

To Test

  • Run the suite: vendor/bin/phpunit tests/src/Drupal11/Rector/Deprecation
    — fixture coverage for every new rector (basic + no-change + edge cases;
    fixture-below-version/ for the two BC-wrapped + version-gated rectors).
  • Static analysis: vendor/bin/phpstan analyse.
  • Live-tested against contrib for: ReplaceHideShowWithPrintedRector (fpa,
    saml_sp, vertical_tabs_config, field_group_background_image),
    DrupalGetHeadersAssocArrayRector (pager_serializer), and others noted
    per-rector in CHANGELOG.

Drupal.org issues

Per-rector issue links are in the CHANGELOG entries.

bbrala added 30 commits May 25, 2026 18:40
Rewrites statement-level calls to the deprecated global hide() and show()
functions to direct $element['#printed'] = TRUE/FALSE assignment.

The functions are deprecated in drupal:11.4.0 and removed in drupal:13.0.0.
Expression-context uses (where the return value is captured) are 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 — 4 modules, 10 calls transformed cleanly.

See https://www.drupal.org/node/2258355
See https://www.drupal.org/node/3261271 (change record)
Migrates removed Drupal/PHPUnit test framework methods to their
PHPUnit 11+ replacements:

- $this->expectDeprecation()         (PHPUnit no-arg form) → removed
- $this->expectDeprecation($msg)     (Drupal trait form)   → expectUserDeprecationMessage($msg)
- $this->expectDeprecationMessage($msg)        → expectUserDeprecationMessage($msg)
- $this->expectDeprecationMessageMatches($p)   → expectUserDeprecationMessageMatches($p)

Renames are BC-wrapped via DeprecationHelper::backwardsCompatibleCall()
so tests keep passing on both pre-11.4 (old methods) and 11.4+ (new
methods). The 0-arg PHPUnit form is unconditionally removed because
real-world contrib uses the 1-arg Drupal-trait form.

Live-tested against honeypot, key, entity_usage, and node_revision_delete.

ExpectDeprecationTrait is deprecated in drupal:11.4.0 and removed
in drupal:12.0.0.

See https://www.drupal.org/node/3550268
See https://www.drupal.org/node/3545276 (change record)
…e #3571874

Four BC class aliases in Drupal\block_content\Access were deprecated in
drupal:11.3.0 and removed in drupal:12.0.0. Add config-only RenameClassRector
entries mapping each to its canonical Drupal\Core\Access home:

- AccessGroupAnd
- DependentAccessInterface
- RefinableDependentAccessInterface
- RefinableDependentAccessTrait

See https://www.drupal.org/node/3571874 and
https://www.drupal.org/node/3527501 (change record).
Introduce a rector → PHPStan-deprecation-message map so a future
upgrade_status PR can replace its hardcoded $rector_covered array
with a generated, version-aware registry.

Source of truth lives on the code, not in a sidecar file:

- Custom rector classes: public const PHPSTAN_MESSAGES = [...].
- Config-only registrations: // PHPSTAN_MESSAGES <Rector>: comment
  block above the ruleWithConfiguration() call.

Tooling:

- scripts/normalize-phpstan-message.php applies the three transforms
  upgrade_status's DeprecationAnalyzer::categorizeMessage() applies
  before isRectorCovered() (whitespace collapse, ": in" → ". Deprecated
  in", strip leading \\Drupal). Same transforms run on stored
  messages so the registry is comparison-ready.

- scripts/generate-coverage-registry.php walks src/ + config/, reads
  both shapes, normalizes, writes config/coverage-registry.php
  (rector short name → list of normalized messages).

Worked example added to ReplaceExpectDeprecationRector (captured via
synthetic probe against installed 11.4-dev). 3571874 config-only block
gets a TODO marker rather than a synthesized guess, since the aliases
are already gone from 11.4-dev core and a live capture requires a
11.3.x test env.

rector-live-test SKILL.md gets a new step 5 wiring the capture into
the existing live-test flow: while the contrib module is still
installed and the pre-transform file on disk, run PHPStan, normalize
the message, store on the class or in the config comment block, then
regenerate the registry.
…#3520946

Replaces $block->setConfigurationValue('items_per_page', 'none') with NULL
for Views block plugins. The string 'none' was deprecated in drupal:11.2.0
and removed in drupal:12.0.0; NULL is the canonical value for inheriting
the items-per-page setting from the view.

See https://www.drupal.org/node/3520946
See https://www.drupal.org/node/3522240 (change record)
Replaces deprecated DrupalTestCaseTrait::getDrupalRoot() instance calls with
direct $this->root property access. The method was deprecated in drupal:11.4.0
and removed in drupal:13.0.0. The rule targets subclasses of BrowserTestBase,
KernelTestBase, and UnitTestCase, which all inherit the $root property via
DrupalTestCaseTrait. Static calls and BuildTestBase (which overrides the method
with a non-deprecated implementation) are intentionally left untouched.

See https://www.drupal.org/node/3589047
See https://www.drupal.org/node/3574112 (change record)
…ctor

Captures the three real PHPStan deprecation messages this rector covers — one
per base test class (BrowserTestBase, KernelTestBase, UnitTestCase). The
BrowserTestBase variant was verified against automatic_updates 4.1.x; the
KernelTestBase variant against project_browser 2.1.x. The UnitTestCase variant
mirrors the deterministic PHPStan format.

Regenerates config/coverage-registry.php for upgrade_status consumption.
…538660

Replaces DRUPAL_DISABLED/OPTIONAL/REQUIRED with CommentPreviewMode enum cases
in CommentTestBase::setCommentPreview() calls. Passing an int to this method
was deprecated in drupal:11.3.0 and is removed in drupal:13.0.0; the new
CommentPreviewMode enum only exists on Drupal >= 11.3.0, so the replacement
is BC-wrapped via AbstractDrupalCoreRector + DeprecationHelper.

The type guard targets Drupal\Tests\comment\Functional\CommentTestBase
(the actual owner of setCommentPreview, not the NodeTypeInterface the
upstream digest incorrectly pointed at). A minimal CommentTestBase stub is
added so PHPStan can resolve the receiver type during rector tests.

A TODO PHPSTAN_MESSAGES comment is included because PHPStan emits no
deprecation for this call: setCommentPreview() itself is not @deprecated
(the trigger_error fires at runtime when an int is passed), and phpstan
does not flag file-scope const usage for DRUPAL_DISABLED/OPTIONAL/REQUIRED.

See https://www.drupal.org/node/3538660
See https://www.drupal.org/node/3538678 (change record)
Rewrites deprecated UiHelperTrait::drupalGet() $headers patterns:
- ['Header-Name: value'] → ['Header-Name' => 'value']
- ['Header-Name' => NULL] → ['Header-Name' => '']

Deprecated in drupal:11.1.0, removed in drupal:12.0.0. The associative
format has always been the documented contract; no new Drupal API is
required, so no BC wrapping is needed.

Type guard: Drupal\Tests\BrowserTestBase (UiHelperTrait fires the
deprecation; KernelTestBase uses HttpKernelUiHelperTrait, which does not).

Validated against pager_serializer 8.x-1.x (Functional/Views/StyleSerializerTest.php).
…sue #3448457

Rewrites EntityFormMode::create([..., 'description' => '', ...]) to use
NULL instead of an 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 via an isName()
static-call guard. Sibling classes (EntityViewMode), non-empty
descriptions, already-migrated NULL values, and other array keys are all
left untouched.

The replacement is plain PHP, so no BC wrapping is needed.

The deprecated pattern is genuinely rare in contrib (16 modules call
EntityFormMode::create() but none with 'description' => ''); validated
via synthetic probe.
…r issue #3529274

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 via
DeprecationHelper::backwardsCompatibleCall().

Three layered isName 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. The
argument guard correctly disambiguates against module subclasses
sharing the short name (validated against entity_hierarchy, which uses
\Drupal\entity_hierarchy\Update\ViewsConfigUpdater and is correctly
skipped).

Live-tested against tripal.
…e #3571593

Rewrites chained \Drupal::config('locale.settings')->get('translation.path')
(and configFactory()/this->config() variants) to \Drupal\Core\Site\Settings::get(
'locale_translation_path', 'public://translations'). The config key was
deprecated in drupal:11.4.0 and is removed in drupal:13.0.0; the customised
translation path must be set as $settings['locale_translation_path'] in
settings.php. BC-wrapped via DeprecationHelper so the rewritten code still
runs on pre-11.4 Drupal — though users must migrate the value to settings.php
before running the rule, otherwise the new branch silently falls back to the
default.

Matches purely structurally: two literal keys ('locale.settings' and
'translation.path') must appear in the expected positions, mirroring
ReplaceSystemPerformanceGzipKeyRector. Standalone $config->get('translation.path'),
unrelated config names, and unrelated keys are left untouched.

PHPStan / upgrade_status cannot detect this deprecation — the deprecated symbol
is the config key, not a PHP API with @deprecated or trigger_error(). A TODO
PHPSTAN_MESSAGES comment in the source documents the gap.
…e #3496369

Removes 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 the unrelated ModuleHandler::writeCache() call.

Removes the entire expression statement, leaving surrounding code intact.
No BC wrapping needed since dropping a no-op call is safe on every Drupal
version that still exposes the methods. Includes PHPSTAN_MESSAGES for both
setCacheKey() and writeCache() captured from real contrib (drupal/redirect)
and a synthetic probe respectively, plus the regenerated coverage-registry.
…3582118

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, 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 DrupalRectorSettings::setDrupalVersion('12.0.0') or higher
is set; the stub default (11.99.x-dev) keeps it inert for normal
D11-focused runs.

Extends AbstractDrupalCoreRector for the version-gate machinery but
overrides supportBackwardsCompatibility() to return false explicitly,
since structural class composition has no DeprecationHelper-style BC
wrapper to emit.
bbrala added 11 commits May 28, 2026 10:52
…ctor

Captures the three real PHPStan deprecation messages this rector covers — one
per base test class (BrowserTestBase, KernelTestBase, UnitTestCase). The
BrowserTestBase variant was verified against automatic_updates 4.1.x; the
KernelTestBase variant against project_browser 2.1.x. The UnitTestCase variant
mirrors the deterministic PHPStan format.

Regenerates config/coverage-registry.php for upgrade_status consumption.
…538660

Replaces DRUPAL_DISABLED/OPTIONAL/REQUIRED with CommentPreviewMode enum cases
in CommentTestBase::setCommentPreview() calls. Passing an int to this method
was deprecated in drupal:11.3.0 and is removed in drupal:13.0.0; the new
CommentPreviewMode enum only exists on Drupal >= 11.3.0, so the replacement
is BC-wrapped via AbstractDrupalCoreRector + DeprecationHelper.

The type guard targets Drupal\Tests\comment\Functional\CommentTestBase
(the actual owner of setCommentPreview, not the NodeTypeInterface the
upstream digest incorrectly pointed at). A minimal CommentTestBase stub is
added so PHPStan can resolve the receiver type during rector tests.

A TODO PHPSTAN_MESSAGES comment is included because PHPStan emits no
deprecation for this call: setCommentPreview() itself is not @deprecated
(the trigger_error fires at runtime when an int is passed), and phpstan
does not flag file-scope const usage for DRUPAL_DISABLED/OPTIONAL/REQUIRED.

See https://www.drupal.org/node/3538660
See https://www.drupal.org/node/3538678 (change record)
Rewrites deprecated UiHelperTrait::drupalGet() $headers patterns:
- ['Header-Name: value'] → ['Header-Name' => 'value']
- ['Header-Name' => NULL] → ['Header-Name' => '']

Deprecated in drupal:11.1.0, removed in drupal:12.0.0. The associative
format has always been the documented contract; no new Drupal API is
required, so no BC wrapping is needed.

Type guard: Drupal\Tests\BrowserTestBase (UiHelperTrait fires the
deprecation; KernelTestBase uses HttpKernelUiHelperTrait, which does not).

Validated against pager_serializer 8.x-1.x (Functional/Views/StyleSerializerTest.php).
…sue #3448457

Rewrites EntityFormMode::create([..., 'description' => '', ...]) to use
NULL instead of an 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 via an isName()
static-call guard. Sibling classes (EntityViewMode), non-empty
descriptions, already-migrated NULL values, and other array keys are all
left untouched.

The replacement is plain PHP, so no BC wrapping is needed.

The deprecated pattern is genuinely rare in contrib (16 modules call
EntityFormMode::create() but none with 'description' => ''); validated
via synthetic probe.
…r issue #3529274

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 via
DeprecationHelper::backwardsCompatibleCall().

Three layered isName 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. The
argument guard correctly disambiguates against module subclasses
sharing the short name (validated against entity_hierarchy, which uses
\Drupal\entity_hierarchy\Update\ViewsConfigUpdater and is correctly
skipped).

Live-tested against tripal.
# Conflicts:
#	CHANGELOG.md
#	config/coverage-registry.php
#	config/drupal-11/drupal-11.3-deprecations.php
#	config/drupal-11/drupal-11.4-deprecations.php
…ure/new-rectors-combined

# Conflicts:
#	config/coverage-registry.php
…ure/new-rectors-combined

# Conflicts:
#	config/coverage-registry.php
@bbrala bbrala changed the title Feature/new rectors combined New rectors and small improvements May 28, 2026
@bbrala
Copy link
Copy Markdown
Owner Author

bbrala commented May 28, 2026

Self-review notes (Claude)

Scope: 18 new D11 rectors, BC-wrap refactor of two existing rectors, opt-in *_BREAKING sets, coverage-registry infrastructure.

Blockers / High

# File:line Issue
1 ReplaceDialogClassOptionRector.php:143,149 Partial-rewrite hazard. Mutates $optionsArray->items (unsets dialogClass) before the final bail-out checks. If 'classes' is a non-literal (variable / fn call) or the new dialogClass value is non-literal while the existing ui-dialog is literal (or vice-versa), the rector returns null with dialogClass already deleted. Net effect: option silently lost. Fix: reorder — bail-out checks before any mutation.
2 MethodToMethodWithCheckRector.php:44–65 Silent maybe-path rewrite. The old code attached a // TODO confirm receiver type comment for the maybe PHPStan inference case. The refactor relies on BC wrapping — but AbstractDrupalCoreRector::createBcCallOnExpr only wraps when introducedVersion ≥ 10.0 AND BC is enabled. Pre-10.0 entries (urlInfo→toUrl @ 8.0.0, clearCsrfTokenSeed→stampNew @ 9.2.0) get rewritten unconditionally with no diagnostic. Either re-attach the TODO when no wrap happens or restrict to yes() in the no-wrap path.
3 ClassConstantToClassConstantConfiguration.php:20, MethodToMethodWithCheckConfiguration.php:19 BC break for library consumers. New required $introducedVersion ctor arg with no default. All in-repo call sites updated, but the classes aren't @internal — downstream consumers will hit ArgumentCountError. Either default to '0.0.0' or add an explicit ### Changed CHANGELOG note.
4 RemoveToolkitArgFromImageToolkitOperationConstructorRector.php:79 No extends check. Rector matches any class with the shape (≥5 ctor params, 4th typed ImageToolkitInterface), not just ImageToolkitOperationBase subclasses. Real-world risk low but advertised scope ≠ enforced scope. Add ClassReflection->isSubclassOf() or an extends name check.
5 RemoveToolkitArgFromImageToolkitOperationConstructorRector.php:102 traverseNodesWithCallable descends into nested closures. A constructor with a closure that shadows $toolkit breaks the "exactly once" count → false negative. Acceptable safety bias, add a fixture.
6 DrupalGetHeadersAssocArrayRector.php:105 explode(':', $raw, 2) on any colon-containing string. A non-header value (e.g. 'http://x') becomes 'http' => '//x'. Low likelihood (BrowserTestBase guard), but worth a no-change fixture.
7 RemoveDrupalToStringTraitRector.php:100 Uses isName($traitName, self::TRAIT_FQCN) — relies on PhpParser name-resolver attaching the FQCN. A class in Drupal\Component\Utility itself using short-name use ToStringTrait; without an import may resolve as unqualified. Common in core, unlikely in contrib. Add a no-change fixture.

Medium

  • ReplaceLocaleTranslationPathConfigRector.php:116 — BC wrap gates on \Drupal::VERSION, not on whether the value was migrated to settings.php. On D11.4+ a customised config-stored path silently becomes 'public://translations'. Docblock surfaces it; the call site does not. Consider emitting an inline // TODO comment via AddCommentService so the warning travels with the diff.
  • TaxonomyTermPageVariableToViewModeRector.php:82,91 — Hook gate uses str_contains — matches helpers like _mymodule_preprocess_taxonomy_term_extra(). Also descends into nested closures that shadow $variables. Tighten to str_ends_with() for procedural, exact match for methods, skip closure bodies.
  • ReplaceHideShowWithPrintedRector.php:70isNames($call->name, ['hide','show']) matches a namespaced MyModule\hide() call. Names are extremely generic. Restrict to fully-qualified global namespace + add a no-change fixture.
  • ReplaceExpectDeprecationRector.php:33 — Drupal's ExpectDeprecationTrait uses regex-fragment matching with %A boundaries; PHPUnit's expectUserDeprecationMessage() is exact string. Tests using partial messages will start failing post-rewrite. Doc note exists; consider emitting an inline comment on the rewritten line.
  • Breaking-sets migration UX — Existing consumers of DRUPAL_11 / DRUPAL_111 / DRUPAL_112 / DRUPAL_113 who relied on the now-moved RenameClassRector entries (AliasWhitelist, pgsql migrate sources, WorkspaceAssociation) will silently stop getting those rewrites. Add an explicit ### Changed callout in CHANGELOG instructing them to opt into the new *_BREAKING sets.
  • Version-discrimination test gapfixture-below-version/ for both refactored rectors uses setDrupalVersion('1.0.0') (gates everything off uniformly). A fixture at e.g. '11.3.0' asserting 11.4 entries skip while 11.2 entries fire would actually validate per-entry gating.
  • RemoveDrupalToStringTraitRector.php:91 — Unconditionally inlines return (string) $this->render();. Classes using ToStringTrait without render() (rare) get a fatal body. Doc the assumption or guard.
  • RemoveRendererAddCacheableDependencyNonObjectRector — No negative fixture for the 1-arg overload on RefinableCacheableDependencyInterface/BubbleableMetadata. Arity guard is correct, tests don't lock it in.

Low / nits

  • GetDrupalRootToRootPropertyRector.php:84 — Receiver-not-$this would already have been a visibility error pre-rewrite; tightening to $this only would be defensive.
  • GroupLegacyToIgnoreDeprecationsRector.php:83str_contains($docText, '@group legacy') would substring-match @group legacy-extras. Word-boundary safer.
  • GroupLegacyToIgnoreDeprecationsRector.php:102 — Single-line docblock /** @group legacy */ not cleaned (attribute still added, annotation lingers).
  • RemoveDrupalToStringTraitRector mixed-traits fixture: __toString() lands at index 0, before the remaining use OtherTrait; — legal PHP, ugly diff.

What looks good

  • Breaking-sets reorg: every removed entry from *-deprecations.php has a home in the matching *-breaking.php; jsonapi ResourceResponseValidator correctly absent (replacement is a test-module symbol).
  • RemoveAliasManagerCacheMethodCallsRector type guard correctly excludes ModuleHandler::writeCache() (fixture-verified).
  • EntityFormModeEmptyDescriptionToNullRector intentionally narrow (no instanceof on subclasses) — false-negative, zero false-positive.
  • RemovePhpUnitCompatibilityTraitRector correctly inert by default (gated to D12+).
  • BC-wrap refactor: introducedVersion values match the Drupal minor where the replacement lands (verified against RequirementSeverity @ 11.2, FormLocation @ 11.4, etc.).
  • Coverage-registry confirmed dev-only (composer autoload is src/ only).

Recommend before merge

  1. Fix ReplaceDialogClassOptionRector guard order (item 1) — clear bug.
  2. Decide on MethodToMethodWithCheckRector maybe-path (item 2) — silent rewrite for pre-10.0 configs is a regression vs. the previous TODO behavior.
  3. Default $introducedVersion to '0.0.0' in both value objects, or add CHANGELOG ### Changed (item 3).
  4. Add breaking-sets migration callout to CHANGELOG.
  5. Tighten name/scope guards on TaxonomyTermPageVariableToViewModeRector and ReplaceHideShowWithPrintedRector — both have realistic false-positive vectors.

The rest can ship and be tightened in follow-ups.

bbrala added 7 commits May 28, 2026 13:18
ClassConstantToClassConstantConfiguration and MethodToMethodWithCheckConfiguration
gained a required $introducedVersion constructor argument in the BC-wrapping
refactor. Make it optional with default '0.0.0' so downstream consumers that
instantiate these value objects directly keep working. The default falls
outside the BC-wrap gate (< 10.0.0), so pre-refactor no-wrap behaviour is
preserved when the argument is omitted.
… strings

Require the input string to use the conventional `Name: value` form
(colon-space) and the name part to match a conservative ASCII pattern
(`[A-Za-z][A-Za-z0-9-]*`) before splitting. Without these guards, strings
like 'http://example.com' were silently rewritten to 'http' => '//example.com'.

Adds a no-change fixture covering URL-shaped strings, paths with queries,
empty/space/digit/underscore name parts, and the no-space-after-colon form.
…fixture

Adds a regression fixture covering a class defined inside the trait's own
namespace (`Drupal\Component\Utility`) using the short-name
`use ToStringTrait;` with no explicit import. Confirms that
`isName($traitName, FQCN)` resolves via PhpParser's NameResolver and
rewrites the class correctly. No production code change.
…ions array

Previously the rector unset `dialogClass` and reindexed the items array
before checking whether the merge path could complete (e.g. existing
`classes` resolves to a literal Array_, or both sides of a ui-dialog
concatenation are String_ literals). When a later check failed, the
rector returned null with `dialogClass` already deleted — a silent
partial rewrite that lost the option entirely.

Refactor splits refactor() into three phases: locate items (capturing
node references, not indexes), validate the chosen merge branch, then
mutate. Also bails on pathological duplicate `dialogClass` keys instead
of silently rewriting one and leaving the other.

Adds three no-change fixtures: non-literal new value, non-Array_
`classes`, and duplicate `dialogClass` entries.
…d on direct parent

Previously the rector matched any class with a 5+ param constructor whose
4th argument was typed ImageToolkitInterface — regardless of the class
hierarchy. A class with that coincidental shape but extending some other
base would have its constructor silently mutated.

Tightens the guard to require the class to directly extend
ImageToolkitOperationBase. Adds a no-change fixture for a lookalike class
with the matching shape but a different parent.
… closures in $toolkit count

The 'exactly once' usage count for $toolkit descended into nested
closures and arrow-functions, where a `$toolkit` parameter shadow or
`use ($toolkit)` capture would have inflated the count and caused the
rector to refuse to rewrite an otherwise-valid constructor.

Contrib search (api.tresbien.tech) found zero ImageToolkitOperationBase
subclasses with this pattern, so the impact is purely defensive — but
the tightening is cheap and locks in the intended outer-scope-only
semantic. No fixture added (no realistic trigger).
The bullet under [Unreleased] / ### Changed previously claimed the BC
wrap "addresses the underlying concern" of the dropped maybe-path TODO
comment in all cases. That's only true for configurations with
introducedVersion >= 10.0.0 — pre-10.0 entries (urlInfo, getLowercaseLabel,
clearCsrfTokenSeed) fall outside the BC-wrap gate and rewrite a
maybe-typed receiver unconditionally.

Contrib audit (api.tresbien.tech) found zero live callers of those three
methods on receivers PHPStan cannot resolve, so the residual risk is
theoretical. Updates the CHANGELOG bullet to acknowledge this explicitly
rather than overclaim safety.
@bbrala
Copy link
Copy Markdown
Owner Author

bbrala commented May 28, 2026

Self-review follow-up — all 7 blocker/high items addressed

Resolution of the items raised in the self-review comment above. Full test suite green (486 / 486).

Item Disposition Commit
1 — ReplaceDialogClassOptionRector partial-rewrite Fixed 84b63657
2 — MethodToMethodWithCheckRector silent maybe-path Closed as no-op + CHANGELOG honesty patch beafd63e
3 — Configuration ctor BC break Fixed a5e0a463
4 — RemoveToolkitArg… missing extends check Fixed bd3fcfd2
5 — RemoveToolkitArg… closure-scope miscount Tightened defensively 190b583c
6 — DrupalGetHeadersAssocArrayRector colon explode Fixed acd29102
7 — RemoveDrupalToStringTrait same-namespace short-name Closed as no-op + regression fixture d221548b

Highlights

Item 1 — ReplaceDialogClassOptionRector: real bug, fixed. The rector now splits refactor() into three phases — locate items (capturing node references, not indexes), validate the chosen merge branch, then mutate. Also bails on pathological duplicate dialogClass keys. Three new no-change fixtures cover: non-literal new value, non-Array_ classes value, and duplicate keys.

Item 3 — Value-object ctor BC: ClassConstantToClassConstantConfiguration and MethodToMethodWithCheckConfiguration now default $introducedVersion to '0.0.0'. The default falls outside the BC-wrap gate (< 10.0.0), so downstream consumers that omit the new arg get pre-refactor no-wrap behaviour. CHANGELOG bullet amended to reflect the optional default.

Item 6 — DrupalGetHeadersAssocArrayRector: rector now requires the input string to use the conventional Name: value form (colon-space) and the name part to match a conservative ASCII pattern before splitting. Strings like 'http://example.com' were previously rewritten to 'http' => '//example.com'. No-change fixture covers URL/path/empty-name/space/digit-start/underscore name cases.

Items 4 + 5 — RemoveToolkitArg…: added a direct-parent check (extends ImageToolkitOperationBase) so coincidentally-shaped constructors on unrelated bases are not rewritten. Closure traversal now skips nested Closure / ArrowFunction bodies so a \$toolkit-shadowing inner scope can't inflate the outer count.

Closed as no-op (contrib search showed no realistic trigger)

Item 2 — pre-10.0 maybe-path silent rewrite: contrib search (api.tresbien.tech) for the three pre-10.0 configured methods returned ~2 live callers for urlInfo(), 0 for getLowercaseLabel() and clearCsrfTokenSeed(), all on @var EntityInterface / function-parameter typed receivers (yes-path). The maybe-path silent rewrite is theoretical. CHANGELOG bullet tightened to honestly state that pre-10.0 configurations fall outside the BC-wrap gate, rather than claim full coverage.

Item 5 — closure-scope $toolkit miscount: contrib search for function (\$toolkit and use (\$toolkit) patterns inside ImageToolkitOperationBase subclasses returned zero matches. Closure-traversal-skip applied as a cheap defensive measure; no fixture added (no realistic trigger to assert on).

Item 7 — same-namespace short-name resolution: added a fixture for a class in Drupal\Component\Utility using short-name use ToStringTrait; without an import. PhpParser's NameResolver resolves it correctly and the rector rewrites as expected — concern was overstated. Fixture stays as a regression guard.

Medium-tier follow-ups not addressed in this batch

Several MED items from the original review remain — ReplaceLocaleTranslationPathConfigRector config-vs-settings storage gap, TaxonomyTermPageVariableToViewModeRector str_contains name-match, ReplaceHideShowWithPrintedRector namespaced-hide() exposure, ReplaceExpectDeprecationRector regex-vs-exact match semantics. Worth a separate PR or follow-up commits — none rise to blocker.

Existing consumers of Drupal11SetList::DRUPAL_111 / DRUPAL_112 / DRUPAL_113
who relied on the listed RenameClassRector entries firing automatically
will silently stop getting those rewrites on upgrade — the entries moved
to the new opt-in *_BREAKING sets. Adds a Changed bullet under
[Unreleased] listing each affected minor and the matching set consumers
must opt into.
@bbrala
Copy link
Copy Markdown
Owner Author

bbrala commented May 28, 2026

MED-tier review items — disposition

Follow-up on the MED items called out in the self-review comment. Each was verified against real Drupal contrib via the api.tresbien.tech JSON API before deciding.

Item Disposition Commit
ReplaceHideShowWithPrintedRector — namespaced hide() / show() exposure Closed as no-op
TaxonomyTermPageVariableToViewModeRectorstr_contains matching helpers Closed as no-op
ReplaceLocaleTranslationPathConfigRector — storage-gap (config vs settings.php) Closed as no-op (docblock sufficient)
ReplaceExpectDeprecationRector — regex-vs-exact semantics Closed as no-op
Breaking-sets CHANGELOG migration callout Fixed e9f0d16a
Version-discrimination fixture gap on the two refactored rectors Closed as no-op

Why the no-ops

ReplaceHideShowWithPrintedRector: the rector already filters on instanceof FuncCall, which excludes method calls. Contrib search showed all hide($form[...]) / show(...) callers are global function calls, and zero contrib modules define their own function hide() / function show() in a namespace that would shadow Drupal's globals. The reviewer's namespace-shadow scenario is hypothetical.

TaxonomyTermPageVariableToViewModeRector: the only contrib hit for the suffix-pattern (sshop_preprocess_taxonomy_term__hot_category) is a legitimate theme-suggestion preprocess hook that the rector correctly rewrites. The reviewer's false-positive examples (_helper_preprocess_taxonomy_term_extra, preprocessTaxonomyTermMobile) returned zero contrib matches. The three live $variables['page'] reads in contrib (mercury_editor, par, rdf_taxonomy) are all simple top-level code; no closure-scope shadowing.

ReplaceLocaleTranslationPathConfigRector: the storage-gap risk is real for sites that customized locale.settings:translation.path in config but haven't migrated to settings.php. The existing docblock (lines 28–36) already warns about this. Adding an inline TODO comment would require sidestepping AbstractDrupalCoreRector's auto-wrap (rewriting ~60 lines of working code). Cost outweighs benefit; the docblock + the post-update.php migration that Drupal core itself runs are sufficient.

ReplaceExpectDeprecationRector: Drupal's ExpectDeprecationTrait::expectDeprecation() does wrap the message in %A...%A regex boundaries (reviewer was correct). But all 23 contrib callers of expectDeprecation('...') pass the complete verbatim deprecation message copied from the @trigger_error() source — zero use partial / wildcard patterns. After the rewrite to exact-match expectUserDeprecationMessage(), all 23 still match because they were always full-string matches.

Version-discrimination fixture gap: the fixture-below-version/ setup currently uses setDrupalVersion('1.0.0'), which gates everything off uniformly. A fixture at an intermediate version (e.g. 11.3.0) that asserts 11.4 entries skip while 11.2 entries fire would validate per-entry gating more thoroughly. Not a correctness bug — the gating logic itself is unit-tested via the per-introducedVersion version_compare in AbstractDrupalCoreRector::supportBackwardsCompatibility(). Deferred as a coverage-quality follow-up rather than a blocker.

What was fixed

e9f0d16a — Breaking-sets CHANGELOG callout: adds an explicit ### Changed bullet under [Unreleased] telling existing DRUPAL_111 / DRUPAL_112 / DRUPAL_113 consumers that the listed RenameClassRector entries moved to opt-in *_BREAKING sets, with the exact mapping per minor. Without this, those consumers would have silently stopped getting the rewrites on upgrade.

Net

All 7 blocker/high items and 6 MED items addressed. 1 production-code fix (#1's ReplaceDialogClassOptionRector validate-then-mutate refactor), 4 defensive code-tightening fixes (items 3, 4, 5, 6), and 3 documentation fixes (items 2, 7, and the breaking-sets callout). 5 items closed as no-op after contrib data showed no realistic trigger.

The contrib-data-driven approach also surfaced a useful pattern for future PR reviews: many "could this break?" concerns about rector matching can be answered cheaply by querying api.tresbien.tech for the pattern across all Drupal contrib, before designing a fix.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant