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