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