• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nikic / PHP-Parser / 18631674169

19 Oct 2025 02:18PM UTC coverage: 92.375% (-0.2%) from 92.539%
18631674169

Pull #1113

github

web-flow
Merge 86a87b02d into 0105ba17b
Pull Request #1113: add ContaintsStmts interface to mark stmt classes with nested stmts inside

2 of 17 new or added lines in 14 files covered. (11.76%)

19 existing lines in 1 file now uncovered.

7681 of 8315 relevant lines covered (92.38%)

226.1 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.49
/lib/PhpParser/PrettyPrinterAbstract.php
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);
641✔
197

198
        $this->newline = $options['newline'] ?? "\n";
641✔
199
        if ($this->newline !== "\n" && $this->newline != "\r\n") {
641✔
200
            throw new \LogicException('Option "newline" must be one of "\n" or "\r\n"');
1✔
201
        }
202

203
        $this->shortArraySyntax =
640✔
204
            $options['shortArraySyntax'] ?? $this->phpVersion->supportsShortArraySyntax();
640✔
205
        $this->docStringEndToken =
640✔
206
            $this->phpVersion->supportsFlexibleHeredoc() ? null : '_DOC_STRING_END_' . mt_rand();
640✔
207

208
        $this->indent = $indent = $options['indent'] ?? '    ';
640✔
209
        if ($indent === "\t") {
640✔
210
            $this->useTabs = true;
4✔
211
            $this->indentWidth = $this->tabWidth;
4✔
212
        } elseif ($indent === \str_repeat(' ', \strlen($indent))) {
636✔
213
            $this->useTabs = false;
635✔
214
            $this->indentWidth = \strlen($indent);
635✔
215
        } else {
216
            throw new \LogicException('Option "indent" must either be all spaces or a single tab');
1✔
217
        }
218
    }
219

220
    /**
221
     * Reset pretty printing state.
222
     */
223
    protected function resetState(): void {
224
        $this->indentLevel = 0;
536✔
225
        $this->nl = $this->newline;
536✔
226
        $this->origTokens = null;
536✔
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;
448✔
236
        if ($this->useTabs) {
448✔
237
            $tabs = \intdiv($level, $this->tabWidth);
4✔
238
            $spaces = $level % $this->tabWidth;
4✔
239
            $this->nl = $this->newline . \str_repeat("\t", $tabs) . \str_repeat(' ', $spaces);
4✔
240
        } else {
241
            $this->nl = $this->newline . \str_repeat(' ', $level);
444✔
242
        }
243
    }
244

245
    /**
246
     * Increase indentation level.
247
     */
248
    protected function indent(): void {
249
        $this->indentLevel += $this->indentWidth;
73✔
250
        $this->nl .= $this->indent;
73✔
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);
73✔
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();
98✔
270
        $this->preprocessNodes($stmts);
98✔
271

272
        return ltrim($this->handleMagicTokens($this->pStmts($stmts, false)));
98✔
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();
43✔
284
        return $this->handleMagicTokens($this->p($node));
43✔
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) {
15✔
296
            return "<?php" . $this->newline . $this->newline;
1✔
297
        }
298

299
        $p = "<?php" . $this->newline . $this->newline . $this->prettyPrint($stmts);
14✔
300

301
        if ($stmts[0] instanceof Stmt\InlineHTML) {
14✔
302
            $p = preg_replace('/^<\?php\s+\?>\r?\n?/', '', $p);
8✔
303
        }
304
        if ($stmts[count($stmts) - 1] instanceof Stmt\InlineHTML) {
14✔
305
            $p = preg_replace('/<\?php$/', '', rtrim($p));
9✔
306
        }
307

308
        return $p;
14✔
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;
493✔
319
        foreach ($nodes as $node) {
493✔
320
            if ($node instanceof Stmt\Namespace_ && null === $node->name) {
492✔
321
                $this->canUseSemicolonNamespaces = false;
4✔
322
                break;
4✔
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) {
534✔
332
            // Replace doc-string-end tokens with nothing or a newline
333
            $str = str_replace(
12✔
334
                $this->docStringEndToken . ';' . $this->newline,
12✔
335
                ';' . $this->newline,
12✔
336
                $str);
12✔
337
            $str = str_replace($this->docStringEndToken, $this->newline, $str);
12✔
338
        }
339

340
        return $str;
534✔
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) {
117✔
353
            $this->indent();
70✔
354
        }
355

356
        $result = '';
117✔
357
        foreach ($nodes as $node) {
117✔
358
            $comments = $node->getComments();
113✔
359
            if ($comments) {
113✔
360
                $result .= $this->nl . $this->pComments($comments);
12✔
361
                if ($node instanceof Stmt\Nop) {
12✔
362
                    continue;
2✔
363
                }
364
            }
365

366
            $result .= $this->nl . $this->p($node);
113✔
367
        }
368

369
        if ($indent) {
115✔
370
            $this->outdent();
70✔
371
        }
372

373
        return $result;
115✔
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];
16✔
393
        $prefix = '';
16✔
394
        $suffix = '';
16✔
395
        if ($opPrecedence >= $precedence) {
16✔
396
            $prefix = '(';
4✔
397
            $suffix = ')';
4✔
398
            $lhsPrecedence = self::MAX_PRECEDENCE;
4✔
399
        }
400
        return $prefix . $this->p($leftNode, $newPrecedenceLHS, $newPrecedenceLHS)
16✔
401
            . $operatorString . $this->p($rightNode, $newPrecedenceRHS, $lhsPrecedence) . $suffix;
16✔
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];
30✔
417
        $prefix = '';
30✔
418
        $suffix = '';
30✔
419
        if ($opPrecedence >= $lhsPrecedence) {
30✔
420
            $prefix = '(';
5✔
421
            $suffix = ')';
5✔
422
            $lhsPrecedence = self::MAX_PRECEDENCE;
5✔
423
        }
424
        $printedArg = $this->p($node, $opPrecedence, $lhsPrecedence);
30✔
425
        if (($operatorString === '+' && $printedArg[0] === '+') ||
30✔
426
            ($operatorString === '-' && $printedArg[0] === '-')
30✔
427
        ) {
428
            // Avoid printing +(+$a) as ++$a and similar.
429
            $printedArg = '(' . $printedArg . ')';
1✔
430
        }
431
        return $prefix . $operatorString . $printedArg . $suffix;
30✔
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];
3✔
447
        $prefix = '';
3✔
448
        $suffix = '';
3✔
449
        if ($opPrecedence >= $precedence) {
3✔
450
            $prefix = '(';
1✔
451
            $suffix = ')';
1✔
452
            $lhsPrecedence = self::MAX_PRECEDENCE;
1✔
453
        }
454
        if ($opPrecedence < $lhsPrecedence) {
3✔
455
            $lhsPrecedence = $opPrecedence;
3✔
456
        }
457
        return $prefix . $this->p($node, $opPrecedence, $lhsPrecedence) . $operatorString . $suffix;
3✔
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 = [];
97✔
470
        foreach ($nodes as $node) {
97✔
471
            if (null === $node) {
75✔
472
                $pNodes[] = '';
3✔
473
            } else {
474
                $pNodes[] = $this->p($node);
75✔
475
            }
476
        }
477

478
        return implode($glue, $pNodes);
97✔
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, ', ');
92✔
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();
10✔
504

505
        $result = '';
10✔
506
        $lastIdx = count($nodes) - 1;
10✔
507
        foreach ($nodes as $idx => $node) {
10✔
508
            if ($node !== null) {
10✔
509
                $comments = $node->getComments();
10✔
510
                if ($comments) {
10✔
511
                    $result .= $this->nl . $this->pComments($comments);
6✔
512
                }
513

514
                $result .= $this->nl . $this->p($node);
10✔
515
            } else {
516
                $result .= $this->nl;
1✔
517
            }
518
            if ($trailingComma || $idx !== $lastIdx) {
10✔
519
                $result .= ',';
7✔
520
            }
521
        }
522

523
        $this->outdent();
10✔
524
        return $result;
10✔
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 = [];
34✔
536

537
        foreach ($comments as $comment) {
34✔
538
            $formattedComments[] = str_replace("\n", $this->nl, $comment->getReformattedText());
34✔
539
        }
540

541
        return implode($this->nl, $formattedComments);
34✔
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();
395✔
561
        $this->initializeLabelCharMap();
395✔
562
        $this->initializeFixupMap();
395✔
563
        $this->initializeRemovalMap();
395✔
564
        $this->initializeInsertionMap();
