PluginProbe
Code Engine – PHP Snippets, AI Functions & Automation for WordPress / 0.4.2
Code Engine – PHP Snippets, AI Functions & Automation for WordPress v0.4.2
0.5.7 0.5.6 0.5.5 0.5.4 0.5.3 0.5.2 0.5.1 0.5.0 0.4.9 0.4.8 0.4.7 0.4.6 trunk 0.0.1 0.0.2 0.2.8 0.2.9 0.3.0 0.3.1 0.3.2 0.3.3 0.3.4 0.3.5 0.3.6 0.3.7 All 33 releases
code-engine / vendor / nikic / php-parser / lib / PhpParser / PrettyPrinterAbstract.php

PrettyPrinterAbstract.php in Code Engine – PHP Snippets, AI Functions & Automation for WordPress 0.4.2, at vendor/nikic/php-parser/lib/PhpParser/PrettyPrinterAbstract.php

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