From fd1bca2e203a923d81fbb29e01aa5fc067db541a Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sun, 5 Jul 2026 10:49:38 +0200 Subject: [PATCH 1/2] [TypeDeclaration] Add ReturnTypeFromGetRepositoryDocblockRector Move @return docblock type into native return type on methods that return an EntityManagerInterface getRepository() fetch. --- .../Fixture/get_repository.php.inc | 44 +++++ .../Fixture/skip_already_typed.php.inc | 18 ++ .../Fixture/skip_no_docblock.php.inc | 17 ++ .../skip_non_existing_repository.php.inc | 20 ++ .../Fixture/skip_not_entity_manager.php.inc | 20 ++ ...ypeFromGetRepositoryDocblockRectorTest.php | 28 +++ .../Source/EventRepository.php | 9 + .../config/configured_rule.php | 9 + ...urnTypeFromGetRepositoryDocblockRector.php | 181 ++++++++++++++++++ 9 files changed, 346 insertions(+) create mode 100644 rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromGetRepositoryDocblockRector/Fixture/get_repository.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromGetRepositoryDocblockRector/Fixture/skip_already_typed.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromGetRepositoryDocblockRector/Fixture/skip_no_docblock.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromGetRepositoryDocblockRector/Fixture/skip_non_existing_repository.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromGetRepositoryDocblockRector/Fixture/skip_not_entity_manager.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromGetRepositoryDocblockRector/ReturnTypeFromGetRepositoryDocblockRectorTest.php create mode 100644 rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromGetRepositoryDocblockRector/Source/EventRepository.php create mode 100644 rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromGetRepositoryDocblockRector/config/configured_rule.php create mode 100644 rules/TypeDeclaration/Rector/ClassMethod/ReturnTypeFromGetRepositoryDocblockRector.php 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); + } +} From 6d3a5240280edd2fa49080329198e5e365f4c824 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sun, 5 Jul 2026 10:53:46 +0200 Subject: [PATCH 2/2] [TypeDeclaration] Register ReturnTypeFromGetRepositoryDocblockRector in type declaration set as last rule --- src/Config/Level/TypeDeclarationLevel.php | 4 ++++ 1 file changed, 4 insertions(+) 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, ]; }