395✔
565
        $this->initializeListInsertionMap();
395✔
566
        $this->initializeEmptyListInsertionMap();
395✔
567
        $this->initializeModifierChangeMap();
395✔
568

569
        $this->resetState();
395✔
570
        $this->origTokens = new TokenStream($origTokens, $this->tabWidth);
395✔
571

572
        $this->preprocessNodes($stmts);
395✔
573

574
        $pos = 0;
395✔
575
        $result = $this->pArray($stmts, $origStmts, $pos, 0, 'File', 'stmts', null);
395✔
576
        if (null !== $result) {
395✔
577
            $result .= $this->origTokens->getTokenCode($pos, count($origTokens) - 1, 0);
395✔
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);
395✔
585
    }
586

587
    protected function pFallback(Node $node, int $precedence, int $lhsPrecedence): string {
588
        return $this->{'p' . $node->getType()}($node, $precedence, $lhsPrecedence);
98✔
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) {
535✔
609
            return $this->{'p' . $node->getType()}($node, $precedence, $lhsPrecedence);
141✔
610
        }
611

612
        /** @var Node|null $origNode */
613
        $origNode = $node->getAttribute('origNode');
394✔
614
        if (null === $origNode) {
394✔
615
            return $this->pFallback($node, $precedence, $lhsPrecedence);
83✔
616
        }
617

618
        $class = \get_class($node);
392✔
619
        \assert($class === \get_class($origNode));
620

621
        $startPos = $origNode->getStartTokenPos();
392✔
622
        $endPos = $origNode->getEndTokenPos();
392✔
623
        \assert($startPos >= 0 && $endPos >= 0);
624

625
        $fallbackNode = $node;
392✔
626
        if ($node instanceof Expr\New_ && $node->class instanceof Stmt\Class_) {
392✔
627
            // Normalize node structure of anonymous classes
628
            assert($origNode instanceof Expr\New_);
629
            $node = PrintableNewAnonClassNode::fromNewNode($node);
11✔
630
            $origNode = PrintableNewAnonClassNode::fromNewNode($origNode);
11✔
631
            $class = PrintableNewAnonClassNode::class;
11✔
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) {
392✔
638
            return $this->pFallback($fallbackNode, $precedence, $lhsPrecedence);
3✔
639
        }
640

641
        $indentAdjustment = $this->indentLevel - $this->origTokens->getIndentationBefore($startPos);
391✔
642

643
        $type = $node->getType();
391✔
644
        $fixupInfo = $this->fixupMap[$class] ?? null;
391✔
645

646
        $result = '';
391✔
647
        $pos = $startPos;
391✔
648
        foreach ($node->getSubNodeNames() as $subNodeName) {
391✔
649
            $subNode = $node->$subNodeName;
389✔
650
            $origSubNode = $origNode->$subNodeName;
389✔
651

652
            if ((!$subNode instanceof Node && $subNode !== null)
389✔
653
                || (!$origSubNode instanceof Node && $origSubNode !== null)
389✔
654
            ) {
655
                if ($subNode === $origSubNode) {
388✔
656
                    // Unchanged, can reuse old code
657
                    continue;
385✔
658
                }
659

660
                if (is_array($subNode) && is_array($origSubNode)) {
292✔
661
                    // Array subnode changed, we might be able to reconstruct it
662
                    $listResult = $this->pArray(
289✔
663
                        $subNode, $origSubNode, $pos, $indentAdjustment, $class, $subNodeName,
289✔
664
                        $fixupInfo[$subNodeName] ?? null
289✔
665
                    );
289✔
666
                    if (null === $listResult) {
289✔
667
                        return $this->pFallback($fallbackNode, $precedence, $lhsPrecedence);
10✔
668
                    }
669

670
                    $result .= $listResult;
280✔
671
                    continue;
280✔
672
                }
673

674
                // Check if this is a modifier change
675
                $key = $class . '->' . $subNodeName;
16✔
676
                if (!isset($this->modifierChangeMap[$key])) {
16✔
677
                    return $this->pFallback($fallbackNode, $precedence, $lhsPrecedence);
9✔
678
                }
679

680
                [$printFn, $findToken] = $this->modifierChangeMap[$key];
7✔
681
                $skipWSPos = $this->origTokens->skipRightWhitespace($pos);
7✔
682
                $result .= $this->origTokens->getTokenCode($pos, $skipWSPos, $indentAdjustment);
7✔
683
                $result .= $this->$printFn($subNode);
7✔
684
                $pos = $this->origTokens->findRight($skipWSPos, $findToken);
7✔
685
                continue;
7✔
686
            }
687

688
            $extraLeft = '';
382✔
689
            $extraRight = '';
382✔
690
            if ($origSubNode !== null) {
382✔
691
                $subStartPos = $origSubNode->getStartTokenPos();
379✔
692
                $subEndPos = $origSubNode->getEndTokenPos();
379✔
693
                \assert($subStartPos >= 0 && $subEndPos >= 0);
379✔
694
            } else {
695
                if ($subNode === null) {
256✔
696
                    // Both null, nothing to do
697
                    continue;
252✔
698
                }
699

700
                // A node has been inserted, check if we have insertion information for it
701
                $key = $type . '->' . $subNodeName;
10✔
702
                if (!isset($this->insertionMap[$key])) {
10✔
703
                    return $this->pFallback($fallbackNode, $precedence, $lhsPrecedence);
3✔
704
                }
705

706
                list($findToken, $beforeToken, $extraLeft, $extraRight) = $this->insertionMap[$key];
8✔
707
                if (null !== $findToken) {
8✔
708
                    $subStartPos = $this->origTokens->findRight($pos, $findToken)
4✔
709
                        + (int) !$beforeToken;
4✔
710
                } else {
711
                    $subStartPos = $pos;
5✔
712
                }
713

714
                if (null === $extraLeft && null !== $extraRight) {
8✔
715
                    // If inserting on the right only, skipping whitespace looks better
716
                    $subStartPos = $this->origTokens->skipRightWhitespace($subStartPos);
3✔
717
                }
718
                $subEndPos = $subStartPos - 1;
8✔
719
            }
720

721
            if (null === $subNode) {
381✔
722
                // A node has been removed, check if we have removal information for it
723
                $key = $type . '->' . $subNodeName;
9✔
724
                if (!isset($this->removalMap[$key])) {
9✔
725
                    return $this->pFallback($fallbackNode, $precedence, $lhsPrecedence);
2✔
726
                }
727

728
                // Adjust positions to account for additional tokens that must be skipped
729
                $removalInfo = $this->removalMap[$key];
8✔
730
                if (isset($removalInfo['left'])) {
8✔
731
                    $subStartPos = $this->origTokens->skipLeft($subStartPos - 1, $removalInfo['left']) + 1;
7✔
732
                }
733
                if (isset($removalInfo['right'])) {
8✔
734
                    $subEndPos = $this->origTokens->skipRight($subEndPos + 1, $removalInfo['right']) - 1;
2✔
735
                }
736
            }
737

738
            $result .= $this->origTokens->getTokenCode($pos, $subStartPos, $indentAdjustment);
381✔
739

740
            if (null !== $subNode) {
381✔
741
                $result .= $extraLeft;
379✔
742

743
                $origIndentLevel = $this->indentLevel;
379✔
744
                $this->setIndentLevel(max($this->origTokens->getIndentationBefore($subStartPos) + $indentAdjustment, 0));
379✔
745

746
                // If it's the same node that was previously in this position, it certainly doesn't
747
                // need fixup. It's important to check this here, because our fixup checks are more
748
                // conservative than strictly necessary.
749
                if (isset($fixupInfo[$subNodeName])
379✔
750
                    && $subNode->getAttribute('origNode') !== $origSubNode
379✔
751
                ) {
752
                    $fixup = $fixupInfo[$subNodeName];
3✔
753
                    $res = $this->pFixup($fixup, $subNode, $class, $subStartPos, $subEndPos);
3✔
754
                } else {
755
                    $res = $this->p($subNode, self::MAX_PRECEDENCE, self::MAX_PRECEDENCE, true);
379✔
756
                }
757

758
                $this->safeAppend($result, $res);
379✔
759
                $this->setIndentLevel($origIndentLevel);
379✔
760

761
                $result .= $extraRight;
379✔
762
            }
763

764
            $pos = $subEndPos + 1;
381✔
765
        }
766

767
        $result .= $this->origTokens->getTokenCode($pos, $endPos + 1, $indentAdjustment);
391✔
768
        return $result;
391✔
769
    }
770

