@prettier/plugin-php
Version:
Prettier PHP Plugin
1,869 lines (1,644 loc) • 78.9 kB
JavaScript
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 : " "