1<?php declare(strict_types=1);
2
3namespace PhpParser;
4
5use PhpParser\Node\Expr;
6use PhpParser\Node\Stmt;
7
8class CodeParsingTest extends CodeTestAbstract {
9    /**
10     * @dataProvider provideTestParse
11     */
12    public function testParse($name, $code, $expected, $modeLine): void {
13        $modes = $this->parseModeLine($modeLine);
14        $parser = $this->createParser($modes['version'] ?? null);
15        list($stmts, $output) = $this->getParseOutput($parser, $code, $modes);
16
17        $this->assertSame($expected, $output, $name);
18        $this->checkAttributes($stmts);
19    }
20
21    public function createParser(?string $version): Parser {
22        $factory = new ParserFactory();
23        $version = $version === null
24            ? PhpVersion::getNewestSupported() : PhpVersion::fromString($version);
25        return $factory->createForVersion($version);
26    }
27
28    // Must be public for updateTests.php
29    public function getParseOutput(Parser $parser, $code, array $modes) {
30        $dumpPositions = isset($modes['positions']);
31        $dumpOtherAttributes = isset($modes['attributes']);
32
33        $errors = new ErrorHandler\Collecting();
34        $stmts = $parser->parse($code, $errors);
35
36        $output = '';
37        foreach ($errors->getErrors() as $error) {
38            $output .= $this->formatErrorMessage($error, $code) . "\n";
39        }
40
41        if (null !== $stmts) {
42            $dumper = new NodeDumper([
43                'dumpComments' => true,
44                'dumpPositions' => $dumpPositions,
45                'dumpOtherAttributes' => $dumpOtherAttributes,
46            ]);
47            $output .= $dumper->dump($stmts, $code);
48        }
49
50        return [$stmts, canonicalize($output)];
51    }
52
53    public static function provideTestParse() {
54        return self::getTests(__DIR__ . '/../code/parser', 'test');
55    }
56
57    private function formatErrorMessage(Error $e, $code) {
58        if ($e->hasColumnInfo()) {
59            return $e->getMessageWithColumnInfo($code);
60        }
61
62        return $e->getMessage();
63    }
64
65    private function checkAttributes($stmts): void {
66        if ($stmts === null) {
67            return;
68        }
69
70        $traverser = new NodeTraverser(new class () extends NodeVisitorAbstract {
71            public function enterNode(Node $node): void {
72                $startLine = $node->getStartLine();
73                $endLine = $node->getEndLine();
74                $startFilePos = $node->getStartFilePos();
75                $endFilePos = $node->getEndFilePos();
76                $startTokenPos = $node->getStartTokenPos();
77                $endTokenPos = $node->getEndTokenPos();
78                if ($startLine < 0 || $endLine < 0 ||
79                    $startFilePos < 0 || $endFilePos < 0 ||
80                    $startTokenPos < 0 || $endTokenPos < 0
81                ) {
82                    throw new \Exception('Missing location information on ' . $node->getType());
83                }
84
85                if ($endLine < $startLine ||
86                    $endFilePos < $startFilePos ||
87                    $endTokenPos < $startTokenPos
88                ) {
89                    // Nop and Error can have inverted order, if they are empty.
90                    // This can also happen for a Param containing an Error.
91                    if (!$node instanceof Stmt\Nop && !$node instanceof Expr\Error &&
92                        !$node instanceof Node\Param
93                    ) {
94                        throw new \Exception('End < start on ' . $node->getType());
95                    }
96                }
97            }
98        });
99        $traverser->traverse($stmts);
100    }
101}
102