diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 000000000..62fda7a17 --- /dev/null +++ b/src/constants.js @@ -0,0 +1 @@ +export const commentsPropertyInOptions = Symbol.for("comments"); diff --git a/src/language-js/comments/handle-comments.js b/src/language-js/comments/handle-comments.js index 59773594b..d22fed80a 100644 --- a/src/language-js/comments/handle-comments.js +++ b/src/language-js/comments/handle-comments.js @@ -13,7 +13,6 @@ import { getCallArguments } from "../utilities/call-arguments.js"; import { isInsideCallOrNewExpressionParentheses } from "../utilities/call-or-new-expression-parentheses.js"; import { createTypeCheckFunction } from "../utilities/create-type-check-function.js"; import { getFunctionParameters } from "../utilities/function-parameters.js"; -import getTextWithoutComments from "../utilities/get-text-without-comments.js"; import { isBlockComment } from "../utilities/is-block-comment.js"; import { isLineComment } from "../utilities/is-line-comment.js"; import { isMethod } from "../utilities/is-method.js"; @@ -29,6 +28,7 @@ import { isMemberExpression, isUnionType, } from "../utilities/node-types.js"; +import { stripComments } from "../utilities/strip-comments.js"; /** @import {Node, Comment, NodeMap} from "../types/estree.js"; @@ -235,7 +235,7 @@ function handleIfStatementComments({ ) { const start = locStart(enclosingNode); const end = locEnd(enclosingNode); - const ifStatementTxt = getTextWithoutComments(options, start, end); + const ifStatementTxt = stripComments(options).slice(start, end); let elseTokenIndex = getNextNonSpaceNonCommentCharacterIndex( ifStatementTxt, locEnd(enclosingNode.consequent) - start, @@ -646,17 +646,10 @@ function isInArgumentOrParameterParentheses(node, comment, options) { return false; } - const nodeText = getTextWithoutComments(options, nodeStart, nodeEnd); - + const text = stripComments(options); return ( - nodeText - .slice(0, locStart(comment) - nodeStart) - .trimEnd() - .endsWith("(") && - nodeText - .slice(locEnd(comment) - nodeStart) - .trimStart() - .startsWith(")") + text.slice(0, locStart(comment)).trimEnd().endsWith("(") && + text.slice(locEnd(comment)).trimStart().startsWith(")") ); } @@ -1022,13 +1015,7 @@ function handleIgnoreComments({ comment, enclosingNode, followingNode }) { @param {Comment} comment */ function isBeforeMappedTypeOpeningBracket(node, comment, options) { - const start = locStart(node); - const textAfter = getTextWithoutComments( - options, - start + 1, - locStart(node.key), - ); - const bracketIndex = start + 1 + textAfter.indexOf("["); + const bracketIndex = stripComments(options).indexOf("[", locStart(node)); return locEnd(comment) < bracketIndex; } diff --git a/src/language-js/loc.js b/src/language-js/loc.js index 47e8b73e3..00ed80959 100644 --- a/src/language-js/loc.js +++ b/src/language-js/loc.js @@ -50,11 +50,9 @@ function locEnd(node) { node.type === "ExportDefaultDeclaration" || node.type === "ExportNamedDeclaration" || node.type === "ExportAllDeclaration") && - // @ts-expect-error -- Added in postprocess.js - node.__end + node.__contentEnd ) { - // @ts-expect-error -- safe - return node.__end; + return node.__contentEnd; } return locEndWithFullText(node); diff --git a/src/language-js/massage-ast/index.js b/src/language-js/massage-ast/index.js index 4293b5064..0ba53128d 100644 --- a/src/language-js/massage-ast/index.js +++ b/src/language-js/massage-ast/index.js @@ -29,7 +29,7 @@ const ignoredProperties = new Set([ "trailingComma", "docblock", // Prettier - "__end", + "__contentEnd", ]); const removeTemplateElementsValue = (node) => { diff --git a/src/language-js/parse/postprocess/index.js b/src/language-js/parse/postprocess/index.js index 80136349d..8d519b66a 100644 --- a/src/language-js/parse/postprocess/index.js +++ b/src/language-js/parse/postprocess/index.js @@ -1,14 +1,19 @@ import * as assert from "#universal/assert"; +import { commentsPropertyInOptions } from "../../../constants.js"; import { locEnd, locEndWithFullText, locStart } from "../../loc.js"; import { createTypeCheckFunction } from "../../utilities/create-type-check-function.js"; import { getRaw } from "../../utilities/get-raw.js"; -import getTextWithoutComments from "../../utilities/get-text-without-comments.js"; import { isBlockComment } from "../../utilities/is-block-comment.js"; import { isLineComment } from "../../utilities/is-line-comment.js"; import { isTypeCastComment } from "../../utilities/is-type-cast-comment.js"; +import { stripComments } from "../../utilities/strip-comments.js"; import { mergeNestledJsdocComments } from "./merge-nestled-jsdoc-comments.js"; import visitNode from "./visit-node.js"; +/** +@import {Node, Comment} from "../../types/estree.js" +*/ + const isNodeWithRaw = createTypeCheckFunction([ // Babel "RegExpLiteral", @@ -172,7 +177,7 @@ function postprocess(ast, options) { case "ExportDefaultDeclaration": case "ExportNamedDeclaration": case "ExportAllDeclaration": - addEnd(node, { comments, text }); + addNodeContentEnd(node, { comments, text }); break; } }, @@ -261,16 +266,26 @@ function assertRaw(node, text) { assert.equal(raw, text.slice(locStart(node), locEnd(node))); } -function addEnd(node, { comments, text: originalText }) { - const start = locStart(node); - const end = locEndWithFullText(node); - const text = getTextWithoutComments( - { [Symbol.for("comments")]: comments, originalText }, - start, - end, - ); - const cleaned = (text.at(-1) === ";" ? text.slice(0, -1) : text).trimEnd(); - node.__end = end - (text.length - cleaned.length); +/** +@param {Node} node +@param {{comments: Comment[], text: string}} param1 +*/ +function addNodeContentEnd(node, { comments, text: originalText }) { + let end = locEndWithFullText(node); + if (originalText[end - 1] !== ";") { + return; + } + + const text = stripComments({ + [commentsPropertyInOptions]: comments, + originalText, + }); + + end -= 1; + const textBeforeSemicolon = text.slice(locStart(node), end); + const cleaned = textBeforeSemicolon.trimEnd(); + + node.__contentEnd = end - (textBeforeSemicolon.length - cleaned.length); } export default postprocess; diff --git a/src/language-js/print/ignored.js b/src/language-js/print/ignored.js index 329d49e88..404c16f8f 100644 --- a/src/language-js/print/ignored.js +++ b/src/language-js/print/ignored.js @@ -16,7 +16,7 @@ function printIgnored(path, options /* , print*/) { node.type === "ExportDefaultDeclaration" || node.type === "ExportNamedDeclaration" || node.type === "ExportAllDeclaration") && - node.__end)) + node.__contentEnd)) ) { text += ";"; } else if (shouldExpressionStatementPrintLeadingSemicolon(path, options)) { diff --git a/src/language-js/print/mapped-type.js b/src/language-js/print/mapped-type.js index ccdeed940..94e5ac65f 100644 --- a/src/language-js/print/mapped-type.js +++ b/src/language-js/print/mapped-type.js @@ -11,8 +11,8 @@ import hasNewline from "../../utilities/has-newline.js"; import hasNewlineInRange from "../../utilities/has-newline-in-range.js"; import { locEnd, locStart } from "../loc.js"; import { CommentCheckFlags, getComments } from "../utilities/comments.js"; -import getTextWithoutComments from "../utilities/get-text-without-comments.js"; import { isLineComment } from "../utilities/is-line-comment.js"; +import { stripComments } from "../utilities/strip-comments.js"; import { printClassMemberSemicolon } from "./class.js"; /** @@ -70,13 +70,11 @@ function printTypeScriptMappedType(path, options, print) { // Break after `{` like `printObject` let shouldBreak = false; if (options.objectWrap === "preserve") { - const start = locStart(node); - const textAfter = getTextWithoutComments( - options, - start + 1, - locStart(node.key), - ); - const nextTokenIndex = start + 1 + textAfter.search(/\S/); + const text = stripComments(options); + // Skip `{` + const start = locStart(node) + 1; + const textAfter = text.slice(start); + const nextTokenIndex = start + textAfter.search(/\S/); if (hasNewlineInRange(options.originalText, start, nextTokenIndex)) { shouldBreak = true; } diff --git a/src/language-js/print/module.js b/src/language-js/print/module.js index e9e614dda..c7c68e3f6 100644 --- a/src/language-js/print/module.js +++ b/src/language-js/print/module.js @@ -13,10 +13,10 @@ import UnexpectedNodeError from "../../utilities/unexpected-node-error.js"; import { locEnd, locStart } from "../loc.js"; import { CommentCheckFlags, hasComment } from "../utilities/comments.js"; import { createTypeCheckFunction } from "../utilities/create-type-check-function.js"; -import getTextWithoutComments from "../utilities/get-text-without-comments.js"; import { isShorthandSpecifier } from "../utilities/is-shorthand-specifier.js"; import { needsHardlineAfterDanglingComment } from "../utilities/needs-hardline-after-dangling-comment.js"; import { isStringLiteral } from "../utilities/node-types.js"; +import { stripComments } from "../utilities/strip-comments.js"; import { printDecoratorsBeforeExport } from "./decorators.js"; import { printDeclareToken, printTrailingComma } from "./miscellaneous.js"; import { printObject } from "./object.js"; @@ -239,8 +239,7 @@ function shouldPrintSpecifiers(node, options) { return true; } - const text = getTextWithoutComments( - options, + const text = stripComments(options).slice( locStart(node), locStart(node.source), ); @@ -249,11 +248,12 @@ function shouldPrintSpecifiers(node, options) { } function getImportAttributesKeyword(node, options) { - const textBetweenSourceAndAttributes = getTextWithoutComments( - options, - locEnd(node.source), - node.attributes?.[0] ? locStart(node.attributes[0]) : locEnd(node), - ).trimStart(); + const textBetweenSourceAndAttributes = stripComments(options) + .slice( + locEnd(node.source), + node.attributes?.[0] ? locStart(node.attributes[0]) : locEnd(node), + ) + .trimStart(); if (textBetweenSourceAndAttributes.startsWith("assert")) { return "assert"; diff --git a/src/language-js/print/object.js b/src/language-js/print/object.js index a2c079a58..9a7e57dd8 100644 --- a/src/language-js/print/object.js +++ b/src/language-js/print/object.js @@ -17,9 +17,9 @@ import { hasComment, } from "../utilities/comments.js"; import { createTypeCheckFunction } from "../utilities/create-type-check-function.js"; -import getTextWithoutComments from "../utilities/get-text-without-comments.js"; import { isNextLineEmpty } from "../utilities/is-next-line-empty.js"; import { isObjectType } from "../utilities/node-types.js"; +import { stripComments } from "../utilities/strip-comments.js"; import { shouldHugTheOnlyParameter } from "./function-parameters.js"; import { printDanglingCommentsInList, @@ -196,17 +196,18 @@ function printObject(path, options, print) { function hasNewLineAfterOpeningBrace(node, firstProperty, options) { const text = options.originalText; - let openingBraceIndex = locStart(node); const firstPropertyStart = locStart(firstProperty); + let openingBraceIndex = locStart(node); if (isPrintingImportAttributes(node)) { - const start = locStart(node); - const textBeforeAttributes = getTextWithoutComments( - options, - start, + openingBraceIndex = stripComments(options).lastIndexOf( + "{", firstPropertyStart, ); - openingBraceIndex = start + textBeforeAttributes.lastIndexOf("{"); + /* c8 ignore next 3 */ + if (process.env.NODE_ENV !== "production") { + assert.ok(openingBraceIndex !== -1); + } } /* c8 ignore next 3 */ diff --git a/src/language-js/types/estree.d.ts b/src/language-js/types/estree.d.ts index 9bdc79c87..b8c399f0d 100644 --- a/src/language-js/types/estree.d.ts +++ b/src/language-js/types/estree.d.ts @@ -12,6 +12,7 @@ type PrettierNodeAdditionalProperties = { }; comments?: Comment[]; prettierIgnore?: boolean; + __contentEnd?: number; }; type PrettierCommentAdditionalProperties = { diff --git a/src/language-js/utilities/call-or-new-expression-parentheses.js b/src/language-js/utilities/call-or-new-expression-parentheses.js index 41fd24774..ee2b1dad2 100644 --- a/src/language-js/utilities/call-or-new-expression-parentheses.js +++ b/src/language-js/utilities/call-or-new-expression-parentheses.js @@ -1,5 +1,5 @@ import { locEnd, locStart } from "../loc.js"; -import getTextWithoutComments from "./get-text-without-comments.js"; +import { stripComments } from "./strip-comments.js"; /** @import {Node, Comment, NodeMap} from "../types/estree.js" @@ -52,11 +52,11 @@ function getCallOrNewExpressionOpeningParenthesisIndex( return; } + const text = stripComments(options); const start = locEnd( callOrNewExpression.typeArguments ?? callOrNewExpression.callee, ); - const text = getTextWithoutComments(options, start, closingParenthesisIndex); - const openingParenthesisIndex = text.indexOf("("); + const openingParenthesisIndex = text.indexOf("(", start); /* c8 ignore next 6 */ if (openingParenthesisIndex === -1) { @@ -68,7 +68,7 @@ function getCallOrNewExpressionOpeningParenthesisIndex( return; } - return start + openingParenthesisIndex; + return openingParenthesisIndex; } /** diff --git a/src/language-js/utilities/get-text-without-comments.js b/src/language-js/utilities/get-text-without-comments.js deleted file mode 100644 index e6109685d..000000000 --- a/src/language-js/utilities/get-text-without-comments.js +++ /dev/null @@ -1,31 +0,0 @@ -import replaceNonLineBreaksWithSpace from "../../utilities/replace-non-line-breaks-with-space.js"; -import { locEnd, locStart } from "../loc.js"; - -function getTextWithoutComments(options, start, end) { - let text = options.originalText.slice(start, end); - - for (const comment of options[Symbol.for("comments")]) { - const commentStart = locStart(comment); - // Comments are sorted, we can escape if the comment is after the range - if (commentStart >= end) { - break; - } - - const commentEnd = locEnd(comment); - if (commentEnd <= start) { - continue; - } - - const startIndex = Math.max(commentStart - start, 0); - const endIndex = commentEnd - start; - - text = - text.slice(0, startIndex) + - replaceNonLineBreaksWithSpace(text.slice(startIndex, endIndex)) + - text.slice(endIndex); - } - - return text; -} - -export default getTextWithoutComments; diff --git a/src/language-js/utilities/strip-comments.js b/src/language-js/utilities/strip-comments.js new file mode 100644 index 000000000..94d0ac60e --- /dev/null +++ b/src/language-js/utilities/strip-comments.js @@ -0,0 +1,48 @@ +import { commentsPropertyInOptions } from "../../constants.js"; +import replaceNonLineBreaksWithSpace from "../../utilities/replace-non-line-breaks-with-space.js"; +import { locEnd, locStart } from "../loc.js"; + +/** +@import {Node, Comment} from "../types/estree.js" +@typedef {Comment[]} Comments +@typedef {{ + originalText: string, + [commentsPropertyInOptions]: Comments +}} Options +*/ + +/** +@param {string} text +@param {Comments} comments +@returns {string} +*/ +function stripCommentsFromText(text, comments) { + for (const comment of comments) { + const start = locStart(comment); + const end = locEnd(comment); + + text = + text.slice(0, start) + + replaceNonLineBreaksWithSpace(text.slice(start, end)) + + text.slice(end); + } + + return text; +} + +/** @type {WeakMap} */ +const cache = new WeakMap(); + +/** +@param {Options} options +@returns {string} +*/ +function stripComments(options) { + const comments = options[commentsPropertyInOptions]; + if (!cache.has(comments)) { + cache.set(comments, stripCommentsFromText(options.originalText, comments)); + } + return cache.get(comments); +} + +export { stripComments }; diff --git a/src/main/ast-to-doc.js b/src/main/ast-to-doc.js index 7b77f6e10..b206d1852 100644 --- a/src/main/ast-to-doc.js +++ b/src/main/ast-to-doc.js @@ -1,4 +1,5 @@ import AstPath from "../common/ast-path.js"; +import { commentsPropertyInOptions } from "../constants.js"; import { cursor, inheritLabel } from "../document/index.js"; import isNonEmptyArray from "../utilities/is-non-empty-array.js"; import isObject from "../utilities/is-object.js"; @@ -144,7 +145,7 @@ function callPluginPrintFunction(path, options, printPath, args, embeds) { async function prepareToPrint(ast, options) { const comments = ast.comments ?? []; - options[Symbol.for("comments")] = comments; + options[commentsPropertyInOptions] = comments; // For JS printer to ignore attached comments options[Symbol.for("printedComments")] = new Set(); diff --git a/src/main/comments/print.js b/src/main/comments/print.js index 8aca909bb..d09fa3beb 100644 --- a/src/main/comments/print.js +++ b/src/main/comments/print.js @@ -1,3 +1,4 @@ +import { commentsPropertyInOptions } from "../../constants.js"; import { breakParent, hardline, @@ -255,7 +256,7 @@ function printComments(path, doc, options, printOptions) { function ensureAllCommentsPrinted(options) { const { - [Symbol.for("comments")]: comments, + [commentsPropertyInOptions]: comments, [Symbol.for("printedComments")]: printedComments, } = options; diff --git a/src/main/core.js b/src/main/core.js index dad5a071f..5f51b86bf 100644 --- a/src/main/core.js +++ b/src/main/core.js @@ -5,6 +5,7 @@ import { guessEndOfLine, normalizeEndOfLine, } from "../common/end-of-line.js"; +import { commentsPropertyInOptions } from "../constants.js"; import { addAlignmentToDoc, hardline, @@ -69,7 +70,7 @@ async function coreFormat(originalText, opts, addAlignmentSize = 0) { trimmed + convertEndOfLineOptionToCharacter(opts.endOfLine); } - const comments = opts[Symbol.for("comments")]; + const comments = opts[commentsPropertyInOptions]; if (opts.cursorOffset >= 0) { // Roughly, our logic for preserving the user's cursor position is as diff --git a/src/main/print-ignored.js b/src/main/print-ignored.js index 27d8fcb1d..9de0d1f34 100644 --- a/src/main/print-ignored.js +++ b/src/main/print-ignored.js @@ -1,7 +1,9 @@ +import { commentsPropertyInOptions } from "../constants.js"; + function printIgnored(path, options, printPath, args) { const { originalText, - [Symbol.for("comments")]: comments, + [commentsPropertyInOptions]: comments, locStart, locEnd, [Symbol.for("printedComments")]: printedComments, diff --git a/tests/format/js/ignore/__snapshots__/format.test.js.snap b/tests/format/js/ignore/__snapshots__/format.test.js.snap index 6cd1bcac0..56a97f8db 100644 --- a/tests/format/js/ignore/__snapshots__/format.test.js.snap +++ b/tests/format/js/ignore/__snapshots__/format.test.js.snap @@ -405,7 +405,7 @@ function HelloWorld(x) { // eslint-disable-next-line x.a | x.b - ).call(null); + ).call(null) } ================================================================================ diff --git a/tests/format/markdown/markdown/__snapshots__/format.test.js.snap b/tests/format/markdown/markdown/__snapshots__/format.test.js.snap index 72d94e311..d1d62bbb7 100644 --- a/tests/format/markdown/markdown/__snapshots__/format.test.js.snap +++ b/tests/format/markdown/markdown/__snapshots__/format.test.js.snap @@ -1531,7 +1531,7 @@ matrix( 1, 0, 0, 0, 1, 0, 0, 0, 1 -); +) \`\`\` will be transformed to: @@ -1544,7 +1544,7 @@ matrix( 1, 0, 0, 0, 1, 0, 0, 0, 1 -); +) \`\`\` ## Options @@ -3503,7 +3503,7 @@ matrix( 1, 0, 0, 0, 1, 0, 0, 0, 1 -); +) \`\`\` will be transformed to: @@ -3516,7 +3516,7 @@ matrix( 1, 0, 0, 0, 1, 0, 0, 0, 1 -); +) \`\`\` ## Options