diff --git a/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/arrow_function.php.inc b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/arrow_function.php.inc new file mode 100644 index 00000000000..d81eef00908 --- /dev/null +++ b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/arrow_function.php.inc @@ -0,0 +1,25 @@ + $x > 5 ? 'high' : 10; + +$ternary = fn(?int $value): string|int|float|null => + $value === null ? null : ($value > 0 ? 'positive' : -1); + +$cast = fn(mixed $input): int|string|array => (int) $input; + +?> +----- + $x > 5 ? 'high' : 10; + +$ternary = fn(?int $value): string|int|null => + $value === null ? null : ($value > 0 ? 'positive' : -1); + +$cast = fn(mixed $input): int => (int) $input; + +?> diff --git a/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/closure.php.inc b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/closure.php.inc new file mode 100644 index 00000000000..1380d360f1e --- /dev/null +++ b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/closure.php.inc @@ -0,0 +1,35 @@ + +----- + diff --git a/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/edge_cases.php.inc b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/edge_cases.php.inc new file mode 100644 index 00000000000..d0ba4dad7c7 --- /dev/null +++ b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/edge_cases.php.inc @@ -0,0 +1,59 @@ + +----- + diff --git a/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/final_class.php.inc b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/final_class.php.inc new file mode 100644 index 00000000000..0afdfd50865 --- /dev/null +++ b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/final_class.php.inc @@ -0,0 +1,87 @@ + 'value']; + } +} + +?> +----- + 'value']; + } +} + +?> diff --git a/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/final_inheritance.php.inc b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/final_inheritance.php.inc new file mode 100644 index 00000000000..e1f81c5f4e2 --- /dev/null +++ b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/final_inheritance.php.inc @@ -0,0 +1,59 @@ + +----- + diff --git a/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/final_methods.php.inc b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/final_methods.php.inc new file mode 100644 index 00000000000..938c6f8429a --- /dev/null +++ b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/final_methods.php.inc @@ -0,0 +1,57 @@ + +----- + diff --git a/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/function.php.inc b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/function.php.inc new file mode 100644 index 00000000000..a2caaafb334 --- /dev/null +++ b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/function.php.inc @@ -0,0 +1,37 @@ + +----- + diff --git a/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/phpdocs.php.inc b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/phpdocs.php.inc new file mode 100644 index 00000000000..40d92c06dd3 --- /dev/null +++ b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/phpdocs.php.inc @@ -0,0 +1,129 @@ + $class + * @return class-string|int + */ + public function bar(string $class): string|int + { + return $class; + } + + /** @return class-string|int */ + public function baz(string $class): string|int + { + return SomeInterface::class; + } + + /** @return \Iterator|string */ + function qux(): \Iterator|string + { + return new \ArrayIterator([1]); + } + + /** @return \Iterator|string */ + function qax(): \Iterator|string + { + return 'text'; + } + + /** + * @param int $a + */ + function quux(int $a): int|string + { + return $a; + } + + /** + * @param int $a + * @return int|string + */ + function mixedReturn(int $a): int|string + { + return $a; + } +} + +?> +----- + $class + * @return class-string + */ + public function bar(string $class): string + { + return $class; + } + + /** @return class-string */ + public function baz(string $class): string + { + return SomeInterface::class; + } + + /** @return \Iterator */ + function qux(): \Iterator + { + return new \ArrayIterator([1]); + } + + /** @return string */ + function qax(): string + { + return 'text'; + } + + /** + * @param int $a + */ + function quux(int $a): int + { + return $a; + } + + /** + * @param int $a + * @return int + */ + function mixedReturn(int $a): int + { + return $a; + } +} + +?> diff --git a/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/skip_abstracts.php.inc b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/skip_abstracts.php.inc new file mode 100644 index 00000000000..4c218f4c0cc --- /dev/null +++ b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/skip_abstracts.php.inc @@ -0,0 +1,28 @@ + diff --git a/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/skip_function_likes_without_parameter_types.php.inc b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/skip_function_likes_without_parameter_types.php.inc new file mode 100644 index 00000000000..b29f390e5f4 --- /dev/null +++ b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/skip_function_likes_without_parameter_types.php.inc @@ -0,0 +1,17 @@ + $class + * @return class-string|int + */ + public function bar($class): string|int + { + return $class; + } +} + +?> diff --git a/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/skip_generator.php.inc b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/skip_generator.php.inc new file mode 100644 index 00000000000..0371e26d348 --- /dev/null +++ b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/skip_generator.php.inc @@ -0,0 +1,23 @@ + diff --git a/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/skip_non_final_classes.php.inc b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/skip_non_final_classes.php.inc new file mode 100644 index 00000000000..df43fc5a948 --- /dev/null +++ b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/skip_non_final_classes.php.inc @@ -0,0 +1,18 @@ + diff --git a/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/skip_non_final_inheritance.php.inc b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/skip_non_final_inheritance.php.inc new file mode 100644 index 00000000000..d0c024cd6bb --- /dev/null +++ b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/skip_non_final_inheritance.php.inc @@ -0,0 +1,29 @@ + diff --git a/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/skip_return_all_types.php.inc b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/skip_return_all_types.php.inc new file mode 100644 index 00000000000..04b9d6f1893 --- /dev/null +++ b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/skip_return_all_types.php.inc @@ -0,0 +1,45 @@ + diff --git a/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/skip_unknown_parameter.php.inc b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/skip_unknown_parameter.php.inc new file mode 100644 index 00000000000..1247323ac5b --- /dev/null +++ b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/skip_unknown_parameter.php.inc @@ -0,0 +1,22 @@ + diff --git a/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/terminating_methods.php.inc b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/terminating_methods.php.inc new file mode 100644 index 00000000000..4a24bc1dacd --- /dev/null +++ b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/terminating_methods.php.inc @@ -0,0 +1,123 @@ + +----- + diff --git a/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/NarrowTooWideReturnTypeRectorTest.php b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/NarrowTooWideReturnTypeRectorTest.php new file mode 100644 index 00000000000..02823bb43a2 --- /dev/null +++ b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/NarrowTooWideReturnTypeRectorTest.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/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Source/SomeAbstractClass.php b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Source/SomeAbstractClass.php new file mode 100644 index 00000000000..82b85203a95 --- /dev/null +++ b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Source/SomeAbstractClass.php @@ -0,0 +1,8 @@ +rule(NarrowTooWideReturnTypeRector::class); + $rectorConfig->phpVersion(PhpVersionFeature::UNION_TYPES); +}; diff --git a/rules/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php b/rules/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php new file mode 100644 index 00000000000..526674e472f --- /dev/null +++ b/rules/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php @@ -0,0 +1,265 @@ +> + */ + public function getNodeTypes(): array + { + return [ClassMethod::class, Function_::class, Closure::class, ArrowFunction::class]; + } + + /** + * @param ClassMethod|Function_|Closure|ArrowFunction $node + */ + public function refactor(Node $node): ?Node + { + $scope = ScopeFetcher::fetch($node); + + if ($this->shouldSkipNode($node, $scope)) { + return null; + } + + $returnStatements = $node instanceof ArrowFunction + ? [] + : $this->betterNodeFinder->findReturnsScoped($node); + $isAlwaysTerminating = ! $this->silentVoidResolver->hasSilentVoid($node); + + if ($returnStatements === [] && ! $node instanceof ArrowFunction) { + return null; + } + + $phpDocInfo = $this->phpDocInfoFactory->createFromNode($node); + $hasReturnDocblock = (bool) $phpDocInfo?->hasByName('@return'); + + if ($hasReturnDocblock) { + $returnType = $phpDocInfo->getReturnType(); + } else { + $returnType = $node->returnType; + Assert::isInstanceOfAny($returnType, [UnionType::class, NullableType::class]); + $returnType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($returnType); + } + + $actualReturnTypes = $this->collectActualReturnTypes($node, $returnStatements, $isAlwaysTerminating); + $newReturnType = $this->narrowReturnType($returnType, $actualReturnTypes); + + if ($newReturnType === null) { + return null; + } + + $node->returnType = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode( + $newReturnType, + TypeKind::RETURN + ); + + if ($hasReturnDocblock) { + $this->phpDocTypeChanger->changeReturnType($node, $phpDocInfo, $newReturnType); + } + + return $node; + } + + private function shouldSkipNode(ClassMethod|Function_|Closure|ArrowFunction $node, Scope $scope): bool + { + $returnType = $node->returnType; + + if (! $returnType instanceof UnionType && ! $returnType instanceof NullableType) { + return true; + } + + if ($this->hasYield($node)) { + return true; + } + + foreach ($node->params as $param) { + if (! $param->type instanceof Node) { + return true; + } + } + + if (! $node instanceof ClassMethod) { + return false; + } + + if ($node->isPrivate() || $node->isFinal()) { + return false; + } + + if ($node->isAbstract()) { + return true; + } + + $classReflection = $this->reflectionResolver->resolveClassReflection($node); + + if (! $classReflection instanceof ClassReflection) { + return true; + } + + if (! $classReflection->isClass()) { + return true; + } + + return ! $classReflection->isFinalByKeyword(); + } + + /** + * @param Return_[] $returnStatements + * @return Type[] + */ + private function collectActualReturnTypes( + ClassMethod|Function_|Closure|ArrowFunction $node, + array $returnStatements, + bool $isAlwaysTerminating, + ): array { + if ($node instanceof ArrowFunction) { + return [$this->getType($node->expr)]; + } + + $returnTypes = []; + foreach ($returnStatements as $returnStatement) { + if ($returnStatement->expr === null) { + $returnTypes[] = new NullType(); + continue; + } + + $returnTypes[] = $this->getType($returnStatement->expr); + } + + if (! $isAlwaysTerminating) { + $returnTypes[] = new NullType(); + } + + return $returnTypes; + } + + /** + * @param Type[] $actualReturnTypes + */ + private function narrowReturnType(Type $returnType, array $actualReturnTypes): Type|null + { + $types = $returnType instanceof PHPStanUnionType ? $returnType->getTypes() : [$returnType]; + $usedTypes = []; + + foreach ($types as $type) { + foreach ($actualReturnTypes as $actualType) { + if (! $type->isSuperTypeOf($actualType)->no()) { + $usedTypes[] = $type; + break; + } + } + } + + $usedTypes = array_unique($usedTypes, SORT_REGULAR); + + if ($usedTypes === [] || count($usedTypes) === count($types)) { + return null; + } + + return TypeCombinator::union(...$usedTypes); + } + + private function hasYield(ClassMethod|Function_|Closure|ArrowFunction $node): bool + { + if ($node instanceof ArrowFunction) { + return false; + } + + return (bool) $this->betterNodeFinder->hasInstancesOfInFunctionLikeScoped( + $node, + [Yield_::class, YieldFrom::class] + ); + } +} diff --git a/src/Config/Level/DeadCodeLevel.php b/src/Config/Level/DeadCodeLevel.php index a67918d0597..b8dfcea0b28 100644 --- a/src/Config/Level/DeadCodeLevel.php +++ b/src/Config/Level/DeadCodeLevel.php @@ -35,6 +35,7 @@ use Rector\DeadCode\Rector\For_\RemoveDeadLoopRector; use Rector\DeadCode\Rector\Foreach_\RemoveUnusedForeachKeyRector; use Rector\DeadCode\Rector\FuncCall\RemoveFilterVarOnExactTypeRector; +use Rector\DeadCode\Rector\FunctionLike\NarrowTooWideReturnTypeRector; use Rector\DeadCode\Rector\FunctionLike\RemoveDeadReturnRector; use Rector\DeadCode\Rector\If_\ReduceAlwaysFalseIfOrRector; use Rector\DeadCode\Rector\If_\RemoveAlwaysTrueIfConditionRector; @@ -141,5 +142,6 @@ final class DeadCodeLevel RemoveDeadReturnRector::class, RemoveArgumentFromDefaultParentCallRector::class, + NarrowTooWideReturnTypeRector::class, ]; }