Skip to content

Commit 2c6591e

Browse files
committed
pipe
1 parent 20db305 commit 2c6591e

4 files changed

Lines changed: 337 additions & 0 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace Rector\Tests\Php85\Rector\StmtsAwareInterface\NestedToPipeOperatorRector\Fixture;
4+
5+
$value = "hello world";
6+
$result1 = function3($value);
7+
$result2 = function2($result1);
8+
$result = function1($result2);
9+
?>
10+
-----
11+
<?php
12+
13+
namespace Rector\Tests\Php85\Rector\StmtsAwareInterface\NestedToPipeOperatorRector\Fixture;
14+
15+
$value = "hello world";
16+
17+
$result = $value
18+
|> function3(...)
19+
|> function2(...)
20+
|> function1(...);
21+
?>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Tests\Php85\Rector\StmtsAwareInterface\NestedToPipeOperatorRector;
6+
7+
use Iterator;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
10+
11+
final class NestedToPipeOperatorRectorTest extends AbstractRectorTestCase
12+
{
13+
#[DataProvider('provideData')]
14+
public function test(string $filePath): void
15+
{
16+
$this->doTestFile($filePath);
17+
}
18+
19+
public static function provideData(): Iterator
20+
{
21+
return self::yieldFilesFromDirectory(__DIR__ . '/Fixture');
22+
}
23+
24+
public function provideConfigFilePath(): string
25+
{
26+
return __DIR__ . '/config/configured_rule.php';
27+
}
28+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Rector\Config\RectorConfig;
6+
use Rector\Php85\Rector\StmtsAwareInterface\NestedToPipeOperatorRector;
7+
8+
return static function (RectorConfig $rectorConfig): void {
9+
$rectorConfig->rule(NestedToPipeOperatorRector::class);
10+
};
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Php85\Rector\StmtsAwareInterface;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Expr\Assign;
9+
use PhpParser\Node\Expr\FuncCall;
10+
use PhpParser\Node\Expr\Variable;
11+
use PhpParser\Node\Stmt\Expression;
12+
use PhpParser\Node\VariadicPlaceholder;
13+
use Rector\Contract\PhpParser\Node\StmtsAwareInterface;
14+
use Rector\Rector\AbstractRector;
15+
use Rector\ValueObject\PhpVersionFeature;
16+
use Rector\VersionBonding\Contract\MinPhpVersionInterface;
17+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
18+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
19+
20+
/**
21+
* @see https://wiki.php.net/rfc/pipe-operator-v3
22+
* @see \Rector\Tests\Php85\Rector\StmtsAwareInterface\NestedToPipeOperatorRector\NestedToPipeOperatorRectorTest
23+
*/
24+
final class NestedToPipeOperatorRector extends AbstractRector implements MinPhpVersionInterface
25+
{
26+
public function getRuleDefinition(): RuleDefinition
27+
{
28+
return new RuleDefinition(
29+
'Transform nested function calls and sequential assignments to pipe operator syntax',
30+
[
31+
new CodeSample(
32+
<<<'CODE_SAMPLE'
33+
$value = "hello world";
34+
$result1 = function3($value);
35+
$result2 = function2($result1);
36+
$result = function1($result2);
37+
CODE_SAMPLE
38+
,
39+
<<<'CODE_SAMPLE'
40+
$value = "hello world";
41+
42+
$result = $value
43+
|> function3(...)
44+
|> function2(...)
45+
|> function1(...);
46+
CODE_SAMPLE
47+
),
48+
]
49+
);
50+
}
51+
52+
public function getNodeTypes(): array
53+
{
54+
return [StmtsAwareInterface::class];
55+
}
56+
57+
public function provideMinPhpVersion(): int
58+
{
59+
return PhpVersionFeature::PIPE_OPERATOER;
60+
}
61+
62+
public function refactor(Node $node): ?Node
63+
{
64+
if (! $node instanceof StmtsAwareInterface || $node->stmts === null) {
65+
return null;
66+
}
67+
68+
$hasChanged = false;
69+
70+
// First, try to transform sequential assignments
71+
$sequentialChanged = $this->transformSequentialAssignments($node);
72+
if ($sequentialChanged) {
73+
$hasChanged = true;
74+
}
75+
76+
// Then, transform nested function calls
77+
$nestedChanged = $this->transformNestedCalls($node);
78+
if ($nestedChanged) {
79+
$hasChanged = true;
80+
}
81+
82+
return $hasChanged ? $node : null;
83+
}
84+
85+
private function transformSequentialAssignments(StmtsAwareInterface $node): bool
86+
{
87+
$hasChanged = false;
88+
$statements = $node->stmts;
89+
90+
for ($i = 0; $i < count($statements) - 1; $i++) {
91+
$chain = $this->findAssignmentChain($statements, $i);
92+
93+
if ($chain !== null && count($chain) >= 2) {
94+
$this->processAssignmentChain($node, $chain, $i);
95+
$hasChanged = true;
96+
// Skip processed statements
97+
$i += count($chain) - 1;
98+
}
99+
}
100+
101+
return $hasChanged;
102+
}
103+
104+
private function findAssignmentChain(array $statements, int $startIndex): ?array
105+
{
106+
$chain = [];
107+
$currentIndex = $startIndex;
108+
109+
while ($currentIndex < count($statements)) {
110+
$stmt = $statements[$currentIndex];
111+
112+
if (! $stmt instanceof Expression) {
113+
break;
114+
}
115+
116+
$expr = $stmt->expr;
117+
if (! $expr instanceof Assign) {
118+
break;
119+
}
120+
121+
// Check if this is a simple function call with one argument
122+
if (! $expr->expr instanceof FuncCall) {
123+
break;
124+
}
125+
126+
$funcCall = $expr->expr;
127+
if (count($funcCall->args) !== 1) {
128+
break;
129+
}
130+
131+
$arg = $funcCall->args[0];
132+
133+
if ($currentIndex === $startIndex) {
134+
// First in chain - must be a variable or simple value
135+
if (! $arg->value instanceof Variable && ! $this->isSimpleValue($arg->value)) {
136+
break;
137+
}
138+
$chain[] = [
139+
'stmt' => $stmt,
140+
'assign' => $expr,
141+
'funcCall' => $funcCall,
142+
];
143+
} else {
144+
// Subsequent in chain - must use previous assignment's variable
145+
$previousAssign = $chain[count($chain) - 1]['assign'];
146+
$previousVarName = $this->getName($previousAssign->var);
147+
148+
if (! $arg->value instanceof Variable || $this->getName(
149+
$arg->value
150+
) !== $previousVarName) {
151+
break;
152+
}
153+
$chain[] = [
154+
'stmt' => $stmt,
155+
'assign' => $expr,
156+
'funcCall' => $funcCall,
157+
];
158+
}
159+
160+
$currentIndex++;
161+
}
162+
163+
return count($chain) >= 2 ? $chain : null;
164+
}
165+
166+
private function isSimpleValue(Node $node): bool
167+
{
168+
return $node instanceof Node\Scalar\String_
169+
|| $node instanceof Node\Scalar\LNumber
170+
|| $node instanceof Node\Scalar\DNumber
171+
|| $node instanceof Node\Expr\ConstFetch
172+
|| $node instanceof Node\Expr\Array_;
173+
}
174+
175+
private function processAssignmentChain(StmtsAwareInterface $node, array $chain, int $startIndex): void
176+
{
177+
$firstAssignment = $chain[0]['assign'];
178+
$lastAssignment = $chain[count($chain) - 1]['assign'];
179+
180+
// Get the initial value from the first function call's argument
181+
$firstFuncCall = $chain[0]['funcCall'];
182+
$initialValue = $firstFuncCall->args[0]->value;
183+
184+
// Build the pipe chain
185+
$pipeExpression = $initialValue;
186+
187+
foreach ($chain as $chainItem) {
188+
$funcCall = $chainItem['funcCall'];
189+
$placeholderCall = $this->createPlaceholderCall($funcCall);
190+
$pipeExpression = new Node\Expr\BinaryOp\Pipe($pipeExpression, $placeholderCall);
191+
}
192+
193+
// Create the final assignment
194+
$finalAssignment = new Assign($lastAssignment->var, $pipeExpression);
195+
$finalExpression = new Expression($finalAssignment);
196+
197+
// Replace the statements
198+
$endIndex = $startIndex + count($chain) - 1;
199+
200+
// Remove all intermediate statements and replace with the final pipe expression
201+
for ($i = $startIndex; $i <= $endIndex; $i++) {
202+
if ($i === $startIndex) {
203+
$node->stmts[$i] = $finalExpression;
204+
} else {
205+
unset($node->stmts[$i]);
206+
}
207+
}
208+
209+
$stmts = array_values($node->stmts);
210+
211+
// Reindex the array
212+
$node->stmts = $stmts;
213+
}
214+
215+
private function transformNestedCalls(StmtsAwareInterface $node): bool
216+
{
217+
$hasChanged = false;
218+
219+
foreach ($node->stmts as $stmt) {
220+
if (! $stmt instanceof Expression) {
221+
continue;
222+
}
223+
224+
$expr = $stmt->expr;
225+
226+
if ($expr instanceof Assign) {
227+
$assignedValue = $expr->expr;
228+
$processedValue = $this->processNestedCalls($assignedValue);
229+
230+
if ($processedValue !== null && $processedValue !== $assignedValue) {
231+
$expr->expr = $processedValue;
232+
$hasChanged = true;
233+
}
234+
} elseif ($expr instanceof FuncCall) {
235+
$processedValue = $this->processNestedCalls($expr);
236+
if ($processedValue !== null && $processedValue !== $expr) {
237+
$stmt->expr = $processedValue;
238+
$hasChanged = true;
239+
}
240+
}
241+
}
242+
243+
return $hasChanged;
244+
}
245+
246+
private function processNestedCalls(Node $node): ?Node
247+
{
248+
if (! $node instanceof FuncCall) {
249+
return null;
250+
}
251+
252+
// Check if any argument is a function call
253+
foreach ($node->args as $arg) {
254+
if ($arg->value instanceof FuncCall) {
255+
return $this->buildPipeExpression($node, $arg->value);
256+
}
257+
}
258+
259+
return null;
260+
}
261+
262+
private function buildPipeExpression(FuncCall $outerCall, FuncCall $innerCall): Node\Expr\BinaryOp\Pipe
263+
{
264+
$pipe = new Node\Expr\BinaryOp\Pipe($innerCall, $this->createPlaceholderCall($outerCall));
265+
266+
return $pipe;
267+
}
268+
269+
private function createPlaceholderCall(FuncCall $originalCall): FuncCall
270+
{
271+
$newArgs = [];
272+
foreach ($originalCall->args as $arg) {
273+
$newArgs[] = new VariadicPlaceholder();
274+
}
275+
276+
return new FuncCall($originalCall->name, $newArgs);
277+
}
278+
}

0 commit comments

Comments
 (0)