771
    /**
772
     * Perform a format-preserving pretty print of an array.
773
     *
774
     * @param Node[] $nodes New nodes
775
     * @param Node[] $origNodes Original nodes
776
     * @param int $pos Current token position (updated by reference)
777
     * @param int $indentAdjustment Adjustment for indentation
778
     * @param string $parentNodeClass Class of the containing node.
779
     * @param string $subNodeName Name of array subnode.
780
     * @param null|int $fixup Fixup information for array item nodes
781
     *
782
     * @return null|string Result of pretty print or null if cannot preserve formatting
783
     */
784
    protected function pArray(
785
        array  $nodes, array $origNodes, int &$pos, int $indentAdjustment,
786
        string $parentNodeClass, string $subNodeName, ?int $fixup
787
    ): ?string {
788
        $diff = $this->nodeListDiffer->diffWithReplacements($origNodes, $nodes);
395✔
789

790
        $mapKey = $parentNodeClass . '->' . $subNodeName;
395✔
791
        $insertStr = $this->listInsertionMap[$mapKey] ?? null;
395✔
792
        $isStmtList = $subNodeName === 'stmts';
395✔
793

794
        $beforeFirstKeepOrReplace = true;
395✔
795
        $skipRemovedNode = false;
395✔
796
        $delayedAdd = [];
395✔
797
        $lastElemIndentLevel = $this->indentLevel;
395✔
798

799
        $insertNewline = false;
395✔
800
        if ($insertStr === "\n") {
395✔
801
            $insertStr = '';
395✔
802
            $insertNewline = true;
395✔
803
        }
804

805
        if ($isStmtList && \count($origNodes) === 1 && \count($nodes) !== 1) {
395✔
806
            $startPos = $origNodes[0]->getStartTokenPos();
5✔
807
            $endPos = $origNodes[0]->getEndTokenPos();
5✔
808
            \assert($startPos >= 0 && $endPos >= 0);
809
            if (!$this->origTokens->haveBraces($startPos, $endPos)) {
5✔
810
                // This was a single statement without braces, but either additional statements
811
                // have been added, or the single statement has been removed. This requires the
812
                // addition of braces. For now fall back.
813
                // TODO: Try to preserve formatting
814
                return null;
1✔
815
            }
816
        }
817

818
        $result = '';
395✔
819
        foreach ($diff as $i => $diffElem) {
395✔
820
            $diffType = $diffElem->type;
394✔
821
            /** @var Node|string|null $arrItem */
822
            $arrItem = $diffElem->new;
394✔
823
            /** @var Node|string|null $origArrItem */
824
            $origArrItem = $diffElem->old;
394✔
825

826
            if ($diffType === DiffElem::TYPE_KEEP || $diffType === DiffElem::TYPE_REPLACE) {
394✔
827
                $beforeFirstKeepOrReplace = false;
394✔
828

829
                if ($origArrItem === null || $arrItem === null) {
394✔
830
                    // We can only handle the case where both are null
831
                    if ($origArrItem === $arrItem) {
7✔
832
                        continue;
7✔
833
                    }
UNCOV
834
                    return null;
×
835
                }
836

837
                if (!$arrItem instanceof Node || !$origArrItem instanceof Node) {
394✔
838
                    // We can only deal with nodes. This can occur for Names, which use string arrays.
UNCOV
839
                    return null;
×
840
                }
841

842
                $itemStartPos = $origArrItem->getStartTokenPos();
394✔
843
                $itemEndPos = $origArrItem->getEndTokenPos();
394✔
844
                \assert($itemStartPos >= 0 && $itemEndPos >= 0 && $itemStartPos >= $pos);
845

846
                $origIndentLevel = $this->indentLevel;
394✔
847
                $lastElemIndentLevel = max($this->origTokens->getIndentationBefore($itemStartPos) + $indentAdjustment, 0);
394✔
848
                $this->setIndentLevel($lastElemIndentLevel);
394✔
849

850
                $comments = $arrItem->getComments();
394✔
851
                $origComments = $origArrItem->getComments();
394✔
852
                $commentStartPos = $origComments ? $origComments[0]->getStartTokenPos() : $itemStartPos;
394✔
853
                \assert($commentStartPos >= 0);
854

855
                if ($commentStartPos < $pos) {
394✔
856
                    // Comments may be assigned to multiple nodes if they start at the same position.
857
                    // Make sure we don't try to print them multiple times.
UNCOV
858
                    $commentStartPos = $itemStartPos;
×
859
                }
860

861
                if ($skipRemovedNode) {
394✔
862
                    if ($isStmtList && $this->origTokens->haveTagInRange($pos, $itemStartPos)) {
10✔
863
                        // We'd remove an opening/closing PHP tag.
864
                        // TODO: Preserve formatting.
865
                        $this->setIndentLevel($origIndentLevel);
1✔
866
                        return null;
10✔
867
                    }
868
                } else {
869
                    $result .= $this->origTokens->getTokenCode(
391✔
870
                        $pos, $commentStartPos, $indentAdjustment);
391✔
871
                }
872

873
                if (!empty($delayedAdd)) {
394✔
874
                    /** @var Node $delayedAddNode */
875
                    foreach ($delayedAdd as $delayedAddNode) {
9✔
876
                        if ($insertNewline) {
9✔
877
                            $delayedAddComments = $delayedAddNode->getComments();
8✔
878
                            if ($delayedAddComments) {
8✔
879
                                $result .= $this->pComments($delayedAddComments) . $this->nl;
4✔
880
                            }
881
                        }
882

883
                        $this->safeAppend($result, $this->p($delayedAddNode, self::MAX_PRECEDENCE, self::MAX_PRECEDENCE, true));
9✔
884

885
                        if ($insertNewline) {
9✔
886
                            $result .= $insertStr . $this->nl;
8✔
887
                        } else {
888
                            $result .= $insertStr;
1✔
889
                        }
890
                    }
891

892
                    $delayedAdd = [];
9✔
893
                }
894

895
                if ($comments !== $origComments) {
394✔
896
                    if ($comments) {
9✔
897
                        $result .= $this->pComments($comments) . $this->nl;
9✔
898
                    }
899
                } else {
900
                    $result .= $this->origTokens->getTokenCode(
392✔
901
                        $commentStartPos, $itemStartPos, $indentAdjustment);
392✔
902
                }
903

904
                // If we had to remove anything, we have done so now.
905
                $skipRemovedNode = false;
394✔
906
            } elseif ($diffType === DiffElem::TYPE_ADD) {
77✔
907
                if (null === $insertStr) {
58✔
908
                    // We don't have insertion information for this list type
909
                    return null;
1✔
910
                }
911

912
                if (!$arrItem instanceof Node) {
57✔
913
                    // We only support list insertion of nodes.
914
                    return null;
1✔
915
                }
916

917
                // We go multiline if the original code was multiline,
918
                // or if it's an array item with a comment above it.
919
                // Match always uses multiline formatting.
920
                if ($insertStr === ', ' &&
56✔
921
                    ($this->isMultiline($origNodes) || $arrItem->getComments() ||
56✔
922
                     $parentNodeClass === Expr\Match_::class)
56✔
923
                ) {
924
                    $insertStr = ',';
10✔
925
                    $insertNewline = true;
10✔
926
                }
927

928
                if ($beforeFirstKeepOrReplace) {
56✔
929
                    // Will be inserted at the next "replace" or "keep" element
930
                    $delayedAdd[] = $arrItem;
17✔
931
                    continue;
17✔
932
                }
933

934
                $itemStartPos = $pos;
40✔
935
                $itemEndPos = $pos - 1;
40✔
936

937
                $origIndentLevel = $this->indentLevel;
40✔
938
                $this->setIndentLevel($lastElemIndentLevel);
40✔
939

940
                if ($insertNewline) {
40✔
941
                    $result .= $insertStr . $this->nl;
26✔
942
                    $comments = $arrItem->getComments();
26✔
943
                    if ($comments) {
26✔
944
                        $result .= $this->pComments($comments) . $this->nl;
26✔
945
                    }
946
                } else {
947
                    $result .= $insertStr;
40✔
948
                }
949
            } elseif ($diffType === DiffElem::TYPE_REMOVE) {
24✔
950
                if (!$origArrItem instanceof Node) {
24✔
951
                    // We only support removal for nodes
UNCOV
952
                    return null;
×
953
                }
954

955
                $itemStartPos = $origArrItem->getStartTokenPos();
24✔
956
                $itemEndPos = $origArrItem->getEndTokenPos();
24✔
957
                \assert($itemStartPos >= 0 && $itemEndPos >= 0);
958

959
                // Consider comments part of the node.
960
                $origComments = $origArrItem->getComments();
24✔
961
                if ($origComments) {
24✔
962
                    $itemStartPos = $origComments[0]->getStartTokenPos();
1✔
963
                }
964

965
                if ($i === 0) {
24✔
966
                    // If we're removing from the start, keep the tokens before the node and drop those after it,
967
                    // instead of the other way around.
968
                    $result .= $this->origTokens->getTokenCode(
11✔
969
                        $pos, $itemStartPos, $indentAdjustment);
11✔
970
                    $skipRemovedNode = true;
11✔
971
                } else {
972
                    if ($isStmtList && $this->origTokens->haveTagInRange($pos, $itemStartPos)) {
14✔
973
                        // We'd remove an opening/closing PHP tag.
974
                        // TODO: Preserve formatting.
975
                        return null;
3✔
976
                    }
977
                }
978

979
                $pos = $itemEndPos + 1;
21✔
980
                continue;
21✔
981
            } else {
UNCOV
982
                throw new \Exception("Shouldn't happen");
×
983
            }
984

985
            if (null !== $fixup && $arrItem->getAttribute('origNode') !== $origArrItem) {
394✔
986
                $res = $this->pFixup($fixup, $arrItem, null, $itemStartPos, $itemEndPos);
2✔
987
            } else {
988
                $res = $this->p($arrItem, self::MAX_PRECEDENCE, self::MAX_PRECEDENCE, true);
394✔
989
            }
990
            $this->safeAppend($result, $res);
394✔
991

992
            $this->setIndentLevel($origIndentLevel);
394✔
993
            $pos = $itemEndPos + 1;
394✔
994
        }
995

996
        if ($skipRemovedNode) {
395✔
997
            // TODO: Support removing single node.
998
            return null;
1✔
999
        }
1000

1001
        if (!empty($delayedAdd)) {
395✔
1002
            if (!isset($this->emptyListInsertionMap[$mapKey])) {
8✔
1003
                return null;
2✔
1004
            }
1005

1006
            list($findToken, $extraLeft, $extraRight) = $this->emptyListInsertionMap[$mapKey];
6✔
1007
            if (null !== $findToken) {
6✔
1008
                $insertPos = $this->origTokens->findRight($pos, $findToken) + 1;
4✔
1009
                $result .= $this->origTokens->getTokenCode($pos, $insertPos, $indentAdjustment);
4✔
1010
                $pos = $insertPos;
4✔
1011
            }
1012

1013
            $first = true;
6✔
1014
            $result .= $extraLeft;
6✔
1015
            foreach ($delayedAdd as $delayedAddNode) {
6✔
1016
                if (!$first) {
6✔
1017
                    $result .= $insertStr;
4✔
1018
                    if ($insertNewline) {
4✔
1019
                        $result .= $this->nl;
1✔
1020
                    }
1021
                }
1022
                $result .= $this->p($delayedAddNode, self::MAX_PRECEDENCE, self::MAX_PRECEDENCE, true);
6✔
1023
                $first = false;
6✔
1024
            }
1025
            $result .= $extraRight === "\n" ? $this->nl : $extraRight;
6✔
1026
        }
1027

1028
        return $result;
395✔
1029
    }
