From 0c6b7c6ec533a3ac6fec64f5d56e2098e6d9d19c Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Mon, 29 Jun 2026 15:25:56 +0200 Subject: [PATCH 1/2] [PHPUnit120] Add MockObjectVarToStubRector to update @var docblock when property changed to Stub --- config/sets/phpunit-code-quality.php | 2 + config/sets/phpunit-mock-to-stub.php | 2 + .../Fixture/intersection_type.php.inc | 33 ++++ .../Fixture/skip_no_mock_object.php.inc | 13 ++ .../Fixture/skip_non_stub_native_type.php.inc | 14 ++ .../Fixture/union_type.php.inc | 33 ++++ .../MockObjectVarToStubRectorTest.php | 28 ++++ .../config/configured_rule.php | 9 ++ .../Property/MockObjectVarToStubRector.php | 150 ++++++++++++++++++ 9 files changed, 284 insertions(+) create mode 100644 rules-tests/PHPUnit120/Rector/Property/MockObjectVarToStubRector/Fixture/intersection_type.php.inc create mode 100644 rules-tests/PHPUnit120/Rector/Property/MockObjectVarToStubRector/Fixture/skip_no_mock_object.php.inc create mode 100644 rules-tests/PHPUnit120/Rector/Property/MockObjectVarToStubRector/Fixture/skip_non_stub_native_type.php.inc create mode 100644 rules-tests/PHPUnit120/Rector/Property/MockObjectVarToStubRector/Fixture/union_type.php.inc create mode 100644 rules-tests/PHPUnit120/Rector/Property/MockObjectVarToStubRector/MockObjectVarToStubRectorTest.php create mode 100644 rules-tests/PHPUnit120/Rector/Property/MockObjectVarToStubRector/config/configured_rule.php create mode 100644 rules/PHPUnit120/Rector/Property/MockObjectVarToStubRector.php diff --git a/config/sets/phpunit-code-quality.php b/config/sets/phpunit-code-quality.php index 70e4b5aaf..a00cf308e 100644 --- a/config/sets/phpunit-code-quality.php +++ b/config/sets/phpunit-code-quality.php @@ -57,6 +57,7 @@ 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\MockObjectVarToStubRector; use Rector\PHPUnit\PHPUnit60\Rector\MethodCall\GetMockBuilderGetMockToCreateMockRector; use Rector\PHPUnit\PHPUnit90\Rector\MethodCall\ReplaceAtMethodWithDesiredMatcherRector; use Rector\Privatization\Rector\Class_\FinalizeTestCaseClassRector; @@ -136,6 +137,7 @@ CreateStubOverCreateMockArgRector::class, ExpressionCreateMockToCreateStubRector::class, PropertyCreateMockToCreateStubRector::class, + MockObjectVarToStubRector::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 e1207e946..53fe62409 100644 --- a/config/sets/phpunit-mock-to-stub.php +++ b/config/sets/phpunit-mock-to-stub.php @@ -7,6 +7,7 @@ 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\MockObjectVarToStubRector; return static function (RectorConfig $rectorConfig): void { $rectorConfig->rules([ @@ -15,5 +16,6 @@ CreateStubInCoalesceArgRector::class, ExpressionCreateMockToCreateStubRector::class, PropertyCreateMockToCreateStubRector::class, + MockObjectVarToStubRector::class, ]); }; diff --git a/rules-tests/PHPUnit120/Rector/Property/MockObjectVarToStubRector/Fixture/intersection_type.php.inc b/rules-tests/PHPUnit120/Rector/Property/MockObjectVarToStubRector/Fixture/intersection_type.php.inc new file mode 100644 index 000000000..975a0e619 --- /dev/null +++ b/rules-tests/PHPUnit120/Rector/Property/MockObjectVarToStubRector/Fixture/intersection_type.php.inc @@ -0,0 +1,33 @@ + +----- + diff --git a/rules-tests/PHPUnit120/Rector/Property/MockObjectVarToStubRector/Fixture/skip_no_mock_object.php.inc b/rules-tests/PHPUnit120/Rector/Property/MockObjectVarToStubRector/Fixture/skip_no_mock_object.php.inc new file mode 100644 index 000000000..9cc98d8a9 --- /dev/null +++ b/rules-tests/PHPUnit120/Rector/Property/MockObjectVarToStubRector/Fixture/skip_no_mock_object.php.inc @@ -0,0 +1,13 @@ + +----- + diff --git a/rules-tests/PHPUnit120/Rector/Property/MockObjectVarToStubRector/MockObjectVarToStubRectorTest.php b/rules-tests/PHPUnit120/Rector/Property/MockObjectVarToStubRector/MockObjectVarToStubRectorTest.php new file mode 100644 index 000000000..91bc48b0f --- /dev/null +++ b/rules-tests/PHPUnit120/Rector/Property/MockObjectVarToStubRector/MockObjectVarToStubRectorTest.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/Property/MockObjectVarToStubRector/config/configured_rule.php b/rules-tests/PHPUnit120/Rector/Property/MockObjectVarToStubRector/config/configured_rule.php new file mode 100644 index 000000000..e56667995 --- /dev/null +++ b/rules-tests/PHPUnit120/Rector/Property/MockObjectVarToStubRector/config/configured_rule.php @@ -0,0 +1,9 @@ +withRules(rules: [MockObjectVarToStubRector::class]); diff --git a/rules/PHPUnit120/Rector/Property/MockObjectVarToStubRector.php b/rules/PHPUnit120/Rector/Property/MockObjectVarToStubRector.php new file mode 100644 index 000000000..09b2552ea --- /dev/null +++ b/rules/PHPUnit120/Rector/Property/MockObjectVarToStubRector.php @@ -0,0 +1,150 @@ +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->replaceMockObjectWithStub($varTagValueNode)) { + return null; + } + + $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node); + + return $node; + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition( + 'Update the @var docblock of a property changed to a Stub native type, from MockObject to Stub', + [ + new CodeSample( + <<<'CODE_SAMPLE' +/** + * @var FieldModel|MockObject + */ +private \PHPUnit\Framework\MockObject\Stub $leadFieldModel; +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +/** + * @var FieldModel|Stub + */ +private \PHPUnit\Framework\MockObject\Stub $leadFieldModel; +CODE_SAMPLE + ), + ] + ); + } + + private function isStubNativeType(?Node $typeNode): bool + { + 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 replaceMockObjectWithStub(VarTagValueNode $varTagValueNode): bool + { + $typeNode = $varTagValueNode->type; + + // fresh nodes (without original token positions) are re-printed, mutating in place is not + if ($typeNode instanceof UnionTypeNode || $typeNode instanceof IntersectionTypeNode) { + $hasChanged = false; + foreach ($typeNode->types as $key => $innerType) { + if ($innerType instanceof IdentifierTypeNode && $this->isMockObjectIdentifier($innerType)) { + $typeNode->types[$key] = new IdentifierTypeNode($this->resolveStubName($innerType->name)); + $hasChanged = true; + } + } + + return $hasChanged; + } + + if ($typeNode instanceof IdentifierTypeNode && $this->isMockObjectIdentifier($typeNode)) { + $varTagValueNode->type = new IdentifierTypeNode($this->resolveStubName($typeNode->name)); + return true; + } + + return false; + } + + private function isMockObjectIdentifier(IdentifierTypeNode $identifierTypeNode): bool + { + $lastBackslashPosition = strrpos($identifierTypeNode->name, '\\'); + $shortName = $lastBackslashPosition === false + ? $identifierTypeNode->name + : substr($identifierTypeNode->name, $lastBackslashPosition + 1); + + return $shortName === 'MockObject'; + } + + private function resolveStubName(string $name): string + { + $lastBackslashPosition = strrpos($name, '\\'); + + return $lastBackslashPosition === false + ? 'Stub' + : substr($name, 0, $lastBackslashPosition + 1) . 'Stub'; + } +} From 69ceffc64ebb674f8812f7c87e367a6592ff2184 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Mon, 29 Jun 2026 15:28:36 +0200 Subject: [PATCH 2/2] Restrict MockObjectVarToStubRector to PHPUnit TestCase scope, add skip fixture --- .../Fixture/skip_non_test_class.php.inc | 11 +++++++++++ .../Rector/Property/MockObjectVarToStubRector.php | 15 +++++++++++---- 2 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 rules-tests/PHPUnit120/Rector/Property/MockObjectVarToStubRector/Fixture/skip_non_test_class.php.inc diff --git a/rules-tests/PHPUnit120/Rector/Property/MockObjectVarToStubRector/Fixture/skip_non_test_class.php.inc b/rules-tests/PHPUnit120/Rector/Property/MockObjectVarToStubRector/Fixture/skip_non_test_class.php.inc new file mode 100644 index 000000000..daae19d05 --- /dev/null +++ b/rules-tests/PHPUnit120/Rector/Property/MockObjectVarToStubRector/Fixture/skip_non_test_class.php.inc @@ -0,0 +1,11 @@ +testsNodeAnalyzer->isInTestClass($node)) { + return null; + } + // only properties already converted to a Stub native type if (! $this->isStubNativeType($node->type)) { return null; @@ -53,7 +60,7 @@ public function refactor(Node $node): ?Property } $varTagValueNode = $phpDocInfo->getVarTagValueNode(); - if (!$varTagValueNode instanceof VarTagValueNode) { + if (! $varTagValueNode instanceof VarTagValueNode) { return null; } @@ -93,7 +100,7 @@ public function getRuleDefinition(): RuleDefinition private function isStubNativeType(?Node $typeNode): bool { if ($typeNode instanceof IntersectionType) { - return array_any($typeNode->types, fn(Identifier|Name $innerType): bool => $this->isStubName($innerType)); + return array_any($typeNode->types, fn (Identifier|Name $innerType): bool => $this->isStubName($innerType)); } return $this->isStubName($typeNode);