1<?php declare(strict_types=1); 2 3namespace PhpParser; 4 5use PhpParser\Internal\DiffElem; 6use PhpParser\Internal\Differ; 7use PhpParser\Internal\PrintableNewAnonClassNode; 8use PhpParser\Internal\TokenStream; 9use PhpParser\Node\AttributeGroup; 10use PhpParser\Node\Expr; 11use PhpParser\Node\Expr\AssignOp; 12use PhpParser\Node\Expr\BinaryOp; 13use PhpParser\Node\Expr\Cast; 14use PhpParser\Node\IntersectionType; 15use PhpParser\Node\MatchArm; 16use PhpParser\Node\Param; 17use PhpParser\Node\PropertyHook; 18use PhpParser\Node\Scalar; 19use PhpParser\Node\Stmt; 20use PhpParser\Node\UnionType; 21 22abstract class PrettyPrinterAbstract implements PrettyPrinter { 23 protected const FIXUP_PREC_LEFT = 0; // LHS operand affected by precedence 24 protected const FIXUP_PREC_RIGHT = 1; // RHS operand affected by precedence 25 protected const FIXUP_PREC_UNARY = 2; // Only operand affected by precedence 26 protected const FIXUP_CALL_LHS = 3; // LHS of call 27 protected const FIXUP_DEREF_LHS = 4; // LHS of dereferencing operation 28 protected const FIXUP_STATIC_DEREF_LHS = 5; // LHS of static dereferencing operation 29 protected const FIXUP_BRACED_NAME = 6; // Name operand that may require bracing 30 protected const FIXUP_VAR_BRACED_NAME = 7; // Name operand that may require ${} bracing 31 protected const FIXUP_ENCAPSED = 8; // Encapsed string part 32 protected const FIXUP_NEW = 9; // New/instanceof operand 33 34 protected const MAX_PRECEDENCE = 1000; 35 36 /** @var array<class-string, array{int, int, int}> */ 37 protected array $precedenceMap = [ 38 // [precedence, precedenceLHS, precedenceRHS] 39 // Where the latter two are the precedences to use for the LHS and RHS of a binary operator, 40 // where 1 is added to one of the sides depending on associativity. This information is not 41 // used for unary operators and set to -1. 42 Expr\Clone_::class => [-10, 0, 1], 43 BinaryOp\Pow::class => [ 0, 0, 1], 44 Expr\BitwiseNot::class => [ 10, -1, -1], 45 Expr\UnaryPlus::class => [ 10, -1, -1], 46 Expr\UnaryMinus::class => [ 10, -1, -1], 47 Cast\Int_::class => [ 10, -1, -1], 48 Cast\Double::class => [ 10, -1, -1], 49 Cast\String_::class => [ 10, -1, -1], 50 Cast\Array_::class => [ 10, -1, -1], 51 Cast\Object_::class => [ 10, -1, -1], 52 Cast\Bool_::class => [ 10, -1, -1], 53 Cast\Unset_::class => [ 10, -1, -1], 54 Expr\ErrorSuppress::class => [ 10, -1, -1], 55 Expr\Instanceof_::class => [ 20, -1, -1], 56 Expr\BooleanNot::class => [ 30, -1, -1], 57 BinaryOp\Mul::class => [ 40, 41, 40], 58 BinaryOp\Div::class => [ 40, 41, 40], 59 BinaryOp\Mod::class => [ 40, 41, 40], 60 BinaryOp\Plus::class => [ 50, 51, 50], 61 BinaryOp\Minus::class => [ 50, 51, 50], 62 BinaryOp\Concat::class => [ 50, 51, 50], 63 BinaryOp\ShiftLeft::class => [ 60, 61, 60], 64 BinaryOp\ShiftRight::class => [ 60, 61, 60], 65 BinaryOp\Smaller::class => [ 70, 70, 70], 66 BinaryOp\SmallerOrEqual::class => [ 70, 70, 70], 67 BinaryOp\Greater::class => [ 70, 70, 70], 68 BinaryOp\GreaterOrEqual::class => [ 70, 70, 70], 69 BinaryOp\Equal::class => [ 80, 80, 80], 70 BinaryOp\NotEqual::class => [ 80, 80, 80], 71 BinaryOp\Identical::class => [ 80, 80, 80], 72 BinaryOp\NotIdentical::class => [ 80, 80, 80], 73 BinaryOp\Spaceship::class => [ 80, 80, 80], 74 BinaryOp\BitwiseAnd::class => [ 90, 91, 90], 75 BinaryOp\BitwiseXor::class => [100, 101, 100], 76 BinaryOp\BitwiseOr::class => [110, 111, 110], 77 BinaryOp\BooleanAnd::class => [120, 121, 120], 78 BinaryOp\BooleanOr::class => [130, 131, 130], 79 BinaryOp\Coalesce::class => [140, 140, 141], 80 Expr\Ternary::class => [150, 150, 150], 81 Expr\Assign::class => [160, -1, -1], 82 Expr\AssignRef::class => [160, -1, -1], 83 AssignOp\Plus::class => [160, -1, -1], 84 AssignOp\Minus::class => [160, -1, -1], 85 AssignOp\Mul::class => [160, -1, -1], 86 AssignOp\Div::class => [160, -1, -1], 87 AssignOp\Concat::class => [160, -1, -1], 88 AssignOp\Mod::class => [160, -1, -1], 89 AssignOp\BitwiseAnd::class => [160, -1, -1], 90 AssignOp\BitwiseOr::class => [160, -1, -1], 91 AssignOp\BitwiseXor::class => [160, -1, -1], 92 AssignOp\ShiftLeft::class => [160, -1, -1], 93 AssignOp\ShiftRight::class => [160, -1, -1], 94 AssignOp\Pow::class => [160, -1, -1], 95 AssignOp\Coalesce::class => [160, -1, -1], 96 Expr\YieldFrom::class => [170, -1, -1], 97 Expr\Yield_::class => [175, -1, -1], 98 Expr\Print_::class => [180, -1, -1], 99 BinaryOp\LogicalAnd::class => [190, 191, 190], 100 BinaryOp\LogicalXor::class => [200, 201, 200], 101 BinaryOp\LogicalOr::class => [210, 211, 210], 102 Expr\Include_::class => [220, -1, -1], 103 Expr\ArrowFunction::class => [230, -1, -1], 104 Expr\Throw_::class => [240, -1, -1], 105 ]; 106 107 /** @var int Current indentation level. */ 108 protected int $indentLevel; 109 /** @var string String for single level of indentation */ 110 private string $indent; 111 /** @var int Width in spaces to indent by. */ 112 private int $indentWidth; 113 /** @var bool Whether to use tab indentation. */ 114 private bool $useTabs; 115 /** @var int Width in spaces of one tab. */ 116 private int $tabWidth = 4; 117 118 /** @var string Newline style. Does not include current indentation. */ 119 protected string $newline; 120 /** @var string Newline including current indentation. */ 121 protected string $nl; 122 /** @var string|null Token placed at end of doc string to ensure it is followed by a newline. 123 * Null if flexible doc strings are used. */ 124 protected ?string $docStringEndToken; 125 /** @var bool Whether semicolon namespaces can be used (i.e. no global namespace is used) */ 126 protected bool $canUseSemicolonNamespaces; 127 /** @var bool Whether to use short array syntax if the node specifies no preference */ 128 protected bool $shortArraySyntax; 129 /** @var PhpVersion PHP version to target */ 130 protected PhpVersion $phpVersion; 131 132 /** @var TokenStream|null Original tokens for use in format-preserving pretty print */ 133 protected ?TokenStream $origTokens; 134 /** @var Internal\Differ<Node> Differ for node lists */ 135 protected Differ $nodeListDiffer; 136 /** @var array<string, bool> Map determining whether a certain character is a label character */ 137 protected array $labelCharMap; 138 /** 139 * @var array<string, array<string, int>> Map from token classes and subnode names to FIXUP_* constants. 140 * This is used during format-preserving prints to place additional parens/braces if necessary. 141 */ 142 protected array $fixupMap; 143 /** 144 * @var array<string, array{left?: int|string, right?: int|string}> Map from "{$node->getType()}->{$subNode}" 145 * to ['left' => $l, 'right' => $r], where $l and $r specify the token type that needs to be stripped 146 * when removing this node. 147 */ 148 protected array $removalMap; 149 /** 150 * @var array<string, array{int|string|null, bool, string|null, string|null}> Map from 151 * "{$node->getType()}->{$subNode}" to [$find, $beforeToken, $extraLeft, $extraRight]. 152 * $find is an optional token after which the insertion occurs. $extraLeft/Right 153 * are optionally added before/after the main insertions. 154 */ 155 protected array $insertionMap; 156 /** 157 * @var array<string, string> Map From "{$class}->{$subNode}" to string that should be inserted 158 * between elements of this list subnode. 159 */ 160 protected array $listInsertionMap; 161 162 /** 163 * @var array<string, array{int|string|null, string, string}> 164 */ 165 protected array $emptyListInsertionMap; 166 /** @var array<string, array{string, int}> Map from "{$class}->{$subNode}" to [$printFn, $token] 167 * where $printFn is the function to print the modifiers and $token is the token before which 168 * the modifiers should be reprinted. */ 169 protected array $modifierChangeMap; 170 171 /** 172 * Creates a pretty printer instance using the given options. 173 * 174 * Supported options: 175 * * PhpVersion $phpVersion: The PHP version to target (default to PHP 7.4). This option 176 * controls compatibility of the generated code with older PHP 177 * versions in cases where a simple stylistic choice exists (e.g. 178 * array() vs []). It is safe to pretty-print an AST for a newer 179 * PHP version while specifying an older target (but the result will 180 * of course not be compatible with the older version in that case). 181 * * string $newline: The newline style to use. Should be "\n" (default) or "\r\n". 182 * * string $indent: The indentation to use. Should either be all spaces or a single 183 * tab. Defaults to four spaces (" "). 184 * * bool $shortArraySyntax: Whether to use [] instead of array() as the default array 185 * syntax, if the node does not specify a format. Defaults to whether 186 * the phpVersion support short array syntax. 187 * 188 * @param array{ 189 * phpVersion?: PhpVersion, newline?: string, indent?: string, shortArraySyntax?: bool 190 * } $options Dictionary of formatting options 191 */ 192 public function __construct(array $options = []) { 193 $this->phpVersion = $options['phpVersion'] ?? PhpVersion::fromComponents(7, 4); 194 195 $this->newline = $options['newline'] ?? "\n"; 196 if ($this->newline !== "\n" && $this->newline != "\r\n") { 197 throw new \LogicException('Option "newline" must be one of "\n" or "\r\n"'); 198 } 199 200 $this->shortArraySyntax = 201 $options['shortArraySyntax'] ?? $this->phpVersion->supportsShortArraySyntax(); 202 $this->docStringEndToken = 203 $this->phpVersion->supportsFlexibleHeredoc() ? null : '_DOC_STRING_END_' . mt_rand(); 204 205 $this->indent = $indent = $options['indent'] ?? ' '; 206 if ($indent === "\t") { 207 $this->useTabs = true; 208 $this->indentWidth = $this->tabWidth; 209 } elseif ($indent === \str_repeat(' ', \strlen($indent))) { 210 $this->useTabs = false; 211 $this->indentWidth = \strlen($indent); 212 } else { 213 throw new \LogicException('Option "indent" must either be all spaces or a single tab'); 214 } 215 } 216 217 /** 218 * Reset pretty printing state. 219 */ 220 protected function resetState(): void { 221 $this->indentLevel = 0; 222 $this->nl = $this->newline; 223 $this->origTokens = null; 224 } 225 226 /** 227 * Set indentation level 228 * 229 * @param int $level Level in number of spaces 230 */ 231 protected function setIndentLevel(int $level): void { 232 $this->indentLevel = $level; 233 if ($this->useTabs) { 234 $tabs = \intdiv($level, $this->tabWidth); 235 $spaces = $level % $this->tabWidth; 236 $this->nl = $this->newline . \str_repeat("\t", $tabs) . \str_repeat(' ', $spaces); 237 } else { 238 $this->nl = $this->newline . \str_repeat(' ', $level); 239 } 240 } 241 242 /** 243 * Increase indentation level. 244 */ 245 protected function indent(): void { 246 $this->indentLevel += $this->indentWidth; 247 $this->nl .= $this->indent; 248 } 249 250 /** 251 * Decrease indentation level. 252 */ 253 protected function outdent(): void { 254 assert($this->indentLevel >= $this->indentWidth); 255 $this->setIndentLevel($this->indentLevel - $this->indentWidth); 256 } 257 258 /** 259 * Pretty prints an array of statements. 260 * 261 * @param Node[] $stmts Array of statements 262 * 263 * @return string Pretty printed statements 264 */ 265 public function prettyPrint(array $stmts): string { 266 $this->resetState(); 267 $this->preprocessNodes($stmts); 268 269 return ltrim($this->handleMagicTokens($this->pStmts($stmts, false))); 270 } 271 272 /** 273 * Pretty prints an expression. 274 * 275 * @param Expr $node Expression node 276 * 277 * @return string Pretty printed node 278 */ 279 public function prettyPrintExpr(Expr $node): string { 280 $this->resetState(); 281 return $this->handleMagicTokens($this->p($node)); 282 } 283 284 /** 285 * Pretty prints a file of statements (includes the opening <?php tag if it is required). 286 * 287 * @param Node[] $stmts Array of statements 288 * 289 * @return string Pretty printed statements 290 */ 291 public function prettyPrintFile(array $stmts): string { 292 if (!$stmts) { 293 return "<?php" . $this->newline . $this->newline; 294 } 295 296 $p = "<?php" . $this->newline . $this->newline . $this->prettyPrint($stmts); 297 298 if ($stmts[0] instanceof Stmt\InlineHTML) { 299 $p = preg_replace('/^<\?php\s+\?>\r?\n?/', '', $p); 300 } 301 if ($stmts[count($stmts) - 1] instanceof Stmt\InlineHTML) { 302 $p = preg_replace('/<\?php$/', '', rtrim($p)); 303 } 304 305 return $p; 306 } 307 308 /** 309 * Preprocesses the top-level nodes to initialize pretty printer state. 310 * 311 * @param Node[] $nodes Array of nodes 312 */ 313 protected function preprocessNodes(array $nodes): void { 314 /* We can use semicolon-namespaces unless there is a global namespace declaration */ 315 $this->canUseSemicolonNamespaces = true; 316 foreach ($nodes as $node) { 317 if ($node instanceof Stmt\Namespace_ && null === $node->name) { 318 $this->canUseSemicolonNamespaces = false; 319 break; 320 } 321 } 322 } 323 324 /** 325 * Handles (and removes) doc-string-end tokens. 326 */ 327 protected function handleMagicTokens(string $str): string { 328 if ($this->docStringEndToken !== null) { 329 // Replace doc-string-end tokens with nothing or a newline 330 $str = str_replace( 331 $this->docStringEndToken . ';' . $this->newline, 332 ';' . $this->newline, 333 $str); 334 $str = str_replace($this->docStringEndToken, $this->newline, $str); 335 } 336 337 return $str; 338 } 339 340 /** 341 * Pretty prints an array of nodes (statements) and indents them optionally. 342 * 343 * @param Node[] $nodes Array of nodes 344 * @param bool $indent Whether to indent the printed nodes 345 * 346 * @return string Pretty printed statements 347 */ 348 protected function pStmts(array $nodes, bool $indent = true): string { 349 if ($indent) { 350 $this->indent(); 351 } 352 353 $result = ''; 354 foreach ($nodes as $node) { 355 $comments = $node->getComments(); 356 if ($comments) { 357 $result .= $this->nl . $this->pComments($comments); 358 if ($node instanceof Stmt\Nop) { 359 continue; 360 } 361 } 362 363 $result .= $this->nl . $this->p($node); 364 } 365 366 if ($indent) { 367 $this->outdent(); 368 } 369 370 return $result; 371 } 372 373 /** 374 * Pretty-print an infix operation while taking precedence into account. 375 * 376 * @param string $class Node class of operator 377 * @param Node $leftNode Left-hand side node 378 * @param string $operatorString String representation of the operator 379 * @param Node $rightNode Right-hand side node 380 * @param int $precedence Precedence of parent operator 381 * @param int $lhsPrecedence Precedence for unary operator on LHS of binary operator 382 * 383 * @return string Pretty printed infix operation 384 */ 385 protected function pInfixOp( 386 string $class, Node $leftNode, string $operatorString, Node $rightNode, 387 int $precedence, int $lhsPrecedence 388 ): string { 389 list($opPrecedence, $newPrecedenceLHS, $newPrecedenceRHS) = $this->precedenceMap[$class]; 390 $prefix = ''; 391 $suffix = ''; 392 if ($opPrecedence >= $precedence) { 393 $prefix = '('; 394 $suffix = ')'; 395 $lhsPrecedence = self::MAX_PRECEDENCE; 396 } 397 return $prefix . $this->p($leftNode, $newPrecedenceLHS, $newPrecedenceLHS) 398 . $operatorString . $this->p($rightNode, $newPrecedenceRHS, $lhsPrecedence) . $suffix; 399 } 400 401 /** 402 * Pretty-print a prefix operation while taking precedence into account. 403 * 404 * @param string $class Node class of operator 405 * @param string $operatorString String representation of the operator 406 * @param Node $node Node 407 * @param int $precedence Precedence of parent operator 408 * @param int $lhsPrecedence Precedence for unary operator on LHS of binary operator 409 * 410 * @return string Pretty printed prefix operation 411 */ 412 protected function pPrefixOp(string $class, string $operatorString, Node $node, int $precedence, int $lhsPrecedence): string { 413 $opPrecedence = $this->precedenceMap[$class][0]; 414 $prefix = ''; 415 $suffix = ''; 416 if ($opPrecedence >= $lhsPrecedence) { 417 $prefix = '('; 418 $suffix = ')'; 419 $lhsPrecedence = self::MAX_PRECEDENCE; 420 } 421 $printedArg = $this->p($node, $opPrecedence, $lhsPrecedence); 422 if (($operatorString === '+' && $printedArg[0] === '+') || 423 ($operatorString === '-' && $printedArg[0] === '-') 424 ) { 425 // Avoid printing +(+$a) as ++$a and similar. 426 $printedArg = '(' . $printedArg . ')'; 427 } 428 return $prefix . $operatorString . $printedArg . $suffix; 429 } 430 431 /** 432 * Pretty-print a postfix operation while taking precedence into account. 433 * 434 * @param string $class Node class of operator 435 * @param string $operatorString String representation of the operator 436 * @param Node $node Node 437 * @param int $precedence Precedence of parent operator 438 * @param int $lhsPrecedence Precedence for unary operator on LHS of binary operator 439 * 440 * @return string Pretty printed postfix operation 441 */ 442 protected function pPostfixOp(string $class, Node $node, string $operatorString, int $precedence, int $lhsPrecedence): string { 443 $opPrecedence = $this->precedenceMap[$class][0]; 444 $prefix = ''; 445 $suffix = ''; 446 if ($opPrecedence >= $precedence) { 447 $prefix = '('; 448 $suffix = ')'; 449 $lhsPrecedence = self::MAX_PRECEDENCE; 450 } 451 if ($opPrecedence < $lhsPrecedence) { 452 $lhsPrecedence = $opPrecedence; 453 } 454 return $prefix . $this->p($node, $opPrecedence, $lhsPrecedence) . $operatorString . $suffix; 455 } 456 457 /** 458 * Pretty prints an array of nodes and implodes the printed values. 459 * 460 * @param Node[] $nodes Array of Nodes to be printed 461 * @param string $glue Character to implode with 462 * 463 * @return string Imploded pretty printed nodes> $pre 464 */ 465 protected function pImplode(array $nodes, string $glue = ''): string { 466 $pNodes = []; 467 foreach ($nodes as $node) { 468 if (null === $node) { 469 $pNodes[] = ''; 470 } else { 471 $pNodes[] = $this->p($node); 472 } 473 } 474 475 return implode($glue, $pNodes); 476 } 477 478 /** 479 * Pretty prints an array of nodes and implodes the printed values with commas. 480 * 481 * @param Node[] $nodes Array of Nodes to be printed 482 * 483 * @return string Comma separated pretty printed nodes 484 */ 485 protected function pCommaSeparated(array $nodes): string { 486 return $this->pImplode($nodes, ', '); 487 } 488 489 /** 490 * Pretty prints a comma-separated list of nodes in multiline style, including comments. 491 * 492 * The result includes a leading newline and one level of indentation (same as pStmts). 493 * 494 * @param Node[] $nodes Array of Nodes to be printed 495 * @param bool $trailingComma Whether to use a trailing comma 496 * 497 * @return string Comma separated pretty printed nodes in multiline style 498 */ 499 protected function pCommaSeparatedMultiline(array $nodes, bool $trailingComma): string { 500 $this->indent(); 501 502 $result = ''; 503 $lastIdx = count($nodes) - 1; 504 foreach ($nodes as $idx => $node) { 505 if ($node !== null) { 506 $comments = $node->getComments(); 507 if ($comments) { 508 $result .= $this->nl . $this->pComments($comments); 509 } 510 511 $result .= $this->nl . $this->p($node); 512 } else { 513 $result .= $this->nl; 514 } 515 if ($trailingComma || $idx !== $lastIdx) { 516 $result .= ','; 517 } 518 } 519 520 $this->outdent(); 521 return $result; 522 } 523 524 /** 525 * Prints reformatted text of the passed comments. 526 * 527 * @param Comment[] $comments List of comments 528 * 529 * @return string Reformatted text of comments 530 */ 531 protected function pComments(array $comments): string { 532 $formattedComments = []; 533 534 foreach ($comments as $comment) { 535 $formattedComments[] = str_replace("\n", $this->nl, $comment->getReformattedText()); 536 } 537 538 return implode($this->nl, $formattedComments); 539 } 540 541 /** 542 * Perform a format-preserving pretty print of an AST. 543 * 544 * The format preservation is best effort. For some changes to the AST the formatting will not 545 * be preserved (at least not locally). 546 * 547 * In order to use this method a number of prerequisites must be satisfied: 548 * * The startTokenPos and endTokenPos attributes in the lexer must be enabled. 549 * * The CloningVisitor must be run on the AST prior to modification. 550 * * The original tokens must be provided, using the getTokens() method on the lexer. 551 * 552 * @param Node[] $stmts Modified AST with links to original AST 553 * @param Node[] $origStmts Original AST with token offset information 554 * @param Token[] $origTokens Tokens of the original code 555 */ 556 public function printFormatPreserving(array $stmts, array $origStmts, array $origTokens): string { 557 $this->initializeNodeListDiffer(); 558 $this->initializeLabelCharMap(); 559 $this->initializeFixupMap(); 560 $this->initializeRemovalMap(); 561 $this->initializeInsertionMap(); 562 $this->initializeListInsertionMap(); 563 $this->initializeEmptyListInsertionMap(); 564 $this->initializeModifierChangeMap(); 565 566 $this->resetState(); 567 $this->origTokens = new TokenStream($origTokens, $this->tabWidth); 568 569 $this->preprocessNodes($stmts); 570 571 $pos = 0; 572 $result = $this->pArray($stmts, $origStmts, $pos, 0, 'File', 'stmts', null); 573 if (null !== $result) { 574 $result .= $this->origTokens->getTokenCode($pos, count($origTokens) - 1, 0); 575 } else { 576 // Fallback 577 // TODO Add <?php properly 578 $result = "<?php" . $this->newline . $this->pStmts($stmts, false); 579 } 580 581 return $this->handleMagicTokens($result); 582 } 583 584 protected function pFallback(Node $node, int $precedence, int $lhsPrecedence): string { 585 return $this->{'p' . $node->getType()}($node, $precedence, $lhsPrecedence); 586 } 587 588 /** 589 * Pretty prints a node. 590 * 591 * This method also handles formatting preservation for nodes. 592 * 593 * @param Node $node Node to be pretty printed 594 * @param int $precedence Precedence of parent operator 595 * @param int $lhsPrecedence Precedence for unary operator on LHS of binary operator 596 * @param bool $parentFormatPreserved Whether parent node has preserved formatting 597 * 598 * @return string Pretty printed node 599 */ 600 protected function p( 601 Node $node, int $precedence = self::MAX_PRECEDENCE, int $lhsPrecedence = self::MAX_PRECEDENCE, 602 bool $parentFormatPreserved = false 603 ): string { 604 // No orig tokens means this is a normal pretty print without preservation of formatting 605 if (!$this->origTokens) { 606 return $this->{'p' . $node->getType()}($node, $precedence, $lhsPrecedence); 607 } 608 609 /** @var Node|null $origNode */ 610 $origNode = $node->getAttribute('origNode'); 611 if (null === $origNode) { 612 return $this->pFallback($node, $precedence, $lhsPrecedence); 613 } 614 615 $class = \get_class($node); 616 \assert($class === \get_class($origNode)); 617 618 $startPos = $origNode->getStartTokenPos(); 619 $endPos = $origNode->getEndTokenPos(); 620 \assert($startPos >= 0 && $endPos >= 0); 621 622 $fallbackNode = $node; 623 if ($node instanceof Expr\New_ && $node->class instanceof Stmt\Class_) { 624 // Normalize node structure of anonymous classes 625 assert($origNode instanceof Expr\New_); 626 $node = PrintableNewAnonClassNode::fromNewNode($node); 627 $origNode = PrintableNewAnonClassNode::fromNewNode($origNode); 628 $class = PrintableNewAnonClassNode::class; 629 } 630 631 // InlineHTML node does not contain closing and opening PHP tags. If the parent formatting 632 // is not preserved, then we need to use the fallback code to make sure the tags are 633 // printed. 634 if ($node instanceof Stmt\InlineHTML && !$parentFormatPreserved) { 635 return $this->pFallback($fallbackNode, $precedence, $lhsPrecedence); 636 } 637 638 $indentAdjustment = $this->indentLevel - $this->origTokens->getIndentationBefore($startPos); 639 640 $type = $node->getType(); 641 $fixupInfo = $this->fixupMap[$class] ?? null; 642 643 $result = ''; 644 $pos = $startPos; 645 foreach ($node->getSubNodeNames() as $subNodeName) { 646 $subNode = $node->$subNodeName; 647 $origSubNode = $origNode->$subNodeName; 648 649 if ((!$subNode instanceof Node && $subNode !== null) 650 || (!$origSubNode instanceof Node && $origSubNode !== null) 651 ) { 652 if ($subNode === $origSubNode) { 653 // Unchanged, can reuse old code 654 continue; 655 } 656 657 if (is_array($subNode) && is_array($origSubNode)) { 658 // Array subnode changed, we might be able to reconstruct it 659 $listResult = $this->pArray( 660 $subNode, $origSubNode, $pos, $indentAdjustment, $class, $subNodeName, 661 $fixupInfo[$subNodeName] ?? null 662 ); 663 if (null === $listResult) { 664 return $this->pFallback($fallbackNode, $precedence, $lhsPrecedence); 665 } 666 667 $result .= $listResult; 668 continue; 669 } 670 671 // Check if this is a modifier change 672 $key = $class . '->' . $subNodeName; 673 if (!isset($this->modifierChangeMap[$key])) { 674 return $this->pFallback($fallbackNode, $precedence, $lhsPrecedence); 675 } 676 677 [$printFn, $findToken] = $this->modifierChangeMap[$key]; 678 $result .= $this->$printFn($subNode); 679 $pos = $this->origTokens->findRight($pos, $findToken); 680 continue; 681 } 682 683 $extraLeft = ''; 684 $extraRight = ''; 685 if ($origSubNode !== null) { 686 $subStartPos = $origSubNode->getStartTokenPos(); 687 $subEndPos = $origSubNode->getEndTokenPos(); 688 \assert($subStartPos >= 0 && $subEndPos >= 0); 689 } else { 690 if ($subNode === null) { 691 // Both null, nothing to do 692 continue; 693 } 694 695 // A node has been inserted, check if we have insertion information for it 696 $key = $type . '->' . $subNodeName; 697 if (!isset($this->insertionMap[$key])) { 698 return $this->pFallback($fallbackNode, $precedence, $lhsPrecedence); 699 } 700 701 list($findToken, $beforeToken, $extraLeft, $extraRight) = $this->insertionMap[$key]; 702 if (null !== $findToken) { 703 $subStartPos = $this->origTokens->findRight($pos, $findToken) 704 + (int) !$beforeToken; 705 } else { 706 $subStartPos = $pos; 707 } 708 709 if (null === $extraLeft && null !== $extraRight) { 710 // If inserting on the right only, skipping whitespace looks better 711 $subStartPos = $this->origTokens->skipRightWhitespace($subStartPos); 712 } 713 $subEndPos = $subStartPos - 1; 714 } 715 716 if (null === $subNode) { 717 // A node has been removed, check if we have removal information for it 718 $key = $type . '->' . $subNodeName; 719 if (!isset($this->removalMap[$key])) { 720 return $this->pFallback($fallbackNode, $precedence, $lhsPrecedence); 721 } 722 723 // Adjust positions to account for additional tokens that must be skipped 724 $removalInfo = $this->removalMap[$key]; 725 if (isset($removalInfo['left'])) { 726 $subStartPos = $this->origTokens->skipLeft($subStartPos - 1, $removalInfo['left']) + 1; 727 } 728 if (isset($removalInfo['right'])) { 729 $subEndPos = $this->origTokens->skipRight($subEndPos + 1, $removalInfo['right']) - 1; 730 } 731 } 732 733 $result .= $this->origTokens->getTokenCode($pos, $subStartPos, $indentAdjustment); 734 735 if (null !== $subNode) { 736 $result .= $extraLeft; 737 738 $origIndentLevel = $this->indentLevel; 739 $this->setIndentLevel(max($this->origTokens->getIndentationBefore($subStartPos) + $indentAdjustment, 0)); 740 741 // If it's the same node that was previously in this position, it certainly doesn't 742 // need fixup. It's important to check this here, because our fixup checks are more 743 // conservative than strictly necessary. 744 if (isset($fixupInfo[$subNodeName]) 745 && $subNode->getAttribute('origNode') !== $origSubNode 746 ) { 747 $fixup = $fixupInfo[$subNodeName]; 748 $res = $this->pFixup($fixup, $subNode, $class, $subStartPos, $subEndPos); 749 } else { 750 $res = $this->p($subNode, self::MAX_PRECEDENCE, self::MAX_PRECEDENCE, true); 751 } 752 753 $this->safeAppend($result, $res); 754 $this->setIndentLevel($origIndentLevel); 755 756 $result .= $extraRight; 757 } 758 759 $pos = $subEndPos + 1; 760 } 761 762 $result .= $this->origTokens->getTokenCode($pos, $endPos + 1, $indentAdjustment); 763 return $result; 764 } 765 766 /** 767 * Perform a format-preserving pretty print of an array. 768 * 769 * @param Node[] $nodes New nodes 770 * @param Node[] $origNodes Original nodes 771 * @param int $pos Current token position (updated by reference) 772 * @param int $indentAdjustment Adjustment for indentation 773 * @param string $parentNodeClass Class of the containing node. 774 * @param string $subNodeName Name of array subnode. 775 * @param null|int $fixup Fixup information for array item nodes 776 * 777 * @return null|string Result of pretty print or null if cannot preserve formatting 778 */ 779 protected function pArray( 780 array $nodes, array $origNodes, int &$pos, int $indentAdjustment, 781 string $parentNodeClass, string $subNodeName, ?int $fixup 782 ): ?string { 783 $diff = $this->nodeListDiffer->diffWithReplacements($origNodes, $nodes); 784 785 $mapKey = $parentNodeClass . '->' . $subNodeName; 786 $insertStr = $this->listInsertionMap[$mapKey] ?? null; 787 $isStmtList = $subNodeName === 'stmts'; 788 789 $beforeFirstKeepOrReplace = true; 790 $skipRemovedNode = false; 791 $delayedAdd = []; 792 $lastElemIndentLevel = $this->indentLevel; 793 794 $insertNewline = false; 795 if ($insertStr === "\n") { 796 $insertStr = ''; 797 $insertNewline = true; 798 } 799 800 if ($isStmtList && \count($origNodes) === 1 && \count($nodes) !== 1) { 801 $startPos = $origNodes[0]->getStartTokenPos(); 802 $endPos = $origNodes[0]->getEndTokenPos(); 803 \assert($startPos >= 0 && $endPos >= 0); 804 if (!$this->origTokens->haveBraces($startPos, $endPos)) { 805 // This was a single statement without braces, but either additional statements 806 // have been added, or the single statement has been removed. This requires the 807 // addition of braces. For now fall back. 808 // TODO: Try to preserve formatting 809 return null; 810 } 811 } 812 813 $result = ''; 814 foreach ($diff as $i => $diffElem) { 815 $diffType = $diffElem->type; 816 /** @var Node|string|null $arrItem */ 817 $arrItem = $diffElem->new; 818 /** @var Node|string|null $origArrItem */ 819 $origArrItem = $diffElem->old; 820 821 if ($diffType === DiffElem::TYPE_KEEP || $diffType === DiffElem::TYPE_REPLACE) { 822 $beforeFirstKeepOrReplace = false; 823 824 if ($origArrItem === null || $arrItem === null) { 825 // We can only handle the case where both are null 826 if ($origArrItem === $arrItem) { 827 continue; 828 } 829 return null; 830 } 831 832 if (!$arrItem instanceof Node || !$origArrItem instanceof Node) { 833 // We can only deal with nodes. This can occur for Names, which use string arrays. 834 return null; 835 } 836 837 $itemStartPos = $origArrItem->getStartTokenPos(); 838 $itemEndPos = $origArrItem->getEndTokenPos(); 839 \assert($itemStartPos >= 0 && $itemEndPos >= 0 && $itemStartPos >= $pos); 840 841 $origIndentLevel = $this->indentLevel; 842 $lastElemIndentLevel = max($this->origTokens->getIndentationBefore($itemStartPos) + $indentAdjustment, 0); 843 $this->setIndentLevel($lastElemIndentLevel); 844 845 $comments = $arrItem->getComments(); 846 $origComments = $origArrItem->getComments(); 847 $commentStartPos = $origComments ? $origComments[0]->getStartTokenPos() : $itemStartPos; 848 \assert($commentStartPos >= 0); 849 850 if ($commentStartPos < $pos) { 851 // Comments may be assigned to multiple nodes if they start at the same position. 852 // Make sure we don't try to print them multiple times. 853 $commentStartPos = $itemStartPos; 854 } 855 856 if ($skipRemovedNode) { 857 if ($isStmtList && $this->origTokens->haveTagInRange($pos, $itemStartPos)) { 858 // We'd remove an opening/closing PHP tag. 859 // TODO: Preserve formatting. 860 $this->setIndentLevel($origIndentLevel); 861 return null; 862 } 863 } else { 864 $result .= $this->origTokens->getTokenCode( 865 $pos, $commentStartPos, $indentAdjustment); 866 } 867 868 if (!empty($delayedAdd)) { 869 /** @var Node $delayedAddNode */ 870 foreach ($delayedAdd as $delayedAddNode) { 871 if ($insertNewline) { 872 $delayedAddComments = $delayedAddNode->getComments(); 873 if ($delayedAddComments) { 874 $result .= $this->pComments($delayedAddComments) . $this->nl; 875 } 876 } 877 878 $this->safeAppend($result, $this->p($delayedAddNode, self::MAX_PRECEDENCE, self::MAX_PRECEDENCE, true)); 879 880 if ($insertNewline) { 881 $result .= $insertStr . $this->nl; 882 } else { 883 $result .= $insertStr; 884 } 885 } 886 887 $delayedAdd = []; 888 } 889 890 if ($comments !== $origComments) { 891 if ($comments) { 892 $result .= $this->pComments($comments) . $this->nl; 893 } 894 } else { 895 $result .= $this->origTokens->getTokenCode( 896 $commentStartPos, $itemStartPos, $indentAdjustment); 897 } 898 899 // If we had to remove anything, we have done so now. 900 $skipRemovedNode = false; 901 } elseif ($diffType === DiffElem::TYPE_ADD) { 902 if (null === $insertStr) { 903 // We don't have insertion information for this list type 904 return null; 905 } 906 907 if (!$arrItem instanceof Node) { 908 // We only support list insertion of nodes. 909 return null; 910 } 911 912 // We go multiline if the original code was multiline, 913 // or if it's an array item with a comment above it. 914 // Match always uses multiline formatting. 915 if ($insertStr === ', ' && 916 ($this->isMultiline($origNodes) || $arrItem->getComments() || 917 $parentNodeClass === Expr\Match_::class) 918 ) { 919 $insertStr = ','; 920 $insertNewline = true; 921 } 922 923 if ($beforeFirstKeepOrReplace) { 924 // Will be inserted at the next "replace" or "keep" element 925 $delayedAdd[] = $arrItem; 926 continue; 927 } 928 929 $itemStartPos = $pos; 930 $itemEndPos = $pos - 1; 931 932 $origIndentLevel = $this->indentLevel; 933 $this->setIndentLevel($lastElemIndentLevel); 934 935 if ($insertNewline) { 936 $result .= $insertStr . $this->nl; 937 $comments = $arrItem->getComments(); 938 if ($comments) { 939 $result .= $this->pComments($comments) . $this->nl; 940 } 941 } else { 942 $result .= $insertStr; 943 } 944 } elseif ($diffType === DiffElem::TYPE_REMOVE) { 945 if (!$origArrItem instanceof Node) { 946 // We only support removal for nodes 947 return null; 948 } 949 950 $itemStartPos = $origArrItem->getStartTokenPos(); 951 $itemEndPos = $origArrItem->getEndTokenPos(); 952 \assert($itemStartPos >= 0 && $itemEndPos >= 0); 953 954 // Consider comments part of the node. 955 $origComments = $origArrItem->getComments(); 956 if ($origComments) { 957 $itemStartPos = $origComments[0]->getStartTokenPos(); 958 } 959 960 if ($i === 0) { 961 // If we're removing from the start, keep the tokens before the node and drop those after it, 962 // instead of the other way around. 963 $result .= $this->origTokens->getTokenCode( 964 $pos, $itemStartPos, $indentAdjustment); 965 $skipRemovedNode = true; 966 } else { 967 if ($isStmtList && $this->origTokens->haveTagInRange($pos, $itemStartPos)) { 968 // We'd remove an opening/closing PHP tag. 969 // TODO: Preserve formatting. 970 return null; 971 } 972 } 973 974 $pos = $itemEndPos + 1; 975 continue; 976 } else { 977 throw new \Exception("Shouldn't happen"); 978 } 979 980 if (null !== $fixup && $arrItem->getAttribute('origNode') !== $origArrItem) { 981 $res = $this->pFixup($fixup, $arrItem, null, $itemStartPos, $itemEndPos); 982 } else { 983 $res = $this->p($arrItem, self::MAX_PRECEDENCE, self::MAX_PRECEDENCE, true); 984 } 985 $this->safeAppend($result, $res); 986 987 $this->setIndentLevel($origIndentLevel); 988 $pos = $itemEndPos + 1; 989 } 990 991 if ($skipRemovedNode) { 992 // TODO: Support removing single node. 993 return null; 994 } 995 996 if (!empty($delayedAdd)) { 997 if (!isset($this->emptyListInsertionMap[$mapKey])) { 998 return null; 999 } 1000 1001 list($findToken, $extraLeft, $extraRight) = $this->emptyListInsertionMap[$mapKey]; 1002 if (null !== $findToken) { 1003 $insertPos = $this->origTokens->findRight($pos, $findToken) + 1; 1004 $result .= $this->origTokens->getTokenCode($pos, $insertPos, $indentAdjustment); 1005 $pos = $insertPos; 1006 } 1007 1008 $first = true; 1009 $result .= $extraLeft; 1010 foreach ($delayedAdd as $delayedAddNode) { 1011 if (!$first) { 1012 $result .= $insertStr; 1013 if ($insertNewline) { 1014 $result .= $this->nl; 1015 } 1016 } 1017 $result .= $this->p($delayedAddNode, self::MAX_PRECEDENCE, self::MAX_PRECEDENCE, true); 1018 $first = false; 1019 } 1020 $result .= $extraRight === "\n" ? $this->nl : $extraRight; 1021 } 1022 1023 return $result; 1024 } 1025 1026 /** 1027 * Print node with fixups. 1028 * 1029 * Fixups here refer to the addition of extra parentheses, braces or other characters, that 1030 * are required to preserve program semantics in a certain context (e.g. to maintain precedence 1031 * or because only certain expressions are allowed in certain places). 1032 * 1033 * @param int $fixup Fixup type 1034 * @param Node $subNode Subnode to print 1035 * @param string|null $parentClass Class of parent node 1036 * @param int $subStartPos Original start pos of subnode 1037 * @param int $subEndPos Original end pos of subnode 1038 * 1039 * @return string Result of fixed-up print of subnode 1040 */ 1041 protected function pFixup(int $fixup, Node $subNode, ?string $parentClass, int $subStartPos, int $subEndPos): string { 1042 switch ($fixup) { 1043 case self::FIXUP_PREC_LEFT: 1044 // We use a conservative approximation where lhsPrecedence == precedence. 1045 if (!$this->origTokens->haveParens($subStartPos, $subEndPos)) { 1046 $precedence = $this->precedenceMap[$parentClass][1]; 1047 return $this->p($subNode, $precedence, $precedence); 1048 } 1049 break; 1050 case self::FIXUP_PREC_RIGHT: 1051 if (!$this->origTokens->haveParens($subStartPos, $subEndPos)) { 1052 $precedence = $this->precedenceMap[$parentClass][2]; 1053 return $this->p($subNode, $precedence, $precedence); 1054 } 1055 break; 1056 case self::FIXUP_PREC_UNARY: 1057 if (!$this->origTokens->haveParens($subStartPos, $subEndPos)) { 1058 $precedence = $this->precedenceMap[$parentClass][0]; 1059 return $this->p($subNode, $precedence, $precedence); 1060 } 1061 break; 1062 case self::FIXUP_CALL_LHS: 1063 if ($this->callLhsRequiresParens($subNode) 1064 && !$this->origTokens->haveParens($subStartPos, $subEndPos) 1065 ) { 1066 return '(' . $this->p($subNode) . ')'; 1067 } 1068 break; 1069 case self::FIXUP_DEREF_LHS: 1070 if ($this->dereferenceLhsRequiresParens($subNode) 1071 && !$this->origTokens->haveParens($subStartPos, $subEndPos) 1072 ) { 1073 return '(' . $this->p($subNode) . ')'; 1074 } 1075 break; 1076 case self::FIXUP_STATIC_DEREF_LHS: 1077 if ($this->staticDereferenceLhsRequiresParens($subNode) 1078 && !$this->origTokens->haveParens($subStartPos, $subEndPos) 1079 ) { 1080 return '(' . $this->p($subNode) . ')'; 1081 } 1082 break; 1083 case self::FIXUP_NEW: 1084 if ($this->newOperandRequiresParens($subNode) 1085 && !$this->origTokens->haveParens($subStartPos, $subEndPos)) { 1086 return '(' . $this->p($subNode) . ')'; 1087 } 1088 break; 1089 case self::FIXUP_BRACED_NAME: 1090 case self::FIXUP_VAR_BRACED_NAME: 1091 if ($subNode instanceof Expr 1092 && !$this->origTokens->haveBraces($subStartPos, $subEndPos) 1093 ) { 1094 return ($fixup === self::FIXUP_VAR_BRACED_NAME ? '$' : '') 1095 . '{' . $this->p($subNode) . '}'; 1096 } 1097 break; 1098 case self::FIXUP_ENCAPSED: 1099 if (!$subNode instanceof Node\InterpolatedStringPart 1100 && !$this->origTokens->haveBraces($subStartPos, $subEndPos) 1101 ) { 1102 return '{' . $this->p($subNode) . '}'; 1103 } 1104 break; 1105 default: 1106 throw new \Exception('Cannot happen'); 1107 } 1108 1109 // Nothing special to do 1110 return $this->p($subNode); 1111 } 1112 1113 /** 1114 * Appends to a string, ensuring whitespace between label characters. 1115 * 1116 * Example: "echo" and "$x" result in "echo$x", but "echo" and "x" result in "echo x". 1117 * Without safeAppend the result would be "echox", which does not preserve semantics. 1118 */ 1119 protected function safeAppend(string &$str, string $append): void { 1120 if ($str === "") { 1121 $str = $append; 1122 return; 1123 } 1124 1125 if ($append === "") { 1126 return; 1127 } 1128 1129 if (!$this->labelCharMap[$append[0]] 1130 || !$this->labelCharMap[$str[\strlen($str) - 1]]) { 1131 $str .= $append; 1132 } else { 1133 $str .= " " . $append; 1134 } 1135 } 1136 1137 /** 1138 * Determines whether the LHS of a call must be wrapped in parenthesis. 1139 * 1140 * @param Node $node LHS of a call 1141 * 1142 * @return bool Whether parentheses are required 1143 */ 1144 protected function callLhsRequiresParens(Node $node): bool { 1145 return !($node instanceof Node\Name 1146 || $node instanceof Expr\Variable 1147 || $node instanceof Expr\ArrayDimFetch 1148 || $node instanceof Expr\FuncCall 1149 || $node instanceof Expr\MethodCall 1150 || $node instanceof Expr\NullsafeMethodCall 1151 || $node instanceof Expr\StaticCall 1152 || $node instanceof Expr\Array_); 1153 } 1154 1155 /** 1156 * Determines whether the LHS of an array/object operation must be wrapped in parentheses. 1157 * 1158 * @param Node $node LHS of dereferencing operation 1159 * 1160 * @return bool Whether parentheses are required 1161 */ 1162 protected function dereferenceLhsRequiresParens(Node $node): bool { 1163 // A constant can occur on the LHS of an array/object deref, but not a static deref. 1164 return $this->staticDereferenceLhsRequiresParens($node) 1165 && !$node instanceof Expr\ConstFetch; 1166 } 1167 1168 /** 1169 * Determines whether the LHS of a static operation must be wrapped in parentheses. 1170 * 1171 * @param Node $node LHS of dereferencing operation 1172 * 1173 * @return bool Whether parentheses are required 1174 */ 1175 protected function staticDereferenceLhsRequiresParens(Node $node): bool { 1176 return !($node instanceof Expr\Variable 1177 || $node instanceof Node\Name 1178 || $node instanceof Expr\ArrayDimFetch 1179 || $node instanceof Expr\PropertyFetch 1180 || $node instanceof Expr\NullsafePropertyFetch 1181 || $node instanceof Expr\StaticPropertyFetch 1182 || $node instanceof Expr\FuncCall 1183 || $node instanceof Expr\MethodCall 1184 || $node instanceof Expr\NullsafeMethodCall 1185 || $node instanceof Expr\StaticCall 1186 || $node instanceof Expr\Array_ 1187 || $node instanceof Scalar\String_ 1188 || $node instanceof Expr\ClassConstFetch); 1189 } 1190 1191 /** 1192 * Determines whether an expression used in "new" or "instanceof" requires parentheses. 1193 * 1194 * @param Node $node New or instanceof operand 1195 * 1196 * @return bool Whether parentheses are required 1197 */ 1198 protected function newOperandRequiresParens(Node $node): bool { 1199 if ($node instanceof Node\Name || $node instanceof Expr\Variable) { 1200 return false; 1201 } 1202 if ($node instanceof Expr\ArrayDimFetch || $node instanceof Expr\PropertyFetch || 1203 $node instanceof Expr\NullsafePropertyFetch 1204 ) { 1205 return $this->newOperandRequiresParens($node->var); 1206 } 1207 if ($node instanceof Expr\StaticPropertyFetch) { 1208 return $this->newOperandRequiresParens($node->class); 1209 } 1210 return true; 1211 } 1212 1213 /** 1214 * Print modifiers, including trailing whitespace. 1215 * 1216 * @param int $modifiers Modifier mask to print 1217 * 1218 * @return string Printed modifiers 1219 */ 1220 protected function pModifiers(int $modifiers): string { 1221 return ($modifiers & Modifiers::FINAL ? 'final ' : '') 1222 . ($modifiers & Modifiers::ABSTRACT ? 'abstract ' : '') 1223 . ($modifiers & Modifiers::PUBLIC ? 'public ' : '') 1224 . ($modifiers & Modifiers::PROTECTED ? 'protected ' : '') 1225 . ($modifiers & Modifiers::PRIVATE ? 'private ' : '') 1226 . ($modifiers & Modifiers::PUBLIC_SET ? 'public(set) ' : '') 1227 . ($modifiers & Modifiers::PROTECTED_SET ? 'protected(set) ' : '') 1228 . ($modifiers & Modifiers::PRIVATE_SET ? 'private(set) ' : '') 1229 . ($modifiers & Modifiers::STATIC ? 'static ' : '') 1230 . ($modifiers & Modifiers::READONLY ? 'readonly ' : ''); 1231 } 1232 1233 protected function pStatic(bool $static): string { 1234 return $static ? 'static ' : ''; 1235 } 1236 1237 /** 1238 * Determine whether a list of nodes uses multiline formatting. 1239 * 1240 * @param (Node|null)[] $nodes Node list 1241 * 1242 * @return bool Whether multiline formatting is used 1243 */ 1244 protected function isMultiline(array $nodes): bool { 1245 if (\count($nodes) < 2) { 1246 return false; 1247 } 1248 1249 $pos = -1; 1250 foreach ($nodes as $node) { 1251 if (null === $node) { 1252 continue; 1253 } 1254 1255 $endPos = $node->getEndTokenPos() + 1; 1256 if ($pos >= 0) { 1257 $text = $this->origTokens->getTokenCode($pos, $endPos, 0); 1258 if (false === strpos($text, "\n")) { 1259 // We require that a newline is present between *every* item. If the formatting 1260 // is inconsistent, with only some items having newlines, we don't consider it 1261 // as multiline 1262 return false; 1263 } 1264 } 1265 $pos = $endPos; 1266 } 1267 1268 return true; 1269 } 1270 1271 /** 1272 * Lazily initializes label char map. 1273 * 1274 * The label char map determines whether a certain character may occur in a label. 1275 */ 1276 protected function initializeLabelCharMap(): void { 1277 if (isset($this->labelCharMap)) { 1278 return; 1279 } 1280 1281 $this->labelCharMap = []; 1282 for ($i = 0; $i < 256; $i++) { 1283 $chr = chr($i); 1284 $this->labelCharMap[$chr] = $i >= 0x80 || ctype_alnum($chr); 1285 } 1286 1287 if ($this->phpVersion->allowsDelInIdentifiers()) { 1288 $this->labelCharMap["\x7f"] = true; 1289 } 1290 } 1291 1292 /** 1293 * Lazily initializes node list differ. 1294 * 1295 * The node list differ is used to determine differences between two array subnodes. 1296 */ 1297 protected function initializeNodeListDiffer(): void { 1298 if (isset($this->nodeListDiffer)) { 1299 return; 1300 } 1301 1302 $this->nodeListDiffer = new Internal\Differ(function ($a, $b) { 1303 if ($a instanceof Node && $b instanceof Node) { 1304 return $a === $b->getAttribute('origNode'); 1305 } 1306 // Can happen for array destructuring 1307 return $a === null && $b === null; 1308 }); 1309 } 1310 1311 /** 1312 * Lazily initializes fixup map. 1313 * 1314 * The fixup map is used to determine whether a certain subnode of a certain node may require 1315 * some kind of "fixup" operation, e.g. the addition of parenthesis or braces. 1316 */ 1317 protected function initializeFixupMap(): void { 1318 if (isset($this->fixupMap)) { 1319 return; 1320 } 1321 1322 $this->fixupMap = [ 1323 Expr\Instanceof_::class => [ 1324 'expr' => self::FIXUP_PREC_UNARY, 1325 'class' => self::FIXUP_NEW, 1326 ], 1327 Expr\Ternary::class => [ 1328 'cond' => self::FIXUP_PREC_LEFT, 1329 'else' => self::FIXUP_PREC_RIGHT, 1330 ], 1331 Expr\Yield_::class => ['value' => self::FIXUP_PREC_UNARY], 1332 1333 Expr\FuncCall::class => ['name' => self::FIXUP_CALL_LHS], 1334 Expr\StaticCall::class => ['class' => self::FIXUP_STATIC_DEREF_LHS], 1335 Expr\ArrayDimFetch::class => ['var' => self::FIXUP_DEREF_LHS], 1336 Expr\ClassConstFetch::class => [ 1337 'class' => self::FIXUP_STATIC_DEREF_LHS, 1338 'name' => self::FIXUP_BRACED_NAME, 1339 ], 1340 Expr\New_::class => ['class' => self::FIXUP_NEW], 1341 Expr\MethodCall::class => [ 1342 'var' => self::FIXUP_DEREF_LHS, 1343 'name' => self::FIXUP_BRACED_NAME, 1344 ], 1345 Expr\NullsafeMethodCall::class => [ 1346 'var' => self::FIXUP_DEREF_LHS, 1347 'name' => self::FIXUP_BRACED_NAME, 1348 ], 1349 Expr\StaticPropertyFetch::class => [ 1350 'class' => self::FIXUP_STATIC_DEREF_LHS, 1351 'name' => self::FIXUP_VAR_BRACED_NAME, 1352 ], 1353 Expr\PropertyFetch::class => [ 1354 'var' => self::FIXUP_DEREF_LHS, 1355 'name' => self::FIXUP_BRACED_NAME, 1356 ], 1357 Expr\NullsafePropertyFetch::class => [ 1358 'var' => self::FIXUP_DEREF_LHS, 1359 'name' => self::FIXUP_BRACED_NAME, 1360 ], 1361 Scalar\InterpolatedString::class => [ 1362 'parts' => self::FIXUP_ENCAPSED, 1363 ], 1364 ]; 1365 1366 $binaryOps = [ 1367 BinaryOp\Pow::class, BinaryOp\Mul::class, BinaryOp\Div::class, BinaryOp\Mod::class, 1368 BinaryOp\Plus::class, BinaryOp\Minus::class, BinaryOp\Concat::class, 1369 BinaryOp\ShiftLeft::class, BinaryOp\ShiftRight::class, BinaryOp\Smaller::class, 1370 BinaryOp\SmallerOrEqual::class, BinaryOp\Greater::class, BinaryOp\GreaterOrEqual::class, 1371 BinaryOp\Equal::class, BinaryOp\NotEqual::class, BinaryOp\Identical::class, 1372 BinaryOp\NotIdentical::class, BinaryOp\Spaceship::class, BinaryOp\BitwiseAnd::class, 1373 BinaryOp\BitwiseXor::class, BinaryOp\BitwiseOr::class, BinaryOp\BooleanAnd::class, 1374 BinaryOp\BooleanOr::class, BinaryOp\Coalesce::class, BinaryOp\LogicalAnd::class, 1375 BinaryOp\LogicalXor::class, BinaryOp\LogicalOr::class, 1376 ]; 1377 foreach ($binaryOps as $binaryOp) { 1378 $this->fixupMap[$binaryOp] = [ 1379 'left' => self::FIXUP_PREC_LEFT, 1380 'right' => self::FIXUP_PREC_RIGHT 1381 ]; 1382 } 1383 1384 $prefixOps = [ 1385 Expr\Clone_::class, Expr\BitwiseNot::class, Expr\BooleanNot::class, Expr\UnaryPlus::class, Expr\UnaryMinus::class, 1386 Cast\Int_::class, Cast\Double::class, Cast\String_::class, Cast\Array_::class, 1387 Cast\Object_::class, Cast\Bool_::class, Cast\Unset_::class, Expr\ErrorSuppress::class, 1388 Expr\YieldFrom::class, Expr\Print_::class, Expr\Include_::class, 1389 Expr\Assign::class, Expr\AssignRef::class, AssignOp\Plus::class, AssignOp\Minus::class, 1390 AssignOp\Mul::class, AssignOp\Div::class, AssignOp\Concat::class, AssignOp\Mod::class, 1391 AssignOp\BitwiseAnd::class, AssignOp\BitwiseOr::class, AssignOp\BitwiseXor::class, 1392 AssignOp\ShiftLeft::class, AssignOp\ShiftRight::class, AssignOp\Pow::class, AssignOp\Coalesce::class, 1393 Expr\ArrowFunction::class, Expr\Throw_::class, 1394 ]; 1395 foreach ($prefixOps as $prefixOp) { 1396 $this->fixupMap[$prefixOp] = ['expr' => self::FIXUP_PREC_UNARY]; 1397 } 1398 } 1399 1400 /** 1401 * Lazily initializes the removal map. 1402 * 1403 * The removal map is used to determine which additional tokens should be removed when a 1404 * certain node is replaced by null. 1405 */ 1406 protected function initializeRemovalMap(): void { 1407 if (isset($this->removalMap)) { 1408 return; 1409 } 1410 1411 $stripBoth = ['left' => \T_WHITESPACE, 'right' => \T_WHITESPACE]; 1412 $stripLeft = ['left' => \T_WHITESPACE]; 1413 $stripRight = ['right' => \T_WHITESPACE]; 1414 $stripDoubleArrow = ['right' => \T_DOUBLE_ARROW]; 1415 $stripColon = ['left' => ':']; 1416 $stripEquals = ['left' => '=']; 1417 $this->removalMap = [ 1418 'Expr_ArrayDimFetch->dim' => $stripBoth, 1419 'ArrayItem->key' => $stripDoubleArrow, 1420 'Expr_ArrowFunction->returnType' => $stripColon, 1421 'Expr_Closure->returnType' => $stripColon, 1422 'Expr_Exit->expr' => $stripBoth, 1423 'Expr_Ternary->if' => $stripBoth, 1424 'Expr_Yield->key' => $stripDoubleArrow, 1425 'Expr_Yield->value' => $stripBoth, 1426 'Param->type' => $stripRight, 1427 'Param->default' => $stripEquals, 1428 'Stmt_Break->num' => $stripBoth, 1429 'Stmt_Catch->var' => $stripLeft, 1430 'Stmt_ClassConst->type' => $stripRight, 1431 'Stmt_ClassMethod->returnType' => $stripColon, 1432 'Stmt_Class->extends' => ['left' => \T_EXTENDS], 1433 'Stmt_Enum->scalarType' => $stripColon, 1434 'Stmt_EnumCase->expr' => $stripEquals, 1435 'Expr_PrintableNewAnonClass->extends' => ['left' => \T_EXTENDS], 1436 'Stmt_Continue->num' => $stripBoth, 1437 'Stmt_Foreach->keyVar' => $stripDoubleArrow, 1438 'Stmt_Function->returnType' => $stripColon, 1439 'Stmt_If->else' => $stripLeft, 1440 'Stmt_Namespace->name' => $stripLeft, 1441 'Stmt_Property->type' => $stripRight, 1442 'PropertyItem->default' => $stripEquals, 1443 'Stmt_Return->expr' => $stripBoth, 1444 'Stmt_StaticVar->default' => $stripEquals, 1445 'Stmt_TraitUseAdaptation_Alias->newName' => $stripLeft, 1446 'Stmt_TryCatch->finally' => $stripLeft, 1447 // 'Stmt_Case->cond': Replace with "default" 1448 // 'Stmt_Class->name': Unclear what to do 1449 // 'Stmt_Declare->stmts': Not a plain node 1450 // 'Stmt_TraitUseAdaptation_Alias->newModifier': Not a plain node 1451 ]; 1452 } 1453 1454 protected function initializeInsertionMap(): void { 1455 if (isset($this->insertionMap)) { 1456 return; 1457 } 1458 1459 // TODO: "yield" where both key and value are inserted doesn't work 1460 // [$find, $beforeToken, $extraLeft, $extraRight] 1461 $this->insertionMap = [ 1462 'Expr_ArrayDimFetch->dim' => ['[', false, null, null], 1463 'ArrayItem->key' => [null, false, null, ' => '], 1464 'Expr_ArrowFunction->returnType' => [')', false, ': ', null], 1465 'Expr_Closure->returnType' => [')', false, ': ', null], 1466 'Expr_Ternary->if' => ['?', false, ' ', ' '], 1467 'Expr_Yield->key' => [\T_YIELD, false, null, ' => '], 1468 'Expr_Yield->value' => [\T_YIELD, false, ' ', null], 1469 'Param->type' => [null, false, null, ' '], 1470 'Param->default' => [null, false, ' = ', null], 1471 'Stmt_Break->num' => [\T_BREAK, false, ' ', null], 1472 'Stmt_Catch->var' => [null, false, ' ', null], 1473 'Stmt_ClassMethod->returnType' => [')', false, ': ', null], 1474 'Stmt_ClassConst->type' => [\T_CONST, false, ' ', null], 1475 'Stmt_Class->extends' => [null, false, ' extends ', null], 1476 'Stmt_Enum->scalarType' => [null, false, ' : ', null], 1477 'Stmt_EnumCase->expr' => [null, false, ' = ', null], 1478 'Expr_PrintableNewAnonClass->extends' => [null, false, ' extends ', null], 1479 'Stmt_Continue->num' => [\T_CONTINUE, false, ' ', null], 1480 'Stmt_Foreach->keyVar' => [\T_AS, false, null, ' => '], 1481 'Stmt_Function->returnType' => [')', false, ': ', null], 1482 'Stmt_If->else' => [null, false, ' ', null], 1483 'Stmt_Namespace->name' => [\T_NAMESPACE, false, ' ', null], 1484 'Stmt_Property->type' => [\T_VARIABLE, true, null, ' '], 1485 'PropertyItem->default' => [null, false, ' = ', null], 1486 'Stmt_Return->expr' => [\T_RETURN, false, ' ', null], 1487 'Stmt_StaticVar->default' => [null, false, ' = ', null], 1488 //'Stmt_TraitUseAdaptation_Alias->newName' => [T_AS, false, ' ', null], // TODO 1489 'Stmt_TryCatch->finally' => [null, false, ' ', null], 1490 1491 // 'Expr_Exit->expr': Complicated due to optional () 1492 // 'Stmt_Case->cond': Conversion from default to case 1493 // 'Stmt_Class->name': Unclear 1494 // 'Stmt_Declare->stmts': Not a proper node 1495 // 'Stmt_TraitUseAdaptation_Alias->newModifier': Not a proper node 1496 ]; 1497 } 1498 1499 protected function initializeListInsertionMap(): void { 1500 if (isset($this->listInsertionMap)) { 1501 return; 1502 } 1503 1504 $this->listInsertionMap = [ 1505 // special 1506 //'Expr_ShellExec->parts' => '', // TODO These need to be treated more carefully 1507 //'Scalar_InterpolatedString->parts' => '', 1508 Stmt\Catch_::class . '->types' => '|', 1509 UnionType::class . '->types' => '|', 1510 IntersectionType::class . '->types' => '&', 1511 Stmt\If_::class . '->elseifs' => ' ', 1512 Stmt\TryCatch::class . '->catches' => ' ', 1513 1514 // comma-separated lists 1515 Expr\Array_::class . '->items' => ', ', 1516 Expr\ArrowFunction::class . '->params' => ', ', 1517 Expr\Closure::class . '->params' => ', ', 1518 Expr\Closure::class . '->uses' => ', ', 1519 Expr\FuncCall::class . '->args' => ', ', 1520 Expr\Isset_::class . '->vars' => ', ', 1521 Expr\List_::class . '->items' => ', ', 1522 Expr\MethodCall::class . '->args' => ', ', 1523 Expr\NullsafeMethodCall::class . '->args' => ', ', 1524 Expr\New_::class . '->args' => ', ', 1525 PrintableNewAnonClassNode::class . '->args' => ', ', 1526 Expr\StaticCall::class . '->args' => ', ', 1527 Stmt\ClassConst::class . '->consts' => ', ', 1528 Stmt\ClassMethod::class . '->params' => ', ', 1529 Stmt\Class_::class . '->implements' => ', ', 1530 Stmt\Enum_::class . '->implements' => ', ', 1531 PrintableNewAnonClassNode::class . '->implements' => ', ', 1532 Stmt\Const_::class . '->consts' => ', ', 1533 Stmt\Declare_::class . '->declares' => ', ', 1534 Stmt\Echo_::class . '->exprs' => ', ', 1535 Stmt\For_::class . '->init' => ', ', 1536 Stmt\For_::class . '->cond' => ', ', 1537 Stmt\For_::class . '->loop' => ', ', 1538 Stmt\Function_::class . '->params' => ', ', 1539 Stmt\Global_::class . '->vars' => ', ', 1540 Stmt\GroupUse::class . '->uses' => ', ', 1541 Stmt\Interface_::class . '->extends' => ', ', 1542 Expr\Match_::class . '->arms' => ', ', 1543 Stmt\Property::class . '->props' => ', ', 1544 Stmt\StaticVar::class . '->vars' => ', ', 1545 Stmt\TraitUse::class . '->traits' => ', ', 1546 Stmt\TraitUseAdaptation\Precedence::class . '->insteadof' => ', ', 1547 Stmt\Unset_::class . '->vars' => ', ', 1548 Stmt\UseUse::class . '->uses' => ', ', 1549 MatchArm::class . '->conds' => ', ', 1550 AttributeGroup::class . '->attrs' => ', ', 1551 PropertyHook::class . '->params' => ', ', 1552 1553 // statement lists 1554 Expr\Closure::class . '->stmts' => "\n", 1555 Stmt\Case_::class . '->stmts' => "\n", 1556 Stmt\Catch_::class . '->stmts' => "\n", 1557 Stmt\Class_::class . '->stmts' => "\n", 1558 Stmt\Enum_::class . '->stmts' => "\n", 1559 PrintableNewAnonClassNode::class . '->stmts' => "\n", 1560 Stmt\Interface_::class . '->stmts' => "\n", 1561 Stmt\Trait_::class . '->stmts' => "\n", 1562 Stmt\ClassMethod::class . '->stmts' => "\n", 1563 Stmt\Declare_::class . '->stmts' => "\n", 1564 Stmt\Do_::class . '->stmts' => "\n", 1565 Stmt\ElseIf_::class . '->stmts' => "\n", 1566 Stmt\Else_::class . '->stmts' => "\n", 1567 Stmt\Finally_::class . '->stmts' => "\n", 1568 Stmt\Foreach_::class . '->stmts' => "\n", 1569 Stmt\For_::class . '->stmts' => "\n", 1570 Stmt\Function_::class . '->stmts' => "\n", 1571 Stmt\If_::class . '->stmts' => "\n", 1572 Stmt\Namespace_::class . '->stmts' => "\n", 1573 Stmt\Block::class . '->stmts' => "\n", 1574 1575 // Attribute groups 1576 Stmt\Class_::class . '->attrGroups' => "\n", 1577 Stmt\Enum_::class . '->attrGroups' => "\n", 1578 Stmt\EnumCase::class . '->attrGroups' => "\n", 1579 Stmt\Interface_::class . '->attrGroups' => "\n", 1580 Stmt\Trait_::class . '->attrGroups' => "\n", 1581 Stmt\Function_::class . '->attrGroups' => "\n", 1582 Stmt\ClassMethod::class . '->attrGroups' => "\n", 1583 Stmt\ClassConst::class . '->attrGroups' => "\n", 1584 Stmt\Property::class . '->attrGroups' => "\n", 1585 PrintableNewAnonClassNode::class . '->attrGroups' => ' ', 1586 Expr\Closure::class . '->attrGroups' => ' ', 1587 Expr\ArrowFunction::class . '->attrGroups' => ' ', 1588 Param::class . '->attrGroups' => ' ', 1589 PropertyHook::class . '->attrGroups' => ' ', 1590 1591 Stmt\Switch_::class . '->cases' => "\n", 1592 Stmt\TraitUse::class . '->adaptations' => "\n", 1593 Stmt\TryCatch::class . '->stmts' => "\n", 1594 Stmt\While_::class . '->stmts' => "\n", 1595 PropertyHook::class . '->body' => "\n", 1596 Stmt\Property::class . '->hooks' => "\n", 1597 Param::class . '->hooks' => "\n", 1598 1599 // dummy for top-level context 1600 'File->stmts' => "\n", 1601 ]; 1602 } 1603 1604 protected function initializeEmptyListInsertionMap(): void { 1605 if (isset($this->emptyListInsertionMap)) { 1606 return; 1607 } 1608 1609 // TODO Insertion into empty statement lists. 1610 1611 // [$find, $extraLeft, $extraRight] 1612 $this->emptyListInsertionMap = [ 1613 Expr\ArrowFunction::class . '->params' => ['(', '', ''], 1614 Expr\Closure::class . '->uses' => [')', ' use (', ')'], 1615 Expr\Closure::class . '->params' => ['(', '', ''], 1616 Expr\FuncCall::class . '->args' => ['(', '', ''], 1617 Expr\MethodCall::class . '->args' => ['(', '', ''], 1618 Expr\NullsafeMethodCall::class . '->args' => ['(', '', ''], 1619 Expr\New_::class . '->args' => ['(', '', ''], 1620 PrintableNewAnonClassNode::class . '->args' => ['(', '', ''], 1621 PrintableNewAnonClassNode::class . '->implements' => [null, ' implements ', ''], 1622 Expr\StaticCall::class . '->args' => ['(', '', ''], 1623 Stmt\Class_::class . '->implements' => [null, ' implements ', ''], 1624 Stmt\Enum_::class . '->implements' => [null, ' implements ', ''], 1625 Stmt\ClassMethod::class . '->params' => ['(', '', ''], 1626 Stmt\Interface_::class . '->extends' => [null, ' extends ', ''], 1627 Stmt\Function_::class . '->params' => ['(', '', ''], 1628 Stmt\Interface_::class . '->attrGroups' => [null, '', "\n"], 1629 Stmt\Class_::class . '->attrGroups' => [null, '', "\n"], 1630 Stmt\ClassConst::class . '->attrGroups' => [null, '', "\n"], 1631 Stmt\ClassMethod::class . '->attrGroups' => [null, '', "\n"], 1632 Stmt\Function_::class . '->attrGroups' => [null, '', "\n"], 1633 Stmt\Property::class . '->attrGroups' => [null, '', "\n"], 1634 Stmt\Trait_::class . '->attrGroups' => [null, '', "\n"], 1635 Expr\ArrowFunction::class . '->attrGroups' => [null, '', ' '], 1636 Expr\Closure::class . '->attrGroups' => [null, '', ' '], 1637 PrintableNewAnonClassNode::class . '->attrGroups' => [\T_NEW, ' ', ''], 1638 1639 /* These cannot be empty to start with: 1640 * Expr_Isset->vars 1641 * Stmt_Catch->types 1642 * Stmt_Const->consts 1643 * Stmt_ClassConst->consts 1644 * Stmt_Declare->declares 1645 * Stmt_Echo->exprs 1646 * Stmt_Global->vars 1647 * Stmt_GroupUse->uses 1648 * Stmt_Property->props 1649 * Stmt_StaticVar->vars 1650 * Stmt_TraitUse->traits 1651 * Stmt_TraitUseAdaptation_Precedence->insteadof 1652 * Stmt_Unset->vars 1653 * Stmt_Use->uses 1654 * UnionType->types 1655 */ 1656 1657 /* TODO 1658 * Stmt_If->elseifs 1659 * Stmt_TryCatch->catches 1660 * Expr_Array->items 1661 * Expr_List->items 1662 * Stmt_For->init 1663 * Stmt_For->cond 1664 * Stmt_For->loop 1665 */ 1666 ]; 1667 } 1668 1669 protected function initializeModifierChangeMap(): void { 1670 if (isset($this->modifierChangeMap)) { 1671 return; 1672 } 1673 1674 $this->modifierChangeMap = [ 1675 Stmt\ClassConst::class . '->flags' => ['pModifiers', \T_CONST], 1676 Stmt\ClassMethod::class . '->flags' => ['pModifiers', \T_FUNCTION], 1677 Stmt\Class_::class . '->flags' => ['pModifiers', \T_CLASS], 1678 Stmt\Property::class . '->flags' => ['pModifiers', \T_VARIABLE], 1679 PrintableNewAnonClassNode::class . '->flags' => ['pModifiers', \T_CLASS], 1680 Param::class . '->flags' => ['pModifiers', \T_VARIABLE], 1681 PropertyHook::class . '->flags' => ['pModifiers', \T_STRING], 1682 Expr\Closure::class . '->static' => ['pStatic', \T_FUNCTION], 1683 Expr\ArrowFunction::class . '->static' => ['pStatic', \T_FN], 1684 //Stmt\TraitUseAdaptation\Alias::class . '->newModifier' => 0, // TODO 1685 ]; 1686 1687 // List of integer subnodes that are not modifiers: 1688 // Expr_Include->type 1689 // Stmt_GroupUse->type 1690 // Stmt_Use->type 1691 // UseItem->type 1692 } 1693} 1694