From eaa40aa266a6ef133d4ba2735e2a844733f6fa52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arnaud=20Barr=C3=A9?= Date: Thu, 5 Jun 2025 10:58:30 +0200 Subject: [PATCH] Improve postprocess perf for type cast comments (#17566) Co-authored-by: fisker --- src/language-js/parse/acorn.js | 2 +- src/language-js/parse/babel.js | 2 +- src/language-js/parse/oxc.js | 6 ++- src/language-js/parse/postprocess/index.js | 42 +++++++++++-------- src/language-js/utils/is-type-cast-comment.js | 23 ++++++---- 5 files changed, 47 insertions(+), 28 deletions(-) diff --git a/src/language-js/parse/acorn.js b/src/language-js/parse/acorn.js index 6b194e618..ae6fc50ed 100644 --- a/src/language-js/parse/acorn.js +++ b/src/language-js/parse/acorn.js @@ -80,7 +80,7 @@ function parse(text, options) { throw createParseError(error); } - return postprocess(ast, { text }); + return postprocess(ast, { text, supportTypeCastComments: true }); } export const acorn = createParser(parse); diff --git a/src/language-js/parse/babel.js b/src/language-js/parse/babel.js index fa6d993e5..07823e48d 100644 --- a/src/language-js/parse/babel.js +++ b/src/language-js/parse/babel.js @@ -163,7 +163,7 @@ function createParse({ isExpression = false, optionsCombinations }) { ast = wrapBabelExpression(ast, { text, rootMarker: options.rootMarker }); } - return postprocess(ast, { text }); + return postprocess(ast, { text, supportTypeCastComments: true }); }; } diff --git a/src/language-js/parse/oxc.js b/src/language-js/parse/oxc.js index 79ca53dc7..8d787f770 100644 --- a/src/language-js/parse/oxc.js +++ b/src/language-js/parse/oxc.js @@ -71,7 +71,11 @@ async function parseJs(text, options) { // @ts-expect-error -- expected ast.comments = comments; - return postprocess(ast, { text, parser: "oxc" }); + return postprocess(ast, { + text, + parser: "oxc", + supportTypeCastComments: true, + }); } async function parseTs(text, options) { diff --git a/src/language-js/parse/postprocess/index.js b/src/language-js/parse/postprocess/index.js index b99c33f55..bd3aaa077 100644 --- a/src/language-js/parse/postprocess/index.js +++ b/src/language-js/parse/postprocess/index.js @@ -34,32 +34,32 @@ const isNodeWithRaw = createTypeCheckFunction([ * @param {{ * text: string, * parser?: string, + * supportTypeCastComments?: boolean, * oxcAstType?: string, * }} options */ function postprocess(ast, options) { - const { parser, text } = options; + const { parser, text, supportTypeCastComments } = options; + const { comments } = ast; // `InterpreterDirective` from babel parser and flow parser // Other parsers parse it as comment, babel treat it as comment too // https://github.com/babel/babel/issues/15116 const program = ast.type === "File" ? ast.program : ast; - const { interpreter } = program; - if (interpreter) { - ast.comments.unshift(interpreter); + if (program.interpreter) { + comments.unshift(program.interpreter); delete program.interpreter; } if (parser === "oxc" && options.oxcAstType === "ts" && ast.hashbang) { - const { comments, hashbang } = ast; - comments.unshift(hashbang); - delete program.hashbang; + comments.unshift(ast.hashbang); + delete ast.hashbang; } - if (ast.comments.length > 0) { + if (comments.length > 0) { let followingComment; - for (let i = ast.comments.length - 1; i >= 0; i--) { - const comment = ast.comments[i]; + for (let i = comments.length - 1; i >= 0; i--) { + const comment = comments[i]; if ( followingComment && @@ -69,7 +69,7 @@ function postprocess(ast, options) { isIndentableBlockComment(comment) && isIndentableBlockComment(followingComment) ) { - ast.comments.splice(i + 1, 1); + comments.splice(i + 1, 1); comment.value += "*//*" + followingComment.value; comment.range = [locStart(comment), locEnd(followingComment)]; } @@ -88,6 +88,15 @@ function postprocess(ast, options) { } } + const typeCastCommentsEnds = []; + if (supportTypeCastComments) { + for (const comment of comments) { + if (isTypeCastComment(comment)) { + typeCastCommentsEnds.push(locEnd(comment)); + } + } + } + ast = visitNode(ast, (node) => { switch (node.type) { case "ParenthesizedExpression": { @@ -101,14 +110,13 @@ function postprocess(ast, options) { } // Keep ParenthesizedExpression nodes only if they have Closure-style type cast comments. - const previousComment = ast.comments.findLast( - (comment) => locEnd(comment) <= start, + const previousCommentEnd = typeCastCommentsEnds.findLast( + (end) => end <= start, ); const keepTypeCast = - previousComment && - isTypeCastComment(previousComment) && + previousCommentEnd && // check that there are only white spaces between the comment and the parenthesis - text.slice(locEnd(previousComment), start).trim().length === 0; + text.slice(previousCommentEnd, start).trim().length === 0; if (!keepTypeCast) { expression.extra = { ...expression.extra, parenthesized: true }; @@ -202,7 +210,7 @@ function postprocess(ast, options) { const textWithoutComments = getTextWithoutComments( { originalText: text, - [Symbol.for("comments")]: ast.comments, + [Symbol.for("comments")]: comments, }, idEnd, members[0] ? locStart(members[0]) : locEnd(node), diff --git a/src/language-js/utils/is-type-cast-comment.js b/src/language-js/utils/is-type-cast-comment.js index 89ea3f4f8..0a9588fba 100644 --- a/src/language-js/utils/is-type-cast-comment.js +++ b/src/language-js/utils/is-type-cast-comment.js @@ -4,19 +4,26 @@ import isBlockComment from "./is-block-comment.js"; * @import {Comment} from "../types/estree.js" */ +const cache = new WeakMap(); + /** * @param {Comment} comment * @returns {boolean} */ function isTypeCastComment(comment) { - return ( - isBlockComment(comment) && - comment.value[0] === "*" && - // TypeScript expects the type to be enclosed in curly brackets, however - // Closure Compiler accepts types in parens and even without any delimiters at all. - // That's why we just search for "@type" and "@satisfies". - /@(?:type|satisfies)\b/u.test(comment.value) - ); + if (!cache.has(comment)) { + cache.set( + comment, + isBlockComment(comment) && + comment.value[0] === "*" && + // TypeScript expects the type to be enclosed in curly brackets, however + // Closure Compiler accepts types in parens and even without any delimiters at all. + // That's why we just search for "@type" and "@satisfies". + /@(?:type|satisfies)\b/u.test(comment.value), + ); + } + + return cache.get(comment); } export default isTypeCastComment;