1030

1031
    /**
1032
     * Print node with fixups.
1033
     *
1034
     * Fixups here refer to the addition of extra parentheses, braces or other characters, that
1035
     * are required to preserve program semantics in a certain context (e.g. to maintain precedence
1036
     * or because only certain expressions are allowed in certain places).
1037
     *
1038
     * @param int $fixup Fixup type
1039
     * @param Node $subNode Subnode to print
1040
     * @param string|null $parentClass Class of parent node
1041
     * @param int $subStartPos Original start pos of subnode
1042
     * @param int $subEndPos Original end pos of subnode
1043
     *
1044
     * @return string Result of fixed-up print of subnode
1045
     */
1046
    protected function pFixup(int $fixup, Node $subNode, ?string $parentClass, int $subStartPos, int $subEndPos): string {
1047
        switch ($fixup) {
1048
            case self::FIXUP_PREC_LEFT:
5✔
1049
                // We use a conservative approximation where lhsPrecedence == precedence.
1050
                if (!$this->origTokens->haveParens($subStartPos, $subEndPos)) {
1✔
1051
                    $precedence = $this->precedenceMap[$parentClass][1];
1✔
1052
                    return $this->p($subNode, $precedence, $precedence);
1✔
1053
                }
1054
                break;
1✔
1055
            case self::FIXUP_PREC_RIGHT:
5✔
1056
                if (!$this->origTokens->haveParens($subStartPos, $subEndPos)) {
1✔
1057
                    $precedence = $this->precedenceMap[$parentClass][2];
1✔
1058
                    return $this->p($subNode, $precedence, $precedence);
1✔
1059
                }
UNCOV
1060
                break;
×
1061
            case self::FIXUP_PREC_UNARY:
4✔
1062
                if (!$this->origTokens->haveParens($subStartPos, $subEndPos)) {
1✔
1063
                    $precedence = $this->precedenceMap[$parentClass][0];
1✔
1064
                    return $this->p($subNode, $precedence, $precedence);
1✔
1065
                }
UNCOV
1066
                break;
×
1067
            case self::FIXUP_CALL_LHS:
3✔
1068
                if ($this->callLhsRequiresParens($subNode)
1✔
1069
                    && !$this->origTokens->haveParens($subStartPos, $subEndPos)
1✔
1070
                ) {
1071
                    return '(' . $this->p($subNode) . ')';
1✔
1072
                }
1073
                break;
1✔
1074
            case self::FIXUP_DEREF_LHS:
3✔
1075
                if ($this->dereferenceLhsRequiresParens($subNode)
1✔
1076
                    && !$this->origTokens->haveParens($subStartPos, $subEndPos)
1✔
1077
                ) {
1078
                    return '(' . $this->p($subNode) . ')';
1✔
1079
                }
1080
                break;
1✔
1081
            case self::FIXUP_STATIC_DEREF_LHS:
3✔
1082
                if ($this->staticDereferenceLhsRequiresParens($subNode)
1✔
1083
                    && !$this->origTokens->haveParens($subStartPos, $subEndPos)
1✔
1084
                ) {
1085
                    return '(' . $this->p($subNode) . ')';
1✔
1086
                }
UNCOV
1087
                break;
×
1088
            case self::FIXUP_NEW:
3✔
1089
                if ($this->newOperandRequiresParens($subNode)
1✔
1090
                    && !$this->origTokens->haveParens($subStartPos, $subEndPos)) {
1✔
1091
                    return '(' . $this->p($subNode) . ')';
1✔
1092
                }
UNCOV
1093
                break;
×
1094
            case self::FIXUP_BRACED_NAME:
3✔
1095
            case self::FIXUP_VAR_BRACED_NAME:
3✔
1096
                if ($subNode instanceof Expr
1✔
1097
                    && !$this->origTokens->haveBraces($subStartPos, $subEndPos)
1✔
1098
                ) {
1099
                    return ($fixup === self::FIXUP_VAR_BRACED_NAME ? '$' : '')
1✔
1100
                        . '{' . $this->p($subNode) . '}';
1✔
1101
                }
1102
                break;
1✔
1103
            case self::FIXUP_ENCAPSED:
2✔
1104
                if (!$subNode instanceof Node\InterpolatedStringPart
2✔
1105
                    && !$this->origTokens->haveBraces($subStartPos, $subEndPos)
2✔
1106
                ) {
1107
                    return '{' . $this->p($subNode) . '}';
1✔
1108
                }
1109
                break;
1✔
1110
            default:
UNCOV
1111
                throw new \Exception('Cannot happen');
×
1112
        }
1113

1114
        // Nothing special to do
1115
        return $this->p($subNode);
3✔
1116
    }
1117

1118
    /**
1119
     * Appends to a string, ensuring whitespace between label characters.
1120
     *
1121
     * Example: "echo" and "$x" result in "echo$x", but "echo" and "x" result in "echo x".
1122
     * Without safeAppend the result would be "echox", which does not preserve semantics.
1123
     */
1124
    protected function safeAppend(string &$str, string $append): void {
1125
        if ($str === "") {
394✔
1126
            $str = $append;
313✔
1127
            return;
313✔
1128
        }
1129

1130
        if ($append === "") {
394✔
1131
            return;
24✔
1132
        }
1133

1134
        if (!$this->labelCharMap[$append[0]]
393✔
1135
                || !$this->labelCharMap[$str[\strlen($str) - 1]]) {
393✔
1136
            $str .= $append;
393✔
1137
        } else {
1138
            $str .= " " . $append;
1✔
1139
        }
1140
    }
