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: $result .= $this->$printFn($subNode);
682: $pos = $this->origTokens->findRight($pos, $findToken);
683: continue;
684: }
685:
686: $extraLeft = '';
687: $extraRight = '';
688: if ($origSubNode !== null) {
689: $subStartPos = $origSubNode->getStartTokenPos();
690: $subEndPos = $origSubNode->getEndTokenPos();
691: \assert($subStartPos >= 0 && $subEndPos >= 0);
692: } else {
693: if ($subNode === null) {
694: // Both null, nothing to do
695: continue;
696: }
697:
698: // A node has been inserted, check if we have insertion information for it
699: $key = $type . '->' . $subNodeName;
700: if (!isset($this->insertionMap[$key])) {
701: return $this->pFallback($fallbackNode, $precedence, $lhsPrecedence);
702: }
703:
704: list($findToken, $beforeToken, $extraLeft, $extraRight) = $this->insertionMap[$key];
705: if (null !== $findToken) {
706: $subStartPos = $this->origTokens->findRight($pos, $findToken)
707: + (int) !$beforeToken;
708: } else {
709: $subStartPos = $pos;
710: }
711:
712: if (null === $extraLeft && null !== $extraRight) {
713: // If inserting on the right only, skipping whitespace looks better
714: $subStartPos = $this->origTokens->skipRightWhitespace($subStartPos);
715: }
716: $subEndPos = $subStartPos - 1;
717: }
718:
719: if (null === $subNode) {
720: // A node has been removed, check if we have removal information for it
721: $key = $type . '->' . $subNodeName;
722: if (!isset($this->removalMap[$key])) {
723: return $this->pFallback($fallbackNode, $precedence, $lhsPrecedence);
724: }
725:
726: // Adjust positions to account for additional tokens that must be skipped
727: $removalInfo = $this->removalMap[$key];
728: if (isset($removalInfo['left'])) {
729: $subStartPos = $this->origTokens->skipLeft($subStartPos - 1, $removalInfo['left']) + 1;
730: }
731: if (isset($removalInfo['right'])) {
732: $subEndPos = $this->origTokens->skipRight($subEndPos + 1, $removalInfo['right']) - 1;
733: }
734: }
735:
736: $result .= $this->origTokens->getTokenCode($pos, $subStartPos, $indentAdjustment);
737:
738: if (null !== $subNode) {
739: $result .= $extraLeft;
740:
741: $origIndentLevel = $this->indentLevel;
742: $this->setIndentLevel(max($this->origTokens->getIndentationBefore($subStartPos) + $indentAdjustment, 0));
743:
744: // If it's the same node that was previously in this position, it certainly doesn't
745: // need fixup. It's important to check this here, because our fixup checks are more
746: // conservative than strictly necessary.
747: if (isset($fixupInfo[$subNodeName])
748: && $subNode->getAttribute('origNode') !== $origSubNode
749: ) {
750: $fixup = $fixupInfo[$subNodeName];
751: $res = $this->pFixup($fixup, $subNode, $class, $subStartPos, $subEndPos);
752: } else {
753: $res = $this->p($subNode, self::MAX_PRECEDENCE, self::MAX_PRECEDENCE, true);
754: }
755:
756: $this->safeAppend($result, $res);
757: $this->setIndentLevel($origIndentLevel);
758:
759: $result .= $extraRight;
760: }
761:
762: $pos = $subEndPos + 1;
763: }
764:
765: $result .= $this->origTokens->getTokenCode($pos, $endPos + 1, $indentAdjustment);
766: return $result;
767: }
768:
769: /**
770: * Perform a format-preserving pretty print of an array.
771: *
772: * @param Node[] $nodes New nodes
773: * @param Node[] $origNodes Original nodes
774: * @param int $pos Current token position (updated by reference)
775: * @param int $indentAdjustment Adjustment for indentation
776: * @param string $parentNodeClass Class of the containing node.
777: * @param string $subNodeName Name of array subnode.
778: * @param null|int $fixup Fixup information for array item nodes
779: *
780: * @return null|string Result of pretty print or null if cannot preserve formatting
781: */
782: protected function pArray(
783: array $nodes, array $origNodes, int &$pos, int $indentAdjustment,
784: string $parentNodeClass, string $subNodeName, ?int $fixup
785: ): ?string {
786: $diff = $this->nodeListDiffer->diffWithReplacements($origNodes, $nodes);
787:
788: $mapKey = $parentNodeClass . '->' . $subNodeName;
789: $insertStr = $this->listInsertionMap[$mapKey] ?? null;
790: $isStmtList = $subNodeName === 'stmts';
791:
792: $beforeFirstKeepOrReplace = true;
793: $skipRemovedNode = false;
794: $delayedAdd = [];
795: $lastElemIndentLevel = $this->indentLevel;
796:
797: $insertNewline = false;
798: if ($insertStr === "\n") {
799: $insertStr = '';
800: $insertNewline = true;
801: }
802:
803: if ($isStmtList && \count($origNodes) === 1 && \count($nodes) !== 1) {
804: $startPos = $origNodes[0]->getStartTokenPos();
805: $endPos = $origNodes[0]->getEndTokenPos();
806: \assert($startPos >= 0 && $endPos >= 0);
807: if (!$this->origTokens->haveBraces($startPos, $endPos)) {
808: // This was a single statement without braces, but either additional statements
809: // have been added, or the single statement has been removed. This requires the
810: // addition of braces. For now fall back.
811: // TODO: Try to preserve formatting
812: return null;
813: }
814: }
815:
816: $result = '';
817: foreach ($diff as $i => $diffElem) {
818: $diffType = $diffElem->type;
819: /** @var Node|string|null $arrItem */
820: $arrItem = $diffElem->new;
821: /** @var Node|string|null $origArrItem */
822: $origArrItem = $diffElem->old;
823:
824: if ($diffType === DiffElem::TYPE_KEEP || $diffType === DiffElem::TYPE_REPLACE) {
825: $beforeFirstKeepOrReplace = false;
826:
827: if ($origArrItem === null || $arrItem === null) {
828: // We can only handle the case where both are null
829: if ($origArrItem === $arrItem) {
830: continue;
831: }
832: return null;
833: }
834:
835: if (!$arrItem instanceof Node || !$origArrItem instanceof Node) {
836: // We can only deal with nodes. This can occur for Names, which use string arrays.
837: return null;
838: }
839:
840: $itemStartPos = $origArrItem->getStartTokenPos();
841: $itemEndPos = $origArrItem->getEndTokenPos();
842: \assert($itemStartPos >= 0 && $itemEndPos >= 0 && $itemStartPos >= $pos);
843:
844: $origIndentLevel = $this->indentLevel;
845: $lastElemIndentLevel = max($this->origTokens->getIndentationBefore($itemStartPos) + $indentAdjustment, 0);
846: $this->setIndentLevel($lastElemIndentLevel);
847:
848: $comments = $arrItem->getComments();
849: $origComments = $origArrItem->getComments();
850: $commentStartPos = $origComments ? $origComments[0]->getStartTokenPos() : $itemStartPos;
851: \assert($commentStartPos >= 0);
852:
853: if ($commentStartPos < $pos) {
854: // Comments may be assigned to multiple nodes if they start at the same position.
855: // Make sure we don't try to print them multiple times.
856: $commentStartPos = $itemStartPos;
857: }
858:
859: if ($skipRemovedNode) {
860: if ($isStmtList && $this->origTokens->haveTagInRange($pos, $itemStartPos)) {
861: // We'd remove an opening/closing PHP tag.
862: // TODO: Preserve formatting.
863: $this->setIndentLevel($origIndentLevel);
864: return null;
865: }
866: } else {
867: $result .= $this->origTokens->getTokenCode(
868: $pos, $commentStartPos, $indentAdjustment);
869: }
870:
871: if (!empty($delayedAdd)) {
872: /** @var Node $delayedAddNode */
873: foreach ($delayedAdd as $delayedAddNode) {
874: if ($insertNewline) {
875: $delayedAddComments = $delayedAddNode->getComments();
876: if ($delayedAddComments) {
877: $result .= $this->pComments($delayedAddComments) . $this->nl;
878: }
879: }
880:
881: $this->safeAppend($result, $this->p($delayedAddNode, self::MAX_PRECEDENCE, self::MAX_PRECEDENCE, true));
882:
883: if ($insertNewline) {
884: $result .= $insertStr . $this->nl;
885: } else {
886: $result .= $insertStr;
887: }
888: }
889:
890: $delayedAdd = [];
891: }
892:
893: if ($comments !== $origComments) {
894: if ($comments) {
895: $result .= $this->pComments($comments) . $this->nl;
896: }
897: } else {
898: $result .= $this->origTokens->getTokenCode(
899: $commentStartPos, $itemStartPos, $indentAdjustment);
900: }
901:
902: // If we had to remove anything, we have done so now.
903: $skipRemovedNode = false;
904: } elseif ($diffType === DiffElem::TYPE_ADD) {
905: if (null === $insertStr) {
906: // We don't have insertion information for this list type
907: return null;
908: }
909:
910: if (!$arrItem instanceof Node) {
911: // We only support list insertion of nodes.
912: return null;
913: }
914:
915: // We go multiline if the original code was multiline,
916: // or if it's an array item with a comment above it.
917: // Match always uses multiline formatting.
918: if ($insertStr === ', ' &&
919: ($this->isMultiline($origNodes) || $arrItem->getComments() ||
920: $parentNodeClass === Expr\Match_::class)
921: ) {
922: $insertStr = ',';
923: $insertNewline = true;
924: }
925:
926: if ($beforeFirstKeepOrReplace) {
927: // Will be inserted at the next "replace" or "keep" element
928: $delayedAdd[] = $arrItem;
929: continue;
930: }
931:
932: $itemStartPos = $pos;
933: $itemEndPos = $pos - 1;
934:
935: $origIndentLevel = $this->indentLevel;
936: $this->setIndentLevel($lastElemIndentLevel);
937:
938: if ($insertNewline) {
939: $result .= $insertStr . $this->nl;
940: $comments = $arrItem->getComments();
941: if ($comments) {
942: $result .= $this->pComments($comments) . $this->nl;
943: }
944: } else {
945: $result .= $insertStr;
946: }
947: } elseif ($diffType === DiffElem::TYPE_REMOVE) {
948: if (!$origArrItem instanceof Node) {
949: // We only support removal for nodes
950: return null;
951: }
952:
953: $itemStartPos = $origArrItem->getStartTokenPos();
954: $itemEndPos = $origArrItem->getEndTokenPos();
955: \assert($itemStartPos >= 0 && $itemEndPos >= 0);
956:
957: // Consider comments part of the node.
958: $origComments = $origArrItem->getComments();
959: if ($origComments) {
960: $itemStartPos = $origComments[0]->getStartTokenPos();
961: }
962:
963: if ($i === 0) {
964: // If we're removing from the start, keep the tokens before the node and drop those after it,
965: // instead of the other way around.
966: $result .= $this->origTokens->getTokenCode(
967: $pos, $itemStartPos, $indentAdjustment);
968: $skipRemovedNode = true;
969: } else {
970: if ($isStmtList && $this->origTokens->haveTagInRange($pos, $itemStartPos)) {
971: // We'd remove an opening/closing PHP tag.
972: // TODO: Preserve formatting.
973: return null;
974: }
975: }
976:
977: $pos = $itemEndPos + 1;
978: continue;
979: } else {
980: throw new \Exception("Shouldn't happen");
981: }
982:
983: if (null !== $fixup && $arrItem->getAttribute('origNode') !== $origArrItem) {
984: $res = $this->pFixup($fixup, $arrItem, null, $itemStartPos, $itemEndPos);
985: } else {
986: $res = $this->p($arrItem, self::MAX_PRECEDENCE, self::MAX_PRECEDENCE, true);
987: }
988: $this->safeAppend($result, $res);
989:
990: $this->setIndentLevel($origIndentLevel);
991: $pos = $itemEndPos + 1;
992: }
993:
994: if ($skipRemovedNode) {
995: // TODO: Support removing single node.
996: return null;
997: }
998:
999: if (!empty($delayedAdd)) {
1000: if (!isset($this->emptyListInsertionMap[$mapKey])) {
1001: return null;
1002: }
1003:
1004: list($findToken, $extraLeft, $extraRight) = $this->emptyListInsertionMap[$mapKey];
1005: if (null !== $findToken) {
1006: $insertPos = $this->origTokens->findRight($pos, $findToken) + 1;
1007: $result .= $this->origTokens->getTokenCode($pos, $insertPos, $indentAdjustment);
1008: $pos = $insertPos;
1009: }
1010:
1011: $first = true;
1012: $result .= $extraLeft;
1013: foreach ($delayedAdd as $delayedAddNode) {
1014: if (!$first) {
1015: $result .= $insertStr;
1016: if ($insertNewline) {
1017: $result .= $this->nl;
1018: }
1019: }
1020: $result .= $this->p($delayedAddNode, self::MAX_PRECEDENCE, self::MAX_PRECEDENCE, true);
1021: $first = false;
1022: }
1023: $result .= $extraRight === "\n" ? $this->nl : $extraRight;
1024: }
1025:
1026: return $result;
1027: }
1028:
1029: /**
1030: * Print node with fixups.
1031: *
1032: * Fixups here refer to the addition of extra parentheses, braces or other characters, that
1033: * are required to preserve program semantics in a certain context (e.g. to maintain precedence
1034: * or because only certain expressions are allowed in certain places).
1035: *
1036: * @param int $fixup Fixup type
1037: * @param Node $subNode Subnode to print
1038: * @param string|null $parentClass Class of parent node
1039: * @param int $subStartPos Original start pos of subnode
1040: * @param int $subEndPos Original end pos of subnode
1041: *
1042: * @return string Result of fixed-up print of subnode
1043: */
1044: protected function pFixup(int $fixup, Node $subNode, ?string $parentClass, int $subStartPos, int $subEndPos): string {
1045: switch ($fixup) {
1046: case self::FIXUP_PREC_LEFT:
1047: // We use a conservative approximation where lhsPrecedence == precedence.
1048: if (!$this->origTokens->haveParens($subStartPos, $subEndPos)) {
1049: $precedence = $this->precedenceMap[$parentClass][1];
1050: return $this->p($subNode, $precedence, $precedence);
1051: }
1052: break;
1053: case self::FIXUP_PREC_RIGHT:
1054: if (!$this->origTokens->haveParens($subStartPos, $subEndPos)) {
1055: $precedence = $this->precedenceMap[$parentClass][2];
1056: return $this->p($subNode, $precedence, $precedence);
1057: }
1058: break;
1059: case self::FIXUP_PREC_UNARY:
1060: if (!$this->origTokens->haveParens($subStartPos, $subEndPos)) {
1061: $precedence = $this->precedenceMap[$parentClass][0];
1062: return $this->p($subNode, $precedence, $precedence);
1063: }
1064: break;
1065: case self::FIXUP_CALL_LHS:
1066: if ($this->callLhsRequiresParens($subNode)
1067: && !$this->origTokens->haveParens($subStartPos, $subEndPos)
1068: ) {
1069: return '(' . $this->p($subNode) . ')';
1070: }
1071: break;
1072: case self::FIXUP_DEREF_LHS:
1073: if ($this->dereferenceLhsRequiresParens($subNode)
1074: && !$this->origTokens->haveParens($subStartPos, $subEndPos)
1075: ) {
1076: return '(' . $this->p($subNode) . ')';
1077: }
1078: break;
1079: case self::FIXUP_STATIC_DEREF_LHS:
1080: if ($this->staticDereferenceLhsRequiresParens($subNode)
1081: && !$this->origTokens->haveParens($subStartPos, $subEndPos)
1082: ) {
1083: return '(' . $this->p($subNode) . ')';
1084: }
1085: break;
1086: case self::FIXUP_NEW:
1087: if ($this->newOperandRequiresParens($subNode)
1088: && !$this->origTokens->haveParens($subStartPos, $subEndPos)) {
1089: return '(' . $this->p($subNode) . ')';
1090: }
1091: break;
1092: case self::FIXUP_BRACED_NAME:
1093: case self::FIXUP_VAR_BRACED_NAME:
1094: if ($subNode instanceof Expr
1095: && !$this->origTokens->haveBraces($subStartPos, $subEndPos)
1096: ) {
1097: return ($fixup === self::FIXUP_VAR_BRACED_NAME ? '$' : '')
1098: . '{' . $this->p($subNode) . '}';
1099: }
1100: break;
1101: case self::FIXUP_ENCAPSED:
1102: if (!$subNode instanceof Node\InterpolatedStringPart
1103: && !$this->origTokens->haveBraces($subStartPos, $subEndPos)
1104: ) {
1105: return '{' . $this->p($subNode) . '}';
1106: }
1107: break;
1108: default:
1109: throw new \Exception('Cannot happen');
1110: }
1111:
1112: // Nothing special to do
1113: return $this->p($subNode);
1114: }
1115:
1116: /**
1117: * Appends to a string, ensuring whitespace between label characters.
1118: *
1119: * Example: "echo" and "$x" result in "echo$x", but "echo" and "x" result in "echo x".
1120: * Without safeAppend the result would be "echox", which does not preserve semantics.
1121: */
1122: protected function safeAppend(string &$str, string $append): void {
1123: if ($str === "") {
1124: $str = $append;
1125: return;
1126: }
1127:
1128: if ($append === "") {
1129: return;
1130: }
1131:
1132: if (!$this->labelCharMap[$append[0]]
1133: || !$this->labelCharMap[$str[\strlen($str) - 1]]) {
1134: $str .= $append;
1135: } else {
1136: $str .= " " . $append;
1137: }
1138: }
1139:
1140: /**
1141: * Determines whether the LHS of a call must be wrapped in parenthesis.
1142: *
1143: * @param Node $node LHS of a call
1144: *
1145: * @return bool Whether parentheses are required
1146: */
1147: protected function callLhsRequiresParens(Node $node): bool {
1148: return !($node instanceof Node\Name
1149: || $node instanceof Expr\Variable
1150: || $node instanceof Expr\ArrayDimFetch
1151: || $node instanceof Expr\FuncCall
1152: || $node instanceof Expr\MethodCall
1153: || $node instanceof Expr\NullsafeMethodCall
1154: || $node instanceof Expr\StaticCall
1155: || $node instanceof Expr\Array_);
1156: }
1157:
1158: /**
1159: * Determines whether the LHS of an array/object operation must be wrapped in parentheses.
1160: *
1161: * @param Node $node LHS of dereferencing operation
1162: *
1163: * @return bool Whether parentheses are required
1164: */
1165: protected function dereferenceLhsRequiresParens(Node $node): bool {
1166: // A constant can occur on the LHS of an array/object deref, but not a static deref.
1167: return $this->staticDereferenceLhsRequiresParens($node)
1168: && !$node instanceof Expr\ConstFetch;
1169: }
1170:
1171: /**
1172: * Determines whether the LHS of a static operation must be wrapped in parentheses.
1173: *
1174: * @param Node $node LHS of dereferencing operation
1175: *
1176: * @return bool Whether parentheses are required
1177: */
1178: protected function staticDereferenceLhsRequiresParens(Node $node): bool {
1179: return !($node instanceof Expr\Variable
1180: || $node instanceof Node\Name
1181: || $node instanceof Expr\ArrayDimFetch
1182: || $node instanceof Expr\PropertyFetch
1183: || $node instanceof Expr\NullsafePropertyFetch
1184: || $node instanceof Expr\StaticPropertyFetch
1185: || $node instanceof Expr\FuncCall
1186: || $node instanceof Expr\MethodCall
1187: || $node instanceof Expr\NullsafeMethodCall
1188: || $node instanceof Expr\StaticCall
1189: || $node instanceof Expr\Array_
1190: || $node instanceof Scalar\String_
1191: || $node instanceof Expr\ClassConstFetch);
1192: }
1193:
1194: /**
1195: * Determines whether an expression used in "new" or "instanceof" requires parentheses.
1196: *
1197: * @param Node $node New or instanceof operand
1198: *
1199: * @return bool Whether parentheses are required
1200: */
1201: protected function newOperandRequiresParens(Node $node): bool {
1202: if ($node instanceof Node\Name || $node instanceof Expr\Variable) {
1203: return false;
1204: }
1205: if ($node instanceof Expr\ArrayDimFetch || $node instanceof Expr\PropertyFetch ||
1206: $node instanceof Expr\NullsafePropertyFetch
1207: ) {
1208: return $this->newOperandRequiresParens($node->var);
1209: }
1210: if ($node instanceof Expr\StaticPropertyFetch) {
1211: return $this->newOperandRequiresParens($node->class);
1212: }
1213: return true;
1214: }
1215:
1216: /**
1217: * Print modifiers, including trailing whitespace.
1218: *
1219: * @param int $modifiers Modifier mask to print
1220: *
1221: * @return string Printed modifiers
1222: */
1223: protected function pModifiers(int $modifiers): string {
1224: return ($modifiers & Modifiers::FINAL ? 'final ' : '')
1225: . ($modifiers & Modifiers::ABSTRACT ? 'abstract ' : '')
1226: . ($modifiers & Modifiers::PUBLIC ? 'public ' : '')
1227: . ($modifiers & Modifiers::PROTECTED ? 'protected ' : '')
1228: . ($modifiers & Modifiers::PRIVATE ? 'private ' : '')
1229: . ($modifiers & Modifiers::PUBLIC_SET ? 'public(set) ' : '')
1230: . ($modifiers & Modifiers::PROTECTED_SET ? 'protected(set) ' : '')
1231: . ($modifiers & Modifiers::PRIVATE_SET ? 'private(set) ' : '')
1232: . ($modifiers & Modifiers::STATIC ? 'static ' : '')
1233: . ($modifiers & Modifiers::READONLY ? 'readonly ' : '');
1234: }
1235:
1236: protected function pStatic(bool $static): string {
1237: return $static ? 'static ' : '';
1238: }
1239:
1240: /**
1241: * Determine whether a list of nodes uses multiline formatting.
1242: *
1243: * @param (Node|null)[] $nodes Node list
1244: *
1245: * @return bool Whether multiline formatting is used
1246: */
1247: protected function isMultiline(array $nodes): bool {
1248: if (\count($nodes) < 2) {
1249: return false;
1250: }
1251:
1252: $pos = -1;
1253: foreach ($nodes as $node) {
1254: if (null === $node) {
1255: continue;
1256: }
1257:
1258: $endPos = $node->getEndTokenPos() + 1;
1259: if ($pos >= 0) {
1260: $text = $this->origTokens->getTokenCode($pos, $endPos, 0);
1261: if (false === strpos($text, "\n")) {
1262: // We require that a newline is present between *every* item. If the formatting
1263: // is inconsistent, with only some items having newlines, we don't consider it
1264: // as multiline
1265: return false;
1266: }
1267: }
1268: $pos = $endPos;
1269: }
1270:
1271: return true;
1272: }
1273:
1274: /**
1275: * Lazily initializes label char map.
1276: *
1277: * The label char map determines whether a certain character may occur in a label.
1278: */
1279: protected function initializeLabelCharMap(): void {
1280: if (isset($this->labelCharMap)) {
1281: return;
1282: }
1283:
1284: $this->labelCharMap = [];
1285: for ($i = 0; $i < 256; $i++) {
1286: $chr = chr($i);
1287: $this->labelCharMap[$chr] = $i >= 0x80 || ctype_alnum($chr);
1288: }
1289:
1290: if ($this->phpVersion->allowsDelInIdentifiers()) {
1291: $this->labelCharMap["\x7f"] = true;
1292: }
1293: }
1294:
1295: /**
1296: * Lazily initializes node list differ.
1297: *
1298: * The node list differ is used to determine differences between two array subnodes.
1299: */
1300: protected function initializeNodeListDiffer(): void {
1301: if (isset($this->nodeListDiffer)) {
1302: return;
1303: }
1304:
1305: $this->nodeListDiffer = new Internal\Differ(function ($a, $b) {
1306: if ($a instanceof Node && $b instanceof Node) {
1307: return $a === $b->getAttribute('origNode');
1308: }
1309: // Can happen for array destructuring
1310: return $a === null && $b === null;
1311: });
1312: }
1313:
1314: /**
1315: * Lazily initializes fixup map.
1316: *
1317: * The fixup map is used to determine whether a certain subnode of a certain node may require
1318: * some kind of "fixup" operation, e.g. the addition of parenthesis or braces.
1319: */
1320: protected function initializeFixupMap(): void {
1321: if (isset($this->fixupMap)) {
1322: return;
1323: }
1324:
1325: $this->fixupMap = [
1326: Expr\Instanceof_::class => [
1327: 'expr' => self::FIXUP_PREC_UNARY,
1328: 'class' => self::FIXUP_NEW,
1329: ],
1330: Expr\Ternary::class => [
1331: 'cond' => self::FIXUP_PREC_LEFT,
1332: 'else' => self::FIXUP_PREC_RIGHT,
1333: ],
1334: Expr\Yield_::class => ['value' => self::FIXUP_PREC_UNARY],
1335:
1336: Expr\FuncCall::class => ['name' => self::FIXUP_CALL_LHS],
1337: Expr\StaticCall::class => ['class' => self::FIXUP_STATIC_DEREF_LHS],
1338: Expr\ArrayDimFetch::class => ['var' => self::FIXUP_DEREF_LHS],
1339: Expr\ClassConstFetch::class => [
1340: 'class' => self::FIXUP_STATIC_DEREF_LHS,
1341: 'name' => self::FIXUP_BRACED_NAME,
1342: ],
1343: Expr\New_::class => ['class' => self::FIXUP_NEW],
1344: Expr\MethodCall::class => [
1345: 'var' => self::FIXUP_DEREF_LHS,
1346: 'name' => self::FIXUP_BRACED_NAME,
1347: ],
1348: Expr\NullsafeMethodCall::class => [
1349: 'var' => self::FIXUP_DEREF_LHS,
1350: 'name' => self::FIXUP_BRACED_NAME,
1351: ],
1352: Expr\StaticPropertyFetch::class => [
1353: 'class' => self::FIXUP_STATIC_DEREF_LHS,
1354: 'name' => self::FIXUP_VAR_BRACED_NAME,
1355: ],
1356: Expr\PropertyFetch::class => [
1357: 'var' => self::FIXUP_DEREF_LHS,
1358: 'name' => self::FIXUP_BRACED_NAME,
1359: ],
1360: Expr\NullsafePropertyFetch::class => [
1361: 'var' => self::FIXUP_DEREF_LHS,
1362: 'name' => self::FIXUP_BRACED_NAME,
1363: ],
1364: Scalar\InterpolatedString::class => [
1365: 'parts' => self::FIXUP_ENCAPSED,
1366: ],
1367: ];
1368:
1369: $binaryOps = [
1370: BinaryOp\Pow::class, BinaryOp\Mul::class, BinaryOp\Div::class, BinaryOp\Mod::class,
1371: BinaryOp\Plus::class, BinaryOp\Minus::class, BinaryOp\Concat::class,
1372: BinaryOp\ShiftLeft::class, BinaryOp\ShiftRight::class, BinaryOp\Smaller::class,
1373: BinaryOp\SmallerOrEqual::class, BinaryOp\Greater::class, BinaryOp\GreaterOrEqual::class,
1374: BinaryOp\Equal::class, BinaryOp\NotEqual::class, BinaryOp\Identical::class,
1375: BinaryOp\NotIdentical::class, BinaryOp\Spaceship::class, BinaryOp\BitwiseAnd::class,
1376: BinaryOp\BitwiseXor::class, BinaryOp\BitwiseOr::class, BinaryOp\BooleanAnd::class,
1377: BinaryOp\BooleanOr::class, BinaryOp\Coalesce::class, BinaryOp\LogicalAnd::class,
1378: BinaryOp\LogicalXor::class, BinaryOp\LogicalOr::class, BinaryOp\Pipe::class,
1379: ];
1380: foreach ($binaryOps as $binaryOp) {
1381: $this->fixupMap[$binaryOp] = [
1382: 'left' => self::FIXUP_PREC_LEFT,
1383: 'right' => self::FIXUP_PREC_RIGHT
1384: ];
1385: }
1386:
1387: $prefixOps = [
1388: Expr\Clone_::class, Expr\BitwiseNot::class, Expr\BooleanNot::class, Expr\UnaryPlus::class, Expr\UnaryMinus::class,
1389: Cast\Int_::class, Cast\Double::class, Cast\String_::class, Cast\Array_::class,
1390: Cast\Object_::class, Cast\Bool_::class, Cast\Unset_::class, Expr\ErrorSuppress::class,
1391: Expr\YieldFrom::class, Expr\Print_::class, Expr\Include_::class,
1392: Expr\Assign::class, Expr\AssignRef::class, AssignOp\Plus::class, AssignOp\Minus::class,
1393: AssignOp\Mul::class, AssignOp\Div::class, AssignOp\Concat::class, AssignOp\Mod::class,
1394: AssignOp\BitwiseAnd::class, AssignOp\BitwiseOr::class, AssignOp\BitwiseXor::class,
1395: AssignOp\ShiftLeft::class, AssignOp\ShiftRight::class, AssignOp\Pow::class, AssignOp\Coalesce::class,
1396: Expr\ArrowFunction::class, Expr\Throw_::class,
1397: ];
1398: foreach ($prefixOps as $prefixOp) {
1399: $this->fixupMap[$prefixOp] = ['expr' => self::FIXUP_PREC_UNARY];
1400: }
1401: }
1402:
1403: /**
1404: * Lazily initializes the removal map.
1405: *
1406: * The removal map is used to determine which additional tokens should be removed when a
1407: * certain node is replaced by null.
1408: */
1409: protected function initializeRemovalMap(): void {
1410: if (isset($this->removalMap)) {
1411: return;
1412: }
1413:
1414: $stripBoth = ['left' => \T_WHITESPACE, 'right' => \T_WHITESPACE];
1415: $stripLeft = ['left' => \T_WHITESPACE];
1416: $stripRight = ['right' => \T_WHITESPACE];
1417: $stripDoubleArrow = ['right' => \T_DOUBLE_ARROW];
1418: $stripColon = ['left' => ':'];
1419: $stripEquals = ['left' => '='];
1420: $this->removalMap = [
1421: 'Expr_ArrayDimFetch->dim' => $stripBoth,
1422: 'ArrayItem->key' => $stripDoubleArrow,
1423: 'Expr_ArrowFunction->returnType' => $stripColon,
1424: 'Expr_Closure->returnType' => $stripColon,
1425: 'Expr_Exit->expr' => $stripBoth,
1426: 'Expr_Ternary->if' => $stripBoth,
1427: 'Expr_Yield->key' => $stripDoubleArrow,
1428: 'Expr_Yield->value' => $stripBoth,
1429: 'Param->type' => $stripRight,
1430: 'Param->default' => $stripEquals,
1431: 'Stmt_Break->num' => $stripBoth,
1432: 'Stmt_Catch->var' => $stripLeft,
1433: 'Stmt_ClassConst->type' => $stripRight,
1434: 'Stmt_ClassMethod->returnType' => $stripColon,
1435: 'Stmt_Class->extends' => ['left' => \T_EXTENDS],
1436: 'Stmt_Enum->scalarType' => $stripColon,
1437: 'Stmt_EnumCase->expr' => $stripEquals,
1438: 'Expr_PrintableNewAnonClass->extends' => ['left' => \T_EXTENDS],
1439: 'Stmt_Continue->num' => $stripBoth,
1440: 'Stmt_Foreach->keyVar' => $stripDoubleArrow,
1441: 'Stmt_Function->returnType' => $stripColon,
1442: 'Stmt_If->else' => $stripLeft,
1443: 'Stmt_Namespace->name' => $stripLeft,
1444: 'Stmt_Property->type' => $stripRight,
1445: 'PropertyItem->default' => $stripEquals,
1446: 'Stmt_Return->expr' => $stripBoth,
1447: 'Stmt_StaticVar->default' => $stripEquals,
1448: 'Stmt_TraitUseAdaptation_Alias->newName' => $stripLeft,
1449: 'Stmt_TryCatch->finally' => $stripLeft,
1450: // 'Stmt_Case->cond': Replace with "default"
1451: // 'Stmt_Class->name': Unclear what to do
1452: // 'Stmt_Declare->stmts': Not a plain node
1453: // 'Stmt_TraitUseAdaptation_Alias->newModifier': Not a plain node
1454: ];
1455: }
1456:
1457: protected function initializeInsertionMap(): void {
1458: if (isset($this->insertionMap)) {
1459: return;
1460: }
1461:
1462: // TODO: "yield" where both key and value are inserted doesn't work
1463: // [$find, $beforeToken, $extraLeft, $extraRight]
1464: $this->insertionMap = [
1465: 'Expr_ArrayDimFetch->dim' => ['[', false, null, null],
1466: 'ArrayItem->key' => [null, false, null, ' => '],
1467: 'Expr_ArrowFunction->returnType' => [')', false, ': ', null],
1468: 'Expr_Closure->returnType' => [')', false, ': ', null],
1469: 'Expr_Ternary->if' => ['?', false, ' ', ' '],
1470: 'Expr_Yield->key' => [\T_YIELD, false, null, ' => '],
1471: 'Expr_Yield->value' => [\T_YIELD, false, ' ', null],
1472: 'Param->type' => [null, false, null, ' '],
1473: 'Param->default' => [null, false, ' = ', null],
1474: 'Stmt_Break->num' => [\T_BREAK, false, ' ', null],
1475: 'Stmt_Catch->var' => [null, false, ' ', null],
1476: 'Stmt_ClassMethod->returnType' => [')', false, ': ', null],
1477: 'Stmt_ClassConst->type' => [\T_CONST, false, ' ', null],
1478: 'Stmt_Class->extends' => [null, false, ' extends ', null],
1479: 'Stmt_Enum->scalarType' => [null, false, ' : ', null],
1480: 'Stmt_EnumCase->expr' => [null, false, ' = ', null],
1481: 'Expr_PrintableNewAnonClass->extends' => [null, false, ' extends ', null],
1482: 'Stmt_Continue->num' => [\T_CONTINUE, false, ' ', null],
1483: 'Stmt_Foreach->keyVar' => [\T_AS, false, null, ' => '],
1484: 'Stmt_Function->returnType' => [')', false, ': ', null],
1485: 'Stmt_If->else' => [null, false, ' ', null],
1486: 'Stmt_Namespace->name' => [\T_NAMESPACE, false, ' ', null],
1487: 'Stmt_Property->type' => [\T_VARIABLE, true, null, ' '],
1488: 'PropertyItem->default' => [null, false, ' = ', null],
1489: 'Stmt_Return->expr' => [\T_RETURN, false, ' ', null],
1490: 'Stmt_StaticVar->default' => [null, false, ' = ', null],
1491: //'Stmt_TraitUseAdaptation_Alias->newName' => [T_AS, false, ' ', null], // TODO
1492: 'Stmt_TryCatch->finally' => [null, false, ' ', null],
1493:
1494: // 'Expr_Exit->expr': Complicated due to optional ()
1495: // 'Stmt_Case->cond': Conversion from default to case
1496: // 'Stmt_Class->name': Unclear
1497: // 'Stmt_Declare->stmts': Not a proper node
1498: // 'Stmt_TraitUseAdaptation_Alias->newModifier': Not a proper node
1499: ];
1500: }
1501:
1502: protected function initializeListInsertionMap(): void {
1503: if (isset($this->listInsertionMap)) {
1504: return;
1505: }
1506:
1507: $this->listInsertionMap = [
1508: // special
1509: //'Expr_ShellExec->parts' => '', // TODO These need to be treated more carefully
1510: //'Scalar_InterpolatedString->parts' => '',
1511: Stmt\Catch_::class . '->types' => '|',
1512: UnionType::class . '->types' => '|',
1513: IntersectionType::class . '->types' => '&',
1514: Stmt\If_::class . '->elseifs' => ' ',
1515: Stmt\TryCatch::class . '->catches' => ' ',
1516:
1517: // comma-separated lists
1518: Expr\Array_::class . '->items' => ', ',
1519: Expr\ArrowFunction::class . '->params' => ', ',
1520: Expr\Closure::class . '->params' => ', ',
1521: Expr\Closure::class . '->uses' => ', ',
1522: Expr\FuncCall::class . '->args' => ', ',
1523: Expr\Isset_::class . '->vars' => ', ',
1524: Expr\List_::class . '->items' => ', ',
1525: Expr\MethodCall::class . '->args' => ', ',
1526: Expr\NullsafeMethodCall::class . '->args' => ', ',
1527: Expr\New_::class . '->args' => ', ',
1528: PrintableNewAnonClassNode::class . '->args' => ', ',
1529: Expr\StaticCall::class . '->args' => ', ',
1530: Stmt\ClassConst::class . '->consts' => ', ',
1531: Stmt\ClassMethod::class . '->params' => ', ',
1532: Stmt\Class_::class . '->implements' => ', ',
1533: Stmt\Enum_::class . '->implements' => ', ',
1534: PrintableNewAnonClassNode::class . '->implements' => ', ',
1535: Stmt\Const_::class . '->consts' => ', ',
1536: Stmt\Declare_::class . '->declares' => ', ',
1537: Stmt\Echo_::class . '->exprs' => ', ',
1538: Stmt\For_::class . '->init' => ', ',
1539: Stmt\For_::class . '->cond' => ', ',
1540: Stmt\For_::class . '->loop' => ', ',
1541: Stmt\Function_::class . '->params' => ', ',
1542: Stmt\Global_::class . '->vars' => ', ',
1543: Stmt\GroupUse::class . '->uses' => ', ',
1544: Stmt\Interface_::class . '->extends' => ', ',
1545: Expr\Match_::class . '->arms' => ', ',
1546: Stmt\Property::class . '->props' => ', ',
1547: Stmt\StaticVar::class . '->vars' => ', ',
1548: Stmt\TraitUse::class . '->traits' => ', ',
1549: Stmt\TraitUseAdaptation\Precedence::class . '->insteadof' => ', ',
1550: Stmt\Unset_::class . '->vars' => ', ',
1551: Stmt\UseUse::class . '->uses' => ', ',
1552: MatchArm::class . '->conds' => ', ',
1553: AttributeGroup::class . '->attrs' => ', ',
1554: PropertyHook::class . '->params' => ', ',
1555:
1556: // statement lists
1557: Expr\Closure::class . '->stmts' => "\n",
1558: Stmt\Case_::class . '->stmts' => "\n",
1559: Stmt\Catch_::class . '->stmts' => "\n",
1560: Stmt\Class_::class . '->stmts' => "\n",
1561: Stmt\Enum_::class . '->stmts' => "\n",
1562: PrintableNewAnonClassNode::class . '->stmts' => "\n",
1563: Stmt\Interface_::class . '->stmts' => "\n",
1564: Stmt\Trait_::class . '->stmts' => "\n",
1565: Stmt\ClassMethod::class . '->stmts' => "\n",
1566: Stmt\Declare_::class . '->stmts' => "\n",
1567: Stmt\Do_::class . '->stmts' => "\n",
1568: Stmt\ElseIf_::class . '->stmts' => "\n",
1569: Stmt\Else_::class . '->stmts' => "\n",
1570: Stmt\Finally_::class . '->stmts' => "\n",
1571: Stmt\Foreach_::class . '->stmts' => "\n",
1572: Stmt\For_::class . '->stmts' => "\n",
1573: Stmt\Function_::class . '->stmts' => "\n",
1574: Stmt\If_::class . '->stmts' => "\n",
1575: Stmt\Namespace_::class . '->stmts' => "\n",
1576: Stmt\Block::class . '->stmts' => "\n",
1577:
1578: // Attribute groups
1579: Stmt\Class_::class . '->attrGroups' => "\n",
1580: Stmt\Enum_::class . '->attrGroups' => "\n",
1581: Stmt\EnumCase::class . '->attrGroups' => "\n",
1582: Stmt\Interface_::class . '->attrGroups' => "\n",
1583: Stmt\Trait_::class . '->attrGroups' => "\n",
1584: Stmt\Function_::class . '->attrGroups' => "\n",
1585: Stmt\ClassMethod::class . '->attrGroups' => "\n",
1586: Stmt\ClassConst::class . '->attrGroups' => "\n",
1587: Stmt\Property::class . '->attrGroups' => "\n",
1588: PrintableNewAnonClassNode::class . '->attrGroups' => ' ',
1589: Expr\Closure::class . '->attrGroups' => ' ',
1590: Expr\ArrowFunction::class . '->attrGroups' => ' ',
1591: Param::class . '->attrGroups' => ' ',
1592: PropertyHook::class . '->attrGroups' => ' ',
1593:
1594: Stmt\Switch_::class . '->cases' => "\n",
1595: Stmt\TraitUse::class . '->adaptations' => "\n",
1596: Stmt\TryCatch::class . '->stmts' => "\n",
1597: Stmt\While_::class . '->stmts' => "\n",
1598: PropertyHook::class . '->body' => "\n",
1599: Stmt\Property::class . '->hooks' => "\n",
1600: Param::class . '->hooks' => "\n",
1601:
1602: // dummy for top-level context
1603: 'File->stmts' => "\n",
1604: ];
1605: }
1606:
1607: protected function initializeEmptyListInsertionMap(): void {
1608: if (isset($this->emptyListInsertionMap)) {
1609: return;
1610: }
1611:
1612: // TODO Insertion into empty statement lists.
1613:
1614: // [$find, $extraLeft, $extraRight]
1615: $this->emptyListInsertionMap = [
1616: Expr\ArrowFunction::class . '->params' => ['(', '', ''],
1617: Expr\Closure::class . '->uses' => [')', ' use (', ')'],
1618: Expr\Closure::class . '->params' => ['(', '', ''],
1619: Expr\FuncCall::class . '->args' => ['(', '', ''],
1620: Expr\MethodCall::class . '->args' => ['(', '', ''],
1621: Expr\NullsafeMethodCall::class . '->args' => ['(', '', ''],
1622: Expr\New_::class . '->args' => ['(', '', ''],
1623: PrintableNewAnonClassNode::class . '->args' => ['(', '', ''],
1624: PrintableNewAnonClassNode::class . '->implements' => [null, ' implements ', ''],
1625: Expr\StaticCall::class . '->args' => ['(', '', ''],
1626: Stmt\Class_::class . '->implements' => [null, ' implements ', ''],
1627: Stmt\Enum_::class . '->implements' => [null, ' implements ', ''],
1628: Stmt\ClassMethod::class . '->params' => ['(', '', ''],
1629: Stmt\Interface_::class . '->extends' => [null, ' extends ', ''],
1630: Stmt\Function_::class . '->params' => ['(', '', ''],
1631: Stmt\Interface_::class . '->attrGroups' => [null, '', "\n"],
1632: Stmt\Class_::class . '->attrGroups' => [null, '', "\n"],
1633: Stmt\ClassConst::class . '->attrGroups' => [null, '', "\n"],
1634: Stmt\ClassMethod::class . '->attrGroups' => [null, '', "\n"],
1635: Stmt\Function_::class . '->attrGroups' => [null, '', "\n"],
1636: Stmt\Property::class . '->attrGroups' => [null, '', "\n"],
1637: Stmt\Trait_::class . '->attrGroups' => [null, '', "\n"],
1638: Expr\ArrowFunction::class . '->attrGroups' => [null, '', ' '],
1639: Expr\Closure::class . '->attrGroups' => [null, '', ' '],
1640: Stmt\Const_::class . '->attrGroups' => [null, '', "\n"],
1641: PrintableNewAnonClassNode::class . '->attrGroups' => [\T_NEW, ' ', ''],
1642:
1643: /* These cannot be empty to start with:
1644: * Expr_Isset->vars
1645: * Stmt_Catch->types
1646: * Stmt_Const->consts
1647: * Stmt_ClassConst->consts
1648: * Stmt_Declare->declares
1649: * Stmt_Echo->exprs
1650: * Stmt_Global->vars
1651: * Stmt_GroupUse->uses
1652: * Stmt_Property->props
1653: * Stmt_StaticVar->vars
1654: * Stmt_TraitUse->traits
1655: * Stmt_TraitUseAdaptation_Precedence->insteadof
1656: * Stmt_Unset->vars
1657: * Stmt_Use->uses
1658: * UnionType->types
1659: */
1660:
1661: /* TODO
1662: * Stmt_If->elseifs
1663: * Stmt_TryCatch->catches
1664: * Expr_Array->items
1665: * Expr_List->items
1666: * Stmt_For->init
1667: * Stmt_For->cond
1668: * Stmt_For->loop
1669: */
1670: ];
1671: }
1672:
1673: protected function initializeModifierChangeMap(): void {
1674: if (isset($this->modifierChangeMap)) {
1675: return;
1676: }
1677:
1678: $this->modifierChangeMap = [
1679: Stmt\ClassConst::class . '->flags' => ['pModifiers', \T_CONST],
1680: Stmt\ClassMethod::class . '->flags' => ['pModifiers', \T_FUNCTION],
1681: Stmt\Class_::class . '->flags' => ['pModifiers', \T_CLASS],
1682: Stmt\Property::class . '->flags' => ['pModifiers', \T_VARIABLE],
1683: PrintableNewAnonClassNode::class . '->flags' => ['pModifiers', \T_CLASS],
1684: Param::class . '->flags' => ['pModifiers', \T_VARIABLE],
1685: PropertyHook::class . '->flags' => ['pModifiers', \T_STRING],
1686: Expr\Closure::class . '->static' => ['pStatic', \T_FUNCTION],
1687: Expr\ArrowFunction::class . '->static' => ['pStatic', \T_FN],
1688: //Stmt\TraitUseAdaptation\Alias::class . '->newModifier' => 0, // TODO
1689: ];
1690:
1691: // List of integer subnodes that are not modifiers:
1692: // Expr_Include->type
1693: // Stmt_GroupUse->type
1694: // Stmt_Use->type
1695: // UseItem->type
1696: }
1697: }
1698: