UNPKG

prettier-plugin-apex

Version:

Salesforce Apex plugin for Prettier

1,390 lines 98.8 kB
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