diff --git a/config/sets/phpunit-mock-to-stub.php b/config/sets/phpunit-mock-to-stub.php index 12efa6790..14a06212f 100644 --- a/config/sets/phpunit-mock-to-stub.php +++ b/config/sets/phpunit-mock-to-stub.php @@ -3,6 +3,7 @@ declare(strict_types=1); use Rector\Config\RectorConfig; +use Rector\PHPUnit\CodeQuality\Rector\Class_\AddIntersectionVarToMockObjectPropertyRector; use Rector\PHPUnit\PHPUnit120\Rector\CallLike\CreateStubInCoalesceArgRector; use Rector\PHPUnit\PHPUnit120\Rector\CallLike\CreateStubOverCreateMockArgRector; use Rector\PHPUnit\PHPUnit120\Rector\Class_\PropertyCreateMockToCreateStubRector; @@ -19,5 +20,6 @@ PropertyCreateMockToCreateStubRector::class, MockObjectVarToStubRector::class, BareVarToStubIntersectionRector::class, + AddIntersectionVarToMockObjectPropertyRector::class, ]); }; diff --git a/rules-tests/CodeQuality/Rector/Class_/AddIntersectionVarToMockObjectPropertyRector/AddIntersectionVarToMockObjectPropertyRectorTest.php b/rules-tests/CodeQuality/Rector/Class_/AddIntersectionVarToMockObjectPropertyRector/AddIntersectionVarToMockObjectPropertyRectorTest.php new file mode 100644 index 000000000..9f9dc115e --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Class_/AddIntersectionVarToMockObjectPropertyRector/AddIntersectionVarToMockObjectPropertyRectorTest.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_/AddIntersectionVarToMockObjectPropertyRector/Fixture/fixture.php.inc b/rules-tests/CodeQuality/Rector/Class_/AddIntersectionVarToMockObjectPropertyRector/Fixture/fixture.php.inc new file mode 100644 index 000000000..97b2668fa --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Class_/AddIntersectionVarToMockObjectPropertyRector/Fixture/fixture.php.inc @@ -0,0 +1,38 @@ +someServiceMock = $this->createMock(\stdClass::class); + } +} + +?> +----- +someServiceMock = $this->createMock(\stdClass::class); + } +} + +?> diff --git a/rules-tests/CodeQuality/Rector/Class_/AddIntersectionVarToMockObjectPropertyRector/Fixture/overwrite_existing_var.php.inc b/rules-tests/CodeQuality/Rector/Class_/AddIntersectionVarToMockObjectPropertyRector/Fixture/overwrite_existing_var.php.inc new file mode 100644 index 000000000..1ac572db3 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Class_/AddIntersectionVarToMockObjectPropertyRector/Fixture/overwrite_existing_var.php.inc @@ -0,0 +1,41 @@ +someServiceMock = $this->createMock(\stdClass::class); + } +} + +?> +----- +someServiceMock = $this->createMock(\stdClass::class); + } +} + +?> diff --git a/rules-tests/CodeQuality/Rector/Class_/AddIntersectionVarToMockObjectPropertyRector/Fixture/skip_non_test_class.php.inc b/rules-tests/CodeQuality/Rector/Class_/AddIntersectionVarToMockObjectPropertyRector/Fixture/skip_non_test_class.php.inc new file mode 100644 index 000000000..17154dd69 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Class_/AddIntersectionVarToMockObjectPropertyRector/Fixture/skip_non_test_class.php.inc @@ -0,0 +1,13 @@ +someServiceMock = $this->createMock(\stdClass::class); + } +} diff --git a/rules-tests/CodeQuality/Rector/Class_/AddIntersectionVarToMockObjectPropertyRector/Fixture/skip_stub_property.php.inc b/rules-tests/CodeQuality/Rector/Class_/AddIntersectionVarToMockObjectPropertyRector/Fixture/skip_stub_property.php.inc new file mode 100644 index 000000000..fcd143237 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Class_/AddIntersectionVarToMockObjectPropertyRector/Fixture/skip_stub_property.php.inc @@ -0,0 +1,15 @@ +someServiceMock = $this->createMock(\stdClass::class); + } +} diff --git a/rules-tests/CodeQuality/Rector/Class_/AddIntersectionVarToMockObjectPropertyRector/config/configured_rule.php b/rules-tests/CodeQuality/Rector/Class_/AddIntersectionVarToMockObjectPropertyRector/config/configured_rule.php new file mode 100644 index 000000000..3ec23fc30 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Class_/AddIntersectionVarToMockObjectPropertyRector/config/configured_rule.php @@ -0,0 +1,9 @@ +withRules([AddIntersectionVarToMockObjectPropertyRector::class]); diff --git a/rules/CodeQuality/Rector/Class_/AddIntersectionVarToMockObjectPropertyRector.php b/rules/CodeQuality/Rector/Class_/AddIntersectionVarToMockObjectPropertyRector.php new file mode 100644 index 000000000..bc7864a0b --- /dev/null +++ b/rules/CodeQuality/Rector/Class_/AddIntersectionVarToMockObjectPropertyRector.php @@ -0,0 +1,159 @@ +testsNodeAnalyzer->isInTestClass($node)) { + return null; + } + + $setUpClassMethod = $node->getMethod(MethodName::SET_UP); + if (! $setUpClassMethod instanceof ClassMethod) { + return null; + } + + $propertyNamesToCreateMockMethodCalls = $this->mockObjectPropertyDetector->collectFromClassMethod( + $setUpClassMethod + ); + if ($propertyNamesToCreateMockMethodCalls === []) { + return null; + } + + $hasChanged = false; + + foreach ($propertyNamesToCreateMockMethodCalls as $propertyName => $createMockMethodCall) { + $property = $node->getProperty($propertyName); + if (! $property instanceof Property) { + continue; + } + + // only properties typed as a bare native MockObject + if (! $this->mockObjectPropertyDetector->detect($property)) { + continue; + } + + $mockedClass = $this->resolveMockedClass($createMockMethodCall); + if ($mockedClass === null) { + continue; + } + + $intersectionTypeNode = new BracketsAwareIntersectionTypeNode([ + new IdentifierTypeNode('\\' . PHPUnitClassName::MOCK_OBJECT), + new IdentifierTypeNode('\\' . $mockedClass), + ]); + + $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($property); + $this->phpDocTypeChanger->changeVarTypeNode($property, $phpDocInfo, $intersectionTypeNode); + + $hasChanged = true; + } + + if (! $hasChanged) { + return null; + } + + return $node; + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition( + 'Add a MockObject intersection @var docblock with the mocked class to a native MockObject property', + [ + new CodeSample( + <<<'CODE_SAMPLE' +use PHPUnit\Framework\TestCase; + +final class SomeTest extends TestCase +{ + private \PHPUnit\Framework\MockObject\MockObject $someServiceMock; + + protected function setUp(): void + { + $this->someServiceMock = $this->createMock(SomeService::class); + } +} +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +use PHPUnit\Framework\TestCase; + +final class SomeTest extends TestCase +{ + /** + * @var \PHPUnit\Framework\MockObject\MockObject&\SomeService + */ + private \PHPUnit\Framework\MockObject\MockObject $someServiceMock; + + protected function setUp(): void + { + $this->someServiceMock = $this->createMock(SomeService::class); + } +} +CODE_SAMPLE + ), + ] + ); + } + + private function resolveMockedClass(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; + } +}