prettier-plugin-apex
Version:
Salesforce Apex plugin for Prettier
1,390 lines • 98.8 kB
JavaScript
import * as prettier from "prettier";
import { getTrailingComments, hasPrettierIgnore, printComment, printDanglingComment, } from "./comments.js";
import { APEX_TYPES, ASSIGNMENT, BINARY, BOOLEAN, DATA_CATEGORY, MODIFIER, ORDER, ORDER_NULL, POSTFIX, PREFIX, QUERY, QUERY_WHERE, TRIGGER_USAGE, } from "./constants.js";
import { checkIfParentIsDottedExpression, getParentType, getPrecedence, isBinaryish, } from "./util.js";
const docBuilders = prettier.doc.builders;
const { align, join, hardline, line, softline, group, indent, dedent } = docBuilders;
function indentConcat(docs) {
return indent(docs);
}
function groupConcat(docs) {
return group(docs);
}
function groupIndentConcat(docs) {
return group(indent(docs));
}
function handlePassthroughCall(prop1, prop2) {
return (path, print) => prop2 ? path.call(print, prop1, prop2) : path.call(print, prop1);
}
function pushIfExist(parts, doc, postDocs, preDocs) {
if (doc) {
if (preDocs) {
preDocs.forEach((preDoc) => parts.push(preDoc));
}
parts.push(doc);
if (postDocs) {
postDocs.forEach((postDoc) => parts.push(postDoc));
}
}
return parts;
}
function escapeString(text) {
// Code from https://stackoverflow.com/a/11716317/477761
return text
.replace(/\\/g, "\\\\")
.replace(/\u0008/g, "\\b") // eslint-disable-line no-control-regex
.replace(/\t/g, "\\t")
.replace(/\n/g, "\\n")
.replace(/\f/g, "\\f")
.replace(/\r/g, "\\r")
.replace(/'/g, "\\'");
}
function handleReturnStatement(path, print) {
const node = path.getNode();
const docs = [];
docs.push("return");
const childDocs = path.call(print, "expr", "value");
if (childDocs) {
docs.push(" ");
docs.push(childDocs);
}
docs.push(";");
if (node.expr.value && isBinaryish(node.expr.value)) {
return groupIndentConcat(docs);
}
return groupConcat(docs);
}
function handleTriggerUsage(path) {
const node = path.getNode();
return TRIGGER_USAGE[node.$];
}
function getOperator(node) {
if (node.op["@class"] === APEX_TYPES.BOOLEAN_OPERATOR) {
return BOOLEAN[node.op.$];
}
return BINARY[node.op.$];
}
function handleBinaryishExpression(path, print) {
const node = path.getNode();
const nodeOp = getOperator(node);
const nodePrecedence = getPrecedence(nodeOp);
const parentNode = path.getParentNode();
const isLeftNodeBinaryish = isBinaryish(node.left);
const isRightNodeBinaryish = isBinaryish(node.right);
const isNestedExpression = isBinaryish(parentNode);
const isNestedRightExpression = isNestedExpression && node === parentNode.right;
const isNodeSamePrecedenceAsLeftChild = isLeftNodeBinaryish &&
nodePrecedence === getPrecedence(getOperator(node.left));
const isNodeSamePrecedenceAsParent = isBinaryish(parentNode) &&
nodePrecedence === getPrecedence(getOperator(parentNode));
const docs = [];
const leftDoc = path.call(print, "left");
const operationDoc = path.call(print, "op");
const rightDoc = path.call(print, "right");
// This variable signifies that this node is a left child with the same
// precedence as its parent, and thus should be laid out on the same indent
// level as its parent, e.g:
// a = b >
// c > // -> the (b > c) node here
// d
const isLeftChildNodeWithoutGrouping = (isNodeSamePrecedenceAsLeftChild || !isLeftNodeBinaryish) &&
isNestedExpression &&
isNodeSamePrecedenceAsParent &&
!isNestedRightExpression;
// #265 - This variable signifies that the right child of this node should
// be laid out on the same indentation level, even though the left child node
// should be in its own group, e.g:
// a = b > c && d && e -> The node (d) here
const hasRightChildNodeWithoutGrouping = !isLeftChildNodeWithoutGrouping &&
isNestedExpression &&
isNodeSamePrecedenceAsParent &&
!isNestedRightExpression;
// This variable signifies that the left node and right has the same
// precedence, and thus they should be laid out on the same indent level, e.g.:
// a = b > 1 &&
// c > 1
const leftChildNodeSamePrecedenceAsRightChildNode = isLeftNodeBinaryish &&
isRightNodeBinaryish &&
getPrecedence(getOperator(node.left)) ===
getPrecedence(getOperator(node.right));
// This variable signifies that this node is the top most binaryish node,
// and its left child node has the same precedence, e.g:
// a = b >
// c >
// d // -> the entire node (b > c > d) here
const isTopMostParentNodeWithoutGrouping = isNodeSamePrecedenceAsLeftChild && !isNestedExpression;
// If this expression is directly inside parentheses, we want to give it
// an extra level indentation, i.e.:
// ```
// createObject(
// firstBoolean &&
// secondBoolean
// );
// ```
// This is different behavior vs when the expression is in a variable
// declaration, i.e.:
// ```
// firstBoolean =
// secondBoolean &&
// thirdBoolean;
// ```
// This behavior is consistent with how upstream formats Javascript
const shouldIndentTopMostExpression = node.insideParenthesis;
if (isLeftChildNodeWithoutGrouping ||
leftChildNodeSamePrecedenceAsRightChildNode ||
isTopMostParentNodeWithoutGrouping) {
docs.push(leftDoc);
docs.push(" ");
docs.push([operationDoc, line, rightDoc]);
return shouldIndentTopMostExpression ? indentConcat(docs) : docs;
}
if (hasRightChildNodeWithoutGrouping) {
docs.push(group(leftDoc));
docs.push(" ");
docs.push([operationDoc, line, rightDoc]);
return docs;
}
// At this point we know that this node is not in a binaryish chain, so we
// can safely group the left doc and right doc separately to have this effect:
// a = b
// .c() > d
docs.push(group(leftDoc));
docs.push(" ");
// If the left child of a binaryish expression has an end of line comment,
// we want to make sure that comment is printed with the left child and
// followed by a hardline. Otherwise, it will lead to unstable comments in
// certain situation, because the EOL comment might become attached to the
// entire binaryish expression after the first format.
const leftChildHasEndOfLineComment = node.left.comments?.filter((comment) => comment.trailing && comment.placement === "endOfLine").length > 0;
if (leftChildHasEndOfLineComment) {
docs.push(groupConcat([operationDoc, hardline, rightDoc]));
}
else {
docs.push(groupConcat([operationDoc, line, rightDoc]));
}
return groupConcat(docs);
}
function handleAssignmentExpression(path, print) {
const node = path.getNode();
const docs = [];
const leftDoc = path.call(print, "left");
const operationDoc = path.call(print, "op");
const rightDoc = path.call(print, "right");
docs.push(leftDoc);
docs.push(" ");
docs.push(operationDoc);
const rightDocComments = node.right.comments;
const rightDocHasLeadingComments = Array.isArray(rightDocComments) &&
rightDocComments.some((comment) => comment.leading);
if (isBinaryish(node.right) || rightDocHasLeadingComments) {
docs.push(line);
docs.push(rightDoc);
return groupIndentConcat(docs);
}
docs.push(" ");
docs.push(rightDoc);
return groupConcat(docs);
}
function shouldDottedExpressionBreak(path) {
const node = path.getNode();
// #62 - `super` cannot be followed any white spaces
if (node.dottedExpr.value["@class"] === APEX_TYPES.SUPER_VARIABLE_EXPRESSION) {
return false;
}
// #98 - Even though `this` can synctactically be followed by whitespaces,
// make the formatted output similar to `super` to provide consistency.
if (node.dottedExpr.value["@class"] === APEX_TYPES.THIS_VARIABLE_EXPRESSION) {
return false;
}
if (node["@class"] !== APEX_TYPES.METHOD_CALL_EXPRESSION) {
return true;
}
if (checkIfParentIsDottedExpression(path)) {
return true;
}
if (node.dottedExpr.value &&
node.dottedExpr.value["@class"] === APEX_TYPES.METHOD_CALL_EXPRESSION) {
return true;
}
return node.dottedExpr.value;
}
function handleDottedExpression(path, print) {
const node = path.getNode();
const dottedExpressionParts = [];
const dottedExpressionDoc = path.call(print, "dottedExpr", "value");
if (dottedExpressionDoc) {
dottedExpressionParts.push(dottedExpressionDoc);
if (shouldDottedExpressionBreak(path)) {
dottedExpressionParts.push(softline);
}
if (node.isSafeNav) {
dottedExpressionParts.push("?");
}
dottedExpressionParts.push(".");
return dottedExpressionParts;
}
return "";
}
function handleArrayExpressionIndex(path, print, withGroup = true) {
const node = path.getNode();
let parts;
if (node.index["@class"] === APEX_TYPES.LITERAL_EXPRESSION) {
// For literal index, we will make sure it's always attached to the [],
// because it's usually short and will look bad being broken up.
parts = ["[", path.call(print, "index"), "]"];
}
else {
parts = ["[", softline, path.call(print, "index"), dedent(softline), "]"];
}
return withGroup ? groupIndentConcat(parts) : parts;
}
function handleVariableExpression(path, print) {
const node = path.getNode();
const parentNode = path.getParentNode();
const nodeName = path.getName();
const { dottedExpr } = node;
const parts = [];
const dottedExpressionDoc = handleDottedExpression(path, print);
const isParentDottedExpression = checkIfParentIsDottedExpression(path);
const isDottedExpressionSoqlExpression = dottedExpr &&
dottedExpr.value &&
(dottedExpr.value["@class"] === APEX_TYPES.SOQL_EXPRESSION ||
(dottedExpr.value["@class"] === APEX_TYPES.ARRAY_EXPRESSION &&
dottedExpr.value.expr &&
dottedExpr.value.expr["@class"] === APEX_TYPES.SOQL_EXPRESSION));
parts.push(dottedExpressionDoc);
// Name chain
const nameDocs = path.map(print, "names");
parts.push(join(".", nameDocs));
// Technically, in a typical array expression (e.g: a[b]),
// the variable expression is a child of the array expression.
// However, for certain situation we need to print the [] part as part of
// the group from the variable expression. For example:
// a
// .b
// .c[
// d.callMethod()
// ]
// If we print the [] as part of the array expression, like we usually do,
// the result will be:
// a
// .b
// .c[
// d.callMethod()
// ]
// Hence why we are deferring the printing of the [] part from handleArrayExpression
// to here.
if (parentNode["@class"] === APEX_TYPES.ARRAY_EXPRESSION &&
nodeName === "expr") {
path.callParent((innerPath) => {
const withGroup = isParentDottedExpression || !!dottedExpressionDoc;
parts.push(handleArrayExpressionIndex(innerPath, print, withGroup));
});
}
if (isParentDottedExpression || isDottedExpressionSoqlExpression) {
return parts;
}
return groupIndentConcat(parts);
}
function handleJavaVariableExpression(path, print) {
const parts = [];
parts.push("java:");
parts.push(join(".", path.map(print, "names")));
return parts;
}
function handleLiteralExpression(path, print, options) {
const node = path.getNode();
const literalType = path.call(print, "type", "$");
if (literalType === "NULL") {
return "null";
}
const literalDoc = path.call(print, "literal", "$");
let doc;
if (literalType === "STRING") {
// #165 - We have to use the original string because it might contain Unicode code points,
// which gets converted to displayed characters automatically by Java after being parsed by jorje.
doc = options.originalText.slice(node.loc.startIndex, node.loc.endIndex);
}
else if (literalType === "LONG" ||
literalType === "DECIMAL" ||
literalType === "DOUBLE") {
const literal = options.originalText.slice(node.loc.startIndex, node.loc.endIndex);
let lastCharacter = literal[literal.length - 1];
/* v8 ignore next 3 */
if (lastCharacter === undefined) {
lastCharacter = "";
}
const lowercasedLastCharacter = lastCharacter.toLowerCase();
// We handle the letters d and l at the end of Decimal and Long manually:
// ```
// Decimal a = 1.0D
// Long b = 4324234234l
// ```
// should be formatted to:
// ```
// Decimal a = 1.0d
// Long b = 4324234234L
// ```
// In general we try to keep keywords lowercase, however uppercase L is better
// the lowercase l because lowercase l can be mistaken for number 1
if (lowercasedLastCharacter === "d") {
doc = `${literal.substring(0, literal.length - 1)}d`;
}
else if (lowercasedLastCharacter === "l") {
doc = `${literal.substring(0, literal.length - 1)}L`;
}
else {
doc = literal;
}
}
if (doc) {
return doc;
}
return literalDoc;
}
function handleBinaryOperation(path) {
const node = path.getNode();
return BINARY[node.$];
}
function handleBooleanOperation(path) {
const node = path.getNode();
return BOOLEAN[node.$];
}
function handleAssignmentOperation(path) {
const node = path.getNode();
return ASSIGNMENT[node.$];
}
function getDanglingCommentDocs(path, _print, options) {
const node = path.getNode();
if (!node.comments) {
return [];
}
node.danglingComments = node.comments.filter((comment) => !comment.leading && !comment.trailing);
const danglingCommentParts = [];
path.each((commentPath) => {
danglingCommentParts.push(printDanglingComment(commentPath, options));
}, "danglingComments");
delete node.danglingComments;
return danglingCommentParts;
}
function handleAnonymousBlockUnit(path, print) {
// Unlike other compilation units, Anonymous Unit cannot have dangling comments,
// so we don't have to handle them here.
const parts = [];
const memberParts = path
.map(print, "members")
.filter((member) => member);
for (let i = 0; i < memberParts.length; i++) {
const memberDoc = memberParts[i];
if (!memberDoc) {
// eslint-disable-next-line no-continue -- for type safety only
continue;
}
if (i === 0) {
parts.push(memberDoc);
}
else {
parts.push(hardline);
parts.push(memberDoc);
}
// #1892 - respect trailing empty line even for ignored nodes
if (path.call((innerPath) => hasPrettierIgnore(innerPath) && innerPath.getNode().trailingEmptyLine, "members", i, "stmnt")) {
parts.push(hardline);
}
}
return parts;
}
function handleTriggerDeclarationUnit(path, print, options) {
const usageDocs = path.map(print, "usages");
const targetDocs = path.map(print, "target");
const danglingCommentDocs = getDanglingCommentDocs(path, print, options);
const parts = [];
const usageParts = [];
parts.push("trigger");
parts.push(" ");
parts.push(path.call(print, "name"));
parts.push(" ");
parts.push("on");
parts.push(" ");
parts.push(join(",", targetDocs));
parts.push("(");
// Usage
usageParts.push(softline);
usageParts.push(join([",", line], usageDocs));
usageParts.push(dedent(softline));
parts.push(groupIndentConcat(usageParts));
parts.push(")");
parts.push(" ");
parts.push("{");
const memberParts = path
.map(print, "members")
.filter((member) => member);
const memberDocs = memberParts.map((memberDoc, index, allMemberDocs) => {
const innerDocs = [memberDoc];
if (index !== allMemberDocs.length - 1) {
innerDocs.push(hardline);
}
// #1892 - respect trailing empty line even for ignored nodes
if (path.call((innerPath) => hasPrettierIgnore(innerPath) &&
innerPath.getNode().trailingEmptyLine, "members", index, "stmnt")) {
innerDocs.push(hardline);
}
return innerDocs;
});
if (danglingCommentDocs.length > 0) {
parts.push(indent([hardline, ...danglingCommentDocs]));
}
else if (memberDocs.length > 0) {
parts.push(indent([hardline, ...memberDocs]));
}
parts.push(dedent([hardline, "}"]));
return parts;
}
function handleInterfaceDeclaration(path, print, options) {
const node = path.getNode();
const superInterface = path.call(print, "superInterface", "value");
const modifierDocs = path.map(print, "modifiers");
const memberParts = path
.map(print, "members")
.filter((member) => member);
const danglingCommentDocs = getDanglingCommentDocs(path, print, options);
const memberDocs = memberParts.map((memberDoc, index, allMemberDocs) => {
if (index !== allMemberDocs.length - 1) {
return [memberDoc, hardline];
}
return memberDoc;
});
const parts = [];
if (modifierDocs.length > 0) {
parts.push(modifierDocs);
}
parts.push("interface");
parts.push(" ");
parts.push(path.call(print, "name"));
if (node.typeArguments.value) {
const typeArgumentParts = path.map(print, "typeArguments", "value");
parts.push("<");
parts.push(join(", ", typeArgumentParts));
parts.push(">");
}
if (superInterface) {
parts.push(" ");
parts.push("extends");
parts.push(" ");
parts.push(superInterface);
}
parts.push(" ");
parts.push("{");
if (danglingCommentDocs.length > 0) {
parts.push(indent([hardline, ...danglingCommentDocs]));
}
else if (memberDocs.length > 0) {
parts.push(indent([hardline, ...memberDocs]));
}
parts.push([hardline, "}"]);
return parts;
}
function handleClassDeclaration(path, print, options) {
const node = path.getNode();
const superClass = path.call(print, "superClass", "value");
const modifierDocs = path.map(print, "modifiers");
const memberParts = path
.map(print, "members")
.filter((member) => member);
const danglingCommentDocs = getDanglingCommentDocs(path, print, options);
const memberDocs = memberParts.map((memberDoc, index, allMemberDocs) => {
if (index !== allMemberDocs.length - 1) {
return [memberDoc, hardline];
}
return memberDoc;
});
const parts = [];
if (modifierDocs.length > 0) {
parts.push(modifierDocs);
}
parts.push("class");
parts.push(" ");
parts.push(path.call(print, "name"));
if (node.typeArguments.value) {
const typeArgumentParts = path.map(print, "typeArguments", "value");
parts.push("<");
parts.push(join(", ", typeArgumentParts));
parts.push(">");
}
if (superClass !== "") {
parts.push(" ");
parts.push("extends");
parts.push(" ");
parts.push(superClass);
}
const interfaces = path.map(print, "interfaces");
if (interfaces.length > 0) {
parts.push(" ");
parts.push("implements");
parts.push(" ");
parts.push(join(", ", interfaces));
}
parts.push(" ");
parts.push("{");
if (danglingCommentDocs.length > 0) {
parts.push(indent([hardline, ...danglingCommentDocs]));
}
else if (memberDocs.length > 0) {
parts.push(indent([hardline, ...memberDocs]));
}
parts.push([hardline, "}"]);
return parts;
}
function handleAnnotation(path, print) {
const node = path.getNode();
const parts = [];
const trailingParts = [];
const parameterParts = [];
const parameterDocs = path.map(print, "parameters");
if (node.comments) {
// We print the comments manually because this method adds a hardline
// at the end of the annotation. If we left it to Prettier to print trailing
// comments it can lead to unstable formatting like this:
// ```
// @isTest
// // Trailing Comment
// void method() {}
// ```
path.each((innerPath) => {
const commentNode = innerPath.getNode();
// This can only be a trailing comment, because if it is a leading one,
// it will be attached to the Annotation's parent node (e.g. MethodDecl)
if (commentNode.trailing) {
trailingParts.push(" ");
trailingParts.push(printComment(innerPath));
}
}, "comments");
}
parts.push("@");
parts.push(path.call(print, "name", "value"));
if (parameterDocs.length > 0) {
parameterParts.push("(");
parameterParts.push(softline);
parameterParts.push(join(line, parameterDocs));
parameterParts.push(dedent(softline));
parameterParts.push(")");
parts.push(groupIndentConcat(parameterParts));
}
parts.push(...trailingParts);
parts.push(hardline);
return parts;
}
function handleAnnotationKeyValue(path, print) {
const parts = [];
parts.push(path.call(print, "key", "value"));
parts.push("=");
parts.push(path.call(print, "value"));
return parts;
}
function handleAnnotationValue(childClass, path, print) {
const parts = [];
switch (childClass) {
case APEX_TYPES.TRUE_ANNOTATION_VALUE:
parts.push("true");
break;
case APEX_TYPES.FALSE_ANNOTATION_VALUE:
parts.push("false");
break;
case APEX_TYPES.STRING_ANNOTATION_VALUE:
parts.push("'");
parts.push(path.call(print, "value"));
parts.push("'");
break;
}
return parts;
}
function handleAnnotationString(path, print) {
const parts = [];
parts.push("'");
parts.push(path.call(print, "value"));
parts.push("'");
return parts;
}
function handleClassTypeRef(path, print) {
const parts = [];
parts.push(join(".", path.map(print, "names")));
const typeArgumentDocs = path.map(print, "typeArguments");
if (typeArgumentDocs.length > 0) {
parts.push("<");
parts.push(join(", ", typeArgumentDocs));
parts.push(">");
}
return parts;
}
function handleArrayTypeRef(path, print) {
const parts = [];
parts.push(path.call(print, "heldType"));
parts.push("[]");
return parts;
}
function handleJavaTypeRef(path, print) {
const parts = [];
parts.push("java:");
parts.push(join(".", path.map(print, "names")));
return parts;
}
function handleStatementBlockMember(modifier) {
return (path, print) => {
const statementDoc = path.call(print, "stmnt");
const parts = [];
if (modifier) {
parts.push(modifier);
parts.push(" ");
}
pushIfExist(parts, statementDoc);
return parts;
};
}
function handlePropertyDeclaration(path, print) {
const modifierDocs = path.map(print, "modifiers");
const getterDoc = path.call(print, "getter", "value");
const setterDoc = path.call(print, "setter", "value");
const parts = [];
const innerParts = [];
parts.push(join("", modifierDocs));
parts.push(path.call(print, "type"));
parts.push(" ");
parts.push(path.call(print, "name"));
parts.push(" ");
parts.push("{");
if (getterDoc || setterDoc) {
innerParts.push(line);
}
if (getterDoc) {
innerParts.push(getterDoc);
if (setterDoc) {
innerParts.push(line);
}
else {
innerParts.push(dedent(line));
}
}
pushIfExist(innerParts, setterDoc, [dedent(line)]);
parts.push(groupIndentConcat(innerParts));
parts.push("}");
return groupConcat(parts);
}
function handlePropertyGetterSetter(action) {
return (path, print) => {
const statementDoc = path.call(print, "stmnt", "value");
const parts = [];
parts.push(path.call(print, "modifier", "value"));
parts.push(action);
if (statementDoc) {
parts.push(" ");
parts.push(statementDoc);
}
else {
parts.push(";");
}
return parts;
};
}
function handleMethodDeclaration(path, print) {
const statementDoc = path.call(print, "stmnt", "value");
const modifierDocs = path.map(print, "modifiers");
const parameterDocs = path.map(print, "parameters");
const parts = [];
const parameterParts = [];
// Modifiers
if (modifierDocs.length > 0) {
parts.push(modifierDocs);
}
// Return type
pushIfExist(parts, path.call(print, "type", "value"), [" "]);
// Method name
parts.push(path.call(print, "name"));
// Params
parts.push("(");
if (parameterDocs.length > 0) {
parameterParts.push(softline);
parameterParts.push(join([",", line], parameterDocs));
parameterParts.push(dedent(softline));
parts.push(groupIndentConcat(parameterParts));
}
parts.push(")");
// Body
pushIfExist(parts, statementDoc, null, [" "]);
if (!statementDoc) {
parts.push(";");
}
return parts;
}
function handleModifierParameterRef(path, print) {
const parts = [];
// Modifiers
parts.push(join("", path.map(print, "modifiers")));
// Type
parts.push(path.call(print, "typeRef"));
parts.push(" ");
// Value
parts.push(path.call(print, "name"));
return parts;
}
function handleEmptyModifierParameterRef(path, print) {
const parts = [];
// Type
parts.push(path.call(print, "typeRef"));
parts.push(" ");
// Value
parts.push(path.call(print, "name"));
return parts;
}
function handleStatement(childClass, path, print) {
let doc;
switch (childClass) {
case APEX_TYPES.DML_INSERT_STATEMENT:
doc = "insert";
break;
case APEX_TYPES.DML_UPDATE_STATEMENT:
doc = "update";
break;
case APEX_TYPES.DML_UPSERT_STATEMENT:
doc = "upsert";
break;
case APEX_TYPES.DML_DELETE_STATEMENT:
doc = "delete";
break;
case APEX_TYPES.DML_UNDELETE_STATEMENT:
doc = "undelete";
break;
/* v8 ignore start */
default:
throw new Error(`Statement ${childClass} is not supported. Please file a bug report.`);
/* v8 ignore stop */
}
const node = path.getNode();
const parts = [];
parts.push(doc);
parts.push(" ");
pushIfExist(parts, path.call(print, "runAsMode", "value"), [" "], ["as "]);
parts.push(path.call(print, "expr"));
// upsert statement has an extra param that can be tacked on at the end
if (node.id) {
pushIfExist(parts, path.call(print, "id", "value"), null, [indent(line)]);
}
parts.push(";");
return groupConcat(parts);
}
function handleDmlMergeStatement(path, print) {
const parts = [];
parts.push("merge");
parts.push(" ");
pushIfExist(parts, path.call(print, "runAsMode", "value"), [" "], ["as "]);
parts.push(path.call(print, "expr1"));
parts.push(line);
parts.push(path.call(print, "expr2"));
parts.push(";");
return groupIndentConcat(parts);
}
function handleEnumDeclaration(path, print, options) {
const modifierDocs = path.map(print, "modifiers");
const memberDocs = path.map(print, "members");
const danglingCommentDocs = getDanglingCommentDocs(path, print, options);
const parts = [];
pushIfExist(parts, join("", modifierDocs));
parts.push("enum");
parts.push(" ");
parts.push(path.call(print, "name"));
parts.push(" ");
parts.push("{");
if (danglingCommentDocs.length > 0) {
parts.push(indent([hardline, ...danglingCommentDocs]));
}
else if (memberDocs.length > 0) {
parts.push(indent([hardline, join([",", hardline], memberDocs)]));
}
parts.push([hardline, "}"]);
return parts;
}
function handleSwitchStatement(path, print) {
const whenBlocks = path.map(print, "whenBlocks");
const parts = [];
parts.push("switch on");
parts.push(groupConcat([line, path.call(print, "expr")]));
parts.push(" ");
parts.push("{");
parts.push(hardline);
parts.push(join(hardline, whenBlocks));
parts.push(dedent(hardline));
parts.push("}");
return groupIndentConcat(parts);
}
function handleValueWhen(path, print) {
const whenCaseDocs = path.map(print, "whenCases");
const statementDoc = path.call(print, "stmnt");
const parts = [];
parts.push("when");
parts.push(" ");
const whenCaseGroup = group(indent(join([",", line], whenCaseDocs)));
parts.push(whenCaseGroup);
parts.push(" ");
pushIfExist(parts, statementDoc);
return parts;
}
function handleElseWhen(path, print) {
const statementDoc = path.call(print, "stmnt");
const parts = [];
parts.push("when");
parts.push(" ");
parts.push("else");
parts.push(" ");
pushIfExist(parts, statementDoc);
return parts;
}
function handleTypeWhen(path, print) {
const statementDoc = path.call(print, "stmnt");
const parts = [];
parts.push("when");
parts.push(" ");
parts.push(path.call(print, "typeRef"));
parts.push(" ");
parts.push(path.call(print, "name"));
parts.push(" ");
pushIfExist(parts, statementDoc);
return parts;
}
function handleEnumCase(path, print) {
return join(".", path.map(print, "identifiers"));
}
function handleInputParameters(path, print) {
// In most cases, the descendant nodes inside `inputParameters` will create
// their own groups. However, in certain circumstances (i.e. with binaryish
// behavior), they rely on groups created by their parents. That's why we
// wrap each inputParameter in a group here. See #693 for an example case.
return path.map(print, "inputParameters").map((paramDoc) => group(paramDoc));
}
function handleRunAsBlock(path, print) {
const paramDocs = handleInputParameters(path, print);
const statementDoc = path.call(print, "stmnt");
const parts = [];
parts.push("System.runAs");
parts.push("(");
parts.push(join([",", line], paramDocs));
parts.push(")");
parts.push(" ");
pushIfExist(parts, statementDoc);
return parts;
}
function handleBlockStatement(path, print, options) {
const parts = [];
const danglingCommentDocs = getDanglingCommentDocs(path, print, options);
const statementDocs = path.map(print, "stmnts");
parts.push("{");
if (danglingCommentDocs.length > 0) {
parts.push([hardline, ...danglingCommentDocs]);
}
else if (statementDocs.length > 0) {
parts.push(hardline);
for (let i = 0; i < statementDocs.length; i++) {
const statementDoc = statementDocs[i];
if (!statementDoc) {
// eslint-disable-next-line no-continue -- for type safety only
continue;
}
if (i === 0) {
parts.push(statementDoc);
}
else {
parts.push(hardline);
parts.push(statementDoc);
}
// #1892 - respect trailing empty line even for ignored nodes
if (path.call((innerPath) => hasPrettierIgnore(innerPath) &&
innerPath.getNode().trailingEmptyLine, "stmnts", i)) {
parts.push(hardline);
}
}
}
parts.push(dedent(hardline));
parts.push("}");
return groupIndentConcat(parts);
}
function handleTryCatchFinallyBlock(path, print) {
const node = path.getNode();
const tryStatementDoc = path.call(print, "tryBlock");
const catchBlockDocs = path.map(print, "catchBlocks");
const finallyBlockDoc = path.call(print, "finallyBlock", "value");
const parts = [];
parts.push("try");
parts.push(" ");
pushIfExist(parts, tryStatementDoc);
const tryBlockContainsTrailingComments = node.tryBlock.comments?.some((comment) => comment.trailing);
let catchBlockContainsLeadingOwnLineComments = [];
let catchBlockContainsTrailingComments = [];
if (catchBlockDocs.length > 0) {
catchBlockContainsLeadingOwnLineComments = node.catchBlocks.map((catchBlock) => catchBlock.comments?.some((comment) => comment.leading && comment.placement === "ownLine"));
catchBlockContainsTrailingComments = node.catchBlocks.map((catchBlock) => catchBlock.comments?.some((comment) => comment.trailing));
catchBlockDocs.forEach((catchBlockDoc, index) => {
const shouldAddHardLineBeforeCatch = catchBlockContainsLeadingOwnLineComments[index] ||
catchBlockContainsTrailingComments[index - 1] ||
(index === 0 && tryBlockContainsTrailingComments);
if (shouldAddHardLineBeforeCatch) {
parts.push(hardline);
}
else {
parts.push(" ");
}
parts.push(catchBlockDoc);
});
}
const finallyBlockContainsLeadingOwnLineComments = node.finallyBlock?.value?.comments?.some((comment) => comment.leading && comment.placement === "ownLine");
const shouldAddHardLineBeforeFinally = finallyBlockContainsLeadingOwnLineComments ||
(catchBlockContainsTrailingComments.length > 0 &&
catchBlockContainsTrailingComments[catchBlockContainsTrailingComments.length - 1]) ||
(catchBlockContainsTrailingComments.length === 0 &&
tryBlockContainsTrailingComments);
pushIfExist(parts, finallyBlockDoc, null, [
shouldAddHardLineBeforeFinally ? hardline : " ",
]);
return parts;
}
function handleCatchBlock(path, print) {
const parts = [];
parts.push("catch");
parts.push(" ");
parts.push("(");
parts.push(path.call(print, "parameter"));
parts.push(")");
parts.push(" ");
pushIfExist(parts, path.call(print, "stmnt"));
return parts;
}
function handleFinallyBlock(path, print) {
const parts = [];
parts.push("finally");
parts.push(" ");
pushIfExist(parts, path.call(print, "stmnt"));
return parts;
}
function handleVariableDeclarations(path, print) {
const modifierDocs = path.map(print, "modifiers");
const parts = [];
// Modifiers
parts.push(join("", modifierDocs));
// Type
parts.push(path.call(print, "type"));
parts.push(" ");
// Variable declarations
const declarationDocs = path.map(print, "decls");
if (declarationDocs.length > 1) {
parts.push(indentConcat([join([",", line], declarationDocs)]));
parts.push(";");
}
else if (declarationDocs.length === 1 && declarationDocs[0] !== undefined) {
parts.push([declarationDocs[0], ";"]);
}
return groupConcat(parts);
}
function handleVariableDeclaration(path, print) {
const node = path.getNode();
const parts = [];
let resultDoc;
parts.push(path.call(print, "name"));
const assignmentDocs = path.call(print, "assignment", "value");
const assignmentComments = node.assignment?.value?.comments;
const assignmentHasLeadingComment = Array.isArray(assignmentComments) &&
assignmentComments.some((comment) => comment.leading);
if (assignmentDocs &&
(isBinaryish(node.assignment.value) || assignmentHasLeadingComment)) {
parts.push(" ");
parts.push("=");
parts.push(line);
parts.push(assignmentDocs);
resultDoc = groupIndentConcat(parts);
}
else if (assignmentDocs) {
parts.push(" ");
parts.push("=");
parts.push(" ");
parts.push(assignmentDocs);
resultDoc = groupConcat(parts);
}
else {
resultDoc = groupConcat(parts);
}
return resultDoc;
}
function handleNewStandard(path, print) {
const paramDocs = handleInputParameters(path, print);
const parts = [];
// Type
parts.push(path.call(print, "type"));
// Params
parts.push("(");
if (paramDocs.length > 0) {
parts.push(softline);
parts.push(join([",", line], paramDocs));
parts.push(dedent(softline));
}
parts.push(")");
return groupIndentConcat(parts);
}
function handleNewKeyValue(path, print) {
const keyValueDocs = path.map(print, "keyValues");
const parts = [];
parts.push(path.call(print, "type"));
parts.push("(");
if (keyValueDocs.length > 0) {
parts.push(softline);
parts.push(join([",", line], keyValueDocs));
parts.push(dedent(softline));
}
parts.push(")");
return groupIndentConcat(parts);
}
function handleNameValueParameter(path, print) {
const node = path.getNode();
const parts = [];
parts.push(path.call(print, "name"));
parts.push(" ");
parts.push("=");
parts.push(" ");
if (isBinaryish(node.value)) {
// Binaryish expressions require their parents to the indentation level,
// instead of setting it themselves like other expressions.
parts.push(group(indent(path.call(print, "value"))));
}
else {
parts.push(path.call(print, "value"));
}
return parts;
}
function handleThisMethodCallExpression(path, print) {
const parts = [];
parts.push("this");
parts.push("(");
parts.push(softline);
const paramDocs = handleInputParameters(path, print);
parts.push(join([",", line], paramDocs));
parts.push(dedent(softline));
parts.push(")");
return groupIndentConcat(parts);
}
function handleSuperMethodCallExpression(path, print) {
const parts = [];
parts.push("super");
parts.push("(");
parts.push(softline);
const paramDocs = handleInputParameters(path, print);
parts.push(join([",", line], paramDocs));
parts.push(dedent(softline));
parts.push(")");
return groupIndentConcat(parts);
}
function handleMethodCallExpression(path, print) {
const node = path.getNode();
const parentNode = path.getParentNode();
const nodeName = path.getName();
const { dottedExpr } = node;
const isParentDottedExpression = checkIfParentIsDottedExpression(path);
const isDottedExpressionSoqlExpression = dottedExpr &&
dottedExpr.value &&
(dottedExpr.value["@class"] === APEX_TYPES.SOQL_EXPRESSION ||
(dottedExpr.value["@class"] === APEX_TYPES.ARRAY_EXPRESSION &&
dottedExpr.value.expr &&
dottedExpr.value.expr["@class"] === APEX_TYPES.SOQL_EXPRESSION));
const isDottedExpressionThisVariableExpression = dottedExpr &&
dottedExpr.value &&
dottedExpr.value["@class"] === APEX_TYPES.THIS_VARIABLE_EXPRESSION;
const isDottedExpressionSuperVariableExpression = dottedExpr &&
dottedExpr.value &&
dottedExpr.value["@class"] === APEX_TYPES.SUPER_VARIABLE_EXPRESSION;
const dottedExpressionDoc = handleDottedExpression(path, print);
const nameDocs = path.map(print, "names");
const paramDocs = handleInputParameters(path, print);
const resultParamDoc = paramDocs.length > 0
? [softline, join([",", line], paramDocs), dedent(softline)]
: "";
const methodCallChainDoc = join(".", nameDocs);
// Handling the array expression index.
// Technically, in this statement: a()[b],
// the method call expression is a child of the array expression.
// However, for certain situation we need to print the [] part as part of
// the group from the method call expression. For example:
// a
// .b
// .c()[
// d.callMethod()
// ]
// If we print the [] as part of the array expression, like we usually do,
// the result will be:
// a
// .b
// .c()[
// d.callMethod()
// ]
// Hence why we are deferring the printing of the [] part from handleArrayExpression
// to here.
let arrayIndexDoc = "";
if (parentNode["@class"] === APEX_TYPES.ARRAY_EXPRESSION &&
nodeName === "expr") {
path.callParent((innerPath) => {
const withGroup = isParentDottedExpression || !!dottedExpressionDoc;
arrayIndexDoc = handleArrayExpressionIndex(innerPath, print, withGroup);
});
}
let resultDoc;
const noGroup =
// If this is a nested dotted expression, we do not want to group it,
// since we want it to be part of the method call chain group, e.g:
// a
// .b() // <- this node here
// .c() // <- this node here
// .d()
isParentDottedExpression ||
// If dotted expression is SOQL and this in inside a binaryish expression,
// we shouldn't group it, otherwise there will be extraneous indentations,
// for example:
// Boolean a =
// [
// SELECT Id FROM Contact
// ].size() > 0
(isDottedExpressionSoqlExpression && isBinaryish(parentNode)) ||
// If dotted expression is a `super` or `this` variable expression, we
// know that this is only one level deep and there's no need to group, e.g:
// `this.simpleMethod();` or `super.simpleMethod();`
isDottedExpressionThisVariableExpression ||
isDottedExpressionSuperVariableExpression;
if (noGroup) {
resultDoc = [
dottedExpressionDoc,
methodCallChainDoc,
"(",
group(indent(resultParamDoc)),
")",
arrayIndexDoc,
];
}
else {
// This means it is the highest level method call expression,
// and we do need to group and indent the expressions in it, e.g:
// a
// .b()
// .c()
// .d() // <- this node here
resultDoc = group(indent([
dottedExpressionDoc,
// If there is no dottedExpr, we should group the method call chain
// to have this effect:
// a.callMethod( // <- 2 names (a and callMethod)
// 'a',
// 'b'
// )
// Otherwise we don't want to group them, so that they're part of the
// parent group. It will format this code:
// a.b().c().callMethod('a', 'b') // <- 4 names (a, b, c, callMethod)
// into this:
// a.b()
// .c()
// .callMethod('a', 'b')
dottedExpressionDoc ? methodCallChainDoc : group(methodCallChainDoc),
"(",
dottedExpressionDoc
? group(indent(resultParamDoc))
: group(resultParamDoc),
")",
arrayIndexDoc,
]));
}
return resultDoc;
}
function handleJavaMethodCallExpression(path, print) {
const parts = [];
parts.push("java:");
parts.push(join(".", path.map(print, "names")));
parts.push("(");
parts.push(softline);
parts.push(join([",", line], handleInputParameters(path, print)));
parts.push(dedent(softline));
parts.push(")");
return groupIndentConcat(parts);
}
function handleNestedExpression(path, print) {
const parts = [];
parts.push("(");
parts.push(path.call(print, "expr"));
parts.push(")");
return parts;
}
function handleNewSetInit(path, print) {
const parts = [];
const expressionDoc = path.call(print, "expr", "value");
// Type
parts.push("Set");
parts.push("<");
parts.push(join([",", " "], path.map(print, "types")));
parts.push(">");
// Param
parts.push("(");
pushIfExist(parts, expressionDoc, [dedent(softline)], [softline]);
parts.push(")");
return groupIndentConcat(parts);
}
function handleNewSetLiteral(path, print) {
const valueDocs = path.map(print, "values");
const parts = [];
// Type
parts.push("Set");
parts.push("<");
parts.push(join([",", " "], path.map(print, "types")));
parts.push(">");
// Values
parts.push("{");
if (valueDocs.length > 0) {
parts.push(line);
parts.push(join([",", line], valueDocs));
parts.push(dedent(line));
}
parts.push("}");
return groupIndentConcat(parts);
}
function handleNewListInit(path, print) {
// We can declare lists in the following ways:
// new Object[size];
// new Object[] { value, ... };
// new List<Object>(); // Provides AST consistency.
// new List<Object>(size);
// #262 - We use Object[size] if a literal number is provided.
// We use List<Object>(param) otherwise.
// This should provide compatibility for all known types without knowing
// if the parameter is a variable (copy constructor) or literal size.
const node = path.getNode();
const expressionDoc = path.call(print, "expr", "value");
const parts = [];
const typeParts = path.map(print, "types");
const hasLiteralNumberInitializer = typeParts.length &&
typeParts[0] !== undefined &&
typeof typeParts[0] !== "string" &&
"length" in typeParts[0] &&
typeParts[0].length < 4 &&
node.expr?.value?.type?.$ === "INTEGER";
// Type
if (!hasLiteralNumberInitializer) {
parts.push("List<");
}
parts.push(join(".", typeParts));
if (!hasLiteralNumberInitializer) {
parts.push(">");
}
// Param
parts.push(hasLiteralNumberInitializer ? "[" : "(");
pushIfExist(parts, expressionDoc, [dedent(softline)], [softline]);
parts.push(hasLiteralNumberInitializer ? "]" : ")");
return groupIndentConcat(parts);
}
function handleNewMapInit(path, print) {
const parts = [];
const expressionDoc = path.call(print, "expr", "value");
parts.push("Map");
// Type
parts.push("<");
const typeDocs = path.map(print, "types");
parts.push(join(", ", typeDocs));
parts.push(">");
parts.push("(");
pushIfExist(parts, expressionDoc, [dedent(softline)], [softline]);
parts.push(")");
return groupIndentConcat(parts);
}
function handleNewMapLiteral(path, print) {
const valueDocs = path.map(print, "pairs");
const parts = [];
// Type
parts.push("Map");
parts.push("<");
parts.push(join(", ", path.map(print, "types")));
parts.push(">");
// Values
parts.push("{");
if (valueDocs.length > 0) {
parts.push(line);
parts.push(join([",", line], valueDocs));
parts.push(dedent(line));
}
parts.push("}");
return groupIndentConcat(parts);
}
function handleMapLiteralKeyValue(path, print) {
const parts = [];
parts.push(path.call(print, "key"));
parts.push(" ");
parts.push("=>");
parts.push(" ");
parts.push(path.call(print, "value"));
return parts;
}
function handleNewListLiteral(path, print) {
const valueDocs = path.map(print, "values");
const parts = [];
// Type
parts.push("List<");
parts.push(join(".", path.map(print, "types")));
parts.push(">");
// Values
parts.push("{");
if (valueDocs.length > 0) {
parts.push(line);
parts.push(join([",", line], valueDocs));
parts.push(dedent(line));
}
parts.push("}");
return groupIndentConcat(parts);
}
function handleNewExpression(path, print) {
const parts = [];
parts.push("new");
parts.push(" ");
parts.push(path.call(print, "creator"));
return parts;
}
function handleIfElseBlock(path, print) {
const node = path.getNode();
const parts = [];
const ifBlockDocs = path.map(print, "ifBlocks");
const elseBlockDoc = path.call(print, "elseBlock", "value");
// There are differences when we handle block vs expression statements in
// if bodies and else body. For expression statement, we need to add a
// hardline after a statement vs a space for block statement. For example:
// if (a)
// b = 1;
// else if (c) {
// b = 2;
// }
const ifBlockContainsBlockStatement = node.ifBlocks.map((ifBlock) => ifBlock.stmnt["@class"] === APEX_TYPES.BLOCK_STATEMENT);
const ifBlockContainsLeadingOwnLineComments = node.ifBlocks.map((ifBlock) => ifBlock.comments?.some((comment) => comment.leading && comment.placement === "ownLine"));
const ifBlockContainsTrailingComments = node.ifBlocks.map((ifBlock) => ifBlock.comments?.some((comment) => comment.trailing));
let lastIfBlockHardLineInserted = false;
ifBlockDocs.forEach((ifBlockDoc, index) => {
if (index > 0) {
const shouldAddHardLineBeforeElseIf = !ifBlockContainsBlockStatement[index - 1] ||
ifBlockContainsLeadingOwnLineComments[index] ||
ifBlockContainsTrailingComments[index - 1];
if (shouldAddHardLineBeforeElseIf) {
parts.push(hardline);
}
else {
parts.push(" ");
}
}
parts.push(ifBlockDoc);
// We also need to handle the last if block, since it might need to add
// either a space or a hardline before the else block
if (index === ifBlockDocs.length - 1 && elseBlockDoc) {
if (ifBlockContainsBlockStatement[index]) {
parts.push(" ");
}
else {
parts.push(hardline);
lastIfBlockHardLineInserted = true;
}
}
});
if (elseBlockDoc) {
const elseBlockContainsLeadingOwnLineComments = node.elseBlock?.value?.comments?.some((comment) => comment.leading && comment.placement === "ownLine");
const lastIfBlockContainsTrailingComments = ifBlockContains