From 6e13fab60fc9cd46fa192f4102c8d0ff32cb5ef8 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Fri, 31 Jul 2026 08:57:55 +0200 Subject: [PATCH 1/2] [PHPUnit 11.5] Add AssertContainsOnlyMethodCallRector, bond both deprecated-generic-assert rules to phpunit >=11.5 --- config/sets/phpunit110.php | 9 +- ...AssertContainsOnlyMethodCallRectorTest.php | 28 +++ .../Fixture/fixture.php.inc | 63 +++++++ .../Fixture/skip_class_name_type.php.inc | 16 ++ .../Fixture/static_fixture.php.inc | 33 ++++ .../config/configured_rule.php | 10 ++ .../AssertContainsOnlyMethodCallRector.php | 159 ++++++++++++++++++ .../Class_/AssertIsTypeMethodCallRector.php | 15 +- 8 files changed, 329 insertions(+), 4 deletions(-) create mode 100644 rules-tests/PHPUnit110/Rector/CallLike/AssertContainsOnlyMethodCallRector/AssertContainsOnlyMethodCallRectorTest.php create mode 100644 rules-tests/PHPUnit110/Rector/CallLike/AssertContainsOnlyMethodCallRector/Fixture/fixture.php.inc create mode 100644 rules-tests/PHPUnit110/Rector/CallLike/AssertContainsOnlyMethodCallRector/Fixture/skip_class_name_type.php.inc create mode 100644 rules-tests/PHPUnit110/Rector/CallLike/AssertContainsOnlyMethodCallRector/Fixture/static_fixture.php.inc create mode 100644 rules-tests/PHPUnit110/Rector/CallLike/AssertContainsOnlyMethodCallRector/config/configured_rule.php create mode 100644 rules/PHPUnit110/Rector/CallLike/AssertContainsOnlyMethodCallRector.php diff --git a/config/sets/phpunit110.php b/config/sets/phpunit110.php index 576dd372..896dd122 100644 --- a/config/sets/phpunit110.php +++ b/config/sets/phpunit110.php @@ -3,8 +3,15 @@ declare(strict_types=1); use Rector\Config\RectorConfig; +use Rector\PHPUnit\PHPUnit110\Rector\CallLike\AssertContainsOnlyMethodCallRector; use Rector\PHPUnit\PHPUnit110\Rector\Class_\NamedArgumentForDataProviderRector; +use Rector\PHPUnit\PHPUnit120\Rector\Class_\AssertIsTypeMethodCallRector; return static function (RectorConfig $rectorConfig): void { - $rectorConfig->rule(NamedArgumentForDataProviderRector::class); + $rectorConfig->rules([ + NamedArgumentForDataProviderRector::class, + // deprecated in PHPUnit 11.5, guarded by composer package constraint + AssertContainsOnlyMethodCallRector::class, + AssertIsTypeMethodCallRector::class, + ]); }; diff --git a/rules-tests/PHPUnit110/Rector/CallLike/AssertContainsOnlyMethodCallRector/AssertContainsOnlyMethodCallRectorTest.php b/rules-tests/PHPUnit110/Rector/CallLike/AssertContainsOnlyMethodCallRector/AssertContainsOnlyMethodCallRectorTest.php new file mode 100644 index 00000000..51ca6ace --- /dev/null +++ b/rules-tests/PHPUnit110/Rector/CallLike/AssertContainsOnlyMethodCallRector/AssertContainsOnlyMethodCallRectorTest.php @@ -0,0 +1,28 @@ +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/rules-tests/PHPUnit110/Rector/CallLike/AssertContainsOnlyMethodCallRector/Fixture/fixture.php.inc b/rules-tests/PHPUnit110/Rector/CallLike/AssertContainsOnlyMethodCallRector/Fixture/fixture.php.inc new file mode 100644 index 00000000..0f9c900b --- /dev/null +++ b/rules-tests/PHPUnit110/Rector/CallLike/AssertContainsOnlyMethodCallRector/Fixture/fixture.php.inc @@ -0,0 +1,63 @@ +assertContainsOnly('array', [[]]); + $this->assertContainsOnly('bool', [true]); + $this->assertContainsOnly('boolean', [true]); + $this->assertContainsOnly('callable', [fn () => 0]); + $this->assertContainsOnly('double', [1.0]); + $this->assertContainsOnly('float', [1.0]); + $this->assertContainsOnly('int', [1]); + $this->assertContainsOnly('integer', [1]); + $this->assertContainsOnly('iterable', [[]]); + $this->assertContainsOnly('null', [null]); + $this->assertContainsOnly('numeric', [12]); + $this->assertContainsOnly('object', [new \stdClass()]); + $this->assertContainsOnly('real', [1.0]); + $this->assertContainsOnly('resource', [$resource]); + $this->assertContainsOnly('resource (closed)', [$closedResource]); + $this->assertContainsOnly('scalar', ['']); + $this->assertContainsOnly('string', ['']); + } +} + +?> +----- +assertContainsOnlyArray([[]]); + $this->assertContainsOnlyBool([true]); + $this->assertContainsOnlyBool([true]); + $this->assertContainsOnlyCallable([fn () => 0]); + $this->assertContainsOnlyFloat([1.0]); + $this->assertContainsOnlyFloat([1.0]); + $this->assertContainsOnlyInt([1]); + $this->assertContainsOnlyInt([1]); + $this->assertContainsOnlyIterable([[]]); + $this->assertContainsOnlyNull([null]); + $this->assertContainsOnlyNumeric([12]); + $this->assertContainsOnlyObject([new \stdClass()]); + $this->assertContainsOnlyFloat([1.0]); + $this->assertContainsOnlyResource([$resource]); + $this->assertContainsOnlyClosedResource([$closedResource]); + $this->assertContainsOnlyScalar(['']); + $this->assertContainsOnlyString(['']); + } +} + +?> diff --git a/rules-tests/PHPUnit110/Rector/CallLike/AssertContainsOnlyMethodCallRector/Fixture/skip_class_name_type.php.inc b/rules-tests/PHPUnit110/Rector/CallLike/AssertContainsOnlyMethodCallRector/Fixture/skip_class_name_type.php.inc new file mode 100644 index 00000000..cfe369ef --- /dev/null +++ b/rules-tests/PHPUnit110/Rector/CallLike/AssertContainsOnlyMethodCallRector/Fixture/skip_class_name_type.php.inc @@ -0,0 +1,16 @@ +assertContainsOnly(\stdClass::class, [new \stdClass()]); + $this->assertContainsOnly('string', ['a'], false); + $this->assertContainsOnly('string', ['a'], $isNativeType); + $this->assertContainsOnly($type, ['a']); + } +} diff --git a/rules-tests/PHPUnit110/Rector/CallLike/AssertContainsOnlyMethodCallRector/Fixture/static_fixture.php.inc b/rules-tests/PHPUnit110/Rector/CallLike/AssertContainsOnlyMethodCallRector/Fixture/static_fixture.php.inc new file mode 100644 index 00000000..205d2031 --- /dev/null +++ b/rules-tests/PHPUnit110/Rector/CallLike/AssertContainsOnlyMethodCallRector/Fixture/static_fixture.php.inc @@ -0,0 +1,33 @@ + +----- + diff --git a/rules-tests/PHPUnit110/Rector/CallLike/AssertContainsOnlyMethodCallRector/config/configured_rule.php b/rules-tests/PHPUnit110/Rector/CallLike/AssertContainsOnlyMethodCallRector/config/configured_rule.php new file mode 100644 index 00000000..6bd28fad --- /dev/null +++ b/rules-tests/PHPUnit110/Rector/CallLike/AssertContainsOnlyMethodCallRector/config/configured_rule.php @@ -0,0 +1,10 @@ +rule(AssertContainsOnlyMethodCallRector::class); +}; diff --git a/rules/PHPUnit110/Rector/CallLike/AssertContainsOnlyMethodCallRector.php b/rules/PHPUnit110/Rector/CallLike/AssertContainsOnlyMethodCallRector.php new file mode 100644 index 00000000..1f8bd93f --- /dev/null +++ b/rules/PHPUnit110/Rector/CallLike/AssertContainsOnlyMethodCallRector.php @@ -0,0 +1,159 @@ + + */ + private const array TYPE_VALUE_TO_METHOD = [ + 'array' => 'assertContainsOnlyArray', + 'bool' => 'assertContainsOnlyBool', + 'boolean' => 'assertContainsOnlyBool', + 'callable' => 'assertContainsOnlyCallable', + 'double' => 'assertContainsOnlyFloat', + 'float' => 'assertContainsOnlyFloat', + 'int' => 'assertContainsOnlyInt', + 'integer' => 'assertContainsOnlyInt', + 'iterable' => 'assertContainsOnlyIterable', + 'null' => 'assertContainsOnlyNull', + 'numeric' => 'assertContainsOnlyNumeric', + 'object' => 'assertContainsOnlyObject', + 'real' => 'assertContainsOnlyFloat', + 'resource' => 'assertContainsOnlyResource', + 'resource (closed)' => 'assertContainsOnlyClosedResource', + 'scalar' => 'assertContainsOnlyScalar', + 'string' => 'assertContainsOnlyString', + ]; + + public function __construct( + private readonly ValueResolver $valueResolver, + private readonly TestsNodeAnalyzer $testsNodeAnalyzer, + ) { + } + + public function provideComposerPackageConstraint(): ComposerPackageConstraint + { + return new ComposerPackageConstraint('phpunit/phpunit', '>=11.5'); + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition( + 'Replaces `Assert::assertContainsOnly()` calls with type-specific `Assert::assertContainsOnly*()` calls', + [ + new CodeSample( + <<<'CODE_SAMPLE' + use PHPUnit\Framework\TestCase; + + final class SomeClass extends TestCase + { + public function testMethod(): void + { + $this->assertContainsOnly('string', ['a', 'b']); + } + } + CODE_SAMPLE + , + <<<'CODE_SAMPLE' + use PHPUnit\Framework\TestCase; + + final class SomeClass extends TestCase + { + public function testMethod(): void + { + $this->assertContainsOnlyString(['a', 'b']); + } + } + CODE_SAMPLE + , + ), + ], + ); + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [MethodCall::class, StaticCall::class]; + } + + /** + * @param MethodCall|StaticCall $node + */ + public function refactor(Node $node): Node|null + { + if ($node->isFirstClassCallable()) { + return null; + } + + if (! $this->testsNodeAnalyzer->isPHPUnitTestCaseCall($node) || ! $this->isName( + $node->name, + 'assertContainsOnly' + )) { + return null; + } + + $typeArg = $node->getArg('type', 0); + $haystackArg = $node->getArg('haystack', 1); + if (! $typeArg instanceof Arg || ! $haystackArg instanceof Arg) { + return null; + } + + $typeValue = $this->valueResolver->getValue($typeArg); + if (! is_string($typeValue)) { + return null; + } + + $newMethodName = self::TYPE_VALUE_TO_METHOD[$typeValue] ?? null; + if ($newMethodName === null) { + return null; + } + + // the $isNativeType argument can turn the type into a class name, that has no type-specific method + $isNativeTypeArg = $node->getArg('isNativeType', 2); + if ($isNativeTypeArg instanceof Arg && ! $this->valueResolver->isTrue( + $isNativeTypeArg->value + ) && ! $this->valueResolver->isNull($isNativeTypeArg->value)) { + return null; + } + + $newArgs = [new Arg($haystackArg->value)]; + + $messageArg = $node->getArg('message', 3); + if ($messageArg instanceof Arg) { + $newArgs[] = new Arg($messageArg->value); + } + + if ($node instanceof MethodCall) { + return new MethodCall($node->var, $newMethodName, $newArgs); + } + + return new StaticCall($node->class, $newMethodName, $newArgs); + } +} diff --git a/rules/PHPUnit120/Rector/Class_/AssertIsTypeMethodCallRector.php b/rules/PHPUnit120/Rector/Class_/AssertIsTypeMethodCallRector.php index 528d7a74..881c3533 100644 --- a/rules/PHPUnit120/Rector/Class_/AssertIsTypeMethodCallRector.php +++ b/rules/PHPUnit120/Rector/Class_/AssertIsTypeMethodCallRector.php @@ -11,16 +11,20 @@ use Rector\PhpParser\Node\Value\ValueResolver; use Rector\PHPUnit\NodeAnalyzer\TestsNodeAnalyzer; use Rector\Rector\AbstractRector; +use Rector\VersionBonding\Contract\ComposerPackageConstraintInterface; +use Rector\VersionBonding\ValueObject\ComposerPackageConstraint; use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; /** - * @see https://github.com/sebastianbergmann/phpunit/issues/6053 - * @see https://github.com/sebastianbergmann/phpunit/blob/12.0.0/ChangeLog-12.0.md + * The is*() methods were added and isType() deprecated in PHPUnit 11.5 + * + * @see https://github.com/sebastianbergmann/phpunit/issues/6052 + * @see https://github.com/sebastianbergmann/phpunit/blob/11.5.0/ChangeLog-11.5.md * * @see \Rector\PHPUnit\Tests\PHPUnit120\Rector\Class_\AssertIsTypeMethodCallRector\AssertIsTypeMethodCallRectorTest */ -final class AssertIsTypeMethodCallRector extends AbstractRector +final class AssertIsTypeMethodCallRector extends AbstractRector implements ComposerPackageConstraintInterface { /** * @var array @@ -51,6 +55,11 @@ public function __construct( ) { } + public function provideComposerPackageConstraint(): ComposerPackageConstraint + { + return new ComposerPackageConstraint('phpunit/phpunit', '>=11.5'); + } + public function getRuleDefinition(): RuleDefinition { return new RuleDefinition( From 4ed153a3c5f87ea28784fc1b677c661d11278c67 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Fri, 31 Jul 2026 12:56:12 +0200 Subject: [PATCH 2/2] register both PHPUnit 11.5 deprecation rules in 12.x and 13.x sets for direct upgrades --- config/sets/phpunit120.php | 3 +++ config/sets/phpunit130.php | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/config/sets/phpunit120.php b/config/sets/phpunit120.php index 946f3630..ec165330 100644 --- a/config/sets/phpunit120.php +++ b/config/sets/phpunit120.php @@ -3,6 +3,7 @@ declare(strict_types=1); use Rector\Config\RectorConfig; +use Rector\PHPUnit\PHPUnit110\Rector\CallLike\AssertContainsOnlyMethodCallRector; use Rector\PHPUnit\PHPUnit120\Rector\Class_\AssertIsTypeMethodCallRector; use Rector\PHPUnit\PHPUnit120\Rector\Class_\RemoveOverrideFinalConstructTestCaseRector; use Rector\PHPUnit\Set\PHPUnitSetList; @@ -12,6 +13,8 @@ $rectorConfig->rules([ RemoveOverrideFinalConstructTestCaseRector::class, + // deprecated in PHPUnit 11.5, repeated here for a direct 11.4 → 12.0 upgrade + AssertContainsOnlyMethodCallRector::class, AssertIsTypeMethodCallRector::class, ]); }; diff --git a/config/sets/phpunit130.php b/config/sets/phpunit130.php index 6ae86d17..0053d3ef 100644 --- a/config/sets/phpunit130.php +++ b/config/sets/phpunit130.php @@ -3,10 +3,18 @@ declare(strict_types=1); use Rector\Config\RectorConfig; +use Rector\PHPUnit\PHPUnit110\Rector\CallLike\AssertContainsOnlyMethodCallRector; +use Rector\PHPUnit\PHPUnit120\Rector\Class_\AssertIsTypeMethodCallRector; use Rector\Renaming\Rector\MethodCall\RenameMethodRector; use Rector\Renaming\ValueObject\MethodCallRename; return static function (RectorConfig $rectorConfig): void { + $rectorConfig->rules([ + // deprecated in PHPUnit 11.5, repeated here for a direct upgrade from an older version + AssertContainsOnlyMethodCallRector::class, + AssertIsTypeMethodCallRector::class, + ]); + $rectorConfig->ruleWithConfiguration(RenameMethodRector::class, [ // @see https://github.com/sebastianbergmann/phpunit/issues/6560 new MethodCallRename('PHPUnit\Framework\TestCase', 'expectExceptionMessage', 'expectExceptionMessageIsOrContains'),