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