diff --git a/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromContainerGetSetUpRector/Fixture/skip_no_docblock.php.inc b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromContainerGetSetUpRector/Fixture/skip_no_docblock.php.inc new file mode 100644 index 00000000000..d7ebdc0727e --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromContainerGetSetUpRector/Fixture/skip_no_docblock.php.inc @@ -0,0 +1,16 @@ +someService = static::getContainer()->get(SomeService::class); + } +} diff --git a/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromContainerGetSetUpRector/Fixture/skip_no_test_case.php.inc b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromContainerGetSetUpRector/Fixture/skip_no_test_case.php.inc new file mode 100644 index 00000000000..87d065e7794 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromContainerGetSetUpRector/Fixture/skip_no_test_case.php.inc @@ -0,0 +1,18 @@ +someService = static::getContainer()->get(SomeService::class); + } +} diff --git a/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromContainerGetSetUpRector/Fixture/skip_non_existing_class.php.inc b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromContainerGetSetUpRector/Fixture/skip_non_existing_class.php.inc new file mode 100644 index 00000000000..d87ed9d87e3 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromContainerGetSetUpRector/Fixture/skip_non_existing_class.php.inc @@ -0,0 +1,18 @@ +someService = static::getContainer()->get('some_service'); + } +} diff --git a/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromContainerGetSetUpRector/Fixture/skip_not_container_fetch.php.inc b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromContainerGetSetUpRector/Fixture/skip_not_container_fetch.php.inc new file mode 100644 index 00000000000..579f8be0ca9 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromContainerGetSetUpRector/Fixture/skip_not_container_fetch.php.inc @@ -0,0 +1,19 @@ +someService = new SomeService(); + } +} diff --git a/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromContainerGetSetUpRector/Fixture/skip_public_property.php.inc b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromContainerGetSetUpRector/Fixture/skip_public_property.php.inc new file mode 100644 index 00000000000..f557026ab4f --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromContainerGetSetUpRector/Fixture/skip_public_property.php.inc @@ -0,0 +1,19 @@ +someService = static::getContainer()->get(SomeService::class); + } +} diff --git a/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromContainerGetSetUpRector/Fixture/skip_scalar_type.php.inc b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromContainerGetSetUpRector/Fixture/skip_scalar_type.php.inc new file mode 100644 index 00000000000..d4488e5e9f4 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromContainerGetSetUpRector/Fixture/skip_scalar_type.php.inc @@ -0,0 +1,18 @@ +value = static::getContainer()->get('some_parameter'); + } +} diff --git a/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromContainerGetSetUpRector/Fixture/static_get_container_get.php.inc b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromContainerGetSetUpRector/Fixture/static_get_container_get.php.inc new file mode 100644 index 00000000000..b91f4ce1df6 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromContainerGetSetUpRector/Fixture/static_get_container_get.php.inc @@ -0,0 +1,44 @@ +someService = static::getContainer()->get(SomeService::class); + } +} + +?> +----- +someService = static::getContainer()->get(SomeService::class); + } +} + +?> diff --git a/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromContainerGetSetUpRector/Fixture/this_container_get.php.inc b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromContainerGetSetUpRector/Fixture/this_container_get.php.inc new file mode 100644 index 00000000000..ea548fb2e20 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromContainerGetSetUpRector/Fixture/this_container_get.php.inc @@ -0,0 +1,40 @@ +someService = $this->container->get('some_service'); + } +} + +?> +----- +someService = $this->container->get('some_service'); + } +} + +?> diff --git a/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromContainerGetSetUpRector/Fixture/this_get.php.inc b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromContainerGetSetUpRector/Fixture/this_get.php.inc new file mode 100644 index 00000000000..fe644be8759 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromContainerGetSetUpRector/Fixture/this_get.php.inc @@ -0,0 +1,40 @@ +someService = $this->get(SomeService::class); + } +} + +?> +----- +someService = $this->get(SomeService::class); + } +} + +?> diff --git a/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromContainerGetSetUpRector/Source/SomeService.php b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromContainerGetSetUpRector/Source/SomeService.php new file mode 100644 index 00000000000..e90cdf1024e --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromContainerGetSetUpRector/Source/SomeService.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_/TypedPropertyFromContainerGetSetUpRector/config/configured_rule.php b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromContainerGetSetUpRector/config/configured_rule.php new file mode 100644 index 00000000000..0a6b2032578 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromContainerGetSetUpRector/config/configured_rule.php @@ -0,0 +1,11 @@ +withRules([TypedPropertyFromContainerGetSetUpRector::class]) + ->withPhpVersion(PhpVersionFeature::TYPED_PROPERTIES); diff --git a/rules/TypeDeclaration/Rector/Class_/TypedPropertyFromContainerGetSetUpRector.php b/rules/TypeDeclaration/Rector/Class_/TypedPropertyFromContainerGetSetUpRector.php new file mode 100644 index 00000000000..366d73ea26e --- /dev/null +++ b/rules/TypeDeclaration/Rector/Class_/TypedPropertyFromContainerGetSetUpRector.php @@ -0,0 +1,240 @@ +someService = static::getContainer()->get(SomeService::class); + } +} +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +use PHPUnit\Framework\TestCase; + +final class SomeTest extends TestCase +{ + private SomeService $someService; + + protected function setUp(): void + { + $this->someService = static::getContainer()->get(SomeService::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->isAssignedViaContainerGetInSetUp($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 isAssignedViaContainerGetInSetUp(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->isContainerGetCall($assign->expr)) { + return true; + } + } + + return false; + } + + private function isContainerGetCall(Expr $expr): bool + { + if (! $expr instanceof MethodCall) { + return false; + } + + if (! $this->isName($expr->name, 'get')) { + return false; + } + + $caller = $expr->var; + + // static::getContainer()->get(...) or $this->getContainer()->get(...) + if ($caller instanceof StaticCall || $caller instanceof MethodCall) { + return $this->isName($caller->name, 'getContainer'); + } + + // $this->container->get(...) + if ($caller instanceof PropertyFetch) { + return $this->isName($caller, 'container'); + } + + // $this->get(...) + return $caller instanceof Variable && $this->isName($caller, 'this'); + } + + 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 bbb63907a4d..ec357dc45a2 100644 --- a/src/Config/Level/TypeDeclarationLevel.php +++ b/src/Config/Level/TypeDeclarationLevel.php @@ -15,6 +15,7 @@ use Rector\TypeDeclaration\Rector\Class_\PropertyTypeFromStrictSetterGetterRector; use Rector\TypeDeclaration\Rector\Class_\ReturnTypeFromStrictTernaryRector; use Rector\TypeDeclaration\Rector\Class_\ScalarTypedPropertyFromJMSSerializerAttributeTypeRector; +use Rector\TypeDeclaration\Rector\Class_\TypedPropertyFromContainerGetSetUpRector; use Rector\TypeDeclaration\Rector\Class_\TypedPropertyFromCreateMockAssignRector; use Rector\TypeDeclaration\Rector\Class_\TypedPropertyFromDocblockSetUpDefinedRector; use Rector\TypeDeclaration\Rector\Class_\TypedStaticPropertyInBehatContextRector; @@ -153,6 +154,7 @@ final class TypeDeclarationLevel MergeDateTimePropertyTypeDeclarationRector::class, PropertyTypeFromStrictSetterGetterRector::class, ParamTypeByMethodCallTypeRector::class, + TypedPropertyFromContainerGetSetUpRector::class, TypedPropertyFromAssignsRector::class, AddReturnTypeDeclarationBasedOnParentClassMethodRector::class, ReturnTypeFromStrictFluentReturnRector::class,