1<?php declare(strict_types=1);
2
3namespace PhpParser;
4
5class DummyNode extends NodeAbstract {
6    public $subNode1;
7    public $subNode2;
8    public $notSubNode;
9
10    public function __construct($subNode1, $subNode2, $notSubNode, $attributes) {
11        parent::__construct($attributes);
12        $this->subNode1 = $subNode1;
13        $this->subNode2 = $subNode2;
14        $this->notSubNode = $notSubNode;
15    }
16
17    public function getSubNodeNames(): array {
18        return ['subNode1', 'subNode2'];
19    }
20
21    // This method is only overwritten because the node is located in an unusual namespace
22    public function getType(): string {
23        return 'Dummy';
24    }
25}
26
27class NodeAbstractTest extends \PHPUnit\Framework\TestCase {
28    public static function provideNodes() {
29        $attributes = [
30            'startLine' => 10,
31            'endLine' => 11,
32            'startTokenPos' => 12,
33            'endTokenPos' => 13,
34            'startFilePos' => 14,
35            'endFilePos' => 15,
36            'comments'  => [
37                new Comment('// Comment 1' . "\n"),
38                new Comment\Doc('/** doc comment */'),
39                new Comment('// Comment 2' . "\n"),
40            ],
41        ];
42
43        $node = new DummyNode('value1', 'value2', 'value3', $attributes);
44
45        return [
46            [$attributes, $node],
47        ];
48    }
49
50    /**
51     * @dataProvider provideNodes
52     */
53    public function testConstruct(array $attributes, Node $node) {
54        $this->assertSame('Dummy', $node->getType());
55        $this->assertSame(['subNode1', 'subNode2'], $node->getSubNodeNames());
56        $this->assertSame(10, $node->getLine());
57        $this->assertSame(10, $node->getStartLine());
58        $this->assertSame(11, $node->getEndLine());
59        $this->assertSame(12, $node->getStartTokenPos());
60        $this->assertSame(13, $node->getEndTokenPos());
61        $this->assertSame(14, $node->getStartFilePos());
62        $this->assertSame(15, $node->getEndFilePos());
63        $this->assertSame('/** doc comment */', $node->getDocComment()->getText());
64        $this->assertSame('value1', $node->subNode1);
65        $this->assertSame('value2', $node->subNode2);
66        $this->assertTrue(isset($node->subNode1));
67        $this->assertTrue(isset($node->subNode2));
68        $this->assertTrue(!isset($node->subNode3));
69        $this->assertSame($attributes, $node->getAttributes());
70        $this->assertSame($attributes['comments'], $node->getComments());
71
72        return $node;
73    }
74
75    /**
76     * @dataProvider provideNodes
77     */
78    public function testGetDocComment(array $attributes, Node $node): void {
79        $this->assertSame('/** doc comment */', $node->getDocComment()->getText());
80        $comments = $node->getComments();
81
82        array_splice($comments, 1, 1, []); // remove doc comment
83        $node->setAttribute('comments', $comments);
84        $this->assertNull($node->getDocComment());
85
86        // Remove all comments.
87        $node->setAttribute('comments', []);
88        $this->assertNull($node->getDocComment());
89    }
90
91    public function testSetDocComment(): void {
92        $node = new DummyNode(null, null, null, []);
93
94        // Add doc comment to node without comments
95        $docComment = new Comment\Doc('/** doc */');
96        $node->setDocComment($docComment);
97        $this->assertSame($docComment, $node->getDocComment());
98
99        // Replace it
100        $docComment = new Comment\Doc('/** doc 2 */');
101        $node->setDocComment($docComment);
102        $this->assertSame($docComment, $node->getDocComment());
103
104        // Add docmment to node with other comments
105        $c1 = new Comment('/* foo */');
106        $c2 = new Comment('/* bar */');
107        $docComment = new Comment\Doc('/** baz */');
108        $node->setAttribute('comments', [$c1, $c2]);
109        $node->setDocComment($docComment);
110        $this->assertSame([$c1, $c2, $docComment], $node->getAttribute('comments'));
111
112        // Replace doc comment that is not at the end.
113        $newDocComment = new Comment\Doc('/** new baz */');
114        $node->setAttribute('comments', [$c1, $docComment, $c2]);
115        $node->setDocComment($newDocComment);
116        $this->assertSame([$c1, $newDocComment, $c2], $node->getAttribute('comments'));
117    }
118
119    /**
120     * @dataProvider provideNodes
121     */
122    public function testChange(array $attributes, DummyNode $node): void {
123        // direct modification
124        $node->subNode1 = 'newValue';
125        $this->assertSame('newValue', $node->subNode1);
126
127        // indirect modification
128        $subNode = &$node->subNode1;
129        $subNode = 'newNewValue';
130        $this->assertSame('newNewValue', $node->subNode1);
131
132        // removal
133        unset($node->subNode1);
134        $this->assertFalse(isset($node->subNode1));
135    }
136
137    /**
138     * @dataProvider provideNodes
139     */
140    public function testIteration(array $attributes, Node $node): void {
141        // Iteration is simple object iteration over properties,
142        // not over subnodes
143        $i = 0;
144        foreach ($node as $key => $value) {
145            if ($i === 0) {
146                $this->assertSame('subNode1', $key);
147                $this->assertSame('value1', $value);
148            } elseif ($i === 1) {
149                $this->assertSame('subNode2', $key);
150                $this->assertSame('value2', $value);
151            } elseif ($i === 2) {
152                $this->assertSame('notSubNode', $key);
153                $this->assertSame('value3', $value);
154            } else {
155                throw new \Exception();
156            }
157            $i++;
158        }
159        $this->assertSame(3, $i);
160    }
161
162    public function testAttributes(): void {
163        /** @var $node Node */
164        $node = $this->getMockForAbstractClass(NodeAbstract::class);
165
166        $this->assertEmpty($node->getAttributes());
167
168        $node->setAttribute('key', 'value');
169        $this->assertTrue($node->hasAttribute('key'));
170        $this->assertSame('value', $node->getAttribute('key'));
171
172        $this->assertFalse($node->hasAttribute('doesNotExist'));
173        $this->assertNull($node->getAttribute('doesNotExist'));
174        $this->assertSame('default', $node->getAttribute('doesNotExist', 'default'));
175
176        $node->setAttribute('null', null);
177        $this->assertTrue($node->hasAttribute('null'));
178        $this->assertNull($node->getAttribute('null'));
179        $this->assertNull($node->getAttribute('null', 'default'));
180
181        $this->assertSame(
182            [
183                'key'  => 'value',
184                'null' => null,
185            ],
186            $node->getAttributes()
187        );
188
189        $node->setAttributes(
190            [
191                'a' => 'b',
192                'c' => null,
193            ]
194        );
195        $this->assertSame(
196            [
197                'a' => 'b',
198                'c' => null,
199            ],
200            $node->getAttributes()
201        );
202    }
203
204    public function testJsonSerialization(): void {
205        $code = <<<'PHP'
206<?php
207// comment
208/** doc comment */
209function functionName(&$a = 0, $b = 1.0) {
210    echo 'Foo';
211}
212PHP;
213        $expected = <<<'JSON'
214[
215    {
216        "nodeType": "Stmt_Function",
217        "byRef": false,
218        "name": {
219            "nodeType": "Identifier",
220            "name": "functionName",
221            "attributes": {
222                "startLine": 4,
223                "startTokenPos": 7,
224                "startFilePos": 45,
225                "endLine": 4,
226                "endTokenPos": 7,
227                "endFilePos": 56
228            }
229        },
230        "params": [
231            {
232                "nodeType": "Param",
233                "type": null,
234                "byRef": true,
235                "variadic": false,
236                "var": {
237                    "nodeType": "Expr_Variable",
238                    "name": "a",
239                    "attributes": {
240                        "startLine": 4,
241                        "startTokenPos": 10,
242                        "startFilePos": 59,
243                        "endLine": 4,
244                        "endTokenPos": 10,
245                        "endFilePos": 60
246                    }
247                },
248                "default": {
249                    "nodeType": "Scalar_Int",
250                    "value": 0,
251                    "attributes": {
252                        "startLine": 4,
253                        "startTokenPos": 14,
254                        "startFilePos": 64,
255                        "endLine": 4,
256                        "endTokenPos": 14,
257                        "endFilePos": 64,
258                        "rawValue": "0",
259                        "kind": 10
260                    }
261                },
262                "flags": 0,
263                "attrGroups": [],
264                "hooks": [],
265                "attributes": {
266                    "startLine": 4,
267                    "startTokenPos": 9,
268                    "startFilePos": 58,
269                    "endLine": 4,
270                    "endTokenPos": 14,
271                    "endFilePos": 64
272                }
273            },
274            {
275                "nodeType": "Param",
276                "type": null,
277                "byRef": false,
278                "variadic": false,
279                "var": {
280                    "nodeType": "Expr_Variable",
281                    "name": "b",
282                    "attributes": {
283                        "startLine": 4,
284                        "startTokenPos": 17,
285                        "startFilePos": 67,
286                        "endLine": 4,
287                        "endTokenPos": 17,
288                        "endFilePos": 68
289                    }
290                },
291                "default": {
292                    "nodeType": "Scalar_Float",
293                    "value": 1,
294                    "attributes": {
295                        "startLine": 4,
296                        "startTokenPos": 21,
297                        "startFilePos": 72,
298                        "endLine": 4,
299                        "endTokenPos": 21,
300                        "endFilePos": 74,
301                        "rawValue": "1.0"
302                    }
303                },
304                "flags": 0,
305                "attrGroups": [],
306                "hooks": [],
307                "attributes": {
308                    "startLine": 4,
309                    "startTokenPos": 17,
310                    "startFilePos": 67,
311                    "endLine": 4,
312                    "endTokenPos": 21,
313                    "endFilePos": 74
314                }
315            }
316        ],
317        "returnType": null,
318        "stmts": [
319            {
320                "nodeType": "Stmt_Echo",
321                "exprs": [
322                    {
323                        "nodeType": "Scalar_String",
324                        "value": "Foo",
325                        "attributes": {
326                            "startLine": 5,
327                            "startTokenPos": 28,
328                            "startFilePos": 88,
329                            "endLine": 5,
330                            "endTokenPos": 28,
331                            "endFilePos": 92,
332                            "kind": 1,
333                            "rawValue": "'Foo'"
334                        }
335                    }
336                ],
337                "attributes": {
338                    "startLine": 5,
339                    "startTokenPos": 26,
340                    "startFilePos": 83,
341                    "endLine": 5,
342                    "endTokenPos": 29,
343                    "endFilePos": 93
344                }
345            }
346        ],
347        "attrGroups": [],
348        "attributes": {
349            "startLine": 4,
350            "startTokenPos": 5,
351            "startFilePos": 36,
352            "endLine": 6,
353            "endTokenPos": 31,
354            "endFilePos": 95,
355            "comments": [
356                {
357                    "nodeType": "Comment",
358                    "text": "\/\/ comment",
359                    "line": 2,
360                    "filePos": 6,
361                    "tokenPos": 1,
362                    "endLine": 2,
363                    "endFilePos": 15,
364                    "endTokenPos": 1
365                },
366                {
367                    "nodeType": "Comment_Doc",
368                    "text": "\/** doc comment *\/",
369                    "line": 3,
370                    "filePos": 17,
371                    "tokenPos": 3,
372                    "endLine": 3,
373                    "endFilePos": 34,
374                    "endTokenPos": 3
375                }
376            ]
377        }
378    }
379]
380JSON;
381        $expected81 = <<<'JSON'
382[
383    {
384        "nodeType": "Stmt_Function",
385        "attributes": {
386            "startLine": 4,
387            "startTokenPos": 5,
388            "startFilePos": 36,
389            "endLine": 6,
390            "endTokenPos": 31,
391            "endFilePos": 95,
392            "comments": [
393                {
394                    "nodeType": "Comment",
395                    "text": "\/\/ comment",
396                    "line": 2,
397                    "filePos": 6,
398                    "tokenPos": 1,
399                    "endLine": 2,
400                    "endFilePos": 15,
401                    "endTokenPos": 1
402                },
403                {
404                    "nodeType": "Comment_Doc",
405                    "text": "\/** doc comment *\/",
406                    "line": 3,
407                    "filePos": 17,
408                    "tokenPos": 3,
409                    "endLine": 3,
410                    "endFilePos": 34,
411                    "endTokenPos": 3
412                }
413            ]
414        },
415        "byRef": false,
416        "name": {
417            "nodeType": "Identifier",
418            "attributes": {
419                "startLine": 4,
420                "startTokenPos": 7,
421                "startFilePos": 45,
422                "endLine": 4,
423                "endTokenPos": 7,
424                "endFilePos": 56
425            },
426            "name": "functionName"
427        },
428        "params": [
429            {
430                "nodeType": "Param",
431                "attributes": {
432                    "startLine": 4,
433                    "startTokenPos": 9,
434                    "startFilePos": 58,
435                    "endLine": 4,
436                    "endTokenPos": 14,
437                    "endFilePos": 64
438                },
439                "type": null,
440                "byRef": true,
441                "variadic": false,
442                "var": {
443                    "nodeType": "Expr_Variable",
444                    "attributes": {
445                        "startLine": 4,
446                        "startTokenPos": 10,
447                        "startFilePos": 59,
448                        "endLine": 4,
449                        "endTokenPos": 10,
450                        "endFilePos": 60
451                    },
452                    "name": "a"
453                },
454                "default": {
455                    "nodeType": "Scalar_Int",
456                    "attributes": {
457                        "startLine": 4,
458                        "startTokenPos": 14,
459                        "startFilePos": 64,
460                        "endLine": 4,
461                        "endTokenPos": 14,
462                        "endFilePos": 64,
463                        "rawValue": "0",
464                        "kind": 10
465                    },
466                    "value": 0
467                },
468                "flags": 0,
469                "attrGroups": [],
470                "hooks": []
471            },
472            {
473                "nodeType": "Param",
474                "attributes": {
475                    "startLine": 4,
476                    "startTokenPos": 17,
477                    "startFilePos": 67,
478                    "endLine": 4,
479                    "endTokenPos": 21,
480                    "endFilePos": 74
481                },
482                "type": null,
483                "byRef": false,
484                "variadic": false,
485                "var": {
486                    "nodeType": "Expr_Variable",
487                    "attributes": {
488                        "startLine": 4,
489                        "startTokenPos": 17,
490                        "startFilePos": 67,
491                        "endLine": 4,
492                        "endTokenPos": 17,
493                        "endFilePos": 68
494                    },
495                    "name": "b"
496                },
497                "default": {
498                    "nodeType": "Scalar_Float",
499                    "attributes": {
500                        "startLine": 4,
501                        "startTokenPos": 21,
502                        "startFilePos": 72,
503                        "endLine": 4,
504                        "endTokenPos": 21,
505                        "endFilePos": 74,
506                        "rawValue": "1.0"
507                    },
508                    "value": 1
509                },
510                "flags": 0,
511                "attrGroups": [],
512                "hooks": []
513            }
514        ],
515        "returnType": null,
516        "stmts": [
517            {
518                "nodeType": "Stmt_Echo",
519                "attributes": {
520                    "startLine": 5,
521                    "startTokenPos": 26,
522                    "startFilePos": 83,
523                    "endLine": 5,
524                    "endTokenPos": 29,
525                    "endFilePos": 93
526                },
527                "exprs": [
528                    {
529                        "nodeType": "Scalar_String",
530                        "attributes": {
531                            "startLine": 5,
532                            "startTokenPos": 28,
533                            "startFilePos": 88,
534                            "endLine": 5,
535                            "endTokenPos": 28,
536                            "endFilePos": 92,
537                            "kind": 1,
538                            "rawValue": "'Foo'"
539                        },
540                        "value": "Foo"
541                    }
542                ]
543            }
544        ],
545        "attrGroups": []
546    }
547]
548JSON;
549
550        if (version_compare(PHP_VERSION, '8.1', '>=')) {
551            $expected = $expected81;
552        }
553
554        $parser = new Parser\Php7(new Lexer());
555        $stmts = $parser->parse(canonicalize($code));
556        $json = json_encode($stmts, JSON_PRETTY_PRINT);
557        $this->assertEquals(canonicalize($expected), canonicalize($json));
558    }
559}
560