Files
rspade_system/node_modules/@prettier/plugin-php/src/printer.mjs
2025-12-03 21:28:08 +00:00

2900 lines
77 KiB
JavaScript
Executable File

import { util as prettierUtil, doc } from "prettier";
import {
printAllComments,
hasTrailingComment,
hasLeadingComment,
printDanglingComments,
printComments,
isBlockComment,
hasLeadingOwnLineComment,
} from "./comments.mjs";
import pathNeedsParens from "./needs-parens.mjs";
import { locStart, locEnd } from "./loc.mjs";
import {
getLast,
getPenultimate,
lineShouldEndWithSemicolon,
printNumber,
shouldFlatten,
maybeStripLeadingSlashFromUse,
fileShouldEndWithHardline,
hasDanglingComments,
docShouldHaveTrailingNewline,
isLookupNode,
isFirstChildrenInlineNode,
shouldPrintHardLineAfterStartInControlStructure,
shouldPrintHardLineBeforeEndInControlStructure,
getAlignment,
isProgramLikeNode,
getNodeKindIncludingLogical,
useDoubleQuote,
hasEmptyBody,
isNextLineEmptyAfterNamespace,
shouldPrintHardlineBeforeTrailingComma,
isDocNode,
getAncestorNode,
isReferenceLikeNode,
normalizeMagicMethodName,
isSimpleCallArgument,
} from "./util.mjs";
const {
breakParent,
join,
line,
lineSuffix,
group,
conditionalGroup,
indent,
dedent,
ifBreak,
hardline,
softline,
literalline,
align,
dedentToRoot,
} = doc.builders;
const { willBreak } = doc.utils;
const {
isNextLineEmptyAfterIndex,
hasNewline,
hasNewlineInRange,
getNextNonSpaceNonCommentCharacterIndex,
isNextLineEmpty,
isPreviousLineEmpty,
} = prettierUtil;
/**
* Determine if we should print a trailing comma based on the config & php version
*
* @param options {object} Prettier Options
* @param requiredVersion {number}
* @returns {boolean}
*/
function shouldPrintComma(options, requiredVersion) {
if (!options.trailingCommaPHP) {
return false;
}
return options.phpVersion >= requiredVersion;
}
function shouldPrintHardlineForOpenBrace(options) {
switch (options.braceStyle) {
case "1tbs":
return false;
case "psr-2":
case "per-cs":
default:
return true;
}
}
function genericPrint(path, options, print) {
const { node } = path;
if (typeof node === "string") {
return node;
}
const printedWithoutParens = printNode(path, options, print);
const parts = [];
const needsParens = pathNeedsParens(path, options);
if (needsParens) {
parts.unshift("(");
}
parts.push(printedWithoutParens);
if (needsParens) {
parts.push(")");
}
if (lineShouldEndWithSemicolon(path)) {
parts.push(";");
}
if (fileShouldEndWithHardline(path)) {
parts.push(hardline);
}
return parts;
}
function printPropertyLookup(path, options, print, nullsafe = false) {
return [nullsafe ? "?" : "", "->", print("offset")];
}
function printNullsafePropertyLookup(path, options, print) {
return printPropertyLookup(path, options, print, true);
}
function printStaticLookup(path, options, print) {
const { node } = path;
const needCurly = !["variable", "identifier"].includes(node.offset.kind);
return ["::", needCurly ? "{" : "", print("offset"), needCurly ? "}" : ""];
}
function printOffsetLookup(path, options, print) {
const { node } = path;
const shouldInline =
(node.offset && node.offset.kind === "number") ||
getAncestorNode(path, "encapsed");
return [
"[",
node.offset
? group([
indent([shouldInline ? "" : softline, print("offset")]),
shouldInline ? "" : softline,
])
: "",
"]",
];
}
// We detect calls on member expressions specially to format a
// common pattern better. The pattern we are looking for is this:
//
// $arr
// ->map(function(x) { return $x + 1; })
// ->filter(function(x) { return $x > 10; })
// ->some(function(x) { return $x % 2; });
//
// The way it is structured in the AST is via a nested sequence of
// propertylookup, staticlookup, offsetlookup and call.
// We need to traverse the AST and make groups out of it
// to print it in the desired way.
function printMemberChain(path, options, print) {
// The first phase is to linearize the AST by traversing it down.
//
// Example:
// a()->b->c()->d();
// has the AST structure
// call (isLookupNode d (
// call (isLookupNode c (
// isLookupNode b (
// call (variable a)
// )
// ))
// ))
// and we transform it into (notice the reversed order)
// [identifier a, call, isLookupNode b, isLookupNode c, call,
// isLookupNode d, call]
const printedNodes = [];
// Here we try to retain one typed empty line after each call expression or
// the first group whether it is in parentheses or not
//
// Example:
// $a
// ->call()
//
// ->otherCall();
//
// ($foo ? $a : $b)
// ->call()
// ->otherCall();
function shouldInsertEmptyLineAfter(node) {
const { originalText } = options;
const nextCharIndex = getNextNonSpaceNonCommentCharacterIndex(
originalText,
locEnd(node)
);
const nextChar = originalText.charAt(nextCharIndex);
// if it is cut off by a parenthesis, we only account for one typed empty
// line after that parenthesis
if (nextChar === ")") {
return isNextLineEmptyAfterIndex(
originalText,
nextCharIndex + 1,
options
);
}
return isNextLineEmpty(originalText, locEnd(node));
}
function traverse(path) {
const { node } = path;
if (
node.kind === "call" &&
(isLookupNode(node.what) || node.what.kind === "call")
) {
printedNodes.unshift({
node,
printed: [
printAllComments(
path,
() => printArgumentsList(path, options, print),
options
),
shouldInsertEmptyLineAfter(node) ? hardline : "",
],
});
path.call((what) => traverse(what), "what");
} else if (isLookupNode(node)) {
// Print *lookup nodes as we standard print them outside member chain
let printedMemberish = null;
if (node.kind === "propertylookup") {
printedMemberish = printPropertyLookup(path, options, print);
} else if (node.kind === "nullsafepropertylookup") {
printedMemberish = printNullsafePropertyLookup(path, options, print);
} else if (node.kind === "staticlookup") {
printedMemberish = printStaticLookup(path, options, print);
} else {
printedMemberish = printOffsetLookup(path, options, print);
}
printedNodes.unshift({
node,
needsParens: pathNeedsParens(path, options),
printed: printAllComments(path, () => printedMemberish, options),
});
path.call((what) => traverse(what), "what");
} else {
printedNodes.unshift({
node,
printed: print(),
});
}
}
const { node } = path;
printedNodes.unshift({
node,
printed: printArgumentsList(path, options, print),
});
path.call((what) => traverse(what), "what");
// Restore parens around `propertylookup` and `staticlookup` nodes with call.
// $value = ($object->foo)();
// $value = ($object::$foo)();
for (let i = 0; i < printedNodes.length; ++i) {
if (
printedNodes[i].node.kind === "call" &&
printedNodes[i - 1] &&
["propertylookup", "nullsafepropertylookup", "staticlookup"].includes(
printedNodes[i - 1].node.kind
) &&
printedNodes[i - 1].needsParens
) {
printedNodes[0].printed = ["(", printedNodes[0].printed];
printedNodes[i - 1].printed = [printedNodes[i - 1].printed, ")"];
}
}
// create groups from list of nodes, i.e.
// [identifier a, call, isLookupNode b, isLookupNode c, call,
// isLookupNode d, call]
// will be grouped as
// [
// [identifier a, Call],
// [isLookupNode b, isLookupNode c, call],
// [isLookupNode d, call]
// ]
// so that we can print it as
// a()
// ->b->c()
// ->d();
const groups = [];
let currentGroup = [printedNodes[0]];
let i = 1;
for (; i < printedNodes.length; ++i) {
if (
printedNodes[i].node.kind === "call" ||
(isLookupNode(printedNodes[i].node) &&
printedNodes[i].node.offset &&
printedNodes[i].node.offset.kind === "number")
) {
currentGroup.push(printedNodes[i]);
} else {
break;
}
}
if (printedNodes[0].node.kind !== "call") {
for (; i + 1 < printedNodes.length; ++i) {
if (
isLookupNode(printedNodes[i].node) &&
isLookupNode(printedNodes[i + 1].node)
) {
currentGroup.push(printedNodes[i]);
} else {
break;
}
}
}
groups.push(currentGroup);
currentGroup = [];
// Then, each following group is a sequence of propertylookup followed by
// a sequence of call. To compute it, we keep adding things to the
// group until we have seen a call in the past and reach a
// propertylookup
let hasSeenCallExpression = false;
for (; i < printedNodes.length; ++i) {
if (hasSeenCallExpression && isLookupNode(printedNodes[i].node)) {
// [0] should be appended at the end of the group instead of the
// beginning of the next one
if (
printedNodes[i].node.kind === "offsetlookup" &&
printedNodes[i].node.offset &&
printedNodes[i].node.offset.kind === "number"
) {
currentGroup.push(printedNodes[i]);
continue;
}
groups.push(currentGroup);
currentGroup = [];
hasSeenCallExpression = false;
}
if (printedNodes[i].node.kind === "call") {
hasSeenCallExpression = true;
}
currentGroup.push(printedNodes[i]);
if (
printedNodes[i].node.comments &&
hasTrailingComment(printedNodes[i].node)
) {
groups.push(currentGroup);
currentGroup = [];
hasSeenCallExpression = false;
}
}
if (currentGroup.length > 0) {
groups.push(currentGroup);
}
// Merge next nodes when:
//
// 1. We have `$this` variable before
//
// Example:
// $this->method()->property;
//
// 2. When we have offsetlookup after *lookup node
//
// Example:
// $foo->Data['key']("foo")
// ->method();
//
// 3. expression statements with variable names shorter than the tab width
//
// Example:
// $foo->bar()
// ->baz()
// ->buzz()
function shouldNotWrap(groups) {
const hasComputed =
groups[1].length && groups[1][0].node.kind === "offsetlookup";
if (groups[0].length === 1) {
const firstNode = groups[0][0].node;
return (
(firstNode.kind === "variable" &&
(firstNode.name === "this" ||
(isExpressionStatement && isShort(firstNode.name)))) ||
isReferenceLikeNode(firstNode)
);
}
function isShort(name) {
return name.length < options.tabWidth;
}
const lastNode = getLast(groups[0]).node;
return (
isLookupNode(lastNode) &&
(lastNode.offset.kind === "identifier" ||
lastNode.offset.kind === "variable") &&
hasComputed
);
}
const isExpressionStatement = path.parent.kind === "expressionstatement";
const shouldMerge =
groups.length >= 2 && !groups[1][0].node.comments && shouldNotWrap(groups);
function printGroup(printedGroup) {
const result = [];
for (let i = 0; i < printedGroup.length; i++) {
// Checks if the next node (i.e. the parent node) needs parens
// and print accordingl y
if (printedGroup[i + 1] && printedGroup[i + 1].needsParens) {
result.push(
"(",
printedGroup[i].printed,
printedGroup[i + 1].printed,
")"
);
i++;
} else {
result.push(printedGroup[i].printed);
}
}
return result;
}
function printIndentedGroup(groups) {
if (groups.length === 0) {
return "";
}
return indent(group([hardline, join(hardline, groups.map(printGroup))]));
}
const printedGroups = groups.map(printGroup);
const oneLine = printedGroups;
// Indicates how many we should merge
//
// Example (true):
// $this->method()->otherMethod(
// 'argument'
// );
//
// Example (false):
// $foo
// ->method()
// ->otherMethod();
const cutoff = shouldMerge ? 3 : 2;
const flatGroups = groups.slice(0, cutoff).flat();
const hasComment =
flatGroups.slice(1, -1).some((node) => hasLeadingComment(node.node)) ||
flatGroups.slice(0, -1).some((node) => hasTrailingComment(node.node)) ||
(groups[cutoff] && hasLeadingComment(groups[cutoff][0].node));
const hasEncapsedAncestor = getAncestorNode(path, "encapsed");
// If we only have a single `->`, we shouldn't do anything fancy and just
// render everything concatenated together.
// In `encapsed` node we always print in one line.
if ((groups.length <= cutoff && !hasComment) || hasEncapsedAncestor) {
return group(oneLine);
}
// Find out the last node in the first group and check if it has an
// empty line after
const lastNodeBeforeIndent = getLast(
shouldMerge ? groups.slice(1, 2)[0] : groups[0]
).node;
const shouldHaveEmptyLineBeforeIndent =
lastNodeBeforeIndent.kind !== "call" &&
shouldInsertEmptyLineAfter(lastNodeBeforeIndent);
const expanded = [
printGroup(groups[0]),
shouldMerge ? groups.slice(1, 2).map(printGroup) : "",
shouldHaveEmptyLineBeforeIndent ? hardline : "",
printIndentedGroup(groups.slice(shouldMerge ? 2 : 1)),
];
const callExpressions = printedNodes.filter(
(tuple) => tuple.node.kind === "call"
);
// We don't want to print in one line if there's:
// * A comment.
// * 3 or more chained calls.
// * Any group but the last one has a hard line.
// If the last group is a function it's okay to inline if it fits.
if (
hasComment ||
(callExpressions.length > 2 &&
callExpressions.some(
(exp) => !exp.node.arguments.every((arg) => isSimpleCallArgument(arg))
)) ||
printedGroups.slice(0, -1).some(willBreak)
) {
return group(expanded);
}
return [
// We only need to check `oneLine` because if `expanded` is chosen
// that means that the parent group has already been broken
// naturally
willBreak(oneLine) || shouldHaveEmptyLineBeforeIndent ? breakParent : "",
conditionalGroup([oneLine, expanded]),
];
}
function couldGroupArg(arg) {
return (
(arg.kind === "array" && (arg.items.length > 0 || arg.comments)) ||
arg.kind === "function" ||
arg.kind === "method" ||
arg.kind === "closure"
);
}
function shouldGroupLastArg(args) {
const lastArg = getLast(args);
const penultimateArg = getPenultimate(args);
return (
!hasLeadingComment(lastArg) &&
!hasTrailingComment(lastArg) &&
couldGroupArg(lastArg) &&
// If the last two arguments are of the same type,
// disable last element expansion.
(!penultimateArg || penultimateArg.kind !== lastArg.kind)
);
}
function shouldGroupFirstArg(args) {
if (args.length !== 2) {
return false;
}
const [firstArg, secondArg] = args;
return (
(!firstArg.comments || !firstArg.comments.length) &&
(firstArg.kind === "function" ||
firstArg.kind === "method" ||
firstArg.kind === "closure") &&
secondArg.kind !== "retif" &&
!couldGroupArg(secondArg)
);
}
function printArgumentsList(path, options, print, argumentsKey = "arguments") {
const args = path.node[argumentsKey];
if (args.length === 0) {
return [
"(",
printDanglingComments(path, options, /* sameIndent */ true),
")",
];
}
let anyArgEmptyLine = false;
let hasEmptyLineFollowingFirstArg = false;
const printedArguments = path.map(({ node: arg, isLast, isFirst }) => {
const parts = [print()];
if (isLast) {
// do nothing
} else if (isNextLineEmpty(options.originalText, locEnd(arg))) {
if (isFirst) {
hasEmptyLineFollowingFirstArg = true;
}
anyArgEmptyLine = true;
parts.push(",", hardline, hardline);
} else {
parts.push(",", line);
}
return parts;
}, argumentsKey);
const { node } = path;
const lastArg = getLast(args);
const maybeTrailingComma =
(shouldPrintComma(options, 7.3) &&
["call", "new", "unset", "isset"].includes(node.kind)) ||
(shouldPrintComma(options, 8.0) &&
["function", "closure", "method", "arrowfunc", "attribute"].includes(
node.kind
))
? indent([
lastArg && shouldPrintHardlineBeforeTrailingComma(lastArg)
? hardline
: "",
",",
])
: "";
function allArgsBrokenOut() {
return group(
["(", indent([line, ...printedArguments]), maybeTrailingComma, line, ")"],
{ shouldBreak: true }
);
}
const shouldGroupFirst = shouldGroupFirstArg(args);
const shouldGroupLast = shouldGroupLastArg(args);
if (shouldGroupFirst || shouldGroupLast) {
const shouldBreak =
(shouldGroupFirst
? printedArguments.slice(1).some(willBreak)
: printedArguments.slice(0, -1).some(willBreak)) || anyArgEmptyLine;
// We want to print the last argument with a special flag
let printedExpanded;
path.each(({ isLast, isFirst }) => {
if (shouldGroupFirst && isFirst) {
printedExpanded = [
print([], { expandFirstArg: true }),
printedArguments.length > 1 ? "," : "",
hasEmptyLineFollowingFirstArg ? hardline : line,
hasEmptyLineFollowingFirstArg ? hardline : "",
printedArguments.slice(1),
];
}
if (shouldGroupLast && isLast) {
printedExpanded = [
...printedArguments.slice(0, -1),
print([], { expandLastArg: true }),
];
}
}, argumentsKey);
const somePrintedArgumentsWillBreak = printedArguments.some(willBreak);
const simpleConcat = ["(", ...printedExpanded, ")"];
return [
somePrintedArgumentsWillBreak ? breakParent : "",
conditionalGroup(
[
!somePrintedArgumentsWillBreak
? simpleConcat
: ifBreak(allArgsBrokenOut(), simpleConcat),
shouldGroupFirst
? [
"(",
group(printedExpanded[0], { shouldBreak: true }),
...printedExpanded.slice(1),
")",
]
: [
"(",
...printedArguments.slice(0, -1),
group(getLast(printedExpanded), {
shouldBreak: true,
}),
")",
],
group(
[
"(",
indent([line, ...printedArguments]),
ifBreak(maybeTrailingComma),
line,
")",
],
{ shouldBreak: true }
),
],
{ shouldBreak }
),
];
}
return group(
[
"(",
indent([softline, ...printedArguments]),
ifBreak(maybeTrailingComma),
softline,
")",
],
{
shouldBreak: printedArguments.some(willBreak) || anyArgEmptyLine,
}
);
}
function shouldInlineRetifFalseExpression(node) {
return node.kind === "array" && node.items.length !== 0;
}
function shouldInlineLogicalExpression(node) {
return node.right.kind === "array" && node.right.items.length !== 0;
}
// For binary expressions to be consistent, we need to group
// subsequent operators with the same precedence level under a single
// group. Otherwise they will be nested such that some of them break
// onto new lines but not all. Operators with the same precedence
// level should either all break or not. Because we group them by
// precedence level and the AST is structured based on precedence
// level, things are naturally broken up correctly, i.e. `&&` is
// broken before `+`.
function printBinaryExpression(
path,
print,
options,
isNested,
isInsideParenthesis
) {
let parts = [];
const { node } = path;
if (node.kind === "bin") {
// Put all operators with the same precedence level in the same
// group. The reason we only need to do this with the `left`
// expression is because given an expression like `1 + 2 - 3`, it
// is always parsed like `((1 + 2) - 3)`, meaning the `left` side
// is where the rest of the expression will exist. Binary
// expressions on the right side mean they have a difference
// precedence level and should be treated as a separate group, so
// print them normally. (This doesn't hold for the `**` operator,
// which is unique in that it is right-associative.)
if (shouldFlatten(node.type, node.left.type)) {
// Flatten them out by recursively calling this function.
parts = parts.concat(
path.call(
() =>
printBinaryExpression(
path,
print,
options,
/* isNested */ true,
isInsideParenthesis
),
"left"
)
);
} else {
parts.push(print("left"));
}
const shouldInline = shouldInlineLogicalExpression(node);
const right = shouldInline
? [node.type, " ", print("right")]
: [node.type, line, print("right")];
// If there's only a single binary expression, we want to create a group
// in order to avoid having a small right part like -1 be on its own line.
const { parent } = path;
const shouldGroup =
!(isInsideParenthesis && ["||", "&&"].includes(node.type)) &&
getNodeKindIncludingLogical(parent) !==
getNodeKindIncludingLogical(node) &&
getNodeKindIncludingLogical(node.left) !==
getNodeKindIncludingLogical(node) &&
getNodeKindIncludingLogical(node.right) !==
getNodeKindIncludingLogical(node);
const shouldNotHaveWhitespace =
isDocNode(node.left) ||
(node.left.kind === "bin" && isDocNode(node.left.right));
parts.push(
shouldNotHaveWhitespace ? "" : " ",
shouldGroup ? group(right) : right
);
// The root comments are already printed, but we need to manually print
// the other ones since we don't call the normal print on bin,
// only for the left and right parts
if (isNested && node.comments) {
parts = printAllComments(path, () => parts, options);
}
} else {
// Our stopping case. Simply print the node normally.
parts.push(print());
}
return parts;
}
function printLookupNodes(path, options, print) {
const { node } = path;
switch (node.kind) {
case "propertylookup":
return printPropertyLookup(path, options, print);
case "nullsafepropertylookup":
return printNullsafePropertyLookup(path, options, print);
case "staticlookup":
return printStaticLookup(path, options, print);
case "offsetlookup":
return printOffsetLookup(path, options, print);
/* c8 ignore next 2 */
default:
throw new Error(`Have not implemented lookup kind ${node.kind} yet.`);
}
}
function getEncapsedQuotes(node, { opening = true } = {}) {
if (node.type === "heredoc") {
return opening ? `<<<${node.label}` : node.label;
}
const quotes = {
string: '"',
shell: "`",
};
if (quotes[node.type]) {
return quotes[node.type];
}
/* c8 ignore next */
throw new Error(`Unimplemented encapsed type ${node.type}`);
}
function printArrayItems(path, options, print) {
const printedElements = [];
let separatorParts = [];
path.each(({ node }) => {
printedElements.push(separatorParts);
printedElements.push(group(print()));
separatorParts = [",", line];
if (node && isNextLineEmpty(options.originalText, locEnd(node))) {
separatorParts.push(softline);
}
}, "items");
return printedElements;
}
// Wrap parts into groups by indexes.
// It is require to have same indent on lines for all parts into group.
// The value of `alignment` option indicates how many spaces must be before each part.
//
// Example:
// <div>
// <?php
// echo '1';
// echo '2';
// echo '3';
// ?>
// </div>
function wrapPartsIntoGroups(parts, indexes) {
if (indexes.length === 0) {
return parts;
}
let lastEnd = 0;
return indexes.reduce((accumulator, index) => {
const { start, end, alignment, before, after } = index;
const printedPartsForGrouping = [
before || "",
...parts.slice(start, end),
after || "",
];
const newArray = accumulator.concat(
parts.slice(lastEnd, start),
alignment
? dedentToRoot(
group(
align(new Array(alignment).join(" "), printedPartsForGrouping)
)
)
: group(printedPartsForGrouping),
end === parts.length - 1 ? parts.slice(end) : ""
);
lastEnd = end;
return newArray;
}, []);
}
function printLines(path, options, print, childrenAttribute = "children") {
const { node, parent: parentNode } = path;
let lastInlineIndex = -1;
const parts = [];
const groupIndexes = [];
path.map(() => {
const {
node: childNode,
next: nextNode,
isFirst: isFirstNode,
isLast: isLastNode,
index,
} = path;
const isInlineNode = childNode.kind === "inline";
const printedPath = print();
const canPrintBlankLine =
!isLastNode &&
!isInlineNode &&
(nextNode && nextNode.kind === "case"
? !isFirstChildrenInlineNode(path)
: nextNode && nextNode.kind !== "inline");
let printed = [
printedPath,
canPrintBlankLine ? hardline : "",
canPrintBlankLine &&
isNextLineEmpty(options.originalText, locEnd(childNode))
? hardline
: "",
];
const isBlockNestedNode =
node.kind === "block" &&
parentNode &&
["function", "closure", "method", "try", "catch"].includes(
parentNode.kind
);
let beforeCloseTagInlineNode = isBlockNestedNode && isFirstNode ? "" : " ";
if (isInlineNode || (!isInlineNode && isLastNode && lastInlineIndex >= 0)) {
const prevLastInlineIndex = lastInlineIndex;
if (isInlineNode) {
lastInlineIndex = index;
}
const shouldCreateGroup =
(isInlineNode && !isFirstNode) || (!isInlineNode && isLastNode);
if (shouldCreateGroup) {
const start =
(isInlineNode ? prevLastInlineIndex : lastInlineIndex) + 1;
const end = isLastNode && !isInlineNode ? index + 1 : index;
const prevInlineNode =
path.siblings[isInlineNode ? prevLastInlineIndex : lastInlineIndex];
const alignment = prevInlineNode
? getAlignment(prevInlineNode.raw)
: "";
const shouldBreak = end - start > 1;
const before = shouldBreak
? (isBlockNestedNode && !prevInlineNode) ||
(isProgramLikeNode(node) && start === 0)
? ""
: hardline
: "";
const after =
shouldBreak && childNode.kind !== "halt"
? isBlockNestedNode && isLastNode
? ""
: hardline
: "";
if (shouldBreak) {
beforeCloseTagInlineNode = "";
}
groupIndexes.push({ start, end, alignment, before, after });
}
}
if (isInlineNode) {
const openTag =
nextNode && nextNode.kind === "echo" && nextNode.shortForm
? "<?="
: "<?php";
const beforeInline =
childNode.leadingComments && childNode.leadingComments.length
? [
isFirstNode && node.kind !== "namespace" && !isBlockNestedNode
? "<?php"
: "",
node.kind === "namespace" || !isBlockNestedNode ? hardline : "",
printComments(childNode.leadingComments, options),
hardline,
"?>",
]
: isProgramLikeNode(node) && isFirstNode && node.kind !== "namespace"
? ""
: [beforeCloseTagInlineNode, "?>"];
//FIXME getNode is used to get ancestors, but it seems this means to get next sibling?
const nextV = path.getNode(index + 1);
const skipLastComment = nextV && nextV.children && nextV.children.length;
const afterInline =
childNode.comments && childNode.comments.length
? [
openTag,
hardline,
skipLastComment ? printComments(childNode.comments, options) : "",
hardline,
]
: isProgramLikeNode(node) && isLastNode
? ""
: [openTag, " "];
printed = [beforeInline, printed, afterInline];
}
parts.push(printed);
}, childrenAttribute);
const wrappedParts = wrapPartsIntoGroups(parts, groupIndexes);
if (node.kind === "program" && !node.extra.parseAsEval) {
const parts = [];
const [firstNode] = node.children;
const hasStartTag = !firstNode || firstNode.kind !== "inline";
if (hasStartTag) {
const between = options.originalText.trim().match(/^<\?(php|=)(\s+)?\S/);
const afterOpenTag = [
between && between[2] && between[2].includes("\n")
? [hardline, between[2].split("\n").length > 2 ? hardline : ""]
: " ",
node.comments ? printComments(node.comments, options) : "",
];
const shortEcho =
firstNode && firstNode.kind === "echo" && firstNode.shortForm;
parts.push([shortEcho ? "<?=" : "<?php", afterOpenTag]);
}
parts.push(wrappedParts);
const hasEndTag = /\?>\n?$/.test(options.originalText);
if (hasEndTag) {
const lastNode = getLast(node.children);
const beforeCloseTag = lastNode
? [
hasNewlineInRange(
options.originalText.trimEnd(),
locEnd(lastNode),
locEnd(node)
)
? !(
lastNode.kind === "inline" &&
lastNode.comments &&
lastNode.comments.length
)
? hardline
: ""
: " ",
isNextLineEmpty(options.originalText, locEnd(lastNode))
? hardline
: "",
]
: node.comments
? hardline
: "";
parts.push(lineSuffix([beforeCloseTag, "?>"]));
}
return parts;
}
return wrappedParts;
}
function printStatements(path, options, print, childrenAttribute) {
return path.map(({ node, isLast }) => {
const parts = [];
parts.push(print());
if (!isLast) {
parts.push(hardline);
if (isNextLineEmpty(options.originalText, locEnd(node))) {
parts.push(hardline);
}
}
return parts;
}, childrenAttribute);
}
function printClassPart(
path,
options,
print,
part = "extends",
beforePart = " ",
afterPart = " "
) {
const value = path.node[part];
const printedBeforePart = hasDanglingComments(value)
? [
hardline,
path.call(() => printDanglingComments(path, options, true), part),
hardline,
]
: beforePart;
const printedPartItems = Array.isArray(value)
? group(
join(
",",
path.map(({ node }) => {
const printedPart = print();
// Check if any of the implements nodes have comments
return hasDanglingComments(node)
? [
hardline,
printDanglingComments(path, options, true),
hardline,
printedPart,
]
: [afterPart, printedPart];
}, part)
)
)
: [afterPart, print(part)];
return indent([
printedBeforePart,
part,
willBreak(printedBeforePart) ? indent(printedPartItems) : printedPartItems,
]);
}
function printAttrs(path, options, print, { inline = false } = {}) {
const allAttrs = [];
if (!path.node.attrGroups) {
return [];
}
path.each(() => {
const attrGroup = ["#["];
if (!inline && allAttrs.length > 0) {
allAttrs.push(hardline);
}
attrGroup.push(softline);
path.each(() => {
const attrNode = path.node;
if (attrGroup.length > 2) {
attrGroup.push(",", line);
}
const attrStmt = [attrNode.name];
if (attrNode.args.length > 0) {
attrStmt.push(printArgumentsList(path, options, print, "args"));
}
attrGroup.push(group(attrStmt));
}, "attrs");
allAttrs.push(
group([
indent(attrGroup),
ifBreak(shouldPrintComma(options, 8.0) ? "," : ""),
softline,
"]",
inline ? ifBreak(softline, " ") : "",
])
);
}, "attrGroups");
if (allAttrs.length === 0) {
return [];
}
return [...allAttrs, inline ? "" : hardline];
}
function printClass(path, options, print) {
const { node } = path;
const isAnonymousClass = node.kind === "class" && node.isAnonymous;
const attrs = printAttrs(path, options, print, { inline: isAnonymousClass });
const declaration = isAnonymousClass ? [] : [...attrs];
if (node.isFinal) {
declaration.push("final ");
}
if (node.isAbstract) {
declaration.push("abstract ");
}
if (node.isReadonly) {
declaration.push("readonly ");
}
// `new` print `class` keyword with arguments
declaration.push(isAnonymousClass ? "" : node.kind);
if (node.name) {
declaration.push(" ", print("name"));
}
if (node.kind === "enum" && node.valueType) {
declaration.push(": ", print("valueType"));
}
// Only `class` can have `extends` and `implements`
if (node.extends && node.implements) {
declaration.push(
conditionalGroup(
[
[
printClassPart(path, options, print, "extends"),
printClassPart(path, options, print, "implements"),
],
[
printClassPart(path, options, print, "extends"),
printClassPart(path, options, print, "implements", " ", hardline),
],
[
printClassPart(path, options, print, "extends", hardline, " "),
printClassPart(
path,
options,
print,
"implements",
hardline,
node.implements.length > 1 ? hardline : " "
),
],
],
{
shouldBreak: hasDanglingComments(node.extends),
}
)
);
} else {
if (node.extends) {
declaration.push(
conditionalGroup([
printClassPart(path, options, print, "extends"),
printClassPart(path, options, print, "extends", " ", hardline),
printClassPart(
path,
options,
print,
"extends",
hardline,
node.extends.length > 1 ? hardline : " "
),
])
);
}
if (node.implements) {
declaration.push(
conditionalGroup([
printClassPart(path, options, print, "implements"),
printClassPart(path, options, print, "implements", " ", hardline),
printClassPart(
path,
options,
print,
"implements",
hardline,
node.implements.length > 1 ? hardline : " "
),
])
);
}
}
const hasEmptyClassBody =
node.body && node.body.length === 0 && !hasDanglingComments(node);
const printedDeclaration = group([
group(declaration),
shouldPrintHardlineForOpenBrace(options) && !hasEmptyClassBody
? isAnonymousClass
? line
: hardline
: " ",
]);
const printedBody = [
"{",
indent([
hasEmptyClassBody ? "" : hardline,
printStatements(path, options, print, "body"),
]),
printDanglingComments(path, options, true),
hasEmptyClassBody ? "" : hardline,
"}",
];
return [printedDeclaration, printedBody];
}
function printFunction(path, options, print) {
const { node } = path;
const declAttrs = printAttrs(path, options, print, {
inline: node.kind === "closure",
});
const declaration = [];
if (node.isFinal) {
declaration.push("final ");
}
if (node.isAbstract) {
declaration.push("abstract ");
}
if (node.visibility) {
declaration.push(node.visibility, " ");
}
if (node.isStatic) {
declaration.push("static ");
}
declaration.push("function ");
if (node.byref) {
declaration.push("&");
}
if (node.name) {
declaration.push(print("name"));
}
declaration.push(printArgumentsList(path, options, print));
if (node.uses && node.uses.length > 0) {
declaration.push(
group([" use ", printArgumentsList(path, options, print, "uses")])
);
}
if (node.type) {
declaration.push([
": ",
hasDanglingComments(node.type)
? [
path.call(() => printDanglingComments(path, options, true), "type"),
" ",
]
: "",
node.nullable ? "?" : "",
print("type"),
]);
}
const printedDeclaration = declaration;
if (!node.body) {
return [...declAttrs, printedDeclaration];
}
const printedBody = [
"{",
indent([hasEmptyBody(path) ? "" : hardline, print("body")]),
hasEmptyBody(path) ? "" : hardline,
"}",
];
const isClosure = node.kind === "closure";
if (isClosure) {
return [...declAttrs, printedDeclaration, " ", printedBody];
}
if (node.arguments.length === 0) {
return [
...declAttrs,
printedDeclaration,
shouldPrintHardlineForOpenBrace(options) && !hasEmptyBody(path)
? hardline
: " ",
printedBody,
];
}
const willBreakDeclaration = declaration.some(willBreak);
if (willBreakDeclaration) {
return [...declAttrs, printedDeclaration, " ", printedBody];
}
return [
...declAttrs,
conditionalGroup([
[
printedDeclaration,
shouldPrintHardlineForOpenBrace(options) && !hasEmptyBody(path)
? hardline
: " ",
printedBody,
],
[printedDeclaration, " ", printedBody],
]),
];
}
function printBodyControlStructure(
path,
options,
print,
bodyProperty = "body"
) {
const { node } = path;
if (!node[bodyProperty]) {
return ";";
}
const printedBody = print(bodyProperty);
return [
node.shortForm ? ":" : " {",
indent(
node[bodyProperty].kind !== "block" ||
(node[bodyProperty].children &&
node[bodyProperty].children.length > 0) ||
(node[bodyProperty].comments && node[bodyProperty].comments.length > 0)
? [
shouldPrintHardLineAfterStartInControlStructure(path)
? node.kind === "switch"
? " "
: ""
: hardline,
printedBody,
]
: ""
),
node.kind === "if" && bodyProperty === "body"
? ""
: [
shouldPrintHardLineBeforeEndInControlStructure(path) ? hardline : "",
node.shortForm ? ["end", node.kind, ";"] : "}",
],
];
}
function printAssignment(
leftNode,
printedLeft,
operator,
rightNode,
printedRight,
hasRef,
options
) {
if (!rightNode) {
return printedLeft;
}
const printed = printAssignmentRight(
leftNode,
rightNode,
printedRight,
hasRef,
options
);
return group([printedLeft, operator, printed]);
}
function isLookupNodeChain(node) {
if (!isLookupNode(node)) {
return false;
}
if (node.what.kind === "variable" || isReferenceLikeNode(node.what)) {
return true;
}
return isLookupNodeChain(node.what);
}
function printAssignmentRight(
leftNode,
rightNode,
printedRight,
hasRef,
options
) {
const ref = hasRef ? "&" : "";
if (hasLeadingOwnLineComment(options.originalText, rightNode)) {
return indent([hardline, ref, printedRight]);
}
const pureRightNode = rightNode.kind === "cast" ? rightNode.expr : rightNode;
const canBreak =
(pureRightNode.kind === "bin" &&
!shouldInlineLogicalExpression(pureRightNode)) ||
(pureRightNode.kind === "retif" &&
((!pureRightNode.trueExpr &&
!shouldInlineRetifFalseExpression(pureRightNode.falseExpr)) ||
(pureRightNode.test.kind === "bin" &&
!shouldInlineLogicalExpression(pureRightNode.test)))) ||
((leftNode.kind === "variable" ||
leftNode.kind === "string" ||
isLookupNode(leftNode)) &&
((pureRightNode.kind === "string" && !stringHasNewLines(pureRightNode)) ||
isLookupNodeChain(pureRightNode)));
if (canBreak) {
return group(indent([line, ref, printedRight]));
}
return [" ", ref, printedRight];
}
function needsHardlineAfterDanglingComment(node) {
if (!node.comments) {
return false;
}
const lastDanglingComment = getLast(
node.comments.filter((comment) => !comment.leading && !comment.trailing)
);
return lastDanglingComment && !isBlockComment(lastDanglingComment);
}
function stringHasNewLines(node) {
return node.raw.includes("\n");
}
function isStringOnItsOwnLine(node, text) {
return (
(node.kind === "string" ||
(node.kind === "encapsed" &&
(node.type === "string" || node.type === "shell"))) &&
stringHasNewLines(node) &&
!hasNewline(text, locStart(node), { backwards: true })
);
}
function printComposedTypes(path, print, glue) {
return group(
path.map(({ isFirst }) => (isFirst ? [print()] : [glue, print()]), "types")
);
}
function printNode(path, options, print) {
const { node } = path;
switch (node.kind) {
case "program": {
return group([
printLines(path, options, print),
printDanglingComments(
path,
options,
/* sameIndent */ true,
(c) => !c.printed
),
]);
}
case "expressionstatement":
return print("expression");
case "block":
return [
printLines(path, options, print),
printDanglingComments(path, options, true),
];
case "declare": {
const printDeclareArguments = (path) => {
return join(", ", path.map(print, "directives"));
};
if (["block", "short"].includes(node.mode)) {
return [
"declare(",
printDeclareArguments(path),
")",
node.mode === "block" ? " {" : ":",
node.children.length > 0
? indent([hardline, printLines(path, options, print)])
: "",
printDanglingComments(path, options),
hardline,
node.mode === "block" ? "}" : "enddeclare;",
];
}
return [
"declare(",
printDeclareArguments(path),
")",
path.next?.kind === "inline" ? "" : ";",
];
}
case "declaredirective":
return [print("key"), "=", print("value")];
case "namespace":
return [
"namespace ",
node.name && typeof node.name === "string"
? [node.name, node.withBrackets ? " " : ""]
: "",
node.withBrackets ? "{" : ";",
hasDanglingComments(node)
? [" ", printDanglingComments(path, options, true)]
: "",
node.children.length > 0
? node.withBrackets
? indent([hardline, printLines(path, options, print)])
: [
node.children[0].kind === "inline"
? ""
: [
hardline,
isNextLineEmptyAfterNamespace(options.originalText, node)
? hardline
: "",
],
printLines(path, options, print),
]
: "",
node.withBrackets ? [hardline, "}"] : "",
];
case "usegroup":
return group([
"use ",
node.type ? [node.type, " "] : "",
indent([
node.name
? [maybeStripLeadingSlashFromUse(node.name), "\\{", softline]
: "",
join([",", line], path.map(print, "items")),
]),
node.name
? [ifBreak(shouldPrintComma(options, 7.2) ? "," : ""), softline, "}"]
: "",
]);
case "useitem":
return [
node.type ? [node.type, " "] : "",
maybeStripLeadingSlashFromUse(node.name),
hasDanglingComments(node)
? [" ", printDanglingComments(path, options, true)]
: "",
node.alias ? [" as ", print("alias")] : "",
];
case "class":
case "enum":
case "interface":
case "trait":
return printClass(path, options, print);
case "traitprecedence":
return [
print("trait"),
"::",
print("method"),
" insteadof ",
join(", ", path.map(print, "instead")),
];
case "traitalias":
return [
node.trait ? [print("trait"), "::"] : "",
node.method ? print("method") : "",
" as ",
join(" ", [
...(node.visibility ? [node.visibility] : []),
...(node.as ? [print("as")] : []),
]),
];
case "traituse":
return group([
"use ",
indent(group(join([",", line], path.map(print, "traits")))),
node.adaptations
? [
" {",
node.adaptations.length > 0
? [
indent([
hardline,
printStatements(path, options, print, "adaptations"),
]),
hardline,
]
: hasDanglingComments(node)
? [line, printDanglingComments(path, options, true), line]
: "",
"}",
]
: "",
]);
case "function":
case "closure":
case "method":
return printFunction(path, options, print);
case "arrowfunc":
return [
node.parenthesizedExpression ? "(" : "",
...printAttrs(path, options, print, { inline: true }),
node.isStatic ? "static " : "",
"fn",
printArgumentsList(path, options, print),
node.type ? [": ", node.nullable ? "?" : "", print("type")] : "",
" => ",
print("body"),
node.parenthesizedExpression ? ")" : "",
];
case "parameter": {
let promoted = "";
if (node.flags === 1) {
promoted = "public ";
} else if (node.flags === 2) {
promoted = "protected ";
} else if (node.flags === 4) {
promoted = "private ";
}
const name = [
...printAttrs(path, options, print, { inline: true }),
promoted,
node.readonly ? "readonly " : "",
node.nullable ? "?" : "",
node.type ? [print("type"), " "] : "",
node.byref ? "&" : "",
node.variadic ? "..." : "",
"$",
print("name"),
];
if (node.value) {
return group([
name,
// see handleFunctionParameter() in ./comments.mjs - since there's
// no node to attach comments that fall in between the parameter name
// and value, we store them as dangling comments
hasDanglingComments(node) ? " " : "",
printDanglingComments(path, options, true),
" =",
printAssignmentRight(
node.name,
node.value,
print("value"),
false,
options
),
]);
}
return name;
}
case "variadic":
return ["...", print("what")];
case "property":
return group([
node.readonly ? "readonly " : "",
node.type ? [node.nullable ? "?" : "", print("type"), " "] : "",
"$",
print("name"),
node.value
? [
" =",
printAssignmentRight(
node.name,
node.value,
print("value"),
false,
options
),
]
: "",
]);
case "propertystatement": {
const attrs = [];
path.each(() => {
attrs.push(...printAttrs(path, options, print));
}, "properties");
const printed = path.map(print, "properties");
const hasValue = node.properties.some((property) => property.value);
let firstProperty;
if (printed.length === 1 && !node.properties[0].comments) {
[firstProperty] = printed;
} else if (printed.length > 0) {
// Indent first property
firstProperty = indent(printed[0]);
}
const hasVisibility = node.visibility || node.visibility === null;
return group([
...attrs,
hasVisibility
? [node.visibility === null ? "var" : node.visibility, ""]
: "",
node.isStatic ? [hasVisibility ? " " : "", "static"] : "",
firstProperty ? [" ", firstProperty] : "",
indent(
printed.slice(1).map((p) => [",", hasValue ? hardline : line, p])
),
]);
}
case "if": {
const parts = [];
const body = printBodyControlStructure(path, options, print, "body");
const opening = group([
"if (",
group([indent([softline, print("test")]), softline]),
")",
body,
]);
parts.push(
opening,
isFirstChildrenInlineNode(path) || !node.body ? "" : hardline
);
if (node.alternate) {
parts.push(node.shortForm ? "" : "} ");
const commentOnOwnLine =
(hasTrailingComment(node.body) &&
node.body.comments.some(
(comment) => comment.trailing && !isBlockComment(comment)
)) ||
needsHardlineAfterDanglingComment(node);
const elseOnSameLine = !commentOnOwnLine;
parts.push(elseOnSameLine ? "" : hardline);
if (hasDanglingComments(node)) {
parts.push(
isNextLineEmpty(options.originalText, locEnd(node.body))
? hardline
: "",
printDanglingComments(path, options, true),
commentOnOwnLine ? hardline : " "
);
}
parts.push(
"else",
group(
node.alternate.kind === "if"
? print("alternate")
: printBodyControlStructure(path, options, print, "alternate")
)
);
} else {
parts.push(node.body ? (node.shortForm ? "endif;" : "}") : "");
}
return parts;
}
case "do":
return [
"do",
printBodyControlStructure(path, options, print, "body"),
" while (",
group([indent([softline, print("test")]), softline]),
")",
];
case "while":
case "switch":
return group([
node.kind,
" (",
group([indent([softline, print("test")]), softline]),
")",
printBodyControlStructure(path, options, print, "body"),
]);
case "for": {
const body = printBodyControlStructure(path, options, print, "body");
// We want to keep dangling comments above the loop to stay consistent.
// Any comment positioned between the for statement and the parentheses
// is going to be printed before the statement.
const dangling = printDanglingComments(
path,
options,
/* sameLine */ true
);
const printedComments = dangling ? [dangling, softline] : "";
if (!node.init.length && !node.test.length && !node.increment.length) {
return [printedComments, group(["for (;;)", body])];
}
return [
printedComments,
group([
"for (",
group([
indent([
softline,
group(join([",", line], path.map(print, "init"))),
";",
line,
group(join([",", line], path.map(print, "test"))),
";",
line,
group(join([",", line], path.map(print, "increment"))),
]),
softline,
]),
")",
body,
]),
];
}
case "foreach": {
const body = printBodyControlStructure(path, options, print, "body");
// We want to keep dangling comments above the loop to stay consistent.
// Any comment positioned between the for statement and the parentheses
// is going to be printed before the statement.
const dangling = printDanglingComments(
path,
options,
/* sameLine */ true
);
const printedComments = dangling ? [dangling, softline] : "";
return [
printedComments,
group([
"foreach (",
group([
indent([
softline,
print("source"),
line,
"as ",
group(
node.key
? indent(join([" =>", line], [print("key"), print("value")]))
: print("value")
),
]),
softline,
]),
")",
body,
]),
];
}
case "try": {
const parts = [];
parts.push(
"try",
printBodyControlStructure(path, options, print, "body")
);
if (node.catches) {
parts.push(path.map(print, "catches"));
}
if (node.always) {
parts.push(
" finally",
printBodyControlStructure(path, options, print, "always")
);
}
return parts;
}
case "catch": {
return [
" catch",
node.what
? [
" (",
join(" | ", path.map(print, "what")),
node.variable ? [" ", print("variable")] : "",
")",
]
: "",
printBodyControlStructure(path, options, print, "body"),
];
}
case "case":
return [
node.test
? [
"case ",
node.test.comments ? indent(print("test")) : print("test"),
":",
]
: "default:",
node.body
? node.body.children && node.body.children.length
? indent([
isFirstChildrenInlineNode(path) ? "" : hardline,
print("body"),
])
: ""
: "",
];
case "break":
case "continue":
if (node.level) {
if (node.level.kind === "number" && node.level.value !== "1") {
return [`${node.kind} `, print("level")];
}
return node.kind;
}
return node.kind;
case "call": {
// Multiline strings as single arguments
if (
node.arguments.length === 1 &&
isStringOnItsOwnLine(node.arguments[0], options.originalText)
) {
return [
print("what"),
"(",
join(", ", path.map(print, "arguments")),
")",
];
}
// chain: Call (*LookupNode (Call (*LookupNode (...))))
if (isLookupNode(node.what)) {
return printMemberChain(path, options, print);
}
return [print("what"), printArgumentsList(path, options, print)];
}
case "new": {
const isAnonymousClassNode =
node.what && node.what.kind === "class" && node.what.isAnonymous;
// Multiline strings as single arguments
if (
!isAnonymousClassNode &&
node.arguments.length === 1 &&
isStringOnItsOwnLine(node.arguments[0], options.originalText)
) {
return [
"new ",
...path.call(printAttrs, "what"),
print("what"),
"(",
join(", ", path.map(print, "arguments")),
")",
];
}
const parts = [];
parts.push("new ");
if (isAnonymousClassNode) {
parts.push(
node.what.leadingComments &&
node.what.leadingComments[0].kind === "commentblock"
? [printComments(node.what.leadingComments, options), " "]
: "",
...path.call(
() => printAttrs(path, options, print, { inline: true }),
"what"
),
"class",
node.arguments.length > 0
? [" ", printArgumentsList(path, options, print)]
: "",
group(print("what"))
);
} else {
const isExpression = ["call", "offsetlookup"].includes(node.what.kind);
const printed = [
isExpression ? "(" : "",
print("what"),
isExpression ? ")" : "",
printArgumentsList(path, options, print),
];
parts.push(hasLeadingComment(node.what) ? indent(printed) : printed);
}
return parts;
}
case "clone":
return [
"clone ",
node.what.comments ? indent(print("what")) : print("what"),
];
case "propertylookup":
case "nullsafepropertylookup":
case "staticlookup":
case "offsetlookup": {
const { parent } = path;
// TODO: Use `AstPath.findAncestor` when it's stable
let firstNonMemberParent;
let i = 0;
do {
firstNonMemberParent = path.getParentNode(i);
i++;
} while (firstNonMemberParent && isLookupNode(firstNonMemberParent));
const hasEncapsedAncestor = getAncestorNode(path, "encapsed");
const shouldInline =
hasEncapsedAncestor ||
(firstNonMemberParent &&
(firstNonMemberParent.kind === "new" ||
(firstNonMemberParent.kind === "assign" &&
firstNonMemberParent.left.kind !== "variable"))) ||
node.kind === "offsetlookup" ||
((isReferenceLikeNode(node.what) || node.what.kind === "variable") &&
["identifier", "variable", "encapsedpart"].includes(
node.offset.kind
) &&
parent &&
!isLookupNode(parent));
return [
print("what"),
shouldInline
? printLookupNodes(path, options, print)
: group(indent([softline, printLookupNodes(path, options, print)])),
];
}
case "exit":
return group([
node.useDie ? "die" : "exit",
"(",
node.expression
? isStringOnItsOwnLine(node.expression, options.originalText)
? print("expression")
: [indent([softline, print("expression")]), softline]
: printDanglingComments(path, options),
")",
]);
case "global":
return group([
"global ",
indent(join([",", line], path.map(print, "items"))),
]);
case "include":
return [
node.require ? "require" : "include",
node.once ? "_once" : "",
" ",
node.target.comments ? indent(print("target")) : print("target"),
];
case "label":
return [print("name"), ":"];
case "goto":
return ["goto ", print("label")];
case "throw":
return [
"throw ",
node.what.comments ? indent(print("what")) : print("what"),
];
case "silent":
return ["@", print("expr")];
case "halt":
return [
hasDanglingComments(node)
? [
printDanglingComments(path, options, /* sameIndent */ true),
hardline,
]
: "",
"__halt_compiler();",
node.after,
];
case "eval":
return group([
"eval(",
isStringOnItsOwnLine(node.source, options.originalText)
? print("source")
: [indent([softline, print("source")]), softline],
")",
]);
case "echo": {
const printedArguments = path.map(print, "expressions");
let firstVariable;
if (printedArguments.length === 1 && !node.expressions[0].comments) {
[firstVariable] = printedArguments;
} else if (printedArguments.length > 0) {
firstVariable =
isDocNode(node.expressions[0]) || node.expressions[0].comments
? indent(printedArguments[0])
: dedent(printedArguments[0]);
}
return group([
node.shortForm ? "" : "echo ",
firstVariable ? firstVariable : "",
indent(printedArguments.slice(1).map((p) => [",", line, p])),
]);
}
case "print": {
return [
"print ",
node.expression.comments
? indent(print("expression"))
: print("expression"),
];
}
case "return": {
const parts = [];
parts.push("return");
if (node.expr) {
const printedExpr = print("expr");
parts.push(" ", node.expr.comments ? indent(printedExpr) : printedExpr);
}
if (hasDanglingComments(node)) {
parts.push(
" ",
printDanglingComments(path, options, /* sameIndent */ true)
);
}
return parts;
}
case "isset":
case "unset":
return group([
node.kind,
printArgumentsList(path, options, print, "variables"),
]);
case "empty":
return group([
"empty(",
indent([softline, print("expression")]),
softline,
")",
]);
case "variable": {
const { parent, grandparent: parentParent } = path;
const ampersand = parent.kind === "assign" ? "" : node.byref ? "&" : "";
const dollar =
(parent.kind === "encapsedpart" &&
parent.syntax === "simple" &&
parent.curly) ||
(parentParent &&
parent.kind === "offsetlookup" &&
parentParent.kind === "encapsedpart" &&
parentParent.syntax === "simple" &&
parentParent.curly)
? ""
: "$";
const openCurly = node.curly ? "{" : "";
const closeCurly = node.curly ? "}" : "";
return [ampersand, dollar, openCurly, print("name"), closeCurly];
}
case "constantstatement":
case "classconstant": {
const attrs = printAttrs(path, options, print);
const printed = path.map(print, "constants");
let firstVariable;
if (printed.length === 1 && !node.constants[0].comments) {
[firstVariable] = printed;
} else if (printed.length > 0) {
// Indent first item
firstVariable = indent(printed[0]);
}
return group([
...attrs,
node.final ? "final " : "",
node.visibility ? [node.visibility, " "] : "",
"const",
node.type ? [node.nullable ? " ?" : " ", print("type")] : "",
firstVariable ? [" ", firstVariable] : "",
indent(printed.slice(1).map((p) => [",", hardline, p])),
]);
}
case "constant":
return printAssignment(
node.name,
print("name"),
" =",
node.value,
print("value"),
false,
options
);
case "static": {
const printed = path.map(print, "variables");
const hasValue = node.variables.some((item) => item.defaultValue);
let firstVariable;
if (printed.length === 1 && !node.variables[0].comments) {
[firstVariable] = printed;
} else if (printed.length > 0) {
// Indent first item
firstVariable = indent(printed[0]);
}
return group([
"static",
firstVariable ? [" ", firstVariable] : "",
indent(
printed.slice(1).map((p) => [",", hasValue ? hardline : line, p])
),
]);
}
case "staticvariable": {
return printAssignment(
node.variable,
print("variable"),
" =",
node.defaultValue,
print("defaultValue"),
false,
options
);
}
case "list":
case "array": {
const useShortForm =
(node.kind === "array" && options.phpVersion >= 5.4) ||
(node.kind === "list" && (node.shortForm || options.phpVersion >= 7.1));
const open = useShortForm ? "[" : [node.kind, "("];
const close = useShortForm ? "]" : ")";
if (node.items.length === 0) {
if (!hasDanglingComments(node)) {
return [open, close];
}
return group([
open,
printDanglingComments(path, options),
softline,
close,
]);
}
const lastElem = getLast(node.items);
// PHP allows you to have empty elements in an array which
// changes its length based on the number of commas. The algorithm
// is that if the last argument is null, we need to force insert
// a comma to ensure PHP recognizes it.
// [,] === $arr;
// [1,] === $arr;
// [1,,] === $arr;
//
// Note that getLast returns null if the array is empty, but
// we already check for an empty array just above so we are safe
const needsForcedTrailingComma = lastElem && lastElem.kind === "noop";
const [firstProperty] = node.items
.filter((node) => node.kind !== "noop")
.sort((a, b) => locStart(a) - locStart(b));
const isAssociative = !!(firstProperty && firstProperty.key);
const shouldBreak =
isAssociative &&
firstProperty &&
hasNewlineInRange(
options.originalText,
locStart(node),
locStart(firstProperty)
);
return group(
[
open,
indent([softline, printArrayItems(path, options, print)]),
needsForcedTrailingComma ? "," : "",
ifBreak(
!needsForcedTrailingComma && shouldPrintComma(options, 5.0)
? [
lastElem && shouldPrintHardlineBeforeTrailingComma(lastElem)
? hardline
: "",
",",
]
: ""
),
printDanglingComments(path, options, true),
softline,
close,
],
{ shouldBreak }
);
}
case "entry": {
const ref = node.byRef ? "&" : "";
const unpack = node.unpack ? "..." : "";
return node.key
? printAssignment(
node.key,
print("key"),
" =>",
node.value,
print("value"),
ref,
options
)
: [ref, unpack, print("value")];
}
case "yield": {
const printedKeyAndValue = [
node.key ? [print("key"), " => "] : "",
print("value"),
];
return [
"yield",
node.key || node.value ? " " : "",
node.value && node.value.comments
? indent(printedKeyAndValue)
: printedKeyAndValue,
];
}
case "yieldfrom":
return [
"yield from ",
node.value.comments ? indent(print("value")) : print("value"),
];
case "unary":
return [node.type, print("what")];
case "pre":
return [node.type + node.type, print("what")];
case "post":
return [print("what"), node.type + node.type];
case "cast":
return [
"(",
node.type,
") ",
node.expr.comments ? indent(print("expr")) : print("expr"),
];
case "assignref":
case "assign": {
const hasRef = node.kind === "assignref";
return printAssignment(
node.left,
print("left"),
[" ", hasRef ? "=" : node.operator],
node.right,
print("right"),
hasRef,
options
);
}
case "bin": {
const { parent, grandparent: parentParent } = path;
const isInsideParenthesis =
node !== parent.body &&
(parent.kind === "if" ||
parent.kind === "while" ||
parent.kind === "switch" ||
parent.kind === "do");
const parts = printBinaryExpression(
path,
print,
options,
/* isNested */ false,
isInsideParenthesis
);
// if (
// $this->hasPlugin('dynamicImports') && $this->lookahead()->type === tt->parenLeft
// ) {
//
// looks super weird, we want to break the children if the parent breaks
//
// if (
// $this->hasPlugin('dynamicImports') &&
// $this->lookahead()->type === tt->parenLeft
// ) {
if (isInsideParenthesis) {
return parts;
}
// Break between the parens in unaries or in a member expression, i.e.
//
// (
// a &&
// b &&
// c
// )->call()
if (
parent.kind === "unary" ||
(isLookupNode(parent) && parent.kind !== "offsetlookup")
) {
return group([indent([softline, ...parts]), softline]);
}
// Avoid indenting sub-expressions in some cases where the first sub-expression is already
// indented accordingly. We should indent sub-expressions where the first case isn't indented.
const shouldNotIndent =
(node !== parent.body && parent.kind === "for") ||
(parent.kind === "retif" &&
parentParent &&
parentParent.kind !== "return");
const shouldIndentIfInlining = [
"assign",
"property",
"constant",
"staticvariable",
"entry",
].includes(parent.kind);
const samePrecedenceSubExpression =
node.left.kind === "bin" && shouldFlatten(node.type, node.left.type);
if (
shouldNotIndent ||
(shouldInlineLogicalExpression(node) && !samePrecedenceSubExpression) ||
(!shouldInlineLogicalExpression(node) && shouldIndentIfInlining)
) {
return group(parts);
}
const rest = parts.slice(1);
return group([
// Don't include the initial expression in the indentation
// level. The first item is guaranteed to be the first
// left-most expression.
parts.length > 0 ? parts[0] : "",
indent(rest),
]);
}
case "retif": {
const parts = [];
const { parent } = path;
// TODO: Use `AstPath.findAncestor` when it's stable
// Find the outermost non-retif parent, and the outermost retif parent.
let currentParent;
let i = 0;
do {
currentParent = path.getParentNode(i);
i++;
} while (currentParent && currentParent.kind === "retif");
const firstNonRetifParent = currentParent || parent;
const printedFalseExpr =
node.falseExpr.kind === "bin"
? indent(print("falseExpr"))
: print("falseExpr");
const part = [
node.trueExpr ? line : " ",
"?",
node.trueExpr
? [
" ",
node.trueExpr.kind === "bin"
? indent(print("trueExpr"))
: print("trueExpr"),
line,
]
: "",
":",
node.trueExpr
? [" ", printedFalseExpr]
: [
shouldInlineRetifFalseExpression(node.falseExpr) ? " " : line,
printedFalseExpr,
],
];
parts.push(part);
// We want a whole chain of retif to all break if any of them break.
const maybeGroup = (doc) =>
parent === firstNonRetifParent ? group(doc) : doc;
// Break the closing parens to keep the chain right after it:
// ($a
// ? $b
// : $c
// )->call()
const parentParent = path.grandparent;
const pureParent =
parent.kind === "cast" && parentParent ? parentParent : parent;
const breakLookupNodes = [
"propertylookup",
"nullsafepropertylookup",
"staticlookup",
];
const breakClosingParens = breakLookupNodes.includes(pureParent.kind);
const printedTest = print("test");
if (!node.trueExpr) {
const printed = [
printedTest,
pureParent.kind === "bin" ||
["print", "echo", "return", "include"].includes(
firstNonRetifParent.kind
)
? indent(parts)
: parts,
];
// Break between the parens in unaries or in a lookup nodes, i.e.
//
// (
// a ?:
// b ?:
// c
// )->call()
if (
(pureParent.kind === "call" && pureParent.what === node) ||
pureParent.kind === "unary" ||
(isLookupNode(pureParent) && pureParent.kind !== "offsetlookup")
) {
return group([indent([softline, printed]), softline]);
}
return maybeGroup(printed);
}
return maybeGroup([
node.test.kind === "retif" ? indent(printedTest) : printedTest,
indent(parts),
breakClosingParens ? softline : "",
]);
}
case "boolean":
return node.value ? "true" : "false";
case "number":
return printNumber(node.value);
case "string": {
const { parent } = path;
if (parent.kind === "encapsedpart") {
const parentParent = path.grandparent;
let closingTagIndentation = 0;
const flexible = options.phpVersion >= 7.3;
let linebreak = literalline;
if (parentParent.type === "heredoc") {
linebreak = flexible ? hardline : literalline;
const lines = parentParent.raw.split("\n");
closingTagIndentation = lines[lines.length - 1].search(/\S/);
if (closingTagIndentation === -1) {
closingTagIndentation = lines[lines.length - 2].search(/\S/);
}
}
return join(
linebreak,
node.raw
.split("\n")
.map((s, i) =>
i > 0 || node.loc.start.column === 0
? s.substring(closingTagIndentation)
: s
)
);
}
const quote = useDoubleQuote(node, options) ? '"' : "'";
let stringValue = node.raw;
if (node.raw[0] === "b") {
stringValue = stringValue.slice(1);
}
// We need to strip out the quotes from the raw value
if (['"', "'"].includes(stringValue[0])) {
stringValue = stringValue.substr(1);
}
if (['"', "'"].includes(stringValue[stringValue.length - 1])) {
stringValue = stringValue.substr(0, stringValue.length - 1);
}
return [
node.raw[0] === "b" ? "b" : "",
quote,
join(literalline, stringValue.split("\n")),
quote,
];
}
case "intersectiontype": {
return printComposedTypes(path, print, "&");
}
case "uniontype": {
return printComposedTypes(path, print, "|");
}
case "encapsedpart": {
const open =
(node.syntax === "simple" && node.curly) || node.syntax === "complex"
? [node.curly ? "$" : "", "{"]
: "";
const close =
(node.syntax === "simple" && node.curly) || node.syntax === "complex"
? "}"
: "";
return [open, print("expression"), close];
}
case "encapsed":
switch (node.type) {
case "string":
case "shell":
case "heredoc": {
const flexible = options.phpVersion >= 7.3;
const linebreak = flexible ? hardline : literalline;
return [
getEncapsedQuotes(node),
// Respect `indent` for `heredoc` nodes
node.type === "heredoc" ? linebreak : "",
...path.map(print, "value"),
getEncapsedQuotes(node, { opening: false }),
node.type === "heredoc" && docShouldHaveTrailingNewline(path)
? hardline
: "",
];
}
/* c8 ignore next 2 */
default:
throw new Error(`Have not implemented kind ${node.type} yet.`);
}
case "inline":
return join(
literalline,
node.raw.replace("___PSEUDO_INLINE_PLACEHOLDER___", "").split("\n")
);
case "magic":
return node.value;
case "nowdoc": {
const flexible = options.phpVersion >= 7.3;
const linebreak = flexible ? hardline : literalline;
return [
"<<<'",
node.label,
"'",
linebreak,
join(linebreak, node.value.split("\n")),
linebreak,
node.label,
docShouldHaveTrailingNewline(path) ? hardline : "",
];
}
case "name":
return [node.resolution === "rn" ? "namespace\\" : "", node.name];
case "literal":
return print("value");
case "parentreference":
return "parent";
case "selfreference":
return "self";
case "staticreference":
return "static";
case "typereference":
return node.name;
case "nullkeyword":
return "null";
case "identifier": {
const { parent } = path;
if (parent.kind === "method") {
node.name = normalizeMagicMethodName(node.name);
}
return print("name");
}
case "match": {
const arms = path.map(() => {
const armNode = path.node;
const maybeLeadingComment = hasLeadingComment(armNode)
? [printComments(armNode.leadingComments, options), hardline]
: [];
const maybeTrailingComma =
!path.isLast || options.trailingCommaPHP ? "," : "";
const maybeTrailingComment = hasTrailingComment(armNode)
? [
" ",
printComments(
armNode.comments.filter((c) => c.trailing),
options
),
]
: [];
const conds =
armNode.conds === null
? "default"
: path.map(
({ isFirst }) => [",", line, print()].slice(isFirst ? 2 : 0),
"conds"
);
const body = print("body");
const maybeEmptyLineBetweenArms =
!path.isFirst &&
isPreviousLineEmpty(options.originalText, locStart(armNode))
? hardline
: "";
return [
"",
hardline,
maybeEmptyLineBetweenArms,
...maybeLeadingComment,
group([
group([conds, indent(line)]),
"=> ",
body,
maybeTrailingComma,
...maybeTrailingComment,
]),
].slice(!path.isFirst ? 0 : 1);
}, "arms");
return group([
"match (",
group([indent([softline, print("cond")]), softline]),
") {",
group(indent([...arms])),
" ",
softline,
"}",
]);
}
case "noop":
return node.comments ? printComments(node.comments, options) : "";
case "namedargument":
return [node.name, ": ", print("value")];
case "enumcase":
return group([
"case ",
print("name"),
node.value
? [
" =",
printAssignmentRight(
node.name,
node.value,
print("value"),
false,
options
),
]
: "",
]);
case "variadicplaceholder":
return "...";
/* c8 ignore next 3 */
case "error":
default:
throw new Error(`Have not implemented kind '${node.kind}' yet.`);
}
}
export default genericPrint;