From 48c3f4f97e92724a77aaba4197586e010a12ca93 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Mon, 22 Jul 2024 00:13:17 +0200 Subject: [PATCH] [TypeDeclaration] Add ReturnTypeFromMockObjectRector --- .../docs/rector_rules_overview.md | 24 ++- .../Fixture/skip_filled_type.php.inc | 16 ++ .../Fixture/some_test_with_mock.php.inc | 35 ++++ .../ReturnTypeFromMockObjectRectorTest.php | 28 ++++ .../config/configured_rule.php | 9 ++ .../ReturnTypeFromMockObjectRector.php | 153 ++++++++++++++++++ src/Config/Level/TypeDeclarationLevel.php | 2 + 7 files changed, 265 insertions(+), 2 deletions(-) create mode 100644 rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromMockObjectRector/Fixture/skip_filled_type.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromMockObjectRector/Fixture/some_test_with_mock.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromMockObjectRector/ReturnTypeFromMockObjectRectorTest.php create mode 100644 rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromMockObjectRector/config/configured_rule.php create mode 100644 rules/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromMockObjectRector.php diff --git a/build/target-repository/docs/rector_rules_overview.md b/build/target-repository/docs/rector_rules_overview.md index 95371cd1433..cfaf2fa5502 100644 --- a/build/target-repository/docs/rector_rules_overview.md +++ b/build/target-repository/docs/rector_rules_overview.md @@ -1,4 +1,4 @@ -# 380 Rules Overview +# 381 Rules Overview
@@ -60,7 +60,7 @@ - [Transform](#transform) (25) -- [TypeDeclaration](#typedeclaration) (55) +- [TypeDeclaration](#typedeclaration) (56) - [Visibility](#visibility) (3) @@ -7110,6 +7110,26 @@ Add basic ? nullable type to class methods and functions, as of PHP 7.1
+### ReturnTypeFromMockObjectRector + +Add known property and return MockObject types + +- class: [`Rector\TypeDeclaration\Rector\ClassMethod\ReturnTypeFromMockObjectRector`](../rules/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromMockObjectRector.php) + +```diff + class SomeTest extends TestCase + { +- public function test() ++ public function test(): \PHPUnit\Framework\MockObject\MockObject + { + $someMock = $this->createMock(SomeClass::class); + return $someMock; + } + } +``` + +
+ ### ReturnTypeFromReturnCastRector Add return type to function like with return cast diff --git a/rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromMockObjectRector/Fixture/skip_filled_type.php.inc b/rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromMockObjectRector/Fixture/skip_filled_type.php.inc new file mode 100644 index 00000000000..4618e9b517a --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromMockObjectRector/Fixture/skip_filled_type.php.inc @@ -0,0 +1,16 @@ +createMock('SomeType'); + } +} diff --git a/rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromMockObjectRector/Fixture/some_test_with_mock.php.inc b/rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromMockObjectRector/Fixture/some_test_with_mock.php.inc new file mode 100644 index 00000000000..411c4f0cbee --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromMockObjectRector/Fixture/some_test_with_mock.php.inc @@ -0,0 +1,35 @@ +createMock('SomeType'); + } +} + +?> +----- +createMock('SomeType'); + } +} + +?> diff --git a/rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromMockObjectRector/ReturnTypeFromMockObjectRectorTest.php b/rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromMockObjectRector/ReturnTypeFromMockObjectRectorTest.php new file mode 100644 index 00000000000..ca11affd54d --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromMockObjectRector/ReturnTypeFromMockObjectRectorTest.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/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromMockObjectRector/config/configured_rule.php b/rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromMockObjectRector/config/configured_rule.php new file mode 100644 index 00000000000..16a43e57ea5 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromMockObjectRector/config/configured_rule.php @@ -0,0 +1,9 @@ +withRules([ReturnTypeFromMockObjectRector::class]); diff --git a/rules/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromMockObjectRector.php b/rules/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromMockObjectRector.php new file mode 100644 index 00000000000..d82940a6929 --- /dev/null +++ b/rules/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromMockObjectRector.php @@ -0,0 +1,153 @@ +createMock(SomeClass::class); + return $someMock; + } +} +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +class SomeTest extends TestCase +{ + public function test(): \PHPUnit\Framework\MockObject\MockObject + { + $someMock = $this->createMock(SomeClass::class); + return $someMock; + } +} +CODE_SAMPLE + ), + + ]); + } + + public function getNodeTypes(): array + { + return [ClassMethod::class]; + } + + /** + * @param ClassMethod $node + */ + public function refactor(Node $node): ?Node + { + // type is already known + if ($node->returnType instanceof Node) { + return null; + } + + if (! $this->isInsideTestCaseClass($node)) { + return null; + } + + // we need exactly 1 return + $returns = $this->betterNodeFinder->findReturnsScoped($node); + if (count($returns) !== 1) { + return null; + } + + $soleReturn = $returns[0]; + if (! $soleReturn->expr instanceof Expr) { + return null; + } + + $returnType = $this->getType($soleReturn->expr); + if (! $this->isMockObjectType($returnType)) { + return null; + } + + $node->returnType = new FullyQualified(self::MOCK_OBJECT_CLASS); + + return $node; + } + + public function provideMinPhpVersion(): int + { + return PhpVersionFeature::SCALAR_TYPES; + } + + private function isIntersectionWithMockObjectType(Type $type): bool + { + if (! $type instanceof IntersectionType) { + return false; + } + + if (count($type->getTypes()) !== 2) { + return false; + } + + return in_array(MockObject::class, $type->getObjectClassNames()); + } + + private function isMockObjectType(Type $returnType): bool + { + if ($returnType instanceof ObjectType && $returnType->isInstanceOf(self::MOCK_OBJECT_CLASS)->yes()) { + return true; + } + + return $this->isIntersectionWithMockObjectType($returnType); + } + + private function isInsideTestCaseClass(ClassMethod $classMethod): bool + { + $scope = $classMethod->getAttribute(AttributeKey::SCOPE); + if (! $scope instanceof Scope) { + return false; + } + + $classReflection = $scope->getClassReflection(); + if (! $classReflection instanceof ClassReflection) { + return false; + } + + // is phpunit test case? + return $classReflection->isSubclassOf(TestCase::class); + } +} diff --git a/src/Config/Level/TypeDeclarationLevel.php b/src/Config/Level/TypeDeclarationLevel.php index 93e413f9edd..8005b97700c 100644 --- a/src/Config/Level/TypeDeclarationLevel.php +++ b/src/Config/Level/TypeDeclarationLevel.php @@ -25,6 +25,7 @@ use Rector\TypeDeclaration\Rector\ClassMethod\ParamTypeByParentCallTypeRector; use Rector\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector; use Rector\TypeDeclaration\Rector\ClassMethod\ReturnNullableTypeRector; +use Rector\TypeDeclaration\Rector\ClassMethod\ReturnTypeFromMockObjectRector; use Rector\TypeDeclaration\Rector\ClassMethod\ReturnTypeFromReturnCastRector; use Rector\TypeDeclaration\Rector\ClassMethod\ReturnTypeFromReturnDirectArrayRector; use Rector\TypeDeclaration\Rector\ClassMethod\ReturnTypeFromReturnNewRector; @@ -66,6 +67,7 @@ final class TypeDeclarationLevel AddFunctionVoidReturnTypeWhereNoReturnRector::class, AddTestsVoidReturnTypeWhereNoReturnRector::class, + ReturnTypeFromMockObjectRector::class, AddArrowFunctionReturnTypeRector::class, ReturnTypeFromStrictConstantReturnRector::class, ReturnTypeFromStrictNewArrayRector::class,