diff --git a/packages/VendorLocker/ParentClassMethodTypeOverrideGuard.php b/packages/VendorLocker/ParentClassMethodTypeOverrideGuard.php index de6ee4ea628..0a39cffb069 100644 --- a/packages/VendorLocker/ParentClassMethodTypeOverrideGuard.php +++ b/packages/VendorLocker/ParentClassMethodTypeOverrideGuard.php @@ -15,6 +15,8 @@ use Rector\Core\Reflection\ReflectionResolver; use Rector\Core\ValueObject\MethodName; use Rector\NodeNameResolver\NodeNameResolver; +use Rector\NodeTypeResolver\TypeComparator\TypeComparator; +use Rector\StaticTypeMapper\StaticTypeMapper; use Rector\TypeDeclaration\TypeInferer\ParamTypeInferer; use Symplify\SmartFileSystem\Normalizer\PathNormalizer; @@ -25,7 +27,9 @@ public function __construct( private readonly PathNormalizer $pathNormalizer, private readonly AstResolver $astResolver, private readonly ParamTypeInferer $paramTypeInferer, - private readonly ReflectionResolver $reflectionResolver + private readonly ReflectionResolver $reflectionResolver, + private readonly TypeComparator $typeComparator, + private readonly StaticTypeMapper $staticTypeMapper ) { } @@ -138,4 +142,19 @@ public function getParentClassMethod(ClassMethod $classMethod): ?MethodReflectio return null; } + + public function shouldSkipReturnTypeChange(ClassMethod $classMethod, Type $parentType): bool + { + if ($classMethod->returnType === null) { + return false; + } + + $currentReturnType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($classMethod->returnType); + + if ($this->typeComparator->isSubtype($currentReturnType, $parentType)) { + return true; + } + + return $this->typeComparator->areTypesEqual($currentReturnType, $parentType); + } } diff --git a/rules-tests/TypeDeclaration/Rector/ClassMethod/AddReturnTypeDeclarationBasedOnParentClassMethodRector/AddReturnTypeDeclarationBasedOnParentClassMethodRectorTest.php b/rules-tests/TypeDeclaration/Rector/ClassMethod/AddReturnTypeDeclarationBasedOnParentClassMethodRector/AddReturnTypeDeclarationBasedOnParentClassMethodRectorTest.php new file mode 100644 index 00000000000..d361a8de63a --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/ClassMethod/AddReturnTypeDeclarationBasedOnParentClassMethodRector/AddReturnTypeDeclarationBasedOnParentClassMethodRectorTest.php @@ -0,0 +1,33 @@ +doTestFileInfo($fileInfo); + } + + /** + * @return Iterator + */ + public function provideData(): Iterator + { + return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/rules-tests/TypeDeclaration/Rector/ClassMethod/AddReturnTypeDeclarationBasedOnParentClassMethodRector/Fixture/extended_class.php.inc b/rules-tests/TypeDeclaration/Rector/ClassMethod/AddReturnTypeDeclarationBasedOnParentClassMethodRector/Fixture/extended_class.php.inc new file mode 100644 index 00000000000..12bc4947185 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/ClassMethod/AddReturnTypeDeclarationBasedOnParentClassMethodRector/Fixture/extended_class.php.inc @@ -0,0 +1,25 @@ + +----- + diff --git a/rules-tests/TypeDeclaration/Rector/ClassMethod/AddReturnTypeDeclarationBasedOnParentClassMethodRector/Fixture/extended_class_no_type.php.inc b/rules-tests/TypeDeclaration/Rector/ClassMethod/AddReturnTypeDeclarationBasedOnParentClassMethodRector/Fixture/extended_class_no_type.php.inc new file mode 100644 index 00000000000..90eaf3dbabb --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/ClassMethod/AddReturnTypeDeclarationBasedOnParentClassMethodRector/Fixture/extended_class_no_type.php.inc @@ -0,0 +1,25 @@ + +----- + diff --git a/rules-tests/TypeDeclaration/Rector/ClassMethod/AddReturnTypeDeclarationBasedOnParentClassMethodRector/Fixture/extended_interface.php.inc b/rules-tests/TypeDeclaration/Rector/ClassMethod/AddReturnTypeDeclarationBasedOnParentClassMethodRector/Fixture/extended_interface.php.inc new file mode 100644 index 00000000000..bcc22253170 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/ClassMethod/AddReturnTypeDeclarationBasedOnParentClassMethodRector/Fixture/extended_interface.php.inc @@ -0,0 +1,25 @@ + +----- + diff --git a/rules-tests/TypeDeclaration/Rector/ClassMethod/AddReturnTypeDeclarationBasedOnParentClassMethodRector/Source/SomeClassWithReturnType.php b/rules-tests/TypeDeclaration/Rector/ClassMethod/AddReturnTypeDeclarationBasedOnParentClassMethodRector/Source/SomeClassWithReturnType.php new file mode 100644 index 00000000000..e42c7a77654 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/ClassMethod/AddReturnTypeDeclarationBasedOnParentClassMethodRector/Source/SomeClassWithReturnType.php @@ -0,0 +1,13 @@ +rule(AddReturnTypeDeclarationBasedOnParentClassMethodRector::class); + + $rectorConfig->phpVersion(PhpVersionFeature::MIXED_TYPE); +}; diff --git a/rules/TypeDeclaration/Rector/ClassMethod/AddReturnTypeDeclarationBasedOnParentClassMethodRector.php b/rules/TypeDeclaration/Rector/ClassMethod/AddReturnTypeDeclarationBasedOnParentClassMethodRector.php new file mode 100644 index 00000000000..253a1ebebbc --- /dev/null +++ b/rules/TypeDeclaration/Rector/ClassMethod/AddReturnTypeDeclarationBasedOnParentClassMethodRector.php @@ -0,0 +1,160 @@ +> + */ + public function getNodeTypes(): array + { + return [ClassMethod::class]; + } + + /** + * @param ClassMethod $node + */ + public function refactor(Node $node): ?Node + { + if ($this->nodeNameResolver->isName($node, MethodName::CONSTRUCT)) { + return null; + } + + $parentMethodReflection = $this->parentClassMethodTypeOverrideGuard->getParentClassMethod($node); + if (! $parentMethodReflection instanceof MethodReflection) { + return null; + } + + $parentClassMethod = $this->astResolver->resolveClassMethodFromMethodReflection($parentMethodReflection); + + if (! $parentClassMethod instanceof ClassMethod) { + return null; + } + + if ($parentClassMethod->isPrivate()) { + return null; + } + + $parentClassMethodReturnType = $parentClassMethod->getReturnType(); + if ($parentClassMethodReturnType === null) { + return null; + } + + $parentClassMethodReturnType = $this->staticTypeMapper->mapPhpParserNodePHPStanType( + $parentClassMethodReturnType + ); + + return $this->processClassMethodReturnType($node, $parentClassMethodReturnType); + } + + private function processClassMethodReturnType(ClassMethod $classMethod, Type $parentType): ?ClassMethod + { + if ($parentType instanceof MixedType) { + $class = $classMethod->getAttribute(AttributeKey::PARENT_NODE); + if (! $class instanceof Class_) { + return null; + } + + $className = (string) $this->nodeNameResolver->getName($class); + $currentObjectType = new ObjectType($className); + if (! $parentType->equals($currentObjectType) && $classMethod->returnType !== null) { + return null; + } + } + + // remove it + if ($parentType instanceof MixedType && ! $this->phpVersionProvider->isAtLeastPhpVersion( + PhpVersionFeature::MIXED_TYPE + )) { + $classMethod->returnType = null; + return null; + } + + // already set and sub type or equal → no change + if ($this->parentClassMethodTypeOverrideGuard->shouldSkipReturnTypeChange($classMethod, $parentType)) { + return null; + } + + $classMethod->returnType = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode( + $parentType, + TypeKind::RETURN + ); + + return $classMethod; + } +} diff --git a/rules/TypeDeclaration/Rector/ClassMethod/AddReturnTypeDeclarationRector.php b/rules/TypeDeclaration/Rector/ClassMethod/AddReturnTypeDeclarationRector.php index 298a0db7d22..44ca19d1cc2 100644 --- a/rules/TypeDeclaration/Rector/ClassMethod/AddReturnTypeDeclarationRector.php +++ b/rules/TypeDeclaration/Rector/ClassMethod/AddReturnTypeDeclarationRector.php @@ -16,9 +16,9 @@ use Rector\Core\Rector\AbstractRector; use Rector\Core\ValueObject\PhpVersionFeature; use Rector\NodeTypeResolver\Node\AttributeKey; -use Rector\NodeTypeResolver\TypeComparator\TypeComparator; use Rector\PHPStanStaticTypeMapper\Enum\TypeKind; use Rector\TypeDeclaration\ValueObject\AddReturnTypeDeclaration; +use Rector\VendorLocker\ParentClassMethodTypeOverrideGuard; use Symplify\RuleDocGenerator\ValueObject\CodeSample\ConfiguredCodeSample; use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; use Webmozart\Assert\Assert; @@ -36,8 +36,8 @@ final class AddReturnTypeDeclarationRector extends AbstractRector implements Con private bool $hasChanged = false; public function __construct( - private readonly TypeComparator $typeComparator, private readonly PhpVersionProvider $phpVersionProvider, + private readonly ParentClassMethodTypeOverrideGuard $parentClassMethodTypeOverrideGuard ) { } @@ -142,7 +142,7 @@ private function processClassMethodNodeWithTypehints( } // already set and sub type or equal → no change - if ($this->shouldSkipType($classMethod, $newType)) { + if ($this->parentClassMethodTypeOverrideGuard->shouldSkipReturnTypeChange($classMethod, $newType)) { return; } @@ -150,19 +150,4 @@ private function processClassMethodNodeWithTypehints( $this->hasChanged = true; } - - private function shouldSkipType(ClassMethod $classMethod, Type $newType): bool - { - if ($classMethod->returnType === null) { - return false; - } - - $currentReturnType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($classMethod->returnType); - - if ($this->typeComparator->isSubtype($currentReturnType, $newType)) { - return true; - } - - return $this->typeComparator->areTypesEqual($currentReturnType, $newType); - } }