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: // For anon classes skip to the class keyword.
1010: $isAnonClassArgs = $mapKey === PrintableNewAnonClassNode::class . '->args';
1011: if ($isAnonClassArgs) {
1012: $insertPos = $this->origTokens->findRight($pos, \T_CLASS) + 1;
1013: $result .= $this->origTokens->getTokenCode($pos, $insertPos, $indentAdjustment);
1014: $pos = $insertPos;
1015: }
1016:
1017: // If "new Foo" was used without arguments, we need to convert to "new Foo()".
1018: if (($mapKey === Expr\New_::class . '->args' || $isAnonClassArgs) &&
1019: !$this->origTokens->haveTokenImmediatelyAfter($pos - 1, '(')
1020: ) {
1021: $extraLeft = '(';
1022: $extraRight = ')';
1023: } else {
1024: $insertPos = $this->origTokens->findRight($pos, $findToken) + 1;
1025: $result .= $this->origTokens->getTokenCode($pos, $insertPos, $indentAdjustment);
1026: $pos = $insertPos;
1027: }
1028: }
1029:
1030: $first = true;
1031: $result .= $extraLeft;
1032: foreach ($delayedAdd as $delayedAddNode) {
1033: if (!$first) {
1034: $result .= $insertStr;
1035: if ($insertNewline) {
1036: $result .= $this->nl;
1037: }
1038: }
1039: $result .= $this->p($delayedAddNode, self::MAX_PRECEDENCE, self::MAX_PRECEDENCE, true);
1040: $first = false;
1041: }
1042: $result .= $extraRight === "\n" ? $this->nl : $extraRight;
1043: }
1044:
1045: return $result;
1046: }
1047:
1048: /**
1049: * Print node with fixups.
1050: *
1051: * Fixups here refer to the addition of extra parentheses, braces or other characters, that
1052: * are required to preserve program semantics in a certain context (e.g. to maintain precedence
1053: * or because only certain expressions are allowed in certain places).
1054: *
1055: * @param int $fixup Fixup type
1056: * @param Node $subNode Subnode to print
1057: * @param string|null $parentClass Class of parent node
1058: * @param int $subStartPos Original start pos of subnode
1059: * @param int $subEndPos Original end pos of subnode
1060: *
1061: * @return string Result of fixed-up print of subnode
1062: */
1063: protected function pFixup(int $fixup, Node $subNode, ?string $parentClass, int $subStartPos, int $subEndPos): string {
1064: switch ($fixup) {
1065: case self::FIXUP_PREC_LEFT:
1066: // We use a conservative approximation where lhsPrecedence == precedence.
1067: if (!$this->origTokens->haveParens($subStartPos, $subEndPos)) {
1068: $precedence = $this->precedenceMap[$parentClass][1];
1069: return $this->p($subNode, $precedence, $precedence);
1070: }
1071: break;
1072: case self::FIXUP_PREC_RIGHT:
1073: if (!$this->origTokens->haveParens($subStartPos, $subEndPos)) {
1074: $precedence = $this->precedenceMap[$parentClass][2];
1075: return $this->p($subNode, $precedence, $precedence);
1076: }
1077: break;
1078: case self::FIXUP_PREC_UNARY:
1079: if (!$this->origTokens->haveParens($subStartPos, $subEndPos)) {
1080: $precedence = $this->precedenceMap[$parentClass][0];
1081: return $this->p($subNode, $precedence, $precedence);
1082: }
1083: break;
1084: case self::FIXUP_CALL_LHS:
1085: if ($this->callLhsRequiresParens($subNode)
1086: && !$this->origTokens->haveParens($subStartPos, $subEndPos)
1087: ) {
1088: return '(' . $this->p($subNode) . ')';
1089: }
1090: break;
1091: case self::FIXUP_DEREF_LHS:
1092: if ($this->dereferenceLhsRequiresParens($subNode)
1093: && !$this->origTokens->haveParens($subStartPos, $subEndPos)
1094: ) {
1095: return '(' . $this->p($subNode) . ')';
1096: }
1097: break;
1098: case self::FIXUP_STATIC_DEREF_LHS:
1099: if ($this->staticDereferenceLhsRequiresParens($subNode)
1100: && !$this->origTokens->haveParens($subStartPos, $subEndPos)
1101: ) {
1102: return '(' . $this->p($subNode) . ')';
1103: }
1104: break;
1105: case self::FIXUP_NEW:
1106: if ($this->newOperandRequiresParens($subNode)
1107: && !$this->origTokens->haveParens($subStartPos, $subEndPos)) {
1108: return '(' . $this->p($subNode) . ')';
1109: }
1110: break;
1111: case self::FIXUP_BRACED_NAME:
1112: case self::FIXUP_VAR_BRACED_NAME:
1113: if ($subNode instanceof Expr
1114: && !$this->origTokens->haveBraces($subStartPos, $subEndPos)
1115: ) {
1116: return ($fixup === self::FIXUP_VAR_BRACED_NAME ? '$' : '')
1117: . '{' . $this->p($subNode) . '}';
1118: }
1119: break;
1120: case self::FIXUP_ENCAPSED:
1121: if (!$subNode instanceof Node\InterpolatedStringPart
1122: && !$this->origTokens->haveBraces($subStartPos, $subEndPos)
1123: ) {
1124: return '{' . $this->p($subNode) . '}';
1125: }
1126: break;
1127: default:
1128: throw new \Exception('Cannot happen');
1129: }
1130:
1131: // Nothing special to do
1132: return $this->p($subNode);
1133: }
1134:
1135: /**
1136: * Appends to a string, ensuring whitespace between label characters.
1137: *
1138: * Example: "echo" and "$x" result in "echo$x", but "echo" and "x" result in "echo x".
1139: * Without safeAppend the result would be "echox", which does not preserve semantics.
1140: */
1141: protected function safeAppend(string &$str, string $append): void {
1142: if ($str === "") {
1143: $str = $append;
1144: return;
1145: }
1146:
1147: if ($append === "") {
1148: return;
1149: }
1150:
1151: if (!$this->labelCharMap[$append[0]]
1152: || !$this->labelCharMap[$str[\strlen($str) - 1]]) {
1153: $str .= $append;
1154: } else {
1155: $str .= " " . $append;
1156: }
1157: }
1158:
1159: /**
1160: * Determines whether the LHS of a call must be wrapped in parenthesis.
1161: *
1162: * @param Node $node LHS of a call
1163: *
1164: * @return bool Whether parentheses are required
1165: */
1166: protected function callLhsRequiresParens(Node $node): bool {
1167: if ($node instanceof Expr\New_) {
1168: return !$this->phpVersion->supportsNewDereferenceWithoutParentheses();
1169: }
1170: return !($node instanceof Node\Name
1171: || $node instanceof Expr\Variable
1172: || $node instanceof Expr\ArrayDimFetch
1173: || $node instanceof Expr\FuncCall
1174: || $node instanceof Expr\MethodCall
1175: || $node instanceof Expr\NullsafeMethodCall
1176: || $node instanceof Expr\StaticCall
1177: || $node instanceof Expr\Array_);
1178: }
1179:
1180: /**
1181: * Determines whether the LHS of an array/object operation must be wrapped in parentheses.
1182: *
1183: * @param Node $node LHS of dereferencing operation
1184: *
1185: * @return bool Whether parentheses are required
1186: */
1187: protected function dereferenceLhsRequiresParens(Node $node): bool {
1188: // A constant can occur on the LHS of an array/object deref, but not a static deref.
1189: return $this->staticDereferenceLhsRequiresParens($node)
1190: && !$node instanceof Expr\ConstFetch;
1191: }
1192:
1193: /**
1194: * Determines whether the LHS of a static operation must be wrapped in parentheses.
1195: *
1196: * @param Node $node LHS of dereferencing operation
1197: *
1198: * @return bool Whether parentheses are required
1199: */
1200: protected function staticDereferenceLhsRequiresParens(Node $node): bool {
1201: if ($node instanceof Expr\New_) {
1202: return !$this->phpVersion->supportsNewDereferenceWithoutParentheses();
1203: }
1204: return !($node instanceof Expr\Variable
1205: || $node instanceof Node\Name
1206: || $node instanceof Expr\ArrayDimFetch
1207: || $node instanceof Expr\PropertyFetch
1208: || $node instanceof Expr\NullsafePropertyFetch
1209: || $node instanceof Expr\StaticPropertyFetch
1210: || $node instanceof Expr\FuncCall
1211: || $node instanceof Expr\MethodCall
1212: || $node instanceof Expr\NullsafeMethodCall
1213: || $node instanceof Expr\StaticCall
1214: || $node instanceof Expr\Array_
1215: || $node instanceof Scalar\String_
1216: || $node instanceof Expr\ClassConstFetch);
1217: }
1218:
1219: /**
1220: * Determines whether an expression used in "new" or "instanceof" requires parentheses.
1221: *
1222: * @param Node $node New or instanceof operand
1223: *
1224: * @return bool Whether parentheses are required
1225: */
1226: protected function newOperandRequiresParens(Node $node): bool {
1227: if ($node instanceof Node\Name || $node instanceof Expr\Variable) {
1228: return false;
1229: }
1230: if ($node instanceof Expr\ArrayDimFetch || $node instanceof Expr\PropertyFetch ||
1231: $node instanceof Expr\NullsafePropertyFetch
1232: ) {
1233: return $this->newOperandRequiresParens($node->var);
1234: }
1235: if ($node instanceof Expr\StaticPropertyFetch) {
1236: return $this->newOperandRequiresParens($node->class);
1237: }
1238: return true;
1239: }
1240:
1241: /**
1242: * Print modifiers, including trailing whitespace.
1243: *
1244: * @param int $modifiers Modifier mask to print
1245: *
1246: * @return string Printed modifiers
1247: */
1248: protected function pModifiers(int $modifiers): string {
1249: return ($modifiers & Modifiers::FINAL ? 'final ' : '')
1250: . ($modifiers & Modifiers::ABSTRACT ? 'abstract ' : '')
1251: . ($modifiers & Modifiers::PUBLIC ? 'public ' : '')
1252: . ($modifiers & Modifiers::PROTECTED ? 'protected ' : '')
1253: . ($modifiers & Modifiers::PRIVATE ? 'private ' : '')
1254: . ($modifiers & Modifiers::PUBLIC_SET ? 'public(set) ' : '')
1255: . ($modifiers & Modifiers::PROTECTED_SET ? 'protected(set) ' : '')
1256: . ($modifiers & Modifiers::PRIVATE_SET ? 'private(set) ' : '')
1257: . ($modifiers & Modifiers::STATIC ? 'static ' : '')
1258: . ($modifiers & Modifiers::READONLY ? 'readonly ' : '');
1259: }
1260:
1261: protected function pStatic(bool $static): string {
1262: return $static ? 'static ' : '';
1263: }
1264:
1265: /**
1266: * Determine whether a list of nodes uses multiline formatting.
1267: *
1268: * @param (Node|null)[] $nodes Node list
1269: *
1270: * @return bool Whether multiline formatting is used
1271: */
1272: protected function isMultiline(array $nodes): bool {
1273: if (\count($nodes) < 2) {
1274: return false;
1275: }
1276:
1277: $pos = -1;
1278: foreach ($nodes as $node) {
1279: if (null === $node) {
1280: continue;
1281: }
1282:
1283: $endPos = $node->getEndTokenPos() + 1;
1284: if ($pos >= 0) {
1285: $text = $this->origTokens->getTokenCode($pos, $endPos, 0);
1286: if (false === strpos($text, "\n")) {
1287: // We require that a newline is present between *every* item. If the formatting
1288: // is inconsistent, with only some items having newlines, we don't consider it
1289: // as multiline
1290: return false;
1291: }
1292: }
1293: $pos = $endPos;
1294: }
1295:
1296: return true;
1297: }
1298:
1299: /**
1300: * Lazily initializes label char map.
1301: *
1302: * The label char map determines whether a certain character may occur in a label.
1303: */
1304: protected function initializeLabelCharMap(): void {
1305: if (isset($this->labelCharMap)) {
1306: return;
1307: }
1308:
1309: $this->labelCharMap = [];
1310: for ($i = 0; $i < 256; $i++) {
1311: $chr = chr($i);
1312: $this->labelCharMap[$chr] = (bool) preg_match('/^[a-zA-Z0-9_\x80-\xff]$/', $chr);
1313: }
1314:
1315: if ($this->phpVersion->allowsDelInIdentifiers()) {
1316: $this->labelCharMap["\x7f"] = true;
1317: }
1318: }
1319:
1320: /**
1321: * Lazily initializes node list differ.
1322: *
1323: * The node list differ is used to determine differences between two array subnodes.
1324: */
1325: protected function initializeNodeListDiffer(): void {
1326: if (isset($this->nodeListDiffer)) {
1327: return;
1328: }
1329:
1330: $this->nodeListDiffer = new Internal\Differ(function ($a, $b) {
1331: if ($a instanceof Node && $b instanceof Node) {
1332: return $a === $b->getAttribute('origNode');
1333: }
1334: // Can happen for array destructuring
1335: return $a === null && $b === null;
1336: });
1337: }
1338:
1339: /**
1340: * Lazily initializes fixup map.
1341: *
1342: * The fixup map is used to determine whether a certain subnode of a certain node may require
1343: * some kind of "fixup" operation, e.g. the addition of parenthesis or braces.
1344: */
1345: protected function initializeFixupMap(): void {
1346: if (isset($this->fixupMap)) {
1347: return;
1348: }
1349:
1350: $this->fixupMap = [
1351: Expr\Instanceof_::class => [
1352: 'expr' => self::FIXUP_PREC_UNARY,
1353: 'class' => self::FIXUP_NEW,
1354: ],
1355: Expr\Ternary::class => [
1356: 'cond' => self::FIXUP_PREC_LEFT,
1357: 'else' => self::FIXUP_PREC_RIGHT,
1358: ],
1359: Expr\Yield_::class => ['value' => self::FIXUP_PREC_UNARY],
1360:
1361: Expr\FuncCall::class => ['name' => self::FIXUP_CALL_LHS],
1362: Expr\StaticCall::class => ['class' => self::FIXUP_STATIC_DEREF_LHS],
1363: Expr\ArrayDimFetch::class => ['var' => self::FIXUP_DEREF_LHS],
1364: Expr\ClassConstFetch::class => [
1365: 'class' => self::FIXUP_STATIC_DEREF_LHS,
1366: 'name' => self::FIXUP_BRACED_NAME,
1367: ],
1368: Expr\New_::class => ['class' => self::FIXUP_NEW],
1369: Expr\MethodCall::class => [
1370: 'var' => self::FIXUP_DEREF_LHS,
1371: 'name' => self::FIXUP_BRACED_NAME,
1372: ],
1373: Expr\NullsafeMethodCall::class => [
1374: 'var' => self::FIXUP_DEREF_LHS,
1375: 'name' => self::FIXUP_BRACED_NAME,
1376: ],
1377: Expr\StaticPropertyFetch::class => [
1378: 'class' => self::FIXUP_STATIC_DEREF_LHS,
1379: 'name' => self::FIXUP_VAR_BRACED_NAME,
1380: ],
1381: Expr\PropertyFetch::class => [
1382: 'var' => self::FIXUP_DEREF_LHS,
1383: 'name' => self::FIXUP_BRACED_NAME,
1384: ],
1385: Expr\NullsafePropertyFetch::class => [
1386: 'var' => self::FIXUP_DEREF_LHS,
1387: 'name' => self::FIXUP_BRACED_NAME,
1388: ],
1389: Scalar\InterpolatedString::class => [
1390: 'parts' => self::FIXUP_ENCAPSED,
1391: ],
1392: ];
1393:
1394: $binaryOps = [
1395: BinaryOp\Pow::class, BinaryOp\Mul::class, BinaryOp\Div::class, BinaryOp\Mod::class,
1396: BinaryOp\Plus::class, BinaryOp\Minus::class, BinaryOp\Concat::class,
1397: BinaryOp\ShiftLeft::class, BinaryOp\ShiftRight::class, BinaryOp\Smaller::class,
1398: BinaryOp\SmallerOrEqual::class, BinaryOp\Greater::class, BinaryOp\GreaterOrEqual::class,
1399: BinaryOp\Equal::class, BinaryOp\NotEqual::class, BinaryOp\Identical::class,
1400: BinaryOp\NotIdentical::class, BinaryOp\Spaceship::class, BinaryOp\BitwiseAnd::class,
1401: BinaryOp\BitwiseXor::class, BinaryOp\BitwiseOr::class, BinaryOp\BooleanAnd::class,
1402: BinaryOp\BooleanOr::class, BinaryOp\Coalesce::class, BinaryOp\LogicalAnd::class,
1403: BinaryOp\LogicalXor::class, BinaryOp\LogicalOr::class, BinaryOp\Pipe::class,
1404: ];
1405: foreach ($binaryOps as $binaryOp) {
1406: $this->fixupMap[$binaryOp] = [
1407: 'left' => self::FIXUP_PREC_LEFT,
1408: 'right' => self::FIXUP_PREC_RIGHT
1409: ];
1410: }
1411:
1412: $prefixOps = [
1413: Expr\Clone_::class, Expr\BitwiseNot::class, Expr\BooleanNot::class, Expr\UnaryPlus::class, Expr\UnaryMinus::class,
1414: Cast\Int_::class, Cast\Double::class, Cast\String_::class, Cast\Array_::class,
1415: Cast\Object_::class, Cast\Bool_::class, Cast\Unset_::class, Expr\ErrorSuppress::class,
1416: Expr\YieldFrom::class, Expr\Print_::class, Expr\Include_::class,
1417: Expr\Assign::class, Expr\AssignRef::class, AssignOp\Plus::class, AssignOp\Minus::class,
1418: AssignOp\Mul::class, AssignOp\Div::class, AssignOp\Concat::class, AssignOp\Mod::class,
1419: AssignOp\BitwiseAnd::class, AssignOp\BitwiseOr::class, AssignOp\BitwiseXor::class,
1420: AssignOp\ShiftLeft::class, AssignOp\ShiftRight::class, AssignOp\Pow::class, AssignOp\Coalesce::class,
1421: Expr\ArrowFunction::class, Expr\Throw_::class,
1422: ];
1423: foreach ($prefixOps as $prefixOp) {
1424: $this->fixupMap[$prefixOp] = ['expr' => self::FIXUP_PREC_UNARY];
1425: }
1426: }
1427:
1428: /**
1429: * Lazily initializes the removal map.
1430: *
1431: * The removal map is used to determine which additional tokens should be removed when a
1432: * certain node is replaced by null.
1433: */
1434: protected function initializeRemovalMap(): void {
1435: if (isset($this->removalMap)) {
1436: return;
1437: }
1438:
1439: $stripBoth = ['left' => \T_WHITESPACE, 'right' => \T_WHITESPACE];
1440: $stripLeft = ['left' => \T_WHITESPACE];
1441: $stripRight = ['right' => \T_WHITESPACE];
1442: $stripDoubleArrow = ['right' => \T_DOUBLE_ARROW];
1443: $stripColon = ['left' => ':'];
1444: $stripEquals = ['left' => '='];
1445: $this->removalMap = [
1446: 'Expr_ArrayDimFetch->dim' => $stripBoth,
1447: 'ArrayItem->key' => $stripDoubleArrow,
1448: 'Expr_ArrowFunction->returnType' => $stripColon,
1449: 'Expr_Closure->returnType' => $stripColon,
1450: 'Expr_Exit->expr' => $stripBoth,
1451: 'Expr_Ternary->if' => $stripBoth,
1452: 'Expr_Yield->key' => $stripDoubleArrow,
1453: 'Expr_Yield->value' => $stripBoth,
1454: 'Param->type' => $stripRight,
1455: 'Param->default' => $stripEquals,
1456: 'Stmt_Break->num' => $stripBoth,
1457: 'Stmt_Catch->var' => $stripLeft,
1458: 'Stmt_ClassConst->type' => $stripRight,
1459: 'Stmt_ClassMethod->returnType' => $stripColon,
1460: 'Stmt_Class->extends' => ['left' => \T_EXTENDS],
1461: 'Stmt_Enum->scalarType' => $stripColon,
1462: 'Stmt_EnumCase->expr' => $stripEquals,
1463: 'Expr_PrintableNewAnonClass->extends' => ['left' => \T_EXTENDS],
1464: 'Stmt_Continue->num' => $stripBoth,
1465: 'Stmt_Foreach->keyVar' => $stripDoubleArrow,
1466: 'Stmt_Function->returnType' => $stripColon,
1467: 'Stmt_If->else' => $stripLeft,
1468: 'Stmt_Namespace->name' => $stripLeft,
1469: 'Stmt_Property->type' => $stripRight,
1470: 'PropertyItem->default' => $stripEquals,
1471: 'Stmt_Return->expr' => $stripBoth,
1472: 'Stmt_StaticVar->default' => $stripEquals,
1473: 'Stmt_TraitUseAdaptation_Alias->newName' => $stripLeft,
1474: 'Stmt_TryCatch->finally' => $stripLeft,
1475: // 'Stmt_Case->cond': Replace with "default"
1476: // 'Stmt_Class->name': Unclear what to do
1477: // 'Stmt_Declare->stmts': Not a plain node
1478: // 'Stmt_TraitUseAdaptation_Alias->newModifier': Not a plain node
1479: ];
1480: }
1481:
1482: protected function initializeInsertionMap(): void {
1483: if (isset($this->insertionMap)) {
1484: return;
1485: }
1486:
1487: // TODO: "yield" where both key and value are inserted doesn't work
1488: // [$find, $beforeToken, $extraLeft, $extraRight]
1489: $this->insertionMap = [
1490: 'Expr_ArrayDimFetch->dim' => ['[', false, null, null],
1491: 'ArrayItem->key' => [null, false, null, ' => '],
1492: 'Expr_ArrowFunction->returnType' => [')', false, ': ', null],
1493: 'Expr_Closure->returnType' => [')', false, ': ', null],
1494: 'Expr_Ternary->if' => ['?', false, ' ', ' '],
1495: 'Expr_Yield->key' => [\T_YIELD, false, null, ' => '],
1496: 'Expr_Yield->value' => [\T_YIELD, false, ' ', null],
1497: 'Param->type' => [null, false, null, ' '],
1498: 'Param->default' => [null, false, ' = ', null],
1499: 'Stmt_Break->num' => [\T_BREAK, false, ' ', null],
1500: 'Stmt_Catch->var' => [null, false, ' ', null],
1501: 'Stmt_ClassMethod->returnType' => [')', false, ': ', null],
1502: 'Stmt_ClassConst->type' => [\T_CONST, false, ' ', null],
1503: 'Stmt_Class->extends' => [null, false, ' extends ', null],
1504: 'Stmt_Enum->scalarType' => [null, false, ' : ', null],
1505: 'Stmt_EnumCase->expr' => [null, false, ' = ', null],
1506: 'Expr_PrintableNewAnonClass->extends' => [null, false, ' extends ', null],
1507: 'Stmt_Continue->num' => [\T_CONTINUE, false, ' ', null],
1508: 'Stmt_Foreach->keyVar' => [\T_AS, false, null, ' => '],
1509: 'Stmt_Function->returnType' => [')', false, ': ', null],
1510: 'Stmt_If->else' => [null, false, ' ', null],
1511: 'Stmt_Namespace->name' => [\T_NAMESPACE, false, ' ', null],
1512: 'Stmt_Property->type' => [\T_VARIABLE, true, null, ' '],
1513: 'PropertyItem->default' => [null, false, ' = ', null],
1514: 'Stmt_Return->expr' => [\T_RETURN, false, ' ', null],
1515: 'Stmt_StaticVar->default' => [null, false, ' = ', null],
1516: //'Stmt_TraitUseAdaptation_Alias->newName' => [T_AS, false, ' ', null], // TODO
1517: 'Stmt_TryCatch->finally' => [null, false, ' ', null],
1518:
1519: // 'Expr_Exit->expr': Complicated due to optional ()
1520: // 'Stmt_Case->cond': Conversion from default to case
1521: // 'Stmt_Class->name': Unclear
1522: // 'Stmt_Declare->stmts': Not a proper node
1523: // 'Stmt_TraitUseAdaptation_Alias->newModifier': Not a proper node
1524: ];
1525: }
1526:
1527: protected function initializeListInsertionMap(): void {
1528: if (isset($this->listInsertionMap)) {
1529: return;
1530: }
1531:
1532: $this->listInsertionMap = [
1533: // special
1534: //'Expr_ShellExec->parts' => '', // TODO These need to be treated more carefully
1535: //'Scalar_InterpolatedString->parts' => '',
1536: Stmt\Catch_::class . '->types' => '|',
1537: UnionType::class . '->types' => '|',
1538: IntersectionType::class . '->types' => '&',
1539: Stmt\If_::class . '->elseifs' => ' ',
1540: Stmt\TryCatch::class . '->catches' => ' ',
1541:
1542: // comma-separated lists
1543: Expr\Array_::class . '->items' => ', ',
1544: Expr\ArrowFunction::class . '->params' => ', ',
1545: Expr\Closure::class . '->params' => ', ',
1546: Expr\Closure::class . '->uses' => ', ',
1547: Expr\FuncCall::class . '->args' => ', ',
1548: Expr\Isset_::class . '->vars' => ', ',
1549: Expr\List_::class . '->items' => ', ',
1550: Expr\MethodCall::class . '->args' => ', ',
1551: Expr\NullsafeMethodCall::class . '->args' => ', ',
1552: Expr\New_::class . '->args' => ', ',
1553: PrintableNewAnonClassNode::class . '->args' => ', ',
1554: Expr\StaticCall::class . '->args' => ', ',
1555: Stmt\ClassConst::class . '->consts' => ', ',
1556: Stmt\ClassMethod::class . '->params' => ', ',
1557: Stmt\Class_::class . '->implements' => ', ',
1558: Stmt\Enum_::class . '->implements' => ', ',
1559: PrintableNewAnonClassNode::class . '->implements' => ', ',
1560: Stmt\Const_::class . '->consts' => ', ',
1561: Stmt\Declare_::class . '->declares' => ', ',
1562: Stmt\Echo_::class . '->exprs' => ', ',
1563: Stmt\For_::class . '->init' => ', ',
1564: Stmt\For_::class . '->cond' => ', ',
1565: Stmt\For_::class . '->loop' => ', ',
1566: Stmt\Function_::class . '->params' => ', ',
1567: Stmt\Global_::class . '->vars' => ', ',
1568: Stmt\GroupUse::class . '->uses' => ', ',
1569: Stmt\Interface_::class . '->extends' => ', ',
1570: Expr\Match_::class . '->arms' => ', ',
1571: Stmt\Property::class . '->props' => ', ',
1572: Stmt\StaticVar::class . '->vars' => ', ',
1573: Stmt\TraitUse::class . '->traits' => ', ',
1574: Stmt\TraitUseAdaptation\Precedence::class . '->insteadof' => ', ',
1575: Stmt\Unset_::class . '->vars' => ', ',
1576: Stmt\UseUse::class . '->uses' => ', ',
1577: MatchArm::class . '->conds' => ', ',
1578: AttributeGroup::class . '->attrs' => ', ',
1579: PropertyHook::class . '->params' => ', ',
1580:
1581: // statement lists
1582: Expr\Closure::class . '->stmts' => "\n",
1583: Stmt\Case_::class . '->stmts' => "\n",
1584: Stmt\Catch_::class . '->stmts' => "\n",
1585: Stmt\Class_::class . '->stmts' => "\n",
1586: Stmt\Enum_::class . '->stmts' => "\n",
1587: PrintableNewAnonClassNode::class . '->stmts' => "\n",
1588: Stmt\Interface_::class . '->stmts' => "\n",
1589: Stmt\Trait_::class . '->stmts' => "\n",
1590: Stmt\ClassMethod::class . '->stmts' => "\n",
1591: Stmt\Declare_::class . '->stmts' => "\n",
1592: Stmt\Do_::class . '->stmts' => "\n",
1593: Stmt\ElseIf_::class . '->stmts' => "\n",
1594: Stmt\Else_::class . '->stmts' => "\n",
1595: Stmt\Finally_::class . '->stmts' => "\n",
1596: Stmt\Foreach_::class . '->stmts' => "\n",
1597: Stmt\For_::class . '->stmts' => "\n",
1598: Stmt\Function_::class . '->stmts' => "\n",
1599: Stmt\If_::class . '->stmts' => "\n",
1600: Stmt\Namespace_::class . '->stmts' => "\n",
1601: Stmt\Block::class . '->stmts' => "\n",
1602:
1603: // Attribute groups
1604: Stmt\Class_::class . '->attrGroups' => "\n",
1605: Stmt\Enum_::class . '->attrGroups' => "\n",
1606: Stmt\EnumCase::class . '->attrGroups' => "\n",
1607: Stmt\Interface_::class . '->attrGroups' => "\n",
1608: Stmt\Trait_::class . '->attrGroups' => "\n",
1609: Stmt\Function_::class . '->attrGroups' => "\n",
1610: Stmt\ClassMethod::class . '->attrGroups' => "\n",
1611: Stmt\ClassConst::class . '->attrGroups' => "\n",
1612: Stmt\Property::class . '->attrGroups' => "\n",
1613: PrintableNewAnonClassNode::class . '->attrGroups' => ' ',
1614: Expr\Closure::class . '->attrGroups' => ' ',
1615: Expr\ArrowFunction::class . '->attrGroups' => ' ',
1616: Param::class . '->attrGroups' => ' ',
1617: PropertyHook::class . '->attrGroups' => ' ',
1618:
1619: Stmt\Switch_::class . '->cases' => "\n",
1620: Stmt\TraitUse::class . '->adaptations' => "\n",
1621: Stmt\TryCatch::class . '->stmts' => "\n",
1622: Stmt\While_::class . '->stmts' => "\n",
1623: PropertyHook::class . '->body' => "\n",
1624: Stmt\Property::class . '->hooks' => "\n",
1625: Param::class . '->hooks' => "\n",
1626:
1627: // dummy for top-level context
1628: 'File->stmts' => "\n",
1629: ];
1630: }
1631:
1632: protected function initializeEmptyListInsertionMap(): void {
1633: if (isset($this->emptyListInsertionMap)) {
1634: return;
1635: }
1636:
1637: // TODO Insertion into empty statement lists.
1638:
1639: // [$find, $extraLeft, $extraRight]
1640: $this->emptyListInsertionMap = [
1641: Expr\ArrowFunction::class . '->params' => ['(', '', ''],
1642: Expr\Closure::class . '->uses' => [')', ' use (', ')'],
1643: Expr\Closure::class . '->params' => ['(', '', ''],
1644: Expr\FuncCall::class . '->args' => ['(', '', ''],
1645: Expr\MethodCall::class . '->args' => ['(', '', ''],
1646: Expr\NullsafeMethodCall::class . '->args' => ['(', '', ''],
1647: Expr\New_::class . '->args' => ['(', '', ''],
1648: PrintableNewAnonClassNode::class . '->args' => ['(', '', ''],
1649: PrintableNewAnonClassNode::class . '->implements' => [null, ' implements ', ''],
1650: Expr\StaticCall::class . '->args' => ['(', '', ''],
1651: Stmt\Class_::class . '->implements' => [null, ' implements ', ''],
1652: Stmt\Enum_::class . '->implements' => [null, ' implements ', ''],
1653: Stmt\ClassMethod::class . '->params' => ['(', '', ''],
1654: Stmt\Interface_::class . '->extends' => [null, ' extends ', ''],
1655: Stmt\Function_::class . '->params' => ['(', '', ''],
1656: Stmt\Interface_::class . '->attrGroups' => [null, '', "\n"],
1657: Stmt\Class_::class . '->attrGroups' => [null, '', "\n"],
1658: Stmt\ClassConst::class . '->attrGroups' => [null, '', "\n"],
1659: Stmt\ClassMethod::class . '->attrGroups' => [null, '', "\n"],
1660: Stmt\Function_::class . '->attrGroups' => [null, '', "\n"],
1661: Stmt\Property::class . '->attrGroups' => [null, '', "\n"],
1662: Stmt\Trait_::class . '->attrGroups' => [null, '', "\n"],
1663: Expr\ArrowFunction::class . '->attrGroups' => [null, '', ' '],
1664: Expr\Closure::class . '->attrGroups' => [null, '', ' '],
1665: Stmt\Const_::class . '->attrGroups' => [null, '', "\n"],
1666: PrintableNewAnonClassNode::class . '->attrGroups' => [\T_NEW, ' ', ''],
1667:
1668: /* These cannot be empty to start with:
1669: * Expr_Isset->vars
1670: * Stmt_Catch->types
1671: * Stmt_Const->consts
1672: * Stmt_ClassConst->consts
1673: * Stmt_Declare->declares
1674: * Stmt_Echo->exprs
1675: * Stmt_Global->vars
1676: * Stmt_GroupUse->uses
1677: * Stmt_Property->props
1678: * Stmt_StaticVar->vars
1679: * Stmt_TraitUse->traits
1680: * Stmt_TraitUseAdaptation_Precedence->insteadof
1681: * Stmt_Unset->vars
1682: * Stmt_Use->uses
1683: * UnionType->types
1684: */
1685:
1686: /* TODO
1687: * Stmt_If->elseifs
1688: * Stmt_TryCatch->catches
1689: * Expr_Array->items
1690: * Expr_List->items
1691: * Stmt_For->init
1692: * Stmt_For->cond
1693: * Stmt_For->loop
1694: */
1695: ];
1696: }
1697:
1698: protected function initializeModifierChangeMap(): void {
1699: if (isset($this->modifierChangeMap)) {
1700: return;
1701: }
1702:
1703: $this->modifierChangeMap = [
1704: Stmt\ClassConst::class . '->flags' => ['pModifiers', \T_WHITESPACE, \T_CONST],
1705: Stmt\ClassMethod::class . '->flags' => ['pModifiers', \T_WHITESPACE, \T_FUNCTION],
1706: Stmt\Class_::class . '->flags' => ['pModifiers', \T_WHITESPACE, \T_CLASS],
1707: Stmt\Property::class . '->flags' => ['pModifiers', \T_WHITESPACE, \T_VARIABLE],
1708: PrintableNewAnonClassNode::class . '->flags' => ['pModifiers', \T_NEW, \T_CLASS],
1709: Param::class . '->flags' => ['pModifiers', \T_WHITESPACE, \T_VARIABLE],
1710: PropertyHook::class . '->flags' => ['pModifiers', \T_WHITESPACE, \T_STRING],
1711: Expr\Closure::class . '->static' => ['pStatic', \T_WHITESPACE, \T_FUNCTION],
1712: Expr\ArrowFunction::class . '->static' => ['pStatic', \T_WHITESPACE, \T_FN],
1713: //Stmt\TraitUseAdaptation\Alias::class . '->newModifier' => 0, // TODO
1714: ];
1715:
1716: // List of integer subnodes that are not modifiers:
1717: // Expr_Include->type
1718: // Stmt_GroupUse->type
1719: // Stmt_Use->type
1720: // UseItem->type
1721: }
1722: }
1723: