Skip to content

[DX] Add typeGuardedClasses() to guard against breaking method signature changes#8135

Merged
TomasVotruba merged 1 commit into
mainfrom
tv-type-guarded-classes-feature
Jul 1, 2026
Merged

[DX] Add typeGuardedClasses() to guard against breaking method signature changes#8135
TomasVotruba merged 1 commit into
mainfrom
tv-type-guarded-classes-feature

Conversation

@TomasVotruba

@TomasVotruba TomasVotruba commented Jul 1, 2026

Copy link
Copy Markdown
Member

What

New config option to protect classes whose method signatures must not change, because doing so would break subclasses in other packages/apps:

return RectorConfig::configure()
    ->withTypeGuardedClasses([SomeContract::class, BaseController::class]);

Adding a return type or param type to a method is a breaking change for any child class that overrides it. When a class is listed here, type-declaration rules leave its method signatures untouched.

Rules

  • Applies to a listed class and all its descendants — resolved upward via ClassReflection::is(). Rector can't enumerate children at runtime, but a descendant has the guarded ancestor, so upward resolution covers the whole subtree.
  • Only non-final classes are guarded — a final class can't be extended, so the change is always safe and still applied.
  • When no classes are configured the guard short-circuits, so existing behaviour is unchanged.

How it is wired

The check lives in ParentClassMethodTypeOverrideGuard::isTypeGuardedClass() and is applied at the shared choke points so it covers whole rule families, not just individual rules:

Layer Covers
ClassMethodReturnTypeOverrideGuard::shouldSkipClassMethod() the return-type family (ReturnTypeFromStrict*, Bool/Numeric/StringReturnTypeFrom*, ...)
ClassMethodParamVendorLockResolver::isVendorLocked() the param-type family routed through the param completer
AddReturnTypeDeclarationRector / AddParamTypeDeclarationRector the configurable rules directly

This reuses the existing vendor-lock layer, which already answers "is it unsafe to change this method's type?" — guarded classes are simply one more reason it is unsafe. (The existing guards handle the upward case: don't diverge from a parent contract. This adds the downward case: don't break unknown children.)

Config API

  • RectorConfig::typeGuardedClasses(string[] $classes)
  • RectorConfigBuilder::withTypeGuardedClasses(string[] $classes)
  • Option::TYPE_GUARDED_CLASSES

Test

rules-tests/TypeDeclaration/TypeGuardedClasses/ — a non-final child of a guarded class is skipped; a final child still gets the return type added.

@TomasVotruba TomasVotruba force-pushed the tv-type-guarded-classes-feature branch from 5e89bca to f8aa84f Compare July 1, 2026 20:34
@TomasVotruba TomasVotruba changed the title [POC] typeGuardedClasses() — guard breaking method signature changes [TypeDeclaration] Add typeGuardedClasses() to guard against breaking method signature changes Jul 1, 2026
@TomasVotruba TomasVotruba force-pushed the tv-type-guarded-classes-feature branch 2 times, most recently from c3b0c00 to 959be9e Compare July 1, 2026 20:46
…method signature changes

Introduces ->withTypeGuardedClasses([...]) / RectorConfig::typeGuardedClasses([...]).
Listed classes and their non-final descendants are left untouched by type-declaration
rules, since adding a return or param type to an extendable class is a breaking change
for its child overrides. Final classes are never guarded (cannot be extended).

Logic lives in ParentClassMethodTypeOverrideGuard::isTypeGuardedClass() and is applied
at the shared choke points, covering the return and param rule families:
- ClassMethodReturnTypeOverrideGuard::shouldSkipClassMethod() (return family)
- ClassMethodParamVendorLockResolver::isVendorLocked() (param completer path)
- AbstractParamTypeByMethodCallTypeRector::shouldSkipClassMethod() (by-method-call param family)
- AddReturnTypeDeclarationRector / AddParamTypeDeclarationRector (configurable rules)

When no classes are configured the guard short-circuits, so existing behaviour is
unchanged.
@TomasVotruba TomasVotruba force-pushed the tv-type-guarded-classes-feature branch from 959be9e to e2c86fc Compare July 1, 2026 20:49
@TomasVotruba TomasVotruba merged commit 356d61b into main Jul 1, 2026
65 checks passed
@TomasVotruba TomasVotruba deleted the tv-type-guarded-classes-feature branch July 1, 2026 20:52
@TomasVotruba TomasVotruba changed the title [TypeDeclaration] Add typeGuardedClasses() to guard against breaking method signature changes [DX] Add typeGuardedClasses() to guard against breaking method signature changes Jul 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant