From 54ed79bb6a836ed67458be755822d688d3ea82af Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 30 Jun 2026 14:27:25 +0200 Subject: [PATCH] [TypeDeclaration] Add TypedPropertyFromGetRepositorySetUpRector --- .../Fixture/em_get_repository.php.inc | 44 ++++ .../Fixture/skip_no_docblock.php.inc | 15 ++ .../Fixture/skip_no_test_case.php.inc | 18 ++ .../Fixture/skip_non_existing_class.php.inc | 18 ++ .../Fixture/skip_not_get_repository.php.inc | 19 ++ .../Fixture/skip_public_property.php.inc | 19 ++ .../Source/SomeEntityRepository.php | 9 + ...opertyFromGetRepositorySetUpRectorTest.php | 28 +++ .../config/configured_rule.php | 11 + ...edPropertyFromGetRepositorySetUpRector.php | 221 ++++++++++++++++++ src/Config/Level/TypeDeclarationLevel.php | 2 + 11 files changed, 404 insertions(+) create mode 100644 rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromGetRepositorySetUpRector/Fixture/em_get_repository.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromGetRepositorySetUpRector/Fixture/skip_no_docblock.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromGetRepositorySetUpRector/Fixture/skip_no_test_case.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromGetRepositorySetUpRector/Fixture/skip_non_existing_class.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromGetRepositorySetUpRector/Fixture/skip_not_get_repository.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromGetRepositorySetUpRector/Fixture/skip_public_property.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromGetRepositorySetUpRector/Source/SomeEntityRepository.php create mode 100644 rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromGetRepositorySetUpRector/TypedPropertyFromGetRepositorySetUpRectorTest.php create mode 100644 rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromGetRepositorySetUpRector/config/configured_rule.php create mode 100644 rules/TypeDeclaration/Rector/Class_/TypedPropertyFromGetRepositorySetUpRector.php diff --git a/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromGetRepositorySetUpRector/Fixture/em_get_repository.php.inc b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromGetRepositorySetUpRector/Fixture/em_get_repository.php.inc new file mode 100644 index 00000000000..8ae97a0c9db --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromGetRepositorySetUpRector/Fixture/em_get_repository.php.inc @@ -0,0 +1,44 @@ +someEntityRepository = $this->em->getRepository(SomeEntity::class); + } +} + +?> +----- +someEntityRepository = $this->em->getRepository(SomeEntity::class); + } +} + +?> diff --git a/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromGetRepositorySetUpRector/Fixture/skip_no_docblock.php.inc b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromGetRepositorySetUpRector/Fixture/skip_no_docblock.php.inc new file mode 100644 index 00000000000..ac6fc293018 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromGetRepositorySetUpRector/Fixture/skip_no_docblock.php.inc @@ -0,0 +1,15 @@ +someEntityRepository = $this->em->getRepository(SomeEntity::class); + } +} diff --git a/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromGetRepositorySetUpRector/Fixture/skip_no_test_case.php.inc b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromGetRepositorySetUpRector/Fixture/skip_no_test_case.php.inc new file mode 100644 index 00000000000..08f59897a15 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromGetRepositorySetUpRector/Fixture/skip_no_test_case.php.inc @@ -0,0 +1,18 @@ +someEntityRepository = $this->em->getRepository(SomeEntity::class); + } +} diff --git a/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromGetRepositorySetUpRector/Fixture/skip_non_existing_class.php.inc b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromGetRepositorySetUpRector/Fixture/skip_non_existing_class.php.inc new file mode 100644 index 00000000000..a983a38c944 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromGetRepositorySetUpRector/Fixture/skip_non_existing_class.php.inc @@ -0,0 +1,18 @@ +someEntityRepository = $this->em->getRepository(SomeEntity::class); + } +} diff --git a/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromGetRepositorySetUpRector/Fixture/skip_not_get_repository.php.inc b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromGetRepositorySetUpRector/Fixture/skip_not_get_repository.php.inc new file mode 100644 index 00000000000..53bbed06f43 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromGetRepositorySetUpRector/Fixture/skip_not_get_repository.php.inc @@ -0,0 +1,19 @@ +someEntityRepository = new SomeEntityRepository(); + } +} diff --git a/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromGetRepositorySetUpRector/Fixture/skip_public_property.php.inc b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromGetRepositorySetUpRector/Fixture/skip_public_property.php.inc new file mode 100644 index 00000000000..77d98f94e3b --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromGetRepositorySetUpRector/Fixture/skip_public_property.php.inc @@ -0,0 +1,19 @@ +someEntityRepository = $this->em->getRepository(SomeEntity::class); + } +} diff --git a/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromGetRepositorySetUpRector/Source/SomeEntityRepository.php b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromGetRepositorySetUpRector/Source/SomeEntityRepository.php new file mode 100644 index 00000000000..7d222d3d346 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromGetRepositorySetUpRector/Source/SomeEntityRepository.php @@ -0,0 +1,9 @@ +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/Class_/TypedPropertyFromGetRepositorySetUpRector/config/configured_rule.php b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromGetRepositorySetUpRector/config/configured_rule.php new file mode 100644 index 00000000000..2716c15e189 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromGetRepositorySetUpRector/config/configured_rule.php @@ -0,0 +1,11 @@ +withRules([TypedPropertyFromGetRepositorySetUpRector::class]) + ->withPhpVersion(PhpVersionFeature::TYPED_PROPERTIES); diff --git a/rules/TypeDeclaration/Rector/Class_/TypedPropertyFromGetRepositorySetUpRector.php b/rules/TypeDeclaration/Rector/Class_/TypedPropertyFromGetRepositorySetUpRector.php new file mode 100644 index 00000000000..5932c89d930 --- /dev/null +++ b/rules/TypeDeclaration/Rector/Class_/TypedPropertyFromGetRepositorySetUpRector.php @@ -0,0 +1,221 @@ +someEntityRepository = $this->em->getRepository(SomeEntity::class); + } +} +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +use PHPUnit\Framework\TestCase; + +final class SomeTest extends TestCase +{ + private SomeEntityRepository $someEntityRepository; + + protected function setUp(): void + { + $this->someEntityRepository = $this->em->getRepository(SomeEntity::class); + } +} +CODE_SAMPLE + ), + ] + ); + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [Class_::class]; + } + + /** + * @param Class_ $node + */ + public function refactor(Node $node): ?Node + { + if (! $this->testsNodeAnalyzer->isInTestClass($node)) { + return null; + } + + $setUpClassMethod = $node->getMethod(MethodName::SET_UP); + if (! $setUpClassMethod instanceof ClassMethod) { + return null; + } + + $hasChanged = false; + + foreach ($node->getProperties() as $property) { + // type is already set + if ($property->type instanceof Node) { + continue; + } + + if (! $property->isPrivate()) { + continue; + } + + if ($property->isStatic()) { + continue; + } + + // exactly one property + if (count($property->props) !== 1) { + continue; + } + + $propertyName = $this->getName($property->props[0]); + if (! $this->isAssignedViaGetRepositoryInSetUp($setUpClassMethod, $propertyName)) { + continue; + } + + $propertyPhpDocInfo = $this->phpDocInfoFactory->createFromNode($property); + if (! $propertyPhpDocInfo instanceof PhpDocInfo) { + continue; + } + + $varType = $propertyPhpDocInfo->getVarType(); + if (! $varType instanceof ObjectType) { + continue; + } + + $propertyTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($varType, TypeKind::PROPERTY); + if (! $propertyTypeNode instanceof Name) { + continue; + } + + // must be an existing object type + if (! $this->reflectionProvider->hasClass($propertyTypeNode->toString())) { + continue; + } + + $property->type = $propertyTypeNode; + $this->removeVarTag($propertyPhpDocInfo, $property); + + $hasChanged = true; + } + + if ($hasChanged) { + return $node; + } + + return null; + } + + public function provideMinPhpVersion(): int + { + return PhpVersionFeature::TYPED_PROPERTIES; + } + + private function isAssignedViaGetRepositoryInSetUp(ClassMethod $setUpClassMethod, string $propertyName): bool + { + /** @var Assign[] $assigns */ + $assigns = $this->betterNodeFinder->findInstanceOf($setUpClassMethod, Assign::class); + + foreach ($assigns as $assign) { + if (! $assign->var instanceof PropertyFetch) { + continue; + } + + $propertyFetch = $assign->var; + if (! $this->isName($propertyFetch->var, 'this')) { + continue; + } + + if (! $this->isName($propertyFetch, $propertyName)) { + continue; + } + + if ($this->isGetRepositoryCall($assign->expr)) { + return true; + } + } + + return false; + } + + private function isGetRepositoryCall(Expr $expr): bool + { + if (! $expr instanceof MethodCall) { + return false; + } + + return $this->isName($expr->name, 'getRepository'); + } + + private function removeVarTag(PhpDocInfo $propertyPhpDocInfo, Property $property): void + { + $varTagValueNode = $propertyPhpDocInfo->getVarTagValueNode(); + if (! $varTagValueNode instanceof VarTagValueNode) { + return; + } + + $propertyPhpDocInfo->removeByType(VarTagValueNode::class); + $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($property); + } +} diff --git a/src/Config/Level/TypeDeclarationLevel.php b/src/Config/Level/TypeDeclarationLevel.php index ec357dc45a2..10a7a4dd509 100644 --- a/src/Config/Level/TypeDeclarationLevel.php +++ b/src/Config/Level/TypeDeclarationLevel.php @@ -18,6 +18,7 @@ use Rector\TypeDeclaration\Rector\Class_\TypedPropertyFromContainerGetSetUpRector; use Rector\TypeDeclaration\Rector\Class_\TypedPropertyFromCreateMockAssignRector; use Rector\TypeDeclaration\Rector\Class_\TypedPropertyFromDocblockSetUpDefinedRector; +use Rector\TypeDeclaration\Rector\Class_\TypedPropertyFromGetRepositorySetUpRector; use Rector\TypeDeclaration\Rector\Class_\TypedStaticPropertyInBehatContextRector; use Rector\TypeDeclaration\Rector\ClassMethod\AddMethodCallBasedStrictParamTypeRector; use Rector\TypeDeclaration\Rector\ClassMethod\AddParamFromDimFetchKeyUseRector; @@ -155,6 +156,7 @@ final class TypeDeclarationLevel PropertyTypeFromStrictSetterGetterRector::class, ParamTypeByMethodCallTypeRector::class, TypedPropertyFromContainerGetSetUpRector::class, + TypedPropertyFromGetRepositorySetUpRector::class, TypedPropertyFromAssignsRector::class, AddReturnTypeDeclarationBasedOnParentClassMethodRector::class, ReturnTypeFromStrictFluentReturnRector::class,