diff --git a/changelog_unreleased/javascript/15522.md b/changelog_unreleased/javascript/15522.md new file mode 100644 index 000000000..cd4623c84 --- /dev/null +++ b/changelog_unreleased/javascript/15522.md @@ -0,0 +1,22 @@ +#### Fix non-idempotent formatting of method chain with empty line (#15522 by @seiyab) + + +```js +// Input +Foo.a() + +.b(); + +// Prettier stable (first format) +Foo.a() +.b(); + +// Prettier stable (second format) +Foo.a().b(); + + +// Prettier main +Foo.a() + + .b(); +``` diff --git a/src/language-js/print/member-chain.js b/src/language-js/print/member-chain.js index c37819831..deccb7feb 100644 --- a/src/language-js/print/member-chain.js +++ b/src/language-js/print/member-chain.js @@ -34,6 +34,11 @@ import { printBindExpressionCallee, } from "./misc.js"; +/** + * @typedef {import("../../document/builders.js").Doc} Doc + * @typedef {{ node: any, printed: Doc, needsParens?: boolean, shouldInline?: boolean, hasTrailingEmptyLine?: boolean }} PrintedNode + */ + // We detect calls on member expressions specially to format a // common pattern better. The pattern we are looking for is this: // @@ -57,6 +62,7 @@ function printMemberChain(path, options, print) { // CallExpression(MemberExpression(CallExpression(Identifier))) // and we transform it into // [Identifier, CallExpression, MemberExpression, CallExpression] + /** @type {PrintedNode[]}} */ const printedNodes = []; // Here we try to retain one typed empty line after each call expression or @@ -87,8 +93,10 @@ function printMemberChain(path, options, print) { isCallExpression(node) && (isMemberish(node.callee) || isCallExpression(node.callee)) ) { + const hasTrailingEmptyLine = shouldInsertEmptyLineAfter(node); printedNodes.unshift({ node, + hasTrailingEmptyLine, printed: [ printComments( path, @@ -99,7 +107,7 @@ function printMemberChain(path, options, print) { ], options, ), - shouldInsertEmptyLineAfter(node) ? hardline : "", + hasTrailingEmptyLine ? hardline : "", ], }); path.call((callee) => rec(callee), "callee"); @@ -170,6 +178,7 @@ function printMemberChain(path, options, print) { // < fn()[0][1][2] >.something() // - then, as many MemberExpression as possible but the last one // < this.items >.something() + /** @type {PrintedNode[][]} */ const groups = []; let currentGroup = [printedNodes[0]]; let i = 1; @@ -331,7 +340,11 @@ function printMemberChain(path, options, print) { // If we only have a single `.`, we shouldn't do anything fancy and just // render everything concatenated together. - if (groups.length <= cutoff && !nodeHasComment) { + if ( + groups.length <= cutoff && + !nodeHasComment && + !groups.some((g) => g.at(-1).hasTrailingEmptyLine) + ) { if (isLongCurriedCallExpression(path)) { return oneLine; } diff --git a/tests/format/js/method-chain/__snapshots__/jsfmt.spec.js.snap b/tests/format/js/method-chain/__snapshots__/jsfmt.spec.js.snap index b3f594e45..61b00e014 100644 --- a/tests/format/js/method-chain/__snapshots__/jsfmt.spec.js.snap +++ b/tests/format/js/method-chain/__snapshots__/jsfmt.spec.js.snap @@ -28,6 +28,26 @@ foo(a--).bar().leet(); ================================================================================ `; +exports[`assignment-lhs.js format 1`] = ` +====================================options===================================== +parsers: ["babel", "flow", "typescript"] +printWidth: 80 + | printWidth +=====================================input====================================== +document.querySelector("#output-abcdef").textContent = \`metaKey pressed? \${e.metaKey}\`; + +document.querySelector("p").textContent = \`Shader program did not link successfully. Error log: \${linkErrLog}\`; + +=====================================output===================================== +document.querySelector("#output-abcdef").textContent = + \`metaKey pressed? \${e.metaKey}\`; + +document.querySelector("p").textContent = + \`Shader program did not link successfully. Error log: \${linkErrLog}\`; + +================================================================================ +`; + exports[`bracket_0.js format 1`] = ` ====================================options===================================== parsers: ["babel", "flow", "typescript"] diff --git a/tests/format/js/method-chain/assignment-lhs.js b/tests/format/js/method-chain/assignment-lhs.js new file mode 100644 index 000000000..ddba03a27 --- /dev/null +++ b/tests/format/js/method-chain/assignment-lhs.js @@ -0,0 +1,3 @@ +document.querySelector("#output-abcdef").textContent = `metaKey pressed? ${e.metaKey}`; + +document.querySelector("p").textContent = `Shader program did not link successfully. Error log: ${linkErrLog}`; diff --git a/tests/format/js/preserve-line/__snapshots__/jsfmt.spec.js.snap b/tests/format/js/preserve-line/__snapshots__/jsfmt.spec.js.snap index 27b575abf..f37329c62 100644 --- a/tests/format/js/preserve-line/__snapshots__/jsfmt.spec.js.snap +++ b/tests/format/js/preserve-line/__snapshots__/jsfmt.spec.js.snap @@ -529,6 +529,14 @@ const sel = this.connections .concat(this.activities.concat(this.operators)) .filter(x => x.selected); +Object.entries(obj) + +.forEach((e) => console.log(e)); + +this.fetch("/foo") + + .then(response => response.json()); + =====================================output===================================== fooBar .doSomething("Hello World") @@ -590,6 +598,14 @@ const sel = this.connections .concat(this.activities.concat(this.operators)) .filter((x) => x.selected); +Object.entries(obj) + + .forEach((e) => console.log(e)); + +this.fetch("/foo") + + .then((response) => response.json()); + ================================================================================ `; diff --git a/tests/format/js/preserve-line/member-chain.js b/tests/format/js/preserve-line/member-chain.js index 267f1ba82..b63b284d6 100644 --- a/tests/format/js/preserve-line/member-chain.js +++ b/tests/format/js/preserve-line/member-chain.js @@ -57,3 +57,11 @@ const sel = this.connections .concat(this.activities.concat(this.operators)) .filter(x => x.selected); + +Object.entries(obj) + +.forEach((e) => console.log(e)); + +this.fetch("/foo") + + .then(response => response.json());