Skip to content

[composer-based] Bond version-specific rules to composer package constraints - #754

Merged
TomasVotruba merged 1 commit into
mainfrom
composer-constraints-for-version-bound-rules
Aug 1, 2026
Merged

[composer-based] Bond version-specific rules to composer package constraints#754
TomasVotruba merged 1 commit into
mainfrom
composer-constraints-for-version-bound-rules

Conversation

@TomasVotruba

@TomasVotruba TomasVotruba commented Aug 1, 2026

Copy link
Copy Markdown
Member

Some rules and configurations emit APIs that only exist since a specific PHPUnit version, but carry no version bond. Applied to an older PHPUnit, they produce code that fatals.

Every version below was resolved from the sebastianbergmann/phpunit git history (git tag --contains <commit>), not from memory.

Rules

rule bond what breaks without it
AllowMockObjectsWhereParentClassRector >=12.5.2 #[AllowMockObjectsWithoutExpectations] does not exist yet (24c208d)
AllowMockObjectsForDataProviderRector >=12.5.2 same attribute
AllowMockObjectsWithoutExpectationsAttributeRector >=12.5.2 same attribute
RemoveOverrideFinalConstructTestCaseRector >=12.0.3 TestCase::__construct() is only final since then (3263f4c)
InlineStubPropertyToCreateStubMethodCallRector >=11.0 stub/mock split, matches the already bonded siblings
SingleMockPropertyTypeRector >=11.0 same
ChangeMockObjectReturnUnionToIntersectionRector >=11.0 same
 use Symfony\Component\Form\Test\TypeTestCase;

+#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
 final class SomeTest extends TypeTestCase
 {
 }

On PHPUnit 12.5.1 and below that attribute class does not exist, so the test class fatals. With the bond, the rule is skipped instead of producing broken code.

Method renames

The generic RenameMethodRector configurations get the same treatment, each bound to the version that added the target method:

rename target method added in
setMethods()onlyMethods() 8.3
expectExceptionMessageRegExp()expectExceptionMessageMatches() 8.4
assertRegExp(), assertNotRegExp(), assertFileNotExists(), assertFileNotIsReadable(), assertDirectoryNotExists(), assertDirectoryNotIsReadable(), assertDirectoryNotIsWritable(), assertNotIsReadable(), assertNotIsWritable() 9.1
getInvocationCount()numberOfInvocations() 10.0
assertObjectHasAttribute()assertObjectHasProperty() 10.1
expectExceptionMessage()expectExceptionMessageIsOrContains() 13.2
-$this->assertObjectHasAttribute('someProperty', new stdClass());
-$this->expectExceptionMessage('some message');
+$this->assertObjectHasProperty('someProperty', new stdClass());
+$this->expectExceptionMessageIsOrContains('some message');

Two versions worth calling out, as they are a minor later than one would assume: assertObjectHasProperty() is 10.1, not 10.0, and expectExceptionMessageIsOrContains() is 13.2. The latter is the sharpest edge - rewriting expectExceptionMessage() on a project still on PHPUnit 11 or 12 points every call at a method that does not exist there.

Set registration

Everything is registered in composer-based.php, so it applies to any project whose installed PHPUnit matches, not only through the single upgrade set that used to carry it.

The per-version sets keep their current registrations untouched, so nothing changes for anyone already using them - a rule registered by both a version set and the composer-based set is tagged only once by RectorConfig::rule(), and rule configurations are merged.

Two tests/ComposerBased fixtures cover the constructor rule and the bounded renames arriving through the composer-based set.

@TomasVotruba
TomasVotruba force-pushed the composer-constraints-for-version-bound-rules branch from d785a78 to af6969b Compare August 1, 2026 08:11
Add ComposerPackageConstraintInterface to rules that emit APIs which only
exist since a specific PHPUnit version, with each version verified against
the phpunit/phpunit git history:

* AllowMockObjectsWhereParentClassRector, AllowMockObjectsForDataProviderRector
  and AllowMockObjectsWithoutExpectationsAttributeRector >=12.5.2, as the
  #[AllowMockObjectsWithoutExpectations] attribute was added there
* RemoveOverrideFinalConstructTestCaseRector >=12.0.3, as TestCase::__construct()
  was declared final there
* InlineStubPropertyToCreateStubMethodCallRector, SingleMockPropertyTypeRector
  and ChangeMockObjectReturnUnionToIntersectionRector >=11.0, to match their
  already bonded stub/mock siblings

Register the method renames of the generic RenameMethodRector in the same way,
each bound to the PHPUnit version that added the target method:

* setMethods() => onlyMethods() since 8.3
* expectExceptionMessageRegExp() => expectExceptionMessageMatches() since 8.4
* the assertRegExp(), assertFileNotExists() and assert*Not*() family since 9.1
* getInvocationCount() => numberOfInvocations() since 10.0
* assertObjectHasAttribute() => assertObjectHasProperty() since 10.1
* expectExceptionMessage() => expectExceptionMessageIsOrContains() since 13.2

Everything lands in the composer-based set, so it applies to any project with a
matching PHPUnit version. The per-version sets keep their current registrations,
so their behavior stays backward compatible.
@TomasVotruba
TomasVotruba force-pushed the composer-constraints-for-version-bound-rules branch from af6969b to 89f684a Compare August 1, 2026 08:18
@TomasVotruba TomasVotruba changed the title Bond version-specific rules to composer package constraints [composer-based] Bond version-specific rules to composer package constraints Aug 1, 2026
@TomasVotruba
TomasVotruba merged commit 3d863a2 into main Aug 1, 2026
7 checks passed
@TomasVotruba
TomasVotruba deleted the composer-constraints-for-version-bound-rules branch August 1, 2026 08:24
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