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])
121 * @expectedException \LogicException
122 * @expectedExceptionMessage Invalid node structure: Contains nested arrays
124 public function testInvalidDeepArray() {
125 $strNode = new String_('Foo');
126 $stmts = [[[$strNode]]];
128 $traverser = new NodeTraverser;
129 $this->assertEquals($stmts, $traverser->traverse($stmts));
132 public function testDontTraverseChildren() {
133 $strNode = new String_('str');
134 $printNode = new Expr\Print_($strNode);
135 $varNode = new Expr\Variable('foo');
136 $mulNode = new Expr\BinaryOp\Mul($varNode, $varNode);
137 $negNode = new Expr\UnaryMinus($mulNode);
138 $stmts = [$printNode, $negNode];
140 $visitor1 = $this->getMockBuilder(NodeVisitor::class)->getMock();
141 $visitor2 = $this->getMockBuilder(NodeVisitor::class)->getMock();
143 $visitor1->expects($this->at(1))->method('enterNode')->with($printNode)
144 ->will($this->returnValue(NodeTraverser::DONT_TRAVERSE_CHILDREN));
145 $visitor2->expects($this->at(1))->method('enterNode')->with($printNode);
147 $visitor1->expects($this->at(2))->method('leaveNode')->with($printNode);
148 $visitor2->expects($this->at(2))->method('leaveNode')->with($printNode);
150 $visitor1->expects($this->at(3))->method('enterNode')->with($negNode);
151 $visitor2->expects($this->at(3))->method('enterNode')->with($negNode);
153 $visitor1->expects($this->at(4))->method('enterNode')->with($mulNode);
154 $visitor2->expects($this->at(4))->method('enterNode')->with($mulNode)
155 ->will($this->returnValue(NodeTraverser::DONT_TRAVERSE_CHILDREN));
157 $visitor1->expects($this->at(5))->method('leaveNode')->with($mulNode);
158 $visitor2->expects($this->at(5))->method('leaveNode')->with($mulNode);
160 $visitor1->expects($this->at(6))->method('leaveNode')->with($negNode);
161 $visitor2->expects($this->at(6))->method('leaveNode')->with($negNode);
163 $traverser = new NodeTraverser;
164 $traverser->addVisitor($visitor1);
165 $traverser->addVisitor($visitor2);
167 $this->assertEquals($stmts, $traverser->traverse($stmts));
170 public function testStopTraversal() {
171 $varNode1 = new Expr\Variable('a');
172 $varNode2 = new Expr\Variable('b');
173 $varNode3 = new Expr\Variable('c');
174 $mulNode = new Expr\BinaryOp\Mul($varNode1, $varNode2);
175 $printNode = new Expr\Print_($varNode3);
176 $stmts = [$mulNode, $printNode];
178 // From enterNode() with array parent
179 $visitor = $this->getMockBuilder(NodeVisitor::class)->getMock();
180 $visitor->expects($this->at(1))->method('enterNode')->with($mulNode)
181 ->will($this->returnValue(NodeTraverser::STOP_TRAVERSAL));
182 $visitor->expects($this->at(2))->method('afterTraverse');
183 $traverser = new NodeTraverser;
184 $traverser->addVisitor($visitor);
185 $this->assertEquals($stmts, $traverser->traverse($stmts));
187 // From enterNode with Node parent
188 $visitor = $this->getMockBuilder(NodeVisitor::class)->getMock();
189 $visitor->expects($this->at(2))->method('enterNode')->with($varNode1)
190 ->will($this->returnValue(NodeTraverser::STOP_TRAVERSAL));
191 $visitor->expects($this->at(3))->method('afterTraverse');
192 $traverser = new NodeTraverser;
193 $traverser->addVisitor($visitor);
194 $this->assertEquals($stmts, $traverser->traverse($stmts));
196 // From leaveNode with Node parent
197 $visitor = $this->getMockBuilder(NodeVisitor::class)->getMock();
198 $visitor->expects($this->at(3))->method('leaveNode')->with($varNode1)
199 ->will($this->returnValue(NodeTraverser::STOP_TRAVERSAL));
200 $visitor->expects($this->at(4))->method('afterTraverse');
201 $traverser = new NodeTraverser;
202 $traverser->addVisitor($visitor);
203 $this->assertEquals($stmts, $traverser->traverse($stmts));
205 // From leaveNode with array parent
206 $visitor = $this->getMockBuilder(NodeVisitor::class)->getMock();
207 $visitor->expects($this->at(6))->method('leaveNode')->with($mulNode)
208 ->will($this->returnValue(NodeTraverser::STOP_TRAVERSAL));
209 $visitor->expects($this->at(7))->method('afterTraverse');
210 $traverser = new NodeTraverser;
211 $traverser->addVisitor($visitor);
212 $this->assertEquals($stmts, $traverser->traverse($stmts));
214 // Check that pending array modifications are still carried out
215 $visitor = $this->getMockBuilder(NodeVisitor::class)->getMock();
216 $visitor->expects($this->at(6))->method('leaveNode')->with($mulNode)
217 ->will($this->returnValue(NodeTraverser::REMOVE_NODE));
218 $visitor->expects($this->at(7))->method('enterNode')->with($printNode)
219 ->will($this->returnValue(NodeTraverser::STOP_TRAVERSAL));
220 $visitor->expects($this->at(8))->method('afterTraverse');
221 $traverser = new NodeTraverser;
222 $traverser->addVisitor($visitor);
223 $this->assertEquals([$printNode], $traverser->traverse($stmts));
227 public function testRemovingVisitor() {
228 $visitor1 = $this->getMockBuilder(NodeVisitor::class)->getMock();
229 $visitor2 = $this->getMockBuilder(NodeVisitor::class)->getMock();
230 $visitor3 = $this->getMockBuilder(NodeVisitor::class)->getMock();
232 $traverser = new NodeTraverser;
233 $traverser->addVisitor($visitor1);
234 $traverser->addVisitor($visitor2);
235 $traverser->addVisitor($visitor3);
237 $preExpected = [$visitor1, $visitor2, $visitor3];
238 $this->assertAttributeSame($preExpected, 'visitors', $traverser, 'The appropriate visitors have not been added');
240 $traverser->removeVisitor($visitor2);
242 $postExpected = [0 => $visitor1, 2 => $visitor3];
243 $this->assertAttributeSame($postExpected, 'visitors', $traverser, 'The appropriate visitors are not present after removal');
246 public function testNoCloneNodes() {
247 $stmts = [new Node\Stmt\Echo_([new String_('Foo'), new String_('Bar')])];
249 $traverser = new NodeTraverser;
251 $this->assertSame($stmts, $traverser->traverse($stmts));
255 * @dataProvider provideTestInvalidReturn
257 public function testInvalidReturn($visitor, $message) {
258 $this->expectException(\LogicException::class);
259 $this->expectExceptionMessage($message);
261 $stmts = [new Node\Stmt\Expression(new Node\Scalar\LNumber(42))];
263 $traverser = new NodeTraverser();
264 $traverser->addVisitor($visitor);
265 $traverser->traverse($stmts);
268 public function provideTestInvalidReturn() {
269 $visitor1 = $this->getMockBuilder(NodeVisitor::class)->getMock();
270 $visitor1->expects($this->at(1))->method('enterNode')
271 ->willReturn('foobar');
273 $visitor2 = $this->getMockBuilder(NodeVisitor::class)->getMock();
274 $visitor2->expects($this->at(2))->method('enterNode')
275 ->willReturn('foobar');
277 $visitor3 = $this->getMockBuilder(NodeVisitor::class)->getMock();
278 $visitor3->expects($this->at(3))->method('leaveNode')
279 ->willReturn('foobar');
281 $visitor4 = $this->getMockBuilder(NodeVisitor::class)->getMock();
282 $visitor4->expects($this->at(4))->method('leaveNode')
283 ->willReturn('foobar');
285 $visitor5 = $this->getMockBuilder(NodeVisitor::class)->getMock();
286 $visitor5->expects($this->at(3))->method('leaveNode')
287 ->willReturn([new Node\Scalar\DNumber(42.0)]);
289 $visitor6 = $this->getMockBuilder(NodeVisitor::class)->getMock();
290 $visitor6->expects($this->at(4))->method('leaveNode')
293 $visitor7 = $this->getMockBuilder(NodeVisitor::class)->getMock();
294 $visitor7->expects($this->at(1))->method('enterNode')
295 ->willReturn(new Node\Scalar\LNumber(42));
297 $visitor8 = $this->getMockBuilder(NodeVisitor::class)->getMock();
298 $visitor8->expects($this->at(2))->method('enterNode')
299 ->willReturn(new Node\Stmt\Return_());
302 [$visitor1, 'enterNode() returned invalid value of type string'],
303 [$visitor2, 'enterNode() returned invalid value of type string'],
304 [$visitor3, 'leaveNode() returned invalid value of type string'],
305 [$visitor4, 'leaveNode() returned invalid value of type string'],
306 [$visitor5, 'leaveNode() may only return an array if the parent structure is an array'],
307 [$visitor6, 'bool(false) return from leaveNode() no longer supported. Return NodeTraverser::REMOVE_NODE instead'],
308 [$visitor7, 'Trying to replace statement (Stmt_Expression) with expression (Scalar_LNumber). Are you missing a Stmt_Expression wrapper?'],
309 [$visitor8, 'Trying to replace expression (Scalar_LNumber) with statement (Stmt_Return)'],