From 706aa4ef90dbf3804595b10adffad6b75228fea6 Mon Sep 17 00:00:00 2001 From: fisker Cheung Date: Mon, 1 Dec 2025 19:36:05 +0800 Subject: [PATCH] Switch js parse postprocess to `onEnter` (#18382) --- src/language-css/parse/parse-value.js | 3 +- src/language-css/parse/utilities.js | 6 +- src/language-css/parser-postcss.js | 3 +- src/language-js/parse/postprocess/index.js | 83 ++++++++++--------- .../parse/postprocess/visit-node.js | 17 +++- src/language-js/utilities/index.js | 3 +- src/main/ast-to-doc.js | 4 +- 7 files changed, 70 insertions(+), 49 deletions(-) diff --git a/src/language-css/parse/parse-value.js b/src/language-css/parse/parse-value.js index dd859dee7..0a275c124 100644 --- a/src/language-css/parse/parse-value.js +++ b/src/language-css/parse/parse-value.js @@ -1,4 +1,5 @@ import PostcssValuesParser from "postcss-values-parser/lib/parser.js"; +import isObject from "../../utilities/is-object.js"; import getFunctionArgumentsText from "../utilities/get-function-arguments-text.js"; import getValueRoot from "../utilities/get-value-root.js"; import hasSCSSInterpolation from "../utilities/has-scss-interpolation.js"; @@ -156,7 +157,7 @@ function flattenGroups(node) { } function parseNestedValue(node, options) { - if (node && typeof node === "object") { + if (isObject(node)) { for (const key in node) { if (key !== "parent") { parseNestedValue(node[key], options); diff --git a/src/language-css/parse/utilities.js b/src/language-css/parse/utilities.js index d5e3a8292..0cee18b13 100644 --- a/src/language-css/parse/utilities.js +++ b/src/language-css/parse/utilities.js @@ -1,5 +1,7 @@ +import isObject from "../../utilities/is-object.js"; + function addTypePrefix(node, prefix, skipPrefix) { - if (node && typeof node === "object") { + if (isObject(node)) { delete node.parent; for (const key in node) { addTypePrefix(node[key], prefix, skipPrefix); @@ -17,7 +19,7 @@ function addTypePrefix(node, prefix, skipPrefix) { } function addMissingType(node) { - if (node && typeof node === "object") { + if (isObject(node)) { delete node.parent; for (const key in node) { addMissingType(node[key]); diff --git a/src/language-css/parser-postcss.js b/src/language-css/parser-postcss.js index dab842b36..ca8d0a25a 100644 --- a/src/language-css/parser-postcss.js +++ b/src/language-css/parser-postcss.js @@ -3,6 +3,7 @@ import postcssLess from "postcss-less"; import postcssScssParse from "postcss-scss/lib/scss-parse"; import createError from "../common/parser-create-error.js"; import { parseFrontMatter } from "../main/front-matter/index.js"; +import isObject from "../utilities/is-object.js"; import { calculateLoc, locEnd, @@ -21,7 +22,7 @@ const DEFAULT_SCSS_DIRECTIVE = /(\s*)(!default).*$/u; const GLOBAL_SCSS_DIRECTIVE = /(\s*)(!global).*$/u; function parseNestedCSS(node, options) { - if (node && typeof node === "object") { + if (isObject(node)) { delete node.parent; for (const key in node) { diff --git a/src/language-js/parse/postprocess/index.js b/src/language-js/parse/postprocess/index.js index 5a4e1cc5f..b91625852 100644 --- a/src/language-js/parse/postprocess/index.js +++ b/src/language-js/parse/postprocess/index.js @@ -43,10 +43,31 @@ function postprocess(ast, options) { mergeNestledJsdocComments(comments); + // `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; + if (program.interpreter) { + comments.unshift(program.interpreter); + delete program.interpreter; + } + + if (isOxcTs && ast.hashbang) { + comments.unshift(ast.hashbang); + delete ast.hashbang; + } + + // In `typescript` and `flow`, `Program` doesn't count whitespace and comments + // See https://github.com/typescript-eslint/typescript-eslint/issues/11026 + // See https://github.com/facebook/flow/issues/8537 + if (ast.type === "Program") { + ast.range = [0, text.length]; + } + let typeCastCommentsEnds; ast = visitNode(ast, { - onLeave(node) { + onEnter(node) { switch (node.type) { case "ParenthesizedExpression": { const { expression } = node; @@ -58,7 +79,7 @@ function postprocess(ast, options) { return expression; } - let keepTypeCast = false; + let shouldKeepParenthesizedExpression = false; if (!isOxcTs) { if (!typeCastCommentsEnds) { typeCastCommentsEnds = []; @@ -74,27 +95,21 @@ function postprocess(ast, options) { const previousCommentEnd = typeCastCommentsEnds.findLast( (end) => end <= start, ); - keepTypeCast = + shouldKeepParenthesizedExpression = previousCommentEnd && // check that there are only white spaces between the comment and the parenthesis text.slice(previousCommentEnd, start).trim().length === 0; } - if (!keepTypeCast) { - expression.extra = { ...expression.extra, parenthesized: true }; - return expression; + if (shouldKeepParenthesizedExpression) { + return; } - break; + + expression.extra = { ...expression.extra, parenthesized: true }; + return expression; } - case "LogicalExpression": - // We remove unneeded parens around same-operator LogicalExpressions - if (isUnbalancedLogicalTree(node)) { - return rebalanceLogicalTree(node); - } - break; - - // This happens when use `oxc-parser` to parse `` `${foo satisfies bar}`; `` + // This happened when use `oxc-parser` to parse `` `${foo satisfies bar}`; `` // https://github.com/oxc-project/oxc/issues/11313 case "TemplateLiteral": /* c8 ignore next 3 */ @@ -128,6 +143,7 @@ function postprocess(ast, options) { } break; } + // remove redundant TypeScript nodes case "TSParenthesizedType": return node.typeAnnotation; @@ -135,9 +151,7 @@ function postprocess(ast, options) { // For hack-style pipeline case "TopicReference": ast.extra = { ...ast.extra, __isUsingHackPipeline: true }; - break; - - // In Flow parser, it doesn't generate union/intersection types for single type + break; // In Flow parser, it doesn't generate union/intersection types for single type case "TSUnionType": case "TSIntersectionType": if (node.types.length === 1) { @@ -151,9 +165,21 @@ function postprocess(ast, options) { node.options = node.attributes; } break; + } + }, + onLeave(node) { + switch (node.type) { + // Children can be parenthesized, need do this in `onLeave` + case "LogicalExpression": + // We remove unneeded parens around same-operator LogicalExpressions + if (isUnbalancedLogicalTree(node)) { + return rebalanceLogicalTree(node); + } + break; // https://github.com/babel/babel/issues/17506 // https://github.com/oxc-project/oxc/issues/16074 + // It's possible to have parenthesized `argument`, need do this in `onLeave` case "TSImportType": if (!node.source && node.argument.type === "TSLiteralType") { node.source = node.argument.literal; @@ -169,31 +195,10 @@ function postprocess(ast, options) { }, }); - // `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; - if (program.interpreter) { - comments.unshift(program.interpreter); - delete program.interpreter; - } - - if (isOxcTs && ast.hashbang) { - comments.unshift(ast.hashbang); - delete ast.hashbang; - } - /* c8 ignore next 3 */ if (process.env.NODE_ENV !== "production") { assertComments(comments, text); } - - // In `typescript` and `flow`, `Program` doesn't count whitespace and comments - // See https://github.com/typescript-eslint/typescript-eslint/issues/11026 - // See https://github.com/facebook/flow/issues/8537 - if (ast.type === "Program") { - ast.range = [0, text.length]; - } return ast; } diff --git a/src/language-js/parse/postprocess/visit-node.js b/src/language-js/parse/postprocess/visit-node.js index ed550df1c..5672c8a2c 100644 --- a/src/language-js/parse/postprocess/visit-node.js +++ b/src/language-js/parse/postprocess/visit-node.js @@ -1,7 +1,8 @@ +import isObject from "../../../utilities/is-object.js"; import getVisitorKeys from "../../traverse/get-visitor-keys.js"; function visitNode(node, options) { - if (!(node !== null && typeof node === "object")) { + if (!isObject(node)) { return node; } @@ -16,7 +17,13 @@ function visitNode(node, options) { } if (options.onEnter) { - node = options.onEnter(node) || node; + const result = options.onEnter(node) ?? node; + // If node is replaced, re-enter + if (result !== node) { + return visitNode(result, options); + } + + node = result; } const keys = getVisitorKeys(node); @@ -24,7 +31,11 @@ function visitNode(node, options) { node[keys[i]] = visitNode(node[keys[i]], options); } - return options.onLeave(node) || node; + if (options.onLeave) { + node = options.onLeave(node) || node; + } + + return node; } export default visitNode; diff --git a/src/language-js/utilities/index.js b/src/language-js/utilities/index.js index 6d6add2f6..20ce9195d 100644 --- a/src/language-js/utilities/index.js +++ b/src/language-js/utilities/index.js @@ -3,6 +3,7 @@ import getStringWidth from "../../utilities/get-string-width.js"; import hasNewline from "../../utilities/has-newline.js"; import isNextLineEmptyAfterIndex from "../../utilities/is-next-line-empty.js"; import isNonEmptyArray from "../../utilities/is-non-empty-array.js"; +import isObject from "../../utilities/is-object.js"; import printString from "../../utilities/print-string.js"; import { hasSameLocStart, locEnd, locStart } from "../loc.js"; import getVisitorKeys from "../traverse/get-visitor-keys.js"; @@ -417,7 +418,7 @@ function getExpressionInnerNodeCount(node, maxCount) { for (const k in node) { const prop = node[k]; - if (prop && typeof prop === "object" && typeof prop.type === "string") { + if (isObject(prop) && typeof prop.type === "string") { count++; count += getExpressionInnerNodeCount(prop, maxCount - count); } diff --git a/src/main/ast-to-doc.js b/src/main/ast-to-doc.js index 909f006ae..641681403 100644 --- a/src/main/ast-to-doc.js +++ b/src/main/ast-to-doc.js @@ -1,5 +1,6 @@ import AstPath from "../common/ast-path.js"; import { cursor, inheritLabel } from "../document/index.js"; +import isObject from "../utilities/is-object.js"; import { attachComments } from "./comments/attach.js"; import { ensureAllCommentsPrinted, printComments } from "./comments/print.js"; import createPrintPreCheckFunction from "./create-print-pre-check-function.js"; @@ -82,8 +83,7 @@ async function printAstToDoc(ast, options) { return ""; } - const shouldCache = - value && typeof value === "object" && args === undefined; + const shouldCache = isObject(value) && args === undefined; if (shouldCache && cache.has(value)) { return cache.get(value);