From cb2137cf8e375970a23196a3d37016b91c9f4c52 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Fri, 31 Jul 2026 18:18:10 +0200 Subject: [PATCH 1/2] Add MockObjectArgCreateStubToCreateMockRector, turn createStub() back to createMock() when passed to MockObject param --- config/sets/phpunit-mock-to-stub.php | 4 + config/sets/phpunit110.php | 2 + .../Fixture/fixture.php.inc | 51 ++++ .../intersection_docblock_param.php.inc | 57 +++++ .../Fixture/named_arg.php.inc | 49 ++++ .../Fixture/skip_plain_param_type.php.inc | 20 ++ .../Fixture/skip_stub_only_usage.php.inc | 17 ++ ...ectArgCreateStubToCreateMockRectorTest.php | 28 ++ .../Source/SomeUser.php | 13 + .../config/configured_rule.php | 10 + ...kObjectArgCreateStubToCreateMockRector.php | 242 ++++++++++++++++++ 11 files changed, 493 insertions(+) create mode 100644 rules-tests/PHPUnit110/Rector/ClassMethod/MockObjectArgCreateStubToCreateMockRector/Fixture/fixture.php.inc create mode 100644 rules-tests/PHPUnit110/Rector/ClassMethod/MockObjectArgCreateStubToCreateMockRector/Fixture/intersection_docblock_param.php.inc create mode 100644 rules-tests/PHPUnit110/Rector/ClassMethod/MockObjectArgCreateStubToCreateMockRector/Fixture/named_arg.php.inc create mode 100644 rules-tests/PHPUnit110/Rector/ClassMethod/MockObjectArgCreateStubToCreateMockRector/Fixture/skip_plain_param_type.php.inc create mode 100644 rules-tests/PHPUnit110/Rector/ClassMethod/MockObjectArgCreateStubToCreateMockRector/Fixture/skip_stub_only_usage.php.inc create mode 100644 rules-tests/PHPUnit110/Rector/ClassMethod/MockObjectArgCreateStubToCreateMockRector/MockObjectArgCreateStubToCreateMockRectorTest.php create mode 100644 rules-tests/PHPUnit110/Rector/ClassMethod/MockObjectArgCreateStubToCreateMockRector/Source/SomeUser.php create mode 100644 rules-tests/PHPUnit110/Rector/ClassMethod/MockObjectArgCreateStubToCreateMockRector/config/configured_rule.php create mode 100644 rules/PHPUnit110/Rector/ClassMethod/MockObjectArgCreateStubToCreateMockRector.php diff --git a/config/sets/phpunit-mock-to-stub.php b/config/sets/phpunit-mock-to-stub.php index a2fee2ea..985a9ebe 100644 --- a/config/sets/phpunit-mock-to-stub.php +++ b/config/sets/phpunit-mock-to-stub.php @@ -6,6 +6,7 @@ use Rector\PHPUnit\CodeQuality\Rector\Class_\AddIntersectionVarToMockObjectPropertyRector; use Rector\PHPUnit\CodeQuality\Rector\Class_\AddStubIntersectionVarToStubPropertyRector; use Rector\PHPUnit\CodeQuality\Rector\ClassMethod\BareCreateMockAssignToDirectUseRector; +use Rector\PHPUnit\PHPUnit110\Rector\ClassMethod\MockObjectArgCreateStubToCreateMockRector; use Rector\PHPUnit\PHPUnit120\Rector\CallLike\CreateStubInCoalesceArgRector; use Rector\PHPUnit\PHPUnit120\Rector\CallLike\CreateStubOverCreateMockArgRector; use Rector\PHPUnit\PHPUnit120\Rector\Class_\PropertyCreateMockToCreateStubRector; @@ -23,5 +24,8 @@ AddIntersectionVarToMockObjectPropertyRector::class, AddStubIntersectionVarToStubPropertyRector::class, BareCreateMockAssignToDirectUseRector::class, + + // mocks back over stubs, where mock object is required + MockObjectArgCreateStubToCreateMockRector::class, ]); }; diff --git a/config/sets/phpunit110.php b/config/sets/phpunit110.php index 896dd122..dc10c6f2 100644 --- a/config/sets/phpunit110.php +++ b/config/sets/phpunit110.php @@ -5,11 +5,13 @@ use Rector\Config\RectorConfig; use Rector\PHPUnit\PHPUnit110\Rector\CallLike\AssertContainsOnlyMethodCallRector; use Rector\PHPUnit\PHPUnit110\Rector\Class_\NamedArgumentForDataProviderRector; +use Rector\PHPUnit\PHPUnit110\Rector\ClassMethod\MockObjectArgCreateStubToCreateMockRector; use Rector\PHPUnit\PHPUnit120\Rector\Class_\AssertIsTypeMethodCallRector; return static function (RectorConfig $rectorConfig): void { $rectorConfig->rules([ NamedArgumentForDataProviderRector::class, + MockObjectArgCreateStubToCreateMockRector::class, // deprecated in PHPUnit 11.5, guarded by composer package constraint AssertContainsOnlyMethodCallRector::class, AssertIsTypeMethodCallRector::class, diff --git a/rules-tests/PHPUnit110/Rector/ClassMethod/MockObjectArgCreateStubToCreateMockRector/Fixture/fixture.php.inc b/rules-tests/PHPUnit110/Rector/ClassMethod/MockObjectArgCreateStubToCreateMockRector/Fixture/fixture.php.inc new file mode 100644 index 00000000..0a7af160 --- /dev/null +++ b/rules-tests/PHPUnit110/Rector/ClassMethod/MockObjectArgCreateStubToCreateMockRector/Fixture/fixture.php.inc @@ -0,0 +1,51 @@ +createStub(SomeUser::class); + $this->prepareUserMock($user); + } + + private function prepareUserMock(MockObject $user): void + { + $user->expects($this->once()) + ->method('getId') + ->willReturn(1); + } +} + +?> +----- +createMock(SomeUser::class); + $this->prepareUserMock($user); + } + + private function prepareUserMock(MockObject $user): void + { + $user->expects($this->once()) + ->method('getId') + ->willReturn(1); + } +} + +?> diff --git a/rules-tests/PHPUnit110/Rector/ClassMethod/MockObjectArgCreateStubToCreateMockRector/Fixture/intersection_docblock_param.php.inc b/rules-tests/PHPUnit110/Rector/ClassMethod/MockObjectArgCreateStubToCreateMockRector/Fixture/intersection_docblock_param.php.inc new file mode 100644 index 00000000..66a446b5 --- /dev/null +++ b/rules-tests/PHPUnit110/Rector/ClassMethod/MockObjectArgCreateStubToCreateMockRector/Fixture/intersection_docblock_param.php.inc @@ -0,0 +1,57 @@ +createStub(SomeUser::class); + $this->prepareCommonMocks('some_event', $user); + } + + /** + * @param MockObject&SomeUser $user + */ + private function prepareCommonMocks(string $event, MockObject $user): void + { + $user->expects($this->once()) + ->method('getId') + ->willReturn(1); + } +} + +?> +----- +createMock(SomeUser::class); + $this->prepareCommonMocks('some_event', $user); + } + + /** + * @param MockObject&SomeUser $user + */ + private function prepareCommonMocks(string $event, MockObject $user): void + { + $user->expects($this->once()) + ->method('getId') + ->willReturn(1); + } +} + +?> diff --git a/rules-tests/PHPUnit110/Rector/ClassMethod/MockObjectArgCreateStubToCreateMockRector/Fixture/named_arg.php.inc b/rules-tests/PHPUnit110/Rector/ClassMethod/MockObjectArgCreateStubToCreateMockRector/Fixture/named_arg.php.inc new file mode 100644 index 00000000..a2122b6a --- /dev/null +++ b/rules-tests/PHPUnit110/Rector/ClassMethod/MockObjectArgCreateStubToCreateMockRector/Fixture/named_arg.php.inc @@ -0,0 +1,49 @@ +createStub(SomeUser::class); + $this->prepareUserMock(user: $user); + } + + private function prepareUserMock(MockObject $user): void + { + $user->expects($this->once()) + ->method('getId'); + } +} + +?> +----- +createMock(SomeUser::class); + $this->prepareUserMock(user: $user); + } + + private function prepareUserMock(MockObject $user): void + { + $user->expects($this->once()) + ->method('getId'); + } +} + +?> diff --git a/rules-tests/PHPUnit110/Rector/ClassMethod/MockObjectArgCreateStubToCreateMockRector/Fixture/skip_plain_param_type.php.inc b/rules-tests/PHPUnit110/Rector/ClassMethod/MockObjectArgCreateStubToCreateMockRector/Fixture/skip_plain_param_type.php.inc new file mode 100644 index 00000000..25963e41 --- /dev/null +++ b/rules-tests/PHPUnit110/Rector/ClassMethod/MockObjectArgCreateStubToCreateMockRector/Fixture/skip_plain_param_type.php.inc @@ -0,0 +1,20 @@ +createStub(SomeUser::class); + $this->useUser($user); + } + + private function useUser(SomeUser $user): void + { + $user->getId(); + } +} diff --git a/rules-tests/PHPUnit110/Rector/ClassMethod/MockObjectArgCreateStubToCreateMockRector/Fixture/skip_stub_only_usage.php.inc b/rules-tests/PHPUnit110/Rector/ClassMethod/MockObjectArgCreateStubToCreateMockRector/Fixture/skip_stub_only_usage.php.inc new file mode 100644 index 00000000..ce44db1c --- /dev/null +++ b/rules-tests/PHPUnit110/Rector/ClassMethod/MockObjectArgCreateStubToCreateMockRector/Fixture/skip_stub_only_usage.php.inc @@ -0,0 +1,17 @@ +createStub(SomeUser::class); + $user->method('getId')->willReturn(1); + + $this->assertSame(1, $user->getId()); + } +} diff --git a/rules-tests/PHPUnit110/Rector/ClassMethod/MockObjectArgCreateStubToCreateMockRector/MockObjectArgCreateStubToCreateMockRectorTest.php b/rules-tests/PHPUnit110/Rector/ClassMethod/MockObjectArgCreateStubToCreateMockRector/MockObjectArgCreateStubToCreateMockRectorTest.php new file mode 100644 index 00000000..f74216af --- /dev/null +++ b/rules-tests/PHPUnit110/Rector/ClassMethod/MockObjectArgCreateStubToCreateMockRector/MockObjectArgCreateStubToCreateMockRectorTest.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/ClassMethod/MockObjectArgCreateStubToCreateMockRector/Source/SomeUser.php b/rules-tests/PHPUnit110/Rector/ClassMethod/MockObjectArgCreateStubToCreateMockRector/Source/SomeUser.php new file mode 100644 index 00000000..ec86aafd --- /dev/null +++ b/rules-tests/PHPUnit110/Rector/ClassMethod/MockObjectArgCreateStubToCreateMockRector/Source/SomeUser.php @@ -0,0 +1,13 @@ +rule(MockObjectArgCreateStubToCreateMockRector::class); +}; diff --git a/rules/PHPUnit110/Rector/ClassMethod/MockObjectArgCreateStubToCreateMockRector.php b/rules/PHPUnit110/Rector/ClassMethod/MockObjectArgCreateStubToCreateMockRector.php new file mode 100644 index 00000000..8fc7f624 --- /dev/null +++ b/rules/PHPUnit110/Rector/ClassMethod/MockObjectArgCreateStubToCreateMockRector.php @@ -0,0 +1,242 @@ +createStub(SomeClass::class); + $this->prepareMock($someMock); + } + + private function prepareMock(MockObject $someMock): void + { + $someMock->expects($this->once()) + ->method('someMethod'); + } +} +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +final class SomeTest extends TestCase +{ + public function test() + { + $someMock = $this->createMock(SomeClass::class); + $this->prepareMock($someMock); + } + + private function prepareMock(MockObject $someMock): void + { + $someMock->expects($this->once()) + ->method('someMethod'); + } +} +CODE_SAMPLE + ), + ] + ); + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [ClassMethod::class]; + } + + /** + * @param ClassMethod $node + */ + public function refactor(Node $node): ?ClassMethod + { + if (! $this->testsNodeAnalyzer->isInTestClass($node)) { + return null; + } + + $createStubMethodCallsByVariableName = $this->collectCreateStubAssigns($node); + if ($createStubMethodCallsByVariableName === []) { + return null; + } + + $hasChanged = false; + + foreach ($createStubMethodCallsByVariableName as $variableName => $createStubMethodCall) { + if (! $this->isPassedAsMockObjectArg($node, (string) $variableName)) { + continue; + } + + $createStubMethodCall->name = new Identifier('createMock'); + $hasChanged = true; + } + + if ($hasChanged) { + return $node; + } + + return null; + } + + /** + * @return array + */ + private function collectCreateStubAssigns(ClassMethod $classMethod): array + { + $createStubMethodCallsByVariableName = []; + + foreach ((array) $classMethod->stmts as $stmt) { + if (! $stmt instanceof Expression) { + continue; + } + + if (! $stmt->expr instanceof Assign) { + continue; + } + + $assign = $stmt->expr; + if (! $assign->var instanceof Variable) { + continue; + } + + if (! $assign->expr instanceof MethodCall) { + continue; + } + + $methodCall = $assign->expr; + if (! $this->isName($methodCall->name, 'createStub')) { + continue; + } + + $variableName = $this->getName($assign->var); + if ($variableName === null) { + continue; + } + + $createStubMethodCallsByVariableName[$variableName] = $methodCall; + } + + return $createStubMethodCallsByVariableName; + } + + private function isPassedAsMockObjectArg(ClassMethod $classMethod, string $variableName): bool + { + /** @var array $callLikes */ + $callLikes = $this->betterNodeFinder->findInstancesOfScoped( + (array) $classMethod->stmts, + [MethodCall::class, StaticCall::class] + ); + + foreach ($callLikes as $callLike) { + if ($callLike->isFirstClassCallable()) { + continue; + } + + foreach ($callLike->getArgs() as $argIndex => $arg) { + if (! $arg->value instanceof Variable) { + continue; + } + + if (! $this->isName($arg->value, $variableName)) { + continue; + } + + if ($this->isMockObjectParam($callLike, $arg->name, $argIndex)) { + return true; + } + } + } + + return false; + } + + private function isMockObjectParam( + MethodCall|StaticCall $callLike, + ?Identifier $identifier, + int $argIndex + ): bool { + $methodReflection = $callLike instanceof MethodCall + ? $this->reflectionResolver->resolveMethodReflectionFromMethodCall($callLike) + : $this->reflectionResolver->resolveMethodReflectionFromStaticCall($callLike); + + if (! $methodReflection instanceof MethodReflection) { + return false; + } + + $mockObjectType = new ObjectType(PHPUnitClassName::MOCK_OBJECT); + + foreach ($methodReflection->getVariants() as $parametersAcceptor) { + $parameters = $parametersAcceptor->getParameters(); + + if ($identifier instanceof Identifier) { + foreach ($parameters as $parameter) { + if ($parameter->getName() !== $identifier->toString()) { + continue; + } + + if ($mockObjectType->isSuperTypeOf($parameter->getType())->yes()) { + return true; + } + } + + continue; + } + + if (! isset($parameters[$argIndex])) { + continue; + } + + if ($mockObjectType->isSuperTypeOf($parameters[$argIndex]->getType())->yes()) { + return true; + } + } + + return false; + } +} From 1112790630354fe03ca8cb07d7e5e2870c3ef65d Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Fri, 31 Jul 2026 18:21:21 +0200 Subject: [PATCH 2/2] Bond rule to phpunit/phpunit >=11.0 composer constraint --- .../MockObjectArgCreateStubToCreateMockRector.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/rules/PHPUnit110/Rector/ClassMethod/MockObjectArgCreateStubToCreateMockRector.php b/rules/PHPUnit110/Rector/ClassMethod/MockObjectArgCreateStubToCreateMockRector.php index 8fc7f624..54a7b9ee 100644 --- a/rules/PHPUnit110/Rector/ClassMethod/MockObjectArgCreateStubToCreateMockRector.php +++ b/rules/PHPUnit110/Rector/ClassMethod/MockObjectArgCreateStubToCreateMockRector.php @@ -19,13 +19,15 @@ use Rector\PHPUnit\NodeAnalyzer\TestsNodeAnalyzer; use Rector\Rector\AbstractRector; use Rector\Reflection\ReflectionResolver; +use Rector\VersionBonding\Contract\ComposerPackageConstraintInterface; +use Rector\VersionBonding\ValueObject\ComposerPackageConstraint; use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; /** * @see \Rector\PHPUnit\Tests\PHPUnit110\Rector\ClassMethod\MockObjectArgCreateStubToCreateMockRector\MockObjectArgCreateStubToCreateMockRectorTest */ -final class MockObjectArgCreateStubToCreateMockRector extends AbstractRector +final class MockObjectArgCreateStubToCreateMockRector extends AbstractRector implements ComposerPackageConstraintInterface { public function __construct( private readonly TestsNodeAnalyzer $testsNodeAnalyzer, @@ -34,6 +36,11 @@ public function __construct( ) { } + public function provideComposerPackageConstraint(): ComposerPackageConstraint + { + return new ComposerPackageConstraint('phpunit/phpunit', '>=11.0'); + } + public function getRuleDefinition(): RuleDefinition { return new RuleDefinition(