From 8a8fefa580ae869613d81bbab7688c396d936c9c Mon Sep 17 00:00:00 2001 From: fisker Cheung Date: Sat, 24 Jan 2026 04:53:28 +0800 Subject: [PATCH] Improve format of member chain with ChainExpression (#18729) --- src/language-js/clean.js | 51 +--------- src/language-js/clean/chain-expression.js | 72 ++++++++++++++ src/language-js/print/member-chain.js | 8 +- .../__snapshots__/format.test.js.snap | 39 ++++++++ .../js/chain-expression/member-chain.js | 17 ++++ .../__snapshots__/format.test.js.snap | 96 +++++++++++++++++++ .../chain-expression/member-chain.ts | 51 ++++++++++ 7 files changed, 284 insertions(+), 50 deletions(-) create mode 100644 src/language-js/clean/chain-expression.js create mode 100644 tests/format/js/chain-expression/member-chain.js create mode 100644 tests/format/typescript/chain-expression/member-chain.ts diff --git a/src/language-js/clean.js b/src/language-js/clean.js index d90d3f383..563f3ebb8 100644 --- a/src/language-js/clean.js +++ b/src/language-js/clean.js @@ -1,3 +1,4 @@ +import { cleanChainExpression } from "./clean/chain-expression.js"; import { isArrayExpression, isBigIntLiteral, @@ -45,46 +46,6 @@ function cleanKey(cloned, original, property) { } } -function cleanEstreeChainExpression(node) { - while (true) { - if (node.type === "MemberExpression") { - if (node.object.type === "ChainExpression") { - node.object = node.object.expression; - } - node = node.object; - continue; - } - - if (node.type === "CallExpression") { - if (node.callee.type === "ChainExpression") { - node.callee = node.callee.expression; - } - node = node.callee; - continue; - } - - return; - } -} - -function cleanBabelChainExpression(node) { - while (true) { - if (node.type === "MemberExpression") { - node.type = "OptionalMemberExpression"; - node = node.object; - continue; - } - - if (node.type === "CallExpression") { - node.type = "OptionalCallExpression"; - node = node.callee; - continue; - } - - return; - } -} - /** @param {Node} original @param {any} cloned @@ -264,15 +225,7 @@ function clean(original, cloned, parent) { } // We don't add parentheses to `(a?.b)?.c` - if (original.type === "ChainExpression") { - cleanEstreeChainExpression(cloned.expression); - } - if (original.type === "OptionalMemberExpression") { - cleanBabelChainExpression(cloned.object); - } - if (original.type === "OptionalCallExpression") { - cleanBabelChainExpression(cloned.callee); - } + cleanChainExpression(cloned, original); // https://github.com/babel/babel/issues/17719 if ( diff --git a/src/language-js/clean/chain-expression.js b/src/language-js/clean/chain-expression.js new file mode 100644 index 000000000..a478360af --- /dev/null +++ b/src/language-js/clean/chain-expression.js @@ -0,0 +1,72 @@ +import { + isCallExpression, + isChainElementWrapper, + isMemberExpression, +} from "../utilities/index.js"; + +/** +@import {Node} from "../types/estree.js" +*/ + +function cleanChainExpression(cloned, original) { + // We don't add parentheses to `(a?.b)?.c` + if (original.type === "ChainExpression") { + cleanEstreeChainExpression(cloned); + } + + if ( + original.type === "OptionalMemberExpression" || + original.type === "OptionalCallExpression" + ) { + cleanBabelChainExpression(cloned); + } +} + +function getChainElementProperty(node) { + if (isMemberExpression(node)) { + return "object"; + } + + if (isCallExpression(node)) { + return "callee"; + } + + if (isChainElementWrapper(node)) { + return "expression"; + } +} + +function getChainElement(node) { + const property = getChainElementProperty(node); + if (property) { + return node[property]; + } +} + +function cleanEstreeChainExpression(node) { + for ( + node = getChainElement(node); + node.type === "MemberExpression" || + node.type === "CallExpression" || + node.type === "TSNonNullExpression"; + node = getChainElement(node) + ) { + const property = getChainElementProperty(node); + const child = node[property]; + if (child.type === "ChainExpression") { + node[property] = child.expression; + } + } +} + +function cleanBabelChainExpression(node) { + for ( + node = getChainElement(node); + node.type === "MemberExpression" || node.type === "CallExpression"; + node = getChainElement(node) + ) { + node.type = `Optional${node.type}`; + } +} + +export { cleanChainExpression }; diff --git a/src/language-js/print/member-chain.js b/src/language-js/print/member-chain.js index 392fc75c9..915d87133 100644 --- a/src/language-js/print/member-chain.js +++ b/src/language-js/print/member-chain.js @@ -32,7 +32,7 @@ import { printOptionalToken } from "./miscellaneous.js"; /** * @import {Doc} from "../../document/index.js" - * @typedef {{ node: any, printed: Doc, needsParens?: boolean, shouldInline?: boolean, hasTrailingEmptyLine?: boolean }} PrintedNode + * @typedef {{ node: any, printed: Doc, shouldInline?: boolean, hasTrailingEmptyLine?: boolean }} PrintedNode */ // We detect calls on member expressions specially to format a @@ -126,6 +126,11 @@ function printMemberChain(path, options, print) { ), }); path.call(rec, "object"); + } else if ( + node.type === "ChainExpression" && + !needsParentheses(path, options) + ) { + path.call(rec, "expression"); } else if ( node.type === "TSNonNullExpression" && !needsParentheses(path, options) @@ -190,6 +195,7 @@ function printMemberChain(path, options, print) { for (; i < printedNodes.length; ++i) { if ( printedNodes[i].node.type === "TSNonNullExpression" || + printedNodes[i].node.type === "ChainExpression" || isCallExpression(printedNodes[i].node) || (isMemberExpression(printedNodes[i].node) && printedNodes[i].node.computed && diff --git a/tests/format/js/chain-expression/__snapshots__/format.test.js.snap b/tests/format/js/chain-expression/__snapshots__/format.test.js.snap index 4f8c17ed4..4358d5a53 100644 --- a/tests/format/js/chain-expression/__snapshots__/format.test.js.snap +++ b/tests/format/js/chain-expression/__snapshots__/format.test.js.snap @@ -182,6 +182,45 @@ const randomFuncion = (value) => { ================================================================================ `; +exports[`member-chain.js format 1`] = ` +====================================options===================================== +parsers: ["babel", "typescript", "flow"] +printWidth: 80 + | printWidth +=====================================input====================================== +bifornCringerMoshedPerplexSawder = ( + glimseGlyphsHazardNoopsTieTie + ?. averredBathersBoxroomBuggyNurl + ?.anodyneCondosMalateOverateRetinol + ) + .annularCooeedSplicesWalksWayWay + .annularCooeedSplicesWalksWayWay(annularCooeedSplicesWalksWayWay) + .annularCooeedSplicesWalksWayWay(); + +bifornCringerMoshedPerplexSawder = ( + glimseGlyphsHazardNoopsTieTie + ?. averredBathersBoxroomBuggyNurl + ?.anodyneCondosMalateOverateRetinol + ) + ?.annularCooeedSplicesWalksWayWay + .annularCooeedSplicesWalksWayWay(annularCooeedSplicesWalksWayWay) + .annularCooeedSplicesWalksWayWay(); + +=====================================output===================================== +bifornCringerMoshedPerplexSawder = (glimseGlyphsHazardNoopsTieTie + ?.averredBathersBoxroomBuggyNurl + ?.anodyneCondosMalateOverateRetinol).annularCooeedSplicesWalksWayWay + .annularCooeedSplicesWalksWayWay(annularCooeedSplicesWalksWayWay) + .annularCooeedSplicesWalksWayWay(); + +bifornCringerMoshedPerplexSawder = + glimseGlyphsHazardNoopsTieTie?.averredBathersBoxroomBuggyNurl?.anodyneCondosMalateOverateRetinol?.annularCooeedSplicesWalksWayWay + .annularCooeedSplicesWalksWayWay(annularCooeedSplicesWalksWayWay) + .annularCooeedSplicesWalksWayWay(); + +================================================================================ +`; + exports[`member-expression.js format 1`] = ` ====================================options===================================== parsers: ["babel", "typescript", "flow"] diff --git a/tests/format/js/chain-expression/member-chain.js b/tests/format/js/chain-expression/member-chain.js new file mode 100644 index 000000000..651f59762 --- /dev/null +++ b/tests/format/js/chain-expression/member-chain.js @@ -0,0 +1,17 @@ +bifornCringerMoshedPerplexSawder = ( + glimseGlyphsHazardNoopsTieTie + ?. averredBathersBoxroomBuggyNurl + ?.anodyneCondosMalateOverateRetinol + ) + .annularCooeedSplicesWalksWayWay + .annularCooeedSplicesWalksWayWay(annularCooeedSplicesWalksWayWay) + .annularCooeedSplicesWalksWayWay(); + +bifornCringerMoshedPerplexSawder = ( + glimseGlyphsHazardNoopsTieTie + ?. averredBathersBoxroomBuggyNurl + ?.anodyneCondosMalateOverateRetinol + ) + ?.annularCooeedSplicesWalksWayWay + .annularCooeedSplicesWalksWayWay(annularCooeedSplicesWalksWayWay) + .annularCooeedSplicesWalksWayWay(); diff --git a/tests/format/typescript/chain-expression/__snapshots__/format.test.js.snap b/tests/format/typescript/chain-expression/__snapshots__/format.test.js.snap index 114f54757..3f1be2c3e 100644 --- a/tests/format/typescript/chain-expression/__snapshots__/format.test.js.snap +++ b/tests/format/typescript/chain-expression/__snapshots__/format.test.js.snap @@ -219,6 +219,102 @@ const theValue3 = Object.entries(someLongObjectName).filter(([listingId]) => ================================================================================ `; +exports[`member-chain.ts format 1`] = ` +====================================options===================================== +parsers: ["typescript"] +printWidth: 80 + | printWidth +=====================================input====================================== +bifornCringerMoshedPerplexSawder = ( + glimseGlyphsHazardNoopsTieTie + ?. averredBathersBoxroomBuggyNurl + ?.anodyneCondosMalateOverateRetinol! + ) + .annularCooeedSplicesWalksWayWay + .annularCooeedSplicesWalksWayWay(annularCooeedSplicesWalksWayWay) + .annularCooeedSplicesWalksWayWay(); +bifornCringerMoshedPerplexSawder = ( + glimseGlyphsHazardNoopsTieTie + ?. averredBathersBoxroomBuggyNurl + ?.anodyneCondosMalateOverateRetinol + )! + .annularCooeedSplicesWalksWayWay + .annularCooeedSplicesWalksWayWay(annularCooeedSplicesWalksWayWay) + .annularCooeedSplicesWalksWayWay(); +bifornCringerMoshedPerplexSawder = ( + glimseGlyphsHazardNoopsTieTie + ?. averredBathersBoxroomBuggyNurl + ?.anodyneCondosMalateOverateRetinol + ) + .annularCooeedSplicesWalksWayWay + .annularCooeedSplicesWalksWayWay(annularCooeedSplicesWalksWayWay) + .annularCooeedSplicesWalksWayWay()!; + +bifornCringerMoshedPerplexSawder = ( + glimseGlyphsHazardNoopsTieTie + ?. averredBathersBoxroomBuggyNurl + ?.anodyneCondosMalateOverateRetinol! + ) + ?.annularCooeedSplicesWalksWayWay + .annularCooeedSplicesWalksWayWay(annularCooeedSplicesWalksWayWay) + .annularCooeedSplicesWalksWayWay(); + +bifornCringerMoshedPerplexSawder = ( + glimseGlyphsHazardNoopsTieTie + ?. averredBathersBoxroomBuggyNurl + ?.anodyneCondosMalateOverateRetinol + )! + ?.annularCooeedSplicesWalksWayWay + .annularCooeedSplicesWalksWayWay(annularCooeedSplicesWalksWayWay) + .annularCooeedSplicesWalksWayWay(); + +bifornCringerMoshedPerplexSawder = ( + glimseGlyphsHazardNoopsTieTie + ?. averredBathersBoxroomBuggyNurl + ?.anodyneCondosMalateOverateRetinol + ) + ?.annularCooeedSplicesWalksWayWay + .annularCooeedSplicesWalksWayWay(annularCooeedSplicesWalksWayWay) + .annularCooeedSplicesWalksWayWay()!; + +=====================================output===================================== +bifornCringerMoshedPerplexSawder = (glimseGlyphsHazardNoopsTieTie + ?.averredBathersBoxroomBuggyNurl + ?.anodyneCondosMalateOverateRetinol!).annularCooeedSplicesWalksWayWay + .annularCooeedSplicesWalksWayWay(annularCooeedSplicesWalksWayWay) + .annularCooeedSplicesWalksWayWay(); +bifornCringerMoshedPerplexSawder = (glimseGlyphsHazardNoopsTieTie + ?.averredBathersBoxroomBuggyNurl + ?.anodyneCondosMalateOverateRetinol)!.annularCooeedSplicesWalksWayWay + .annularCooeedSplicesWalksWayWay(annularCooeedSplicesWalksWayWay) + .annularCooeedSplicesWalksWayWay(); +bifornCringerMoshedPerplexSawder = (glimseGlyphsHazardNoopsTieTie + ?.averredBathersBoxroomBuggyNurl + ?.anodyneCondosMalateOverateRetinol).annularCooeedSplicesWalksWayWay + .annularCooeedSplicesWalksWayWay(annularCooeedSplicesWalksWayWay) + .annularCooeedSplicesWalksWayWay()!; + +bifornCringerMoshedPerplexSawder = + glimseGlyphsHazardNoopsTieTie?.averredBathersBoxroomBuggyNurl + ?.anodyneCondosMalateOverateRetinol!?.annularCooeedSplicesWalksWayWay.annularCooeedSplicesWalksWayWay( + annularCooeedSplicesWalksWayWay, + ) + .annularCooeedSplicesWalksWayWay(); + +bifornCringerMoshedPerplexSawder = (glimseGlyphsHazardNoopsTieTie + ?.averredBathersBoxroomBuggyNurl + ?.anodyneCondosMalateOverateRetinol)!?.annularCooeedSplicesWalksWayWay + .annularCooeedSplicesWalksWayWay(annularCooeedSplicesWalksWayWay) + .annularCooeedSplicesWalksWayWay(); + +bifornCringerMoshedPerplexSawder = + glimseGlyphsHazardNoopsTieTie?.averredBathersBoxroomBuggyNurl?.anodyneCondosMalateOverateRetinol?.annularCooeedSplicesWalksWayWay + .annularCooeedSplicesWalksWayWay(annularCooeedSplicesWalksWayWay) + .annularCooeedSplicesWalksWayWay()!; + +================================================================================ +`; + exports[`member-expression.ts format 1`] = ` ====================================options===================================== parsers: ["typescript"] diff --git a/tests/format/typescript/chain-expression/member-chain.ts b/tests/format/typescript/chain-expression/member-chain.ts new file mode 100644 index 000000000..a4b14dffc --- /dev/null +++ b/tests/format/typescript/chain-expression/member-chain.ts @@ -0,0 +1,51 @@ +bifornCringerMoshedPerplexSawder = ( + glimseGlyphsHazardNoopsTieTie + ?. averredBathersBoxroomBuggyNurl + ?.anodyneCondosMalateOverateRetinol! + ) + .annularCooeedSplicesWalksWayWay + .annularCooeedSplicesWalksWayWay(annularCooeedSplicesWalksWayWay) + .annularCooeedSplicesWalksWayWay(); +bifornCringerMoshedPerplexSawder = ( + glimseGlyphsHazardNoopsTieTie + ?. averredBathersBoxroomBuggyNurl + ?.anodyneCondosMalateOverateRetinol + )! + .annularCooeedSplicesWalksWayWay + .annularCooeedSplicesWalksWayWay(annularCooeedSplicesWalksWayWay) + .annularCooeedSplicesWalksWayWay(); +bifornCringerMoshedPerplexSawder = ( + glimseGlyphsHazardNoopsTieTie + ?. averredBathersBoxroomBuggyNurl + ?.anodyneCondosMalateOverateRetinol + ) + .annularCooeedSplicesWalksWayWay + .annularCooeedSplicesWalksWayWay(annularCooeedSplicesWalksWayWay) + .annularCooeedSplicesWalksWayWay()!; + +bifornCringerMoshedPerplexSawder = ( + glimseGlyphsHazardNoopsTieTie + ?. averredBathersBoxroomBuggyNurl + ?.anodyneCondosMalateOverateRetinol! + ) + ?.annularCooeedSplicesWalksWayWay + .annularCooeedSplicesWalksWayWay(annularCooeedSplicesWalksWayWay) + .annularCooeedSplicesWalksWayWay(); + +bifornCringerMoshedPerplexSawder = ( + glimseGlyphsHazardNoopsTieTie + ?. averredBathersBoxroomBuggyNurl + ?.anodyneCondosMalateOverateRetinol + )! + ?.annularCooeedSplicesWalksWayWay + .annularCooeedSplicesWalksWayWay(annularCooeedSplicesWalksWayWay) + .annularCooeedSplicesWalksWayWay(); + +bifornCringerMoshedPerplexSawder = ( + glimseGlyphsHazardNoopsTieTie + ?. averredBathersBoxroomBuggyNurl + ?.anodyneCondosMalateOverateRetinol + ) + ?.annularCooeedSplicesWalksWayWay + .annularCooeedSplicesWalksWayWay(annularCooeedSplicesWalksWayWay) + .annularCooeedSplicesWalksWayWay()!;