From cd5666907683523402ab66dc46abba0d2f399a05 Mon Sep 17 00:00:00 2001 From: Caleb White Date: Wed, 13 Aug 2025 22:42:39 -0500 Subject: [PATCH 01/17] feat: add TooWideReturnTypeRector --- .../Fixture/arrow_function.php.inc | 25 ++ .../Fixture/closure.php.inc | 35 +++ .../Fixture/edge_cases.php.inc | 73 ++++++ .../Fixture/final_class.php.inc | 87 +++++++ .../Fixture/final_methods.php.inc | 57 ++++ .../Fixture/function.php.inc | 37 +++ .../Fixture/skip_abstract.php.inc | 28 ++ .../Fixture/skip_generator.php.inc | 23 ++ .../Fixture/skip_inheritance.php.inc | 35 +++ .../Fixture/skip_non_final_classes.php.inc | 18 ++ .../Fixture/skip_return_all_types.php.inc | 21 ++ .../Fixture/terminating_methods.php.inc | 161 ++++++++++++ .../TooWideReturnTypeRectorTest.php | 28 ++ .../config/configured_rule.php | 9 + .../FunctionLike/TooWideReturnTypeRector.php | 243 ++++++++++++++++++ src/Config/Level/DeadCodeLevel.php | 2 + src/NodeAnalyzer/TerminatedNodeAnalyzer.php | 45 +++- 17 files changed, 924 insertions(+), 3 deletions(-) create mode 100644 rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/arrow_function.php.inc create mode 100644 rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/closure.php.inc create mode 100644 rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/edge_cases.php.inc create mode 100644 rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/final_class.php.inc create mode 100644 rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/final_methods.php.inc create mode 100644 rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/function.php.inc create mode 100644 rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/skip_abstract.php.inc create mode 100644 rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/skip_generator.php.inc create mode 100644 rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/skip_inheritance.php.inc create mode 100644 rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/skip_non_final_classes.php.inc create mode 100644 rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/skip_return_all_types.php.inc create mode 100644 rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/terminating_methods.php.inc create mode 100644 rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/TooWideReturnTypeRectorTest.php create mode 100644 rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/config/configured_rule.php create mode 100644 rules/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector.php diff --git a/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/arrow_function.php.inc b/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/arrow_function.php.inc new file mode 100644 index 00000000000..ba94304a4ba --- /dev/null +++ b/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/arrow_function.php.inc @@ -0,0 +1,25 @@ + $x > 5 ? 'high' : 10; + +$ternary = fn($value): string|int|float|null => + $value === null ? null : ($value > 0 ? 'positive' : -1); + +$cast = fn($input): int|string|array => (int) $input; + +?> +----- + $x > 5 ? 'high' : 10; + +$ternary = fn($value): string|int|null => + $value === null ? null : ($value > 0 ? 'positive' : -1); + +$cast = fn($input): int => (int) $input; + +?> diff --git a/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/closure.php.inc b/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/closure.php.inc new file mode 100644 index 00000000000..4ea7bf3c613 --- /dev/null +++ b/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/closure.php.inc @@ -0,0 +1,35 @@ + +----- + diff --git a/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/edge_cases.php.inc b/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/edge_cases.php.inc new file mode 100644 index 00000000000..7870c377e05 --- /dev/null +++ b/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/edge_cases.php.inc @@ -0,0 +1,73 @@ + +----- + diff --git a/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/final_class.php.inc b/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/final_class.php.inc new file mode 100644 index 00000000000..d2a0d7aabce --- /dev/null +++ b/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/final_class.php.inc @@ -0,0 +1,87 @@ + 'value']; + } +} + +?> +----- + 'value']; + } +} + +?> diff --git a/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/final_methods.php.inc b/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/final_methods.php.inc new file mode 100644 index 00000000000..17715c56095 --- /dev/null +++ b/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/final_methods.php.inc @@ -0,0 +1,57 @@ + +----- + diff --git a/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/function.php.inc b/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/function.php.inc new file mode 100644 index 00000000000..a800068845b --- /dev/null +++ b/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/function.php.inc @@ -0,0 +1,37 @@ + +----- + diff --git a/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/skip_abstract.php.inc b/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/skip_abstract.php.inc new file mode 100644 index 00000000000..f08bb9a941e --- /dev/null +++ b/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/skip_abstract.php.inc @@ -0,0 +1,28 @@ + diff --git a/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/skip_generator.php.inc b/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/skip_generator.php.inc new file mode 100644 index 00000000000..c9d68d8e9a5 --- /dev/null +++ b/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/skip_generator.php.inc @@ -0,0 +1,23 @@ + diff --git a/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/skip_inheritance.php.inc b/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/skip_inheritance.php.inc new file mode 100644 index 00000000000..d9272627776 --- /dev/null +++ b/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/skip_inheritance.php.inc @@ -0,0 +1,35 @@ + diff --git a/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/skip_non_final_classes.php.inc b/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/skip_non_final_classes.php.inc new file mode 100644 index 00000000000..16fb31377c2 --- /dev/null +++ b/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/skip_non_final_classes.php.inc @@ -0,0 +1,18 @@ + diff --git a/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/skip_return_all_types.php.inc b/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/skip_return_all_types.php.inc new file mode 100644 index 00000000000..08281210368 --- /dev/null +++ b/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/skip_return_all_types.php.inc @@ -0,0 +1,21 @@ + diff --git a/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/terminating_methods.php.inc b/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/terminating_methods.php.inc new file mode 100644 index 00000000000..7a379297c2c --- /dev/null +++ b/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/terminating_methods.php.inc @@ -0,0 +1,161 @@ + +----- + diff --git a/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/TooWideReturnTypeRectorTest.php b/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/TooWideReturnTypeRectorTest.php new file mode 100644 index 00000000000..b296880fd6e --- /dev/null +++ b/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/TooWideReturnTypeRectorTest.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/TooWideReturnTypeRector/config/configured_rule.php b/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/config/configured_rule.php new file mode 100644 index 00000000000..55e53024136 --- /dev/null +++ b/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/config/configured_rule.php @@ -0,0 +1,9 @@ +withRules([TooWideReturnTypeRector::class]); diff --git a/rules/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector.php b/rules/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector.php new file mode 100644 index 00000000000..880a9a054f7 --- /dev/null +++ b/rules/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector.php @@ -0,0 +1,243 @@ +> + */ + 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 = $this->betterNodeFinder->findInstanceOf($node, Return_::class); + $isAlwaysTerminating = $node instanceof ArrowFunction + || $this->terminatedNodeAnalyzer->isAlwaysTerminating($node); + + if ($returnStatements === [] && ! $node instanceof ArrowFunction) { + $node->returnType = $isAlwaysTerminating + ? new Identifier('never') + : new Identifier('void'); + + return $node; + } + + $returnType = $node->returnType; + + if (! $returnType instanceof UnionType) { + return null; + } + + $actualReturnTypes = $this->collectActualReturnTypes($node, $returnStatements, $isAlwaysTerminating); + $newReturnType = $this->narrowUnionReturnType($returnType, $actualReturnTypes); + + if ($newReturnType === null) { + return null; + } + + $node->returnType = $newReturnType; + + return $node; + } + + private function shouldSkipNode(ClassMethod|Function_|Closure|ArrowFunction $node, Scope $scope): bool + { + if ($this->hasYield($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; + } + + return ! $classReflection->isFinal(); + } + + /** + * @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 narrowUnionReturnType( + UnionType $unionType, + array $actualReturnTypes + ): ComplexType|Identifier|Name|null { + $types = $unionType->types; + $usedTypes = []; + + foreach ($types as $type) { + $declaredType = $this->getType($type); + if ($declaredType instanceof MixedType) { + // Mixed type covers all other types, so we should only keep mixed + return new Identifier('mixed'); + } + foreach ($actualReturnTypes as $actualType) { + if (! $declaredType->isSuperTypeOf($actualType)->no()) { + $usedTypes[] = $declaredType; + break; + } + } + } + + if ($usedTypes === [] || count($usedTypes) === count($types)) { + return null; + } + + return $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode( + TypeCombinator::union(...$usedTypes), + TypeKind::RETURN + ); + } + + private function hasYield(ClassMethod|Function_|Closure|ArrowFunction $node): bool + { + if ($node instanceof ArrowFunction) { + return false; + } + + $stmts = $node->stmts; + + if ($stmts === null || $stmts === []) { + return false; + } + + return (bool) $this->betterNodeFinder->findFirst( + $stmts, + fn (Node $subNode): bool => $subNode instanceof Yield_ || $subNode instanceof YieldFrom + ); + } +} diff --git a/src/Config/Level/DeadCodeLevel.php b/src/Config/Level/DeadCodeLevel.php index a67918d0597..9760484765e 100644 --- a/src/Config/Level/DeadCodeLevel.php +++ b/src/Config/Level/DeadCodeLevel.php @@ -36,6 +36,7 @@ use Rector\DeadCode\Rector\Foreach_\RemoveUnusedForeachKeyRector; use Rector\DeadCode\Rector\FuncCall\RemoveFilterVarOnExactTypeRector; use Rector\DeadCode\Rector\FunctionLike\RemoveDeadReturnRector; +use Rector\DeadCode\Rector\FunctionLike\TooWideReturnTypeRector; use Rector\DeadCode\Rector\If_\ReduceAlwaysFalseIfOrRector; use Rector\DeadCode\Rector\If_\RemoveAlwaysTrueIfConditionRector; use Rector\DeadCode\Rector\If_\RemoveDeadInstanceOfRector; @@ -126,6 +127,7 @@ final class DeadCodeLevel RemoveParentCallWithoutParentRector::class, RemoveDeadConditionAboveReturnRector::class, RemoveDeadLoopRector::class, + TooWideReturnTypeRector::class, // removing methods could be risky if there is some magic loading them RemoveUnusedPromotedPropertyRector::class, diff --git a/src/NodeAnalyzer/TerminatedNodeAnalyzer.php b/src/NodeAnalyzer/TerminatedNodeAnalyzer.php index 1a0ec572129..d1a3734d112 100644 --- a/src/NodeAnalyzer/TerminatedNodeAnalyzer.php +++ b/src/NodeAnalyzer/TerminatedNodeAnalyzer.php @@ -45,6 +45,29 @@ final class TerminatedNodeAnalyzer */ private const ALLOWED_CONTINUE_CURRENT_STMTS = [InlineHTML::class, Nop::class]; + public function isAlwaysTerminating(StmtsAwareInterface $stmtsAware): bool + { + $stmts = $stmtsAware->stmts; + + if ($stmts === null || $stmts === []) { + return false; + } + + foreach ($stmts as $key => $stmt) { + if (! isset($stmts[$key - 1])) { + continue; + } + + $previousStmt = $stmts[$key - 1]; + + if ($this->isAlwaysTerminated($stmtsAware, $previousStmt, $stmt)) { + return true; + } + } + + return $this->isTerminatedInLastStmts($stmts, $stmts[array_key_last($stmts)]); + } + public function isAlwaysTerminated(StmtsAwareInterface $stmtsAware, Stmt $node, Stmt $currentStmt): bool { if (in_array($currentStmt::class, self::ALLOWED_CONTINUE_CURRENT_STMTS, true)) { @@ -157,7 +180,7 @@ private function isTerminatedInLastStmtsIf(If_ $if, Stmt $stmt): bool /** * @param Stmt[] $stmts */ - private function isTerminatedInLastStmts(array $stmts, Node $node): bool + private function isTerminatedInLastStmts(array $stmts, Stmt $stmt): bool { if ($stmts === []) { return false; @@ -166,7 +189,7 @@ private function isTerminatedInLastStmts(array $stmts, Node $node): bool $lastKey = array_key_last($stmts); $lastNode = $stmts[$lastKey]; - if (isset($stmts[$lastKey - 1]) && ! $this->isTerminatedNode($stmts[$lastKey - 1], $node)) { + if (isset($stmts[$lastKey - 1]) && ! $this->isTerminatedNode($stmts[$lastKey - 1], $stmt)) { return false; } @@ -174,6 +197,22 @@ private function isTerminatedInLastStmts(array $stmts, Node $node): bool return $lastNode->expr instanceof Exit_ || $lastNode->expr instanceof Throw_; } - return $lastNode instanceof Return_; + if ($lastNode instanceof Return_) { + return true; + } + + if ($lastNode instanceof If_) { + return $this->isTerminatedInLastStmtsIf($lastNode, $stmt); + } + + if ($lastNode instanceof TryCatch) { + return $this->isTerminatedInLastStmtsTryCatch($lastNode, $stmt); + } + + if ($lastNode instanceof Switch_) { + return $this->isTerminatedInLastStmtsSwitch($lastNode, $stmt); + } + + return false; } } From 74322a74a9246063635a3f1e62d7814951fc33a1 Mon Sep 17 00:00:00 2001 From: Caleb White Date: Fri, 15 Aug 2025 07:37:09 -0500 Subject: [PATCH 02/17] chore: rename TooWideReturnTypeRector to NarrowTooWideReturnTypeRector --- .../Fixture/arrow_function.php.inc | 4 ++-- .../Fixture/closure.php.inc | 4 ++-- .../Fixture/edge_cases.php.inc | 4 ++-- .../Fixture/final_class.php.inc | 4 ++-- .../Fixture/final_methods.php.inc | 4 ++-- .../Fixture/function.php.inc | 4 ++-- .../Fixture/skip_abstract.php.inc | 2 +- .../Fixture/skip_generator.php.inc | 2 +- .../Fixture/skip_inheritance.php.inc | 2 +- .../Fixture/skip_non_final_classes.php.inc | 2 +- .../Fixture/skip_return_all_types.php.inc | 2 +- .../Fixture/terminating_methods.php.inc | 4 ++-- .../NarrowTooWideReturnTypeRectorTest.php} | 4 ++-- .../config/configured_rule.php | 9 ++++++++ .../config/configured_rule.php | 9 -------- ....php => NarrowTooWideReturnTypeRector.php} | 22 ++++++++++++++----- src/Config/Level/DeadCodeLevel.php | 4 ++-- 17 files changed, 48 insertions(+), 38 deletions(-) rename rules-tests/DeadCode/Rector/FunctionLike/{TooWideReturnTypeRector => NarrowTooWideReturnTypeRector}/Fixture/arrow_function.php.inc (71%) rename rules-tests/DeadCode/Rector/FunctionLike/{TooWideReturnTypeRector => NarrowTooWideReturnTypeRector}/Fixture/closure.php.inc (66%) rename rules-tests/DeadCode/Rector/FunctionLike/{TooWideReturnTypeRector => NarrowTooWideReturnTypeRector}/Fixture/edge_cases.php.inc (85%) rename rules-tests/DeadCode/Rector/FunctionLike/{TooWideReturnTypeRector => NarrowTooWideReturnTypeRector}/Fixture/final_class.php.inc (87%) rename rules-tests/DeadCode/Rector/FunctionLike/{TooWideReturnTypeRector => NarrowTooWideReturnTypeRector}/Fixture/final_methods.php.inc (81%) rename rules-tests/DeadCode/Rector/FunctionLike/{TooWideReturnTypeRector => NarrowTooWideReturnTypeRector}/Fixture/function.php.inc (66%) rename rules-tests/DeadCode/Rector/FunctionLike/{TooWideReturnTypeRector => NarrowTooWideReturnTypeRector}/Fixture/skip_abstract.php.inc (80%) rename rules-tests/DeadCode/Rector/FunctionLike/{TooWideReturnTypeRector => NarrowTooWideReturnTypeRector}/Fixture/skip_generator.php.inc (76%) rename rules-tests/DeadCode/Rector/FunctionLike/{TooWideReturnTypeRector => NarrowTooWideReturnTypeRector}/Fixture/skip_inheritance.php.inc (84%) rename rules-tests/DeadCode/Rector/FunctionLike/{TooWideReturnTypeRector => NarrowTooWideReturnTypeRector}/Fixture/skip_non_final_classes.php.inc (69%) rename rules-tests/DeadCode/Rector/FunctionLike/{TooWideReturnTypeRector => NarrowTooWideReturnTypeRector}/Fixture/skip_return_all_types.php.inc (73%) rename rules-tests/DeadCode/Rector/FunctionLike/{TooWideReturnTypeRector => NarrowTooWideReturnTypeRector}/Fixture/terminating_methods.php.inc (94%) rename rules-tests/DeadCode/Rector/FunctionLike/{TooWideReturnTypeRector/TooWideReturnTypeRectorTest.php => NarrowTooWideReturnTypeRector/NarrowTooWideReturnTypeRectorTest.php} (77%) create mode 100644 rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/config/configured_rule.php delete mode 100644 rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/config/configured_rule.php rename rules/DeadCode/Rector/FunctionLike/{TooWideReturnTypeRector.php => NarrowTooWideReturnTypeRector.php} (90%) diff --git a/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/arrow_function.php.inc b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/arrow_function.php.inc similarity index 71% rename from rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/arrow_function.php.inc rename to rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/arrow_function.php.inc index ba94304a4ba..1e02f85474c 100644 --- a/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/arrow_function.php.inc +++ b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/arrow_function.php.inc @@ -1,6 +1,6 @@ $x > 5 ? 'high' : 10; @@ -13,7 +13,7 @@ $cast = fn($input): int|string|array => (int) $input; ----- $x > 5 ? 'high' : 10; diff --git a/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/closure.php.inc b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/closure.php.inc similarity index 66% rename from rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/closure.php.inc rename to rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/closure.php.inc index 4ea7bf3c613..1380d360f1e 100644 --- a/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/Fixture/closure.php.inc +++ b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/closure.php.inc @@ -1,6 +1,6 @@ withRules([NarrowTooWideReturnTypeRector::class]); diff --git a/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/config/configured_rule.php b/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/config/configured_rule.php deleted file mode 100644 index 55e53024136..00000000000 --- a/rules-tests/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector/config/configured_rule.php +++ /dev/null @@ -1,9 +0,0 @@ -withRules([TooWideReturnTypeRector::class]); diff --git a/rules/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector.php b/rules/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php similarity index 90% rename from rules/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector.php rename to rules/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php index 880a9a054f7..547a15ee66e 100644 --- a/rules/DeadCode/Rector/FunctionLike/TooWideReturnTypeRector.php +++ b/rules/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php @@ -26,6 +26,7 @@ use Rector\PhpParser\Node\BetterNodeFinder; use Rector\PHPStan\ScopeFetcher; use Rector\PHPStanStaticTypeMapper\Enum\TypeKind; +use Rector\Php\PhpVersionProvider; use Rector\Rector\AbstractRector; use Rector\Reflection\ReflectionResolver; use Rector\StaticTypeMapper\StaticTypeMapper; @@ -33,9 +34,9 @@ use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; /** - * @see \Rector\Tests\DeadCode\Rector\FunctionLike\TooWideReturnTypeRector\TooWideReturnTypeRectorTest + * @see \Rector\Tests\DeadCode\Rector\FunctionLike\NarrowTooWideReturnTypeRector\NarrowTooWideReturnTypeRectorTest */ -final class TooWideReturnTypeRector extends AbstractRector +final class NarrowTooWideReturnTypeRector extends AbstractRector { public function __construct( private readonly BetterNodeFinder $betterNodeFinder, @@ -107,11 +108,16 @@ public function refactor(Node $node): ?Node || $this->terminatedNodeAnalyzer->isAlwaysTerminating($node); if ($returnStatements === [] && ! $node instanceof ArrowFunction) { - $node->returnType = $isAlwaysTerminating - ? new Identifier('never') - : new Identifier('void'); + if ($isAlwaysTerminating) { + if (! $this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::NEVER_TYPE)) { + return null; + } + $node->returnType = new Identifier('never'); + } else { + $node->returnType = new Identifier('void'); + } - return $node; + return null; } $returnType = $node->returnType; @@ -134,6 +140,10 @@ public function refactor(Node $node): ?Node private function shouldSkipNode(ClassMethod|Function_|Closure|ArrowFunction $node, Scope $scope): bool { + if ($node->returnType === null) { + return false; + } + if ($this->hasYield($node)) { return true; } diff --git a/src/Config/Level/DeadCodeLevel.php b/src/Config/Level/DeadCodeLevel.php index 9760484765e..09d97eb94ee 100644 --- a/src/Config/Level/DeadCodeLevel.php +++ b/src/Config/Level/DeadCodeLevel.php @@ -36,7 +36,7 @@ use Rector\DeadCode\Rector\Foreach_\RemoveUnusedForeachKeyRector; use Rector\DeadCode\Rector\FuncCall\RemoveFilterVarOnExactTypeRector; use Rector\DeadCode\Rector\FunctionLike\RemoveDeadReturnRector; -use Rector\DeadCode\Rector\FunctionLike\TooWideReturnTypeRector; +use Rector\DeadCode\Rector\FunctionLike\NarrowTooWideReturnTypeRector; use Rector\DeadCode\Rector\If_\ReduceAlwaysFalseIfOrRector; use Rector\DeadCode\Rector\If_\RemoveAlwaysTrueIfConditionRector; use Rector\DeadCode\Rector\If_\RemoveDeadInstanceOfRector; @@ -127,7 +127,7 @@ final class DeadCodeLevel RemoveParentCallWithoutParentRector::class, RemoveDeadConditionAboveReturnRector::class, RemoveDeadLoopRector::class, - TooWideReturnTypeRector::class, + NarrowTooWideReturnTypeRector::class, // removing methods could be risky if there is some magic loading them RemoveUnusedPromotedPropertyRector::class, From 76541db354f0a489e92e0deb2b7a11d08ee61518 Mon Sep 17 00:00:00 2001 From: Caleb White Date: Fri, 15 Aug 2025 07:44:10 -0500 Subject: [PATCH 03/17] chore: review updates --- .../Fixture/edge_cases.php.inc | 10 ---- .../Fixture/skip_non_final_classes.php.inc | 2 +- .../Fixture/skip_return_all_types.php.inc | 24 ++++++++++ .../NarrowTooWideReturnTypeRector.php | 47 +++++++++---------- src/Config/Level/DeadCodeLevel.php | 4 +- 5 files changed, 50 insertions(+), 37 deletions(-) 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 index 8c08c17a9a1..787dcf956a2 100644 --- a/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/edge_cases.php.inc +++ b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/edge_cases.php.inc @@ -26,11 +26,6 @@ final class EdgeCases echo 'something'; } } - - public function withMixed(): mixed|string|int - { - return 'text'; - } } ?> @@ -63,11 +58,6 @@ final class EdgeCases echo 'something'; } } - - public function withMixed(): mixed - { - return 'text'; - } } ?> 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 index 4802a1572b2..df43fc5a948 100644 --- 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 @@ -2,7 +2,7 @@ namespace Rector\Tests\DeadCode\Rector\FunctionLike\NarrowTooWideReturnTypeRector\Fixture; -class NonFinalClass +class SkipNonFinalClass { public function getData(): string|int|\DateTime { 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 index 3dcbf39b923..04b9d6f1893 100644 --- 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 @@ -16,6 +16,30 @@ final class SkipReturnAllTypes return 1000; } + + public function implicitReturn(): string|null + { + if (rand(0, 1)) { + return 'something'; + } + } + + function foo(): int|null + { + if (rand(0, 1)) { + return 1; + } + + if (rand(0, 1)) { + return 2; + } + + if (rand(0, 1)) { + return 3; + } + + return null; + } } ?> diff --git a/rules/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php b/rules/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php index 547a15ee66e..faa00b3eccc 100644 --- a/rules/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php +++ b/rules/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php @@ -6,23 +6,23 @@ use PhpParser\Node; use PhpParser\Node\ComplexType; +use PhpParser\Node\Expr; use PhpParser\Node\Expr\ArrowFunction; use PhpParser\Node\Expr\Closure; use PhpParser\Node\Expr\Yield_; use PhpParser\Node\Expr\YieldFrom; use PhpParser\Node\Identifier; use PhpParser\Node\Name; +use PhpParser\Node\NullableType; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Function_; use PhpParser\Node\Stmt\Return_; use PhpParser\Node\UnionType; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ClassReflection; -use PHPStan\Type\MixedType; use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use Rector\NodeAnalyzer\TerminatedNodeAnalyzer; use Rector\PhpParser\Node\BetterNodeFinder; use Rector\PHPStan\ScopeFetcher; use Rector\PHPStanStaticTypeMapper\Enum\TypeKind; @@ -30,6 +30,7 @@ use Rector\Rector\AbstractRector; use Rector\Reflection\ReflectionResolver; use Rector\StaticTypeMapper\StaticTypeMapper; +use Rector\TypeDeclaration\TypeInferer\SilentVoidResolver; use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; @@ -42,7 +43,7 @@ public function __construct( private readonly BetterNodeFinder $betterNodeFinder, private readonly StaticTypeMapper $staticTypeMapper, private readonly ReflectionResolver $reflectionResolver, - private readonly TerminatedNodeAnalyzer $terminatedNodeAnalyzer + private readonly SilentVoidResolver $silentVoidResolver, ) { } @@ -103,9 +104,10 @@ public function refactor(Node $node): ?Node return null; } - $returnStatements = $this->betterNodeFinder->findInstanceOf($node, Return_::class); - $isAlwaysTerminating = $node instanceof ArrowFunction - || $this->terminatedNodeAnalyzer->isAlwaysTerminating($node); + $returnStatements = $node instanceof ArrowFunction + ? [] + : $this->betterNodeFinder->findReturnsScoped($node); + $isAlwaysTerminating = ! $this->silentVoidResolver->hasSilentVoid($node); if ($returnStatements === [] && ! $node instanceof ArrowFunction) { if ($isAlwaysTerminating) { @@ -140,7 +142,9 @@ public function refactor(Node $node): ?Node private function shouldSkipNode(ClassMethod|Function_|Closure|ArrowFunction $node, Scope $scope): bool { - if ($node->returnType === null) { + $returnType = $node->returnType; + + if (! $returnType instanceof UnionType && ! $returnType instanceof NullableType) { return false; } @@ -166,7 +170,7 @@ private function shouldSkipNode(ClassMethod|Function_|Closure|ArrowFunction $nod return true; } - return ! $classReflection->isFinal(); + return ! $classReflection->isFinalByKeyword(); } /** @@ -179,7 +183,7 @@ private function collectActualReturnTypes( bool $isAlwaysTerminating, ): array { if ($node instanceof ArrowFunction) { - return [$this->getType($node->expr)]; + return [$this->nodeTypeResolver->getNativeType($node->expr)]; } $returnTypes = []; @@ -189,7 +193,7 @@ private function collectActualReturnTypes( continue; } - $returnTypes[] = $this->getType($returnStatement->expr); + $returnTypes[] = $this->nodeTypeResolver->getNativeType($returnStatement->expr); } if (! $isAlwaysTerminating) { @@ -210,11 +214,10 @@ private function narrowUnionReturnType( $usedTypes = []; foreach ($types as $type) { - $declaredType = $this->getType($type); - if ($declaredType instanceof MixedType) { - // Mixed type covers all other types, so we should only keep mixed - return new Identifier('mixed'); - } + $declaredType = $type instanceof Expr + ? $this->nodeTypeResolver->getNativeType($type) + : $this->getType($type); + foreach ($actualReturnTypes as $actualType) { if (! $declaredType->isSuperTypeOf($actualType)->no()) { $usedTypes[] = $declaredType; @@ -223,6 +226,8 @@ private function narrowUnionReturnType( } } + $usedTypes = array_unique($usedTypes, SORT_REGULAR); + if ($usedTypes === [] || count($usedTypes) === count($types)) { return null; } @@ -239,15 +244,9 @@ private function hasYield(ClassMethod|Function_|Closure|ArrowFunction $node): bo return false; } - $stmts = $node->stmts; - - if ($stmts === null || $stmts === []) { - return false; - } - - return (bool) $this->betterNodeFinder->findFirst( - $stmts, - fn (Node $subNode): bool => $subNode instanceof Yield_ || $subNode instanceof YieldFrom + 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 09d97eb94ee..b8dfcea0b28 100644 --- a/src/Config/Level/DeadCodeLevel.php +++ b/src/Config/Level/DeadCodeLevel.php @@ -35,8 +35,8 @@ use Rector\DeadCode\Rector\For_\RemoveDeadLoopRector; use Rector\DeadCode\Rector\Foreach_\RemoveUnusedForeachKeyRector; use Rector\DeadCode\Rector\FuncCall\RemoveFilterVarOnExactTypeRector; -use Rector\DeadCode\Rector\FunctionLike\RemoveDeadReturnRector; use Rector\DeadCode\Rector\FunctionLike\NarrowTooWideReturnTypeRector; +use Rector\DeadCode\Rector\FunctionLike\RemoveDeadReturnRector; use Rector\DeadCode\Rector\If_\ReduceAlwaysFalseIfOrRector; use Rector\DeadCode\Rector\If_\RemoveAlwaysTrueIfConditionRector; use Rector\DeadCode\Rector\If_\RemoveDeadInstanceOfRector; @@ -127,7 +127,6 @@ final class DeadCodeLevel RemoveParentCallWithoutParentRector::class, RemoveDeadConditionAboveReturnRector::class, RemoveDeadLoopRector::class, - NarrowTooWideReturnTypeRector::class, // removing methods could be risky if there is some magic loading them RemoveUnusedPromotedPropertyRector::class, @@ -143,5 +142,6 @@ final class DeadCodeLevel RemoveDeadReturnRector::class, RemoveArgumentFromDefaultParentCallRector::class, + NarrowTooWideReturnTypeRector::class, ]; } From 144d9e4b3aec33764f190f315c86d03909e2a6b9 Mon Sep 17 00:00:00 2001 From: Caleb White Date: Fri, 15 Aug 2025 08:22:13 -0500 Subject: [PATCH 04/17] chore: add min PHP version to NarrowTooWideReturnTypeRector --- .../config/configured_rule.php | 7 +++++-- .../FunctionLike/NarrowTooWideReturnTypeRector.php | 12 ++++++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/config/configured_rule.php b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/config/configured_rule.php index 35f49f9f17a..bc0b3c54e02 100644 --- a/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/config/configured_rule.php +++ b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/config/configured_rule.php @@ -4,6 +4,9 @@ use Rector\Config\RectorConfig; use Rector\DeadCode\Rector\FunctionLike\NarrowTooWideReturnTypeRector; +use Rector\ValueObject\PhpVersionFeature; -return RectorConfig::configure() - ->withRules([NarrowTooWideReturnTypeRector::class]); +return static function (RectorConfig $rectorConfig): void { + $rectorConfig->rule(NarrowTooWideReturnTypeRector::class); + $rectorConfig->phpVersion(PhpVersionFeature::NEVER_TYPE); +}; diff --git a/rules/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php b/rules/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php index faa00b3eccc..98e2bc3478a 100644 --- a/rules/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php +++ b/rules/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php @@ -23,27 +23,30 @@ use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use Rector\Php\PhpVersionProvider; use Rector\PhpParser\Node\BetterNodeFinder; use Rector\PHPStan\ScopeFetcher; use Rector\PHPStanStaticTypeMapper\Enum\TypeKind; -use Rector\Php\PhpVersionProvider; use Rector\Rector\AbstractRector; use Rector\Reflection\ReflectionResolver; use Rector\StaticTypeMapper\StaticTypeMapper; use Rector\TypeDeclaration\TypeInferer\SilentVoidResolver; +use Rector\ValueObject\PhpVersionFeature; +use Rector\VersionBonding\Contract\MinPhpVersionInterface; use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; /** * @see \Rector\Tests\DeadCode\Rector\FunctionLike\NarrowTooWideReturnTypeRector\NarrowTooWideReturnTypeRectorTest */ -final class NarrowTooWideReturnTypeRector extends AbstractRector +final class NarrowTooWideReturnTypeRector extends AbstractRector implements MinPhpVersionInterface { public function __construct( private readonly BetterNodeFinder $betterNodeFinder, private readonly StaticTypeMapper $staticTypeMapper, private readonly ReflectionResolver $reflectionResolver, private readonly SilentVoidResolver $silentVoidResolver, + private readonly PhpVersionProvider $phpVersionProvider, ) { } @@ -85,6 +88,11 @@ public function foo(): string|int ); } + public function provideMinPhpVersion(): int + { + return PhpVersionFeature::UNION_TYPES; + } + /** * @return array> */ From f592e943126393e5f4a8d0c8f693ca2a9b1a3eb3 Mon Sep 17 00:00:00 2001 From: Caleb White Date: Fri, 15 Aug 2025 10:28:47 -0500 Subject: [PATCH 05/17] chore: move base classes to Source --- .../Fixture/final_inheritance.php.inc | 59 +++++++++++++++++++ ...bstract.php.inc => skip_abstracts.php.inc} | 0 ...inc => skip_non_final_inheritance.php.inc} | 14 ++--- .../Source/SomeAbstractClass.php | 8 +++ .../Source/SomeInterface.php | 8 +++ 5 files changed, 79 insertions(+), 10 deletions(-) create mode 100644 rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/final_inheritance.php.inc rename rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/{skip_abstract.php.inc => skip_abstracts.php.inc} (100%) rename rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/{skip_inheritance.php.inc => skip_non_final_inheritance.php.inc} (61%) create mode 100644 rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Source/SomeAbstractClass.php create mode 100644 rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Source/SomeInterface.php 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/skip_abstract.php.inc b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/skip_abstracts.php.inc similarity index 100% rename from rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/skip_abstract.php.inc rename to rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/skip_abstracts.php.inc diff --git a/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/skip_inheritance.php.inc b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/skip_non_final_inheritance.php.inc similarity index 61% rename from rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/skip_inheritance.php.inc rename to rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/skip_non_final_inheritance.php.inc index 83512abf0c3..d0c024cd6bb 100644 --- a/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/skip_inheritance.php.inc +++ b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/skip_non_final_inheritance.php.inc @@ -1,13 +1,12 @@ Date: Fri, 15 Aug 2025 10:47:13 -0500 Subject: [PATCH 06/17] fix: add nullable edge case --- .../Fixture/edge_cases.php.inc | 10 ++++++++++ .../FunctionLike/NarrowTooWideReturnTypeRector.php | 12 ++++++------ 2 files changed, 16 insertions(+), 6 deletions(-) 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 index 787dcf956a2..0823d73c9d4 100644 --- a/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/edge_cases.php.inc +++ b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/edge_cases.php.inc @@ -26,6 +26,11 @@ final class EdgeCases echo 'something'; } } + + public function nullableReturn(): ?string + { + return 'something'; + } } ?> @@ -58,6 +63,11 @@ final class EdgeCases echo 'something'; } } + + public function nullableReturn(): string + { + return 'something'; + } } ?> diff --git a/rules/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php b/rules/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php index 98e2bc3478a..e02df590eda 100644 --- a/rules/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php +++ b/rules/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php @@ -35,6 +35,7 @@ use Rector\VersionBonding\Contract\MinPhpVersionInterface; use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; +use Webmozart\Assert\Assert; /** * @see \Rector\Tests\DeadCode\Rector\FunctionLike\NarrowTooWideReturnTypeRector\NarrowTooWideReturnTypeRectorTest @@ -131,10 +132,7 @@ public function refactor(Node $node): ?Node } $returnType = $node->returnType; - - if (! $returnType instanceof UnionType) { - return null; - } + Assert::isInstanceOfAny($returnType, [UnionType::class, NullableType::class]); $actualReturnTypes = $this->collectActualReturnTypes($node, $returnStatements, $isAlwaysTerminating); $newReturnType = $this->narrowUnionReturnType($returnType, $actualReturnTypes); @@ -215,10 +213,12 @@ private function collectActualReturnTypes( * @param Type[] $actualReturnTypes */ private function narrowUnionReturnType( - UnionType $unionType, + UnionType|NullableType $returnType, array $actualReturnTypes ): ComplexType|Identifier|Name|null { - $types = $unionType->types; + $types = $returnType instanceof UnionType + ? $returnType->types + : [$returnType->type, new Identifier('null')]; $usedTypes = []; foreach ($types as $type) { From dfd1cbca9e1c848a4ee20e6be34310ee65b7de3b Mon Sep 17 00:00:00 2001 From: Caleb White Date: Fri, 15 Aug 2025 11:03:24 -0500 Subject: [PATCH 07/17] chore: remove never/void handling from NarrowTooWideReturnTypeRector --- .../Fixture/edge_cases.php.inc | 14 ------- .../Fixture/terminating_methods.php.inc | 38 ------------------- .../config/configured_rule.php | 2 +- .../NarrowTooWideReturnTypeRector.php | 13 +------ 4 files changed, 2 insertions(+), 65 deletions(-) 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 index 0823d73c9d4..d0ba4dad7c7 100644 --- a/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/edge_cases.php.inc +++ b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/edge_cases.php.inc @@ -20,13 +20,6 @@ final class EdgeCases } } - public function noReturn(): string|int|null - { - if (rand(0, 1)) { - echo 'something'; - } - } - public function nullableReturn(): ?string { return 'something'; @@ -57,13 +50,6 @@ final class EdgeCases } } - public function noReturn(): void - { - if (rand(0, 1)) { - echo 'something'; - } - } - public function nullableReturn(): string { return 'something'; 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 index 6b6884c6f90..4a24bc1dacd 100644 --- a/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/terminating_methods.php.inc +++ b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/terminating_methods.php.inc @@ -13,15 +13,6 @@ final class TerminatingMethods } } - public function alwaysThrows(): string|int|null - { - if (rand(0, 1)) { - throw new \Exception('error'); - } else { - throw new \RuntimeException('runtime error'); - } - } - public function mixedTerminating(): string|int|null { if (rand(0, 1)) { @@ -57,16 +48,6 @@ final class TerminatingMethods } } - public function alwaysExits(): string|int|null - { - exit(1); - } - - public function noReturnStatements(): string|int - { - echo 'something'; - } - public function hasReturnButAlsoThrows(): string|int|null { if (rand(0, 1)) { @@ -94,15 +75,6 @@ final class TerminatingMethods } } - public function alwaysThrows(): never - { - if (rand(0, 1)) { - throw new \Exception('error'); - } else { - throw new \RuntimeException('runtime error'); - } - } - public function mixedTerminating(): string { if (rand(0, 1)) { @@ -138,16 +110,6 @@ final class TerminatingMethods } } - public function alwaysExits(): never - { - exit(1); - } - - public function noReturnStatements(): void - { - echo 'something'; - } - public function hasReturnButAlsoThrows(): string { if (rand(0, 1)) { diff --git a/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/config/configured_rule.php b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/config/configured_rule.php index bc0b3c54e02..a96a0f92e6b 100644 --- a/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/config/configured_rule.php +++ b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/config/configured_rule.php @@ -8,5 +8,5 @@ return static function (RectorConfig $rectorConfig): void { $rectorConfig->rule(NarrowTooWideReturnTypeRector::class); - $rectorConfig->phpVersion(PhpVersionFeature::NEVER_TYPE); + $rectorConfig->phpVersion(PhpVersionFeature::UNION_TYPES); }; diff --git a/rules/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php b/rules/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php index e02df590eda..ac6ec891b09 100644 --- a/rules/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php +++ b/rules/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php @@ -23,7 +23,6 @@ use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use Rector\Php\PhpVersionProvider; use Rector\PhpParser\Node\BetterNodeFinder; use Rector\PHPStan\ScopeFetcher; use Rector\PHPStanStaticTypeMapper\Enum\TypeKind; @@ -47,7 +46,6 @@ public function __construct( private readonly StaticTypeMapper $staticTypeMapper, private readonly ReflectionResolver $reflectionResolver, private readonly SilentVoidResolver $silentVoidResolver, - private readonly PhpVersionProvider $phpVersionProvider, ) { } @@ -91,7 +89,7 @@ public function foo(): string|int public function provideMinPhpVersion(): int { - return PhpVersionFeature::UNION_TYPES; + return PhpVersionFeature::NULLABLE_TYPE; } /** @@ -119,15 +117,6 @@ public function refactor(Node $node): ?Node $isAlwaysTerminating = ! $this->silentVoidResolver->hasSilentVoid($node); if ($returnStatements === [] && ! $node instanceof ArrowFunction) { - if ($isAlwaysTerminating) { - if (! $this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::NEVER_TYPE)) { - return null; - } - $node->returnType = new Identifier('never'); - } else { - $node->returnType = new Identifier('void'); - } - return null; } From df132268d55af165ea65fd2d99d09cdf7657b706 Mon Sep 17 00:00:00 2001 From: Caleb White Date: Fri, 15 Aug 2025 11:16:20 -0500 Subject: [PATCH 08/17] chore: fix logic bug --- .../Rector/FunctionLike/NarrowTooWideReturnTypeRector.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rules/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php b/rules/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php index ac6ec891b09..8aac1b5b453 100644 --- a/rules/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php +++ b/rules/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php @@ -140,7 +140,7 @@ private function shouldSkipNode(ClassMethod|Function_|Closure|ArrowFunction $nod $returnType = $node->returnType; if (! $returnType instanceof UnionType && ! $returnType instanceof NullableType) { - return false; + return true; } if ($this->hasYield($node)) { From 1b00789c736515ef8f48e339bcae5fe019d22857 Mon Sep 17 00:00:00 2001 From: Caleb White Date: Fri, 15 Aug 2025 11:22:54 -0500 Subject: [PATCH 09/17] chore: early return if not class --- .../Rector/FunctionLike/NarrowTooWideReturnTypeRector.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rules/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php b/rules/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php index 8aac1b5b453..243a0876d37 100644 --- a/rules/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php +++ b/rules/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php @@ -165,6 +165,10 @@ private function shouldSkipNode(ClassMethod|Function_|Closure|ArrowFunction $nod return true; } + if (! $classReflection->isClass()) { + return true; + } + return ! $classReflection->isFinalByKeyword(); } From 7e87b263a5e5e72bb6007402ded88775f24de995 Mon Sep 17 00:00:00 2001 From: Caleb White Date: Fri, 15 Aug 2025 11:28:55 -0500 Subject: [PATCH 10/17] chore: revert changes to TerminatedNodeAnalyzer --- src/NodeAnalyzer/TerminatedNodeAnalyzer.php | 45 ++------------------- 1 file changed, 3 insertions(+), 42 deletions(-) diff --git a/src/NodeAnalyzer/TerminatedNodeAnalyzer.php b/src/NodeAnalyzer/TerminatedNodeAnalyzer.php index d1a3734d112..1a0ec572129 100644 --- a/src/NodeAnalyzer/TerminatedNodeAnalyzer.php +++ b/src/NodeAnalyzer/TerminatedNodeAnalyzer.php @@ -45,29 +45,6 @@ final class TerminatedNodeAnalyzer */ private const ALLOWED_CONTINUE_CURRENT_STMTS = [InlineHTML::class, Nop::class]; - public function isAlwaysTerminating(StmtsAwareInterface $stmtsAware): bool - { - $stmts = $stmtsAware->stmts; - - if ($stmts === null || $stmts === []) { - return false; - } - - foreach ($stmts as $key => $stmt) { - if (! isset($stmts[$key - 1])) { - continue; - } - - $previousStmt = $stmts[$key - 1]; - - if ($this->isAlwaysTerminated($stmtsAware, $previousStmt, $stmt)) { - return true; - } - } - - return $this->isTerminatedInLastStmts($stmts, $stmts[array_key_last($stmts)]); - } - public function isAlwaysTerminated(StmtsAwareInterface $stmtsAware, Stmt $node, Stmt $currentStmt): bool { if (in_array($currentStmt::class, self::ALLOWED_CONTINUE_CURRENT_STMTS, true)) { @@ -180,7 +157,7 @@ private function isTerminatedInLastStmtsIf(If_ $if, Stmt $stmt): bool /** * @param Stmt[] $stmts */ - private function isTerminatedInLastStmts(array $stmts, Stmt $stmt): bool + private function isTerminatedInLastStmts(array $stmts, Node $node): bool { if ($stmts === []) { return false; @@ -189,7 +166,7 @@ private function isTerminatedInLastStmts(array $stmts, Stmt $stmt): bool $lastKey = array_key_last($stmts); $lastNode = $stmts[$lastKey]; - if (isset($stmts[$lastKey - 1]) && ! $this->isTerminatedNode($stmts[$lastKey - 1], $stmt)) { + if (isset($stmts[$lastKey - 1]) && ! $this->isTerminatedNode($stmts[$lastKey - 1], $node)) { return false; } @@ -197,22 +174,6 @@ private function isTerminatedInLastStmts(array $stmts, Stmt $stmt): bool return $lastNode->expr instanceof Exit_ || $lastNode->expr instanceof Throw_; } - if ($lastNode instanceof Return_) { - return true; - } - - if ($lastNode instanceof If_) { - return $this->isTerminatedInLastStmtsIf($lastNode, $stmt); - } - - if ($lastNode instanceof TryCatch) { - return $this->isTerminatedInLastStmtsTryCatch($lastNode, $stmt); - } - - if ($lastNode instanceof Switch_) { - return $this->isTerminatedInLastStmtsSwitch($lastNode, $stmt); - } - - return false; + return $lastNode instanceof Return_; } } From 7558829e57e1f581037ea058fb532a3d2884f767 Mon Sep 17 00:00:00 2001 From: Caleb White Date: Fri, 15 Aug 2025 20:12:57 -0500 Subject: [PATCH 11/17] test: skip unknown parameter --- .../Fixture/skip_unknown_parameter.php.inc | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/skip_unknown_parameter.php.inc 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 @@ + From 6837fd47f5e101bb0d04c14ca9fd3c0744541001 Mon Sep 17 00:00:00 2001 From: Caleb White Date: Sat, 16 Aug 2025 15:48:24 -0500 Subject: [PATCH 12/17] feat: add support for phpdoc return types --- .../Fixture/final_class.php.inc | 4 +- .../Fixture/phpdocs.php.inc | 83 +++++++++++++++++++ .../NarrowTooWideReturnTypeRector.php | 59 +++++++------ 3 files changed, 117 insertions(+), 29 deletions(-) create mode 100644 rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/phpdocs.php.inc 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 index 3f5d3b11142..0afdfd50865 100644 --- a/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/final_class.php.inc +++ b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/final_class.php.inc @@ -2,7 +2,7 @@ namespace Rector\Tests\DeadCode\Rector\FunctionLike\NarrowTooWideReturnTypeRector\Fixture; -final class ClassMethodCase +final class FinalClass { public function getData(): string|int|\DateTime { @@ -46,7 +46,7 @@ final class ClassMethodCase namespace Rector\Tests\DeadCode\Rector\FunctionLike\NarrowTooWideReturnTypeRector\Fixture; -final class ClassMethodCase +final class FinalClass { public function getData(): string|int { 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..bedaa9f436d --- /dev/null +++ b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/phpdocs.php.inc @@ -0,0 +1,83 @@ + $class + * @return class-string|int + */ + public function bar($class): string|int + { + return $class; + } + + /** @return class-string|int */ + public function baz($class): string|int + { + return SomeInterface::class; + } + + /** @return \Iterator|string */ + function qux(): \Iterator|string + { + return new \ArrayIterator([1]); + } +} + +?> +----- + $class + * @return class-string + */ + public function bar($class): string + { + return $class; + } + + /** @return class-string */ + public function baz($class): string + { + return SomeInterface::class; + } + + /** @return \Iterator */ + function qux(): \Iterator + { + return new \ArrayIterator([1]); + } +} + +?> diff --git a/rules/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php b/rules/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php index 243a0876d37..3915689f0d4 100644 --- a/rules/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php +++ b/rules/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php @@ -5,14 +5,10 @@ namespace Rector\DeadCode\Rector\FunctionLike; use PhpParser\Node; -use PhpParser\Node\ComplexType; -use PhpParser\Node\Expr; use PhpParser\Node\Expr\ArrowFunction; use PhpParser\Node\Expr\Closure; use PhpParser\Node\Expr\Yield_; use PhpParser\Node\Expr\YieldFrom; -use PhpParser\Node\Identifier; -use PhpParser\Node\Name; use PhpParser\Node\NullableType; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Function_; @@ -23,6 +19,10 @@ use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\UnionType as PHPStanUnionType; +use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo; +use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory; +use Rector\BetterPhpDocParser\PhpDocManipulator\PhpDocTypeChanger; use Rector\PhpParser\Node\BetterNodeFinder; use Rector\PHPStan\ScopeFetcher; use Rector\PHPStanStaticTypeMapper\Enum\TypeKind; @@ -46,6 +46,8 @@ public function __construct( private readonly StaticTypeMapper $staticTypeMapper, private readonly ReflectionResolver $reflectionResolver, private readonly SilentVoidResolver $silentVoidResolver, + private readonly PhpDocTypeChanger $phpDocTypeChanger, + private readonly PhpDocInfoFactory $phpDocInfoFactory, ) { } @@ -120,17 +122,31 @@ public function refactor(Node $node): ?Node return null; } - $returnType = $node->returnType; - Assert::isInstanceOfAny($returnType, [UnionType::class, NullableType::class]); + $phpDocInfo = $this->phpDocInfoFactory->createFromNode($node); + + if ($phpDocInfo?->hasByName('@return')) { + $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->narrowUnionReturnType($returnType, $actualReturnTypes); + $newReturnType = $this->narrowReturnType($returnType, $actualReturnTypes); if ($newReturnType === null) { return null; } - $node->returnType = $newReturnType; + $node->returnType = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode( + $newReturnType, + TypeKind::RETURN + ); + + if ($phpDocInfo instanceof PhpDocInfo) { + $this->phpDocTypeChanger->changeReturnType($node, $phpDocInfo, $newReturnType); + } return $node; } @@ -182,7 +198,7 @@ private function collectActualReturnTypes( bool $isAlwaysTerminating, ): array { if ($node instanceof ArrowFunction) { - return [$this->nodeTypeResolver->getNativeType($node->expr)]; + return [$this->getType($node->expr)]; } $returnTypes = []; @@ -192,7 +208,7 @@ private function collectActualReturnTypes( continue; } - $returnTypes[] = $this->nodeTypeResolver->getNativeType($returnStatement->expr); + $returnTypes[] = $this->getType($returnStatement->expr); } if (! $isAlwaysTerminating) { @@ -205,23 +221,15 @@ private function collectActualReturnTypes( /** * @param Type[] $actualReturnTypes */ - private function narrowUnionReturnType( - UnionType|NullableType $returnType, - array $actualReturnTypes - ): ComplexType|Identifier|Name|null { - $types = $returnType instanceof UnionType - ? $returnType->types - : [$returnType->type, new Identifier('null')]; + private function narrowReturnType(Type $returnType, array $actualReturnTypes): Type|null + { + $types = $returnType instanceof PHPStanUnionType ? $returnType->getTypes() : [$returnType]; $usedTypes = []; foreach ($types as $type) { - $declaredType = $type instanceof Expr - ? $this->nodeTypeResolver->getNativeType($type) - : $this->getType($type); - foreach ($actualReturnTypes as $actualType) { - if (! $declaredType->isSuperTypeOf($actualType)->no()) { - $usedTypes[] = $declaredType; + if (! $type->isSuperTypeOf($actualType)->no()) { + $usedTypes[] = $type; break; } } @@ -233,10 +241,7 @@ private function narrowUnionReturnType( return null; } - return $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode( - TypeCombinator::union(...$usedTypes), - TypeKind::RETURN - ); + return TypeCombinator::union(...$usedTypes); } private function hasYield(ClassMethod|Function_|Closure|ArrowFunction $node): bool From cc5f1545f93e1b8e1417e6d63f98db96150a00eb Mon Sep 17 00:00:00 2001 From: Caleb White Date: Sat, 16 Aug 2025 20:10:12 -0500 Subject: [PATCH 13/17] fix: don't add return type to phpdoc if it doesn't exist --- .../Fixture/phpdocs.php.inc | 16 ++++++++++++++++ .../NarrowTooWideReturnTypeRector.php | 5 +++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/phpdocs.php.inc b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/phpdocs.php.inc index bedaa9f436d..69e8187754b 100644 --- a/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/phpdocs.php.inc +++ b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/phpdocs.php.inc @@ -36,6 +36,14 @@ final class PhpDocs { return new \ArrayIterator([1]); } + + /** + * @param int $a + */ + function quux($a): int|string + { + return $a; + } } ?> @@ -78,6 +86,14 @@ final class PhpDocs { return new \ArrayIterator([1]); } + + /** + * @param int $a + */ + function quux($a): int + { + return $a; + } } ?> diff --git a/rules/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php b/rules/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php index 3915689f0d4..938f72260c7 100644 --- a/rules/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php +++ b/rules/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php @@ -123,8 +123,9 @@ public function refactor(Node $node): ?Node } $phpDocInfo = $this->phpDocInfoFactory->createFromNode($node); + $hasReturnDocblock = (bool) $phpDocInfo?->hasByName('@return'); - if ($phpDocInfo?->hasByName('@return')) { + if ($hasReturnDocblock) { $returnType = $phpDocInfo->getReturnType(); } else { $returnType = $node->returnType; @@ -144,7 +145,7 @@ public function refactor(Node $node): ?Node TypeKind::RETURN ); - if ($phpDocInfo instanceof PhpDocInfo) { + if ($hasReturnDocblock) { $this->phpDocTypeChanger->changeReturnType($node, $phpDocInfo, $newReturnType); } From 193e1d98317cfa04c054ac5eef32a6879f62675c Mon Sep 17 00:00:00 2001 From: Caleb White Date: Sat, 16 Aug 2025 20:17:34 -0500 Subject: [PATCH 14/17] tests: add requested fixture --- .../Fixture/phpdocs.php.inc | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/phpdocs.php.inc b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/phpdocs.php.inc index 69e8187754b..8a48f447515 100644 --- a/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/phpdocs.php.inc +++ b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/phpdocs.php.inc @@ -44,6 +44,15 @@ final class PhpDocs { return $a; } + + /** + * @param int $a + * @return int|string + */ + function mixedReturn($a): int|string + { + return $a; + } } ?> @@ -94,6 +103,15 @@ final class PhpDocs { return $a; } + + /** + * @param int $a + * @return int + */ + function mixedReturn($a): int + { + return $a; + } } ?> From 81468f30bcb0d7e549cf987e0a766e38f0c607ba Mon Sep 17 00:00:00 2001 From: Caleb White Date: Sat, 16 Aug 2025 20:21:49 -0500 Subject: [PATCH 15/17] tests: add test to remove unused phpdoc generic --- .../Fixture/phpdocs.php.inc | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/phpdocs.php.inc b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/phpdocs.php.inc index 8a48f447515..59aaeb38f65 100644 --- a/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/phpdocs.php.inc +++ b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/phpdocs.php.inc @@ -37,6 +37,12 @@ final class PhpDocs return new \ArrayIterator([1]); } + /** @return \Iterator|string */ + function qax(): \Iterator|string + { + return 'text'; + } + /** * @param int $a */ @@ -96,6 +102,12 @@ final class PhpDocs return new \ArrayIterator([1]); } + /** @return string */ + function qax(): string + { + return 'text'; + } + /** * @param int $a */ From c16f49af91f4a8026c9d47d0fcd9a49ad36b2f03 Mon Sep 17 00:00:00 2001 From: Caleb White Date: Sat, 16 Aug 2025 20:26:47 -0500 Subject: [PATCH 16/17] chore: update fixtures --- .../NarrowTooWideReturnTypeRector/Fixture/phpdocs.php.inc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/phpdocs.php.inc b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/phpdocs.php.inc index 59aaeb38f65..9a852f64f50 100644 --- a/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/phpdocs.php.inc +++ b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/phpdocs.php.inc @@ -20,13 +20,13 @@ final class PhpDocs * @param class-string $class * @return class-string|int */ - public function bar($class): string|int + public function bar(string $class): string|int { return $class; } /** @return class-string|int */ - public function baz($class): string|int + public function baz(string $class): string|int { return SomeInterface::class; } @@ -85,13 +85,13 @@ final class PhpDocs * @param class-string $class * @return class-string */ - public function bar($class): string + public function bar(string $class): string { return $class; } /** @return class-string */ - public function baz($class): string + public function baz(string $class): string { return SomeInterface::class; } From a9e401f4c0e5b7ca52d61f5e0afb22bdc8718934 Mon Sep 17 00:00:00 2001 From: Caleb White Date: Sat, 16 Aug 2025 20:45:11 -0500 Subject: [PATCH 17/17] chore: skip function likes without parameter types --- .../Fixture/arrow_function.php.inc | 12 ++++++------ .../Fixture/phpdocs.php.inc | 8 ++++---- ...nction_likes_without_parameter_types.php.inc | 17 +++++++++++++++++ .../NarrowTooWideReturnTypeRector.php | 6 ++++++ 4 files changed, 33 insertions(+), 10 deletions(-) create mode 100644 rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/skip_function_likes_without_parameter_types.php.inc 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 index 1e02f85474c..d81eef00908 100644 --- a/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/arrow_function.php.inc +++ b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/arrow_function.php.inc @@ -2,12 +2,12 @@ namespace Rector\Tests\DeadCode\Rector\FunctionLike\NarrowTooWideReturnTypeRector\Fixture; -$simple = fn($x): string|int|bool => $x > 5 ? 'high' : 10; +$simple = fn(int $x): string|int|bool => $x > 5 ? 'high' : 10; -$ternary = fn($value): string|int|float|null => +$ternary = fn(?int $value): string|int|float|null => $value === null ? null : ($value > 0 ? 'positive' : -1); -$cast = fn($input): int|string|array => (int) $input; +$cast = fn(mixed $input): int|string|array => (int) $input; ?> ----- @@ -15,11 +15,11 @@ $cast = fn($input): int|string|array => (int) $input; namespace Rector\Tests\DeadCode\Rector\FunctionLike\NarrowTooWideReturnTypeRector\Fixture; -$simple = fn($x): string|int => $x > 5 ? 'high' : 10; +$simple = fn(int $x): string|int => $x > 5 ? 'high' : 10; -$ternary = fn($value): string|int|null => +$ternary = fn(?int $value): string|int|null => $value === null ? null : ($value > 0 ? 'positive' : -1); -$cast = fn($input): int => (int) $input; +$cast = fn(mixed $input): int => (int) $input; ?> diff --git a/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/phpdocs.php.inc b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/phpdocs.php.inc index 9a852f64f50..40d92c06dd3 100644 --- a/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/phpdocs.php.inc +++ b/rules-tests/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector/Fixture/phpdocs.php.inc @@ -46,7 +46,7 @@ final class PhpDocs /** * @param int $a */ - function quux($a): int|string + function quux(int $a): int|string { return $a; } @@ -55,7 +55,7 @@ final class PhpDocs * @param int $a * @return int|string */ - function mixedReturn($a): int|string + function mixedReturn(int $a): int|string { return $a; } @@ -111,7 +111,7 @@ final class PhpDocs /** * @param int $a */ - function quux($a): int + function quux(int $a): int { return $a; } @@ -120,7 +120,7 @@ final class PhpDocs * @param int $a * @return int */ - function mixedReturn($a): int + function mixedReturn(int $a): int { return $a; } 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/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php b/rules/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php index 938f72260c7..526674e472f 100644 --- a/rules/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php +++ b/rules/DeadCode/Rector/FunctionLike/NarrowTooWideReturnTypeRector.php @@ -164,6 +164,12 @@ private function shouldSkipNode(ClassMethod|Function_|Closure|ArrowFunction $nod return true; } + foreach ($node->params as $param) { + if (! $param->type instanceof Node) { + return true; + } + } + if (! $node instanceof ClassMethod) { return false; }