1141

1142
    /**
1143
     * Determines whether the LHS of a call must be wrapped in parenthesis.
1144
     *
1145
     * @param Node $node LHS of a call
1146
     *
1147
     * @return bool Whether parentheses are required
1148
     */
1149
    protected function callLhsRequiresParens(Node $node): bool {
1150
        return !($node instanceof Node\Name
15✔
1151
            || $node instanceof Expr\Variable
15✔
1152
            || $node instanceof Expr\ArrayDimFetch
15✔
1153
            || $node instanceof Expr\FuncCall
15✔
1154
            || $node instanceof Expr\MethodCall
15✔
1155
            || $node instanceof Expr\NullsafeMethodCall
15✔
1156
            || $node instanceof Expr\StaticCall
15✔
1157
            || $node instanceof Expr\Array_);
15✔
1158
    }
1159

1160
    /**
1161
     * Determines whether the LHS of an array/object operation must be wrapped in parentheses.
1162
     *
1163
     * @param Node $node LHS of dereferencing operation
1164
     *
1165
     * @return bool Whether parentheses are required
1166
     */
1167
    protected function dereferenceLhsRequiresParens(Node $node): bool {
1168
        // A constant can occur on the LHS of an array/object deref, but not a static deref.
1169
        return $this->staticDereferenceLhsRequiresParens($node)
17✔
1170
            && !$node instanceof Expr\ConstFetch;
17✔
1171
    }
1172

1173
    /**
1174
     * Determines whether the LHS of a static operation must be wrapped in parentheses.
1175
     *
1176
     * @param Node $node LHS of dereferencing operation
1177
     *
1178
     * @return bool Whether parentheses are required
1179
     */
1180
    protected function staticDereferenceLhsRequiresParens(Node $node): bool {
1181
        return !($node instanceof Expr\Variable
20✔
1182
            || $node instanceof Node\Name
20✔
1183
            || $node instanceof Expr\ArrayDimFetch
20✔
1184
            || $node instanceof Expr\PropertyFetch
20✔
1185
            || $node instanceof Expr\NullsafePropertyFetch
20✔
1186
            || $node instanceof Expr\StaticPropertyFetch
20✔
1187
            || $node instanceof Expr\FuncCall
20✔
1188
            || $node instanceof Expr\MethodCall
20✔
1189
            || $node instanceof Expr\NullsafeMethodCall
20✔
1190
            || $node instanceof Expr\StaticCall
20✔
1191
            || $node instanceof Expr\Array_
20✔
1192
            || $node instanceof Scalar\String_
20✔
1193
            || $node instanceof Expr\ClassConstFetch);
20✔
1194
    }
1195

1196
    /**
1197
     * Determines whether an expression used in "new" or "instanceof" requires parentheses.
1198
     *
1199
     * @param Node $node New or instanceof operand
1200
     *
1201
     * @return bool Whether parentheses are required
1202
     */
1203
    protected function newOperandRequiresParens(Node $node): bool {
1204
        if ($node instanceof Node\Name || $node instanceof Expr\Variable) {
8✔
1205
            return false;
6✔
1206
        }
1207
        if ($node instanceof Expr\ArrayDimFetch || $node instanceof Expr\PropertyFetch ||
4✔
1208
            $node instanceof Expr\NullsafePropertyFetch
4✔
1209
        ) {
1210
            return $this->newOperandRequiresParens($node->var);
3✔
1211
        }
1212
        if ($node instanceof Expr\StaticPropertyFetch) {
2✔
1213
            return $this->newOperandRequiresParens($node->class);
1✔
1214
        }
1215
        return true;
2✔
1216
    }
1217

1218
    /**
1219
     * Print modifiers, including trailing whitespace.
1220
     *
1221
     * @param int $modifiers Modifier mask to print
1222
     *
1223
     * @return string Printed modifiers
1224
     */
1225
    protected function pModifiers(int $modifiers): string {
1226
        return ($modifiers & Modifiers::FINAL ? 'final ' : '')
44✔
1227
             . ($modifiers & Modifiers::ABSTRACT ? 'abstract ' : '')
44✔
1228
             . ($modifiers & Modifiers::PUBLIC ? 'public ' : '')
44✔
1229
             . ($modifiers & Modifiers::PROTECTED ? 'protected ' : '')
44✔
1230
             . ($modifiers & Modifiers::PRIVATE ? 'private ' : '')
44✔
1231
             . ($modifiers & Modifiers::PUBLIC_SET ? 'public(set) ' : '')
44✔
1232
             . ($modifiers & Modifiers::PROTECTED_SET ? 'protected(set) ' : '')
44✔
1233
             . ($modifiers & Modifiers::PRIVATE_SET ? 'private(set) ' : '')
44✔
1234
             . ($modifiers & Modifiers::STATIC ? 'static ' : '')
44✔
1235
             . ($modifiers & Modifiers::READONLY ? 'readonly ' : '');
44✔
1236
    }
1237

1238
    protected function pStatic(bool $static): string {
1239
        return $static ? 'static ' : '';
12✔
1240
    }
1241

1242
    /**
1243
     * Determine whether a list of nodes uses multiline formatting.
1244
     *
1245
     * @param (Node|null)[] $nodes Node list
1246
     *
1247
     * @return bool Whether multiline formatting is used
1248
     */
1249
    protected function isMultiline(array $nodes): bool {
1250
        if (\count($nodes) < 2) {
24✔
1251
            return false;
15✔
1252
        }
1253

1254
        $pos = -1;
9✔
1255
        foreach ($nodes as $node) {
9✔
1256
            if (null === $node) {
9✔
UNCOV
1257
                continue;
×
1258
            }
1259

1260
            $endPos = $node->getEndTokenPos() + 1;
9✔
1261
            if ($pos >= 0) {
9✔
1262
                $text = $this->origTokens->getTokenCode($pos, $endPos, 0);
9✔
1263
                if (false === strpos($text, "\n")) {
9✔
1264
                    // We require that a newline is present between *every* item. If the formatting
1265
                    // is inconsistent, with only some items having newlines, we don't consider it
1266
                    // as multiline
1267
                    return false;
4✔
1268
                }
1269
            }
1270
            $pos = $endPos;
9✔
1271
        }
1272

1273
        return true;
7✔
1274
    }
1275

1276
    /**
1277
     * Lazily initializes label char map.
1278
     *
1279
     * The label char map determines whether a certain character may occur in a label.
1280
     */
1281
    protected function initializeLabelCharMap(): void {
1282
        if (isset($this->labelCharMap)) {
395✔
UNCOV
1283
            return;
×
1284
        }
1285

1286
        $this->labelCharMap = [];
395✔
1287
        for ($i = 0; $i < 256; $i++) {
395✔
1288
            $chr = chr($i);
395✔
1289
            $this->labelCharMap[$chr] = $i >= 0x80 || ctype_alnum($chr);
395✔
1290
        }
1291

1292
        if ($this->phpVersion->allowsDelInIdentifiers()) {
395✔
1293
            $this->labelCharMap["\x7f"] = true;
6✔
1294
        }
1295
    }
1296

1297
    /**
1298
     * Lazily initializes node list differ.
1299
     *
1300
     * The node list differ is used to determine differences between two array subnodes.
1301
     */
1302
    protected function initializeNodeListDiffer(): void {
1303
        if (isset($this->nodeListDiffer)) {
395✔
UNCOV
1304
            return;
×
1305
        }
1306

1307
        $this->nodeListDiffer = new Internal\Differ(function ($a, $b) {
395✔
1308
            if ($a instanceof Node && $b instanceof Node) {
394✔
1309
                return $a === $b->getAttribute('origNode');
394✔
1310
            }
1311
            // Can happen for array destructuring
1312
            return $a === null && $b === null;
8✔
1313
        });
395✔
1314
    }
1315

1316
    /**
1317
     * Lazily initializes fixup map.
1318
     *
1319
     * The fixup map is used to determine whether a certain subnode of a certain node may require
1320
     * some kind of "fixup" operation, e.g. the addition of parenthesis or braces.
1321
     */
