prettier-plugin-apex
Version:
Salesforce Apex plugin for Prettier
370 lines (369 loc) • 12.8 kB
JavaScript
import * as prettier from "prettier";
import { ALLOW_DANGLING_COMMENTS, APEX_TYPES } from "./constants.js";
import { isApexDocComment, isBinaryish, } from "./util.js";
const { join, lineSuffix, hardline } = prettier.doc.builders;
const { addDanglingComment, addLeadingComment, addTrailingComment, hasNewlineInRange, skipWhitespace, } = prettier.util;
/**
* Print ApexDoc comment. This is straight from prettier handling of JSDoc
* @param comment the comment to print.
*/
function printApexDocComment(comment) {
const lines = comment.value.split("\n");
return [
join(hardline, lines.map((commentLine, index) => (index > 0 ? " " : "") +
(index < lines.length - 1
? commentLine.trim()
: commentLine.trimStart()))),
];
}
export function isPrettierIgnore(comment) {
let content;
if (comment["@class"] === APEX_TYPES.BLOCK_COMMENT) {
// For simplicity sake we only support this format
// /* prettier-ignore */
content = comment.value
.trim()
.substring(2, comment.value.length - 2)
.trim();
}
else {
content = comment.value.trim().substring(2).trim();
}
return content === "prettier-ignore";
}
export function printComment(path) {
// This handles both Inline and Block Comments.
// We don't just pass through the value because unlike other string literals,
// this should not be escaped
let result;
const node = path.getNode();
if (isApexDocComment(node)) {
result = printApexDocComment(node);
}
else {
result = node.value;
}
if (node.trailingEmptyLine) {
result = [result, hardline];
}
node.printed = true;
return result;
}
export function printDanglingComment(commentPath, options) {
const sourceCode = options.originalText;
const comment = commentPath.getNode();
const loc = comment.location;
const isFirstComment = commentPath.getName() === 0;
const parts = [];
let fromPos = skipWhitespace(sourceCode, loc.startIndex - 1, {
backwards: true,
});
/* v8 ignore next 3 */
if (fromPos === false) {
return "";
}
fromPos += 1;
const leadingSpace = sourceCode.slice(fromPos, loc.startIndex);
const numberOfNewLines = isFirstComment
? 0
: /* v8 ignore next 1 */
(leadingSpace.match(/\n/g) || []).length;
if (numberOfNewLines > 0) {
// If the leading space contains newlines, then add at most 2 new lines
const numberOfNewLinesToInsert = Math.min(numberOfNewLines, 2);
parts.push(...Array(numberOfNewLinesToInsert).fill(hardline));
}
if (comment["@class"] === APEX_TYPES.INLINE_COMMENT) {
parts.push(lineSuffix(printComment(commentPath)));
}
else {
parts.push(printComment(commentPath));
}
comment.printed = true;
return parts;
}
/**
* This is called by Prettier's comment handling code, in order for Prettier
* to tell if this is a node to which a comment can be attached.
*
* @param node The current node
* @returns {boolean} whether a comment can be attached to this node or not.
*/
export function canAttachComment(node) {
return (node.loc &&
node["@class"] &&
node["@class"] !== APEX_TYPES.INLINE_COMMENT &&
node["@class"] !== APEX_TYPES.BLOCK_COMMENT);
}
/**
* This is called by Prettier's comment handling code, in order to find out
* if this is a block comment.
*
* @param comment The current comment node.
* @returns {boolean} whether it is a block comment.
*/
export function isBlockComment(comment) {
return comment["@class"] === APEX_TYPES.BLOCK_COMMENT;
}
/**
* This is called by Prettier's comment handling code.
* We can use this to tell Prettier that we will print comments manually on
* certain nodes.
* @returns {boolean} whether or not we will print the comment on this node manually.
*/
export function willPrintOwnComments(path) {
const node = path.getNode();
return !node || !node["@class"] || node["@class"] === APEX_TYPES.ANNOTATION;
}
export function getTrailingComments(node) {
return node.comments.filter((comment) => comment.trailing);
}
function handleDanglingComment(comment) {
const { enclosingNode } = comment;
if (enclosingNode &&
ALLOW_DANGLING_COMMENTS.indexOf(enclosingNode["@class"]) !== -1 &&
((enclosingNode.stmnts && enclosingNode.stmnts.length === 0) ||
(enclosingNode.members && enclosingNode.members.length === 0))) {
addDanglingComment(enclosingNode, comment, null);
return true;
}
return false;
}
/**
* Turn the leading comment to a WhereExpression inside a
* WhereCompoundExpression into a trailing comment to the previous WhereExpression.
* The reason is that a WhereExpression does not contain the location of
* the WhereCompoundOp (e.g. AND, OR), and without doing that, the following
* transformation occurs:
* ```
* SELECT Id
* FROM Contact
* WHERE
* Name = 'Name'
* AND
* // Comment
* Name = 'Another Name'
* ```
* Instead, this looks better:
* ```
* SELECT Id
* FROM Contact
* WHERE
* Name = 'Name'
* // Comment
* AND Name = 'Another Name'
* ```
*/
function handleWhereExpression(comment, sourceCode) {
const { enclosingNode, precedingNode, followingNode } = comment;
if (!enclosingNode ||
!precedingNode ||
!followingNode ||
!precedingNode["@class"] ||
!followingNode["@class"] ||
enclosingNode["@class"] !== APEX_TYPES.WHERE_COMPOUND_EXPRESSION ||
comment.location === undefined ||
comment.location.startIndex === undefined) {
return false;
}
if (hasNewlineInRange(sourceCode, precedingNode.loc.endIndex, comment.location.startIndex)) {
addTrailingComment(precedingNode, comment);
return true;
}
return false;
}
/**
* Bring leading comment before Block Statement into the block itself:
* ```
* for (
* Contact a: [SELECT Id FROM Contact]
* // Trailing EOL Inline comment
* ) {
* System.debug('Hello');
* }
* ```
* transformed into
* ```
* for (Contact a: [SELECT Id FROM Contact]) {
* // Trailing EOL Inline Comment
* System.debug('Hello');
* }
* ```
*/
function handleBlockStatementLeadingComment(comment) {
const { followingNode } = comment;
if (!followingNode ||
followingNode["@class"] !== APEX_TYPES.BLOCK_STATEMENT) {
return false;
}
if (followingNode.stmnts.length) {
addLeadingComment(followingNode.stmnts[0], comment);
}
else {
addDanglingComment(followingNode, comment, null);
}
return true;
}
/**
* In a binaryish expression, if there is an end of line comment, we want to
* attach it to the right child expression instead of the entire binaryish
* expression, because doing the latter can lead to unstable comments in
* certain situations.
*/
function handleBinaryishExpressionRightChildTrailingComment(comment) {
const { precedingNode } = comment;
if (comment.placement !== "endOfLine" ||
!precedingNode ||
!isBinaryish(precedingNode)) {
return false;
}
addTrailingComment(precedingNode.right, comment);
return true;
}
/**
* Turn the leading comment in a long method or variable chain into the preceding
* comment of a previous node. Without doing that, we have an awkward position
* for the . character like so:
* ```
* return StringBuilder()
* .// Test Comment
* append('Hello')
* .toString();
* ```
* Instead, this looks better:
* ```
* return StringBuilder()
* // Test Comment
* .append('Hello')
* .toString();
* ```
*/
function handleLongChainComment(comment) {
const { enclosingNode, precedingNode, followingNode } = comment;
if (!enclosingNode ||
!precedingNode ||
!followingNode ||
(enclosingNode["@class"] !== APEX_TYPES.METHOD_CALL_EXPRESSION &&
enclosingNode["@class"] !== APEX_TYPES.VARIABLE_EXPRESSION)) {
return false;
}
if (enclosingNode.dottedExpr &&
enclosingNode.dottedExpr.value === precedingNode) {
addTrailingComment(precedingNode, comment);
return true;
}
return false;
}
// #1946 - when a comment is between the `continue`/`break`/`return` statement and
// the `;` at the end of the line, it is technically a dangling comment to that
// node. However, it makes more sense to simply classify it as a trailing
// comment to the statement itself, i.e.:
// ```
// continue /* Comment */;
// ```
// should be formatted as:
// ```
// continue; /* Comment */
// ```
function handleContinueBreakDanglingComment(comment) {
const { enclosingNode } = comment;
if (!enclosingNode) {
return false;
}
if (enclosingNode["@class"] === APEX_TYPES.CONTINUE_STATEMENT ||
enclosingNode["@class"] === APEX_TYPES.BREAK_STATEMENT) {
addTrailingComment(enclosingNode, comment);
return true;
}
if (enclosingNode["@class"] === APEX_TYPES.RETURN_STATEMENT &&
// if there is some value that's returned, the comment is attached to that
// value, so we don't need to handle this case
!enclosingNode.expr.value) {
addTrailingComment(enclosingNode, comment);
return true;
}
return false;
}
/**
* #383 (bug number 2) - If a prettier-ignore comment is attached to a modifier,
* we need to bring it up a level, otherwise the only thing that's getting
* ignored is the modifier itself, not the expression surrounding it (which is
* more likely what the user wants).
*/
function handleModifierPrettierIgnoreComment(comment) {
const { enclosingNode, followingNode } = comment;
if (!isPrettierIgnore(comment) ||
!enclosingNode ||
!followingNode ||
!followingNode["@class"] ||
!followingNode["@class"].startsWith(APEX_TYPES.MODIFIER)) {
return false;
}
addLeadingComment(enclosingNode, comment);
return true;
}
/**
* This is called by Prettier's comment handling code, in order to handle
* comments that are on their own line.
*
* @param comment The comment node.
* @param sourceCode The entire source code.
* @returns {boolean} Whether we have manually attached this comment to some AST
* node. If `true` is returned, Prettier will no longer try to attach this
* comment based on its internal heuristic.
*/
export function handleOwnLineComment(comment, sourceCode) {
return (handleDanglingComment(comment) ||
handleBlockStatementLeadingComment(comment) ||
handleWhereExpression(comment, sourceCode) ||
handleModifierPrettierIgnoreComment(comment) ||
handleLongChainComment(comment));
}
/**
* This is called by Prettier's comment handling code, in order to handle
* comments that have preceding text but no trailing text on a line.
*
* @param comment The comment node.
* @param sourceCode The entire source code.
* @returns {boolean} Whether we have manually attached this comment to some AST
* node. If `true` is returned, Prettier will no longer try to attach this
* comment based on its internal heuristic.
*/
export function handleEndOfLineComment(comment, sourceCode) {
return (handleDanglingComment(comment) ||
handleBinaryishExpressionRightChildTrailingComment(comment) ||
handleBlockStatementLeadingComment(comment) ||
handleWhereExpression(comment, sourceCode) ||
handleModifierPrettierIgnoreComment(comment) ||
handleLongChainComment(comment) ||
handleContinueBreakDanglingComment(comment));
}
/**
* This is called by Prettier's comment handling code, in order to handle
* comments that have both preceding text and trailing text on a line.
*
* @param comment The comment node.
* @param sourceCode The entire source code.
* @returns {boolean} Whether we have manually attached this comment to some AST
* node. If `true` is returned, Prettier will no longer try to attach this
* comment based on its internal heuristic.
*/
export function handleRemainingComment(comment, sourceCode) {
return (handleWhereExpression(comment, sourceCode) ||
handleModifierPrettierIgnoreComment(comment) ||
handleLongChainComment(comment) ||
handleContinueBreakDanglingComment(comment));
}
/**
* This is called by Prettier's comment handling code, in order to find out
* if a node should be formatted or not.
* @param path The FastPath object.
* @returns {boolean} Whether the path should be formatted.
*/
export function hasPrettierIgnore(path) {
const node = path.getNode();
return (node &&
node.comments &&
node.comments.length > 0 &&
node.comments.filter(isPrettierIgnore).length > 0);
}