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