351 lines
9.9 KiB
JavaScript
351 lines
9.9 KiB
JavaScript
import { printComments } from "../../main/comments/print.js";
|
|
import { DOC_TYPE_FILL, DOC_TYPE_GROUP } from "../../document/constants.js";
|
|
import {
|
|
join,
|
|
line,
|
|
softline,
|
|
group,
|
|
indent,
|
|
align,
|
|
indentIfBreak,
|
|
} from "../../document/builders.js";
|
|
import { cleanDoc, getDocParts } from "../../document/utils.js";
|
|
import {
|
|
hasLeadingOwnLineComment,
|
|
isBinaryish,
|
|
isJsxElement,
|
|
shouldFlatten,
|
|
hasComment,
|
|
CommentCheckFlags,
|
|
isCallExpression,
|
|
isMemberExpression,
|
|
isObjectProperty,
|
|
isArrayOrTupleExpression,
|
|
isObjectOrRecordExpression,
|
|
} from "../utils/index.js";
|
|
|
|
/** @typedef {import("../../document/builders.js").Doc} Doc */
|
|
|
|
let uid = 0;
|
|
function printBinaryishExpression(path, options, print) {
|
|
const { node, parent, grandparent, key } = path;
|
|
const isInsideParenthesis =
|
|
key !== "body" &&
|
|
(parent.type === "IfStatement" ||
|
|
parent.type === "WhileStatement" ||
|
|
parent.type === "SwitchStatement" ||
|
|
parent.type === "DoWhileStatement");
|
|
const isHackPipeline =
|
|
node.operator === "|>" && path.root.extra?.__isUsingHackPipeline;
|
|
|
|
const parts = printBinaryishExpressions(
|
|
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;
|
|
}
|
|
|
|
if (isHackPipeline) {
|
|
return group(parts);
|
|
}
|
|
|
|
// Break between the parens in
|
|
// unaries or in a member or specific call expression, i.e.
|
|
//
|
|
// (
|
|
// a &&
|
|
// b &&
|
|
// c
|
|
// ).call()
|
|
if (
|
|
(isCallExpression(parent) && parent.callee === node) ||
|
|
parent.type === "UnaryExpression" ||
|
|
(isMemberExpression(parent) && !parent.computed)
|
|
) {
|
|
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 =
|
|
parent.type === "ReturnStatement" ||
|
|
parent.type === "ThrowStatement" ||
|
|
(parent.type === "JSXExpressionContainer" &&
|
|
grandparent.type === "JSXAttribute") ||
|
|
(node.operator !== "|" && parent.type === "JsExpressionRoot") ||
|
|
(node.type !== "NGPipeExpression" &&
|
|
((parent.type === "NGRoot" && options.parser === "__ng_binding") ||
|
|
(parent.type === "NGMicrosyntaxExpression" &&
|
|
grandparent.type === "NGMicrosyntax" &&
|
|
grandparent.body.length === 1))) ||
|
|
(node === parent.body && parent.type === "ArrowFunctionExpression") ||
|
|
(node !== parent.body && parent.type === "ForStatement") ||
|
|
(parent.type === "ConditionalExpression" &&
|
|
grandparent.type !== "ReturnStatement" &&
|
|
grandparent.type !== "ThrowStatement" &&
|
|
!isCallExpression(grandparent)) ||
|
|
parent.type === "TemplateLiteral";
|
|
|
|
const shouldIndentIfInlining =
|
|
parent.type === "AssignmentExpression" ||
|
|
parent.type === "VariableDeclarator" ||
|
|
parent.type === "ClassProperty" ||
|
|
parent.type === "PropertyDefinition" ||
|
|
parent.type === "TSAbstractPropertyDefinition" ||
|
|
parent.type === "ClassPrivateProperty" ||
|
|
isObjectProperty(parent);
|
|
|
|
const samePrecedenceSubExpression =
|
|
isBinaryish(node.left) && shouldFlatten(node.operator, node.left.operator);
|
|
|
|
if (
|
|
shouldNotIndent ||
|
|
(shouldInlineLogicalExpression(node) && !samePrecedenceSubExpression) ||
|
|
(!shouldInlineLogicalExpression(node) && shouldIndentIfInlining)
|
|
) {
|
|
return group(parts);
|
|
}
|
|
|
|
if (parts.length === 0) {
|
|
return "";
|
|
}
|
|
|
|
// If the right part is a JSX node, we include it in a separate group to
|
|
// prevent it breaking the whole chain, so we can print the expression like:
|
|
//
|
|
// foo && bar && (
|
|
// <Foo>
|
|
// <Bar />
|
|
// </Foo>
|
|
// )
|
|
|
|
const hasJsx = isJsxElement(node.right);
|
|
|
|
const firstGroupIndex = parts.findIndex(
|
|
(part) =>
|
|
typeof part !== "string" &&
|
|
!Array.isArray(part) &&
|
|
part.type === DOC_TYPE_GROUP
|
|
);
|
|
|
|
// Separate the leftmost expression, possibly with its leading comments.
|
|
const headParts = parts.slice(
|
|
0,
|
|
firstGroupIndex === -1 ? 1 : firstGroupIndex + 1
|
|
);
|
|
|
|
const rest = parts.slice(headParts.length, hasJsx ? -1 : undefined);
|
|
|
|
const groupId = Symbol("logicalChain-" + ++uid);
|
|
|
|
const chain = group(
|
|
[
|
|
// Don't include the initial expression in the indentation
|
|
// level. The first item is guaranteed to be the first
|
|
// left-most expression.
|
|
...headParts,
|
|
indent(rest),
|
|
],
|
|
{ id: groupId }
|
|
);
|
|
|
|
if (!hasJsx) {
|
|
return chain;
|
|
}
|
|
|
|
const jsxPart = parts.at(-1);
|
|
return group([chain, indentIfBreak(jsxPart, { groupId })]);
|
|
}
|
|
|
|
// 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 printBinaryishExpressions(
|
|
path,
|
|
print,
|
|
options,
|
|
isNested,
|
|
isInsideParenthesis
|
|
) {
|
|
const { node } = path;
|
|
|
|
// Simply print the node normally.
|
|
if (!isBinaryish(node)) {
|
|
return [group(print())];
|
|
}
|
|
|
|
/** @type{Doc[]} */
|
|
let parts = [];
|
|
|
|
// We treat BinaryExpression and LogicalExpression nodes the same.
|
|
|
|
// 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.operator, node.left.operator)) {
|
|
// Flatten them out by recursively calling this function.
|
|
parts = path.call(
|
|
(left) =>
|
|
printBinaryishExpressions(
|
|
left,
|
|
print,
|
|
options,
|
|
/* isNested */ true,
|
|
isInsideParenthesis
|
|
),
|
|
"left"
|
|
);
|
|
} else {
|
|
parts.push(group(print("left")));
|
|
}
|
|
|
|
const shouldInline = shouldInlineLogicalExpression(node);
|
|
const lineBeforeOperator =
|
|
(node.operator === "|>" ||
|
|
node.type === "NGPipeExpression" ||
|
|
isVueFilterSequenceExpression(path, options)) &&
|
|
!hasLeadingOwnLineComment(options.originalText, node.right);
|
|
|
|
const operator = node.type === "NGPipeExpression" ? "|" : node.operator;
|
|
const rightSuffix =
|
|
node.type === "NGPipeExpression" && node.arguments.length > 0
|
|
? group(
|
|
indent([
|
|
line,
|
|
": ",
|
|
join(
|
|
[line, ": "],
|
|
path.map(() => align(2, group(print())), "arguments")
|
|
),
|
|
])
|
|
)
|
|
: "";
|
|
|
|
/** @type {Doc} */
|
|
let right;
|
|
if (shouldInline) {
|
|
right = [operator, " ", print("right"), rightSuffix];
|
|
} else {
|
|
const isHackPipeline =
|
|
operator === "|>" && path.root.extra?.__isUsingHackPipeline;
|
|
const rightContent = isHackPipeline
|
|
? path.call(
|
|
(left) =>
|
|
printBinaryishExpressions(
|
|
left,
|
|
print,
|
|
options,
|
|
/* isNested */ true,
|
|
isInsideParenthesis
|
|
),
|
|
"right"
|
|
)
|
|
: print("right");
|
|
right = [
|
|
lineBeforeOperator ? line : "",
|
|
operator,
|
|
lineBeforeOperator ? " " : line,
|
|
rightContent,
|
|
rightSuffix,
|
|
];
|
|
}
|
|
|
|
// 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 shouldBreak = hasComment(
|
|
node.left,
|
|
CommentCheckFlags.Trailing | CommentCheckFlags.Line
|
|
);
|
|
const shouldGroup =
|
|
shouldBreak ||
|
|
(!(isInsideParenthesis && node.type === "LogicalExpression") &&
|
|
parent.type !== node.type &&
|
|
node.left.type !== node.type &&
|
|
node.right.type !== node.type);
|
|
|
|
parts.push(
|
|
lineBeforeOperator ? "" : " ",
|
|
shouldGroup ? group(right, { shouldBreak }) : 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 BinaryExpression,
|
|
// only for the left and right parts
|
|
if (isNested && hasComment(node)) {
|
|
const printed = cleanDoc(printComments(path, parts, options));
|
|
/* c8 ignore next 3 */
|
|
if (Array.isArray(printed) || printed.type === DOC_TYPE_FILL) {
|
|
return getDocParts(printed);
|
|
}
|
|
|
|
return [printed];
|
|
}
|
|
|
|
return parts;
|
|
}
|
|
|
|
function shouldInlineLogicalExpression(node) {
|
|
if (node.type !== "LogicalExpression") {
|
|
return false;
|
|
}
|
|
|
|
if (
|
|
isObjectOrRecordExpression(node.right) &&
|
|
node.right.properties.length > 0
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
if (isArrayOrTupleExpression(node.right) && node.right.elements.length > 0) {
|
|
return true;
|
|
}
|
|
|
|
if (isJsxElement(node.right)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
const isBitwiseOrExpression = (node) =>
|
|
node.type === "BinaryExpression" && node.operator === "|";
|
|
|
|
function isVueFilterSequenceExpression(path, options) {
|
|
return (
|
|
options.parser === "__vue_expression" &&
|
|
isBitwiseOrExpression(path.node) &&
|
|
!path.hasAncestor(
|
|
(node) => !isBitwiseOrExpression(node) && node.type !== "JsExpressionRoot"
|
|
)
|
|
);
|
|
}
|
|
|
|
export { printBinaryishExpression, shouldInlineLogicalExpression };
|