From 0e64280d22faa076843f195910bd9877841d5c9e Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Mon, 29 Jun 2026 23:39:32 +0200 Subject: [PATCH 1/6] [CodeQuality] Skip AddIntersectionVarToMockObjectPropertyRector when @var already has intersection type --- .../skip_existing_intersection_var.php.inc | 18 ++++++++++++++++++ ...tersectionVarToMockObjectPropertyRector.php | 9 +++++++++ 2 files changed, 27 insertions(+) create mode 100644 rules-tests/CodeQuality/Rector/Class_/AddIntersectionVarToMockObjectPropertyRector/Fixture/skip_existing_intersection_var.php.inc diff --git a/rules-tests/CodeQuality/Rector/Class_/AddIntersectionVarToMockObjectPropertyRector/Fixture/skip_existing_intersection_var.php.inc b/rules-tests/CodeQuality/Rector/Class_/AddIntersectionVarToMockObjectPropertyRector/Fixture/skip_existing_intersection_var.php.inc new file mode 100644 index 00000000..1c221ad3 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Class_/AddIntersectionVarToMockObjectPropertyRector/Fixture/skip_existing_intersection_var.php.inc @@ -0,0 +1,18 @@ +someServiceMock = $this->createMock(\stdClass::class); + } +} diff --git a/rules/CodeQuality/Rector/Class_/AddIntersectionVarToMockObjectPropertyRector.php b/rules/CodeQuality/Rector/Class_/AddIntersectionVarToMockObjectPropertyRector.php index bc7864a0..5745ddf0 100644 --- a/rules/CodeQuality/Rector/Class_/AddIntersectionVarToMockObjectPropertyRector.php +++ b/rules/CodeQuality/Rector/Class_/AddIntersectionVarToMockObjectPropertyRector.php @@ -10,7 +10,9 @@ use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Property; +use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode; use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory; use Rector\BetterPhpDocParser\PhpDocManipulator\PhpDocTypeChanger; use Rector\BetterPhpDocParser\ValueObject\Type\BracketsAwareIntersectionTypeNode; @@ -85,6 +87,13 @@ public function refactor(Node $node): ?Class_ ]); $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($property); + + // already has an intersection @var, skip + $varTagValueNode = $phpDocInfo->getVarTagValueNode(); + if ($varTagValueNode instanceof VarTagValueNode && $varTagValueNode->type instanceof IntersectionTypeNode) { + continue; + } + $this->phpDocTypeChanger->changeVarTypeNode($property, $phpDocInfo, $intersectionTypeNode); $hasChanged = true; From 4675450fd0dc07270aeb795d852bc7b6bdb4357c Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Mon, 29 Jun 2026 23:46:51 +0200 Subject: [PATCH 2/6] [CodeQuality] Add AddStubIntersectionVarToStubPropertyRector Add @var Stub&MockedClass intersection docblock to a bare native Stub property whose setUp() assigns createStub(). Generalize MockObjectPropertyDetector to be parametric on class name + create method, shared by both mock and stub rules. --- config/sets/phpunit-mock-to-stub.php | 2 + ...ntersectionVarToStubPropertyRectorTest.php | 28 +++ .../Fixture/fixture.php.inc | 38 ++++ .../skip_existing_intersection_var.php.inc | 18 ++ .../Fixture/skip_mock_object_property.php.inc | 15 ++ .../Fixture/skip_non_test_class.php.inc | 13 ++ .../config/configured_rule.php | 9 + .../MockObjectPropertyDetector.php | 8 +- ...tubIntersectionVarToStubPropertyRector.php | 169 ++++++++++++++++++ 9 files changed, 296 insertions(+), 4 deletions(-) create mode 100644 rules-tests/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector/AddStubIntersectionVarToStubPropertyRectorTest.php create mode 100644 rules-tests/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector/Fixture/fixture.php.inc create mode 100644 rules-tests/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector/Fixture/skip_existing_intersection_var.php.inc create mode 100644 rules-tests/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector/Fixture/skip_mock_object_property.php.inc create mode 100644 rules-tests/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector/Fixture/skip_non_test_class.php.inc create mode 100644 rules-tests/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector/config/configured_rule.php create mode 100644 rules/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector.php diff --git a/config/sets/phpunit-mock-to-stub.php b/config/sets/phpunit-mock-to-stub.php index 14a06212..d3798482 100644 --- a/config/sets/phpunit-mock-to-stub.php +++ b/config/sets/phpunit-mock-to-stub.php @@ -4,6 +4,7 @@ use Rector\Config\RectorConfig; use Rector\PHPUnit\CodeQuality\Rector\Class_\AddIntersectionVarToMockObjectPropertyRector; +use Rector\PHPUnit\CodeQuality\Rector\Class_\AddStubIntersectionVarToStubPropertyRector; use Rector\PHPUnit\PHPUnit120\Rector\CallLike\CreateStubInCoalesceArgRector; use Rector\PHPUnit\PHPUnit120\Rector\CallLike\CreateStubOverCreateMockArgRector; use Rector\PHPUnit\PHPUnit120\Rector\Class_\PropertyCreateMockToCreateStubRector; @@ -21,5 +22,6 @@ MockObjectVarToStubRector::class, BareVarToStubIntersectionRector::class, AddIntersectionVarToMockObjectPropertyRector::class, + AddStubIntersectionVarToStubPropertyRector::class, ]); }; diff --git a/rules-tests/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector/AddStubIntersectionVarToStubPropertyRectorTest.php b/rules-tests/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector/AddStubIntersectionVarToStubPropertyRectorTest.php new file mode 100644 index 00000000..9b599683 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector/AddStubIntersectionVarToStubPropertyRectorTest.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/Class_/AddStubIntersectionVarToStubPropertyRector/Fixture/fixture.php.inc b/rules-tests/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector/Fixture/fixture.php.inc new file mode 100644 index 00000000..a1e8791a --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector/Fixture/fixture.php.inc @@ -0,0 +1,38 @@ +someServiceStub = $this->createStub(\stdClass::class); + } +} + +?> +----- +someServiceStub = $this->createStub(\stdClass::class); + } +} + +?> diff --git a/rules-tests/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector/Fixture/skip_existing_intersection_var.php.inc b/rules-tests/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector/Fixture/skip_existing_intersection_var.php.inc new file mode 100644 index 00000000..257c5697 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector/Fixture/skip_existing_intersection_var.php.inc @@ -0,0 +1,18 @@ +someServiceStub = $this->createStub(\stdClass::class); + } +} diff --git a/rules-tests/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector/Fixture/skip_mock_object_property.php.inc b/rules-tests/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector/Fixture/skip_mock_object_property.php.inc new file mode 100644 index 00000000..a7015200 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector/Fixture/skip_mock_object_property.php.inc @@ -0,0 +1,15 @@ +someServiceMock = $this->createStub(\stdClass::class); + } +} diff --git a/rules-tests/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector/Fixture/skip_non_test_class.php.inc b/rules-tests/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector/Fixture/skip_non_test_class.php.inc new file mode 100644 index 00000000..067e18ad --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector/Fixture/skip_non_test_class.php.inc @@ -0,0 +1,13 @@ +someServiceStub = $this->createStub(\stdClass::class); + } +} diff --git a/rules-tests/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector/config/configured_rule.php b/rules-tests/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector/config/configured_rule.php new file mode 100644 index 00000000..e74fe74d --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector/config/configured_rule.php @@ -0,0 +1,9 @@ +withRules([AddStubIntersectionVarToStubPropertyRector::class]); diff --git a/rules/CodeQuality/NodeAnalyser/MockObjectPropertyDetector.php b/rules/CodeQuality/NodeAnalyser/MockObjectPropertyDetector.php index 232bfbe7..f5c850bf 100644 --- a/rules/CodeQuality/NodeAnalyser/MockObjectPropertyDetector.php +++ b/rules/CodeQuality/NodeAnalyser/MockObjectPropertyDetector.php @@ -21,19 +21,19 @@ public function __construct( ) { } - public function detect(Property $property): bool + public function detect(Property $property, string $className = PHPUnitClassName::MOCK_OBJECT): bool { if (! $property->type instanceof FullyQualified) { return false; } - return $property->type->toString() === PHPUnitClassName::MOCK_OBJECT; + return $property->type->toString() === $className; } /** * @return array */ - public function collectFromClassMethod(ClassMethod $classMethod): array + public function collectFromClassMethod(ClassMethod $classMethod, string $methodName = 'createMock'): array { $propertyNamesToCreateMockMethodCalls = []; @@ -57,7 +57,7 @@ public function collectFromClassMethod(ClassMethod $classMethod): array } $methodCall = $assign->expr; - if (! $this->nodeNameResolver->isName($methodCall->name, 'createMock')) { + if (! $this->nodeNameResolver->isName($methodCall->name, $methodName)) { continue; } diff --git a/rules/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector.php b/rules/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector.php new file mode 100644 index 00000000..922597ab --- /dev/null +++ b/rules/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector.php @@ -0,0 +1,169 @@ +testsNodeAnalyzer->isInTestClass($node)) { + return null; + } + + $setUpClassMethod = $node->getMethod(MethodName::SET_UP); + if (! $setUpClassMethod instanceof ClassMethod) { + return null; + } + + $propertyNamesToCreateStubMethodCalls = $this->mockObjectPropertyDetector->collectFromClassMethod( + $setUpClassMethod, + 'createStub' + ); + if ($propertyNamesToCreateStubMethodCalls === []) { + return null; + } + + $hasChanged = false; + + foreach ($propertyNamesToCreateStubMethodCalls as $propertyName => $createStubMethodCall) { + $property = $node->getProperty($propertyName); + if (! $property instanceof Property) { + continue; + } + + // only properties typed as a bare native Stub + if (! $this->mockObjectPropertyDetector->detect($property, PHPUnitClassName::STUB)) { + continue; + } + + $stubbedClass = $this->resolveStubbedClass($createStubMethodCall); + if ($stubbedClass === null) { + continue; + } + + $intersectionTypeNode = new BracketsAwareIntersectionTypeNode([ + new IdentifierTypeNode('\\' . PHPUnitClassName::STUB), + new IdentifierTypeNode('\\' . $stubbedClass), + ]); + + $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($property); + + // already has an intersection @var, skip + $varTagValueNode = $phpDocInfo->getVarTagValueNode(); + if ($varTagValueNode instanceof VarTagValueNode && $varTagValueNode->type instanceof IntersectionTypeNode) { + continue; + } + + $this->phpDocTypeChanger->changeVarTypeNode($property, $phpDocInfo, $intersectionTypeNode); + + $hasChanged = true; + } + + if (! $hasChanged) { + return null; + } + + return $node; + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition( + 'Add a Stub intersection @var docblock with the stubbed class to a native Stub property', + [ + new CodeSample( + <<<'CODE_SAMPLE' +use PHPUnit\Framework\TestCase; + +final class SomeTest extends TestCase +{ + private \PHPUnit\Framework\MockObject\Stub $someServiceStub; + + protected function setUp(): void + { + $this->someServiceStub = $this->createStub(SomeService::class); + } +} +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +use PHPUnit\Framework\TestCase; + +final class SomeTest extends TestCase +{ + /** + * @var \PHPUnit\Framework\MockObject\Stub&\SomeService + */ + private \PHPUnit\Framework\MockObject\Stub $someServiceStub; + + protected function setUp(): void + { + $this->someServiceStub = $this->createStub(SomeService::class); + } +} +CODE_SAMPLE + ), + ] + ); + } + + private function resolveStubbedClass(MethodCall $methodCall): ?string + { + $firstArg = $methodCall->getArgs()[0] ?? null; + if ($firstArg === null) { + return null; + } + + if (! $firstArg->value instanceof ClassConstFetch) { + return null; + } + + $className = $this->getName($firstArg->value->class); + if (! is_string($className)) { + return null; + } + + return $className; + } +} From 55b8303776d41db57c4e59f69fbd46394e8d5840 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Mon, 29 Jun 2026 23:51:10 +0200 Subject: [PATCH 3/6] [CodeQuality] Support self::createStub()/createMock() static calls in intersection @var rules Generalize MockObjectPropertyDetector to collect both $this->create*() (MethodCall) and self::create*() (StaticCall). Widen both rules' class resolvers accordingly. Add static-call fixtures. --- .../Fixture/static_call.php.inc | 38 +++++++++++++++++++ .../Fixture/static_call.php.inc | 38 +++++++++++++++++++ .../MockObjectPropertyDetector.php | 12 +++--- ...ersectionVarToMockObjectPropertyRector.php | 5 ++- ...tubIntersectionVarToStubPropertyRector.php | 5 ++- 5 files changed, 89 insertions(+), 9 deletions(-) create mode 100644 rules-tests/CodeQuality/Rector/Class_/AddIntersectionVarToMockObjectPropertyRector/Fixture/static_call.php.inc create mode 100644 rules-tests/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector/Fixture/static_call.php.inc diff --git a/rules-tests/CodeQuality/Rector/Class_/AddIntersectionVarToMockObjectPropertyRector/Fixture/static_call.php.inc b/rules-tests/CodeQuality/Rector/Class_/AddIntersectionVarToMockObjectPropertyRector/Fixture/static_call.php.inc new file mode 100644 index 00000000..0624b3f1 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Class_/AddIntersectionVarToMockObjectPropertyRector/Fixture/static_call.php.inc @@ -0,0 +1,38 @@ +someServiceMock = self::createMock(\stdClass::class); + } +} + +?> +----- +someServiceMock = self::createMock(\stdClass::class); + } +} + +?> diff --git a/rules-tests/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector/Fixture/static_call.php.inc b/rules-tests/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector/Fixture/static_call.php.inc new file mode 100644 index 00000000..f435e2b9 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector/Fixture/static_call.php.inc @@ -0,0 +1,38 @@ +someServiceStub = self::createStub(\stdClass::class); + } +} + +?> +----- +someServiceStub = self::createStub(\stdClass::class); + } +} + +?> diff --git a/rules/CodeQuality/NodeAnalyser/MockObjectPropertyDetector.php b/rules/CodeQuality/NodeAnalyser/MockObjectPropertyDetector.php index f5c850bf..46b0d567 100644 --- a/rules/CodeQuality/NodeAnalyser/MockObjectPropertyDetector.php +++ b/rules/CodeQuality/NodeAnalyser/MockObjectPropertyDetector.php @@ -7,6 +7,7 @@ use PhpParser\Node\Expr\Assign; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Expr\PropertyFetch; +use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Name\FullyQualified; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Expression; @@ -31,7 +32,7 @@ public function detect(Property $property, string $className = PHPUnitClassName: } /** - * @return array + * @return array */ public function collectFromClassMethod(ClassMethod $classMethod, string $methodName = 'createMock'): array { @@ -52,12 +53,13 @@ public function collectFromClassMethod(ClassMethod $classMethod, string $methodN continue; } - if (! $assign->expr instanceof MethodCall) { + // both $this->createMock() and self::createMock() + if (! $assign->expr instanceof MethodCall && ! $assign->expr instanceof StaticCall) { continue; } - $methodCall = $assign->expr; - if (! $this->nodeNameResolver->isName($methodCall->name, $methodName)) { + $createCall = $assign->expr; + if (! $this->nodeNameResolver->isName($createCall->name, $methodName)) { continue; } @@ -68,7 +70,7 @@ public function collectFromClassMethod(ClassMethod $classMethod, string $methodN continue; } - $propertyNamesToCreateMockMethodCalls[$propertyName] = $methodCall; + $propertyNamesToCreateMockMethodCalls[$propertyName] = $createCall; } return $propertyNamesToCreateMockMethodCalls; diff --git a/rules/CodeQuality/Rector/Class_/AddIntersectionVarToMockObjectPropertyRector.php b/rules/CodeQuality/Rector/Class_/AddIntersectionVarToMockObjectPropertyRector.php index 5745ddf0..e67230bc 100644 --- a/rules/CodeQuality/Rector/Class_/AddIntersectionVarToMockObjectPropertyRector.php +++ b/rules/CodeQuality/Rector/Class_/AddIntersectionVarToMockObjectPropertyRector.php @@ -7,6 +7,7 @@ use PhpParser\Node; use PhpParser\Node\Expr\ClassConstFetch; use PhpParser\Node\Expr\MethodCall; +use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Property; @@ -147,9 +148,9 @@ protected function setUp(): void ); } - private function resolveMockedClass(MethodCall $methodCall): ?string + private function resolveMockedClass(MethodCall|StaticCall $createMockCall): ?string { - $firstArg = $methodCall->getArgs()[0] ?? null; + $firstArg = $createMockCall->getArgs()[0] ?? null; if ($firstArg === null) { return null; } diff --git a/rules/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector.php b/rules/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector.php index 922597ab..c37b6ea0 100644 --- a/rules/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector.php +++ b/rules/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector.php @@ -7,6 +7,7 @@ use PhpParser\Node; use PhpParser\Node\Expr\ClassConstFetch; use PhpParser\Node\Expr\MethodCall; +use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Property; @@ -148,9 +149,9 @@ protected function setUp(): void ); } - private function resolveStubbedClass(MethodCall $methodCall): ?string + private function resolveStubbedClass(MethodCall|StaticCall $createStubCall): ?string { - $firstArg = $methodCall->getArgs()[0] ?? null; + $firstArg = $createStubCall->getArgs()[0] ?? null; if ($firstArg === null) { return null; } From 43ac26002b69849e85623bfedb94ae1ad657cba8 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Mon, 29 Jun 2026 23:55:21 +0200 Subject: [PATCH 4/6] [CodeQuality] Cover overwriting existing single @var to Stub intersection --- .../Fixture/overwrite_existing_var.php.inc | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 rules-tests/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector/Fixture/overwrite_existing_var.php.inc diff --git a/rules-tests/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector/Fixture/overwrite_existing_var.php.inc b/rules-tests/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector/Fixture/overwrite_existing_var.php.inc new file mode 100644 index 00000000..30daa551 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector/Fixture/overwrite_existing_var.php.inc @@ -0,0 +1,41 @@ +someServiceStub = $this->createStub(\stdClass::class); + } +} + +?> +----- +someServiceStub = $this->createStub(\stdClass::class); + } +} + +?> From cab42eb604e5844b19df5a5535213471ddebacc3 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Mon, 29 Jun 2026 23:57:08 +0200 Subject: [PATCH 5/6] [CodeQuality] Fix PHPStan: widen MethodCall|StaticCall param in RemoveNeverUsedMockPropertyRector --- .../Rector/Class_/RemoveNeverUsedMockPropertyRector.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rules/CodeQuality/Rector/Class_/RemoveNeverUsedMockPropertyRector.php b/rules/CodeQuality/Rector/Class_/RemoveNeverUsedMockPropertyRector.php index a54b69dc..42b3bc7c 100644 --- a/rules/CodeQuality/Rector/Class_/RemoveNeverUsedMockPropertyRector.php +++ b/rules/CodeQuality/Rector/Class_/RemoveNeverUsedMockPropertyRector.php @@ -8,6 +8,7 @@ use PhpParser\Node\Expr\Assign; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Expr\PropertyFetch; +use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Expression; @@ -193,7 +194,7 @@ private function removePropertyFromClass(Class_ $class, string $propertyNameToRe } /** - * @param array $propertyNamesToCreateMockMethodCalls + * @param array $propertyNamesToCreateMockMethodCalls * @return string[] */ private function resolvePropertyNamesToRemove(array $propertyNamesToCreateMockMethodCalls, Class_ $class): array From b688833783e8367e880ebbf1a5ead1178a8ff2d2 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 30 Jun 2026 00:13:02 +0200 Subject: [PATCH 6/6] [CodeQuality] Merge BareVarToStubIntersectionRector into AddStubIntersectionVarToStubPropertyRector Iterate all native-Stub properties: derive the stubbed class from a setUp() createStub() arg when present, else from a bare single-class @var docblock. Absorbs and removes BareVarToStubIntersectionRector. Stub-first intersection ordering. --- config/sets/phpunit-code-quality.php | 4 +- config/sets/phpunit-mock-to-stub.php | 2 - .../Fixture/bare_var_without_setup.php.inc | 31 ++++ .../Fixture/skip_stub_short_name_var.php.inc} | 4 +- .../BareVarToStubIntersectionRectorTest.php | 28 ---- .../Fixture/bare_var.php.inc | 31 ---- .../Fixture/skip_already_intersection.php.inc | 13 -- .../Fixture/skip_non_stub_native_type.php.inc | 13 -- .../Fixture/skip_non_test_class.php.inc | 11 -- .../config/configured_rule.php | 9 -- ...tubIntersectionVarToStubPropertyRector.php | 114 ++++++++++---- .../BareVarToStubIntersectionRector.php | 145 ------------------ 12 files changed, 120 insertions(+), 285 deletions(-) create mode 100644 rules-tests/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector/Fixture/bare_var_without_setup.php.inc rename rules-tests/{PHPUnit120/Rector/Property/BareVarToStubIntersectionRector/Fixture/skip_stub_short_name.php.inc => CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector/Fixture/skip_stub_short_name_var.php.inc} (52%) delete mode 100644 rules-tests/PHPUnit120/Rector/Property/BareVarToStubIntersectionRector/BareVarToStubIntersectionRectorTest.php delete mode 100644 rules-tests/PHPUnit120/Rector/Property/BareVarToStubIntersectionRector/Fixture/bare_var.php.inc delete mode 100644 rules-tests/PHPUnit120/Rector/Property/BareVarToStubIntersectionRector/Fixture/skip_already_intersection.php.inc delete mode 100644 rules-tests/PHPUnit120/Rector/Property/BareVarToStubIntersectionRector/Fixture/skip_non_stub_native_type.php.inc delete mode 100644 rules-tests/PHPUnit120/Rector/Property/BareVarToStubIntersectionRector/Fixture/skip_non_test_class.php.inc delete mode 100644 rules-tests/PHPUnit120/Rector/Property/BareVarToStubIntersectionRector/config/configured_rule.php delete mode 100644 rules/PHPUnit120/Rector/Property/BareVarToStubIntersectionRector.php diff --git a/config/sets/phpunit-code-quality.php b/config/sets/phpunit-code-quality.php index be67fa3f..10aba745 100644 --- a/config/sets/phpunit-code-quality.php +++ b/config/sets/phpunit-code-quality.php @@ -6,6 +6,7 @@ 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_\AddStubIntersectionVarToStubPropertyRector; use Rector\PHPUnit\CodeQuality\Rector\Class_\ConstructClassMethodToSetUpTestCaseRector; use Rector\PHPUnit\CodeQuality\Rector\Class_\InlineStubPropertyToCreateStubMethodCallRector; use Rector\PHPUnit\CodeQuality\Rector\Class_\NarrowUnusedSetUpDefinedPropertyRector; @@ -57,7 +58,6 @@ use Rector\PHPUnit\PHPUnit120\Rector\CallLike\CreateStubOverCreateMockArgRector; use Rector\PHPUnit\PHPUnit120\Rector\Class_\PropertyCreateMockToCreateStubRector; use Rector\PHPUnit\PHPUnit120\Rector\ClassMethod\ExpressionCreateMockToCreateStubRector; -use Rector\PHPUnit\PHPUnit120\Rector\Property\BareVarToStubIntersectionRector; use Rector\PHPUnit\PHPUnit120\Rector\Property\MockObjectVarToStubRector; use Rector\PHPUnit\PHPUnit60\Rector\MethodCall\GetMockBuilderGetMockToCreateMockRector; use Rector\PHPUnit\PHPUnit90\Rector\MethodCall\ReplaceAtMethodWithDesiredMatcherRector; @@ -139,7 +139,7 @@ ExpressionCreateMockToCreateStubRector::class, PropertyCreateMockToCreateStubRector::class, MockObjectVarToStubRector::class, - BareVarToStubIntersectionRector::class, + AddStubIntersectionVarToStubPropertyRector::class, InlineStubPropertyToCreateStubMethodCallRector::class, // @test first, enable later diff --git a/config/sets/phpunit-mock-to-stub.php b/config/sets/phpunit-mock-to-stub.php index d3798482..72cd48c8 100644 --- a/config/sets/phpunit-mock-to-stub.php +++ b/config/sets/phpunit-mock-to-stub.php @@ -9,7 +9,6 @@ use Rector\PHPUnit\PHPUnit120\Rector\CallLike\CreateStubOverCreateMockArgRector; use Rector\PHPUnit\PHPUnit120\Rector\Class_\PropertyCreateMockToCreateStubRector; use Rector\PHPUnit\PHPUnit120\Rector\ClassMethod\ExpressionCreateMockToCreateStubRector; -use Rector\PHPUnit\PHPUnit120\Rector\Property\BareVarToStubIntersectionRector; use Rector\PHPUnit\PHPUnit120\Rector\Property\MockObjectVarToStubRector; return static function (RectorConfig $rectorConfig): void { @@ -20,7 +19,6 @@ ExpressionCreateMockToCreateStubRector::class, PropertyCreateMockToCreateStubRector::class, MockObjectVarToStubRector::class, - BareVarToStubIntersectionRector::class, AddIntersectionVarToMockObjectPropertyRector::class, AddStubIntersectionVarToStubPropertyRector::class, ]); diff --git a/rules-tests/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector/Fixture/bare_var_without_setup.php.inc b/rules-tests/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector/Fixture/bare_var_without_setup.php.inc new file mode 100644 index 00000000..f3556375 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector/Fixture/bare_var_without_setup.php.inc @@ -0,0 +1,31 @@ + +----- + diff --git a/rules-tests/PHPUnit120/Rector/Property/BareVarToStubIntersectionRector/Fixture/skip_stub_short_name.php.inc b/rules-tests/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector/Fixture/skip_stub_short_name_var.php.inc similarity index 52% rename from rules-tests/PHPUnit120/Rector/Property/BareVarToStubIntersectionRector/Fixture/skip_stub_short_name.php.inc rename to rules-tests/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector/Fixture/skip_stub_short_name_var.php.inc index cc1a6fde..0447056a 100644 --- a/rules-tests/PHPUnit120/Rector/Property/BareVarToStubIntersectionRector/Fixture/skip_stub_short_name.php.inc +++ b/rules-tests/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector/Fixture/skip_stub_short_name_var.php.inc @@ -1,11 +1,11 @@ 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/Property/BareVarToStubIntersectionRector/Fixture/bare_var.php.inc b/rules-tests/PHPUnit120/Rector/Property/BareVarToStubIntersectionRector/Fixture/bare_var.php.inc deleted file mode 100644 index c1b96e07..00000000 --- a/rules-tests/PHPUnit120/Rector/Property/BareVarToStubIntersectionRector/Fixture/bare_var.php.inc +++ /dev/null @@ -1,31 +0,0 @@ - ------ - diff --git a/rules-tests/PHPUnit120/Rector/Property/BareVarToStubIntersectionRector/Fixture/skip_already_intersection.php.inc b/rules-tests/PHPUnit120/Rector/Property/BareVarToStubIntersectionRector/Fixture/skip_already_intersection.php.inc deleted file mode 100644 index b8205c43..00000000 --- a/rules-tests/PHPUnit120/Rector/Property/BareVarToStubIntersectionRector/Fixture/skip_already_intersection.php.inc +++ /dev/null @@ -1,13 +0,0 @@ -withRules(rules: [BareVarToStubIntersectionRector::class]); diff --git a/rules/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector.php b/rules/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector.php index c37b6ea0..33e3b412 100644 --- a/rules/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector.php +++ b/rules/CodeQuality/Rector/Class_/AddStubIntersectionVarToStubPropertyRector.php @@ -53,49 +53,35 @@ public function refactor(Node $node): ?Class_ } $setUpClassMethod = $node->getMethod(MethodName::SET_UP); - if (! $setUpClassMethod instanceof ClassMethod) { - return null; - } - - $propertyNamesToCreateStubMethodCalls = $this->mockObjectPropertyDetector->collectFromClassMethod( - $setUpClassMethod, - 'createStub' - ); - if ($propertyNamesToCreateStubMethodCalls === []) { - return null; - } + $propertyNamesToCreateStubCalls = $setUpClassMethod instanceof ClassMethod + ? $this->mockObjectPropertyDetector->collectFromClassMethod($setUpClassMethod, 'createStub') + : []; $hasChanged = false; - foreach ($propertyNamesToCreateStubMethodCalls as $propertyName => $createStubMethodCall) { - $property = $node->getProperty($propertyName); - if (! $property instanceof Property) { - continue; - } - - // only properties typed as a bare native Stub + foreach ($node->getProperties() as $property) { + // only properties typed as a native Stub if (! $this->mockObjectPropertyDetector->detect($property, PHPUnitClassName::STUB)) { continue; } - $stubbedClass = $this->resolveStubbedClass($createStubMethodCall); - if ($stubbedClass === null) { - continue; - } - - $intersectionTypeNode = new BracketsAwareIntersectionTypeNode([ - new IdentifierTypeNode('\\' . PHPUnitClassName::STUB), - new IdentifierTypeNode('\\' . $stubbedClass), - ]); - $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($property); + $varTagValueNode = $phpDocInfo->getVarTagValueNode(); // already has an intersection @var, skip - $varTagValueNode = $phpDocInfo->getVarTagValueNode(); if ($varTagValueNode instanceof VarTagValueNode && $varTagValueNode->type instanceof IntersectionTypeNode) { continue; } + $intersectionTypeNode = $this->resolveStubIntersection( + $property, + $propertyNamesToCreateStubCalls, + $varTagValueNode + ); + if (! $intersectionTypeNode instanceof BracketsAwareIntersectionTypeNode) { + continue; + } + $this->phpDocTypeChanger->changeVarTypeNode($property, $phpDocInfo, $intersectionTypeNode); $hasChanged = true; @@ -143,12 +129,82 @@ protected function setUp(): void $this->someServiceStub = $this->createStub(SomeService::class); } } +CODE_SAMPLE + ), + new CodeSample( + <<<'CODE_SAMPLE' +use PHPUnit\Framework\TestCase; + +final class SomeTest extends TestCase +{ + /** + * @var SomeService + */ + private \PHPUnit\Framework\MockObject\Stub $someServiceStub; +} +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +use PHPUnit\Framework\TestCase; + +final class SomeTest extends TestCase +{ + /** + * @var \PHPUnit\Framework\MockObject\Stub&SomeService + */ + private \PHPUnit\Framework\MockObject\Stub $someServiceStub; +} CODE_SAMPLE ), ] ); } + /** + * @param array $propertyNamesToCreateStubCalls + */ + private function resolveStubIntersection( + Property $property, + array $propertyNamesToCreateStubCalls, + ?VarTagValueNode $varTagValueNode + ): ?BracketsAwareIntersectionTypeNode { + $propertyName = $property->props[0]->name->toString(); + + // 1. prefer the stubbed class from a setUp() createStub() call + $createStubCall = $propertyNamesToCreateStubCalls[$propertyName] ?? null; + if ($createStubCall !== null) { + $stubbedClass = $this->resolveStubbedClass($createStubCall); + if ($stubbedClass !== null) { + return new BracketsAwareIntersectionTypeNode([ + new IdentifierTypeNode('\\' . PHPUnitClassName::STUB), + new IdentifierTypeNode('\\' . $stubbedClass), + ]); + } + } + + // 2. fall back to a bare single-class @var docblock + if ($varTagValueNode instanceof VarTagValueNode && $varTagValueNode->type instanceof IdentifierTypeNode) { + // skip Stub/MockObject themselves, only real stubbed class types + if (in_array($this->resolveShortName($varTagValueNode->type->name), ['Stub', 'MockObject'], true)) { + return null; + } + + return new BracketsAwareIntersectionTypeNode([ + new IdentifierTypeNode('\\' . PHPUnitClassName::STUB), + $varTagValueNode->type, + ]); + } + + return null; + } + + private function resolveShortName(string $name): string + { + $lastBackslashPosition = strrpos($name, '\\'); + + return $lastBackslashPosition === false ? $name : substr($name, $lastBackslashPosition + 1); + } + private function resolveStubbedClass(MethodCall|StaticCall $createStubCall): ?string { $firstArg = $createStubCall->getArgs()[0] ?? null; diff --git a/rules/PHPUnit120/Rector/Property/BareVarToStubIntersectionRector.php b/rules/PHPUnit120/Rector/Property/BareVarToStubIntersectionRector.php deleted file mode 100644 index 081df04f..00000000 --- a/rules/PHPUnit120/Rector/Property/BareVarToStubIntersectionRector.php +++ /dev/null @@ -1,145 +0,0 @@ -testsNodeAnalyzer->isInTestClass($node)) { - return null; - } - - // only properties already converted to a Stub native type - if (! $this->isStubNativeType($node->type)) { - return null; - } - - $phpDocInfo = $this->phpDocInfoFactory->createFromNode($node); - if (! $phpDocInfo instanceof PhpDocInfo) { - return null; - } - - $varTagValueNode = $phpDocInfo->getVarTagValueNode(); - if (! $varTagValueNode instanceof VarTagValueNode) { - return null; - } - - if (! $this->addStubIntersection($varTagValueNode)) { - return null; - } - - $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node); - - return $node; - } - - public function getRuleDefinition(): RuleDefinition - { - return new RuleDefinition( - 'Add a &Stub intersection to a bare single-class @var docblock of a property changed to a Stub native type', - [ - new CodeSample( - <<<'CODE_SAMPLE' -/** - * @var FormBuilderInterface - */ -private \PHPUnit\Framework\MockObject\Stub $formBuilder; -CODE_SAMPLE - , - <<<'CODE_SAMPLE' -/** - * @var FormBuilderInterface&\PHPUnit\Framework\MockObject\Stub - */ -private \PHPUnit\Framework\MockObject\Stub $formBuilder; -CODE_SAMPLE - ), - ] - ); - } - - private function isStubNativeType(?Node $typeNode): bool - { - if (! $typeNode instanceof Node) { - return false; - } - - if ($typeNode instanceof IntersectionType) { - return array_any($typeNode->types, fn (Identifier|Name $innerType): bool => $this->isStubName($innerType)); - } - - return $this->isStubName($typeNode); - } - - private function isStubName(?Node $node): bool - { - return $node instanceof Node && $this->getName($node) === PHPUnitClassName::STUB; - } - - private function addStubIntersection(VarTagValueNode $varTagValueNode): bool - { - $typeNode = $varTagValueNode->type; - - // only a single bare class type, not already a union/intersection - if (! $typeNode instanceof IdentifierTypeNode) { - return false; - } - - // skip Stub/MockObject themselves, only mocked class types - if (in_array($this->resolveShortName($typeNode->name), ['Stub', 'MockObject'], true)) { - return false; - } - - $varTagValueNode->type = new BracketsAwareIntersectionTypeNode([ - $typeNode, - new IdentifierTypeNode('\\' . PHPUnitClassName::STUB), - ]); - - return true; - } - - private function resolveShortName(string $name): string - { - $lastBackslashPosition = strrpos($name, '\\'); - - return $lastBackslashPosition === false ? $name : substr($name, $lastBackslashPosition + 1); - } -}