diff --git a/phpstan.neon b/phpstan.neon index 756eba937d9..04e6ce25fe1 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -219,6 +219,8 @@ parameters: - '#Register "Rector\\Php80\\Rector\\NotIdentical\\MbStrContainsRector" service to "php80\.php" config set#' + - '#Register "Rector\\Php85\\Rector\\StmtsAwareInterface\\NestedToPipeOperatorRector" service to "php85\.php" config set#' + # closure detailed - '#Method Rector\\Config\\RectorConfig\:\:singleton\(\) has parameter \$concrete with no signature specified for Closure#' diff --git a/rules-tests/Php85/Rector/StmtsAwareInterface/NestedToPipeOperatorRector/Fixture/basic.php.inc b/rules-tests/Php85/Rector/StmtsAwareInterface/NestedToPipeOperatorRector/Fixture/basic.php.inc new file mode 100644 index 00000000000..b9a9e57d3d3 --- /dev/null +++ b/rules-tests/Php85/Rector/StmtsAwareInterface/NestedToPipeOperatorRector/Fixture/basic.php.inc @@ -0,0 +1,17 @@ + +----- + function3(...) |> function2(...) |> function1(...); +?> \ No newline at end of file diff --git a/rules-tests/Php85/Rector/StmtsAwareInterface/NestedToPipeOperatorRector/Fixture/nested_func.php.inc b/rules-tests/Php85/Rector/StmtsAwareInterface/NestedToPipeOperatorRector/Fixture/nested_func.php.inc new file mode 100644 index 00000000000..05a509160ad --- /dev/null +++ b/rules-tests/Php85/Rector/StmtsAwareInterface/NestedToPipeOperatorRector/Fixture/nested_func.php.inc @@ -0,0 +1,13 @@ + +----- + trim(...); +?> \ No newline at end of file diff --git a/rules-tests/Php85/Rector/StmtsAwareInterface/NestedToPipeOperatorRector/NestedToPipeOperatorRectorTest.php b/rules-tests/Php85/Rector/StmtsAwareInterface/NestedToPipeOperatorRector/NestedToPipeOperatorRectorTest.php new file mode 100644 index 00000000000..471384621ae --- /dev/null +++ b/rules-tests/Php85/Rector/StmtsAwareInterface/NestedToPipeOperatorRector/NestedToPipeOperatorRectorTest.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/Php85/Rector/StmtsAwareInterface/NestedToPipeOperatorRector/config/configured_rule.php b/rules-tests/Php85/Rector/StmtsAwareInterface/NestedToPipeOperatorRector/config/configured_rule.php new file mode 100644 index 00000000000..c3be1247dd9 --- /dev/null +++ b/rules-tests/Php85/Rector/StmtsAwareInterface/NestedToPipeOperatorRector/config/configured_rule.php @@ -0,0 +1,10 @@ +rule(NestedToPipeOperatorRector::class); +}; diff --git a/rules/Php85/Rector/StmtsAwareInterface/NestedToPipeOperatorRector.php b/rules/Php85/Rector/StmtsAwareInterface/NestedToPipeOperatorRector.php new file mode 100644 index 00000000000..c625076b6a0 --- /dev/null +++ b/rules/Php85/Rector/StmtsAwareInterface/NestedToPipeOperatorRector.php @@ -0,0 +1,315 @@ + function3(...) + |> function2(...) + |> function1(...); +CODE_SAMPLE + ), + ] + ); + } + + public function getNodeTypes(): array + { + return [StmtsAwareInterface::class]; + } + + public function provideMinPhpVersion(): int + { + return PhpVersionFeature::PIPE_OPERATOER; + } + + public function refactor(Node $node): ?Node + { + if (! $node instanceof StmtsAwareInterface || $node->stmts === null) { + return null; + } + + $hasChanged = false; + + // First, try to transform sequential assignments + $sequentialChanged = $this->transformSequentialAssignments($node); + if ($sequentialChanged) { + $hasChanged = true; + } + + // Then, transform nested function calls + $nestedChanged = $this->transformNestedCalls($node); + if ($nestedChanged) { + $hasChanged = true; + } + + return $hasChanged ? $node : null; + } + + private function transformSequentialAssignments(StmtsAwareInterface $node): bool + { + $hasChanged = false; + $statements = $node->stmts; + $totalStatements = count($statements) - 1; + + for ($i = 0; $i < $totalStatements; ++$i) { + $chain = $this->findAssignmentChain($statements, $i); + + if ($chain && count($chain) >= 2) { + $this->processAssignmentChain($node, $chain, $i); + $hasChanged = true; + // Skip processed statements + $i += count($chain) - 1; + } + } + + return $hasChanged; + } + + /** + * @param array $statements + * @return array|null + */ + private function findAssignmentChain(array $statements, int $startIndex): ?array + { + $chain = []; + $currentIndex = $startIndex; + $totalStatements = count($statements); + + while ($currentIndex < $totalStatements) { + $stmt = $statements[$currentIndex]; + + if (! $stmt instanceof Expression) { + break; + } + + $expr = $stmt->expr; + if (! $expr instanceof Assign) { + return null; + } + + // Check if this is a simple function call with one argument + if (! $expr->expr instanceof FuncCall) { + return null; + } + + $funcCall = $expr->expr; + if (count($funcCall->args) !== 1) { + return null; + } + + $arg = $funcCall->args[0]; + if (! $arg instanceof Arg) { + return null; + } + + if ($currentIndex === $startIndex) { + + // First in chain - must be a variable or simple value + if (! $arg->value instanceof Variable && ! $this->isSimpleValue($arg->value)) { + return null; + } + $chain[] = [ + 'stmt' => $stmt, + 'assign' => $expr, + 'funcCall' => $funcCall, + ]; + } else { + // Subsequent in chain - must use previous assignment's variable + $previousAssign = $chain[count($chain) - 1]['assign']; + $previousVarName = $this->getName($previousAssign->var); + + if (! $arg->value instanceof Variable || $this->getName($arg->value) !== $previousVarName) { + break; + } + $chain[] = [ + 'stmt' => $stmt, + 'assign' => $expr, + 'funcCall' => $funcCall, + ]; + } + + ++$currentIndex; + } + + return $chain; + } + + private function isSimpleValue(Expr $expr): bool + { + return $expr instanceof Variable + || $expr instanceof ConstFetch + || $expr instanceof String_ + || $expr instanceof Float_ + || $expr instanceof Int_ + || $expr instanceof Array_; + } + + /** + * @param array $chain + */ + private function processAssignmentChain(StmtsAwareInterface $node, array $chain, int $startIndex): void + { + $firstAssignment = $chain[0]['assign']; + $lastAssignment = $chain[count($chain) - 1]['assign']; + + // Get the initial value from the first function call's argument + $firstFuncCall = $chain[0]['funcCall']; + + if (! $firstFuncCall instanceof FuncCall) { + return; + } + + $firstArg = $firstFuncCall->args[0]; + if (! $firstArg instanceof Arg) { + return; + } + + $initialValue = $firstArg->value; + + // Build the pipe chain + $pipeExpression = $initialValue; + + foreach ($chain as $chainItem) { + $funcCall = $chainItem['funcCall']; + $placeholderCall = $this->createPlaceholderCall($funcCall); + $pipeExpression = new Node\Expr\BinaryOp\Pipe($pipeExpression, $placeholderCall); + } + + if (! $lastAssignment instanceof Assign) { + return; + } + // Create the final assignment + $finalAssignment = new Assign($lastAssignment->var, $pipeExpression); + $finalExpression = new Expression($finalAssignment); + + // Replace the statements + $endIndex = $startIndex + count($chain) - 1; + + // Remove all intermediate statements and replace with the final pipe expression + for ($i = $startIndex; $i <= $endIndex; ++$i) { + if ($i === $startIndex) { + $node->stmts[$i] = $finalExpression; + } else { + unset($node->stmts[$i]); + } + } + + $stmts = array_values($node->stmts); + + // Reindex the array + $node->stmts = $stmts; + } + + private function transformNestedCalls(StmtsAwareInterface $node): bool + { + $hasChanged = false; + + foreach ($node->stmts as $stmt) { + if (! $stmt instanceof Expression) { + continue; + } + + $expr = $stmt->expr; + + if ($expr instanceof Assign) { + $assignedValue = $expr->expr; + $processedValue = $this->processNestedCalls($assignedValue); + + if ($processedValue !== null && $processedValue !== $assignedValue) { + $expr->expr = $processedValue; + $hasChanged = true; + } + } elseif ($expr instanceof FuncCall) { + $processedValue = $this->processNestedCalls($expr); + + if ($processedValue !== null && $processedValue !== $expr) { + $stmt->expr = $processedValue; + $hasChanged = true; + } + } + } + + return $hasChanged; + } + + private function processNestedCalls(Node $node): ?Expr + { + if (! $node instanceof FuncCall) { + return null; + } + + // Check if any argument is a function call + foreach ($node->args as $arg) { + if (! $arg instanceof Arg) { + return null; + } + if ($arg->value instanceof FuncCall) { + return $this->buildPipeExpression($node, $arg->value); + } + } + + return null; + } + + private function buildPipeExpression(FuncCall $outerCall, FuncCall $innerCall): Node\Expr\BinaryOp\Pipe + { + $pipe = new Node\Expr\BinaryOp\Pipe($innerCall, $this->createPlaceholderCall($outerCall)); + + return $pipe; + } + + private function createPlaceholderCall(FuncCall $originalCall): FuncCall + { + $newArgs = []; + foreach ($originalCall->args as $arg) { + $newArgs[] = new VariadicPlaceholder(); + } + + return new FuncCall($originalCall->name, $newArgs); + } +} diff --git a/src/ValueObject/PhpVersionFeature.php b/src/ValueObject/PhpVersionFeature.php index 6730eebcb47..8360b704856 100644 --- a/src/ValueObject/PhpVersionFeature.php +++ b/src/ValueObject/PhpVersionFeature.php @@ -834,4 +834,10 @@ final class PhpVersionFeature * @var int */ public const DEPRECATE_ORD_WITH_MULTIBYTE_STRING = PhpVersion::PHP_85; + + /** + * @see https://wiki.php.net/rfc/pipe-operator-v3 + * @var int + */ + public const PIPE_OPERATOER = PhpVersion::PHP_85; }