From 680912f0fea41ae4812a0242bb3ce517669d42d7 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Mon, 26 Jan 2026 12:48:07 +0100 Subject: [PATCH 1/2] [code-quality] Add DirectInstanceOverMockArgRector --- config/sets/phpunit-code-quality.php | 2 + .../DirectInstanceOverMockArgRectorTest.php | 28 +++ .../Fixture/handle_array_item.php.inc | 37 ++++ .../Fixture/handle_static_call.php | 18 ++ .../Fixture/handle_static_call.php.inc | 37 ++++ .../Fixture/skip_another_object.php.inc | 17 ++ .../Source/AnotherObject.php | 8 + .../config/configured_rule.php | 9 + .../DirectInstanceOverMockArgRector.php | 165 ++++++++++++++++++ .../Component/HttpFoundation/Request.php | 7 + 10 files changed, 328 insertions(+) create mode 100644 rules-tests/CodeQuality/Rector/CallLike/DirectInstanceOverMockArgRector/DirectInstanceOverMockArgRectorTest.php create mode 100644 rules-tests/CodeQuality/Rector/CallLike/DirectInstanceOverMockArgRector/Fixture/handle_array_item.php.inc create mode 100644 rules-tests/CodeQuality/Rector/CallLike/DirectInstanceOverMockArgRector/Fixture/handle_static_call.php create mode 100644 rules-tests/CodeQuality/Rector/CallLike/DirectInstanceOverMockArgRector/Fixture/handle_static_call.php.inc create mode 100644 rules-tests/CodeQuality/Rector/CallLike/DirectInstanceOverMockArgRector/Fixture/skip_another_object.php.inc create mode 100644 rules-tests/CodeQuality/Rector/CallLike/DirectInstanceOverMockArgRector/Source/AnotherObject.php create mode 100644 rules-tests/CodeQuality/Rector/CallLike/DirectInstanceOverMockArgRector/config/configured_rule.php create mode 100644 rules/CodeQuality/Rector/CallLike/DirectInstanceOverMockArgRector.php create mode 100644 stubs/Symfony/Component/HttpFoundation/Request.php diff --git a/config/sets/phpunit-code-quality.php b/config/sets/phpunit-code-quality.php index 9a168f34..d9fd8c33 100644 --- a/config/sets/phpunit-code-quality.php +++ b/config/sets/phpunit-code-quality.php @@ -3,6 +3,7 @@ declare(strict_types=1); use Rector\Config\RectorConfig; +use Rector\PHPUnit\CodeQuality\Rector\CallLike\DirectInstanceOverMockArgRector; use Rector\PHPUnit\CodeQuality\Rector\Class_\AddParamTypeFromDependsRector; use Rector\PHPUnit\CodeQuality\Rector\Class_\AddReturnTypeToDependedRector; use Rector\PHPUnit\CodeQuality\Rector\Class_\ConstructClassMethodToSetUpTestCaseRector; @@ -120,6 +121,7 @@ RemoveExpectAnyFromMockRector::class, SingleMockPropertyTypeRector::class, SimplerWithIsInstanceOfRector::class, + DirectInstanceOverMockArgRector::class, FinalizeTestCaseClassRector::class, DeclareStrictTypesTestsRector::class, diff --git a/rules-tests/CodeQuality/Rector/CallLike/DirectInstanceOverMockArgRector/DirectInstanceOverMockArgRectorTest.php b/rules-tests/CodeQuality/Rector/CallLike/DirectInstanceOverMockArgRector/DirectInstanceOverMockArgRectorTest.php new file mode 100644 index 00000000..8861c737 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/CallLike/DirectInstanceOverMockArgRector/DirectInstanceOverMockArgRectorTest.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/CodeQuality/Rector/CallLike/DirectInstanceOverMockArgRector/Fixture/handle_array_item.php.inc b/rules-tests/CodeQuality/Rector/CallLike/DirectInstanceOverMockArgRector/Fixture/handle_array_item.php.inc new file mode 100644 index 00000000..46d27a16 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/CallLike/DirectInstanceOverMockArgRector/Fixture/handle_array_item.php.inc @@ -0,0 +1,37 @@ +createMock(Request::class), + ]; + } +} + +?> +----- + diff --git a/rules-tests/CodeQuality/Rector/CallLike/DirectInstanceOverMockArgRector/Fixture/handle_static_call.php b/rules-tests/CodeQuality/Rector/CallLike/DirectInstanceOverMockArgRector/Fixture/handle_static_call.php new file mode 100644 index 00000000..cb78c945 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/CallLike/DirectInstanceOverMockArgRector/Fixture/handle_static_call.php @@ -0,0 +1,18 @@ +createMock(Request::class), + ); + } +} + +?> diff --git a/rules-tests/CodeQuality/Rector/CallLike/DirectInstanceOverMockArgRector/Fixture/handle_static_call.php.inc b/rules-tests/CodeQuality/Rector/CallLike/DirectInstanceOverMockArgRector/Fixture/handle_static_call.php.inc new file mode 100644 index 00000000..bcb7c47f --- /dev/null +++ b/rules-tests/CodeQuality/Rector/CallLike/DirectInstanceOverMockArgRector/Fixture/handle_static_call.php.inc @@ -0,0 +1,37 @@ +createMock(Request::class), + ); + } +} + +?> +----- + diff --git a/rules-tests/CodeQuality/Rector/CallLike/DirectInstanceOverMockArgRector/Fixture/skip_another_object.php.inc b/rules-tests/CodeQuality/Rector/CallLike/DirectInstanceOverMockArgRector/Fixture/skip_another_object.php.inc new file mode 100644 index 00000000..2a30c7a4 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/CallLike/DirectInstanceOverMockArgRector/Fixture/skip_another_object.php.inc @@ -0,0 +1,17 @@ +createMock(AnotherObject::class), + ); + } +} diff --git a/rules-tests/CodeQuality/Rector/CallLike/DirectInstanceOverMockArgRector/Source/AnotherObject.php b/rules-tests/CodeQuality/Rector/CallLike/DirectInstanceOverMockArgRector/Source/AnotherObject.php new file mode 100644 index 00000000..8c10a994 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/CallLike/DirectInstanceOverMockArgRector/Source/AnotherObject.php @@ -0,0 +1,8 @@ +withRules([DirectInstanceOverMockArgRector::class]); diff --git a/rules/CodeQuality/Rector/CallLike/DirectInstanceOverMockArgRector.php b/rules/CodeQuality/Rector/CallLike/DirectInstanceOverMockArgRector.php new file mode 100644 index 00000000..dd2e58f8 --- /dev/null +++ b/rules/CodeQuality/Rector/CallLike/DirectInstanceOverMockArgRector.php @@ -0,0 +1,165 @@ +someMethod($this->createMock(Request::class)); + } + + private function someMethod($someClass) + { + } +} +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; + +final class SomeTest extends TestCase +{ + public function test() + { + $this->someMethod(new Request()); + } + + private function someMethod($someClass) + { + } +} +CODE_SAMPLE + ), + + ] + ); + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [StaticCall::class, MethodCall::class, New_::class, ArrayItem::class]; + } + + /** + * @param MethodCall|StaticCall|New_|ArrayItem $node + */ + public function refactor(Node $node): MethodCall|StaticCall|New_|ArrayItem|null + { + $scope = ScopeFetcher::fetch($node); + if (! $scope->isInClass()) { + return null; + } + + $classReflection = $scope->getClassReflection(); + if (! $classReflection->is(PHPUnitClassName::TEST_CASE)) { + return null; + } + + $hasChanged = false; + + if ($node instanceof ArrayItem) { + + return $this->refactorArrayItem($node); + } + + foreach ($node->getArgs() as $arg) { + $firstArg = $this->matchCreateMockMethodCallArg($arg->value); + if (! $firstArg instanceof Arg) { + continue; + } + + $className = $this->valueResolver->getValue($firstArg->value); + if (! in_array($className, [SymfonyClass::REQUEST, SymfonyClass::REQUEST_STACK])) { + continue; + } + + $arg->value = new New_(new FullyQualified($className)); + $hasChanged = true; + } + + if ($hasChanged) { + return $node; + } + + return null; + } + + private function matchCreateMockMethodCallArg(Expr $expr): ?Arg + { + if (! $expr instanceof MethodCall) { + return null; + } + + $methodCall = $expr; + if (! $this->isName($methodCall->name, 'createMock')) { + return null; + } + + if ($methodCall->isFirstClassCallable()) { + return null; + } + + return $methodCall->getArgs()[0]; + } + + private function refactorArrayItem(ArrayItem $arrayItem): ?ArrayItem + { + $mockedCallArg = $this->matchCreateMockMethodCallArg($arrayItem->value); + if (! $mockedCallArg instanceof Arg) { + return null; + } + + $className = $this->valueResolver->getValue($mockedCallArg->value); + + if (! in_array($className, [SymfonyClass::REQUEST, SymfonyClass::REQUEST_STACK])) { + return null; + } + + $arrayItem->value = new New_(new FullyQualified($className)); + + return $arrayItem; + } +} diff --git a/stubs/Symfony/Component/HttpFoundation/Request.php b/stubs/Symfony/Component/HttpFoundation/Request.php new file mode 100644 index 00000000..6d2cfca6 --- /dev/null +++ b/stubs/Symfony/Component/HttpFoundation/Request.php @@ -0,0 +1,7 @@ + Date: Mon, 26 Jan 2026 13:05:50 +0100 Subject: [PATCH 2/2] add array item support to CreateStubOverCreateMockArgRector --- .../Fixture/handle_static_call.php | 18 ---------- .../Fixture/handle_array_item.php.inc | 35 +++++++++++++++++++ .../CreateStubOverCreateMockArgRector.php | 24 ++++++++++--- 3 files changed, 55 insertions(+), 22 deletions(-) delete mode 100644 rules-tests/CodeQuality/Rector/CallLike/DirectInstanceOverMockArgRector/Fixture/handle_static_call.php create mode 100644 rules-tests/PHPUnit120/Rector/CallLike/CreateStubOverCreateMockArgRector/Fixture/handle_array_item.php.inc diff --git a/rules-tests/CodeQuality/Rector/CallLike/DirectInstanceOverMockArgRector/Fixture/handle_static_call.php b/rules-tests/CodeQuality/Rector/CallLike/DirectInstanceOverMockArgRector/Fixture/handle_static_call.php deleted file mode 100644 index cb78c945..00000000 --- a/rules-tests/CodeQuality/Rector/CallLike/DirectInstanceOverMockArgRector/Fixture/handle_static_call.php +++ /dev/null @@ -1,18 +0,0 @@ -createMock(Request::class), - ); - } -} - -?> diff --git a/rules-tests/PHPUnit120/Rector/CallLike/CreateStubOverCreateMockArgRector/Fixture/handle_array_item.php.inc b/rules-tests/PHPUnit120/Rector/CallLike/CreateStubOverCreateMockArgRector/Fixture/handle_array_item.php.inc new file mode 100644 index 00000000..c1341a3b --- /dev/null +++ b/rules-tests/PHPUnit120/Rector/CallLike/CreateStubOverCreateMockArgRector/Fixture/handle_array_item.php.inc @@ -0,0 +1,35 @@ +createMock(\stdClass::class), + ]; + } +} + +?> +----- +createStub(\stdClass::class), + ]; + } +} + +?> diff --git a/rules/PHPUnit120/Rector/CallLike/CreateStubOverCreateMockArgRector.php b/rules/PHPUnit120/Rector/CallLike/CreateStubOverCreateMockArgRector.php index 86a1d513..1aac6d07 100644 --- a/rules/PHPUnit120/Rector/CallLike/CreateStubOverCreateMockArgRector.php +++ b/rules/PHPUnit120/Rector/CallLike/CreateStubOverCreateMockArgRector.php @@ -5,6 +5,7 @@ namespace Rector\PHPUnit\PHPUnit120\Rector\CallLike; use PhpParser\Node; +use PhpParser\Node\ArrayItem; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Expr\New_; use PhpParser\Node\Expr\StaticCall; @@ -25,7 +26,7 @@ final class CreateStubOverCreateMockArgRector extends AbstractRector public function getRuleDefinition(): RuleDefinition { return new RuleDefinition( - 'Use createStub() over createMock() when used as argument and does not add any mock requirements', + 'Use createStub() over createMock() when used as argument or array value and does not add any mock requirements', [ new CodeSample( <<<'CODE_SAMPLE' @@ -69,13 +70,13 @@ private function someMethod($someClass) */ public function getNodeTypes(): array { - return [StaticCall::class, MethodCall::class, New_::class]; + return [StaticCall::class, MethodCall::class, New_::class, ArrayItem::class]; } /** - * @param MethodCall|StaticCall|New_ $node + * @param MethodCall|StaticCall|New_|ArrayItem $node */ - public function refactor(Node $node): MethodCall|StaticCall|New_|null + public function refactor(Node $node): MethodCall|StaticCall|New_|ArrayItem|null { $scope = ScopeFetcher::fetch($node); if (! $scope->isInClass()) { @@ -87,6 +88,21 @@ public function refactor(Node $node): MethodCall|StaticCall|New_|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'); + + return $node; + } + $hasChanges = false; foreach ($node->getArgs() as $arg) {