prettier/src/main/comments.js

535 lines
15 KiB
JavaScript
Raw Normal View History

"use strict";
const assert = require("assert");
const {
concat,
hardline,
breakParent,
indent,
lineSuffix,
join,
cursor
} = require("../doc").builders;
const {
hasNewline,
skipNewline,
isPreviousLineEmpty
} = require("../common/util");
const {
addLeadingComment,
addDanglingComment,
addTrailingComment
} = require("../common/util-shared");
const childNodesCacheKey = Symbol("child-nodes");
function getSortedChildNodes(node, options, resultArray) {
if (!node) {
return;
}
const { printer, locStart, locEnd } = options;
if (resultArray) {
if (node && printer.canAttachComment && printer.canAttachComment(node)) {
// This reverse insertion sort almost always takes constant
// time because we almost always (maybe always?) append the
// nodes in order anyway.
let i;
for (i = resultArray.length - 1; i >= 0; --i) {
if (
locStart(resultArray[i]) <= locStart(node) &&
locEnd(resultArray[i]) <= locEnd(node)
) {
break;
}
}
resultArray.splice(i + 1, 0, node);
return;
}
} else if (node[childNodesCacheKey]) {
return node[childNodesCacheKey];
}
let childNodes;
if (printer.getCommentChildNodes) {
childNodes = printer.getCommentChildNodes(node);
} else if (node && typeof node === "object") {
childNodes = Object.keys(node)
.filter(
n =>
n !== "enclosingNode" &&
n !== "precedingNode" &&
n !== "followingNode"
)
.map(n => node[n]);
}
if (!childNodes) {
return;
}
if (!resultArray) {
Object.defineProperty(node, childNodesCacheKey, {
2017-03-04 02:39:37 +03:00
value: (resultArray = []),
enumerable: false
});
}
childNodes.forEach(childNode => {
getSortedChildNodes(childNode, options, resultArray);
});
return resultArray;
}
// As efficiently as possible, decorate the comment object with
// .precedingNode, .enclosingNode, and/or .followingNode properties, at
// least one of which is guaranteed to be defined.
function decorateComment(node, comment, options) {
const { locStart, locEnd } = options;
const childNodes = getSortedChildNodes(node, options);
let precedingNode;
let followingNode;
// Time to dust off the old binary search robes and wizard hat.
let left = 0;
let right = childNodes.length;
while (left < right) {
const middle = (left + right) >> 1;
const child = childNodes[middle];
2017-01-13 23:03:53 +03:00
if (
locStart(child) - locStart(comment) <= 0 &&
2017-02-16 06:56:11 +03:00
locEnd(comment) - locEnd(child) <= 0
2017-01-13 23:03:53 +03:00
) {
// The comment is completely contained by this child node.
comment.enclosingNode = child;
decorateComment(child, comment, options);
return; // Abandon the binary search at this level.
}
2017-01-10 05:24:42 +03:00
if (locEnd(child) - locStart(comment) <= 0) {
// This child node falls completely before the comment.
// Because we will never consider this node or any nodes
// before it again, this node must be the closest preceding
// node we have encountered so far.
precedingNode = child;
left = middle + 1;
continue;
}
2017-01-10 05:24:42 +03:00
if (locEnd(comment) - locStart(child) <= 0) {
// This child node falls completely after the comment.
// Because we will never consider this node or any nodes after
// it again, this node must be the closest following node we
// have encountered so far.
followingNode = child;
right = middle;
continue;
}
/* istanbul ignore next */
throw new Error("Comment location overlaps with node location");
}
// We don't want comments inside of different expressions inside of the same
// template literal to move to another expression.
if (
comment.enclosingNode &&
comment.enclosingNode.type === "TemplateLiteral"
) {
const quasis = comment.enclosingNode.quasis;
const commentIndex = findExpressionIndexForComment(
quasis,
comment,
options
);
if (
precedingNode &&
findExpressionIndexForComment(quasis, precedingNode, options) !==
commentIndex
) {
precedingNode = null;
}
if (
followingNode &&
findExpressionIndexForComment(quasis, followingNode, options) !==
commentIndex
) {
followingNode = null;
}
}
if (precedingNode) {
comment.precedingNode = precedingNode;
}
if (followingNode) {
comment.followingNode = followingNode;
}
}
function attach(comments, ast, text, options) {
if (!Array.isArray(comments)) {
return;
}
const tiesToBreak = [];
const { locStart, locEnd } = options;
comments.forEach((comment, i) => {
if (
2018-11-04 18:03:07 +03:00
options.parser === "json" ||
options.parser === "json5" ||
options.parser === "__js_expression" ||
options.parser === "__vue_expression"
) {
2018-11-04 18:03:07 +03:00
if (locStart(comment) - locStart(ast) <= 0) {
addLeadingComment(ast, comment);
return;
}
if (locEnd(comment) - locEnd(ast) >= 0) {
addTrailingComment(ast, comment);
return;
}
}
decorateComment(ast, comment, options);
const { precedingNode, enclosingNode, followingNode } = comment;
const pluginHandleOwnLineComment =
options.printer.handleComments && options.printer.handleComments.ownLine
? options.printer.handleComments.ownLine
: () => false;
const pluginHandleEndOfLineComment =
options.printer.handleComments && options.printer.handleComments.endOfLine
? options.printer.handleComments.endOfLine
: () => false;
const pluginHandleRemainingComment =
options.printer.handleComments && options.printer.handleComments.remaining
? options.printer.handleComments.remaining
: () => false;
const isLastComment = comments.length - 1 === i;
if (hasNewline(text, locStart(comment), { backwards: true })) {
// If a comment exists on its own line, prefer a leading comment.
// We also need to check if it's the first line of the file.
2017-02-03 19:50:51 +03:00
if (
pluginHandleOwnLineComment(comment, text, options, ast, isLastComment)
2017-02-03 19:50:51 +03:00
) {
// We're good
} else if (followingNode) {
// Always a leading comment.
addLeadingComment(followingNode, comment);
} else if (precedingNode) {
addTrailingComment(precedingNode, comment);
} else if (enclosingNode) {
addDanglingComment(enclosingNode, comment);
} else {
// There are no nodes, let's attach it to the root of the ast
/* istanbul ignore next */
addDanglingComment(ast, comment);
}
} else if (hasNewline(text, locEnd(comment))) {
2017-02-16 06:56:11 +03:00
if (
pluginHandleEndOfLineComment(comment, text, options, ast, isLastComment)
2017-02-16 06:56:11 +03:00
) {
// We're good
} else if (precedingNode) {
// There is content before this comment on the same line, but
// none after it, so prefer a trailing comment of the previous node.
addTrailingComment(precedingNode, comment);
} else if (followingNode) {
addLeadingComment(followingNode, comment);
} else if (enclosingNode) {
addDanglingComment(enclosingNode, comment);
2017-01-28 18:50:22 +03:00
} else {
2017-02-05 05:53:13 +03:00
// There are no nodes, let's attach it to the root of the ast
/* istanbul ignore next */
2017-02-05 05:53:13 +03:00
addDanglingComment(ast, comment);
}
} else {
if (
pluginHandleRemainingComment(comment, text, options, ast, isLastComment)
) {
// We're good
} else if (precedingNode && followingNode) {
// Otherwise, text exists both before and after the comment on
// the same line. If there is both a preceding and following
// node, use a tie-breaking algorithm to determine if it should
// be attached to the next or previous node. In the last case,
// simply attach the right node;
const tieCount = tiesToBreak.length;
if (tieCount > 0) {
const lastTie = tiesToBreak[tieCount - 1];
if (lastTie.followingNode !== comment.followingNode) {
breakTies(tiesToBreak, text, options);
}
}
tiesToBreak.push(comment);
2017-01-28 18:50:22 +03:00
} else if (precedingNode) {
addTrailingComment(precedingNode, comment);
2017-01-28 18:50:22 +03:00
} else if (followingNode) {
addLeadingComment(followingNode, comment);
} else if (enclosingNode) {
addDanglingComment(enclosingNode, comment);
} else {
2017-02-05 05:53:13 +03:00
// There are no nodes, let's attach it to the root of the ast
/* istanbul ignore next */
2017-02-05 05:53:13 +03:00
addDanglingComment(ast, comment);
}
}
});
breakTies(tiesToBreak, text, options);
comments.forEach(comment => {
// These node references were useful for breaking ties, but we
// don't need them anymore, and they create cycles in the AST that
// may lead to infinite recursion if we don't delete them here.
delete comment.precedingNode;
delete comment.enclosingNode;
delete comment.followingNode;
});
2017-01-20 21:12:37 +03:00
}
function breakTies(tiesToBreak, text, options) {
const tieCount = tiesToBreak.length;
if (tieCount === 0) {
return;
}
const { precedingNode, followingNode } = tiesToBreak[0];
let gapEndPos = options.locStart(followingNode);
// Iterate backwards through tiesToBreak, examining the gaps
// between the tied comments. In order to qualify as leading, a
// comment must be separated from followingNode by an unbroken series of
// gaps (or other comments). Gaps should only contain whitespace or open
// parentheses.
let indexOfFirstLeadingComment;
2017-01-13 23:03:53 +03:00
for (
indexOfFirstLeadingComment = tieCount;
2017-01-13 23:03:53 +03:00
indexOfFirstLeadingComment > 0;
--indexOfFirstLeadingComment
) {
const comment = tiesToBreak[indexOfFirstLeadingComment - 1];
assert.strictEqual(comment.precedingNode, precedingNode);
assert.strictEqual(comment.followingNode, followingNode);
const gap = text.slice(options.locEnd(comment), gapEndPos);
if (/^[\s(]*$/.test(gap)) {
gapEndPos = options.locStart(comment);
} else {
// The gap string contained something other than whitespace or open
// parentheses.
break;
}
}
tiesToBreak.forEach((comment, i) => {
if (i < indexOfFirstLeadingComment) {
addTrailingComment(precedingNode, comment);
} else {
addLeadingComment(followingNode, comment);
}
});
tiesToBreak.length = 0;
}
function printComment(commentPath, options) {
const comment = commentPath.getValue();
comment.printed = true;
return options.printer.printComment(commentPath, options);
}
function findExpressionIndexForComment(quasis, comment, options) {
const startPos = options.locStart(comment) - 1;
for (let i = 1; i < quasis.length; ++i) {
if (startPos < getQuasiRange(quasis[i]).start) {
return i - 1;
}
}
// We haven't found it, it probably means that some of the locations are off.
// Let's just return the first one.
/* istanbul ignore next */
return 0;
}
function getQuasiRange(expr) {
if (expr.start !== undefined) {
2018-12-27 16:05:19 +03:00
// Babel
2017-02-23 20:57:51 +03:00
return { start: expr.start, end: expr.end };
}
// Flow
2017-02-23 20:57:51 +03:00
return { start: expr.range[0], end: expr.range[1] };
}
function printLeadingComment(commentPath, print, options) {
const comment = commentPath.getValue();
const contents = printComment(commentPath, options);
if (!contents) {
return "";
}
const isBlock =
options.printer.isBlockComment && options.printer.isBlockComment(comment);
// Leading block comments should see if they need to stay on the
// same line or not.
2017-01-28 18:50:22 +03:00
if (isBlock) {
return concat([
contents,
hasNewline(options.originalText, options.locEnd(comment)) ? hardline : " "
]);
}
return concat([contents, hardline]);
}
function printTrailingComment(commentPath, print, options) {
const comment = commentPath.getValue();
const contents = printComment(commentPath, options);
if (!contents) {
return "";
}
const isBlock =
options.printer.isBlockComment && options.printer.isBlockComment(comment);
// We don't want the line to break
// when the parentParentNode is a ClassDeclaration/-Expression
// And the parentNode is in the superClass property
const parentNode = commentPath.getNode(1);
const parentParentNode = commentPath.getNode(2);
const isParentSuperClass =
parentParentNode &&
(parentParentNode.type === "ClassDeclaration" ||
parentParentNode.type === "ClassExpression") &&
parentParentNode.superClass === parentNode;
if (
hasNewline(options.originalText, options.locStart(comment), {
2017-01-28 18:50:22 +03:00
backwards: true
})
) {
// This allows comments at the end of nested structures:
// {
// x: 1,
// y: 2
// // A comment
// }
// Those kinds of comments are almost always leading comments, but
// here it doesn't go "outside" the block and turns it into a
// trailing comment for `2`. We can simulate the above by checking
// if this a comment on its own line; normal trailing comments are
// always at the end of another expression.
const isLineBeforeEmpty = isPreviousLineEmpty(
options.originalText,
comment,
options.locStart
);
2017-02-16 06:56:11 +03:00
return lineSuffix(
concat([hardline, isLineBeforeEmpty ? hardline : "", contents])
);
} else if (isBlock || isParentSuperClass) {
// Trailing block comments never need a newline
return concat([" ", contents]);
}
return concat([
lineSuffix(concat([" ", contents])),
!isBlock ? breakParent : ""
]);
}
function printDanglingComments(path, options, sameIndent, filter) {
const parts = [];
const node = path.getValue();
2017-01-28 18:50:22 +03:00
if (!node || !node.comments) {
return "";
}
path.each(commentPath => {
const comment = commentPath.getValue();
if (
comment &&
!comment.leading &&
!comment.trailing &&
(!filter || filter(comment))
) {
parts.push(printComment(commentPath, options));
}
}, "comments");
if (parts.length === 0) {
return "";
}
if (sameIndent) {
return join(hardline, parts);
}
return indent(concat([hardline, join(hardline, parts)]));
}
Add `cursorOffset` option for cursor translation (#1637) * Add `formatWithCursor` API with `cursorOffset` option This addresses https://github.com/prettier/prettier/issues/93 by adding a new option, `cursorOffset`, that tells prettier to determine the location of the cursor after the code has been formatted. This is accessible through the API via a new function, `formatWithCursor`, which returns a `{formatted: string, cursorOffset: ?number}`. Here's a usage example: ```js require("prettier").formatWithCursor(" 1", { cursorOffset: 2 }); // -> { formatted: '1;\n', cursorOffset: 1 } ``` * Add `--cursor-offset` CLI option It will print out the offset instead of the formatted output. This makes it easier to test. For example: echo ' 1' | prettier --stdin --cursor-offset 2 # prints 1 * Add basic test of cursor translation * Document `cursorOffset` option and `formatWithCursor()` * Print translated cursor offset to stderr when --cursor-offset is given This lets us continue to print the formatted code, while also communicating the updated cursor position. See https://github.com/prettier/prettier/pull/1637#discussion_r119735496 * doc-print cursor placeholder in comments.printComments() See https://github.com/prettier/prettier/pull/1637#discussion_r119735149 * Compare array index to -1 instead of >= 0 to determine element presence See https://github.com/prettier/prettier/pull/1637#discussion_r119736623 * Return {formatted, cursor} from printDocToString() instead of mutating options See https://github.com/prettier/prettier/pull/1637#discussion_r119737354
2017-06-02 01:52:29 +03:00
function prependCursorPlaceholder(path, options, printed) {
if (path.getNode() === options.cursorNode && path.getValue()) {
return concat([cursor, printed, cursor]);
Add `cursorOffset` option for cursor translation (#1637) * Add `formatWithCursor` API with `cursorOffset` option This addresses https://github.com/prettier/prettier/issues/93 by adding a new option, `cursorOffset`, that tells prettier to determine the location of the cursor after the code has been formatted. This is accessible through the API via a new function, `formatWithCursor`, which returns a `{formatted: string, cursorOffset: ?number}`. Here's a usage example: ```js require("prettier").formatWithCursor(" 1", { cursorOffset: 2 }); // -> { formatted: '1;\n', cursorOffset: 1 } ``` * Add `--cursor-offset` CLI option It will print out the offset instead of the formatted output. This makes it easier to test. For example: echo ' 1' | prettier --stdin --cursor-offset 2 # prints 1 * Add basic test of cursor translation * Document `cursorOffset` option and `formatWithCursor()` * Print translated cursor offset to stderr when --cursor-offset is given This lets us continue to print the formatted code, while also communicating the updated cursor position. See https://github.com/prettier/prettier/pull/1637#discussion_r119735496 * doc-print cursor placeholder in comments.printComments() See https://github.com/prettier/prettier/pull/1637#discussion_r119735149 * Compare array index to -1 instead of >= 0 to determine element presence See https://github.com/prettier/prettier/pull/1637#discussion_r119736623 * Return {formatted, cursor} from printDocToString() instead of mutating options See https://github.com/prettier/prettier/pull/1637#discussion_r119737354
2017-06-02 01:52:29 +03:00
}
return printed;
}
function printComments(path, print, options, needsSemi) {
const value = path.getValue();
const printed = print(path);
const comments = value && value.comments;
if (!comments || comments.length === 0) {
Add `cursorOffset` option for cursor translation (#1637) * Add `formatWithCursor` API with `cursorOffset` option This addresses https://github.com/prettier/prettier/issues/93 by adding a new option, `cursorOffset`, that tells prettier to determine the location of the cursor after the code has been formatted. This is accessible through the API via a new function, `formatWithCursor`, which returns a `{formatted: string, cursorOffset: ?number}`. Here's a usage example: ```js require("prettier").formatWithCursor(" 1", { cursorOffset: 2 }); // -> { formatted: '1;\n', cursorOffset: 1 } ``` * Add `--cursor-offset` CLI option It will print out the offset instead of the formatted output. This makes it easier to test. For example: echo ' 1' | prettier --stdin --cursor-offset 2 # prints 1 * Add basic test of cursor translation * Document `cursorOffset` option and `formatWithCursor()` * Print translated cursor offset to stderr when --cursor-offset is given This lets us continue to print the formatted code, while also communicating the updated cursor position. See https://github.com/prettier/prettier/pull/1637#discussion_r119735496 * doc-print cursor placeholder in comments.printComments() See https://github.com/prettier/prettier/pull/1637#discussion_r119735149 * Compare array index to -1 instead of >= 0 to determine element presence See https://github.com/prettier/prettier/pull/1637#discussion_r119736623 * Return {formatted, cursor} from printDocToString() instead of mutating options See https://github.com/prettier/prettier/pull/1637#discussion_r119737354
2017-06-02 01:52:29 +03:00
return prependCursorPlaceholder(path, options, printed);
}
const leadingParts = [];
const trailingParts = [needsSemi ? ";" : "", printed];
2017-01-13 23:03:53 +03:00
path.each(commentPath => {
const comment = commentPath.getValue();
const { leading, trailing } = comment;
2017-01-13 23:03:53 +03:00
if (leading) {
const contents = printLeadingComment(commentPath, print, options);
if (!contents) {
return;
}
leadingParts.push(contents);
2017-01-13 23:03:53 +03:00
const text = options.originalText;
const index = skipNewline(text, options.locEnd(comment));
if (index !== false && hasNewline(text, index)) {
leadingParts.push(hardline);
}
} else if (trailing) {
trailingParts.push(printTrailingComment(commentPath, print, options));
}
}, "comments");
Add `cursorOffset` option for cursor translation (#1637) * Add `formatWithCursor` API with `cursorOffset` option This addresses https://github.com/prettier/prettier/issues/93 by adding a new option, `cursorOffset`, that tells prettier to determine the location of the cursor after the code has been formatted. This is accessible through the API via a new function, `formatWithCursor`, which returns a `{formatted: string, cursorOffset: ?number}`. Here's a usage example: ```js require("prettier").formatWithCursor(" 1", { cursorOffset: 2 }); // -> { formatted: '1;\n', cursorOffset: 1 } ``` * Add `--cursor-offset` CLI option It will print out the offset instead of the formatted output. This makes it easier to test. For example: echo ' 1' | prettier --stdin --cursor-offset 2 # prints 1 * Add basic test of cursor translation * Document `cursorOffset` option and `formatWithCursor()` * Print translated cursor offset to stderr when --cursor-offset is given This lets us continue to print the formatted code, while also communicating the updated cursor position. See https://github.com/prettier/prettier/pull/1637#discussion_r119735496 * doc-print cursor placeholder in comments.printComments() See https://github.com/prettier/prettier/pull/1637#discussion_r119735149 * Compare array index to -1 instead of >= 0 to determine element presence See https://github.com/prettier/prettier/pull/1637#discussion_r119736623 * Return {formatted, cursor} from printDocToString() instead of mutating options See https://github.com/prettier/prettier/pull/1637#discussion_r119737354
2017-06-02 01:52:29 +03:00
return prependCursorPlaceholder(
path,
options,
concat(leadingParts.concat(trailingParts))
);
}
2017-01-20 21:12:37 +03:00
Find nearest node when formatting range (#1659) * Move range extension code into helper functions * Add findNodeByOffset() helper This was adapted from https://github.com/prettier/prettier/pull/1637/commits/cbc1929c64db558b4e444500bca3d2ce1d550359 * Test extending formatted range to entire node * Fix extending formatted range to entire node * Fix style errors * Add run_file test function This makes it possible to use different options on a per-file basis, which is useful for things like range formatting tests. * Test extending the format range to nearest parseable node This means you can select the range of a `catch` clause, attempt to format it, and have the `try` formatted as well, rather than throwing an error. * Fix extending the format range to nearest parseable node This means you can select the range of a `catch` clause, attempt to format it, and have the `try` formatted as well, rather than throwing an error. * Test that external indentation is left alone when formatting a range * Preserve external indentation when formatting a range * Dedupe range formatting traversal callbacks * Simplify range formatting traversal using ast-types See https://github.com/prettier/prettier/pull/1659#issuecomment-302974798 * Make range formatting traversal more efficient There's less unnecessary parsing now. * Fix style errors * Add test where range expanding fails * Fix test where range expanding fails This makes sure that the range contains the entirety of the nodes containing each of the range's endpoints. * Add test for expanding range to beginning of line * Pass test for expanding range to beginning of line This makes it so that indentation before the range is added to the formatted range. * Don't parse/stringify AST to detect pre-range indentation See https://github.com/prettier/prettier/pull/1659#discussion_r117790671 * When formatting a range, find closest statement rather than parsing The `isStatement` implementation came from `docs/prettier.min.js`. See https://github.com/prettier/prettier/pull/1659#issuecomment-303154770 * Add test for range-formatting a FunctionDeclaration's argument object * Include FunctionDeclaration when searching for nearest node to range-format From the spec, a Program is a series of SourceElements, each of which is either a Statement or a FunctionDeclaration. See https://www.ecma-international.org/ecma-262/5.1/#sec-A.5 * Remove unnecessary try-catch See https://github.com/prettier/prettier/pull/1659#discussion_r117810096 * Add tests with multiple statements See https://github.com/prettier/prettier/pull/1659#discussion_r117810753 * Remove unnecessary arguments from findNodeByOffset() * Contract format range to ensure it starts/ends on nodes * Specify test ranges in the fixtures See https://github.com/prettier/prettier/pull/1659#discussion_r117811186 * Remove unnecessary comments from range fixtures * Remove run_file test function It's no longer used. This essentially reverts 8241216e68f2e0da997a4f558b03658d642c89a2 * Update range formatting docs Clarify that the range expands to the nearest statement, and not to the end of the line. * Don't overwrite test options when detecting range Now that multiple files share the same object again, we shouldn't be re-assigning to it. * Reuse already-read fixtures for AST_COMPARE=1 tests * Remove `run_file` global from test eslintrc * Undo package.json churn `yarn` reformatted it before, but the whitespace visually sets off the comment, so let's put it back how it was before. See https://github.com/prettier/prettier/pull/1659#discussion_r117864655 * Remove misleading comments from isSourceElement See https://github.com/prettier/prettier/pull/1659#discussion_r117865196 * Loop backwards through string instead of reversing it See https://github.com/prettier/prettier/pull/1659#discussion_r117865759 * Don't recompute indent string when formatting range See https://github.com/prettier/prettier/pull/1659#discussion_r117867268 * Rename findNodeByOffset to findNodeAtOffset "Find x by y" is the common usage for finding an `x` by a key `y`. However, since "by" has positional meaning, let's use "at" instead. See https://github.com/prettier/prettier/pull/1659#discussion_r117865121 * Always trimRight() in formatRange and explain why See https://github.com/prettier/prettier/pull/1659#discussion_r117864635 * Test formatting a range that crosses AST levels See https://github.com/prettier/prettier/pull/1659#issuecomment-303243688 * Fix formatting a range that crosses AST levels See https://github.com/prettier/prettier/pull/1659#issuecomment-303243688 * Remove unnecessary try-catch See https://github.com/prettier/prettier/pull/1659/files/e52db5e9f9ec4af599f658da03739e206dd4578c#r117878763 * Add test demonstrating range formatting indent detection * Detect alignment from text on line before range, but don't reformat it This avoids reformatting non-indentation that happens to precede the range on the same line, while still correctly indenting the range based on it. See https://github.com/prettier/prettier/pull/1659#discussion_r117881430
2017-05-23 17:43:58 +03:00
module.exports = {
attach,
printComments,
printDanglingComments,
getSortedChildNodes
};