UNPKG

79.3 kBJavaScriptView Raw
1"use strict";
2
3const {
4 breakParent,
5 concat,
6 join,
7 line,
8 lineSuffix,
9 group,
10 conditionalGroup,
11 indent,
12 dedent,
13 ifBreak,
14 hardline,
15 softline,
16 literalline,
17 align,
18 dedentToRoot,
19} = require("prettier").doc.builders;
20const { willBreak } = require("prettier").doc.utils;
21const {
22 isNextLineEmptyAfterIndex,
23 hasNewline,
24 hasNewlineInRange,
25} = require("prettier").util;
26const comments = require("./comments");
27const pathNeedsParens = require("./needs-parens");
28
29const {
30 getLast,
31 getPenultimate,
32 isLastStatement,
33 lineShouldEndWithSemicolon,
34 printNumber,
35 shouldFlatten,
36 maybeStripLeadingSlashFromUse,
37 fileShouldEndWithHardline,
38 hasDanglingComments,
39 hasLeadingComment,
40 hasTrailingComment,
41 docShouldHaveTrailingNewline,
42 isLookupNode,
43 isFirstChildrenInlineNode,
44 shouldPrintHardLineAfterStartInControlStructure,
45 shouldPrintHardLineBeforeEndInControlStructure,
46 getAlignment,
47 isProgramLikeNode,
48 getNodeKindIncludingLogical,
49 useDoubleQuote,
50 hasEmptyBody,
51 isNextLineEmptyAfterNamespace,
52 shouldPrintHardlineBeforeTrailingComma,
53 isDocNode,
54 getAncestorNode,
55 isReferenceLikeNode,
56 getNextNode,
57 normalizeMagicMethodName,
58 getNextNonSpaceNonCommentCharacterIndex,
59 isNextLineEmpty,
60} = require("./util");
61
62function isMinVersion(actualVersion, requiredVersion) {
63 return parseFloat(actualVersion) >= parseFloat(requiredVersion);
64}
65
66function shouldPrintComma(options, requiredVersion) {
67 if (!options.trailingCommaPHP) {
68 return false;
69 }
70
71 return isMinVersion(options.phpVersion, requiredVersion);
72}
73
74function shouldPrintHardlineForOpenBrace(options) {
75 switch (options.braceStyle) {
76 case "1tbs":
77 return false;
78 case "psr-2":
79 default:
80 return true;
81 }
82}
83
84function genericPrint(path, options, print) {
85 const node = path.getValue();
86
87 if (!node) {
88 return "";
89 } else if (typeof node === "string") {
90 return node;
91 }
92
93 const printedWithoutParens = printNode(path, options, print);
94
95 const parts = [];
96 const needsParens = pathNeedsParens(path, options);
97
98 if (needsParens) {
99 parts.unshift("(");
100 }
101
102 parts.push(printedWithoutParens);
103
104 if (needsParens) {
105 parts.push(")");
106 }
107
108 if (lineShouldEndWithSemicolon(path)) {
109 parts.push(";");
110 }
111
112 if (fileShouldEndWithHardline(path)) {
113 parts.push(hardline);
114 }
115
116 return concat(parts);
117}
118
119function printPropertyLookup(path, options, print) {
120 return concat(["->", path.call(print, "offset")]);
121}
122
123function printStaticLookup(path, options, print) {
124 const node = path.getValue();
125 const needCurly = !["variable", "identifier"].includes(node.offset.kind);
126
127 return concat([
128 "::",
129 needCurly ? "{" : "",
130 path.call(print, "offset"),
131 needCurly ? "}" : "",
132 ]);
133}
134
135function printOffsetLookup(path, options, print) {
136 const node = path.getValue();
137 const shouldInline =
138 (node.offset && node.offset.kind === "number") ||
139 getAncestorNode(path, "encapsed");
140
141 return concat([
142 "[",
143 node.offset
144 ? group(
145 concat([
146 indent(
147 concat([shouldInline ? "" : softline, path.call(print, "offset")])
148 ),
149 shouldInline ? "" : softline,
150 ])
151 )
152 : "",
153 "]",
154 ]);
155}
156
157// We detect calls on member expressions specially to format a
158// common pattern better. The pattern we are looking for is this:
159//
160// $arr
161// ->map(function(x) { return $x + 1; })
162// ->filter(function(x) { return $x > 10; })
163// ->some(function(x) { return $x % 2; });
164//
165// The way it is structured in the AST is via a nested sequence of
166// propertylookup, staticlookup, offsetlookup and call.
167// We need to traverse the AST and make groups out of it
168// to print it in the desired way.
169function printMemberChain(path, options, print) {
170 // The first phase is to linearize the AST by traversing it down.
171 //
172 // Example:
173 // a()->b->c()->d();
174 // has the AST structure
175 // call (isLookupNode d (
176 // call (isLookupNode c (
177 // isLookupNode b (
178 // call (variable a)
179 // )
180 // ))
181 // ))
182 // and we transform it into (notice the reversed order)
183 // [identifier a, call, isLookupNode b, isLookupNode c, call,
184 // isLookupNode d, call]
185 const printedNodes = [];
186
187 // Here we try to retain one typed empty line after each call expression or
188 // the first group whether it is in parentheses or not
189 //
190 // Example:
191 // $a
192 // ->call()
193 //
194 // ->otherCall();
195 //
196 // ($foo ? $a : $b)
197 // ->call()
198 // ->otherCall();
199 function shouldInsertEmptyLineAfter(node) {
200 const { originalText } = options;
201 const nextCharIndex = getNextNonSpaceNonCommentCharacterIndex(
202 originalText,
203 node,
204 options
205 );
206 const nextChar = originalText.charAt(nextCharIndex);
207
208 // if it is cut off by a parenthesis, we only account for one typed empty
209 // line after that parenthesis
210 if (nextChar === ")") {
211 return isNextLineEmptyAfterIndex(
212 originalText,
213 nextCharIndex + 1,
214 options
215 );
216 }
217
218 return isNextLineEmpty(originalText, node, options);
219 }
220
221 function traverse(path) {
222 const node = path.getValue();
223
224 if (
225 node.kind === "call" &&
226 (isLookupNode(node.what) || node.what.kind === "call")
227 ) {
228 printedNodes.unshift({
229 node,
230 printed: concat([
231 comments.printAllComments(
232 path,
233 () => concat([printArgumentsList(path, options, print)]),
234 options
235 ),
236 shouldInsertEmptyLineAfter(node) ? hardline : "",
237 ]),
238 });
239 path.call((what) => traverse(what), "what");
240 } else if (isLookupNode(node)) {
241 // Print *lookup nodes as we standard print them outside member chain
242 let printedMemberish = null;
243
244 if (node.kind === "propertylookup") {
245 printedMemberish = printPropertyLookup(path, options, print);
246 } else if (node.kind === "staticlookup") {
247 printedMemberish = printStaticLookup(path, options, print);
248 } else {
249 printedMemberish = printOffsetLookup(path, options, print);
250 }
251
252 printedNodes.unshift({
253 node,
254 needsParens: pathNeedsParens(path, options),
255 printed: comments.printAllComments(
256 path,
257 () => printedMemberish,
258 options
259 ),
260 });
261 path.call((what) => traverse(what), "what");
262 } else {
263 printedNodes.unshift({
264 node,
265 printed: path.call(print),
266 });
267 }
268 }
269
270 const node = path.getValue();
271
272 printedNodes.unshift({
273 node,
274 printed: printArgumentsList(path, options, print),
275 });
276 path.call((what) => traverse(what), "what");
277
278 // Restore parens around `propertylookup` and `staticlookup` nodes with call.
279 // $value = ($object->foo)();
280 // $value = ($object::$foo)();
281 for (let i = 0; i < printedNodes.length; ++i) {
282 if (
283 printedNodes[i].node.kind === "call" &&
284 printedNodes[i - 1] &&
285 ["propertylookup", "staticlookup"].includes(
286 printedNodes[i - 1].node.kind
287 ) &&
288 printedNodes[i - 1].needsParens
289 ) {
290 printedNodes[0].printed = concat(["(", printedNodes[0].printed]);
291 printedNodes[i - 1].printed = concat([printedNodes[i - 1].printed, ")"]);
292 }
293 }
294
295 // create groups from list of nodes, i.e.
296 // [identifier a, call, isLookupNode b, isLookupNode c, call,
297 // isLookupNode d, call]
298 // will be grouped as
299 // [
300 // [identifier a, Call],
301 // [isLookupNode b, isLookupNode c, call],
302 // [isLookupNode d, call]
303 // ]
304 // so that we can print it as
305 // a()
306 // ->b->c()
307 // ->d();
308 const groups = [];
309
310 let currentGroup = [printedNodes[0]];
311 let i = 1;
312
313 for (; i < printedNodes.length; ++i) {
314 if (
315 printedNodes[i].node.kind === "call" ||
316 (isLookupNode(printedNodes[i].node) &&
317 printedNodes[i].node.offset &&
318 printedNodes[i].node.offset.kind === "number")
319 ) {
320 currentGroup.push(printedNodes[i]);
321 } else {
322 break;
323 }
324 }
325
326 if (printedNodes[0].node.kind !== "call") {
327 for (; i + 1 < printedNodes.length; ++i) {
328 if (
329 isLookupNode(printedNodes[i].node) &&
330 isLookupNode(printedNodes[i + 1].node)
331 ) {
332 currentGroup.push(printedNodes[i]);
333 } else {
334 break;
335 }
336 }
337 }
338
339 groups.push(currentGroup);
340 currentGroup = [];
341
342 // Then, each following group is a sequence of propertylookup followed by
343 // a sequence of call. To compute it, we keep adding things to the
344 // group until we have seen a call in the past and reach a
345 // propertylookup
346 let hasSeenCallExpression = false;
347
348 for (; i < printedNodes.length; ++i) {
349 if (hasSeenCallExpression && isLookupNode(printedNodes[i].node)) {
350 // [0] should be appended at the end of the group instead of the
351 // beginning of the next one
352 if (
353 printedNodes[i].node.kind === "offsetlookup" &&
354 printedNodes[i].node.offset &&
355 printedNodes[i].node.offset.kind === "number"
356 ) {
357 currentGroup.push(printedNodes[i]);
358 continue;
359 }
360
361 groups.push(currentGroup);
362 currentGroup = [];
363 hasSeenCallExpression = false;
364 }
365
366 if (printedNodes[i].node.kind === "call") {
367 hasSeenCallExpression = true;
368 }
369 currentGroup.push(printedNodes[i]);
370
371 if (
372 printedNodes[i].node.comments &&
373 comments.hasTrailingComment(printedNodes[i].node)
374 ) {
375 groups.push(currentGroup);
376 currentGroup = [];
377 hasSeenCallExpression = false;
378 }
379 }
380
381 if (currentGroup.length > 0) {
382 groups.push(currentGroup);
383 }
384
385 // Merge next nodes when:
386 //
387 // 1. We have `$this` variable before
388 //
389 // Example:
390 // $this->method()->property;
391 //
392 // 2. When we have offsetlookup after *lookup node
393 //
394 // Example:
395 // $foo->Data['key']("foo")
396 // ->method();
397 //
398
399 function shouldNotWrap(groups) {
400 const hasComputed =
401 groups[1].length && groups[1][0].node.kind === "offsetlookup";
402
403 if (groups[0].length === 1) {
404 const firstNode = groups[0][0].node;
405
406 return (
407 (firstNode.kind === "variable" && firstNode.name === "this") ||
408 isReferenceLikeNode(firstNode)
409 );
410 }
411
412 const lastNode = getLast(groups[0]).node;
413
414 return (
415 isLookupNode(lastNode) &&
416 (lastNode.offset.kind === "identifier" ||
417 lastNode.offset.kind === "variable") &&
418 hasComputed
419 );
420 }
421
422 const shouldMerge =
423 groups.length >= 2 && !groups[1][0].node.comments && shouldNotWrap(groups);
424
425 function printGroup(printedGroup) {
426 const result = [];
427
428 for (let i = 0; i < printedGroup.length; i++) {
429 // Checks if the next node (i.e. the parent node) needs parens
430 // and print accordingl y
431 if (printedGroup[i + 1] && printedGroup[i + 1].needsParens) {
432 result.push(
433 "(",
434 printedGroup[i].printed,
435 printedGroup[i + 1].printed,
436 ")"
437 );
438 i++;
439 } else {
440 result.push(printedGroup[i].printed);
441 }
442 }
443
444 return concat(result);
445 }
446
447 function printIndentedGroup(groups) {
448 if (groups.length === 0) {
449 return "";
450 }
451
452 return indent(
453 group(concat([hardline, join(hardline, groups.map(printGroup))]))
454 );
455 }
456
457 const printedGroups = groups.map(printGroup);
458 const oneLine = concat(printedGroups);
459
460 // Indicates how many we should merge
461 //
462 // Example (true):
463 // $this->method()->otherMethod(
464 // 'argument'
465 // );
466 //
467 // Example (false):
468 // $foo
469 // ->method()
470 // ->otherMethod();
471 const cutoff = shouldMerge ? 3 : 2;
472 const flatGroups = groups
473 .slice(0, cutoff)
474 .reduce((res, group) => res.concat(group), []);
475
476 const hasComment =
477 flatGroups
478 .slice(1, -1)
479 .some((node) => comments.hasLeadingComment(node.node)) ||
480 flatGroups
481 .slice(0, -1)
482 .some((node) => comments.hasTrailingComment(node.node)) ||
483 (groups[cutoff] && comments.hasLeadingComment(groups[cutoff][0].node));
484
485 const hasEncapsedAncestor = getAncestorNode(path, "encapsed");
486
487 // If we only have a single `->`, we shouldn't do anything fancy and just
488 // render everything concatenated together.
489 // In `encapsed` node we always print in one line.
490 if ((groups.length <= cutoff && !hasComment) || hasEncapsedAncestor) {
491 return group(oneLine);
492 }
493
494 // Find out the last node in the first group and check if it has an
495 // empty line after
496 const lastNodeBeforeIndent = getLast(
497 shouldMerge ? groups.slice(1, 2)[0] : groups[0]
498 ).node;
499 const shouldHaveEmptyLineBeforeIndent =
500 lastNodeBeforeIndent.kind !== "call" &&
501 shouldInsertEmptyLineAfter(lastNodeBeforeIndent);
502
503 const expanded = concat([
504 printGroup(groups[0]),
505 shouldMerge ? concat(groups.slice(1, 2).map(printGroup)) : "",
506 shouldHaveEmptyLineBeforeIndent ? hardline : "",
507 printIndentedGroup(groups.slice(shouldMerge ? 2 : 1)),
508 ]);
509
510 const callExpressionCount = printedNodes.filter(
511 (tuple) => tuple.node.kind === "call"
512 ).length;
513
514 // We don't want to print in one line if there's:
515 // * A comment.
516 // * 3 or more chained calls.
517 // * Any group but the last one has a hard line.
518 // If the last group is a function it's okay to inline if it fits.
519 if (
520 hasComment ||
521 callExpressionCount >= 3 ||
522 printedGroups.slice(0, -1).some(willBreak)
523 ) {
524 return group(expanded);
525 }
526
527 return concat([
528 // We only need to check `oneLine` because if `expanded` is chosen
529 // that means that the parent group has already been broken
530 // naturally
531 willBreak(oneLine) || shouldHaveEmptyLineBeforeIndent ? breakParent : "",
532 conditionalGroup([oneLine, expanded]),
533 ]);
534}
535
536function couldGroupArg(arg) {
537 return (
538 (arg.kind === "array" && (arg.items.length > 0 || arg.comments)) ||
539 arg.kind === "function" ||
540 arg.kind === "method" ||
541 arg.kind === "closure"
542 );
543}
544
545function shouldGroupLastArg(args) {
546 const lastArg = getLast(args);
547 const penultimateArg = getPenultimate(args);
548
549 return (
550 !hasLeadingComment(lastArg) &&
551 !hasTrailingComment(lastArg) &&
552 couldGroupArg(lastArg) &&
553 // If the last two arguments are of the same type,
554 // disable last element expansion.
555 (!penultimateArg || penultimateArg.kind !== lastArg.kind)
556 );
557}
558
559function shouldGroupFirstArg(args) {
560 if (args.length !== 2) {
561 return false;
562 }
563
564 const [firstArg, secondArg] = args;
565
566 return (
567 (!firstArg.comments || !firstArg.comments.length) &&
568 (firstArg.kind === "function" ||
569 firstArg.kind === "method" ||
570 firstArg.kind === "closure") &&
571 secondArg.kind !== "retif" &&
572 !couldGroupArg(secondArg)
573 );
574}
575
576function printArgumentsList(path, options, print, argumentsKey = "arguments") {
577 const args = path.getValue()[argumentsKey];
578
579 if (args.length === 0) {
580 return concat([
581 "(",
582 comments.printDanglingComments(path, options, /* sameIndent */ true),
583 ")",
584 ]);
585 }
586
587 let anyArgEmptyLine = false;
588 let hasEmptyLineFollowingFirstArg = false;
589
590 const lastArgIndex = args.length - 1;
591 const printedArguments = path.map((argPath, index) => {
592 const arg = argPath.getNode();
593 const parts = [print(argPath)];
594
595 if (index === lastArgIndex) {
596 // do nothing
597 } else if (isNextLineEmpty(options.originalText, arg, options)) {
598 if (index === 0) {
599 hasEmptyLineFollowingFirstArg = true;
600 }
601
602 anyArgEmptyLine = true;
603 parts.push(",", hardline, hardline);
604 } else {
605 parts.push(",", line);
606 }
607
608 return concat(parts);
609 }, argumentsKey);
610
611 const node = path.getValue();
612 const lastArg = getLast(args);
613
614 const maybeTrailingComma =
615 ["call", "new", "unset", "isset"].includes(node.kind) &&
616 shouldPrintComma(options, "7.3")
617 ? indent(
618 concat([
619 lastArg && shouldPrintHardlineBeforeTrailingComma(lastArg)
620 ? hardline
621 : "",
622 ",",
623 ])
624 )
625 : "";
626
627 function allArgsBrokenOut() {
628 return group(
629 concat([
630 "(",
631 indent(concat([line, concat(printedArguments)])),
632 maybeTrailingComma,
633 line,
634 ")",
635 ]),
636 { shouldBreak: true }
637 );
638 }
639
640 const shouldGroupFirst = shouldGroupFirstArg(args);
641 const shouldGroupLast = shouldGroupLastArg(args);
642
643 if (shouldGroupFirst || shouldGroupLast) {
644 const shouldBreak =
645 (shouldGroupFirst
646 ? printedArguments.slice(1).some(willBreak)
647 : printedArguments.slice(0, -1).some(willBreak)) || anyArgEmptyLine;
648
649 // We want to print the last argument with a special flag
650 let printedExpanded;
651 let i = 0;
652
653 path.each((argPath) => {
654 if (shouldGroupFirst && i === 0) {
655 printedExpanded = [
656 concat([
657 argPath.call((p) => print(p, { expandFirstArg: true })),
658 printedArguments.length > 1 ? "," : "",
659 hasEmptyLineFollowingFirstArg ? hardline : line,
660 hasEmptyLineFollowingFirstArg ? hardline : "",
661 ]),
662 ].concat(printedArguments.slice(1));
663 }
664
665 if (shouldGroupLast && i === args.length - 1) {
666 printedExpanded = printedArguments
667 .slice(0, -1)
668 .concat(argPath.call((p) => print(p, { expandLastArg: true })));
669 }
670
671 i++;
672 }, argumentsKey);
673
674 const somePrintedArgumentsWillBreak = printedArguments.some(willBreak);
675 const simpleConcat = concat(["(", concat(printedExpanded), ")"]);
676
677 return concat([
678 somePrintedArgumentsWillBreak ? breakParent : "",
679 conditionalGroup(
680 [
681 !somePrintedArgumentsWillBreak
682 ? simpleConcat
683 : ifBreak(allArgsBrokenOut(), simpleConcat),
684 shouldGroupFirst
685 ? concat([
686 "(",
687 group(printedExpanded[0], { shouldBreak: true }),
688 concat(printedExpanded.slice(1)),
689 ")",
690 ])
691 : concat([
692 "(",
693 concat(printedArguments.slice(0, -1)),
694 group(getLast(printedExpanded), {
695 shouldBreak: true,
696 }),
697 ")",
698 ]),
699 group(
700 concat([
701 "(",
702 indent(concat([line, concat(printedArguments)])),
703 ifBreak(maybeTrailingComma),
704 line,
705 ")",
706 ]),
707 { shouldBreak: true }
708 ),
709 ],
710 { shouldBreak }
711 ),
712 ]);
713 }
714
715 return group(
716 concat([
717 "(",
718 indent(concat([softline, concat(printedArguments)])),
719 ifBreak(maybeTrailingComma),
720 softline,
721 ")",
722 ]),
723 {
724 shouldBreak: printedArguments.some(willBreak) || anyArgEmptyLine,
725 }
726 );
727}
728
729function shouldInlineRetifFalseExpression(node) {
730 return node.kind === "array" && node.items.length !== 0;
731}
732
733function shouldInlineLogicalExpression(node) {
734 return node.right.kind === "array" && node.right.items.length !== 0;
735}
736
737// For binary expressions to be consistent, we need to group
738// subsequent operators with the same precedence level under a single
739// group. Otherwise they will be nested such that some of them break
740// onto new lines but not all. Operators with the same precedence
741// level should either all break or not. Because we group them by
742// precedence level and the AST is structured based on precedence
743// level, things are naturally broken up correctly, i.e. `&&` is
744// broken before `+`.
745function printBinaryExpression(
746 path,
747 print,
748 options,
749 isNested,
750 isInsideParenthesis
751) {
752 let parts = [];
753 const node = path.getValue();
754
755 if (node.kind === "bin") {
756 // Put all operators with the same precedence level in the same
757 // group. The reason we only need to do this with the `left`
758 // expression is because given an expression like `1 + 2 - 3`, it
759 // is always parsed like `((1 + 2) - 3)`, meaning the `left` side
760 // is where the rest of the expression will exist. Binary
761 // expressions on the right side mean they have a difference
762 // precedence level and should be treated as a separate group, so
763 // print them normally. (This doesn't hold for the `**` operator,
764 // which is unique in that it is right-associative.)
765 if (shouldFlatten(node.type, node.left.type)) {
766 // Flatten them out by recursively calling this function.
767 parts = parts.concat(
768 path.call(
769 (left) =>
770 printBinaryExpression(
771 left,
772 print,
773 options,
774 /* isNested */ true,
775 isInsideParenthesis
776 ),
777 "left"
778 )
779 );
780 } else {
781 parts.push(path.call(print, "left"));
782 }
783
784 const shouldInline = shouldInlineLogicalExpression(node);
785
786 const right = shouldInline
787 ? concat([node.type, " ", path.call(print, "right")])
788 : concat([node.type, line, path.call(print, "right")]);
789
790 // If there's only a single binary expression, we want to create a group
791 // in order to avoid having a small right part like -1 be on its own line.
792 const parent = path.getParentNode();
793 const shouldGroup =
794 !(isInsideParenthesis && ["||", "&&"].includes(node.type)) &&
795 getNodeKindIncludingLogical(parent) !==
796 getNodeKindIncludingLogical(node) &&
797 getNodeKindIncludingLogical(node.left) !==
798 getNodeKindIncludingLogical(node) &&
799 getNodeKindIncludingLogical(node.right) !==
800 getNodeKindIncludingLogical(node);
801
802 const shouldNotHaveWhitespace =
803 isDocNode(node.left) ||
804 (node.left.kind === "bin" && isDocNode(node.left.right));
805
806 parts.push(
807 shouldNotHaveWhitespace ? "" : " ",
808 shouldGroup ? group(right) : right
809 );
810
811 // The root comments are already printed, but we need to manually print
812 // the other ones since we don't call the normal print on bin,
813 // only for the left and right parts
814 if (isNested && node.comments) {
815 parts = comments.printAllComments(path, () => concat(parts), options);
816 }
817 } else {
818 // Our stopping case. Simply print the node normally.
819 parts.push(path.call(print));
820 }
821
822 return parts;
823}
824
825function printLookupNodes(path, options, print) {
826 const node = path.getValue();
827
828 switch (node.kind) {
829 case "propertylookup":
830 return printPropertyLookup(path, options, print);
831 case "staticlookup":
832 return printStaticLookup(path, options, print);
833 case "offsetlookup":
834 return printOffsetLookup(path, options, print);
835 /* istanbul ignore next */
836 default:
837 return `Have not implemented lookup kind ${node.kind} yet.`;
838 }
839}
840
841function getEncapsedQuotes(node, { opening = true } = {}) {
842 if (node.type === "heredoc") {
843 return opening ? `<<<${node.label}` : node.label;
844 }
845
846 const quotes = {
847 string: '"',
848 shell: "`",
849 };
850
851 if (quotes[node.type]) {
852 return quotes[node.type];
853 }
854
855 /* istanbul ignore next */
856 return `Unimplemented encapsed type ${node.type}`;
857}
858
859function printArrayItems(path, options, print) {
860 const printedElements = [];
861 let separatorParts = [];
862
863 path.each((childPath) => {
864 printedElements.push(concat(separatorParts));
865 printedElements.push(group(print(childPath)));
866
867 separatorParts = [",", line];
868
869 if (
870 childPath.getValue() &&
871 isNextLineEmpty(options.originalText, childPath.getValue(), options)
872 ) {
873 separatorParts.push(softline);
874 }
875 }, "items");
876
877 return concat(printedElements);
878}
879
880// Wrap parts into groups by indexes.
881// It is require to have same indent on lines for all parts into group.
882// The value of `alignment` option indicates how many spaces must be before each part.
883//
884// Example:
885// <div>
886// <?php
887// echo '1';
888// echo '2';
889// echo '3';
890// ?>
891// </div>
892function wrapPartsIntoGroups(parts, indexes) {
893 if (indexes.length === 0) {
894 return parts;
895 }
896
897 let lastEnd = 0;
898
899 return indexes.reduce((accumulator, index) => {
900 const { start, end, alignment, before, after } = index;
901 const printedPartsForGrouping = concat([
902 before || "",
903 concat(parts.slice(start, end)),
904 after || "",
905 ]);
906 const newArray = accumulator.concat(
907 parts.slice(lastEnd, start),
908 alignment
909 ? dedentToRoot(
910 group(
911 concat([
912 align(new Array(alignment).join(" "), printedPartsForGrouping),
913 ])
914 )
915 )
916 : group(printedPartsForGrouping),
917 end === parts.length - 1 ? parts.slice(end) : ""
918 );
919
920 lastEnd = end;
921
922 return newArray;
923 }, []);
924}
925
926function printLines(path, options, print, childrenAttribute = "children") {
927 const node = path.getValue();
928 const parentNode = path.getParentNode();
929
930 let lastInlineIndex = -1;
931
932 const parts = [];
933 const groupIndexes = [];
934
935 path.map((childPath, index) => {
936 const childNode = childPath.getValue();
937 const isInlineNode = childNode.kind === "inline";
938 const printedPath = print(childPath);
939 const children = node[childrenAttribute];
940 const nextNode = children[index + 1];
941 const canPrintBlankLine =
942 !isLastStatement(childPath) &&
943 !isInlineNode &&
944 (nextNode && nextNode.kind === "case"
945 ? !isFirstChildrenInlineNode(path)
946 : nextNode && nextNode.kind !== "inline");
947
948 let printed = concat([
949 printedPath,
950 canPrintBlankLine ? hardline : "",
951 canPrintBlankLine &&
952 isNextLineEmpty(options.originalText, childNode, options)
953 ? hardline
954 : "",
955 ]);
956
957 const isFirstNode = index === 0;
958 const isLastNode = children.length - 1 === index;
959 const isBlockNestedNode =
960 node.kind === "block" &&
961 parentNode &&
962 ["function", "closure", "method", "try", "catch"].includes(
963 parentNode.kind
964 );
965
966 let beforeCloseTagInlineNode = isBlockNestedNode && isFirstNode ? "" : " ";
967
968 if (isInlineNode || (!isInlineNode && isLastNode && lastInlineIndex >= 0)) {
969 const prevLastInlineIndex = lastInlineIndex;
970
971 if (isInlineNode) {
972 lastInlineIndex = index;
973 }
974
975 const shouldCreateGroup =
976 (isInlineNode && !isFirstNode) || (!isInlineNode && isLastNode);
977
978 if (shouldCreateGroup) {
979 const start =
980 (isInlineNode ? prevLastInlineIndex : lastInlineIndex) + 1;
981 const end = isLastNode && !isInlineNode ? index + 1 : index;
982 const prevInlineNode =
983 children[isInlineNode ? prevLastInlineIndex : lastInlineIndex];
984 const alignment = prevInlineNode
985 ? getAlignment(prevInlineNode.raw)
986 : "";
987 const shouldBreak = end - start > 1;
988 const before = shouldBreak
989 ? (isBlockNestedNode && !prevInlineNode) ||
990 (isProgramLikeNode(node) && start === 0)
991 ? ""
992 : hardline
993 : "";
994 const after =
995 shouldBreak && childNode.kind !== "halt"
996 ? isBlockNestedNode && isLastNode
997 ? ""
998 : hardline
999 : "";
1000
1001 if (shouldBreak) {
1002 beforeCloseTagInlineNode = "";
1003 }
1004
1005 groupIndexes.push({ start, end, alignment, before, after });
1006 }
1007 }
1008
1009 if (isInlineNode) {
1010 const openTag =
1011 nextNode && nextNode.kind === "echo" && nextNode.shortForm
1012 ? "<?="
1013 : "<?php";
1014 const beforeInline =
1015 childNode.leadingComments && childNode.leadingComments.length
1016 ? concat([
1017 isFirstNode && node.kind !== "namespace" && !isBlockNestedNode
1018 ? openTag
1019 : "",
1020 node.kind === "namespace" || !isBlockNestedNode ? hardline : "",
1021 comments.printComments(childNode.leadingComments, options),
1022 hardline,
1023 "?>",
1024 ])
1025 : isProgramLikeNode(node) && isFirstNode && node.kind !== "namespace"
1026 ? ""
1027 : concat([beforeCloseTagInlineNode, "?>"]);
1028 const afterInline =
1029 childNode.comments && childNode.comments.length
1030 ? concat([
1031 openTag,
1032 hardline,
1033 comments.printComments(childNode.comments, options),
1034 hardline,
1035 "?>",
1036 ])
1037 : isProgramLikeNode(node) && isLastNode
1038 ? ""
1039 : concat([openTag, " "]);
1040
1041 printed = concat([beforeInline, printed, afterInline]);
1042 }
1043
1044 parts.push(printed);
1045 }, childrenAttribute);
1046
1047 const wrappedParts = wrapPartsIntoGroups(parts, groupIndexes);
1048
1049 if (node.kind === "program" && !node.extra.parseAsEval) {
1050 const parts = [];
1051
1052 const [firstNode] = node.children;
1053 const hasStartTag = !firstNode || firstNode.kind !== "inline";
1054
1055 if (hasStartTag) {
1056 const between = options.originalText.trim().match(/^<\?(php|=)(\s+)?\S/);
1057 const afterOpenTag = concat([
1058 between && between[2] && between[2].includes("\n")
1059 ? concat([
1060 hardline,
1061 between[2].split("\n").length > 2 ? hardline : "",
1062 ])
1063 : " ",
1064 node.comments ? comments.printComments(node.comments, options) : "",
1065 ]);
1066
1067 const shortEcho =
1068 firstNode && firstNode.kind === "echo" && firstNode.shortForm;
1069 parts.push(concat([shortEcho ? "<?=" : "<?php", afterOpenTag]));
1070 }
1071
1072 parts.push(concat(wrappedParts));
1073
1074 const hasEndTag = options.originalText.trim().endsWith("?>");
1075
1076 if (hasEndTag) {
1077 const lastNode = getLast(node.children);
1078 const beforeCloseTag = lastNode
1079 ? concat([
1080 hasNewlineInRange(
1081 options.originalText,
1082 options.locEnd(lastNode),
1083 options.locEnd(node)
1084 )
1085 ? hardline
1086 : " ",
1087 isNextLineEmpty(options.originalText, lastNode, options)
1088 ? hardline
1089 : "",
1090 ])
1091 : node.comments
1092 ? hardline
1093 : "";
1094
1095 parts.push(lineSuffix(concat([beforeCloseTag, "?>"])));
1096 }
1097
1098 return concat(parts);
1099 }
1100
1101 return concat(wrappedParts);
1102}
1103
1104function printStatements(path, options, print, childrenAttribute) {
1105 return concat(
1106 path.map((childPath) => {
1107 const parts = [];
1108
1109 parts.push(print(childPath));
1110
1111 if (!isLastStatement(childPath)) {
1112 parts.push(hardline);
1113
1114 if (
1115 isNextLineEmpty(options.originalText, childPath.getValue(), options)
1116 ) {
1117 parts.push(hardline);
1118 }
1119 }
1120
1121 return concat(parts);
1122 }, childrenAttribute)
1123 );
1124}
1125
1126function printClassPart(
1127 path,
1128 options,
1129 print,
1130 part = "extends",
1131 beforePart = " ",
1132 afterPart = " "
1133) {
1134 const node = path.getValue();
1135 const printedBeforePart = hasDanglingComments(node[part])
1136 ? concat([
1137 hardline,
1138 path.call(
1139 (partPath) => comments.printDanglingComments(partPath, options, true),
1140 part
1141 ),
1142 hardline,
1143 ])
1144 : beforePart;
1145 const printedPartItems = Array.isArray(node[part])
1146 ? group(
1147 concat([
1148 join(
1149 ",",
1150 path.map((itemPartPath) => {
1151 const printedPart = print(itemPartPath);
1152 // Check if any of the implements nodes have comments
1153 return hasDanglingComments(itemPartPath.getValue())
1154 ? concat([
1155 hardline,
1156 comments.printDanglingComments(itemPartPath, options, true),
1157 hardline,
1158 printedPart,
1159 ])
1160 : concat([afterPart, printedPart]);
1161 }, part)
1162 ),
1163 ])
1164 )
1165 : concat([afterPart, path.call(print, part)]);
1166
1167 return indent(
1168 concat([
1169 printedBeforePart,
1170 part,
1171 willBreak(printedBeforePart)
1172 ? indent(printedPartItems)
1173 : printedPartItems,
1174 ])
1175 );
1176}
1177
1178function printClass(path, options, print) {
1179 const node = path.getValue();
1180
1181 const declaration = [];
1182
1183 if (node.isFinal) {
1184 declaration.push("final ");
1185 }
1186
1187 if (node.isAbstract) {
1188 declaration.push("abstract ");
1189 }
1190
1191 const isAnonymousClass = node.kind === "class" && node.isAnonymous;
1192
1193 // `new` print `class` keyword with arguments
1194 declaration.push(isAnonymousClass ? "" : node.kind);
1195
1196 if (node.name) {
1197 declaration.push(" ", path.call(print, "name"));
1198 }
1199
1200 // Only `class` can have `extends` and `implements`
1201 if (node.extends && node.implements) {
1202 declaration.push(
1203 conditionalGroup(
1204 [
1205 concat([
1206 printClassPart(path, options, print, "extends"),
1207 printClassPart(path, options, print, "implements"),
1208 ]),
1209 concat([
1210 printClassPart(path, options, print, "extends"),
1211 printClassPart(path, options, print, "implements", " ", hardline),
1212 ]),
1213 concat([
1214 printClassPart(path, options, print, "extends", hardline, " "),
1215 printClassPart(
1216 path,
1217 options,
1218 print,
1219 "implements",
1220 hardline,
1221 node.implements.length > 1 ? hardline : " "
1222 ),
1223 ]),
1224 ],
1225 {
1226 shouldBreak: hasDanglingComments(node.extends),
1227 }
1228 )
1229 );
1230 } else {
1231 if (node.extends) {
1232 declaration.push(
1233 conditionalGroup([
1234 printClassPart(path, options, print, "extends"),
1235 printClassPart(path, options, print, "extends", " ", hardline),
1236 printClassPart(
1237 path,
1238 options,
1239 print,
1240 "extends",
1241 hardline,
1242 node.extends.length > 1 ? hardline : " "
1243 ),
1244 ])
1245 );
1246 }
1247
1248 if (node.implements) {
1249 declaration.push(
1250 conditionalGroup([
1251 printClassPart(path, options, print, "implements"),
1252 printClassPart(path, options, print, "implements", " ", hardline),
1253 printClassPart(
1254 path,
1255 options,
1256 print,
1257 "implements",
1258 hardline,
1259 node.implements.length > 1 ? hardline : " "
1260 ),
1261 ])
1262 );
1263 }
1264 }
1265
1266 const printedDeclaration = group(
1267 concat([
1268 group(concat(declaration)),
1269 shouldPrintHardlineForOpenBrace(options)
1270 ? isAnonymousClass
1271 ? line
1272 : hardline
1273 : " ",
1274 ])
1275 );
1276
1277 const hasEmptyClassBody =
1278 node.body && node.body.length === 0 && !hasDanglingComments(node);
1279 const printedBody = concat([
1280 "{",
1281 indent(
1282 concat([
1283 hasEmptyClassBody ? "" : hardline,
1284 printStatements(path, options, print, "body"),
1285 ])
1286 ),
1287 comments.printDanglingComments(path, options, true),
1288 isAnonymousClass && hasEmptyClassBody ? softline : hardline,
1289 "}",
1290 ]);
1291
1292 return concat([printedDeclaration, printedBody]);
1293}
1294
1295function printFunction(path, options, print) {
1296 const node = path.getValue();
1297 const declaration = [];
1298
1299 if (node.isFinal) {
1300 declaration.push("final ");
1301 }
1302
1303 if (node.isAbstract) {
1304 declaration.push("abstract ");
1305 }
1306
1307 if (node.visibility) {
1308 declaration.push(node.visibility, " ");
1309 }
1310
1311 if (node.isStatic) {
1312 declaration.push("static ");
1313 }
1314
1315 declaration.push("function ");
1316
1317 if (node.byref) {
1318 declaration.push("&");
1319 }
1320
1321 if (node.name) {
1322 declaration.push(path.call(print, "name"));
1323 }
1324
1325 declaration.push(printArgumentsList(path, options, print));
1326
1327 if (node.uses && node.uses.length > 0) {
1328 declaration.push(
1329 group(concat([" use ", printArgumentsList(path, options, print, "uses")]))
1330 );
1331 }
1332
1333 if (node.type) {
1334 declaration.push(
1335 concat([
1336 ": ",
1337 hasDanglingComments(node.type)
1338 ? concat([
1339 path.call(
1340 (typePath) =>
1341 comments.printDanglingComments(typePath, options, true),
1342 "type"
1343 ),
1344 " ",
1345 ])
1346 : "",
1347 node.nullable ? "?" : "",
1348 path.call(print, "type"),
1349 ])
1350 );
1351 }
1352
1353 const printedDeclaration = concat(declaration);
1354
1355 if (!node.body) {
1356 return printedDeclaration;
1357 }
1358
1359 const isClosure = node.kind === "closure";
1360 const printedBody = concat([
1361 "{",
1362 indent(
1363 concat([hasEmptyBody(path) ? "" : hardline, path.call(print, "body")])
1364 ),
1365 isClosure && hasEmptyBody(path) ? "" : hardline,
1366 "}",
1367 ]);
1368
1369 if (isClosure) {
1370 return concat([printedDeclaration, " ", printedBody]);
1371 }
1372
1373 if (node.arguments.length === 0) {
1374 return concat([
1375 printedDeclaration,
1376 shouldPrintHardlineForOpenBrace(options) ? hardline : " ",
1377 printedBody,
1378 ]);
1379 }
1380
1381 const willBreakDeclaration = declaration.some(willBreak);
1382
1383 if (willBreakDeclaration) {
1384 return concat([printedDeclaration, " ", printedBody]);
1385 }
1386
1387 return conditionalGroup([
1388 concat([
1389 printedDeclaration,
1390 shouldPrintHardlineForOpenBrace(options) ? hardline : " ",
1391 printedBody,
1392 ]),
1393 concat([printedDeclaration, " ", printedBody]),
1394 ]);
1395}
1396
1397function printBodyControlStructure(
1398 path,
1399 options,
1400 print,
1401 bodyProperty = "body"
1402) {
1403 const node = path.getValue();
1404
1405 if (!node[bodyProperty]) {
1406 return ";";
1407 }
1408
1409 const printedBody = path.call(print, bodyProperty);
1410
1411 return concat([
1412 node.shortForm ? ":" : " {",
1413 indent(
1414 concat([
1415 node[bodyProperty].kind !== "block" ||
1416 (node[bodyProperty].children &&
1417 node[bodyProperty].children.length > 0) ||
1418 (node[bodyProperty].comments && node[bodyProperty].comments.length > 0)
1419 ? concat([
1420 shouldPrintHardLineAfterStartInControlStructure(path)
1421 ? node.kind === "switch"
1422 ? " "
1423 : ""
1424 : hardline,
1425 printedBody,
1426 ])
1427 : "",
1428 ])
1429 ),
1430 node.kind === "if" && bodyProperty === "body"
1431 ? ""
1432 : concat([
1433 shouldPrintHardLineBeforeEndInControlStructure(path) ? hardline : "",
1434 node.shortForm ? concat(["end", node.kind, ";"]) : "}",
1435 ]),
1436 ]);
1437}
1438
1439function printAssignment(
1440 leftNode,
1441 printedLeft,
1442 operator,
1443 rightNode,
1444 printedRight,
1445 hasRef,
1446 options
1447) {
1448 if (!rightNode) {
1449 return printedLeft;
1450 }
1451
1452 const printed = printAssignmentRight(
1453 leftNode,
1454 rightNode,
1455 printedRight,
1456 hasRef,
1457 options
1458 );
1459
1460 return group(concat([printedLeft, operator, printed]));
1461}
1462
1463function isLookupNodeChain(node) {
1464 if (!isLookupNode(node)) {
1465 return false;
1466 }
1467
1468 if (node.what.kind === "variable" || isReferenceLikeNode(node.what)) {
1469 return true;
1470 }
1471
1472 return isLookupNodeChain(node.what);
1473}
1474
1475function printAssignmentRight(
1476 leftNode,
1477 rightNode,
1478 printedRight,
1479 hasRef,
1480 options
1481) {
1482 const ref = hasRef ? "&" : "";
1483
1484 if (
1485 comments.hasLeadingOwnLineComment(options.originalText, rightNode, options)
1486 ) {
1487 return indent(concat([hardline, ref, printedRight]));
1488 }
1489
1490 const pureRightNode = rightNode.kind === "cast" ? rightNode.expr : rightNode;
1491
1492 const canBreak =
1493 (pureRightNode.kind === "bin" &&
1494 !shouldInlineLogicalExpression(pureRightNode)) ||
1495 (pureRightNode.kind === "retif" &&
1496 ((!pureRightNode.trueExpr &&
1497 !shouldInlineRetifFalseExpression(pureRightNode.falseExpr)) ||
1498 (pureRightNode.test.kind === "bin" &&
1499 !shouldInlineLogicalExpression(pureRightNode.test)))) ||
1500 ((leftNode.kind === "variable" ||
1501 leftNode.kind === "string" ||
1502 isLookupNode(leftNode)) &&
1503 ((pureRightNode.kind === "string" && !stringHasNewLines(pureRightNode)) ||
1504 isLookupNodeChain(pureRightNode)));
1505
1506 if (canBreak) {
1507 return group(indent(concat([line, ref, printedRight])));
1508 }
1509
1510 return concat([" ", ref, printedRight]);
1511}
1512
1513function needsHardlineAfterDanglingComment(node) {
1514 if (!node.comments) {
1515 return false;
1516 }
1517
1518 const lastDanglingComment = getLast(
1519 node.comments.filter((comment) => !comment.leading && !comment.trailing)
1520 );
1521
1522 return lastDanglingComment && !comments.isBlockComment(lastDanglingComment);
1523}
1524
1525function stringHasNewLines(node) {
1526 return node.raw.includes("\n");
1527}
1528
1529function isStringOnItsOwnLine(node, text, options) {
1530 return (
1531 (node.kind === "string" ||
1532 (node.kind === "encapsed" &&
1533 (node.type === "string" || node.type === "shell"))) &&
1534 stringHasNewLines(node) &&
1535 !hasNewline(text, options.locStart(node), { backwards: true })
1536 );
1537}
1538
1539function printNode(path, options, print) {
1540 const node = path.getValue();
1541
1542 switch (node.kind) {
1543 case "program": {
1544 return group(
1545 concat([
1546 printLines(path, options, print),
1547 comments.printDanglingComments(
1548 path,
1549 options,
1550 /* sameIndent */ true,
1551 (c) => !c.printed
1552 ),
1553 ])
1554 );
1555 }
1556 case "expressionstatement":
1557 return path.call(print, "expression");
1558 case "block":
1559 return concat([
1560 printLines(path, options, print),
1561 comments.printDanglingComments(path, options, true),
1562 ]);
1563 case "declare": {
1564 const printDeclareArguments = (path) => {
1565 return join(
1566 ", ",
1567 path.map((directive) => concat([print(directive)]), "directives")
1568 );
1569 };
1570
1571 if (["block", "short"].includes(node.mode)) {
1572 return concat([
1573 "declare(",
1574 printDeclareArguments(path),
1575 ")",
1576 node.mode === "block" ? " {" : ":",
1577 node.children.length > 0
1578 ? indent(concat([hardline, printLines(path, options, print)]))
1579 : "",
1580 comments.printDanglingComments(path, options),
1581 hardline,
1582 node.mode === "block" ? "}" : "enddeclare;",
1583 ]);
1584 }
1585
1586 const nextNode = getNextNode(path, node);
1587
1588 return concat([
1589 "declare(",
1590 printDeclareArguments(path),
1591 ")",
1592 nextNode && nextNode.kind === "inline" ? "" : ";",
1593 ]);
1594 }
1595 case "declaredirective":
1596 return concat([path.call(print, "key"), "=", path.call(print, "value")]);
1597 case "namespace":
1598 return concat([
1599 "namespace ",
1600 node.name && typeof node.name === "string"
1601 ? concat([node.name, node.withBrackets ? " " : ""])
1602 : "",
1603 node.withBrackets ? "{" : ";",
1604 hasDanglingComments(node)
1605 ? concat([" ", comments.printDanglingComments(path, options, true)])
1606 : "",
1607 node.children.length > 0
1608 ? node.withBrackets
1609 ? indent(concat([hardline, printLines(path, options, print)]))
1610 : concat([
1611 node.children[0].kind === "inline"
1612 ? ""
1613 : concat([
1614 hardline,
1615 isNextLineEmptyAfterNamespace(
1616 options.originalText,
1617 node,
1618 options.locStart
1619 )
1620 ? hardline
1621 : "",
1622 ]),
1623 printLines(path, options, print),
1624 ])
1625 : "",
1626 node.withBrackets ? concat([hardline, "}"]) : "",
1627 ]);
1628 case "usegroup":
1629 return group(
1630 concat([
1631 "use ",
1632 node.type ? concat([node.type, " "]) : "",
1633 indent(
1634 concat([
1635 node.name
1636 ? concat([
1637 maybeStripLeadingSlashFromUse(node.name),
1638 "\\{",
1639 softline,
1640 ])
1641 : "",
1642 join(
1643 concat([",", line]),
1644 path.map((item) => concat([print(item)]), "items")
1645 ),
1646 ])
1647 ),
1648 node.name
1649 ? concat([
1650 ifBreak(shouldPrintComma(options, "7.2") ? "," : ""),
1651 softline,
1652 "}",
1653 ])
1654 : "",
1655 ])
1656 );
1657 case "useitem":
1658 return concat([
1659 node.type ? concat([node.type, " "]) : "",
1660 maybeStripLeadingSlashFromUse(node.name),
1661 hasDanglingComments(node)
1662 ? concat([" ", comments.printDanglingComments(path, options, true)])
1663 : "",
1664 node.alias ? concat([" as ", path.call(print, "alias")]) : "",
1665 ]);
1666 case "class":
1667 case "interface":
1668 case "trait":
1669 return printClass(path, options, print);
1670 case "traitprecedence":
1671 return concat([
1672 path.call(print, "trait"),
1673 "::",
1674 path.call(print, "method"),
1675 " insteadof ",
1676 join(", ", path.map(print, "instead")),
1677 ]);
1678 case "traitalias":
1679 return concat([
1680 node.trait ? concat([path.call(print, "trait"), "::"]) : "",
1681 node.method ? path.call(print, "method") : "",
1682 " as ",
1683 join(" ", [
1684 ...(node.visibility ? [node.visibility] : []),
1685 ...(node.as ? [path.call(print, "as")] : []),
1686 ]),
1687 ]);
1688 case "traituse":
1689 return group(
1690 concat([
1691 "use ",
1692 indent(group(join(concat([",", line]), path.map(print, "traits")))),
1693 node.adaptations
1694 ? concat([
1695 " {",
1696 node.adaptations.length > 0
1697 ? concat([
1698 indent(
1699 concat([
1700 hardline,
1701 printStatements(path, options, print, "adaptations"),
1702 ])
1703 ),
1704 hardline,
1705 ])
1706 : hasDanglingComments(node)
1707 ? concat([
1708 line,
1709 comments.printDanglingComments(path, options, true),
1710 line,
1711 ])
1712 : "",
1713 "}",
1714 ])
1715 : "",
1716 ])
1717 );
1718 case "function":
1719 case "closure":
1720 case "method":
1721 return printFunction(path, options, print);
1722 case "arrowfunc":
1723 return concat([
1724 node.isStatic ? "static " : "",
1725 "fn",
1726 printArgumentsList(path, options, print),
1727 node.type
1728 ? concat([": ", node.nullable ? "?" : "", path.call(print, "type")])
1729 : "",
1730 " => ",
1731 path.call(print, "body"),
1732 ]);
1733 case "parameter": {
1734 const name = concat([
1735 node.nullable ? "?" : "",
1736 node.type ? concat([path.call(print, "type"), " "]) : "",
1737 node.byref ? "&" : "",
1738 node.variadic ? "..." : "",
1739 "$",
1740 path.call(print, "name"),
1741 ]);
1742
1743 if (node.value) {
1744 return group(
1745 concat([
1746 name,
1747 // see handleFunctionParameter() in ./comments.js - since there's
1748 // no node to attach comments that fall in between the parameter name
1749 // and value, we store them as dangling comments
1750 hasDanglingComments(node) ? " " : "",
1751 comments.printDanglingComments(path, options, true),
1752 concat([
1753 " =",
1754 printAssignmentRight(
1755 node.name,
1756 node.value,
1757 path.call(print, "value"),
1758 false,
1759 options
1760 ),
1761 ]),
1762 ])
1763 );
1764 }
1765
1766 return name;
1767 }
1768 case "variadic":
1769 return concat(["...", path.call(print, "what")]);
1770 case "property":
1771 return group(
1772 concat([
1773 node.type
1774 ? concat([node.nullable ? "?" : "", path.call(print, "type"), " "])
1775 : "",
1776 "$",
1777 path.call(print, "name"),
1778 node.value
1779 ? concat([
1780 " =",
1781 printAssignmentRight(
1782 node.name,
1783 node.value,
1784 path.call(print, "value"),
1785 false,
1786 options
1787 ),
1788 ])
1789 : "",
1790 ])
1791 );
1792 case "propertystatement": {
1793 const printed = path.map((childPath) => {
1794 return print(childPath);
1795 }, "properties");
1796
1797 const hasValue = node.properties.some((property) => property.value);
1798
1799 let firstProperty;
1800
1801 if (printed.length === 1 && !node.properties[0].comments) {
1802 [firstProperty] = printed;
1803 } else if (printed.length > 0) {
1804 // Indent first property
1805 firstProperty = indent(printed[0]);
1806 }
1807
1808 const hasVisibility = node.visibility || node.visibility === null;
1809
1810 return group(
1811 concat([
1812 hasVisibility
1813 ? concat([node.visibility === null ? "var" : node.visibility, ""])
1814 : "",
1815 node.isStatic ? concat([hasVisibility ? " " : "", "static"]) : "",
1816 firstProperty ? concat([" ", firstProperty]) : "",
1817 indent(
1818 concat(
1819 printed
1820 .slice(1)
1821 .map((p) => concat([",", hasValue ? hardline : line, p]))
1822 )
1823 ),
1824 ])
1825 );
1826 }
1827 case "if": {
1828 const parts = [];
1829 const body = printBodyControlStructure(path, options, print, "body");
1830 const opening = group(
1831 concat([
1832 "if (",
1833 group(
1834 concat([
1835 indent(concat([softline, path.call(print, "test")])),
1836 softline,
1837 ])
1838 ),
1839 ")",
1840 body,
1841 ])
1842 );
1843
1844 parts.push(
1845 opening,
1846 isFirstChildrenInlineNode(path) || !node.body ? "" : hardline
1847 );
1848
1849 if (node.alternate) {
1850 parts.push(node.shortForm ? "" : "} ");
1851
1852 const commentOnOwnLine =
1853 (hasTrailingComment(node.body) &&
1854 node.body.comments.some(
1855 (comment) => comment.trailing && !comments.isBlockComment(comment)
1856 )) ||
1857 needsHardlineAfterDanglingComment(node);
1858 const elseOnSameLine = !commentOnOwnLine;
1859 parts.push(elseOnSameLine ? "" : hardline);
1860
1861 if (hasDanglingComments(node)) {
1862 parts.push(
1863 isNextLineEmpty(options.originalText, node.body, options)
1864 ? hardline
1865 : "",
1866 comments.printDanglingComments(path, options, true),
1867 commentOnOwnLine ? hardline : " "
1868 );
1869 }
1870
1871 parts.push(
1872 "else",
1873 group(
1874 node.alternate.kind === "if"
1875 ? path.call(print, "alternate")
1876 : printBodyControlStructure(path, options, print, "alternate")
1877 )
1878 );
1879 } else {
1880 parts.push(node.body ? (node.shortForm ? "endif;" : "}") : "");
1881 }
1882
1883 return concat(parts);
1884 }
1885 case "do":
1886 return concat([
1887 "do",
1888 printBodyControlStructure(path, options, print, "body"),
1889 " while (",
1890 group(
1891 concat([
1892 indent(concat([softline, path.call(print, "test")])),
1893 softline,
1894 ])
1895 ),
1896 ")",
1897 ]);
1898 case "while":
1899 case "switch":
1900 return group(
1901 concat([
1902 node.kind,
1903 " (",
1904 group(
1905 concat([
1906 indent(concat([softline, path.call(print, "test")])),
1907 softline,
1908 ])
1909 ),
1910 ")",
1911 printBodyControlStructure(path, options, print, "body"),
1912 ])
1913 );
1914 case "for": {
1915 const body = printBodyControlStructure(path, options, print, "body");
1916
1917 // We want to keep dangling comments above the loop to stay consistent.
1918 // Any comment positioned between the for statement and the parentheses
1919 // is going to be printed before the statement.
1920 const dangling = comments.printDanglingComments(
1921 path,
1922 options,
1923 /* sameLine */ true
1924 );
1925 const printedComments = dangling ? concat([dangling, softline]) : "";
1926
1927 if (!node.init.length && !node.test.length && !node.increment.length) {
1928 return concat([printedComments, group(concat(["for (;;)", body]))]);
1929 }
1930
1931 return concat([
1932 printedComments,
1933 group(
1934 concat([
1935 "for (",
1936 group(
1937 concat([
1938 indent(
1939 concat([
1940 softline,
1941 group(
1942 concat([
1943 join(concat([",", line]), path.map(print, "init")),
1944 ])
1945 ),
1946 ";",
1947 line,
1948 group(
1949 concat([
1950 join(concat([",", line]), path.map(print, "test")),
1951 ])
1952 ),
1953 ";",
1954 line,
1955 group(
1956 join(concat([",", line]), path.map(print, "increment"))
1957 ),
1958 ])
1959 ),
1960 softline,
1961 ])
1962 ),
1963 ")",
1964 body,
1965 ])
1966 ),
1967 ]);
1968 }
1969 case "foreach": {
1970 const body = printBodyControlStructure(path, options, print, "body");
1971
1972 // We want to keep dangling comments above the loop to stay consistent.
1973 // Any comment positioned between the for statement and the parentheses
1974 // is going to be printed before the statement.
1975 const dangling = comments.printDanglingComments(
1976 path,
1977 options,
1978 /* sameLine */ true
1979 );
1980 const printedComments = dangling ? concat([dangling, softline]) : "";
1981
1982 return concat([
1983 printedComments,
1984 group(
1985 concat([
1986 "foreach (",
1987 group(
1988 concat([
1989 indent(
1990 concat([
1991 softline,
1992 path.call(print, "source"),
1993 line,
1994 "as ",
1995 group(
1996 node.key
1997 ? indent(
1998 join(concat([" =>", line]), [
1999 path.call(print, "key"),
2000 path.call(print, "value"),
2001 ])
2002 )
2003 : path.call(print, "value")
2004 ),
2005 ])
2006 ),
2007 softline,
2008 ])
2009 ),
2010 ")",
2011 body,
2012 ])
2013 ),
2014 ]);
2015 }
2016 case "try": {
2017 const parts = [];
2018
2019 parts.push(
2020 "try",
2021 printBodyControlStructure(path, options, print, "body")
2022 );
2023
2024 if (node.catches) {
2025 parts.push(concat(path.map(print, "catches")));
2026 }
2027
2028 if (node.always) {
2029 parts.push(
2030 " finally",
2031 printBodyControlStructure(path, options, print, "always")
2032 );
2033 }
2034
2035 return concat(parts);
2036 }
2037 case "catch": {
2038 return concat([
2039 " catch",
2040 node.what
2041 ? concat([
2042 " (",
2043 join(" | ", path.map(print, "what")),
2044 " ",
2045 path.call(print, "variable"),
2046 ")",
2047 ])
2048 : "",
2049 printBodyControlStructure(path, options, print, "body"),
2050 ]);
2051 }
2052 case "case":
2053 return concat([
2054 node.test
2055 ? concat([
2056 "case ",
2057 node.test.comments
2058 ? indent(path.call(print, "test"))
2059 : path.call(print, "test"),
2060 ":",
2061 ])
2062 : "default:",
2063 node.body
2064 ? node.body.children && node.body.children.length
2065 ? indent(
2066 concat([
2067 isFirstChildrenInlineNode(path) ? "" : hardline,
2068 path.call(print, "body"),
2069 ])
2070 )
2071 : ""
2072 : "",
2073 ]);
2074 case "break":
2075 case "continue":
2076 if (node.level) {
2077 if (node.level.kind === "number" && node.level.value !== "1") {
2078 return concat([`${node.kind} `, path.call(print, "level")]);
2079 }
2080
2081 return node.kind;
2082 }
2083
2084 return node.kind;
2085 case "call": {
2086 // Multiline strings as single arguments
2087 if (
2088 node.arguments.length === 1 &&
2089 isStringOnItsOwnLine(node.arguments[0], options.originalText, options)
2090 ) {
2091 return concat([
2092 path.call(print, "what"),
2093 concat(["(", join(", ", path.map(print, "arguments")), ")"]),
2094 ]);
2095 }
2096
2097 // chain: Call (*LookupNode (Call (*LookupNode (...))))
2098 if (isLookupNode(node.what)) {
2099 return printMemberChain(path, options, print);
2100 }
2101
2102 return concat([
2103 path.call(print, "what"),
2104 printArgumentsList(path, options, print),
2105 ]);
2106 }
2107 case "new": {
2108 const isAnonymousClassNode =
2109 node.what && node.what.kind === "class" && node.what.isAnonymous;
2110
2111 // Multiline strings as single arguments
2112 if (
2113 !isAnonymousClassNode &&
2114 node.arguments.length === 1 &&
2115 isStringOnItsOwnLine(node.arguments[0], options.originalText, options)
2116 ) {
2117 return concat([
2118 "new ",
2119 path.call(print, "what"),
2120 "(",
2121 join(", ", path.map(print, "arguments")),
2122 ")",
2123 ]);
2124 }
2125
2126 const parts = [];
2127
2128 parts.push("new ");
2129
2130 if (isAnonymousClassNode) {
2131 parts.push(
2132 "class",
2133 node.arguments.length > 0
2134 ? printArgumentsList(path, options, print)
2135 : "",
2136 group(path.call(print, "what"))
2137 );
2138 } else {
2139 const printed = concat([
2140 path.call(print, "what"),
2141 printArgumentsList(path, options, print),
2142 ]);
2143
2144 parts.push(hasLeadingComment(node.what) ? indent(printed) : printed);
2145 }
2146
2147 return concat(parts);
2148 }
2149 case "clone":
2150 return concat([
2151 "clone ",
2152 node.what.comments
2153 ? indent(path.call(print, "what"))
2154 : path.call(print, "what"),
2155 ]);
2156 case "propertylookup":
2157 case "staticlookup":
2158 case "offsetlookup": {
2159 const parent = path.getParentNode();
2160
2161 let firstNonMemberParent;
2162 let i = 0;
2163
2164 do {
2165 firstNonMemberParent = path.getParentNode(i);
2166 i++;
2167 } while (firstNonMemberParent && isLookupNode(firstNonMemberParent));
2168
2169 const hasEncapsedAncestor = getAncestorNode(path, "encapsed");
2170 const shouldInline =
2171 hasEncapsedAncestor ||
2172 (firstNonMemberParent &&
2173 (firstNonMemberParent.kind === "new" ||
2174 (firstNonMemberParent.kind === "assign" &&
2175 firstNonMemberParent.left.kind !== "variable"))) ||
2176 node.kind === "offsetlookup" ||
2177 ((isReferenceLikeNode(node.what) || node.what.kind === "variable") &&
2178 ["identifier", "variable", "encapsedpart"].includes(
2179 node.offset.kind
2180 ) &&
2181 parent &&
2182 !isLookupNode(parent));
2183
2184 return concat([
2185 path.call(print, "what"),
2186 shouldInline
2187 ? printLookupNodes(path, options, print)
2188 : group(
2189 indent(concat([softline, printLookupNodes(path, options, print)]))
2190 ),
2191 ]);
2192 }
2193 case "exit":
2194 return group(
2195 concat([
2196 node.useDie ? "die" : "exit",
2197 "(",
2198 node.expression
2199 ? isStringOnItsOwnLine(
2200 node.expression,
2201 options.originalText,
2202 options
2203 )
2204 ? path.call(print, "expression")
2205 : concat([
2206 indent(concat([softline, path.call(print, "expression")])),
2207 softline,
2208 ])
2209 : comments.printDanglingComments(path, options),
2210 ")",
2211 ])
2212 );
2213 case "global":
2214 return group(
2215 concat([
2216 "global ",
2217 indent(concat([join(concat([",", line]), path.map(print, "items"))])),
2218 ])
2219 );
2220 case "include":
2221 return concat([
2222 node.require ? "require" : "include",
2223 node.once ? "_once" : "",
2224 " ",
2225 node.target.comments
2226 ? indent(path.call(print, "target"))
2227 : path.call(print, "target"),
2228 ]);
2229 case "label":
2230 return concat([path.call(print, "name"), ":"]);
2231 case "goto":
2232 return concat(["goto ", path.call(print, "label")]);
2233 case "throw":
2234 return concat([
2235 "throw ",
2236 node.what.comments
2237 ? indent(path.call(print, "what"))
2238 : path.call(print, "what"),
2239 ]);
2240 case "silent":
2241 return concat(["@", path.call(print, "expr")]);
2242 case "halt":
2243 return concat([
2244 hasDanglingComments(node)
2245 ? concat([
2246 comments.printDanglingComments(
2247 path,
2248 options,
2249 /* sameIndent */ true
2250 ),
2251 hardline,
2252 ])
2253 : "",
2254 "__halt_compiler();",
2255 node.after,
2256 ]);
2257 case "eval":
2258 return group(
2259 concat([
2260 "eval(",
2261 isStringOnItsOwnLine(node.source, options.originalText, options)
2262 ? path.call(print, "source")
2263 : concat([
2264 indent(concat([softline, path.call(print, "source")])),
2265 softline,
2266 ]),
2267 ")",
2268 ])
2269 );
2270 case "echo": {
2271 const printedArguments = path.map((childPath) => {
2272 return print(childPath);
2273 }, "expressions");
2274
2275 let firstVariable;
2276
2277 if (printedArguments.length === 1 && !node.expressions[0].comments) {
2278 [firstVariable] = printedArguments;
2279 } else if (printedArguments.length > 0) {
2280 firstVariable =
2281 isDocNode(node.expressions[0]) || node.expressions[0].comments
2282 ? indent(printedArguments[0])
2283 : dedent(printedArguments[0]);
2284 }
2285
2286 return group(
2287 concat([
2288 node.shortForm ? "" : "echo ",
2289 firstVariable ? firstVariable : "",
2290 indent(
2291 concat(printedArguments.slice(1).map((p) => concat([",", line, p])))
2292 ),
2293 ])
2294 );
2295 }
2296 case "print": {
2297 return concat([
2298 "print ",
2299 node.expression.comments
2300 ? indent(path.call(print, "expression"))
2301 : path.call(print, "expression"),
2302 ]);
2303 }
2304 case "return": {
2305 const parts = [];
2306
2307 parts.push("return");
2308
2309 if (node.expr) {
2310 const printedExpr = path.call(print, "expr");
2311
2312 parts.push(" ", node.expr.comments ? indent(printedExpr) : printedExpr);
2313 }
2314
2315 if (hasDanglingComments(node)) {
2316 parts.push(
2317 " ",
2318 comments.printDanglingComments(path, options, /* sameIndent */ true)
2319 );
2320 }
2321
2322 return concat(parts);
2323 }
2324 case "isset":
2325 case "unset":
2326 return group(
2327 concat([
2328 node.kind,
2329 printArgumentsList(path, options, print, "variables"),
2330 ])
2331 );
2332 case "empty":
2333 return group(
2334 concat([
2335 "empty(",
2336 indent(concat([softline, path.call(print, "expression")])),
2337 softline,
2338 ")",
2339 ])
2340 );
2341 case "variable": {
2342 const parent = path.getParentNode();
2343 const parentParent = path.getParentNode(1);
2344 const ampersand = parent.kind === "assign" ? "" : node.byref ? "&" : "";
2345 const dollar =
2346 (parent.kind === "encapsedpart" &&
2347 parent.syntax === "simple" &&
2348 parent.curly) ||
2349 (parentParent &&
2350 parent.kind === "offsetlookup" &&
2351 parentParent.kind === "encapsedpart" &&
2352 parentParent.syntax === "simple" &&
2353 parentParent.curly)
2354 ? ""
2355 : "$";
2356 const openCurly = node.curly ? "{" : "";
2357 const closeCurly = node.curly ? "}" : "";
2358
2359 return concat([
2360 ampersand,
2361 dollar,
2362 openCurly,
2363 path.call(print, "name"),
2364 closeCurly,
2365 ]);
2366 }
2367 case "constantstatement":
2368 case "classconstant": {
2369 const printed = path.map((childPath) => {
2370 return print(childPath);
2371 }, "constants");
2372
2373 let firstVariable;
2374
2375 if (printed.length === 1 && !node.constants[0].comments) {
2376 [firstVariable] = printed;
2377 } else if (printed.length > 0) {
2378 // Indent first item
2379 firstVariable = indent(printed[0]);
2380 }
2381
2382 return group(
2383 concat([
2384 node.visibility ? concat([node.visibility, " "]) : "",
2385 "const",
2386 firstVariable ? concat([" ", firstVariable]) : "",
2387 indent(
2388 concat(printed.slice(1).map((p) => concat([",", hardline, p])))
2389 ),
2390 ])
2391 );
2392 }
2393 case "constant":
2394 return printAssignment(
2395 node.name,
2396 path.call(print, "name"),
2397 " =",
2398 node.value,
2399 path.call(print, "value"),
2400 false,
2401 options
2402 );
2403 case "static": {
2404 const printed = path.map((childPath) => {
2405 return print(childPath);
2406 }, "variables");
2407
2408 const hasValue = node.variables.some((item) => item.defaultValue);
2409
2410 let firstVariable;
2411
2412 if (printed.length === 1 && !node.variables[0].comments) {
2413 [firstVariable] = printed;
2414 } else if (printed.length > 0) {
2415 // Indent first item
2416 firstVariable = indent(printed[0]);
2417 }
2418
2419 return group(
2420 concat([
2421 "static",
2422 firstVariable ? concat([" ", firstVariable]) : "",
2423 indent(
2424 concat(
2425 printed
2426 .slice(1)
2427 .map((p) => concat([",", hasValue ? hardline : line, p]))
2428 )
2429 ),
2430 ])
2431 );
2432 }
2433 case "staticvariable": {
2434 return printAssignment(
2435 node.variable,
2436 path.call(print, "variable"),
2437 " =",
2438 node.defaultValue,
2439 path.call(print, "defaultValue"),
2440 false,
2441 options
2442 );
2443 }
2444 case "list":
2445 case "array": {
2446 const useShortForm =
2447 (node.kind === "array" && isMinVersion(options.phpVersion, "5.4")) ||
2448 (node.kind === "list" && node.shortForm);
2449 const open = useShortForm ? "[" : concat([node.kind, "("]);
2450 const close = useShortForm ? "]" : ")";
2451
2452 if (node.items.length === 0) {
2453 if (!hasDanglingComments(node)) {
2454 return concat([open, close]);
2455 }
2456
2457 return group(
2458 concat([
2459 open,
2460 comments.printDanglingComments(path, options),
2461 softline,
2462 close,
2463 ])
2464 );
2465 }
2466
2467 const lastElem = getLast(node.items);
2468
2469 // PHP allows you to have empty elements in an array which
2470 // changes its length based on the number of commas. The algorithm
2471 // is that if the last argument is null, we need to force insert
2472 // a comma to ensure PHP recognizes it.
2473 // [,] === $arr;
2474 // [1,] === $arr;
2475 // [1,,] === $arr;
2476 //
2477 // Note that getLast returns null if the array is empty, but
2478 // we already check for an empty array just above so we are safe
2479 const needsForcedTrailingComma = lastElem && lastElem.kind === "noop";
2480
2481 const [firstProperty] = node.items
2482 .filter((node) => node.kind !== "noop")
2483 .sort((a, b) => options.locStart(a) - options.locStart(b));
2484 const isAssociative = !!(firstProperty && firstProperty.key);
2485 const shouldBreak =
2486 isAssociative &&
2487 firstProperty &&
2488 hasNewlineInRange(
2489 options.originalText,
2490 options.locStart(node),
2491 options.locStart(firstProperty)
2492 );
2493
2494 return group(
2495 concat([
2496 open,
2497 indent(concat([softline, printArrayItems(path, options, print)])),
2498 needsForcedTrailingComma ? "," : "",
2499 ifBreak(
2500 !needsForcedTrailingComma && shouldPrintComma(options, "5.0")
2501 ? concat([
2502 lastElem && shouldPrintHardlineBeforeTrailingComma(lastElem)
2503 ? hardline
2504 : "",
2505 ",",
2506 ])
2507 : ""
2508 ),
2509 comments.printDanglingComments(path, options, true),
2510 softline,
2511 close,
2512 ]),
2513 { shouldBreak }
2514 );
2515 }
2516 case "entry": {
2517 const ref = node.byRef ? "&" : "";
2518 const unpack = node.unpack ? "..." : "";
2519 return node.key
2520 ? printAssignment(
2521 node.key,
2522 path.call(print, "key"),
2523 " =>",
2524 node.value,
2525 path.call(print, "value"),
2526 ref,
2527 options
2528 )
2529 : concat([ref, unpack, path.call(print, "value")]);
2530 }
2531 case "yield": {
2532 const printedKeyAndValue = concat([
2533 node.key ? concat([path.call(print, "key"), " => "]) : "",
2534 path.call(print, "value"),
2535 ]);
2536
2537 return concat([
2538 "yield",
2539 node.key || node.value ? " " : "",
2540 node.value && node.value.comments
2541 ? indent(printedKeyAndValue)
2542 : printedKeyAndValue,
2543 ]);
2544 }
2545 case "yieldfrom":
2546 return concat([
2547 "yield from ",
2548 node.value.comments
2549 ? indent(path.call(print, "value"))
2550 : path.call(print, "value"),
2551 ]);
2552 case "unary":
2553 return concat([node.type, path.call(print, "what")]);
2554 case "pre":
2555 return concat([node.type + node.type, path.call(print, "what")]);
2556 case "post":
2557 return concat([path.call(print, "what"), node.type + node.type]);
2558 case "cast":
2559 return concat([
2560 "(",
2561 node.type,
2562 ") ",
2563 node.expr.comments
2564 ? indent(path.call(print, "expr"))
2565 : path.call(print, "expr"),
2566 ]);
2567 case "assignref":
2568 case "assign": {
2569 const hasRef = node.kind === "assignref";
2570
2571 return printAssignment(
2572 node.left,
2573 path.call(print, "left"),
2574 concat([" ", hasRef ? "=" : node.operator]),
2575 node.right,
2576 path.call(print, "right"),
2577 hasRef,
2578 options
2579 );
2580 }
2581 case "bin": {
2582 const parent = path.getParentNode();
2583 const parentParent = path.getParentNode(1);
2584 const isInsideParenthesis =
2585 node !== parent.body &&
2586 (parent.kind === "if" ||
2587 parent.kind === "while" ||
2588 parent.kind === "switch" ||
2589 parent.kind === "do");
2590
2591 const parts = printBinaryExpression(
2592 path,
2593 print,
2594 options,
2595 /* isNested */ false,
2596 isInsideParenthesis
2597 );
2598
2599 // if (
2600 // $this->hasPlugin('dynamicImports') && $this->lookahead()->type === tt->parenLeft
2601 // ) {
2602 //
2603 // looks super weird, we want to break the children if the parent breaks
2604 //
2605 // if (
2606 // $this->hasPlugin('dynamicImports') &&
2607 // $this->lookahead()->type === tt->parenLeft
2608 // ) {
2609 if (isInsideParenthesis) {
2610 return concat(parts);
2611 }
2612
2613 // Break between the parens in unaries or in a member expression, i.e.
2614 //
2615 // (
2616 // a &&
2617 // b &&
2618 // c
2619 // )->call()
2620 if (
2621 parent.kind === "unary" ||
2622 (isLookupNode(parent) && parent.kind !== "offsetlookup")
2623 ) {
2624 return group(
2625 concat([indent(concat([softline, concat(parts)])), softline])
2626 );
2627 }
2628
2629 // Avoid indenting sub-expressions in some cases where the first sub-expression is already
2630 // indented accordingly. We should indent sub-expressions where the first case isn't indented.
2631 const shouldNotIndent =
2632 (node !== parent.body && parent.kind === "for") ||
2633 (parent.kind === "retif" &&
2634 parentParent &&
2635 parentParent.kind !== "return");
2636
2637 const shouldIndentIfInlining = [
2638 "assign",
2639 "property",
2640 "constant",
2641 "staticvariable",
2642 "entry",
2643 ].includes(parent.kind);
2644
2645 const samePrecedenceSubExpression =
2646 node.left.kind === "bin" && shouldFlatten(node.type, node.left.type);
2647
2648 if (
2649 shouldNotIndent ||
2650 (shouldInlineLogicalExpression(node) && !samePrecedenceSubExpression) ||
2651 (!shouldInlineLogicalExpression(node) && shouldIndentIfInlining)
2652 ) {
2653 return group(concat(parts));
2654 }
2655
2656 const rest = concat(parts.slice(1));
2657
2658 return group(
2659 concat([
2660 // Don't include the initial expression in the indentation
2661 // level. The first item is guaranteed to be the first
2662 // left-most expression.
2663 parts.length > 0 ? parts[0] : "",
2664 indent(rest),
2665 ])
2666 );
2667 }
2668 case "retif": {
2669 const parts = [];
2670 const parent = path.getParentNode();
2671
2672 // Find the outermost non-retif parent, and the outermost retif parent.
2673 let currentParent;
2674 let i = 0;
2675
2676 do {
2677 currentParent = path.getParentNode(i);
2678 i++;
2679 } while (currentParent && currentParent.kind === "retif");
2680 const firstNonRetifParent = currentParent || parent;
2681
2682 const printedFalseExpr =
2683 node.falseExpr.kind === "bin"
2684 ? indent(path.call(print, "falseExpr"))
2685 : path.call(print, "falseExpr");
2686 const part = concat([
2687 node.trueExpr ? line : " ",
2688 "?",
2689 node.trueExpr
2690 ? concat([
2691 " ",
2692 node.trueExpr.kind === "bin"
2693 ? indent(path.call(print, "trueExpr"))
2694 : path.call(print, "trueExpr"),
2695 line,
2696 ])
2697 : "",
2698 ":",
2699 node.trueExpr
2700 ? concat([" ", printedFalseExpr])
2701 : concat([
2702 shouldInlineRetifFalseExpression(node.falseExpr) ? " " : line,
2703 printedFalseExpr,
2704 ]),
2705 ]);
2706
2707 parts.push(part);
2708
2709 // We want a whole chain of retif to all break if any of them break.
2710 const maybeGroup = (doc) =>
2711 parent === firstNonRetifParent ? group(doc) : doc;
2712
2713 // Break the closing parens to keep the chain right after it:
2714 // ($a
2715 // ? $b
2716 // : $c
2717 // )->call()
2718 const parentParent = path.getParentNode(1);
2719 const pureParent =
2720 parent.kind === "cast" && parentParent ? parentParent : parent;
2721 const breakLookupNodes = ["propertylookup", "staticlookup"];
2722 const breakClosingParens = breakLookupNodes.includes(pureParent.kind);
2723
2724 const printedTest = path.call(print, "test");
2725
2726 if (!node.trueExpr) {
2727 const printed = concat([
2728 printedTest,
2729 pureParent.kind === "bin" ||
2730 ["print", "echo", "return", "include"].includes(
2731 firstNonRetifParent.kind
2732 )
2733 ? indent(concat(parts))
2734 : concat(parts),
2735 ]);
2736
2737 // Break between the parens in unaries or in a lookup nodes, i.e.
2738 //
2739 // (
2740 // a ?:
2741 // b ?:
2742 // c
2743 // )->call()
2744 if (
2745 (pureParent.kind === "call" && pureParent.what === node) ||
2746 pureParent.kind === "unary" ||
2747 (isLookupNode(pureParent) && pureParent.kind !== "offsetlookup")
2748 ) {
2749 return group(concat([indent(concat([softline, printed])), softline]));
2750 }
2751
2752 return maybeGroup(printed);
2753 }
2754
2755 return maybeGroup(
2756 concat([
2757 node.test.kind === "retif" ? indent(printedTest) : printedTest,
2758 indent(concat(parts)),
2759 breakClosingParens ? softline : "",
2760 ])
2761 );
2762 }
2763 case "boolean":
2764 return node.value ? "true" : "false";
2765 case "number":
2766 return printNumber(node.value);
2767 case "string": {
2768 const parent = path.getParentNode();
2769
2770 if (parent.kind === "encapsedpart") {
2771 const parentParent = path.getParentNode(1);
2772 let closingTagIndentation = 0;
2773 if (parentParent.type === "heredoc") {
2774 const lines = parentParent.raw.split(/\r?\n/g);
2775 closingTagIndentation = lines[lines.length - 1].search(/\S/);
2776 }
2777 return join(
2778 literalline,
2779 node.raw
2780 .split(/\r?\n/g)
2781 .map((s) => s.substring(closingTagIndentation))
2782 );
2783 }
2784
2785 const quote = useDoubleQuote(node, options) ? '"' : "'";
2786
2787 let stringValue = node.raw;
2788
2789 if (node.raw[0] === "b") {
2790 stringValue = stringValue.slice(1);
2791 }
2792
2793 // We need to strip out the quotes from the raw value
2794 if (['"', "'"].includes(stringValue[0])) {
2795 stringValue = stringValue.substr(1);
2796 }
2797
2798 if (['"', "'"].includes(stringValue[stringValue.length - 1])) {
2799 stringValue = stringValue.substr(0, stringValue.length - 1);
2800 }
2801
2802 return concat([
2803 node.raw[0] === "b" ? "b" : "",
2804 quote,
2805 join(literalline, stringValue.split(/\r?\n/g)),
2806 quote,
2807 ]);
2808 }
2809 case "encapsedpart": {
2810 const open =
2811 (node.syntax === "simple" && node.curly) || node.syntax === "complex"
2812 ? concat([node.curly ? "$" : "", "{"])
2813 : "";
2814 const close =
2815 (node.syntax === "simple" && node.curly) || node.syntax === "complex"
2816 ? "}"
2817 : "";
2818
2819 return concat([open, path.call(print, "expression"), close]);
2820 }
2821 case "encapsed":
2822 switch (node.type) {
2823 case "string":
2824 case "shell":
2825 case "heredoc":
2826 return concat([
2827 getEncapsedQuotes(node),
2828 // Respect `indent` for `heredoc` nodes
2829 node.type === "heredoc" ? literalline : "",
2830 concat(path.map(print, "value")),
2831 getEncapsedQuotes(node, { opening: false }),
2832 node.type === "heredoc" && docShouldHaveTrailingNewline(path)
2833 ? hardline
2834 : "",
2835 ]);
2836 // istanbul ignore next
2837 default:
2838 return `Have not implemented kind ${node.type} yet.`;
2839 }
2840 case "inline":
2841 return join(
2842 literalline,
2843 node.raw.replace("___PSEUDO_INLINE_PLACEHOLDER___", "").split(/\r?\n/g)
2844 );
2845 case "magic":
2846 return node.value;
2847 case "nowdoc": {
2848 const flexible = isMinVersion(options.phpVersion, "7.3");
2849 const linebreak = flexible ? hardline : literalline;
2850 return concat([
2851 "<<<'",
2852 node.label,
2853 "'",
2854 linebreak,
2855 join(linebreak, node.value.split(/\r?\n/g)),
2856 linebreak,
2857 node.label,
2858 docShouldHaveTrailingNewline(path) ? hardline : "",
2859 ]);
2860 }
2861 case "name":
2862 return concat([node.resolution === "rn" ? "namespace\\" : "", node.name]);
2863 case "literal":
2864 return path.call(print, "value");
2865 case "parentreference":
2866 return "parent";
2867 case "selfreference":
2868 return "self";
2869 case "staticreference":
2870 return "static";
2871 case "typereference":
2872 return node.name;
2873 case "nullkeyword":
2874 return "null";
2875 case "identifier": {
2876 const parent = path.getParentNode();
2877
2878 if (parent.kind === "method") {
2879 node.name = normalizeMagicMethodName(node.name);
2880 }
2881
2882 return path.call(print, "name");
2883 }
2884 case "noop":
2885 return node.comments
2886 ? comments.printComments(path.getValue().comments, options)
2887 : "";
2888
2889 case "error":
2890 default:
2891 // istanbul ignore next
2892 return `Have not implemented kind ${node.kind} yet.`;
2893 }
2894}
2895
2896module.exports = genericPrint;