1 <?php declare(strict_types=1);
5 use PhpParser\Node\Expr;
6 use PhpParser\Node\Scalar\String_;
7 use PhpParser\NodeVisitor;
8 use PHPUnit\Framework\TestCase;
10 class NodeTraverserTest extends TestCase
12 public function testNonModifying() {
13 $str1Node = new String_('Foo');
14 $str2Node = new String_('Bar');
15 $echoNode = new Node\Stmt\Echo_([$str1Node, $str2Node]);
18 $visitor = $this->getMockBuilder(NodeVisitor::class)->getMock();
20 $visitor->expects($this->at(0))->method('beforeTraverse')->with($stmts);
21 $visitor->expects($this->at(1))->method('enterNode')->with($echoNode);
22 $visitor->expects($this->at(2))->method('enterNode')->with($str1Node);
23 $visitor->expects($this->at(3))->method('leaveNode')->with($str1Node);
24 $visitor->expects($this->at(4))->method('enterNode')->with($str2Node);
25 $visitor->expects($this->at(5))->method('leaveNode')->with($str2Node);
26 $visitor->expects($this->at(6))->method('leaveNode')->with($echoNode);
27 $visitor->expects($this->at(7))->method('afterTraverse')->with($stmts);
29 $traverser = new NodeTraverser;
30 $traverser->addVisitor($visitor);
32 $this->assertEquals($stmts, $traverser->traverse($stmts));
35 public function testModifying() {
36 $str1Node = new String_('Foo');
37 $str2Node = new String_('Bar');
38 $printNode = new Expr\Print_($str1Node);
40 // first visitor changes the node, second verifies the change
41 $visitor1 = $this->getMockBuilder(NodeVisitor::class)->getMock();
42 $visitor2 = $this->getMockBuilder(NodeVisitor::class)->getMock();
44 // replace empty statements with string1 node
45 $visitor1->expects($this->at(0))->method('beforeTraverse')->with([])
46 ->will($this->returnValue([$str1Node]));
47 $visitor2->expects($this->at(0))->method('beforeTraverse')->with([$str1Node]);
49 // replace string1 node with print node
50 $visitor1->expects($this->at(1))->method('enterNode')->with($str1Node)
51 ->will($this->returnValue($printNode));
52 $visitor2->expects($this->at(1))->method('enterNode')->with($printNode);
54 // replace string1 node with string2 node
55 $visitor1->expects($this->at(2))->method('enterNode')->with($str1Node)
56 ->will($this->returnValue($str2Node));
57 $visitor2->expects($this->at(2))->method('enterNode')->with($str2Node);
59 // replace string2 node with string1 node again
60 $visitor1->expects($this->at(3))->method('leaveNode')->with($str2Node)
61 ->will($this->returnValue($str1Node));
62 $visitor2->expects($this->at(3))->method('leaveNode')->with($str1Node);
64 // replace print node with string1 node again
65 $visitor1->expects($this->at(4))->method('leaveNode')->with($printNode)
66 ->will($this->returnValue($str1Node));
67 $visitor2->expects($this->at(4))->method('leaveNode')->with($str1Node);
69 // replace string1 node with empty statements again
70 $visitor1->expects($this->at(5))->method('afterTraverse')->with([$str1Node])
71 ->will($this->returnValue([]));
72 $visitor2->expects($this->at(5))->method('afterTraverse')->with([]);
74 $traverser = new NodeTraverser;
75 $traverser->addVisitor($visitor1);
76 $traverser->addVisitor($visitor2);
78 // as all operations are reversed we end where we start
79 $this->assertEquals([], $traverser->traverse([]));
82 public function testRemove() {
83 $str1Node = new String_('Foo');
84 $str2Node = new String_('Bar');
86 $visitor = $this->getMockBuilder(NodeVisitor::class)->getMock();
88 // remove the string1 node, leave the string2 node
89 $visitor->expects($this->at(2))->method('leaveNode')->with($str1Node)
90 ->will($this->returnValue(NodeTraverser::REMOVE_NODE));
92 $traverser = new NodeTraverser;
93 $traverser->addVisitor($visitor);
95 $this->assertEquals([$str2Node], $traverser->traverse([$str1Node, $str2Node]));
98 public function testMerge() {
99 $strStart = new String_('Start');
100 $strMiddle = new String_('End');
101 $strEnd = new String_('Middle');
102 $strR1 = new String_('Replacement 1');
103 $strR2 = new String_('Replacement 2');
105 $visitor = $this->getMockBuilder(NodeVisitor::class)->getMock();
107 // replace strMiddle with strR1 and strR2 by merge
108 $visitor->expects($this->at(4))->method('leaveNode')->with($strMiddle)
109 ->will($this->returnValue([$strR1, $strR2]));
111 $traverser = new NodeTraverser;
112 $traverser->addVisitor($visitor);
115 [$strStart, $strR1, $strR2, $strEnd],
116 $traverser->traverse([$strStart, $strMiddle, $strEnd])
120 public function testInvalidDeepArray() {
121 $this->expectException(\LogicException::class);
122 $this->expectExceptionMessage('Invalid node structure: Contains nested arrays');
123 $strNode = new String_('Foo');
124 $stmts = [[[$strNode]]];
126 $traverser = new NodeTraverser;
127 $this->assertEquals($stmts, $traverser->traverse($stmts));
130 public function testDontTraverseChildren() {
131 $strNode = new String_('str');
132 $printNode = new Expr\Print_($strNode);
133 $varNode = new Expr\Variable('foo');
134 $mulNode = new Expr\BinaryOp\Mul($varNode, $varNode);
135 $negNode = new Expr\UnaryMinus($mulNode);
136 $stmts = [$printNode, $negNode];
138 $visitor1 = $this->getMockBuilder(NodeVisitor::class)->getMock();
139 $visitor2 = $this->getMockBuilder(NodeVisitor::class)->getMock();
141 $visitor1->expects($this->at(1))->method('enterNode')->with($printNode)
142 ->will($this->returnValue(NodeTraverser::DONT_TRAVERSE_CHILDREN));
143 $visitor2->expects($this->at(1))->method('enterNode')->with($printNode);
145 $visitor1->expects($this->at(2))->method('leaveNode')->with($printNode);
146 $visitor2->expects($this->at(2))->method('leaveNode')->with($printNode);
148 $visitor1->expects($this->at(3))->method('enterNode')->with($negNode);
149 $visitor2->expects($this->at(3))->method('enterNode')->with($negNode);
151 $visitor1->expects($this->at(4))->method('enterNode')->with($mulNode);
152 $visitor2->expects($this->at(4))->method('enterNode')->with($mulNode)
153 ->will($this->returnValue(NodeTraverser::DONT_TRAVERSE_CHILDREN));
155 $visitor1->expects($this->at(5))->method('leaveNode')->with($mulNode);
156 $visitor2->expects($this->at(5))->method('leaveNode')->with($mulNode);
158 $visitor1->expects($this->at(6))->method('leaveNode')->with($negNode);
159 $visitor2->expects($this->at(6))->method('leaveNode')->with($negNode);
161 $traverser = new NodeTraverser;
162 $traverser->addVisitor($visitor1);
163 $traverser->addVisitor($visitor2);
165 $this->assertEquals($stmts, $traverser->traverse($stmts));
168 public function testDontTraverseCurrentAndChildren() {
169 // print 'str'; -($foo * $foo);
170 $strNode = new String_('str');
171 $printNode = new Expr\Print_($strNode);
172 $varNode = new Expr\Variable('foo');
173 $mulNode = new Expr\BinaryOp\Mul($varNode, $varNode);
174 $divNode = new Expr\BinaryOp\Div($varNode, $varNode);
175 $negNode = new Expr\UnaryMinus($mulNode);
176 $stmts = [$printNode, $negNode];
178 $visitor1 = $this->getMockBuilder(NodeVisitor::class)->getMock();
179 $visitor2 = $this->getMockBuilder(NodeVisitor::class)->getMock();
181 $visitor1->expects($this->at(1))->method('enterNode')->with($printNode)
182 ->will($this->returnValue(NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN));
183 $visitor1->expects($this->at(2))->method('leaveNode')->with($printNode);
185 $visitor1->expects($this->at(3))->method('enterNode')->with($negNode);
186 $visitor2->expects($this->at(1))->method('enterNode')->with($negNode);
188 $visitor1->expects($this->at(4))->method('enterNode')->with($mulNode)
189 ->will($this->returnValue(NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN));
190 $visitor1->expects($this->at(5))->method('leaveNode')->with($mulNode)->willReturn($divNode);
192 $visitor1->expects($this->at(6))->method('leaveNode')->with($negNode);
193 $visitor2->expects($this->at(2))->method('leaveNode')->with($negNode);
195 $traverser = new NodeTraverser;
196 $traverser->addVisitor($visitor1);
197 $traverser->addVisitor($visitor2);
199 $resultStmts = $traverser->traverse($stmts);
201 $this->assertInstanceOf(Expr\BinaryOp\Div::class, $resultStmts[1]->expr);
204 public function testStopTraversal() {
205 $varNode1 = new Expr\Variable('a');
206 $varNode2 = new Expr\Variable('b');
207 $varNode3 = new Expr\Variable('c');
208 $mulNode = new Expr\BinaryOp\Mul($varNode1, $varNode2);
209 $printNode = new Expr\Print_($varNode3);
210 $stmts = [$mulNode, $printNode];
212 // From enterNode() with array parent
213 $visitor = $this->getMockBuilder(NodeVisitor::class)->getMock();
214 $visitor->expects($this->at(1))->method('enterNode')->with($mulNode)
215 ->will($this->returnValue(NodeTraverser::STOP_TRAVERSAL));
216 $visitor->expects($this->at(2))->method('afterTraverse');
217 $traverser = new NodeTraverser;
218 $traverser->addVisitor($visitor);
219 $this->assertEquals($stmts, $traverser->traverse($stmts));
221 // From enterNode with Node parent
222 $visitor = $this->getMockBuilder(NodeVisitor::class)->getMock();
223 $visitor->expects($this->at(2))->method('enterNode')->with($varNode1)
224 ->will($this->returnValue(NodeTraverser::STOP_TRAVERSAL));
225 $visitor->expects($this->at(3))->method('afterTraverse');
226 $traverser = new NodeTraverser;
227 $traverser->addVisitor($visitor);
228 $this->assertEquals($stmts, $traverser->traverse($stmts));
230 // From leaveNode with Node parent
231 $visitor = $this->getMockBuilder(NodeVisitor::class)->getMock();
232 $visitor->expects($this->at(3))->method('leaveNode')->with($varNode1)
233 ->will($this->returnValue(NodeTraverser::STOP_TRAVERSAL));
234 $visitor->expects($this->at(4))->method('afterTraverse');
235 $traverser = new NodeTraverser;
236 $traverser->addVisitor($visitor);
237 $this->assertEquals($stmts, $traverser->traverse($stmts));
239 // From leaveNode with array parent
240 $visitor = $this->getMockBuilder(NodeVisitor::class)->getMock();
241 $visitor->expects($this->at(6))->method('leaveNode')->with($mulNode)
242 ->will($this->returnValue(NodeTraverser::STOP_TRAVERSAL));
243 $visitor->expects($this->at(7))->method('afterTraverse');
244 $traverser = new NodeTraverser;
245 $traverser->addVisitor($visitor);
246 $this->assertEquals($stmts, $traverser->traverse($stmts));
248 // Check that pending array modifications are still carried out
249 $visitor = $this->getMockBuilder(NodeVisitor::class)->getMock();
250 $visitor->expects($this->at(6))->method('leaveNode')->with($mulNode)
251 ->will($this->returnValue(NodeTraverser::REMOVE_NODE));
252 $visitor->expects($this->at(7))->method('enterNode')->with($printNode)
253 ->will($this->returnValue(NodeTraverser::STOP_TRAVERSAL));
254 $visitor->expects($this->at(8))->method('afterTraverse');
255 $traverser = new NodeTraverser;
256 $traverser->addVisitor($visitor);
257 $this->assertEquals([$printNode], $traverser->traverse($stmts));
261 public function testRemovingVisitor() {
262 $visitor1 = $this->getMockBuilder(NodeVisitor::class)->getMock();
263 $visitor2 = $this->getMockBuilder(NodeVisitor::class)->getMock();
264 $visitor3 = $this->getMockBuilder(NodeVisitor::class)->getMock();
266 $traverser = new NodeTraverser;
267 $traverser->addVisitor($visitor1);
268 $traverser->addVisitor($visitor2);
269 $traverser->addVisitor($visitor3);
271 $preExpected = [$visitor1, $visitor2, $visitor3];
272 $this->assertAttributeSame($preExpected, 'visitors', $traverser, 'The appropriate visitors have not been added');
274 $traverser->removeVisitor($visitor2);
276 $postExpected = [0 => $visitor1, 2 => $visitor3];
277 $this->assertAttributeSame($postExpected, 'visitors', $traverser, 'The appropriate visitors are not present after removal');
280 public function testNoCloneNodes() {
281 $stmts = [new Node\Stmt\Echo_([new String_('Foo'), new String_('Bar')])];
283 $traverser = new NodeTraverser;
285 $this->assertSame($stmts, $traverser->traverse($stmts));
289 * @dataProvider provideTestInvalidReturn
291 public function testInvalidReturn($visitor, $message) {
292 $this->expectException(\LogicException::class);
293 $this->expectExceptionMessage($message);
295 $stmts = [new Node\Stmt\Expression(new Node\Scalar\LNumber(42))];
297 $traverser = new NodeTraverser();
298 $traverser->addVisitor($visitor);
299 $traverser->traverse($stmts);
302 public function provideTestInvalidReturn() {
303 $visitor1 = $this->getMockBuilder(NodeVisitor::class)->getMock();
304 $visitor1->expects($this->at(1))->method('enterNode')
305 ->willReturn('foobar');
307 $visitor2 = $this->getMockBuilder(NodeVisitor::class)->getMock();
308 $visitor2->expects($this->at(2))->method('enterNode')
309 ->willReturn('foobar');
311 $visitor3 = $this->getMockBuilder(NodeVisitor::class)->getMock();
312 $visitor3->expects($this->at(3))->method('leaveNode')
313 ->willReturn('foobar');
315 $visitor4 = $this->getMockBuilder(NodeVisitor::class)->getMock();
316 $visitor4->expects($this->at(4))->method('leaveNode')
317 ->willReturn('foobar');
319 $visitor5 = $this->getMockBuilder(NodeVisitor::class)->getMock();
320 $visitor5->expects($this->at(3))->method('leaveNode')
321 ->willReturn([new Node\Scalar\DNumber(42.0)]);
323 $visitor6 = $this->getMockBuilder(NodeVisitor::class)->getMock();
324 $visitor6->expects($this->at(4))->method('leaveNode')
327 $visitor7 = $this->getMockBuilder(NodeVisitor::class)->getMock();
328 $visitor7->expects($this->at(1))->method('enterNode')
329 ->willReturn(new Node\Scalar\LNumber(42));
331 $visitor8 = $this->getMockBuilder(NodeVisitor::class)->getMock();
332 $visitor8->expects($this->at(2))->method('enterNode')
333 ->willReturn(new Node\Stmt\Return_());
336 [$visitor1, 'enterNode() returned invalid value of type string'],
337 [$visitor2, 'enterNode() returned invalid value of type string'],
338 [$visitor3, 'leaveNode() returned invalid value of type string'],
339 [$visitor4, 'leaveNode() returned invalid value of type string'],
340 [$visitor5, 'leaveNode() may only return an array if the parent structure is an array'],
341 [$visitor6, 'bool(false) return from leaveNode() no longer supported. Return NodeTraverser::REMOVE_NODE instead'],
342 [$visitor7, 'Trying to replace statement (Stmt_Expression) with expression (Scalar_LNumber). Are you missing a Stmt_Expression wrapper?'],
343 [$visitor8, 'Trying to replace expression (Scalar_LNumber) with statement (Stmt_Return)'],