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