1322
    protected function initializeFixupMap(): void {
1323
        if (isset($this->fixupMap)) {
395✔
UNCOV
1324
            return;
×
1325
        }
1326

1327
        $this->fixupMap = [
395✔
1328
            Expr\Instanceof_::class => [
395✔
1329
                'expr' => self::FIXUP_PREC_UNARY,
395✔
1330
                'class' => self::FIXUP_NEW,
395✔
1331
            ],
395✔
1332
            Expr\Ternary::class => [
395✔
1333
                'cond' => self::FIXUP_PREC_LEFT,
395✔
1334
                'else' => self::FIXUP_PREC_RIGHT,
395✔
1335
            ],
395✔
1336
            Expr\Yield_::class => ['value' => self::FIXUP_PREC_UNARY],
395✔
1337

1338
            Expr\FuncCall::class => ['name' => self::FIXUP_CALL_LHS],
395✔
1339
            Expr\StaticCall::class => ['class' => self::FIXUP_STATIC_DEREF_LHS],
395✔
1340
            Expr\ArrayDimFetch::class => ['var' => self::FIXUP_DEREF_LHS],
395✔
1341
            Expr\ClassConstFetch::class => [
395✔
1342
                'class' => self::FIXUP_STATIC_DEREF_LHS,
395✔
1343
                'name' => self::FIXUP_BRACED_NAME,
395✔
1344
            ],
395✔
1345
            Expr\New_::class => ['class' => self::FIXUP_NEW],
395✔
1346
            Expr\MethodCall::class => [
395✔
1347
                'var' => self::FIXUP_DEREF_LHS,
395✔
1348
                'name' => self::FIXUP_BRACED_NAME,
395✔
1349
            ],
395✔
1350
            Expr\NullsafeMethodCall::class => [
395✔
1351
                'var' => self::FIXUP_DEREF_LHS,
395✔
1352
                'name' => self::FIXUP_BRACED_NAME,
395✔
1353
            ],
395✔
1354
            Expr\StaticPropertyFetch::class => [
395✔
1355
                'class' => self::FIXUP_STATIC_DEREF_LHS,
395✔
1356
                'name' => self::FIXUP_VAR_BRACED_NAME,
395✔
1357
            ],
395✔
1358
            Expr\PropertyFetch::class => [
395✔
1359
                'var' => self::FIXUP_DEREF_LHS,
395✔
1360
                'name' => self::FIXUP_BRACED_NAME,
395✔
1361
            ],
395✔
1362
            Expr\NullsafePropertyFetch::class => [
395✔
1363
                'var' => self::FIXUP_DEREF_LHS,
395✔
1364
                'name' => self::FIXUP_BRACED_NAME,
395✔
1365
            ],
395✔
1366
            Scalar\InterpolatedString::class => [
395✔
1367
                'parts' => self::FIXUP_ENCAPSED,
395✔
1368
            ],
395✔
1369
        ];
395✔
1370

1371
        $binaryOps = [
395✔
1372
            BinaryOp\Pow::class, BinaryOp\Mul::class, BinaryOp\Div::class, BinaryOp\Mod::class,
395✔
1373
            BinaryOp\Plus::class, BinaryOp\Minus::class, BinaryOp\Concat::class,
395✔
1374
            BinaryOp\ShiftLeft::class, BinaryOp\ShiftRight::class, BinaryOp\Smaller::class,
395✔
1375
            BinaryOp\SmallerOrEqual::class, BinaryOp\Greater::class, BinaryOp\GreaterOrEqual::class,
395✔
1376
            BinaryOp\Equal::class, BinaryOp\NotEqual::class, BinaryOp\Identical::class,
395✔
1377
            BinaryOp\NotIdentical::class, BinaryOp\Spaceship::class, BinaryOp\BitwiseAnd::class,
395✔
1378
            BinaryOp\BitwiseXor::class, BinaryOp\BitwiseOr::class, BinaryOp\BooleanAnd::class,
395✔
1379
            BinaryOp\BooleanOr::class, BinaryOp\Coalesce::class, BinaryOp\LogicalAnd::class,
395✔
1380
            BinaryOp\LogicalXor::class, BinaryOp\LogicalOr::class, BinaryOp\Pipe::class,
395✔
1381
        ];
395✔
1382
        foreach ($binaryOps as $binaryOp) {
395✔
1383
            $this->fixupMap[$binaryOp] = [
395✔
1384
                'left' => self::FIXUP_PREC_LEFT,
395✔
1385
                'right' => self::FIXUP_PREC_RIGHT
395✔
1386
            ];
395✔
1387
        }
1388

1389
        $prefixOps = [
395✔
1390
            Expr\Clone_::class, Expr\BitwiseNot::class, Expr\BooleanNot::class, Expr\UnaryPlus::class, Expr\UnaryMinus::class,
395✔
1391
            Cast\Int_::class, Cast\Double::class, Cast\String_::class, Cast\Array_::class,
395✔
1392
            Cast\Object_::class, Cast\Bool_::class, Cast\Unset_::class, Expr\ErrorSuppress::class,
395✔
1393
            Expr\YieldFrom::class, Expr\Print_::class, Expr\Include_::class,
395✔
1394
            Expr\Assign::class, Expr\AssignRef::class, AssignOp\Plus::class, AssignOp\Minus::class,
395✔
1395
            AssignOp\Mul::class, AssignOp\Div::class, AssignOp\Concat::class, AssignOp\Mod::class,
395✔
1396
            AssignOp\BitwiseAnd::class, AssignOp\BitwiseOr::class, AssignOp\BitwiseXor::class,
395✔
1397
            AssignOp\ShiftLeft::class, AssignOp\ShiftRight::class, AssignOp\Pow::class, AssignOp\Coalesce::class,
395✔
1398
            Expr\ArrowFunction::class, Expr\Throw_::class,
395✔
1399
        ];
395✔
1400
        foreach ($prefixOps as $prefixOp) {
395✔
1401
            $this->fixupMap[$prefixOp] = ['expr' => self::FIXUP_PREC_UNARY];
395✔
1402
        }
1403
    }
1404

1405
    /**
1406
     * Lazily initializes the removal map.
1407
     *
1408
     * The removal map is used to determine which additional tokens should be removed when a
1409
     * certain node is replaced by null.
1410
     */
1411
    protected function initializeRemovalMap(): void {
1412
        if (isset($this->removalMap)) {
395✔
UNCOV
1413
            return;
×
1414
        }
1415

1416
        $stripBoth = ['left' => \T_WHITESPACE, 'right' => \T_WHITESPACE];
395✔
1417
        $stripLeft = ['left' => \T_WHITESPACE];
395✔
1418
        $stripRight = ['right' => \T_WHITESPACE];
395✔
1419
        $stripDoubleArrow = ['right' => \T_DOUBLE_ARROW];
395✔
1420
        $stripColon = ['left' => ':'];
395✔
1421
        $stripEquals = ['left' => '='];
395✔
1422
        $this->removalMap = [
395✔
1423
            'Expr_ArrayDimFetch->dim' => $stripBoth,
395✔
1424
            'ArrayItem->key' => $stripDoubleArrow,
395✔
1425
            'Expr_ArrowFunction->returnType' => $stripColon,
395✔
1426
            'Expr_Closure->returnType' => $stripColon,
395✔
1427
            'Expr_Exit->expr' => $stripBoth,
395✔
1428
            'Expr_Ternary->if' => $stripBoth,
395✔
1429
            'Expr_Yield->key' => $stripDoubleArrow,
395✔
1430
            'Expr_Yield->value' => $stripBoth,
395✔
1431
            'Param->type' => $stripRight,
395✔
1432
            'Param->default' => $stripEquals,
395✔
1433
            'Stmt_Break->num' => $stripBoth,
395✔
1434
            'Stmt_Catch->var' => $stripLeft,
395✔
1435
            'Stmt_ClassConst->type' => $stripRight,
395✔
1436
            'Stmt_ClassMethod->returnType' => $stripColon,
395✔
1437
            'Stmt_Class->extends' => ['left' => \T_EXTENDS],
395✔
1438
            'Stmt_Enum->scalarType' => $stripColon,
395✔
1439
            'Stmt_EnumCase->expr' => $stripEquals,
395✔
1440
            'Expr_PrintableNewAnonClass->extends' => ['left' => \T_EXTENDS],
395✔
1441
            'Stmt_Continue->num' => $stripBoth,
395✔
1442
            'Stmt_Foreach->keyVar' => $stripDoubleArrow,
395✔
1443
            'Stmt_Function->returnType' => $stripColon,
395✔
1444
            'Stmt_If->else' => $stripLeft,
395✔
1445
            'Stmt_Namespace->name' => $stripLeft,
395✔
1446
            'Stmt_Property->type' => $stripRight,
395✔
1447
            'PropertyItem->default' => $stripEquals,
395✔
1448
            'Stmt_Return->expr' => $stripBoth,
395✔
1449
            'Stmt_StaticVar->default' => $stripEquals,
395✔
1450
            'Stmt_TraitUseAdaptation_Alias->newName' => $stripLeft,
395✔
1451
            'Stmt_TryCatch->finally' => $stripLeft,
395✔
1452
            // 'Stmt_Case->cond': Replace with "default"
395✔
1453
            // 'Stmt_Class->name': Unclear what to do
395✔
1454
            // 'Stmt_Declare->stmts': Not a plain node
395✔
1455
            // 'Stmt_TraitUseAdaptation_Alias->newModifier': Not a plain node
395✔
1456
        ];
395✔
1457
    }
