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