diff --git a/rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromGetRepositoryDocblockRector/Fixture/get_repository.php.inc b/rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromGetRepositoryDocblockRector/Fixture/get_repository.php.inc new file mode 100644 index 00000000000..b235ba04c39 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromGetRepositoryDocblockRector/Fixture/get_repository.php.inc @@ -0,0 +1,44 @@ +em->getRepository(Event::class); + } +} + +?> +----- +em->getRepository(Event::class); + } +} + +?> diff --git a/rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromGetRepositoryDocblockRector/Fixture/skip_already_typed.php.inc b/rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromGetRepositoryDocblockRector/Fixture/skip_already_typed.php.inc new file mode 100644 index 00000000000..84931c1bb04 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromGetRepositoryDocblockRector/Fixture/skip_already_typed.php.inc @@ -0,0 +1,18 @@ +em->getRepository(Event::class); + } +} diff --git a/rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromGetRepositoryDocblockRector/Fixture/skip_no_docblock.php.inc b/rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromGetRepositoryDocblockRector/Fixture/skip_no_docblock.php.inc new file mode 100644 index 00000000000..5e0341f1bc3 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromGetRepositoryDocblockRector/Fixture/skip_no_docblock.php.inc @@ -0,0 +1,17 @@ +em->getRepository(Event::class); + } +} diff --git a/rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromGetRepositoryDocblockRector/Fixture/skip_non_existing_repository.php.inc b/rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromGetRepositoryDocblockRector/Fixture/skip_non_existing_repository.php.inc new file mode 100644 index 00000000000..3e7b9a5a152 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromGetRepositoryDocblockRector/Fixture/skip_non_existing_repository.php.inc @@ -0,0 +1,20 @@ +em->getRepository(Event::class); + } +} diff --git a/rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromGetRepositoryDocblockRector/Fixture/skip_not_entity_manager.php.inc b/rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromGetRepositoryDocblockRector/Fixture/skip_not_entity_manager.php.inc new file mode 100644 index 00000000000..ab891f5c037 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromGetRepositoryDocblockRector/Fixture/skip_not_entity_manager.php.inc @@ -0,0 +1,20 @@ +registry->getRepository(Event::class); + } +} diff --git a/rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromGetRepositoryDocblockRector/ReturnTypeFromGetRepositoryDocblockRectorTest.php b/rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromGetRepositoryDocblockRector/ReturnTypeFromGetRepositoryDocblockRectorTest.php new file mode 100644 index 00000000000..3b17212c053 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromGetRepositoryDocblockRector/ReturnTypeFromGetRepositoryDocblockRectorTest.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/ReturnTypeFromGetRepositoryDocblockRector/Source/EventRepository.php b/rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromGetRepositoryDocblockRector/Source/EventRepository.php new file mode 100644 index 00000000000..5e1b6b09ac8 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromGetRepositoryDocblockRector/Source/EventRepository.php @@ -0,0 +1,9 @@ +withRules([ReturnTypeFromGetRepositoryDocblockRector::class]); diff --git a/rules/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromGetRepositoryDocblockRector.php b/rules/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromGetRepositoryDocblockRector.php new file mode 100644 index 00000000000..4895b9b8b8d --- /dev/null +++ b/rules/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromGetRepositoryDocblockRector.php @@ -0,0 +1,181 @@ +em->getRepository(Event::class); + } +} +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +final class EventModel +{ + public function __construct(private \Doctrine\ORM\EntityManagerInterface $em) + { + } + + public function getRepository(): \App\Repository\EventRepository + { + return $this->em->getRepository(Event::class); + } +} +CODE_SAMPLE + ), + ] + ); + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [ClassMethod::class]; + } + + /** + * @param ClassMethod $node + */ + public function refactor(Node $node): ?Node + { + // return type is already set + if ($node->returnType instanceof Node) { + return null; + } + + if (! $this->isGetRepositoryReturnOnly($node)) { + return null; + } + + $scope = ScopeFetcher::fetch($node); + if ($this->classMethodReturnTypeOverrideGuard->shouldSkipClassMethod($node, $scope)) { + return null; + } + + $phpDocInfo = $this->phpDocInfoFactory->createFromNode($node); + if (! $phpDocInfo instanceof PhpDocInfo) { + return null; + } + + $returnType = $phpDocInfo->getReturnType(); + if (! $returnType instanceof ObjectType) { + return null; + } + + $returnTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($returnType, TypeKind::RETURN); + if (! $returnTypeNode instanceof Name) { + return null; + } + + // must be an existing repository class + if (! $this->reflectionProvider->hasClass($returnTypeNode->toString())) { + return null; + } + + $node->returnType = $returnTypeNode; + $this->removeReturnTag($phpDocInfo, $node); + + return $node; + } + + private function isGetRepositoryReturnOnly(ClassMethod $classMethod): bool + { + $returns = $this->betterNodeFinder->findReturnsScoped($classMethod); + if (count($returns) !== 1) { + return false; + } + + $methodCall = $returns[0]->expr; + if (! $methodCall instanceof MethodCall) { + return false; + } + + if (! $this->isName($methodCall->name, 'getRepository')) { + return false; + } + + return $this->isEntityManagerType($methodCall); + } + + private function isEntityManagerType(MethodCall $methodCall): bool + { + $callerType = $this->nodeTypeResolver->getType($methodCall->var); + if (! $callerType instanceof ObjectType) { + return false; + } + + if ($callerType->getClassName() === self::ENTITY_MANAGER_INTERFACE) { + return true; + } + + return $callerType->isInstanceOf(self::ENTITY_MANAGER_INTERFACE) + ->yes(); + } + + private function removeReturnTag(PhpDocInfo $phpDocInfo, ClassMethod $classMethod): void + { + $returnTagValueNode = $phpDocInfo->getReturnTagValue(); + if (! $returnTagValueNode instanceof ReturnTagValueNode) { + return; + } + + $phpDocInfo->removeByType(ReturnTagValueNode::class); + $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($classMethod); + } +} diff --git a/src/Config/Level/TypeDeclarationLevel.php b/src/Config/Level/TypeDeclarationLevel.php index a4772b1557e..e9af567a9bd 100644 --- a/src/Config/Level/TypeDeclarationLevel.php +++ b/src/Config/Level/TypeDeclarationLevel.php @@ -40,6 +40,7 @@ use Rector\TypeDeclaration\Rector\ClassMethod\ParamTypeByParentCallTypeRector; use Rector\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector; use Rector\TypeDeclaration\Rector\ClassMethod\ReturnNullableTypeRector; +use Rector\TypeDeclaration\Rector\ClassMethod\ReturnTypeFromGetRepositoryDocblockRector; use Rector\TypeDeclaration\Rector\ClassMethod\ReturnTypeFromMockObjectRector; use Rector\TypeDeclaration\Rector\ClassMethod\ReturnTypeFromReturnCastRector; use Rector\TypeDeclaration\Rector\ClassMethod\ReturnTypeFromReturnDirectArrayRector; @@ -188,5 +189,8 @@ final class TypeDeclarationLevel NarrowArrayAnyAllNullableParamTypeRector::class, AddArrayAnyAllClosureParamTypeRector::class, ClosureReturnTypeFromAssertInstanceOfRector::class, + + // docblock @return into native return type, intentionally last + ReturnTypeFromGetRepositoryDocblockRector::class, ]; }