From 2623de7205615b2fd601863435082ae548215e16 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sat, 12 Sep 2026 11:33:36 +0200 Subject: [PATCH] [Php84] [TypeDeclaration] Extract duplicated rule logic into shared services --- .../ForeachToArrayAnyAllFactory.php | 335 ++++++++++++++++++ .../NodeFactory/ForeachToArrayFindFactory.php | 177 +++++++++ .../Foreach_/ForeachToArrayAllRector.php | 268 +------------- .../Foreach_/ForeachToArrayAnyRector.php | 269 +------------- .../Foreach_/ForeachToArrayFindKeyRector.php | 146 +------- .../Foreach_/ForeachToArrayFindRector.php | 142 +------- .../ArrayCallbackParamTypeResolver.php | 126 +++++++ .../SetUpAssignedPropertyTyper.php | 157 ++++++++ ...pedPropertyFromContainerGetSetUpRector.php | 131 +------ ...edPropertyFromGetRepositorySetUpRector.php | 132 +------ .../AddArrayAnyAllClosureParamTypeRector.php | 85 +---- ...rrowArrayAnyAllNullableParamTypeRector.php | 87 +---- 12 files changed, 825 insertions(+), 1230 deletions(-) create mode 100644 rules/Php84/NodeFactory/ForeachToArrayAnyAllFactory.php create mode 100644 rules/Php84/NodeFactory/ForeachToArrayFindFactory.php create mode 100644 rules/TypeDeclaration/NodeAnalyzer/ArrayCallbackParamTypeResolver.php create mode 100644 rules/TypeDeclaration/NodeAnalyzer/SetUpAssignedPropertyTyper.php diff --git a/rules/Php84/NodeFactory/ForeachToArrayAnyAllFactory.php b/rules/Php84/NodeFactory/ForeachToArrayAnyAllFactory.php new file mode 100644 index 00000000000..bc5c2206beb --- /dev/null +++ b/rules/Php84/NodeFactory/ForeachToArrayAnyAllFactory.php @@ -0,0 +1,335 @@ +refactorBooleanAssignmentPattern($node, $functionName, $negateCondition, $initialBool, $rejectElseBranches) + ?? $this->refactorEarlyReturnPattern($node, $functionName, $negateCondition, $initialBool, $rejectElseBranches); + } + + /** + * @param StmtsAware $node + */ + private function refactorBooleanAssignmentPattern( + Node $node, + string $functionName, + bool $negateCondition, + bool $initialBool, + bool $rejectElseBranches + ): ?Node { + if ($node->stmts === null) { + return null; + } + + foreach ($node->stmts as $key => $stmt) { + if (! $stmt instanceof Foreach_) { + continue; + } + + $prevStmt = $node->stmts[$key - 1] ?? null; + if (! $prevStmt instanceof Expression) { + continue; + } + + if (! $prevStmt->expr instanceof Assign) { + continue; + } + + $foreach = $stmt; + $prevAssign = $prevStmt->expr; + + if (! $this->isExpectedBool($prevAssign->expr, $initialBool)) { + continue; + } + + if (! $prevAssign->var instanceof Variable) { + continue; + } + + $assignedVariable = $prevAssign->var; + + if (! $this->isValidBooleanAssignmentForeachStructure($foreach, $assignedVariable, $initialBool, $rejectElseBranches)) { + continue; + } + + if ($this->stmtsManipulator->isVariableUsedInNextStmt( + $node, + $key + 1, + (string) $this->nodeNameResolver->getName($foreach->valueVar) + )) { + continue; + } + + /** @var If_ $firstNodeInsideForeach */ + $firstNodeInsideForeach = $foreach->stmts[0]; + + $condition = $firstNodeInsideForeach->cond; + $valueParam = $foreach->valueVar; + + if (! $valueParam instanceof Variable) { + continue; + } + + $params = [new Param($valueParam)]; + + if ($foreach->keyVar instanceof Variable && $this->foreachKeyUsedInConditionalAnalyzer->isUsed( + $foreach->keyVar, + $condition + )) { + $params[] = new Param(new Variable((string) $this->nodeNameResolver->getName($foreach->keyVar))); + } + + $arrowFunction = new ArrowFunction([ + 'params' => $params, + 'expr' => $this->applyNegation($condition, $negateCondition), + ]); + + $funcCall = $this->nodeFactory->createFuncCall($functionName, [$foreach->expr, $arrowFunction]); + + $newAssign = new Assign($assignedVariable, $funcCall); + $newExpression = new Expression($newAssign); + + unset($node->stmts[$key - 1]); + $node->stmts[$key] = $newExpression; + + $node->stmts = array_values($node->stmts); + + return $node; + } + + return null; + } + + /** + * @param StmtsAware $node + */ + private function refactorEarlyReturnPattern( + Node $node, + string $functionName, + bool $negateCondition, + bool $initialBool, + bool $rejectElseBranches + ): ?Node { + if ($node->stmts === null) { + return null; + } + + foreach ($node->stmts as $key => $stmt) { + if (! $stmt instanceof Foreach_) { + continue; + } + + $foreach = $stmt; + $nextStmt = $node->stmts[$key + 1] ?? null; + + if (! $nextStmt instanceof Return_) { + continue; + } + + if (! $nextStmt->expr instanceof Expr) { + continue; + } + + if (! $this->isExpectedBool($nextStmt->expr, $initialBool)) { + continue; + } + + if (! $this->isValidEarlyReturnForeachStructure($foreach, $initialBool, $rejectElseBranches)) { + continue; + } + + /** @var If_ $firstNodeInsideForeach */ + $firstNodeInsideForeach = $foreach->stmts[0]; + $condition = $firstNodeInsideForeach->cond; + + $params = []; + + if ($foreach->valueVar instanceof Variable) { + $params[] = new Param($foreach->valueVar); + } + + if ( + $foreach->keyVar instanceof Variable && + $this->foreachKeyUsedInConditionalAnalyzer->isUsed($foreach->keyVar, $condition) + ) { + $params[] = new Param(new Variable((string) $this->nodeNameResolver->getName($foreach->keyVar))); + } + + $arrowFunction = new ArrowFunction([ + 'params' => $params, + 'expr' => $this->applyNegation($condition, $negateCondition), + ]); + + $funcCall = $this->nodeFactory->createFuncCall($functionName, [$foreach->expr, $arrowFunction]); + + $node->stmts[$key] = new Return_($funcCall); + unset($node->stmts[$key + 1]); + $node->stmts = array_values($node->stmts); + + return $node; + } + + return null; + } + + private function isValidBooleanAssignmentForeachStructure( + Foreach_ $foreach, + Variable $assignedVariable, + bool $initialBool, + bool $rejectElseBranches + ): bool { + if (count($foreach->stmts) !== 1) { + return false; + } + + $firstStmt = $foreach->stmts[0]; + if ( + ! $firstStmt instanceof If_ || + count($firstStmt->stmts) !== 2 + ) { + return false; + } + + if ($rejectElseBranches && ($firstStmt->elseifs !== [] || $firstStmt->else instanceof Else_)) { + return false; + } + + $assignmentStmt = $firstStmt->stmts[0]; + $breakStmt = $firstStmt->stmts[1]; + + if ( + ! $assignmentStmt instanceof Expression || + ! $assignmentStmt->expr instanceof Assign || + ! $breakStmt instanceof Break_ + ) { + return false; + } + + $assignment = $assignmentStmt->expr; + + if (! $this->nodeComparator->areNodesEqual($assignment->var, $assignedVariable)) { + return false; + } + + if (! $this->isExpectedBool($assignment->expr, ! $initialBool)) { + return false; + } + + $type = $this->nodeTypeResolver->getNativeType($foreach->expr); + return $type->isArray() + ->yes(); + } + + private function isValidEarlyReturnForeachStructure( + Foreach_ $foreach, + bool $initialBool, + bool $rejectElseBranches + ): bool { + if (count($foreach->stmts) !== 1) { + return false; + } + + if (! $foreach->stmts[0] instanceof If_) { + return false; + } + + $ifStmt = $foreach->stmts[0]; + + if ($rejectElseBranches && ($ifStmt->elseifs !== [] || $ifStmt->else instanceof Else_)) { + return false; + } + + if (count($ifStmt->stmts) !== 1) { + return false; + } + + if (! $ifStmt->stmts[0] instanceof Return_) { + return false; + } + + $returnStmt = $ifStmt->stmts[0]; + + if (! $returnStmt->expr instanceof Expr) { + return false; + } + + if (! $this->isExpectedBool($returnStmt->expr, ! $initialBool)) { + return false; + } + + if (! $foreach->valueVar instanceof Variable) { + return false; + } + + $type = $this->nodeTypeResolver->getNativeType($foreach->expr); + + return $type->isArray() + ->yes(); + } + + private function isExpectedBool(Expr $expr, bool $expected): bool + { + if ($expected) { + return $this->valueResolver->isTrue($expr); + } + + return $this->valueResolver->isFalse($expr); + } + + private function applyNegation(Expr $expr, bool $negate): Expr + { + if (! $negate) { + return $expr; + } + + return $expr instanceof BooleanNot ? $expr->expr : new BooleanNot($expr); + } +} diff --git a/rules/Php84/NodeFactory/ForeachToArrayFindFactory.php b/rules/Php84/NodeFactory/ForeachToArrayFindFactory.php new file mode 100644 index 00000000000..25340c3d82c --- /dev/null +++ b/rules/Php84/NodeFactory/ForeachToArrayFindFactory.php @@ -0,0 +1,177 @@ +stmts === null) { + return null; + } + + foreach ($node->stmts as $key => $stmt) { + if (! $stmt instanceof Foreach_) { + continue; + } + + $prevStmt = $node->stmts[$key - 1] ?? null; + if (! $prevStmt instanceof Expression) { + continue; + } + + if (! $prevStmt->expr instanceof Assign) { + continue; + } + + $foreach = $stmt; + $prevAssign = $prevStmt->expr; + + if (! $this->valueResolver->isNull($prevAssign->expr)) { + continue; + } + + if (! $prevAssign->var instanceof Variable) { + continue; + } + + $assignedVariable = $prevAssign->var; + + if (! $this->isValidForeachStructure($foreach, $assignedVariable, $compareKey)) { + continue; + } + + if ($this->stmtsManipulator->isVariableUsedInNextStmt( + $node, + $key + 1, + (string) $this->nodeNameResolver->getName($foreach->valueVar) + )) { + continue; + } + + /** @var If_ $firstNodeInsideForeach */ + $firstNodeInsideForeach = $foreach->stmts[0]; + + $condition = $firstNodeInsideForeach->cond; + $valueParam = $foreach->valueVar; + + if (! $valueParam instanceof Variable) { + continue; + } + + $params = [new Param($valueParam)]; + + if ($foreach->keyVar instanceof Variable && $this->foreachKeyUsedInConditionalAnalyzer->isUsed( + $foreach->keyVar, + $condition + )) { + $params[] = new Param(new Variable((string) $this->nodeNameResolver->getName($foreach->keyVar))); + } + + $arrowFunction = new ArrowFunction([ + 'params' => $params, + 'expr' => $condition, + ]); + + $funcCall = $this->nodeFactory->createFuncCall($functionName, [$foreach->expr, $arrowFunction]); + + $newAssign = new Assign($assignedVariable, $funcCall); + $newExpression = new Expression($newAssign); + + unset($node->stmts[$key - 1]); + $node->stmts[$key] = $newExpression; + + $node->stmts = array_values($node->stmts); + + return $node; + } + + return null; + } + + private function isValidForeachStructure(Foreach_ $foreach, Variable $assignedVariable, bool $compareKey): bool + { + if (count($foreach->stmts) !== 1) { + return false; + } + + $comparedExpr = $compareKey ? $foreach->keyVar : $foreach->valueVar; + if (! $comparedExpr instanceof Expr) { + return false; + } + + $firstStmt = $foreach->stmts[0]; + if ( + ! $firstStmt instanceof If_ || + count($firstStmt->stmts) !== 2 + ) { + return false; + } + + $assignmentStmt = $firstStmt->stmts[0]; + $breakStmt = $firstStmt->stmts[1]; + + if ( + ! $assignmentStmt instanceof Expression || + ! $assignmentStmt->expr instanceof Assign || + ! $breakStmt instanceof Break_ + ) { + return false; + } + + $assignment = $assignmentStmt->expr; + + if (! $this->nodeComparator->areNodesEqual($assignment->var, $assignedVariable)) { + return false; + } + + if (! $this->nodeComparator->areNodesEqual($assignment->expr, $comparedExpr)) { + return false; + } + + if (! $foreach->valueVar instanceof Variable) { + return false; + } + + $type = $this->nodeTypeResolver->getNativeType($foreach->expr); + return $type->isArray() + ->yes(); + } +} diff --git a/rules/Php84/Rector/Foreach_/ForeachToArrayAllRector.php b/rules/Php84/Rector/Foreach_/ForeachToArrayAllRector.php index cf8da7d4600..009d21344c7 100644 --- a/rules/Php84/Rector/Foreach_/ForeachToArrayAllRector.php +++ b/rules/Php84/Rector/Foreach_/ForeachToArrayAllRector.php @@ -5,21 +5,8 @@ namespace Rector\Php84\Rector\Foreach_; use PhpParser\Node; -use PhpParser\Node\Expr; -use PhpParser\Node\Expr\ArrowFunction; -use PhpParser\Node\Expr\Assign; -use PhpParser\Node\Expr\BooleanNot; -use PhpParser\Node\Expr\Variable; -use PhpParser\Node\Param; -use PhpParser\Node\Stmt\Break_; -use PhpParser\Node\Stmt\Expression; -use PhpParser\Node\Stmt\Foreach_; -use PhpParser\Node\Stmt\If_; -use PhpParser\Node\Stmt\Return_; -use Rector\NodeManipulator\StmtsManipulator; -use Rector\Php84\NodeAnalyzer\ForeachKeyUsedInConditionalAnalyzer; +use Rector\Php84\NodeFactory\ForeachToArrayAnyAllFactory; use Rector\PhpParser\Enum\NodeGroup; -use Rector\PhpParser\Node\Value\ValueResolver; use Rector\Rector\AbstractRector; use Rector\ValueObject\PhpVersionFeature; use Rector\ValueObject\PolyfillPackage; @@ -34,9 +21,7 @@ final class ForeachToArrayAllRector extends AbstractRector implements MinPhpVersionInterface, RelatedPolyfillInterface { public function __construct( - private readonly ValueResolver $valueResolver, - private readonly StmtsManipulator $stmtsManipulator, - private readonly ForeachKeyUsedInConditionalAnalyzer $foreachKeyUsedInConditionalAnalyzer + private readonly ForeachToArrayAnyAllFactory $foreachToArrayAnyAllFactory ) { } @@ -91,12 +76,7 @@ public function getNodeTypes(): array */ public function refactor(Node $node): ?Node { - if ($node->stmts === null) { - return null; - } - - return $this->refactorBooleanAssignmentPattern($node) ?? - $this->refactorEarlyReturnPattern($node); + return $this->foreachToArrayAnyAllFactory->refactorToArrayAnyAll($node, 'array_all', true, true, false); } public function provideMinPhpVersion(): int @@ -108,246 +88,4 @@ public function providePolyfillPackage(): string { return PolyfillPackage::PHP_84; } - - /** - * @param StmtsAware $stmtsAware - */ - private function refactorBooleanAssignmentPattern(Node $stmtsAware): ?Node - { - if ($stmtsAware->stmts === null) { - return null; - } - - foreach ($stmtsAware->stmts as $key => $stmt) { - if (! $stmt instanceof Foreach_) { - continue; - } - - $prevStmt = $stmtsAware->stmts[$key - 1] ?? null; - if (! $prevStmt instanceof Expression) { - continue; - } - - if (! $prevStmt->expr instanceof Assign) { - continue; - } - - $foreach = $stmt; - $prevAssign = $prevStmt->expr; - - if (! $this->valueResolver->isTrue($prevAssign->expr)) { - continue; - } - - if (! $prevAssign->var instanceof Variable) { - continue; - } - - $assignedVariable = $prevAssign->var; - - if (! $this->isValidBooleanAssignmentForeachStructure($foreach, $assignedVariable)) { - continue; - } - - if ($this->stmtsManipulator->isVariableUsedInNextStmt( - $stmtsAware, - $key + 1, - (string) $this->getName($foreach->valueVar) - )) { - continue; - } - - /** @var If_ $firstNodeInsideForeach */ - $firstNodeInsideForeach = $foreach->stmts[0]; - - $condition = $firstNodeInsideForeach->cond; - $valueParam = $foreach->valueVar; - - if (! $valueParam instanceof Variable) { - continue; - } - - $params = [new Param($valueParam)]; - - if ($foreach->keyVar instanceof Variable && $this->foreachKeyUsedInConditionalAnalyzer->isUsed( - $foreach->keyVar, - $condition - )) { - $params[] = new Param(new Variable((string) $this->getName($foreach->keyVar))); - } - - $negatedCondition = $condition instanceof BooleanNot ? $condition->expr : new BooleanNot($condition); - - $arrowFunction = new ArrowFunction([ - 'params' => $params, - 'expr' => $negatedCondition, - ]); - - $funcCall = $this->nodeFactory->createFuncCall('array_all', [$foreach->expr, $arrowFunction]); - - $newAssign = new Assign($assignedVariable, $funcCall); - $newExpression = new Expression($newAssign); - - unset($stmtsAware->stmts[$key - 1]); - $stmtsAware->stmts[$key] = $newExpression; - - $stmtsAware->stmts = array_values($stmtsAware->stmts); - - return $stmtsAware; - } - - return null; - } - - /** - * @param StmtsAware $stmtsAware - */ - private function refactorEarlyReturnPattern(Node $stmtsAware): ?Node - { - if ($stmtsAware->stmts === null) { - return null; - } - - foreach ($stmtsAware->stmts as $key => $stmt) { - if (! $stmt instanceof Foreach_) { - continue; - } - - $foreach = $stmt; - $nextStmt = $stmtsAware->stmts[$key + 1] ?? null; - - if (! $nextStmt instanceof Return_) { - continue; - } - - if (! $nextStmt->expr instanceof Expr) { - continue; - } - - if (! $this->valueResolver->isTrue($nextStmt->expr)) { - continue; - } - - if (! $this->isValidEarlyReturnForeachStructure($foreach)) { - continue; - } - - /** @var If_ $firstNodeInsideForeach */ - $firstNodeInsideForeach = $foreach->stmts[0]; - $condition = $firstNodeInsideForeach->cond; - - $params = []; - if ($foreach->valueVar instanceof Variable) { - $params[] = new Param($foreach->valueVar); - } - - if ( - $foreach->keyVar instanceof Variable && - $this->foreachKeyUsedInConditionalAnalyzer->isUsed($foreach->keyVar, $condition) - ) { - $params[] = new Param(new Variable((string) $this->getName($foreach->keyVar))); - } - - $negatedCondition = $condition instanceof BooleanNot ? $condition->expr : new BooleanNot($condition); - - $arrowFunction = new ArrowFunction([ - 'params' => $params, - 'expr' => $negatedCondition, - ]); - - $funcCall = $this->nodeFactory->createFuncCall('array_all', [$foreach->expr, $arrowFunction]); - - $stmtsAware->stmts[$key] = new Return_($funcCall); - unset($stmtsAware->stmts[$key + 1]); - $stmtsAware->stmts = array_values($stmtsAware->stmts); - - return $stmtsAware; - } - - return null; - } - - private function isValidEarlyReturnForeachStructure(Foreach_ $foreach): bool - { - if (count($foreach->stmts) !== 1) { - return false; - } - - if (! $foreach->stmts[0] instanceof If_) { - return false; - } - - $ifStmt = $foreach->stmts[0]; - - if (count($ifStmt->stmts) !== 1) { - return false; - } - - if (! $ifStmt->stmts[0] instanceof Return_) { - return false; - } - - $returnStmt = $ifStmt->stmts[0]; - - if (! $returnStmt->expr instanceof Expr) { - return false; - } - - if (! $this->valueResolver->isFalse($returnStmt->expr)) { - return false; - } - - if (! $foreach->valueVar instanceof Variable) { - return false; - } - - $type = $this->nodeTypeResolver->getNativeType($foreach->expr); - - return $type->isArray() - ->yes(); - } - - private function isValidBooleanAssignmentForeachStructure(Foreach_ $foreach, Variable $assignedVariable): bool - { - if (count($foreach->stmts) !== 1) { - return false; - } - - $firstStmt = $foreach->stmts[0]; - if ( - ! $firstStmt instanceof If_ || - count($firstStmt->stmts) !== 2 - ) { - return false; - } - - $assignmentStmt = $firstStmt->stmts[0]; - $breakStmt = $firstStmt->stmts[1]; - - if ( - ! $assignmentStmt instanceof Expression || - ! $assignmentStmt->expr instanceof Assign || - ! $breakStmt instanceof Break_ - ) { - return false; - } - - $assignment = $assignmentStmt->expr; - - if (! $this->nodeComparator->areNodesEqual($assignment->var, $assignedVariable)) { - return false; - } - - if (! $this->valueResolver->isFalse($assignment->expr)) { - return false; - } - - if (! $foreach->valueVar instanceof Variable) { - return false; - } - - $type = $this->nodeTypeResolver->getNativeType($foreach->expr); - return $type->isArray() - ->yes(); - } } diff --git a/rules/Php84/Rector/Foreach_/ForeachToArrayAnyRector.php b/rules/Php84/Rector/Foreach_/ForeachToArrayAnyRector.php index 587e3c8048f..04db354885d 100644 --- a/rules/Php84/Rector/Foreach_/ForeachToArrayAnyRector.php +++ b/rules/Php84/Rector/Foreach_/ForeachToArrayAnyRector.php @@ -5,21 +5,8 @@ namespace Rector\Php84\Rector\Foreach_; use PhpParser\Node; -use PhpParser\Node\Expr; -use PhpParser\Node\Expr\ArrowFunction; -use PhpParser\Node\Expr\Assign; -use PhpParser\Node\Expr\Variable; -use PhpParser\Node\Param; -use PhpParser\Node\Stmt\Break_; -use PhpParser\Node\Stmt\Else_; -use PhpParser\Node\Stmt\Expression; -use PhpParser\Node\Stmt\Foreach_; -use PhpParser\Node\Stmt\If_; -use PhpParser\Node\Stmt\Return_; -use Rector\NodeManipulator\StmtsManipulator; -use Rector\Php84\NodeAnalyzer\ForeachKeyUsedInConditionalAnalyzer; +use Rector\Php84\NodeFactory\ForeachToArrayAnyAllFactory; use Rector\PhpParser\Enum\NodeGroup; -use Rector\PhpParser\Node\Value\ValueResolver; use Rector\Rector\AbstractRector; use Rector\ValueObject\PhpVersionFeature; use Rector\ValueObject\PolyfillPackage; @@ -34,9 +21,7 @@ final class ForeachToArrayAnyRector extends AbstractRector implements MinPhpVersionInterface, RelatedPolyfillInterface { public function __construct( - private readonly ValueResolver $valueResolver, - private readonly ForeachKeyUsedInConditionalAnalyzer $foreachKeyUsedInConditionalAnalyzer, - private readonly StmtsManipulator $stmtsManipulator, + private readonly ForeachToArrayAnyAllFactory $foreachToArrayAnyAllFactory ) { } @@ -91,12 +76,7 @@ public function getNodeTypes(): array */ public function refactor(Node $node): ?Node { - if ($node->stmts === null) { - return null; - } - - return $this->refactorBooleanAssignmentPattern($node) - ?? $this->refactorEarlyReturnPattern($node); + return $this->foreachToArrayAnyAllFactory->refactorToArrayAnyAll($node, 'array_any', false, false, true); } public function provideMinPhpVersion(): int @@ -108,247 +88,4 @@ public function providePolyfillPackage(): string { return PolyfillPackage::PHP_84; } - - /** - * @param StmtsAware $stmtsAware - */ - private function refactorBooleanAssignmentPattern(Node $stmtsAware): ?Node - { - if ($stmtsAware->stmts === null) { - return null; - } - - foreach ($stmtsAware->stmts as $key => $stmt) { - if (! $stmt instanceof Foreach_) { - continue; - } - - $prevStmt = $stmtsAware->stmts[$key - 1] ?? null; - if (! $prevStmt instanceof Expression) { - continue; - } - - if (! $prevStmt->expr instanceof Assign) { - continue; - } - - $foreach = $stmt; - $prevAssign = $prevStmt->expr; - - if (! $this->valueResolver->isFalse($prevAssign->expr)) { - continue; - } - - if (! $prevAssign->var instanceof Variable) { - continue; - } - - $assignedVariable = $prevAssign->var; - - if (! $this->isValidBooleanAssignmentForeachStructure($foreach, $assignedVariable)) { - continue; - } - - if ($this->stmtsManipulator->isVariableUsedInNextStmt( - $stmtsAware, - $key + 1, - (string) $this->getName($foreach->valueVar) - )) { - continue; - } - - /** @var If_ $firstNodeInsideForeach */ - $firstNodeInsideForeach = $foreach->stmts[0]; - - $condition = $firstNodeInsideForeach->cond; - $valueParam = $foreach->valueVar; - - if (! $valueParam instanceof Variable) { - continue; - } - - $params = [new Param($valueParam)]; - - if ($foreach->keyVar instanceof Variable && $this->foreachKeyUsedInConditionalAnalyzer->isUsed( - $foreach->keyVar, - $condition - )) { - $params[] = new Param(new Variable((string) $this->getName($foreach->keyVar))); - } - - $arrowFunction = new ArrowFunction([ - 'params' => $params, - 'expr' => $condition, - ]); - - $funcCall = $this->nodeFactory->createFuncCall('array_any', [$foreach->expr, $arrowFunction]); - - $newAssign = new Assign($assignedVariable, $funcCall); - $newExpression = new Expression($newAssign); - - unset($stmtsAware->stmts[$key - 1]); - $stmtsAware->stmts[$key] = $newExpression; - - $stmtsAware->stmts = array_values($stmtsAware->stmts); - - return $stmtsAware; - } - - return null; - } - - /** - * @param StmtsAware $stmtsAware - */ - private function refactorEarlyReturnPattern(Node $stmtsAware): ?Node - { - if ($stmtsAware->stmts === null) { - return null; - } - - foreach ($stmtsAware->stmts as $key => $stmt) { - if (! $stmt instanceof Foreach_) { - continue; - } - - $foreach = $stmt; - $nextStmt = $stmtsAware->stmts[$key + 1] ?? null; - - if (! $nextStmt instanceof Return_) { - continue; - } - - if (! $nextStmt->expr instanceof Expr) { - continue; - } - - if (! $this->valueResolver->isFalse($nextStmt->expr)) { - continue; - } - - if (! $this->isValidEarlyReturnForeachStructure($foreach)) { - continue; - } - - /** @var If_ $firstNodeInsideForeach */ - $firstNodeInsideForeach = $foreach->stmts[0]; - $condition = $firstNodeInsideForeach->cond; - - $params = []; - - if ($foreach->valueVar instanceof Variable) { - $params[] = new Param($foreach->valueVar); - } - - if ( - $foreach->keyVar instanceof Variable && - $this->foreachKeyUsedInConditionalAnalyzer->isUsed($foreach->keyVar, $condition) - ) { - $params[] = new Param(new Variable((string) $this->getName($foreach->keyVar))); - } - - $arrowFunction = new ArrowFunction([ - 'params' => $params, - 'expr' => $condition, - ]); - - $funcCall = $this->nodeFactory->createFuncCall('array_any', [$foreach->expr, $arrowFunction]); - - $stmtsAware->stmts[$key] = new Return_($funcCall); - unset($stmtsAware->stmts[$key + 1]); - $stmtsAware->stmts = array_values($stmtsAware->stmts); - - return $stmtsAware; - } - - return null; - } - - private function isValidBooleanAssignmentForeachStructure(Foreach_ $foreach, Variable $assignedVariable): bool - { - if (count($foreach->stmts) !== 1) { - return false; - } - - $firstStmt = $foreach->stmts[0]; - if ( - ! $firstStmt instanceof If_ || - count($firstStmt->stmts) !== 2 - ) { - return false; - } - - if ($firstStmt->elseifs !== [] || $firstStmt->else instanceof Else_) { - return false; - } - - $assignmentStmt = $firstStmt->stmts[0]; - $breakStmt = $firstStmt->stmts[1]; - - if ( - ! $assignmentStmt instanceof Expression || - ! $assignmentStmt->expr instanceof Assign || - ! $breakStmt instanceof Break_ - ) { - return false; - } - - $assignment = $assignmentStmt->expr; - - if (! $this->nodeComparator->areNodesEqual($assignment->var, $assignedVariable)) { - return false; - } - - if (! $this->valueResolver->isTrue($assignment->expr)) { - return false; - } - - $type = $this->nodeTypeResolver->getNativeType($foreach->expr); - return $type->isArray() - ->yes(); - } - - private function isValidEarlyReturnForeachStructure(Foreach_ $foreach): bool - { - if (count($foreach->stmts) !== 1) { - return false; - } - - if (! $foreach->stmts[0] instanceof If_) { - return false; - } - - $ifStmt = $foreach->stmts[0]; - - if ($ifStmt->elseifs !== [] || $ifStmt->else instanceof Else_) { - return false; - } - - if (count($ifStmt->stmts) !== 1) { - return false; - } - - if (! $ifStmt->stmts[0] instanceof Return_) { - return false; - } - - $returnStmt = $ifStmt->stmts[0]; - - if (! $returnStmt->expr instanceof Expr) { - return false; - } - - if (! $this->valueResolver->isTrue($returnStmt->expr)) { - return false; - } - - if (! $foreach->valueVar instanceof Variable) { - return false; - } - - $type = $this->nodeTypeResolver->getNativeType($foreach->expr); - - return $type->isArray() - ->yes(); - } } diff --git a/rules/Php84/Rector/Foreach_/ForeachToArrayFindKeyRector.php b/rules/Php84/Rector/Foreach_/ForeachToArrayFindKeyRector.php index 2e89fd7011c..db7b7f3438c 100644 --- a/rules/Php84/Rector/Foreach_/ForeachToArrayFindKeyRector.php +++ b/rules/Php84/Rector/Foreach_/ForeachToArrayFindKeyRector.php @@ -5,19 +5,8 @@ namespace Rector\Php84\Rector\Foreach_; use PhpParser\Node; -use PhpParser\Node\Expr; -use PhpParser\Node\Expr\ArrowFunction; -use PhpParser\Node\Expr\Assign; -use PhpParser\Node\Expr\Variable; -use PhpParser\Node\Param; -use PhpParser\Node\Stmt\Break_; -use PhpParser\Node\Stmt\Expression; -use PhpParser\Node\Stmt\Foreach_; -use PhpParser\Node\Stmt\If_; -use Rector\NodeManipulator\StmtsManipulator; -use Rector\Php84\NodeAnalyzer\ForeachKeyUsedInConditionalAnalyzer; +use Rector\Php84\NodeFactory\ForeachToArrayFindFactory; use Rector\PhpParser\Enum\NodeGroup; -use Rector\PhpParser\Node\Value\ValueResolver; use Rector\Rector\AbstractRector; use Rector\ValueObject\PhpVersionFeature; use Rector\ValueObject\PolyfillPackage; @@ -32,9 +21,7 @@ final class ForeachToArrayFindKeyRector extends AbstractRector implements MinPhpVersionInterface, RelatedPolyfillInterface { public function __construct( - private readonly ValueResolver $valueResolver, - private readonly StmtsManipulator $stmtsManipulator, - private readonly ForeachKeyUsedInConditionalAnalyzer $foreachKeyUsedInConditionalAnalyzer + private readonly ForeachToArrayFindFactory $foreachToArrayFindFactory ) { } @@ -79,87 +66,7 @@ public function getNodeTypes(): array */ public function refactor(Node $node): ?Node { - if ($node->stmts === null) { - return null; - } - - foreach ($node->stmts as $key => $stmt) { - if (! $stmt instanceof Foreach_) { - continue; - } - - $prevStmt = $node->stmts[$key - 1] ?? null; - if (! $prevStmt instanceof Expression) { - continue; - } - - if (! $prevStmt->expr instanceof Assign) { - continue; - } - - $foreach = $stmt; - $prevAssign = $prevStmt->expr; - - if (! $this->valueResolver->isNull($prevAssign->expr)) { - continue; - } - - if (! $prevAssign->var instanceof Variable) { - continue; - } - - $assignedVariable = $prevAssign->var; - - if (! $this->isValidForeachStructure($foreach, $assignedVariable)) { - continue; - } - - if ($this->stmtsManipulator->isVariableUsedInNextStmt( - $node, - $key + 1, - (string) $this->getName($foreach->valueVar) - )) { - continue; - } - - /** @var If_ $firstNodeInsideForeach */ - $firstNodeInsideForeach = $foreach->stmts[0]; - - $condition = $firstNodeInsideForeach->cond; - $valueParam = $foreach->valueVar; - - if (! $valueParam instanceof Variable) { - continue; - } - - $params = [new Param($valueParam)]; - - if ($foreach->keyVar instanceof Variable && $this->foreachKeyUsedInConditionalAnalyzer->isUsed( - $foreach->keyVar, - $condition - )) { - $params[] = new Param(new Variable((string) $this->getName($foreach->keyVar))); - } - - $arrowFunction = new ArrowFunction([ - 'params' => $params, - 'expr' => $condition, - ]); - - $funcCall = $this->nodeFactory->createFuncCall('array_find_key', [$foreach->expr, $arrowFunction]); - - $newAssign = new Assign($assignedVariable, $funcCall); - $newExpression = new Expression($newAssign); - - unset($node->stmts[$key - 1]); - $node->stmts[$key] = $newExpression; - - $node->stmts = array_values($node->stmts); - - return $node; - } - - return null; + return $this->foreachToArrayFindFactory->createArrayFindAssign($node, 'array_find_key', true); } public function provideMinPhpVersion(): int @@ -171,51 +78,4 @@ public function providePolyfillPackage(): string { return PolyfillPackage::PHP_84; } - - private function isValidForeachStructure(Foreach_ $foreach, Variable $assignedVariable): bool - { - if ( - ! $foreach->keyVar instanceof Expr || - count($foreach->stmts) !== 1 - ) { - return false; - } - - $firstStmt = $foreach->stmts[0]; - if ( - ! $firstStmt instanceof If_ || - count($firstStmt->stmts) !== 2 - ) { - return false; - } - - $assignmentStmt = $firstStmt->stmts[0]; - $breakStmt = $firstStmt->stmts[1]; - - if ( - ! $assignmentStmt instanceof Expression || - ! $assignmentStmt->expr instanceof Assign || - ! $breakStmt instanceof Break_ - ) { - return false; - } - - $assignment = $assignmentStmt->expr; - - if (! $this->nodeComparator->areNodesEqual($assignment->var, $assignedVariable)) { - return false; - } - - if (! $this->nodeComparator->areNodesEqual($assignment->expr, $foreach->keyVar)) { - return false; - } - - if (! $foreach->valueVar instanceof Variable) { - return false; - } - - $type = $this->nodeTypeResolver->getNativeType($foreach->expr); - return $type->isArray() - ->yes(); - } } diff --git a/rules/Php84/Rector/Foreach_/ForeachToArrayFindRector.php b/rules/Php84/Rector/Foreach_/ForeachToArrayFindRector.php index ddfd5d6b957..445961185e7 100644 --- a/rules/Php84/Rector/Foreach_/ForeachToArrayFindRector.php +++ b/rules/Php84/Rector/Foreach_/ForeachToArrayFindRector.php @@ -5,18 +5,8 @@ namespace Rector\Php84\Rector\Foreach_; use PhpParser\Node; -use PhpParser\Node\Expr\ArrowFunction; -use PhpParser\Node\Expr\Assign; -use PhpParser\Node\Expr\Variable; -use PhpParser\Node\Param; -use PhpParser\Node\Stmt\Break_; -use PhpParser\Node\Stmt\Expression; -use PhpParser\Node\Stmt\Foreach_; -use PhpParser\Node\Stmt\If_; -use Rector\NodeManipulator\StmtsManipulator; -use Rector\Php84\NodeAnalyzer\ForeachKeyUsedInConditionalAnalyzer; +use Rector\Php84\NodeFactory\ForeachToArrayFindFactory; use Rector\PhpParser\Enum\NodeGroup; -use Rector\PhpParser\Node\Value\ValueResolver; use Rector\Rector\AbstractRector; use Rector\ValueObject\PhpVersionFeature; use Rector\ValueObject\PolyfillPackage; @@ -31,9 +21,7 @@ final class ForeachToArrayFindRector extends AbstractRector implements MinPhpVersionInterface, RelatedPolyfillInterface { public function __construct( - private readonly ValueResolver $valueResolver, - private readonly StmtsManipulator $stmtsManipulator, - private readonly ForeachKeyUsedInConditionalAnalyzer $foreachKeyUsedInConditionalAnalyzer + private readonly ForeachToArrayFindFactory $foreachToArrayFindFactory ) { } @@ -74,87 +62,7 @@ public function getNodeTypes(): array */ public function refactor(Node $node): ?Node { - if ($node->stmts === null) { - return null; - } - - foreach ($node->stmts as $key => $stmt) { - if (! $stmt instanceof Foreach_) { - continue; - } - - $prevStmt = $node->stmts[$key - 1] ?? null; - if (! $prevStmt instanceof Expression) { - continue; - } - - if (! $prevStmt->expr instanceof Assign) { - continue; - } - - $foreach = $stmt; - $prevAssign = $prevStmt->expr; - - if (! $this->valueResolver->isNull($prevAssign->expr)) { - continue; - } - - if (! $prevAssign->var instanceof Variable) { - continue; - } - - $assignedVariable = $prevAssign->var; - - if (! $this->isValidForeachStructure($foreach, $assignedVariable)) { - continue; - } - - if ($this->stmtsManipulator->isVariableUsedInNextStmt( - $node, - $key + 1, - (string) $this->getName($foreach->valueVar) - )) { - continue; - } - - /** @var If_ $firstNodeInsideForeach */ - $firstNodeInsideForeach = $foreach->stmts[0]; - - $condition = $firstNodeInsideForeach->cond; - $valueParam = $foreach->valueVar; - - if (! $valueParam instanceof Variable) { - continue; - } - - $params = [new Param($valueParam)]; - - if ($foreach->keyVar instanceof Variable && $this->foreachKeyUsedInConditionalAnalyzer->isUsed( - $foreach->keyVar, - $condition - )) { - $params[] = new Param(new Variable((string) $this->getName($foreach->keyVar))); - } - - $arrowFunction = new ArrowFunction([ - 'params' => $params, - 'expr' => $condition, - ]); - - $funcCall = $this->nodeFactory->createFuncCall('array_find', [$foreach->expr, $arrowFunction]); - - $newAssign = new Assign($assignedVariable, $funcCall); - $newExpression = new Expression($newAssign); - - unset($node->stmts[$key - 1]); - $node->stmts[$key] = $newExpression; - - $node->stmts = array_values($node->stmts); - - return $node; - } - - return null; + return $this->foreachToArrayFindFactory->createArrayFindAssign($node, 'array_find', false); } public function provideMinPhpVersion(): int @@ -166,48 +74,4 @@ public function providePolyfillPackage(): string { return PolyfillPackage::PHP_84; } - - private function isValidForeachStructure(Foreach_ $foreach, Variable $assignedVariable): bool - { - if (count($foreach->stmts) !== 1) { - return false; - } - - $firstStmt = $foreach->stmts[0]; - if ( - ! $firstStmt instanceof If_ || - count($firstStmt->stmts) !== 2 - ) { - return false; - } - - $assignmentStmt = $firstStmt->stmts[0]; - $breakStmt = $firstStmt->stmts[1]; - - if ( - ! $assignmentStmt instanceof Expression || - ! $assignmentStmt->expr instanceof Assign || - ! $breakStmt instanceof Break_ - ) { - return false; - } - - $assignment = $assignmentStmt->expr; - - if (! $this->nodeComparator->areNodesEqual($assignment->var, $assignedVariable)) { - return false; - } - - if (! $this->nodeComparator->areNodesEqual($assignment->expr, $foreach->valueVar)) { - return false; - } - - if (! $foreach->valueVar instanceof Variable) { - return false; - } - - $type = $this->nodeTypeResolver->getNativeType($foreach->expr); - return $type->isArray() - ->yes(); - } } diff --git a/rules/TypeDeclaration/NodeAnalyzer/ArrayCallbackParamTypeResolver.php b/rules/TypeDeclaration/NodeAnalyzer/ArrayCallbackParamTypeResolver.php new file mode 100644 index 00000000000..4f9d604b58a --- /dev/null +++ b/rules/TypeDeclaration/NodeAnalyzer/ArrayCallbackParamTypeResolver.php @@ -0,0 +1,126 @@ +isFirstClassCallable()) { + return null; + } + + if (! $this->nodeNameResolver->isNames($funcCall, $functionNames)) { + return null; + } + + $arrayArg = $funcCall->getArg('array', 0); + $callbackArg = $funcCall->getArg('callback', 1); + if (! $arrayArg instanceof Arg || ! $callbackArg instanceof Arg) { + return null; + } + + $callbackExpr = $callbackArg->value; + if (! $callbackExpr instanceof ArrowFunction && ! $callbackExpr instanceof Closure) { + return null; + } + + $valueParam = $callbackExpr->getParams()[0] ?? null; + if (! $valueParam instanceof Param) { + return null; + } + + if ($narrowNullable) { + // only narrow a param that currently allows null + if (! $valueParam->type instanceof NullableType) { + return null; + } + } elseif ($valueParam->type instanceof Node) { + // only fill in a param that has no type yet + return null; + } + + $itemType = $this->resolveArrayItemType($this->nodeTypeResolver->getType($arrayArg->value)); + if (! $itemType instanceof Type) { + return null; + } + + if ($itemType instanceof MixedType) { + return null; + } + + // nothing to narrow when the item type is itself nullable or unknown + if ($narrowNullable && ! $itemType->isNull()->no()) { + return null; + } + + $paramTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($itemType, TypeKind::PARAM); + if (! $paramTypeNode instanceof Node) { + return null; + } + + $valueParam->type = $paramTypeNode; + + return $funcCall; + } + + private function resolveArrayItemType(Type $arrayType): ?Type + { + if ($arrayType instanceof ConstantArrayType || $arrayType instanceof ArrayType) { + return $arrayType->getItemType(); + } + + if ($arrayType instanceof IntersectionType) { + foreach ($arrayType->getTypes() as $subType) { + if ($subType instanceof AccessoryArrayListType) { + continue; + } + + if (! $subType instanceof ArrayType) { + continue; + } + + return $subType->getItemType(); + } + } + + return null; + } +} diff --git a/rules/TypeDeclaration/NodeAnalyzer/SetUpAssignedPropertyTyper.php b/rules/TypeDeclaration/NodeAnalyzer/SetUpAssignedPropertyTyper.php new file mode 100644 index 00000000000..71609cf008b --- /dev/null +++ b/rules/TypeDeclaration/NodeAnalyzer/SetUpAssignedPropertyTyper.php @@ -0,0 +1,157 @@ +testsNodeAnalyzer->isInTestClass($class)) { + return null; + } + + $setUpClassMethod = $class->getMethod(MethodName::SET_UP); + if (! $setUpClassMethod instanceof ClassMethod) { + return null; + } + + $hasChanged = false; + + foreach ($class->getProperties() as $property) { + // type is already set + if ($property->type instanceof Node) { + continue; + } + + if (! $property->isPrivate()) { + continue; + } + + if ($property->isStatic()) { + continue; + } + + // exactly one property + if (count($property->props) !== 1) { + continue; + } + + $propertyName = $this->nodeNameResolver->getName($property->props[0]); + if (! $this->isAssignedInSetUp($setUpClassMethod, $propertyName, $isTargetAssignExpr)) { + continue; + } + + $propertyPhpDocInfo = $this->phpDocInfoFactory->createFromNode($property); + if (! $propertyPhpDocInfo instanceof PhpDocInfo) { + continue; + } + + $varType = $propertyPhpDocInfo->getVarType(); + if (! $varType instanceof ObjectType) { + continue; + } + + $propertyTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($varType, TypeKind::PROPERTY); + if (! $propertyTypeNode instanceof Name) { + continue; + } + + // must be an existing object type + if (! $this->reflectionProvider->hasClass($propertyTypeNode->toString())) { + continue; + } + + $property->type = $propertyTypeNode; + $this->removeVarTag($propertyPhpDocInfo, $property); + + $hasChanged = true; + } + + if ($hasChanged) { + return $class; + } + + return null; + } + + /** + * @param callable(Expr): bool $isTargetAssignExpr + */ + private function isAssignedInSetUp(ClassMethod $setUpClassMethod, ?string $propertyName, callable $isTargetAssignExpr): bool + { + /** @var Assign[] $assigns */ + $assigns = $this->betterNodeFinder->findInstanceOf($setUpClassMethod, Assign::class); + + foreach ($assigns as $assign) { + if (! $assign->var instanceof PropertyFetch) { + continue; + } + + $propertyFetch = $assign->var; + if (! $this->nodeNameResolver->isName($propertyFetch->var, 'this')) { + continue; + } + + if (! $this->nodeNameResolver->isName($propertyFetch, (string) $propertyName)) { + continue; + } + + if ($isTargetAssignExpr($assign->expr)) { + return true; + } + } + + return false; + } + + private function removeVarTag(PhpDocInfo $propertyPhpDocInfo, Property $property): void + { + $varTagValueNode = $propertyPhpDocInfo->getVarTagValueNode(); + if (! $varTagValueNode instanceof VarTagValueNode) { + return; + } + + $propertyPhpDocInfo->removeByType(VarTagValueNode::class); + $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($property); + } +} diff --git a/rules/TypeDeclaration/Rector/Class_/TypedPropertyFromContainerGetSetUpRector.php b/rules/TypeDeclaration/Rector/Class_/TypedPropertyFromContainerGetSetUpRector.php index 366d73ea26e..f8be6cc50e5 100644 --- a/rules/TypeDeclaration/Rector/Class_/TypedPropertyFromContainerGetSetUpRector.php +++ b/rules/TypeDeclaration/Rector/Class_/TypedPropertyFromContainerGetSetUpRector.php @@ -6,27 +6,13 @@ use PhpParser\Node; use PhpParser\Node\Expr; -use PhpParser\Node\Expr\Assign; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Expr\PropertyFetch; use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Expr\Variable; -use PhpParser\Node\Name; use PhpParser\Node\Stmt\Class_; -use PhpParser\Node\Stmt\ClassMethod; -use PhpParser\Node\Stmt\Property; -use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode; -use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Type\ObjectType; -use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo; -use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory; -use Rector\Comments\NodeDocBlock\DocBlockUpdater; -use Rector\PhpParser\Node\BetterNodeFinder; -use Rector\PHPStanStaticTypeMapper\Enum\TypeKind; -use Rector\PHPUnit\NodeAnalyzer\TestsNodeAnalyzer; use Rector\Rector\AbstractRector; -use Rector\StaticTypeMapper\StaticTypeMapper; -use Rector\ValueObject\MethodName; +use Rector\TypeDeclaration\NodeAnalyzer\SetUpAssignedPropertyTyper; use Rector\ValueObject\PhpVersionFeature; use Rector\VersionBonding\Contract\MinPhpVersionInterface; use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; @@ -38,12 +24,7 @@ final class TypedPropertyFromContainerGetSetUpRector extends AbstractRector implements MinPhpVersionInterface { public function __construct( - private readonly TestsNodeAnalyzer $testsNodeAnalyzer, - private readonly PhpDocInfoFactory $phpDocInfoFactory, - private readonly StaticTypeMapper $staticTypeMapper, - private readonly DocBlockUpdater $docBlockUpdater, - private readonly BetterNodeFinder $betterNodeFinder, - private readonly ReflectionProvider $reflectionProvider + private readonly SetUpAssignedPropertyTyper $setUpAssignedPropertyTyper ) { } @@ -101,72 +82,10 @@ public function getNodeTypes(): array */ public function refactor(Node $node): ?Node { - if (! $this->testsNodeAnalyzer->isInTestClass($node)) { - return null; - } - - $setUpClassMethod = $node->getMethod(MethodName::SET_UP); - if (! $setUpClassMethod instanceof ClassMethod) { - return null; - } - - $hasChanged = false; - - foreach ($node->getProperties() as $property) { - // type is already set - if ($property->type instanceof Node) { - continue; - } - - if (! $property->isPrivate()) { - continue; - } - - if ($property->isStatic()) { - continue; - } - - // exactly one property - if (count($property->props) !== 1) { - continue; - } - - $propertyName = $this->getName($property->props[0]); - if (! $this->isAssignedViaContainerGetInSetUp($setUpClassMethod, $propertyName)) { - continue; - } - - $propertyPhpDocInfo = $this->phpDocInfoFactory->createFromNode($property); - if (! $propertyPhpDocInfo instanceof PhpDocInfo) { - continue; - } - - $varType = $propertyPhpDocInfo->getVarType(); - if (! $varType instanceof ObjectType) { - continue; - } - - $propertyTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($varType, TypeKind::PROPERTY); - if (! $propertyTypeNode instanceof Name) { - continue; - } - - // must be an existing object type - if (! $this->reflectionProvider->hasClass($propertyTypeNode->toString())) { - continue; - } - - $property->type = $propertyTypeNode; - $this->removeVarTag($propertyPhpDocInfo, $property); - - $hasChanged = true; - } - - if ($hasChanged) { - return $node; - } - - return null; + return $this->setUpAssignedPropertyTyper->refactorClass( + $node, + fn (Expr $expr): bool => $this->isContainerGetCall($expr) + ); } public function provideMinPhpVersion(): int @@ -174,33 +93,6 @@ public function provideMinPhpVersion(): int return PhpVersionFeature::TYPED_PROPERTIES; } - private function isAssignedViaContainerGetInSetUp(ClassMethod $setUpClassMethod, string $propertyName): bool - { - /** @var Assign[] $assigns */ - $assigns = $this->betterNodeFinder->findInstanceOf($setUpClassMethod, Assign::class); - - foreach ($assigns as $assign) { - if (! $assign->var instanceof PropertyFetch) { - continue; - } - - $propertyFetch = $assign->var; - if (! $this->isName($propertyFetch->var, 'this')) { - continue; - } - - if (! $this->isName($propertyFetch, $propertyName)) { - continue; - } - - if ($this->isContainerGetCall($assign->expr)) { - return true; - } - } - - return false; - } - private function isContainerGetCall(Expr $expr): bool { if (! $expr instanceof MethodCall) { @@ -226,15 +118,4 @@ private function isContainerGetCall(Expr $expr): bool // $this->get(...) return $caller instanceof Variable && $this->isName($caller, 'this'); } - - private function removeVarTag(PhpDocInfo $propertyPhpDocInfo, Property $property): void - { - $varTagValueNode = $propertyPhpDocInfo->getVarTagValueNode(); - if (! $varTagValueNode instanceof VarTagValueNode) { - return; - } - - $propertyPhpDocInfo->removeByType(VarTagValueNode::class); - $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($property); - } } diff --git a/rules/TypeDeclaration/Rector/Class_/TypedPropertyFromGetRepositorySetUpRector.php b/rules/TypeDeclaration/Rector/Class_/TypedPropertyFromGetRepositorySetUpRector.php index 5932c89d930..4512563e87d 100644 --- a/rules/TypeDeclaration/Rector/Class_/TypedPropertyFromGetRepositorySetUpRector.php +++ b/rules/TypeDeclaration/Rector/Class_/TypedPropertyFromGetRepositorySetUpRector.php @@ -6,25 +6,10 @@ use PhpParser\Node; use PhpParser\Node\Expr; -use PhpParser\Node\Expr\Assign; use PhpParser\Node\Expr\MethodCall; -use PhpParser\Node\Expr\PropertyFetch; -use PhpParser\Node\Name; use PhpParser\Node\Stmt\Class_; -use PhpParser\Node\Stmt\ClassMethod; -use PhpParser\Node\Stmt\Property; -use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode; -use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Type\ObjectType; -use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo; -use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory; -use Rector\Comments\NodeDocBlock\DocBlockUpdater; -use Rector\PhpParser\Node\BetterNodeFinder; -use Rector\PHPStanStaticTypeMapper\Enum\TypeKind; -use Rector\PHPUnit\NodeAnalyzer\TestsNodeAnalyzer; use Rector\Rector\AbstractRector; -use Rector\StaticTypeMapper\StaticTypeMapper; -use Rector\ValueObject\MethodName; +use Rector\TypeDeclaration\NodeAnalyzer\SetUpAssignedPropertyTyper; use Rector\ValueObject\PhpVersionFeature; use Rector\VersionBonding\Contract\MinPhpVersionInterface; use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; @@ -36,12 +21,7 @@ final class TypedPropertyFromGetRepositorySetUpRector extends AbstractRector implements MinPhpVersionInterface { public function __construct( - private readonly TestsNodeAnalyzer $testsNodeAnalyzer, - private readonly PhpDocInfoFactory $phpDocInfoFactory, - private readonly StaticTypeMapper $staticTypeMapper, - private readonly DocBlockUpdater $docBlockUpdater, - private readonly BetterNodeFinder $betterNodeFinder, - private readonly ReflectionProvider $reflectionProvider + private readonly SetUpAssignedPropertyTyper $setUpAssignedPropertyTyper ) { } @@ -99,72 +79,10 @@ public function getNodeTypes(): array */ public function refactor(Node $node): ?Node { - if (! $this->testsNodeAnalyzer->isInTestClass($node)) { - return null; - } - - $setUpClassMethod = $node->getMethod(MethodName::SET_UP); - if (! $setUpClassMethod instanceof ClassMethod) { - return null; - } - - $hasChanged = false; - - foreach ($node->getProperties() as $property) { - // type is already set - if ($property->type instanceof Node) { - continue; - } - - if (! $property->isPrivate()) { - continue; - } - - if ($property->isStatic()) { - continue; - } - - // exactly one property - if (count($property->props) !== 1) { - continue; - } - - $propertyName = $this->getName($property->props[0]); - if (! $this->isAssignedViaGetRepositoryInSetUp($setUpClassMethod, $propertyName)) { - continue; - } - - $propertyPhpDocInfo = $this->phpDocInfoFactory->createFromNode($property); - if (! $propertyPhpDocInfo instanceof PhpDocInfo) { - continue; - } - - $varType = $propertyPhpDocInfo->getVarType(); - if (! $varType instanceof ObjectType) { - continue; - } - - $propertyTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($varType, TypeKind::PROPERTY); - if (! $propertyTypeNode instanceof Name) { - continue; - } - - // must be an existing object type - if (! $this->reflectionProvider->hasClass($propertyTypeNode->toString())) { - continue; - } - - $property->type = $propertyTypeNode; - $this->removeVarTag($propertyPhpDocInfo, $property); - - $hasChanged = true; - } - - if ($hasChanged) { - return $node; - } - - return null; + return $this->setUpAssignedPropertyTyper->refactorClass( + $node, + fn (Expr $expr): bool => $this->isGetRepositoryCall($expr) + ); } public function provideMinPhpVersion(): int @@ -172,33 +90,6 @@ public function provideMinPhpVersion(): int return PhpVersionFeature::TYPED_PROPERTIES; } - private function isAssignedViaGetRepositoryInSetUp(ClassMethod $setUpClassMethod, string $propertyName): bool - { - /** @var Assign[] $assigns */ - $assigns = $this->betterNodeFinder->findInstanceOf($setUpClassMethod, Assign::class); - - foreach ($assigns as $assign) { - if (! $assign->var instanceof PropertyFetch) { - continue; - } - - $propertyFetch = $assign->var; - if (! $this->isName($propertyFetch->var, 'this')) { - continue; - } - - if (! $this->isName($propertyFetch, $propertyName)) { - continue; - } - - if ($this->isGetRepositoryCall($assign->expr)) { - return true; - } - } - - return false; - } - private function isGetRepositoryCall(Expr $expr): bool { if (! $expr instanceof MethodCall) { @@ -207,15 +98,4 @@ private function isGetRepositoryCall(Expr $expr): bool return $this->isName($expr->name, 'getRepository'); } - - private function removeVarTag(PhpDocInfo $propertyPhpDocInfo, Property $property): void - { - $varTagValueNode = $propertyPhpDocInfo->getVarTagValueNode(); - if (! $varTagValueNode instanceof VarTagValueNode) { - return; - } - - $propertyPhpDocInfo->removeByType(VarTagValueNode::class); - $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($property); - } } diff --git a/rules/TypeDeclaration/Rector/FuncCall/AddArrayAnyAllClosureParamTypeRector.php b/rules/TypeDeclaration/Rector/FuncCall/AddArrayAnyAllClosureParamTypeRector.php index 3992ba9841b..7d86b163b4e 100644 --- a/rules/TypeDeclaration/Rector/FuncCall/AddArrayAnyAllClosureParamTypeRector.php +++ b/rules/TypeDeclaration/Rector/FuncCall/AddArrayAnyAllClosureParamTypeRector.php @@ -5,20 +5,9 @@ namespace Rector\TypeDeclaration\Rector\FuncCall; use PhpParser\Node; -use PhpParser\Node\Arg; -use PhpParser\Node\Expr\ArrowFunction; -use PhpParser\Node\Expr\Closure; use PhpParser\Node\Expr\FuncCall; -use PhpParser\Node\Param; -use PHPStan\Type\Accessory\AccessoryArrayListType; -use PHPStan\Type\ArrayType; -use PHPStan\Type\Constant\ConstantArrayType; -use PHPStan\Type\IntersectionType; -use PHPStan\Type\MixedType; -use PHPStan\Type\Type; -use Rector\PHPStanStaticTypeMapper\Enum\TypeKind; use Rector\Rector\AbstractRector; -use Rector\StaticTypeMapper\StaticTypeMapper; +use Rector\TypeDeclaration\NodeAnalyzer\ArrayCallbackParamTypeResolver; use Rector\ValueObject\PhpVersionFeature; use Rector\VersionBonding\Contract\MinPhpVersionInterface; use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; @@ -35,7 +24,7 @@ final class AddArrayAnyAllClosureParamTypeRector extends AbstractRector implemen private const array FUNCTION_NAMES = ['array_any', 'array_all']; public function __construct( - private readonly StaticTypeMapper $staticTypeMapper, + private readonly ArrayCallbackParamTypeResolver $arrayCallbackParamTypeResolver, ) { } @@ -72,79 +61,11 @@ public function getNodeTypes(): array */ public function refactor(Node $node): ?Node { - if ($node->isFirstClassCallable()) { - return null; - } - - if (! $this->isNames($node, self::FUNCTION_NAMES)) { - return null; - } - - $arrayArg = $node->getArg('array', 0); - $callbackArg = $node->getArg('callback', 1); - if (! $arrayArg instanceof Arg || ! $callbackArg instanceof Arg) { - return null; - } - - $callbackExpr = $callbackArg->value; - if (! $callbackExpr instanceof ArrowFunction && ! $callbackExpr instanceof Closure) { - return null; - } - - $valueParam = $callbackExpr->getParams()[0] ?? null; - if (! $valueParam instanceof Param) { - return null; - } - - // only fill in a param that has no type yet - if ($valueParam->type instanceof Node) { - return null; - } - - $itemType = $this->resolveArrayItemType($this->getType($arrayArg->value)); - if (! $itemType instanceof Type) { - return null; - } - - if ($itemType instanceof MixedType) { - return null; - } - - $paramTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($itemType, TypeKind::PARAM); - if (! $paramTypeNode instanceof Node) { - return null; - } - - $valueParam->type = $paramTypeNode; - - return $node; + return $this->arrayCallbackParamTypeResolver->refactorFirstParamType($node, self::FUNCTION_NAMES, false); } public function provideMinPhpVersion(): int { return PhpVersionFeature::ARRAY_ANY; } - - private function resolveArrayItemType(Type $arrayType): ?Type - { - if ($arrayType instanceof ConstantArrayType || $arrayType instanceof ArrayType) { - return $arrayType->getItemType(); - } - - if ($arrayType instanceof IntersectionType) { - foreach ($arrayType->getTypes() as $subType) { - if ($subType instanceof AccessoryArrayListType) { - continue; - } - - if (! $subType instanceof ArrayType) { - continue; - } - - return $subType->getItemType(); - } - } - - return null; - } } diff --git a/rules/TypeDeclaration/Rector/FuncCall/NarrowArrayAnyAllNullableParamTypeRector.php b/rules/TypeDeclaration/Rector/FuncCall/NarrowArrayAnyAllNullableParamTypeRector.php index 11503232b7e..0e181b41000 100644 --- a/rules/TypeDeclaration/Rector/FuncCall/NarrowArrayAnyAllNullableParamTypeRector.php +++ b/rules/TypeDeclaration/Rector/FuncCall/NarrowArrayAnyAllNullableParamTypeRector.php @@ -5,21 +5,9 @@ namespace Rector\TypeDeclaration\Rector\FuncCall; use PhpParser\Node; -use PhpParser\Node\Arg; -use PhpParser\Node\Expr\ArrowFunction; -use PhpParser\Node\Expr\Closure; use PhpParser\Node\Expr\FuncCall; -use PhpParser\Node\NullableType; -use PhpParser\Node\Param; -use PHPStan\Type\Accessory\AccessoryArrayListType; -use PHPStan\Type\ArrayType; -use PHPStan\Type\Constant\ConstantArrayType; -use PHPStan\Type\IntersectionType; -use PHPStan\Type\MixedType; -use PHPStan\Type\Type; -use Rector\PHPStanStaticTypeMapper\Enum\TypeKind; use Rector\Rector\AbstractRector; -use Rector\StaticTypeMapper\StaticTypeMapper; +use Rector\TypeDeclaration\NodeAnalyzer\ArrayCallbackParamTypeResolver; use Rector\ValueObject\PhpVersionFeature; use Rector\VersionBonding\Contract\MinPhpVersionInterface; use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; @@ -36,7 +24,7 @@ final class NarrowArrayAnyAllNullableParamTypeRector extends AbstractRector impl private const array FUNCTION_NAMES = ['array_any', 'array_all', 'array_find', 'array_find_key']; public function __construct( - private readonly StaticTypeMapper $staticTypeMapper, + private readonly ArrayCallbackParamTypeResolver $arrayCallbackParamTypeResolver, ) { } @@ -73,80 +61,11 @@ public function getNodeTypes(): array */ public function refactor(Node $node): ?Node { - if ($node->isFirstClassCallable()) { - return null; - } - - if (! $this->isNames($node, self::FUNCTION_NAMES)) { - return null; - } - - $arrayArg = $node->getArg('array', 0); - $callbackArg = $node->getArg('callback', 1); - if (! $arrayArg instanceof Arg || ! $callbackArg instanceof Arg) { - return null; - } - - $callbackExpr = $callbackArg->value; - if (! $callbackExpr instanceof ArrowFunction && ! $callbackExpr instanceof Closure) { - return null; - } - - $valueParam = $callbackExpr->getParams()[0] ?? null; - if (! $valueParam instanceof Param) { - return null; - } - - // only narrow a param that currently allows null - if (! $valueParam->type instanceof NullableType) { - return null; - } - - $itemType = $this->resolveArrayItemType($this->getType($arrayArg->value)); - if (! $itemType instanceof Type) { - return null; - } - - // nothing to narrow when the item type is itself nullable or unknown - if ($itemType instanceof MixedType || ! $itemType->isNull()->no()) { - return null; - } - - $paramTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($itemType, TypeKind::PARAM); - if (! $paramTypeNode instanceof Node) { - return null; - } - - $valueParam->type = $paramTypeNode; - - return $node; + return $this->arrayCallbackParamTypeResolver->refactorFirstParamType($node, self::FUNCTION_NAMES, true); } public function provideMinPhpVersion(): int { return PhpVersionFeature::ARRAY_ANY; } - - private function resolveArrayItemType(Type $arrayType): ?Type - { - if ($arrayType instanceof ConstantArrayType || $arrayType instanceof ArrayType) { - return $arrayType->getItemType(); - } - - if ($arrayType instanceof IntersectionType) { - foreach ($arrayType->getTypes() as $subType) { - if ($subType instanceof AccessoryArrayListType) { - continue; - } - - if (! $subType instanceof ArrayType) { - continue; - } - - return $subType->getItemType(); - } - } - - return null; - } }