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\Parser\Php7; 14use PhpParser\PrettyPrinter\Standard; 15 16class PrettyPrinterTest extends CodeTestAbstract { 17 protected function doTestPrettyPrintMethod($method, $name, $code, $expected, $modeLine) { 18 $lexer = new Lexer\Emulative(); 19 $parser = new Parser\Php7($lexer); 20 21 $options = $this->parseModeLine($modeLine); 22 $version = isset($options['version']) ? PhpVersion::fromString($options['version']) : null; 23 $prettyPrinter = new Standard(['phpVersion' => $version]); 24 25 $output = canonicalize($prettyPrinter->$method($parser->parse($code))); 26 $this->assertSame($expected, $output, $name); 27 } 28 29 /** 30 * @dataProvider provideTestPrettyPrint 31 */ 32 public function testPrettyPrint($name, $code, $expected, $mode): void { 33 $this->doTestPrettyPrintMethod('prettyPrint', $name, $code, $expected, $mode); 34 } 35 36 /** 37 * @dataProvider provideTestPrettyPrintFile 38 */ 39 public function testPrettyPrintFile($name, $code, $expected, $mode): void { 40 $this->doTestPrettyPrintMethod('prettyPrintFile', $name, $code, $expected, $mode); 41 } 42 43 public function provideTestPrettyPrint() { 44 return $this->getTests(__DIR__ . '/../code/prettyPrinter', 'test'); 45 } 46 47 public function provideTestPrettyPrintFile() { 48 return $this->getTests(__DIR__ . '/../code/prettyPrinter', 'file-test'); 49 } 50 51 public function testPrettyPrintExpr(): void { 52 $prettyPrinter = new Standard(); 53 $expr = new Expr\BinaryOp\Mul( 54 new Expr\BinaryOp\Plus(new Expr\Variable('a'), new Expr\Variable('b')), 55 new Expr\Variable('c') 56 ); 57 $this->assertEquals('($a + $b) * $c', $prettyPrinter->prettyPrintExpr($expr)); 58 59 $expr = new Expr\Closure([ 60 'stmts' => [new Stmt\Return_(new String_("a\nb"))] 61 ]); 62 $this->assertEquals("function () {\n return 'a\nb';\n}", $prettyPrinter->prettyPrintExpr($expr)); 63 } 64 65 public function testCommentBeforeInlineHTML(): void { 66 $prettyPrinter = new PrettyPrinter\Standard(); 67 $comment = new Comment\Doc("/**\n * This is a comment\n */"); 68 $stmts = [new Stmt\InlineHTML('Hello World!', ['comments' => [$comment]])]; 69 $expected = "<?php\n\n/**\n * This is a comment\n */\n?>\nHello World!"; 70 $this->assertSame($expected, $prettyPrinter->prettyPrintFile($stmts)); 71 } 72 73 public function testArraySyntaxDefault(): void { 74 $prettyPrinter = new Standard(['shortArraySyntax' => true]); 75 $expr = new Expr\Array_([ 76 new Node\ArrayItem(new String_('val'), new String_('key')) 77 ]); 78 $expected = "['key' => 'val']"; 79 $this->assertSame($expected, $prettyPrinter->prettyPrintExpr($expr)); 80 } 81 82 /** 83 * @dataProvider provideTestKindAttributes 84 */ 85 public function testKindAttributes($node, $expected): void { 86 $prttyPrinter = new PrettyPrinter\Standard(); 87 $result = $prttyPrinter->prettyPrintExpr($node); 88 $this->assertSame($expected, $result); 89 } 90 91 public function provideTestKindAttributes() { 92 $nowdoc = ['kind' => String_::KIND_NOWDOC, 'docLabel' => 'STR']; 93 $heredoc = ['kind' => String_::KIND_HEREDOC, 'docLabel' => 'STR']; 94 return [ 95 // Defaults to single quoted 96 [new String_('foo'), "'foo'"], 97 // Explicit single/double quoted 98 [new String_('foo', ['kind' => String_::KIND_SINGLE_QUOTED]), "'foo'"], 99 [new String_('foo', ['kind' => String_::KIND_DOUBLE_QUOTED]), '"foo"'], 100 // Fallback from doc string if no label 101 [new String_('foo', ['kind' => String_::KIND_NOWDOC]), "'foo'"], 102 [new String_('foo', ['kind' => String_::KIND_HEREDOC]), '"foo"'], 103 // Fallback if string contains label 104 [new String_("A\nB\nC", ['kind' => String_::KIND_NOWDOC, 'docLabel' => 'A']), "'A\nB\nC'"], 105 [new String_("A\nB\nC", ['kind' => String_::KIND_NOWDOC, 'docLabel' => 'B']), "'A\nB\nC'"], 106 [new String_("A\nB\nC", ['kind' => String_::KIND_NOWDOC, 'docLabel' => 'C']), "'A\nB\nC'"], 107 [new String_("STR;", $nowdoc), "'STR;'"], 108 [new String_("STR,", $nowdoc), "'STR,'"], 109 [new String_(" STR", $nowdoc), "' STR'"], 110 [new String_("\tSTR", $nowdoc), "'\tSTR'"], 111 [new String_("STR\x80", $heredoc), '"STR\x80"'], 112 // Doc string if label not contained (or not in ending position) 113 [new String_("foo", $nowdoc), "<<<'STR'\nfoo\nSTR"], 114 [new String_("foo", $heredoc), "<<<STR\nfoo\nSTR"], 115 [new String_("STRx", $nowdoc), "<<<'STR'\nSTRx\nSTR"], 116 [new String_("xSTR", $nowdoc), "<<<'STR'\nxSTR\nSTR"], 117 [new String_("STRä", $nowdoc), "<<<'STR'\nSTRä\nSTR"], 118 [new String_("STR\x80", $nowdoc), "<<<'STR'\nSTR\x80\nSTR"], 119 // Empty doc string variations (encapsed variant does not occur naturally) 120 [new String_("", $nowdoc), "<<<'STR'\nSTR"], 121 [new String_("", $heredoc), "<<<STR\nSTR"], 122 [new InterpolatedString([new InterpolatedStringPart('')], $heredoc), "<<<STR\nSTR"], 123 // Isolated \r in doc string 124 [new String_("\r", $heredoc), "<<<STR\n\\r\nSTR"], 125 [new String_("\r", $nowdoc), "'\r'"], 126 [new String_("\rx", $nowdoc), "<<<'STR'\n\rx\nSTR"], 127 // Encapsed doc string variations 128 [new InterpolatedString([new InterpolatedStringPart('foo')], $heredoc), "<<<STR\nfoo\nSTR"], 129 [new InterpolatedString([new InterpolatedStringPart('foo'), new Expr\Variable('y')], $heredoc), "<<<STR\nfoo{\$y}\nSTR"], 130 [new InterpolatedString([new Expr\Variable('y'), new InterpolatedStringPart("STR\n")], $heredoc), "<<<STR\n{\$y}STR\n\nSTR"], 131 // Encapsed doc string fallback 132 [new InterpolatedString([new Expr\Variable('y'), new InterpolatedStringPart("\nSTR")], $heredoc), '"{$y}\\nSTR"'], 133 [new InterpolatedString([new InterpolatedStringPart("STR\n"), new Expr\Variable('y')], $heredoc), '"STR\\n{$y}"'], 134 [new InterpolatedString([new InterpolatedStringPart("STR")], $heredoc), '"STR"'], 135 [new InterpolatedString([new InterpolatedStringPart("\nSTR"), new Expr\Variable('y')], $heredoc), '"\nSTR{$y}"'], 136 [new InterpolatedString([new InterpolatedStringPart("STR\x80"), new Expr\Variable('y')], $heredoc), '"STR\x80{$y}"'], 137 ]; 138 } 139 140 /** @dataProvider provideTestUnnaturalLiterals */ 141 public function testUnnaturalLiterals($node, $expected): void { 142 $prttyPrinter = new PrettyPrinter\Standard(); 143 $result = $prttyPrinter->prettyPrintExpr($node); 144 $this->assertSame($expected, $result); 145 } 146 147 public function provideTestUnnaturalLiterals() { 148 return [ 149 [new Int_(-1), '-1'], 150 [new Int_(-PHP_INT_MAX - 1), '(-' . PHP_INT_MAX . '-1)'], 151 [new Int_(-1, ['kind' => Int_::KIND_BIN]), '-0b1'], 152 [new Int_(-1, ['kind' => Int_::KIND_OCT]), '-01'], 153 [new Int_(-1, ['kind' => Int_::KIND_HEX]), '-0x1'], 154 [new Float_(\INF), '1.0E+1000'], 155 [new Float_(-\INF), '-1.0E+1000'], 156 [new Float_(-\NAN), '\NAN'], 157 ]; 158 } 159 160 public function testPrettyPrintWithError(): void { 161 $this->expectException(\LogicException::class); 162 $this->expectExceptionMessage('Cannot pretty-print AST with Error nodes'); 163 $stmts = [new Stmt\Expression( 164 new Expr\PropertyFetch(new Expr\Variable('a'), new Expr\Error()) 165 )]; 166 $prettyPrinter = new PrettyPrinter\Standard(); 167 $prettyPrinter->prettyPrint($stmts); 168 } 169 170 public function testPrettyPrintWithErrorInClassConstFetch(): void { 171 $this->expectException(\LogicException::class); 172 $this->expectExceptionMessage('Cannot pretty-print AST with Error nodes'); 173 $stmts = [new Stmt\Expression( 174 new Expr\ClassConstFetch(new Name('Foo'), new Expr\Error()) 175 )]; 176 $prettyPrinter = new PrettyPrinter\Standard(); 177 $prettyPrinter->prettyPrint($stmts); 178 } 179 180 /** 181 * @dataProvider provideTestFormatPreservingPrint 182 */ 183 public function testFormatPreservingPrint($name, $code, $modification, $expected, $modeLine): void { 184 $lexer = new Lexer\Emulative(); 185 $parser = new Parser\Php7($lexer); 186 $traverser = new NodeTraverser(new NodeVisitor\CloningVisitor()); 187 188 $printer = new PrettyPrinter\Standard(); 189 190 $oldStmts = $parser->parse($code); 191 $oldTokens = $parser->getTokens(); 192 193 $newStmts = $traverser->traverse($oldStmts); 194 195 /** @var callable $fn */ 196 eval(<<<CODE 197use PhpParser\Comment; 198use PhpParser\Node; 199use PhpParser\Node\Expr; 200use PhpParser\Node\Scalar; 201use PhpParser\Node\Stmt; 202use PhpParser\Modifiers; 203\$fn = function(&\$stmts) { $modification }; 204CODE 205 ); 206 $fn($newStmts); 207 208 $newCode = $printer->printFormatPreserving($newStmts, $oldStmts, $oldTokens); 209 $this->assertSame(canonicalize($expected), canonicalize($newCode), $name); 210 } 211 212 public function provideTestFormatPreservingPrint() { 213 return $this->getTests(__DIR__ . '/../code/formatPreservation', 'test', 3); 214 } 215 216 /** 217 * @dataProvider provideTestRoundTripPrint 218 */ 219 public function testRoundTripPrint($name, $code, $expected, $modeLine): void { 220 /** 221 * This test makes sure that the format-preserving pretty printer round-trips for all 222 * the pretty printer tests (i.e. returns the input if no changes occurred). 223 */ 224 225 $lexer = new Lexer\Emulative(); 226 227 $parser = new Php7($lexer); 228 229 $traverser = new NodeTraverser(new NodeVisitor\CloningVisitor()); 230 231 $printer = new PrettyPrinter\Standard(); 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 function provideTestRoundTripPrint() { 249 return array_merge( 250 $this->getTests(__DIR__ . '/../code/prettyPrinter', 'test'), 251 $this->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