1458

1459
    protected function initializeInsertionMap(): void {
1460
        if (isset($this->insertionMap)) {
395✔
UNCOV
1461
            return;
×
1462
        }
1463

1464
        // TODO: "yield" where both key and value are inserted doesn't work
1465
        // [$find, $beforeToken, $extraLeft, $extraRight]
1466
        $this->insertionMap = [
395✔
1467
            'Expr_ArrayDimFetch->dim' => ['[', false, null, null],
395✔
1468
            'ArrayItem->key' => [null, false, null, ' => '],
395✔
1469
            'Expr_ArrowFunction->returnType' => [')', false, ': ', null],
395✔
1470
            'Expr_Closure->returnType' => [')', false, ': ', null],
395✔
1471
            'Expr_Ternary->if' => ['?', false, ' ', ' '],
395✔
1472
            'Expr_Yield->key' => [\T_YIELD, false, null, ' => '],
395✔
1473
            'Expr_Yield->value' => [\T_YIELD, false, ' ', null],
395✔
1474
            'Param->type' => [null, false, null, ' '],
395✔
1475
            'Param->default' => [null, false, ' = ', null],
395✔
1476
            'Stmt_Break->num' => [\T_BREAK, false, ' ', null],
395✔
1477
            'Stmt_Catch->var' => [null, false, ' ', null],
395✔
1478
            'Stmt_ClassMethod->returnType' => [')', false, ': ', null],
395✔
1479
            'Stmt_ClassConst->type' => [\T_CONST, false, ' ', null],
395✔
1480
            'Stmt_Class->extends' => [null, false, ' extends ', null],
395✔
1481
            'Stmt_Enum->scalarType' => [null, false, ' : ', null],
395✔
1482
            'Stmt_EnumCase->expr' => [null, false, ' = ', null],
395✔
1483
            'Expr_PrintableNewAnonClass->extends' => [null, false, ' extends ', null],
395✔
1484
            'Stmt_Continue->num' => [\T_CONTINUE, false, ' ', null],
395✔
1485
            'Stmt_Foreach->keyVar' => [\T_AS, false, null, ' => '],
395✔
1486
            'Stmt_Function->returnType' => [')', false, ': ', null],
395✔
1487
            'Stmt_If->else' => [null, false, ' ', null],
395✔
1488
            'Stmt_Namespace->name' => [\T_NAMESPACE, false, ' ', null],
395✔
1489
            'Stmt_Property->type' => [\T_VARIABLE, true, null, ' '],
395✔
1490
            'PropertyItem->default' => [null, false, ' = ', null],
395✔
1491
            'Stmt_Return->expr' => [\T_RETURN, false, ' ', null],
395✔
1492
            'Stmt_StaticVar->default' => [null, false, ' = ', null],
395✔
1493
            //'Stmt_TraitUseAdaptation_Alias->newName' => [T_AS, false, ' ', null], // TODO
1494
            'Stmt_TryCatch->finally' => [null, false, ' ', null],
395✔
1495

1496
            // 'Expr_Exit->expr': Complicated due to optional ()
395✔
1497
            // 'Stmt_Case->cond': Conversion from default to case
395✔
1498
            // 'Stmt_Class->name': Unclear
395✔
1499
            // 'Stmt_Declare->stmts': Not a proper node
395✔
1500
            // 'Stmt_TraitUseAdaptation_Alias->newModifier': Not a proper node
395✔
1501
        ];
395✔
1502
    }
1503

1504
    protected function initializeListInsertionMap(): void {
1505
        if (isset($this->listInsertionMap)) {
395✔
UNCOV
1506
            return;
×
1507
        }
1508

1509
        $this->listInsertionMap = [
395✔
1510
            // special
1511
            //'Expr_ShellExec->parts' => '', // TODO These need to be treated more carefully
1512
            //'Scalar_InterpolatedString->parts' => '',
1513
            Stmt\Catch_::class . '->types' => '|',
395✔
1514
            UnionType::class . '->types' => '|',
395✔
1515
            IntersectionType::class . '->types' => '&',
395✔
1516
            Stmt\If_::class . '->elseifs' => ' ',
395✔
1517
            Stmt\TryCatch::class . '->catches' => ' ',
395✔
1518

1519
            // comma-separated lists
1520
            Expr\Array_::class . '->items' => ', ',
395✔
1521
            Expr\ArrowFunction::class . '->params' => ', ',
395✔
1522
            Expr\Closure::class . '->params' => ', ',
395✔
1523
            Expr\Closure::class . '->uses' => ', ',
395✔
1524
            Expr\FuncCall::class . '->args' => ', ',
395✔
1525
            Expr\Isset_::class . '->vars' => ', ',
395✔
1526
            Expr\List_::class . '->items' => ', ',
395✔
1527
            Expr\MethodCall::class . '->args' => ', ',
395✔
1528
            Expr\NullsafeMethodCall::class . '->args' => ', ',
395✔
1529
            Expr\New_::class . '->args' => ', ',
395✔
1530
            PrintableNewAnonClassNode::class . '->args' => ', ',
395✔
1531
            Expr\StaticCall::class . '->args' => ', ',
395✔
1532
            Stmt\ClassConst::class . '->consts' => ', ',
395✔
1533
            Stmt\ClassMethod::class . '->params' => ', ',
395✔
1534
            Stmt\Class_::class . '->implements' => ', ',
395✔
1535
            Stmt\Enum_::class . '->implements' => ', ',
395✔
1536
            PrintableNewAnonClassNode::class . '->implements' => ', ',
395✔
1537
            Stmt\Const_::class . '->consts' => ', ',
395✔
1538
            Stmt\Declare_::class . '->declares' => ', ',
395✔
1539
            Stmt\Echo_::class . '->exprs' => ', ',
395✔
1540
            Stmt\For_::class . '->init' => ', ',
395✔
1541
            Stmt\For_::class . '->cond' => ', ',
395✔
1542
            Stmt\For_::class . '->loop' => ', ',
395✔
1543
            Stmt\Function_::class . '->params' => ', ',
395✔
1544
            Stmt\Global_::class . '->vars' => ', ',
395✔
1545
            Stmt\GroupUse::class . '->uses' => ', ',
395✔
1546
            Stmt\Interface_::class . '->extends' => ', ',
395✔
1547
            Expr\Match_::class . '->arms' => ', ',
395✔
1548
            Stmt\Property::class . '->props' => ', ',
395✔
1549
            Stmt\StaticVar::class . '->vars' => ', ',
395✔
1550
            Stmt\TraitUse::class . '->traits' => ', ',
395✔
1551
            Stmt\TraitUseAdaptation\Precedence::class . '->insteadof' => ', ',
395✔
1552
            Stmt\Unset_::class .  '->vars' => ', ',
395✔
1553
            Stmt\UseUse::class . '->uses' => ', ',
395✔
1554
            MatchArm::class . '->conds' => ', ',
395✔
1555
            AttributeGroup::class . '->attrs' => ', ',
395✔
1556
            PropertyHook::class . '->params' => ', ',
395✔
1557

1558
            // statement lists
1559
            Expr\Closure::class . '->stmts' => "\n",
395✔
1560
            Stmt\Case_::class . '->stmts' => "\n",
395✔
1561
            Stmt\Catch_::class . '->stmts' => "\n",
395✔
1562
            Stmt\Class_::class . '->stmts' => "\n",
395✔
1563
            Stmt\Enum_::class . '->stmts' => "\n",
395✔
1564
            PrintableNewAnonClassNode::class . '->stmts' => "\n",
395✔
1565
            Stmt\Interface_::class . '->stmts' => "\n",
395✔
1566
            Stmt\Trait_::class . '->stmts' => "\n",
395✔
1567
            Stmt\ClassMethod::class . '->stmts' => "\n",
395✔
1568
            Stmt\Declare_::class . '->stmts' => "\n",
395✔
1569
            Stmt\Do_::class . '->stmts' => "\n",
395✔
1570
            Stmt\ElseIf_::class . '->stmts' => "\n",
395✔
1571
            Stmt\Else_::class . '->stmts' => "\n",
395✔
1572
            Stmt\Finally_::class . '->stmts' => "\n",
395✔
1573
            Stmt\Foreach_::class . '->stmts' => "\n",
395✔
1574
            Stmt\For_::class . '->stmts' => "\n",
395✔
1575
            Stmt\Function_::class . '->stmts' => "\n",
395✔
1576
            Stmt\If_::class . '->stmts' => "\n",
395✔
1577
            Stmt\Namespace_::class . '->stmts' => "\n",
395✔
1578
            Stmt\Block::class . '->stmts' => "\n",
395✔
1579

1580
            // Attribute groups
1581
            Stmt\Class_::class . '->attrGroups' => "\n",
395✔
1582
            Stmt\Enum_::class . '->attrGroups' => "\n",
395✔
1583
            Stmt\EnumCase::class . '->attrGroups' => "\n",
395✔
1584
            Stmt\Interface_::class . '->attrGroups' => "\n",
395✔
1585
            Stmt\Trait_::class . '->attrGroups' => "\n",
395✔
1586
            Stmt\Function_::class . '->attrGroups' => "\n",
395✔
1587
            Stmt\ClassMethod::class . '->attrGroups' => "\n",
395✔
1588
            Stmt\ClassConst::class . '->attrGroups' => "\n",
395✔
1589
            Stmt\Property::class . '->attrGroups' => "\n",
395✔
1590
            PrintableNewAnonClassNode::class . '->attrGroups' => ' ',
395✔
1591
            Expr\Closure::class . '->attrGroups' => ' ',
395✔
1592
            Expr\ArrowFunction::class . '->attrGroups' => ' ',
395✔
1593
            Param::class . '->attrGroups' => ' ',
395✔
1594
            PropertyHook::class . '->attrGroups' => ' ',
395✔
1595

1596
            Stmt\Switch_::class . '->cases' => "\n",
395✔
1597
            Stmt\TraitUse::class . '->adaptations' => "\n",
395✔
1598
            Stmt\TryCatch::class . '->stmts' => "\n",
395✔
1599
            Stmt\While_::class . '->stmts' => "\n",
395✔
1600
            PropertyHook::class . '->body' => "\n",
395✔
1601
            Stmt\Property::class . '->hooks' => "\n",
395✔
1602
            Param::class . '->hooks' => "\n",
395✔
1603

1604
            // dummy for top-level context
1605
            'File->stmts' => "\n",
395✔
1606
        ];
395✔
1607
    }
