PluginProbe
WPIDE – File Manager & Code Editor / 3.5.9
WPIDE – File Manager & Code Editor v3.5.9
3.5.9 3.5.8 3.5.7 2.0.14 2.0.15 2.0.16 2.0.2 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 2.1 2.2 2.3 2.3.1 2.3.2 2.4.0 2.5 2.6 3.0 3.1 3.2 3.3 All 55 releases
wpide / vendor / nikic / php-parser / lib / PhpParser / PrettyPrinterAbstract.php

PrettyPrinterAbstract.php in WPIDE – File Manager & Code Editor 3.5.9, at vendor/nikic/php-parser/lib/PhpParser/PrettyPrinterAbstract.php

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