From dddef182b882992fd1aa85b3c74789f5b7436f8e Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Fri, 30 Jan 2026 23:19:06 +0100 Subject: [PATCH 1/2] [phpunit 12] Add /ExpressionCreateMockToCreateStubRector --- config/sets/phpunit120.php | 2 + .../Source/InstanceWithMock.php | 15 ++ ...essionCreateMockToCreateStubRectorTest.php | 28 +++ .../Fixture/fixture.php.inc | 39 ++++ .../Fixture/skip_mocking.php.inc | 18 ++ .../Fixture/skip_used_outside_arg.php.inc | 21 ++ .../Source/ClassWithDependency.php | 16 ++ .../config/configured_rule.php | 9 + ...ExpressionCreateMockToCreateStubRector.php | 187 ++++++++++++++++++ 9 files changed, 335 insertions(+) create mode 100644 rules-tests/PHPUnit120/Rector/CallLike/CreateStubOverCreateMockArgRector/Source/InstanceWithMock.php create mode 100644 rules-tests/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector/ExpressionCreateMockToCreateStubRectorTest.php create mode 100644 rules-tests/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector/Fixture/fixture.php.inc create mode 100644 rules-tests/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector/Fixture/skip_mocking.php.inc create mode 100644 rules-tests/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector/Fixture/skip_used_outside_arg.php.inc create mode 100644 rules-tests/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector/Source/ClassWithDependency.php create mode 100644 rules-tests/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector/config/configured_rule.php create mode 100644 rules/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector.php diff --git a/config/sets/phpunit120.php b/config/sets/phpunit120.php index 5990bd7b..5f3cec26 100644 --- a/config/sets/phpunit120.php +++ b/config/sets/phpunit120.php @@ -6,6 +6,7 @@ use Rector\PHPUnit\PHPUnit120\Rector\CallLike\CreateStubOverCreateMockArgRector; use Rector\PHPUnit\PHPUnit120\Rector\Class_\AssertIsTypeMethodCallRector; use Rector\PHPUnit\PHPUnit120\Rector\Class_\RemoveOverrideFinalConstructTestCaseRector; +use Rector\PHPUnit\PHPUnit120\Rector\ClassMethod\ExpressionCreateMockToCreateStubRector; return static function (RectorConfig $rectorConfig): void { $rectorConfig->rules([ @@ -14,6 +15,7 @@ // stubs over mocks CreateStubOverCreateMockArgRector::class, + ExpressionCreateMockToCreateStubRector::class, // experimental, from PHPUnit 12.5.2 // @see https://github.com/sebastianbergmann/phpunit/commit/24c208d6a340c3071f28a9b5cce02b9377adfd43 diff --git a/rules-tests/PHPUnit120/Rector/CallLike/CreateStubOverCreateMockArgRector/Source/InstanceWithMock.php b/rules-tests/PHPUnit120/Rector/CallLike/CreateStubOverCreateMockArgRector/Source/InstanceWithMock.php new file mode 100644 index 00000000..a3e96ca6 --- /dev/null +++ b/rules-tests/PHPUnit120/Rector/CallLike/CreateStubOverCreateMockArgRector/Source/InstanceWithMock.php @@ -0,0 +1,15 @@ +object; + } +} diff --git a/rules-tests/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector/ExpressionCreateMockToCreateStubRectorTest.php b/rules-tests/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector/ExpressionCreateMockToCreateStubRectorTest.php new file mode 100644 index 00000000..5742a88f --- /dev/null +++ b/rules-tests/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector/ExpressionCreateMockToCreateStubRectorTest.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/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector/Fixture/fixture.php.inc b/rules-tests/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector/Fixture/fixture.php.inc new file mode 100644 index 00000000..3f4db74f --- /dev/null +++ b/rules-tests/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector/Fixture/fixture.php.inc @@ -0,0 +1,39 @@ +createMock(\stdClass::class); + + $someObject = new ClassWithDependency($mock); + $this->assertSame($mock, $someObject->getDependency()); + } +} + +?> +----- +createStub(\stdClass::class); + + $someObject = new ClassWithDependency($mock); + $this->assertSame($mock, $someObject->getDependency()); + } +} + +?> diff --git a/rules-tests/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector/Fixture/skip_mocking.php.inc b/rules-tests/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector/Fixture/skip_mocking.php.inc new file mode 100644 index 00000000..0e2bc46a --- /dev/null +++ b/rules-tests/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector/Fixture/skip_mocking.php.inc @@ -0,0 +1,18 @@ +createMock(\stdClass::class); + $mock->expects($this->once())->method('someMethod')->willReturn('someValue'); + + $someObject = new ClassWithDependency($mock); + $this->assertSame($mock, $someObject->getDependency()); + } +} diff --git a/rules-tests/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector/Fixture/skip_used_outside_arg.php.inc b/rules-tests/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector/Fixture/skip_used_outside_arg.php.inc new file mode 100644 index 00000000..90840606 --- /dev/null +++ b/rules-tests/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector/Fixture/skip_used_outside_arg.php.inc @@ -0,0 +1,21 @@ +createMock(\stdClass::class); + + if ($mock instanceof \stdClass) { + // do something + } + + $someObject = new ClassWithDependency($mock); + $this->assertSame($mock, $someObject->getDependency()); + } +} diff --git a/rules-tests/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector/Source/ClassWithDependency.php b/rules-tests/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector/Source/ClassWithDependency.php new file mode 100644 index 00000000..e09079bc --- /dev/null +++ b/rules-tests/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector/Source/ClassWithDependency.php @@ -0,0 +1,16 @@ +dependency; + } +} diff --git a/rules-tests/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector/config/configured_rule.php b/rules-tests/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector/config/configured_rule.php new file mode 100644 index 00000000..533d9330 --- /dev/null +++ b/rules-tests/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector/config/configured_rule.php @@ -0,0 +1,9 @@ +withRules(rules: [ExpressionCreateMockToCreateStubRector::class]); diff --git a/rules/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector.php b/rules/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector.php new file mode 100644 index 00000000..c1c481e1 --- /dev/null +++ b/rules/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector.php @@ -0,0 +1,187 @@ +createMock(SomeClass::class); + + $someObject = new SomeClass($mock); + $this->assertSame($mock, $someObject->getDependency()); + } +} +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +use PHPUnit\Framework\TestCase; + +final class SomeTest extends TestCase +{ + public function test(): void + { + $mock = $this->createStub(SomeClass::class); + + $someObject = new SomeClass($mock); + $this->assertSame($mock, $someObject->getDependency()); + } +} +CODE_SAMPLE + ), + + ] + ); + } + + public function getNodeTypes(): array + { + return [ClassMethod::class]; + } + + /** + * @param ClassMethod $node + */ + public function refactor(Node $node): ?ClassMethod + { + if (! $this->testsNodeAnalyzer->isTestClassMethod($node)) { + return null; + } + + if ($node->stmts === null || count($node->stmts) < 2) { + return null; + } + + $hasChanged = false; + + foreach ($node->stmts as $stmt) { + if (! $stmt instanceof Expression) { + continue; + } + + if (! $stmt->expr instanceof Assign) { + continue; + } + + $typeArg = $this->assignedMocksCollector->matchCreateMockArgAssignedToVariable($stmt->expr); + if (! $typeArg instanceof Arg) { + continue; + } + + /** @var Assign $assign */ + $assign = $stmt->expr; + + if (! $assign->var instanceof Variable) { + continue; + } + + $assignedVariable = $assign->var; + $variableName = $this->getName($assignedVariable); + if ($variableName === null) { + continue; + } + + // find variable usages outside call like and inside it + $usedVariables = $this->variableFinder->find($node, $variableName); + + // used variable in calls + /** @var array $callLikes */ + $callLikes = $this->betterNodeFinder->findInstancesOfScoped($node->stmts, [CallLike::class]); + + $callLikeUsedVariables = $this->collectVariableInCallLikeArg($callLikes, $variableName); + + if (count($usedVariables) - 1 !== count($callLikeUsedVariables)) { + continue; + } + + // here we can flip the createMock() to createStub() + + if (! $assign->expr instanceof MethodCall) { + continue; + } + + $methodCall = $assign->expr; + $methodCall->name = new Identifier('createStub'); + + $hasChanged = true; + } + + if ($hasChanged) { + return $node; + } + + return null; + } + + /** + * @param CallLike[] $callLikes + * @return Variable[] + */ + private function collectVariableInCallLikeArg(array $callLikes, string $variableName): array + { + $callLikeUsedVariables = []; + + foreach ($callLikes as $callLike) { + if ($callLike->isFirstClassCallable()) { + continue; + } + + foreach ($callLike->getArgs() as $arg) { + if (! $arg->value instanceof Variable) { + continue; + } + + if (! $this->isName($arg->value, $variableName)) { + continue; + } + + $callLikeUsedVariables[] = $arg->value; + } + } + + return $callLikeUsedVariables; + } +} From 7f028e8d62affe208d81ae2a0cbd8b161ffd3f8f Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sat, 31 Jan 2026 09:34:14 +0100 Subject: [PATCH 2/2] fixup! [phpunit 12] Add /ExpressionCreateMockToCreateStubRector --- .../skip_property_for_external_scope.php.inc | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 rules-tests/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector/Fixture/skip_property_for_external_scope.php.inc diff --git a/rules-tests/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector/Fixture/skip_property_for_external_scope.php.inc b/rules-tests/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector/Fixture/skip_property_for_external_scope.php.inc new file mode 100644 index 00000000..babc90b2 --- /dev/null +++ b/rules-tests/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector/Fixture/skip_property_for_external_scope.php.inc @@ -0,0 +1,19 @@ +mock = $this->createMock(\stdClass::class); + + $someObject = new ClassWithDependency($this->mock); + $this->assertSame($this->mock, $someObject->getDependency()); + } +}