1608

1609
    protected function initializeEmptyListInsertionMap(): void {
1610
        if (isset($this->emptyListInsertionMap)) {
395✔
UNCOV
1611
            return;
×
1612
        }
1613

1614
        // TODO Insertion into empty statement lists.
1615

1616
        // [$find, $extraLeft, $extraRight]
1617
        $this->emptyListInsertionMap = [
395✔
1618
            Expr\ArrowFunction::class . '->params' => ['(', '', ''],
395✔
1619
            Expr\Closure::class . '->uses' => [')', ' use (', ')'],
395✔
1620
            Expr\Closure::class . '->params' => ['(', '', ''],
395✔
1621
            Expr\FuncCall::class . '->args' => ['(', '', ''],
395✔
1622
            Expr\MethodCall::class . '->args' => ['(', '', ''],
395✔
1623
            Expr\NullsafeMethodCall::class . '->args' => ['(', '', ''],
395✔
1624
            Expr\New_::class . '->args' => ['(', '', ''],
395✔
1625
            PrintableNewAnonClassNode::class . '->args' => ['(', '', ''],
395✔
1626
            PrintableNewAnonClassNode::class . '->implements' => [null, ' implements ', ''],
395✔
1627
            Expr\StaticCall::class . '->args' => ['(', '', ''],
395✔
1628
            Stmt\Class_::class . '->implements' => [null, ' implements ', ''],
395✔
1629
            Stmt\Enum_::class . '->implements' => [null, ' implements ', ''],
395✔
1630
            Stmt\ClassMethod::class . '->params' => ['(', '', ''],
395✔
1631
            Stmt\Interface_::class . '->extends' => [null, ' extends ', ''],
395✔
1632
            Stmt\Function_::class . '->params' => ['(', '', ''],
395✔
1633
            Stmt\Interface_::class . '->attrGroups' => [null, '', "\n"],
395✔
1634
            Stmt\Class_::class . '->attrGroups' => [null, '', "\n"],
395✔
1635
            Stmt\ClassConst::class . '->attrGroups' => [null, '', "\n"],
395✔
1636
            Stmt\ClassMethod::class . '->attrGroups' => [null, '', "\n"],
395✔
1637
            Stmt\Function_::class . '->attrGroups' => [null, '', "\n"],
395✔
1638
            Stmt\Property::class . '->attrGroups' => [null, '', "\n"],
395✔
1639
            Stmt\Trait_::class . '->attrGroups' => [null, '', "\n"],
395✔
1640
            Expr\ArrowFunction::class . '->attrGroups' => [null, '', ' '],
395✔
1641
            Expr\Closure::class . '->attrGroups' => [null, '', ' '],
395✔
1642
            Stmt\Const_::class . '->attrGroups' => [null, '', "\n"],
395✔
1643
            PrintableNewAnonClassNode::class . '->attrGroups' => [\T_NEW, ' ', ''],
395✔
1644

1645
            /* These cannot be empty to start with:
395✔
1646
             * Expr_Isset->vars
395✔
1647
             * Stmt_Catch->types
395✔
1648
             * Stmt_Const->consts
395✔
1649
             * Stmt_ClassConst->consts
395✔
1650
             * Stmt_Declare->declares
395✔
1651
             * Stmt_Echo->exprs
395✔
1652
             * Stmt_Global->vars
395✔
1653
             * Stmt_GroupUse->uses
395✔
1654
             * Stmt_Property->props
395✔
1655
             * Stmt_StaticVar->vars
395✔
1656
             * Stmt_TraitUse->traits
395✔
1657
             * Stmt_TraitUseAdaptation_Precedence->insteadof
395✔
1658
             * Stmt_Unset->vars
395✔
1659
             * Stmt_Use->uses
395✔
1660
             * UnionType->types
395✔
1661
             */
395✔
1662

1663
            /* TODO
395✔
1664
             * Stmt_If->elseifs
395✔
1665
             * Stmt_TryCatch->catches
395✔
1666
             * Expr_Array->items
395✔
1667
             * Expr_List->items
395✔
1668
             * Stmt_For->init
395✔
1669
             * Stmt_For->cond
395✔
1670
             * Stmt_For->loop
395✔
1671
             */
395✔
1672
        ];
395✔
1673
    }
1674

1675
    protected function initializeModifierChangeMap(): void {
1676
        if (isset($this->modifierChangeMap)) {
395✔
UNCOV
1677
            return;
×
1678
        }
1679

1680
        $this->modifierChangeMap = [
395✔
1681
            Stmt\ClassConst::class . '->flags' => ['pModifiers', \T_CONST],
395✔
1682
            Stmt\ClassMethod::class . '->flags' => ['pModifiers', \T_FUNCTION],
395✔
1683
            Stmt\Class_::class . '->flags' => ['pModifiers', \T_CLASS],
395✔
1684
            Stmt\Property::class . '->flags' => ['pModifiers', \T_VARIABLE],
395✔
1685
            PrintableNewAnonClassNode::class . '->flags' => ['pModifiers', \T_CLASS],
395✔
1686
            Param::class . '->flags' => ['pModifiers', \T_VARIABLE],
395✔
1687
            PropertyHook::class . '->flags' => ['pModifiers', \T_STRING],
395✔
1688
            Expr\Closure::class . '->static' => ['pStatic', \T_FUNCTION],
395✔
1689
            Expr\ArrowFunction::class . '->static' => ['pStatic', \T_FN],
395✔
1690
            //Stmt\TraitUseAdaptation\Alias::class . '->newModifier' => 0, // TODO
395✔
1691
        ];
395✔
1692

1693
        // List of integer subnodes that are not modifiers:
1694
        // Expr_Include->type
1695
        // Stmt_GroupUse->type
1696
        // Stmt_Use->type
1697
        // UseItem->type
1698
    }
1699
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc