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..a9011365f --- /dev/null +++ b/rules/PHPUnit120/Rector/Property/MockObjectVarToStubRector.php @@ -0,0 +1,157 @@ +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->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'; + } +}