@prettier/plugin-php
Version:
Prettier PHP Plugin
1,863 lines (1,643 loc) • 79.3 kB
JavaScript
"use strict";
const {
breakParent,
concat,
join,
line,
lineSuffix,
group,
conditionalGroup,
indent,
dedent,
ifBreak,
hardline,
softline,
literalline,
align,
dedentToRoot,
} = require("prettier").doc.builders;
const { willBreak } = require("prettier").doc.utils;
const {
isNextLineEmptyAfterIndex,
hasNewline,
hasNewlineInRange,
} = require("prettier").util;
const comments = require("./comments");
const pathNeedsParens = require("./needs-parens");
const {
getLast,
getPenultimate,
isLastStatement,
lineShouldEndWithSemicolon,
printNumber,
shouldFlatten,
maybeStripLeadingSlashFromUse,
fileShouldEndWithHardline,
hasDanglingComments,
hasLeadingComment,
hasTrailingComment,
docShouldHaveTrailingNewline,
isLookupNode,
isFirstChildrenInlineNode,
shouldPrintHardLineAfterStartInControlStructure,
shouldPrintHardLineBeforeEndInControlStructure,
getAlignment,
isProgramLikeNode,
getNodeKindIncludingLogical,
useDoubleQuote,
hasEmptyBody,
isNextLineEmptyAfterNamespace,
shouldPrintHardlineBeforeTrailingComma,
isDocNode,
getAncestorNode,
isReferenceLikeNode,
getNextNode,
normalizeMagicMethodName,
getNextNonSpaceNonCommentCharacterIndex,
isNextLineEmpty,
} = require("./util");
function isMinVersion(actualVersion, requiredVersion) {
return parseFloat(actualVersion) >= parseFloat(requiredVersion);
}
function shouldPrintComma(options, requiredVersion) {
if (!options.trailingCommaPHP) {
return false;
}
return isMinVersion(options.phpVersion, requiredVersion);
}
function shouldPrintHardlineForOpenBrace(options) {
switch (options.braceStyle) {
case "1tbs":
return false;
case "psr-2":
default:
return true;
}
}
function genericPrint(path, options, print) {
const node = path.getValue();
if (!node) {
return "";
} else 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 concat(parts);
}
function printPropertyLookup(path, options, print) {
return concat(["->", path.call(print, "offset")]);
}
function printStaticLookup(path, options, print) {
const node = path.getValue();
const needCurly = !["variable", "identifier"].includes(node.offset.kind);
return concat([
"::",
needCurly ? "{" : "",
path.call(print, "offset"),
needCurly ? "}" : "",
]);
}
function printOffsetLookup(path, options, print) {
const node = path.getValue();
const shouldInline =
(node.offset && node.offset.kind === "number") ||
getAncestorNode(path, "encapsed");
return concat([
"[",
node.offset
? group(
concat([
indent(
concat([shouldInline ? "" : softline, path.call(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,
node,
options
);
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, node, options);
}
function traverse(path) {
const node = path.getValue();
if (
node.kind === "call" &&
(isLookupNode(node.what) || node.what.kind === "call")
) {
printedNodes.unshift({
node,
printed: concat([
comments.printAllComments(
path,
() => concat([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 === "staticlookup") {
printedMemberish = printStaticLookup(path, options, print);
} else {
printedMemberish = printOffsetLookup(path, options, print);
}
printedNodes.unshift({
node,
needsParens: pathNeedsParens(path, options),
printed: comments.printAllComments(
path,
() => printedMemberish,
options
),
});
path.call((what) => traverse(what), "what");
} else {
printedNodes.unshift({
node,
printed: path.call(print),
});
}
}
const node = path.getValue();
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", "staticlookup"].includes(
printedNodes[i - 1].node.kind
) &&
printedNodes[i - 1].needsParens
) {
printedNodes[0].printed = concat(["(", printedNodes[0].printed]);
printedNodes[i - 1].printed = concat([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 &&
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();
//
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") ||
isReferenceLikeNode(firstNode)
);
}
const lastNode = getLast(groups[0]).node;
return (
isLookupNode(lastNode) &&
(lastNode.offset.kind === "identifier" ||
lastNode.offset.kind === "variable") &&
hasComputed
);
}
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 concat(result);
}
function printIndentedGroup(groups) {
if (groups.length === 0) {
return "";
}
return indent(
group(concat([hardline, join(hardline, groups.map(printGroup))]))
);
}
const printedGroups = groups.map(printGroup);
const oneLine = concat(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)
.reduce((res, group) => res.concat(group), []);
const hasComment =
flatGroups
.slice(1, -1)
.some((node) => comments.hasLeadingComment(node.node)) ||
flatGroups
.slice(0, -1)
.some((node) => comments.hasTrailingComment(node.node)) ||
(groups[cutoff] && comments.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 = concat([
printGroup(groups[0]),
shouldMerge ? concat(groups.slice(1, 2).map(printGroup)) : "",
shouldHaveEmptyLineBeforeIndent ? hardline : "",
printIndentedGroup(groups.slice(shouldMerge ? 2 : 1)),
]);
const callExpressionCount = printedNodes.filter(
(tuple) => tuple.node.kind === "call"
).length;
// 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 ||
callExpressionCount >= 3 ||
printedGroups.slice(0, -1).some(willBreak)
) {
return group(expanded);
}
return concat([
// 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.getValue()[argumentsKey];
if (args.length === 0) {
return concat([
"(",
comments.printDanglingComments(path, options, /* sameIndent */ true),
")",
]);
}
let anyArgEmptyLine = false;
let hasEmptyLineFollowingFirstArg = false;
const lastArgIndex = args.length - 1;
const printedArguments = path.map((argPath, index) => {
const arg = argPath.getNode();
const parts = [print(argPath)];
if (index === lastArgIndex) {
// do nothing
} else if (isNextLineEmpty(options.originalText, arg, options)) {
if (index === 0) {
hasEmptyLineFollowingFirstArg = true;
}
anyArgEmptyLine = true;
parts.push(",", hardline, hardline);
} else {
parts.push(",", line);
}
return concat(parts);
}, argumentsKey);
const node = path.getValue();
const lastArg = getLast(args);
const maybeTrailingComma =
["call", "new", "unset", "isset"].includes(node.kind) &&
shouldPrintComma(options, "7.3")
? indent(
concat([
lastArg && shouldPrintHardlineBeforeTrailingComma(lastArg)
? hardline
: "",
",",
])
)
: "";
function allArgsBrokenOut() {
return group(
concat([
"(",
indent(concat([line, concat(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;
let i = 0;
path.each((argPath) => {
if (shouldGroupFirst && i === 0) {
printedExpanded = [
concat([
argPath.call((p) => print(p, { expandFirstArg: true })),
printedArguments.length > 1 ? "," : "",
hasEmptyLineFollowingFirstArg ? hardline : line,
hasEmptyLineFollowingFirstArg ? hardline : "",
]),
].concat(printedArguments.slice(1));
}
if (shouldGroupLast && i === args.length - 1) {
printedExpanded = printedArguments
.slice(0, -1)
.concat(argPath.call((p) => print(p, { expandLastArg: true })));
}
i++;
}, argumentsKey);
const somePrintedArgumentsWillBreak = printedArguments.some(willBreak);
const simpleConcat = concat(["(", concat(printedExpanded), ")"]);
return concat([
somePrintedArgumentsWillBreak ? breakParent : "",
conditionalGroup(
[
!somePrintedArgumentsWillBreak
? simpleConcat
: ifBreak(allArgsBrokenOut(), simpleConcat),
shouldGroupFirst
? concat([
"(",
group(printedExpanded[0], { shouldBreak: true }),
concat(printedExpanded.slice(1)),
")",
])
: concat([
"(",
concat(printedArguments.slice(0, -1)),
group(getLast(printedExpanded), {
shouldBreak: true,
}),
")",
]),
group(
concat([
"(",
indent(concat([line, concat(printedArguments)])),
ifBreak(maybeTrailingComma),
line,
")",
]),
{ shouldBreak: true }
),
],
{ shouldBreak }
),
]);
}
return group(
concat([
"(",
indent(concat([softline, concat(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.getValue();
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(
(left) =>
printBinaryExpression(
left,
print,
options,
/* isNested */ true,
isInsideParenthesis
),
"left"
)
);
} else {
parts.push(path.call(print, "left"));
}
const shouldInline = shouldInlineLogicalExpression(node);
const right = shouldInline
? concat([node.type, " ", path.call(print, "right")])
: concat([node.type, line, path.call(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.getParentNode();
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 = comments.printAllComments(path, () => concat(parts), options);
}
} else {
// Our stopping case. Simply print the node normally.
parts.push(path.call(print));
}
return parts;
}
function printLookupNodes(path, options, print) {
const node = path.getValue();
switch (node.kind) {
case "propertylookup":
return printPropertyLookup(path, options, print);
case "staticlookup":
return printStaticLookup(path, options, print);
case "offsetlookup":
return printOffsetLookup(path, options, print);
/* istanbul ignore next */
default:
return `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];
}
/* istanbul ignore next */
return `Unimplemented encapsed type ${node.type}`;
}
function printArrayItems(path, options, print) {
const printedElements = [];
let separatorParts = [];
path.each((childPath) => {
printedElements.push(concat(separatorParts));
printedElements.push(group(print(childPath)));
separatorParts = [",", line];
if (
childPath.getValue() &&
isNextLineEmpty(options.originalText, childPath.getValue(), options)
) {
separatorParts.push(softline);
}
}, "items");
return concat(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 = concat([
before || "",
concat(parts.slice(start, end)),
after || "",
]);
const newArray = accumulator.concat(
parts.slice(lastEnd, start),
alignment
? dedentToRoot(
group(
concat([
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 = path.getValue();
const parentNode = path.getParentNode();
let lastInlineIndex = -1;
const parts = [];
const groupIndexes = [];
path.map((childPath, index) => {
const childNode = childPath.getValue();
const isInlineNode = childNode.kind === "inline";
const printedPath = print(childPath);
const children = node[childrenAttribute];
const nextNode = children[index + 1];
const canPrintBlankLine =
!isLastStatement(childPath) &&
!isInlineNode &&
(nextNode && nextNode.kind === "case"
? !isFirstChildrenInlineNode(path)
: nextNode && nextNode.kind !== "inline");
let printed = concat([
printedPath,
canPrintBlankLine ? hardline : "",
canPrintBlankLine &&
isNextLineEmpty(options.originalText, childNode, options)
? hardline
: "",
]);
const isFirstNode = index === 0;
const isLastNode = children.length - 1 === index;
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 =
children[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
? concat([
isFirstNode && node.kind !== "namespace" && !isBlockNestedNode
? openTag
: "",
node.kind === "namespace" || !isBlockNestedNode ? hardline : "",
comments.printComments(childNode.leadingComments, options),
hardline,
"?>",
])
: isProgramLikeNode(node) && isFirstNode && node.kind !== "namespace"
? ""
: concat([beforeCloseTagInlineNode, "?>"]);
const afterInline =
childNode.comments && childNode.comments.length
? concat([
openTag,
hardline,
comments.printComments(childNode.comments, options),
hardline,
"?>",
])
: isProgramLikeNode(node) && isLastNode
? ""
: concat([openTag, " "]);
printed = concat([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 = concat([
between && between[2] && between[2].includes("\n")
? concat([
hardline,
between[2].split("\n").length > 2 ? hardline : "",
])
: " ",
node.comments ? comments.printComments(node.comments, options) : "",
]);
const shortEcho =
firstNode && firstNode.kind === "echo" && firstNode.shortForm;
parts.push(concat([shortEcho ? "<?=" : "<?php", afterOpenTag]));
}
parts.push(concat(wrappedParts));
const hasEndTag = options.originalText.trim().endsWith("?>");
if (hasEndTag) {
const lastNode = getLast(node.children);
const beforeCloseTag = lastNode
? concat([
hasNewlineInRange(
options.originalText,
options.locEnd(lastNode),
options.locEnd(node)
)
? hardline
: " ",
isNextLineEmpty(options.originalText, lastNode, options)
? hardline
: "",
])
: node.comments
? hardline
: "";
parts.push(lineSuffix(concat([beforeCloseTag, "?>"])));
}
return concat(parts);
}
return concat(wrappedParts);
}
function printStatements(path, options, print, childrenAttribute) {
return concat(
path.map((childPath) => {
const parts = [];
parts.push(print(childPath));
if (!isLastStatement(childPath)) {
parts.push(hardline);
if (
isNextLineEmpty(options.originalText, childPath.getValue(), options)
) {
parts.push(hardline);
}
}
return concat(parts);
}, childrenAttribute)
);
}
function printClassPart(
path,
options,
print,
part = "extends",
beforePart = " ",
afterPart = " "
) {
const node = path.getValue();
const printedBeforePart = hasDanglingComments(node[part])
? concat([
hardline,
path.call(
(partPath) => comments.printDanglingComments(partPath, options, true),
part
),
hardline,
])
: beforePart;
const printedPartItems = Array.isArray(node[part])
? group(
concat([
join(
",",
path.map((itemPartPath) => {
const printedPart = print(itemPartPath);
// Check if any of the implements nodes have comments
return hasDanglingComments(itemPartPath.getValue())
? concat([
hardline,
comments.printDanglingComments(itemPartPath, options, true),
hardline,
printedPart,
])
: concat([afterPart, printedPart]);
}, part)
),
])
)
: concat([afterPart, path.call(print, part)]);
return indent(
concat([
printedBeforePart,
part,
willBreak(printedBeforePart)
? indent(printedPartItems)
: printedPartItems,
])
);
}
function printClass(path, options, print) {
const node = path.getValue();
const declaration = [];
if (node.isFinal) {
declaration.push("final ");
}
if (node.isAbstract) {
declaration.push("abstract ");
}
const isAnonymousClass = node.kind === "class" && node.isAnonymous;
// `new` print `class` keyword with arguments
declaration.push(isAnonymousClass ? "" : node.kind);
if (node.name) {
declaration.push(" ", path.call(print, "name"));
}
// Only `class` can have `extends` and `implements`
if (node.extends && node.implements) {
declaration.push(
conditionalGroup(
[
concat([
printClassPart(path, options, print, "extends"),
printClassPart(path, options, print, "implements"),
]),
concat([
printClassPart(path, options, print, "extends"),
printClassPart(path, options, print, "implements", " ", hardline),
]),
concat([
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 printedDeclaration = group(
concat([
group(concat(declaration)),
shouldPrintHardlineForOpenBrace(options)
? isAnonymousClass
? line
: hardline
: " ",
])
);
const hasEmptyClassBody =
node.body && node.body.length === 0 && !hasDanglingComments(node);
const printedBody = concat([
"{",
indent(
concat([
hasEmptyClassBody ? "" : hardline,
printStatements(path, options, print, "body"),
])
),
comments.printDanglingComments(path, options, true),
isAnonymousClass && hasEmptyClassBody ? softline : hardline,
"}",
]);
return concat([printedDeclaration, printedBody]);
}
function printFunction(path, options, print) {
const node = path.getValue();
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(path.call(print, "name"));
}
declaration.push(printArgumentsList(path, options, print));
if (node.uses && node.uses.length > 0) {
declaration.push(
group(concat([" use ", printArgumentsList(path, options, print, "uses")]))
);
}
if (node.type) {
declaration.push(
concat([
": ",
hasDanglingComments(node.type)
? concat([
path.call(
(typePath) =>
comments.printDanglingComments(typePath, options, true),
"type"
),
" ",
])
: "",
node.nullable ? "?" : "",
path.call(print, "type"),
])
);
}
const printedDeclaration = concat(declaration);
if (!node.body) {
return printedDeclaration;
}
const isClosure = node.kind === "closure";
const printedBody = concat([
"{",
indent(
concat([hasEmptyBody(path) ? "" : hardline, path.call(print, "body")])
),
isClosure && hasEmptyBody(path) ? "" : hardline,
"}",
]);
if (isClosure) {
return concat([printedDeclaration, " ", printedBody]);
}
if (node.arguments.length === 0) {
return concat([
printedDeclaration,
shouldPrintHardlineForOpenBrace(options) ? hardline : " ",
printedBody,
]);
}
const willBreakDeclaration = declaration.some(willBreak);
if (willBreakDeclaration) {
return concat([printedDeclaration, " ", printedBody]);
}
return conditionalGroup([
concat([
printedDeclaration,
shouldPrintHardlineForOpenBrace(options) ? hardline : " ",
printedBody,
]),
concat([printedDeclaration, " ", printedBody]),
]);
}
function printBodyControlStructure(
path,
options,
print,
bodyProperty = "body"
) {
const node = path.getValue();
if (!node[bodyProperty]) {
return ";";
}
const printedBody = path.call(print, bodyProperty);
return concat([
node.shortForm ? ":" : " {",
indent(
concat([
node[bodyProperty].kind !== "block" ||
(node[bodyProperty].children &&
node[bodyProperty].children.length > 0) ||
(node[bodyProperty].comments && node[bodyProperty].comments.length > 0)
? concat([
shouldPrintHardLineAfterStartInControlStructure(path)
? node.kind === "switch"
? " "
: ""
: hardline,
printedBody,
])
: "",
])
),
node.kind === "if" && bodyProperty === "body"
? ""
: concat([
shouldPrintHardLineBeforeEndInControlStructure(path) ? hardline : "",
node.shortForm ? concat(["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(concat([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 (
comments.hasLeadingOwnLineComment(options.originalText, rightNode, options)
) {
return indent(concat([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(concat([line, ref, printedRight])));
}
return concat([" ", ref, printedRight]);
}
function needsHardlineAfterDanglingComment(node) {
if (!node.comments) {
return false;
}
const lastDanglingComment = getLast(
node.comments.filter((comment) => !comment.leading && !comment.trailing)
);
return lastDanglingComment && !comments.isBlockComment(lastDanglingComment);
}
function stringHasNewLines(node) {
return node.raw.includes("\n");
}
function isStringOnItsOwnLine(node, text, options) {
return (
(node.kind === "string" ||
(node.kind === "encapsed" &&
(node.type === "string" || node.type === "shell"))) &&
stringHasNewLines(node) &&
!hasNewline(text, options.locStart(node), { backwards: true })
);
}
function printNode(path, options, print) {
const node = path.getValue();
switch (node.kind) {
case "program": {
return group(
concat([
printLines(path, options, print),
comments.printDanglingComments(
path,
options,
/* sameIndent */ true,
(c) => !c.printed
),
])
);
}
case "expressionstatement":
return path.call(print, "expression");
case "block":
return concat([
printLines(path, options, print),
comments.printDanglingComments(path, options, true),
]);
case "declare": {
const printDeclareArguments = (path) => {
return join(
", ",
path.map((directive) => concat([print(directive)]), "directives")
);
};
if (["block", "short"].includes(node.mode)) {
return concat([
"declare(",
printDeclareArguments(path),
")",
node.mode === "block" ? " {" : ":",
node.children.length > 0
? indent(concat([hardline, printLines(path, options, print)]))
: "",
comments.printDanglingComments(path, options),
hardline,
node.mode === "block" ? "}" : "enddeclare;",
]);
}
const nextNode = getNextNode(path, node);
return concat([
"declare(",
printDeclareArguments(path),
")",
nextNode && nextNode.kind === "inline" ? "" : ";",
]);
}
case "declaredirective":
return concat([path.call(print, "key"), "=", path.call(print, "value")]);
case "namespace":
return concat([
"namespace ",
node.name && typeof node.name === "string"
? concat([node.name, node.withBrackets ? " " : ""])
: "",
node.withBrackets ? "{" : ";",
hasDanglingComments(node)
? concat([" ", comments.printDanglingComments(path, options, true)])
: "",
node.children.length > 0
? node.withBrackets
? indent(concat([hardline, printLines(path, options, print)]))
: concat([
node.children[0].kind === "inline"
? ""
: concat([
hardline,
isNextLineEmptyAfterNamespace(
options.originalText,
node,
options.locStart
)
? hardline
: "",
]),
printLines(path, options, print),
])
: "",
node.withBrackets ? concat([hardline, "}"]) : "",
]);
case "usegroup":
return group(
concat([
"use ",
node.type ? concat([node.type, " "]) : "",
indent(
concat([
node.name
? concat([
maybeStripLeadingSlashFromUse(node.name),
"\\{",
softline,
])
: "",
join(
concat([",", line]),
path.map((item) => concat([print(item)]), "items")
),
])
),
node.name
? concat([
ifBreak(shouldPrintComma(options, "7.2") ? "," : ""),
softline,
"}",
])
: "",
])
);
case "useitem":
return concat([
node.type ? concat([node.type, " "]) : "",
maybeStripLeadingSlashFromUse(node.name),
hasDanglingComments(node)
? concat([" ", comments.printDanglingComments(path, options, true)])
: "",
node.alias ? concat([" as ", path.call(print, "alias")]) : "",
]);
case "class":
case "interface":
case "trait":
return printClass(path, options, print);
case "traitprecedence":
return concat([
path.call(print, "trait"),
"::",
path.call(print, "method"),
" insteadof ",
join(", ", path.map(print, "instead")),
]);
case "traitalias":
return concat([
node.trait ? concat([path.call(print, "trait"), "::"]) : "",
node.method ? path.call(print, "method") : "",
" as ",
join(" ", [
...(node.visibility ? [node.visibility] : []),
...(node.as ? [path.call(print, "as")] : []),
]),
]);
case "traituse":
return group(
concat([
"use ",
indent(group(join(concat([",", line]), path.map(print, "traits")))),
node.adaptations
? concat([
" {",
node.adaptations.length > 0
? concat([
indent(
concat([
hardline,
printStatements(path, options, print, "adaptations"),
])
),
hardline,
])
: hasDanglingComments(node)
? concat([
line,
comments.printDanglingComments(path, options, true),
line,
])
: "",
"}",
])
: "",
])
);
case "function":
case "closure":
case "method":
return printFunction(path, options, print);
case "arrowfunc":
return concat([
node.isStatic ? "static " : "",
"fn",
printArgumentsList(path, options, print),
node.type
? concat([": ", node.nullable ? "?" : "", path.call(print, "type")])
: "",
" => ",
path.call(print, "body"),
]);
case "parameter": {
const name = concat([
node.nullable ? "?" : "",
node.type ? concat([path.call(print, "type"), " "]) : "",
node.byref ? "&" : "",
node.variadic ? "..." : "",
"$",
path.call(print, "name"),
]);
if (node.value) {
return group(
concat([
name,
// see handleFunctionParameter() in ./comments.js - 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) ? " " : "",
comments.printDanglingComments(path, options, true),
concat([
" =",
printAssignmentRight(
node.name,
node.value,
path.call(print, "value"),
false,
options
),
]),
])
);
}
return name;
}
case "variadic":
return concat(["...", path.call(print, "what")]);
case "property":
return group(
concat([
node.type
? concat([node.nullable ? "?" : "", path.call(print, "type"), " "])
: "",
"$",
path.call(print, "name"),
node.value
? concat([
" =",
printAssignmentRight(
node.name,
node.value,
path.call(print, "value"),
false,
options
),
])
: "",
])
);
case "propertystatement": {
const printed = path.map((childPath) => {
return print(childPath);
}, "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(
concat([
hasVisibility
? concat([node.visibility === null ? "var" : node.visibility, ""])
: "",
node.isStatic ? concat([hasVisibility ? " " : "", "static"]) : "",
firstProperty ? concat([" ", firstProperty]) : "",
indent(
concat(
printed
.slice(1)
.map((p) => concat([",", hasValue ? hardline : line, p]))
)
),
])
);
}
case "if": {
const parts = [];
const body = printBodyControlStructure(path, options, print, "body");
const opening = group(
concat([
"if (",
group(
concat([
indent(concat([softline, path.call(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 && !comments.isBlockComment(comment)
)) ||
needsHardlineAfterDanglingComment(node);
const elseOnSameLine = !commentOnOwnLine;
parts.push(elseOnSameLine ? "" : hardline);
if (hasDanglingComments(node)) {
parts.push(