From 8438bd2616ab78f79c0c58454fc4ea2606e6356d Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 3 Feb 2026 11:28:25 +0100 Subject: [PATCH] add variable support to CreateStubOverCreateMockArgRector --- config/sets/phpunit-code-quality.php | 6 + .../Fixture/handle_assigned_variable.php.inc | 39 +++++++ ...e_assigned_variable_multiple_times.php.inc | 43 ++++++++ .../Fixture/handle_used_outside_arg.php.inc | 51 +++++++++ .../Fixture/skip_used_outside_arg.php.inc | 21 ---- .../NodeAnalyser/AssertMethodAnalyzer.php | 2 +- .../NodeAnalyser/MockObjectExprDetector.php | 61 +++++++++++ .../CreateStubOverCreateMockArgRector.php | 103 +++++++++++++++--- ...ExpressionCreateMockToCreateStubRector.php | 62 +---------- 9 files changed, 293 insertions(+), 95 deletions(-) create mode 100644 rules-tests/PHPUnit120/Rector/CallLike/CreateStubOverCreateMockArgRector/Fixture/handle_assigned_variable.php.inc create mode 100644 rules-tests/PHPUnit120/Rector/CallLike/CreateStubOverCreateMockArgRector/Fixture/handle_assigned_variable_multiple_times.php.inc create mode 100644 rules-tests/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector/Fixture/handle_used_outside_arg.php.inc delete mode 100644 rules-tests/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector/Fixture/skip_used_outside_arg.php.inc create mode 100644 rules/CodeQuality/NodeAnalyser/MockObjectExprDetector.php diff --git a/config/sets/phpunit-code-quality.php b/config/sets/phpunit-code-quality.php index 27ee9b5d..277318ed 100644 --- a/config/sets/phpunit-code-quality.php +++ b/config/sets/phpunit-code-quality.php @@ -50,6 +50,8 @@ use Rector\PHPUnit\CodeQuality\Rector\MethodCall\UseSpecificWithMethodRector; use Rector\PHPUnit\CodeQuality\Rector\MethodCall\WithCallbackIdenticalToStandaloneAssertsRector; use Rector\PHPUnit\CodeQuality\Rector\StmtsAwareInterface\DeclareStrictTypesTestsRector; +use Rector\PHPUnit\PHPUnit120\Rector\CallLike\CreateStubOverCreateMockArgRector; +use Rector\PHPUnit\PHPUnit120\Rector\ClassMethod\ExpressionCreateMockToCreateStubRector; use Rector\PHPUnit\PHPUnit60\Rector\MethodCall\GetMockBuilderGetMockToCreateMockRector; use Rector\PHPUnit\PHPUnit90\Rector\MethodCall\ReplaceAtMethodWithDesiredMatcherRector; use Rector\Privatization\Rector\Class_\FinalizeTestCaseClassRector; @@ -125,6 +127,10 @@ SimplerWithIsInstanceOfRector::class, DirectInstanceOverMockArgRector::class, + // stub over mock + CreateStubOverCreateMockArgRector::class, + ExpressionCreateMockToCreateStubRector::class, + // @test first, enable later // \Rector\PHPUnit\CodeQuality\Rector\Expression\ConfiguredMockEntityToSetterObjectRector::class, diff --git a/rules-tests/PHPUnit120/Rector/CallLike/CreateStubOverCreateMockArgRector/Fixture/handle_assigned_variable.php.inc b/rules-tests/PHPUnit120/Rector/CallLike/CreateStubOverCreateMockArgRector/Fixture/handle_assigned_variable.php.inc new file mode 100644 index 00000000..e47a202f --- /dev/null +++ b/rules-tests/PHPUnit120/Rector/CallLike/CreateStubOverCreateMockArgRector/Fixture/handle_assigned_variable.php.inc @@ -0,0 +1,39 @@ +createMock(\stdClass::class); + + $items = [ + $itemMock + ]; + } +} + +?> +----- +createStub(\stdClass::class); + + $items = [ + $itemMock + ]; + } +} + +?> diff --git a/rules-tests/PHPUnit120/Rector/CallLike/CreateStubOverCreateMockArgRector/Fixture/handle_assigned_variable_multiple_times.php.inc b/rules-tests/PHPUnit120/Rector/CallLike/CreateStubOverCreateMockArgRector/Fixture/handle_assigned_variable_multiple_times.php.inc new file mode 100644 index 00000000..6fdaa7c7 --- /dev/null +++ b/rules-tests/PHPUnit120/Rector/CallLike/CreateStubOverCreateMockArgRector/Fixture/handle_assigned_variable_multiple_times.php.inc @@ -0,0 +1,43 @@ +createMock(\stdClass::class); + + $items = [ + $itemMock + ]; + + $anotherValue = new \stdClass($itemMock); + } +} + +?> +----- +createStub(\stdClass::class); + + $items = [ + $itemMock + ]; + + $anotherValue = new \stdClass($itemMock); + } +} + +?> diff --git a/rules-tests/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector/Fixture/handle_used_outside_arg.php.inc b/rules-tests/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector/Fixture/handle_used_outside_arg.php.inc new file mode 100644 index 00000000..dd1c19da --- /dev/null +++ b/rules-tests/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector/Fixture/handle_used_outside_arg.php.inc @@ -0,0 +1,51 @@ +createMock(\stdClass::class); + + if ($mock instanceof \stdClass) { + // do something + } + + $someObject = new ClassWithDependency($mock); + $this->assertSame($mock, $someObject->getDependency()); + } +} + +?> +----- +createStub(\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/Fixture/skip_used_outside_arg.php.inc b/rules-tests/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector/Fixture/skip_used_outside_arg.php.inc deleted file mode 100644 index 90840606..00000000 --- a/rules-tests/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector/Fixture/skip_used_outside_arg.php.inc +++ /dev/null @@ -1,21 +0,0 @@ -createMock(\stdClass::class); - - if ($mock instanceof \stdClass) { - // do something - } - - $someObject = new ClassWithDependency($mock); - $this->assertSame($mock, $someObject->getDependency()); - } -} diff --git a/rules/CodeQuality/NodeAnalyser/AssertMethodAnalyzer.php b/rules/CodeQuality/NodeAnalyser/AssertMethodAnalyzer.php index 562ccbfa..e03a42f0 100644 --- a/rules/CodeQuality/NodeAnalyser/AssertMethodAnalyzer.php +++ b/rules/CodeQuality/NodeAnalyser/AssertMethodAnalyzer.php @@ -30,7 +30,7 @@ public function detectTestCaseCall(MethodCall|StaticCall $call): bool ? $call->var : $call->class; - if (! $this->nodeTypeResolver->isObjectType($objectCaller, new ObjectType('PHPUnit\Framework\TestCase'))) { + if (! $this->nodeTypeResolver->isObjectType($objectCaller, new ObjectType(PHPUnitClassName::TEST_CASE))) { return false; } diff --git a/rules/CodeQuality/NodeAnalyser/MockObjectExprDetector.php b/rules/CodeQuality/NodeAnalyser/MockObjectExprDetector.php new file mode 100644 index 00000000..4d485e75 --- /dev/null +++ b/rules/CodeQuality/NodeAnalyser/MockObjectExprDetector.php @@ -0,0 +1,61 @@ +nodeNameResolver->getName($expr); + + // to be safe + if ($variableName === null) { + return true; + } + + $relatedVariables = $this->variableFinder->find($classMethod, $variableName); + + // only self variable found, nothing to mock + if (count($relatedVariables) === 1) { + return false; + } + + // find out, how many are used in call likes as args + /** @var array $methodCalls */ + $methodCalls = $this->betterNodeFinder->findInstancesOfScoped((array) $classMethod->stmts, [MethodCall::class]); + + foreach ($methodCalls as $methodCall) { + if (! $methodCall->var instanceof Variable) { + continue; + } + + if ($this->nodeNameResolver->isName($methodCall->var, $variableName)) { + // variable is being called on, most like mocking, lets skip + return true; + } + } + + return false; + } +} diff --git a/rules/PHPUnit120/Rector/CallLike/CreateStubOverCreateMockArgRector.php b/rules/PHPUnit120/Rector/CallLike/CreateStubOverCreateMockArgRector.php index bc120696..52284971 100644 --- a/rules/PHPUnit120/Rector/CallLike/CreateStubOverCreateMockArgRector.php +++ b/rules/PHPUnit120/Rector/CallLike/CreateStubOverCreateMockArgRector.php @@ -6,12 +6,18 @@ use PhpParser\Node; use PhpParser\Node\ArrayItem; +use PhpParser\Node\Expr; +use PhpParser\Node\Expr\Assign; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Expr\New_; use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Identifier; +use PhpParser\Node\Stmt\ClassMethod; +use PhpParser\Node\Stmt\Expression; use Rector\PHPStan\ScopeFetcher; +use Rector\PHPUnit\CodeQuality\NodeAnalyser\MockObjectExprDetector; use Rector\PHPUnit\Enum\PHPUnitClassName; +use Rector\PHPUnit\NodeAnalyzer\TestsNodeAnalyzer; use Rector\Rector\AbstractRector; use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; @@ -23,6 +29,12 @@ */ final class CreateStubOverCreateMockArgRector extends AbstractRector { + public function __construct( + private readonly TestsNodeAnalyzer $testsNodeAnalyzer, + private readonly MockObjectExprDetector $mockObjectExprDetector, + ) { + } + public function getRuleDefinition(): RuleDefinition { return new RuleDefinition( @@ -70,13 +82,13 @@ private function someMethod($someClass) */ public function getNodeTypes(): array { - return [StaticCall::class, MethodCall::class, New_::class, ArrayItem::class]; + return [StaticCall::class, MethodCall::class, New_::class, ArrayItem::class, ClassMethod::class]; } /** - * @param MethodCall|StaticCall|New_|ArrayItem $node + * @param MethodCall|StaticCall|New_|ArrayItem|ClassMethod $node */ - public function refactor(Node $node): MethodCall|StaticCall|New_|ArrayItem|null + public function refactor(Node $node): MethodCall|StaticCall|New_|ArrayItem|ClassMethod|null { $scope = ScopeFetcher::fetch($node); if (! $scope->isInClass()) { @@ -88,19 +100,12 @@ public function refactor(Node $node): MethodCall|StaticCall|New_|ArrayItem|null return null; } - if ($node instanceof ArrayItem) { - if (! $node->value instanceof MethodCall) { - return null; - } - - $methodCall = $node->value; - if (! $this->isName($methodCall->name, 'createMock')) { - return null; - } - - $methodCall->name = new Identifier('createStub'); + if ($node instanceof ClassMethod) { + return $this->refactorClassMethod($node); + } - return $node; + if ($node instanceof ArrayItem) { + return $this->refactorArrayItem($node); } $hasChanges = false; @@ -129,4 +134,72 @@ public function refactor(Node $node): MethodCall|StaticCall|New_|ArrayItem|null return null; } + + private function matchCreateMockMethodCall(Expr $expr): null|MethodCall + { + if (! $expr instanceof MethodCall) { + return null; + } + + if (! $this->isName($expr->name, 'createMock')) { + return null; + } + + return $expr; + } + + private function refactorClassMethod(ClassMethod $classMethod): ?ClassMethod + { + if (! $this->testsNodeAnalyzer->isTestClassMethod($classMethod)) { + return null; + } + + $hasChanged = false; + foreach ((array) $classMethod->stmts as $stmt) { + if (! $stmt instanceof Expression) { + continue; + } + + if (! $stmt->expr instanceof Assign) { + continue; + } + + $assign = $stmt->expr; + $createMockMethodCall = $this->matchCreateMockMethodCall($assign->expr); + + if (! $createMockMethodCall instanceof MethodCall) { + continue; + } + + // no change, as we use the variable for mocking later + if ($this->mockObjectExprDetector->isUsedForMocking($assign->var, $classMethod)) { + continue; + } + + $createMockMethodCall->name = new Identifier('createStub'); + $hasChanged = true; + } + + if ($hasChanged) { + return $classMethod; + } + + return null; + } + + private function refactorArrayItem(ArrayItem $arrayItem): ?ArrayItem + { + if (! $arrayItem->value instanceof MethodCall) { + return null; + } + + $methodCall = $arrayItem->value; + if (! $this->isName($methodCall->name, 'createMock')) { + return null; + } + + $methodCall->name = new Identifier('createStub'); + + return $arrayItem; + } } diff --git a/rules/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector.php b/rules/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector.php index c1c481e1..5c1df1b1 100644 --- a/rules/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector.php +++ b/rules/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector.php @@ -7,17 +7,13 @@ use PhpParser\Node; use PhpParser\Node\Arg; use PhpParser\Node\Expr\Assign; -use PhpParser\Node\Expr\CallLike; use PhpParser\Node\Expr\MethodCall; -use PhpParser\Node\Expr\New_; -use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Expr\Variable; use PhpParser\Node\Identifier; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Expression; -use Rector\PhpParser\Node\BetterNodeFinder; use Rector\PHPUnit\CodeQuality\NodeAnalyser\AssignedMocksCollector; -use Rector\PHPUnit\CodeQuality\NodeFinder\VariableFinder; +use Rector\PHPUnit\CodeQuality\NodeAnalyser\MockObjectExprDetector; use Rector\PHPUnit\NodeAnalyzer\TestsNodeAnalyzer; use Rector\Rector\AbstractRector; use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; @@ -31,8 +27,7 @@ final class ExpressionCreateMockToCreateStubRector extends AbstractRector public function __construct( private readonly AssignedMocksCollector $assignedMocksCollector, private readonly TestsNodeAnalyzer $testsNodeAnalyzer, - private readonly VariableFinder $variableFinder, - private readonly BetterNodeFinder $betterNodeFinder, + private readonly MockObjectExprDetector $mockObjectExprDetector, ) { } @@ -118,31 +113,11 @@ public function refactor(Node $node): ?ClassMethod 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) { + if ($this->mockObjectExprDetector->isUsedForMocking($assign->var, $node)) { continue; } + /** @var MethodCall $methodCall */ $methodCall = $assign->expr; $methodCall->name = new Identifier('createStub'); @@ -155,33 +130,4 @@ public function refactor(Node $node): ?ClassMethod 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; - } }