diff --git a/rules-tests/CodeQuality/Rector/StmtsAwareInterface/InitVariableDefaultBeforeNullCoalesceReturnRector/Fixture/array_dim_fill.php.inc b/rules-tests/CodeQuality/Rector/StmtsAwareInterface/InitVariableDefaultBeforeNullCoalesceReturnRector/Fixture/array_dim_fill.php.inc new file mode 100644 index 00000000000..6cf56e8106c --- /dev/null +++ b/rules-tests/CodeQuality/Rector/StmtsAwareInterface/InitVariableDefaultBeforeNullCoalesceReturnRector/Fixture/array_dim_fill.php.inc @@ -0,0 +1,36 @@ + +----- + diff --git a/rules-tests/CodeQuality/Rector/StmtsAwareInterface/InitVariableDefaultBeforeNullCoalesceReturnRector/Fixture/skip_already_initialized.php.inc b/rules-tests/CodeQuality/Rector/StmtsAwareInterface/InitVariableDefaultBeforeNullCoalesceReturnRector/Fixture/skip_already_initialized.php.inc new file mode 100644 index 00000000000..f13fd94570a --- /dev/null +++ b/rules-tests/CodeQuality/Rector/StmtsAwareInterface/InitVariableDefaultBeforeNullCoalesceReturnRector/Fixture/skip_already_initialized.php.inc @@ -0,0 +1,16 @@ + +----- + diff --git a/rules-tests/CodeQuality/Rector/StmtsAwareInterface/InitVariableDefaultBeforeNullCoalesceReturnRector/InitVariableDefaultBeforeNullCoalesceReturnRectorTest.php b/rules-tests/CodeQuality/Rector/StmtsAwareInterface/InitVariableDefaultBeforeNullCoalesceReturnRector/InitVariableDefaultBeforeNullCoalesceReturnRectorTest.php new file mode 100644 index 00000000000..78a668fcbbd --- /dev/null +++ b/rules-tests/CodeQuality/Rector/StmtsAwareInterface/InitVariableDefaultBeforeNullCoalesceReturnRector/InitVariableDefaultBeforeNullCoalesceReturnRectorTest.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/CodeQuality/Rector/StmtsAwareInterface/InitVariableDefaultBeforeNullCoalesceReturnRector/config/configured_rule.php b/rules-tests/CodeQuality/Rector/StmtsAwareInterface/InitVariableDefaultBeforeNullCoalesceReturnRector/config/configured_rule.php new file mode 100644 index 00000000000..3daae322b59 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/StmtsAwareInterface/InitVariableDefaultBeforeNullCoalesceReturnRector/config/configured_rule.php @@ -0,0 +1,10 @@ +rule(InitVariableDefaultBeforeNullCoalesceReturnRector::class); +}; diff --git a/rules/CodeQuality/Rector/StmtsAwareInterface/InitVariableDefaultBeforeNullCoalesceReturnRector.php b/rules/CodeQuality/Rector/StmtsAwareInterface/InitVariableDefaultBeforeNullCoalesceReturnRector.php new file mode 100644 index 00000000000..30f99a6defa --- /dev/null +++ b/rules/CodeQuality/Rector/StmtsAwareInterface/InitVariableDefaultBeforeNullCoalesceReturnRector.php @@ -0,0 +1,208 @@ +> + */ + public function getNodeTypes(): array + { + return NodeGroup::STMTS_AWARE; + } + + /** + * @param StmtsAware $node + */ + public function refactor(Node $node): ?Node + { + if ($node->stmts === null) { + return null; + } + + $stmts = $node->stmts; + + foreach ($stmts as $returnKey => $stmt) { + if (! $stmt instanceof Return_) { + continue; + } + + if (! $stmt->expr instanceof Coalesce) { + continue; + } + + $coalesce = $stmt->expr; + if (! $coalesce->left instanceof Variable) { + continue; + } + + if (! $this->isLiteralDefault($coalesce->right)) { + continue; + } + + $variableName = $this->getName($coalesce->left); + if ($variableName === null) { + continue; + } + + if (! $this->isFilledOnlyAsArrayDim($stmts, $variableName)) { + continue; + } + + $insertKey = $this->resolveFirstArrayDimWriteKey($stmts, $returnKey, $variableName); + if ($insertKey === null) { + continue; + } + + $initExpression = new Expression(new Assign(new Variable($variableName), $coalesce->right)); + array_splice($stmts, $insertKey, 0, [$initExpression]); + + $stmt->expr = $coalesce->left; + + $node->stmts = $stmts; + + return $node; + } + + return null; + } + + private function isLiteralDefault(Expr $expr): bool + { + return $expr instanceof Array_ || $expr instanceof Scalar || $expr instanceof ConstFetch; + } + + /** + * The variable must be written at least once as an array dimension and never assigned directly, + * otherwise the null coalesce guards a real value, not just an undefined variable. + * + * @param Stmt[] $stmts + */ + private function isFilledOnlyAsArrayDim(array $stmts, string $variableName): bool + { + $hasArrayDimAssign = false; + + $assigns = $this->betterNodeFinder->findInstancesOf($stmts, [Assign::class]); + foreach ($assigns as $assign) { + if ($assign->var instanceof Variable && $this->isName($assign->var, $variableName)) { + return false; + } + + if (! $assign->var instanceof ArrayDimFetch) { + continue; + } + + $rootVariable = $this->resolveArrayDimRootVariable($assign->var); + if ($rootVariable instanceof Variable && $this->isName($rootVariable, $variableName)) { + $hasArrayDimAssign = true; + } + } + + return $hasArrayDimAssign; + } + + /** + * @param Stmt[] $stmts + */ + private function resolveFirstArrayDimWriteKey(array $stmts, int $returnKey, string $variableName): ?int + { + foreach ($stmts as $key => $stmt) { + if ($key >= $returnKey) { + return null; + } + + $foundArrayDimAssign = $this->betterNodeFinder->findFirst($stmt, function (Node $subNode) use ( + $variableName + ): bool { + if (! $subNode instanceof Assign) { + return false; + } + + if (! $subNode->var instanceof ArrayDimFetch) { + return false; + } + + $expr = $this->resolveArrayDimRootVariable($subNode->var); + return $expr instanceof Variable && $this->isName($expr, $variableName); + }); + + if ($foundArrayDimAssign instanceof Node) { + return $key; + } + } + + return null; + } + + private function resolveArrayDimRootVariable(ArrayDimFetch $arrayDimFetch): Expr + { + $currentVariable = $arrayDimFetch->var; + while ($currentVariable instanceof ArrayDimFetch) { + $currentVariable = $currentVariable->var; + } + + return $currentVariable; + } +} diff --git a/src/Config/Level/CodeQualityLevel.php b/src/Config/Level/CodeQualityLevel.php index 92ffa6a1970..a8f3c4b69ab 100644 --- a/src/Config/Level/CodeQualityLevel.php +++ b/src/Config/Level/CodeQualityLevel.php @@ -74,6 +74,7 @@ use Rector\CodeQuality\Rector\NotEqual\CommonNotEqualRector; use Rector\CodeQuality\Rector\NullsafeMethodCall\CleanupUnneededNullsafeOperatorRector; use Rector\CodeQuality\Rector\Property\FixClassCaseSensitivityVarDocblockRector; +use Rector\CodeQuality\Rector\StmtsAwareInterface\InitVariableDefaultBeforeNullCoalesceReturnRector; use Rector\CodeQuality\Rector\StmtsAwareInterface\MoveInnerFunctionToTopLevelRector; use Rector\CodeQuality\Rector\Switch_\SingularSwitchToIfRector; use Rector\CodeQuality\Rector\Switch_\SwitchTrueToIfRector; @@ -121,6 +122,7 @@ final class CodeQualityLevel RepeatedOrEqualToInArrayRector::class, RepeatedAndNotEqualToNotInArrayRector::class, MoveInnerFunctionToTopLevelRector::class, + InitVariableDefaultBeforeNullCoalesceReturnRector::class, InnerFunctionToPrivateMethodRector::class, SimplifyForeachToCoalescingRector::class, SimplifyFuncGetArgsCountRector::class,