1<?php declare(strict_types=1); 2 3namespace PhpParser; 4 5use PhpParser\Node\Expr; 6use PhpParser\Node\Name; 7use PhpParser\Node\Scalar\Float_; 8use PhpParser\Node\Scalar\InterpolatedString; 9use PhpParser\Node\InterpolatedStringPart; 10use PhpParser\Node\Scalar\Int_; 11use PhpParser\Node\Scalar\String_; 12use PhpParser\Node\Stmt; 13use PhpParser\PrettyPrinter\Standard; 14 15class PrettyPrinterTest extends CodeTestAbstract { 16 /** @return array{0: Parser, 1: PrettyPrinter} */ 17 private function createParserAndPrinter(array $options): array { 18 $parserVersion = $options['parserVersion'] ?? $options['version'] ?? null; 19 $printerVersion = $options['version'] ?? null; 20 $indent = isset($options['indent']) ? json_decode($options['indent']) : null; 21 $factory = new ParserFactory(); 22 $parser = $factory->createForVersion($parserVersion !== null 23 ? PhpVersion::fromString($parserVersion) : PhpVersion::getNewestSupported()); 24 $prettyPrinter = new Standard([ 25 'phpVersion' => $printerVersion !== null ? PhpVersion::fromString($printerVersion) : null, 26 'indent' => $indent, 27 ]); 28 return [$parser, $prettyPrinter]; 29 } 30 31 protected function doTestPrettyPrintMethod($method, $name, $code, $expected, $modeLine) { 32 [$parser, $prettyPrinter] = $this->createParserAndPrinter($this->parseModeLine($modeLine)); 33 $output = canonicalize($prettyPrinter->$method($parser->parse($code))); 34 $this->assertSame($expected, $output, $name); 35 } 36 37 /** 38 * @dataProvider provideTestPrettyPrint 39 */ 40 public function testPrettyPrint($name, $code, $expected, $mode): void { 41 $this->doTestPrettyPrintMethod('prettyPrint', $name, $code, $expected, $mode); 42 } 43 44 /** 45 * @dataProvider provideTestPrettyPrintFile 46 */ 47 public function testPrettyPrintFile($name, $code, $expected, $mode): void { 48 $this->doTestPrettyPrintMethod('prettyPrintFile', $name, $code, $expected, $mode); 49 } 50 51 public static function provideTestPrettyPrint() { 52 return self::getTests(__DIR__ . '/../code/prettyPrinter', 'test'); 53 } 54 55 public static function provideTestPrettyPrintFile() { 56 return self::getTests(__DIR__ . '/../code/prettyPrinter', 'file-test'); 57 } 58 59 public function testPrettyPrintExpr(): void { 60 $prettyPrinter = new Standard(); 61 $expr = new Expr\BinaryOp\Mul( 62 new Expr\BinaryOp\Plus(new Expr\Variable('a'), new Expr\Variable('b')), 63 new Expr\Variable('c') 64 ); 65 $this->assertEquals('($a + $b) * $c', $prettyPrinter->prettyPrintExpr($expr)); 66 67 $expr = new Expr\Closure([ 68 'stmts' => [new Stmt\Return_(new String_("a\nb"))] 69 ]); 70 $this->assertEquals("function () {\n return 'a\nb';\n}", $prettyPrinter->prettyPrintExpr($expr)); 71 } 72 73 public function testCommentBeforeInlineHTML(): void { 74 $prettyPrinter = new PrettyPrinter\Standard(); 75 $comment = new Comment\Doc("/**\n * This is a comment\n */"); 76 $stmts = [new Stmt\InlineHTML('Hello World!', ['comments' => [$comment]])]; 77 $expected = "<?php\n\n/**\n * This is a comment\n */\n?>\nHello World!"; 78 $this->assertSame($expected, $prettyPrinter->prettyPrintFile($stmts)); 79 } 80 81 public function testArraySyntaxDefault(): void { 82 $prettyPrinter = new Standard(['shortArraySyntax' => true]); 83 $expr = new Expr\Array_([ 84 new Node\ArrayItem(new String_('val'), new String_('key')) 85 ]); 86 $expected = "['key' => 'val']"; 87 $this->assertSame($expected, $prettyPrinter->prettyPrintExpr($expr)); 88 } 89 90 /** 91 * @dataProvider provideTestKindAttributes 92 */ 93 public function testKindAttributes($node, $expected): void { 94 $prttyPrinter = new PrettyPrinter\Standard(); 95 $result = $prttyPrinter->prettyPrintExpr($node); 96 $this->assertSame($expected, $result); 97 } 98 99 public static function provideTestKindAttributes() { 100 $nowdoc = ['kind' => String_::KIND_NOWDOC, 'docLabel' => 'STR']; 101 $heredoc = ['kind' => String_::KIND_HEREDOC, 'docLabel' => 'STR']; 102 return [ 103 // Defaults to single quoted 104 [new String_('foo'), "'foo'"], 105 // Explicit single/double quoted 106 [new String_('foo', ['kind' => String_::KIND_SINGLE_QUOTED]), "'foo'"], 107 [new String_('foo', ['kind' => String_::KIND_DOUBLE_QUOTED]), '"foo"'], 108 // Fallback from doc string if no label 109 [new String_('foo', ['kind' => String_::KIND_NOWDOC]), "'foo'"], 110 [new String_('foo', ['kind' => String_::KIND_HEREDOC]), '"foo"'], 111 // Fallback if string contains label 112 [new String_("A\nB\nC", ['kind' => String_::KIND_NOWDOC, 'docLabel' => 'A']), "'A\nB\nC'"], 113 [new String_("A\nB\nC", ['kind' => String_::KIND_NOWDOC, 'docLabel' => 'B']), "'A\nB\nC'"], 114 [new String_("A\nB\nC", ['kind' => String_::KIND_NOWDOC, 'docLabel' => 'C']), "'A\nB\nC'"], 115 [new String_("STR;", $nowdoc), "'STR;'"], 116 [new String_("STR,", $nowdoc), "'STR,'"], 117 [new String_(" STR", $nowdoc), "' STR'"], 118 [new String_("\tSTR", $nowdoc), "'\tSTR'"], 119 [new String_("STR\x80", $heredoc), '"STR\x80"'], 120 // Doc string if label not contained (or not in ending position) 121 [new String_("foo", $nowdoc), "<<<'STR'\nfoo\nSTR"], 122 [new String_("foo", $heredoc), "<<<STR\nfoo\nSTR"], 123 [new String_("STRx", $nowdoc), "<<<'STR'\nSTRx\nSTR"], 124 [new String_("xSTR", $nowdoc), "<<<'STR'\nxSTR\nSTR"], 125 [new String_("STRä", $nowdoc), "<<<'STR'\nSTRä\nSTR"], 126 [new String_("STR\x80", $nowdoc), "<<<'STR'\nSTR\x80\nSTR"], 127 // Empty doc string variations (encapsed variant does not occur naturally) 128 [new String_("", $nowdoc), "<<<'STR'\nSTR"], 129 [new String_("", $heredoc), "<<<STR\nSTR"], 130 [new InterpolatedString([new InterpolatedStringPart('')], $heredoc), "<<<STR\nSTR"], 131 // Isolated \r in doc string 132 [new String_("\r", $heredoc), "<<<STR\n\\r\nSTR"], 133 [new String_("\r", $nowdoc), "'\r'"], 134 [new String_("\rx", $nowdoc), "<<<'STR'\n\rx\nSTR"], 135 // Encapsed doc string variations 136 [new InterpolatedString([new InterpolatedStringPart('foo')], $heredoc), "<<<STR\nfoo\nSTR"], 137 [new InterpolatedString([new InterpolatedStringPart('foo'), new Expr\Variable('y')], $heredoc), "<<<STR\nfoo{\$y}\nSTR"], 138 [new InterpolatedString([new Expr\Variable('y'), new InterpolatedStringPart("STR\n")], $heredoc), "<<<STR\n{\$y}STR\n\nSTR"], 139 // Encapsed doc string fallback 140 [new InterpolatedString([new Expr\Variable('y'), new InterpolatedStringPart("\nSTR")], $heredoc), '"{$y}\\nSTR"'], 141 [new InterpolatedString([new InterpolatedStringPart("STR\n"), new Expr\Variable('y')], $heredoc), '"STR\\n{$y}"'], 142 [new InterpolatedString([new InterpolatedStringPart("STR")], $heredoc), '"STR"'], 143 [new InterpolatedString([new InterpolatedStringPart("\nSTR"), new Expr\Variable('y')], $heredoc), '"\nSTR{$y}"'], 144 [new InterpolatedString([new InterpolatedStringPart("STR\x80"), new Expr\Variable('y')], $heredoc), '"STR\x80{$y}"'], 145 ]; 146 } 147 148 /** @dataProvider provideTestUnnaturalLiterals */ 149 public function testUnnaturalLiterals($node, $expected): void { 150 $prttyPrinter = new PrettyPrinter\Standard(); 151 $result = $prttyPrinter->prettyPrintExpr($node); 152 $this->assertSame($expected, $result); 153 } 154 155 public static function provideTestUnnaturalLiterals() { 156 return [ 157 [new Int_(-1), '-1'], 158 [new Int_(-PHP_INT_MAX - 1), '(-' . PHP_INT_MAX . '-1)'], 159 [new Int_(-1, ['kind' => Int_::KIND_BIN]), '-0b1'], 160 [new Int_(-1, ['kind' => Int_::KIND_OCT]), '-01'], 161 [new Int_(-1, ['kind' => Int_::KIND_HEX]), '-0x1'], 162 [new Float_(\INF), '1.0E+1000'], 163 [new Float_(-\INF), '-1.0E+1000'], 164 [new Float_(-\NAN), '\NAN'], 165 ]; 166 } 167 168 public function testPrettyPrintWithError(): void { 169 $this->expectException(\LogicException::class); 170 $this->expectExceptionMessage('Cannot pretty-print AST with Error nodes'); 171 $stmts = [new Stmt\Expression( 172 new Expr\PropertyFetch(new Expr\Variable('a'), new Expr\Error()) 173 )]; 174 $prettyPrinter = new PrettyPrinter\Standard(); 175 $prettyPrinter->prettyPrint($stmts); 176 } 177 178 public function testPrettyPrintWithErrorInClassConstFetch(): void { 179 $this->expectException(\LogicException::class); 180 $this->expectExceptionMessage('Cannot pretty-print AST with Error nodes'); 181 $stmts = [new Stmt\Expression( 182 new Expr\ClassConstFetch(new Name('Foo'), new Expr\Error()) 183 )]; 184 $prettyPrinter = new PrettyPrinter\Standard(); 185 $prettyPrinter->prettyPrint($stmts); 186 } 187 188 /** 189 * @dataProvider provideTestFormatPreservingPrint 190 */ 191 public function testFormatPreservingPrint($name, $code, $modification, $expected, $modeLine): void { 192 [$parser, $printer] = $this->createParserAndPrinter($this->parseModeLine($modeLine)); 193 $traverser = new NodeTraverser(new NodeVisitor\CloningVisitor()); 194 195 $oldStmts = $parser->parse($code); 196 $oldTokens = $parser->getTokens(); 197 198 $newStmts = $traverser->traverse($oldStmts); 199 200 /** @var callable $fn */ 201 eval(<<<CODE 202use PhpParser\Comment; 203use PhpParser\Node; 204use PhpParser\Node\Expr; 205use PhpParser\Node\Scalar; 206use PhpParser\Node\Stmt; 207use PhpParser\Modifiers; 208\$fn = function(&\$stmts) { $modification }; 209CODE 210 ); 211 $fn($newStmts); 212 213 $newCode = $printer->printFormatPreserving($newStmts, $oldStmts, $oldTokens); 214 $this->assertSame(canonicalize($expected), canonicalize($newCode), $name); 215 } 216 217 public static function provideTestFormatPreservingPrint() { 218 return self::getTests(__DIR__ . '/../code/formatPreservation', 'test', 3); 219 } 220 221 /** 222 * @dataProvider provideTestRoundTripPrint 223 */ 224 public function testRoundTripPrint($name, $code, $expected, $modeLine): void { 225 /** 226 * This test makes sure that the format-preserving pretty printer round-trips for all 227 * the pretty printer tests (i.e. returns the input if no changes occurred). 228 */ 229 230 [$parser, $printer] = $this->createParserAndPrinter($this->parseModeLine($modeLine)); 231 $traverser = new NodeTraverser(new NodeVisitor\CloningVisitor()); 232 233 try { 234 $oldStmts = $parser->parse($code); 235 } catch (Error $e) { 236 // Can't do a format-preserving print on a file with errors 237 return; 238 } 239 240 $oldTokens = $parser->getTokens(); 241 242 $newStmts = $traverser->traverse($oldStmts); 243 244 $newCode = $printer->printFormatPreserving($newStmts, $oldStmts, $oldTokens); 245 $this->assertSame(canonicalize($code), canonicalize($newCode), $name); 246 } 247 248 public static function provideTestRoundTripPrint() { 249 return array_merge( 250 self::getTests(__DIR__ . '/../code/prettyPrinter', 'test'), 251 self::getTests(__DIR__ . '/../code/parser', 'test') 252 ); 253 } 254 255 public function testWindowsNewline(): void { 256 $prettyPrinter = new Standard([ 257 'newline' => "\r\n", 258 'phpVersion' => PhpVersion::fromComponents(7, 2), 259 ]); 260 $stmts = [ 261 new Stmt\If_(new Int_(1), [ 262 'stmts' => [ 263 new Stmt\Echo_([new String_('Hello')]), 264 new Stmt\Echo_([new String_('World')]), 265 ], 266 ]), 267 ]; 268 $code = $prettyPrinter->prettyPrint($stmts); 269 $this->assertSame("if (1) {\r\n echo 'Hello';\r\n echo 'World';\r\n}", $code); 270 $code = $prettyPrinter->prettyPrintFile($stmts); 271 $this->assertSame("<?php\r\n\r\nif (1) {\r\n echo 'Hello';\r\n echo 'World';\r\n}", $code); 272 273 $stmts = [new Stmt\InlineHTML('Hello world')]; 274 $code = $prettyPrinter->prettyPrintFile($stmts); 275 $this->assertSame("Hello world", $code); 276 277 $stmts = [ 278 new Stmt\Expression(new String_('Test', [ 279 'kind' => String_::KIND_NOWDOC, 280 'docLabel' => 'STR' 281 ])), 282 new Stmt\Expression(new String_('Test 2', [ 283 'kind' => String_::KIND_HEREDOC, 284 'docLabel' => 'STR' 285 ])), 286 new Stmt\Expression(new InterpolatedString([new InterpolatedStringPart('Test 3')], [ 287 'kind' => String_::KIND_HEREDOC, 288 'docLabel' => 'STR' 289 ])), 290 ]; 291 $code = $prettyPrinter->prettyPrint($stmts); 292 $this->assertSame( 293 "<<<'STR'\r\nTest\r\nSTR;\r\n<<<STR\r\nTest 2\r\nSTR;\r\n<<<STR\r\nTest 3\r\nSTR\r\n;", 294 $code); 295 } 296 297 public function testInvalidNewline(): void { 298 $this->expectException(\LogicException::class); 299 $this->expectExceptionMessage('Option "newline" must be one of "\n" or "\r\n"'); 300 new PrettyPrinter\Standard(['newline' => 'foo']); 301 } 302 303 public function testInvalidIndent(): void { 304 $this->expectException(\LogicException::class); 305 $this->expectExceptionMessage('Option "indent" must either be all spaces or a single tab'); 306 new PrettyPrinter\Standard(['indent' => "\t "]); 307 } 308} 309