From dba734dc128ee8dacee4f67b03e8a11dd8a8130c Mon Sep 17 00:00:00 2001 From: fisker Cheung Date: Sat, 31 Jan 2026 00:21:33 +0800 Subject: [PATCH] Preserve blank line before comments above `else` (#18813) --- changelog_unreleased/javascript/18813.md | 30 ++++ .../attach/handle-if-statement-comments.js | 7 +- src/language-js/print/if-statement.js | 65 ++++++--- src/language-js/utilities/comments.js | 8 +- .../utilities/is-previous-line-empty.js | 18 +++ .../js/if/__snapshots__/format.test.js.snap | 136 ++++++++++++++++++ tests/format/js/if/blank-lines.js | 65 +++++++++ 7 files changed, 304 insertions(+), 25 deletions(-) create mode 100644 changelog_unreleased/javascript/18813.md create mode 100644 src/language-js/utilities/is-previous-line-empty.js create mode 100644 tests/format/js/if/blank-lines.js diff --git a/changelog_unreleased/javascript/18813.md b/changelog_unreleased/javascript/18813.md new file mode 100644 index 000000000..d69164c52 --- /dev/null +++ b/changelog_unreleased/javascript/18813.md @@ -0,0 +1,30 @@ +#### Fix comments above `else` (#18813 by @fisker) + + +```jsx +// Input +/* Case 1 */ +if (1) { +} + +/* Case 2 */ +else if (2) { +} + +// Prettier stable +/* Case 1 */ +if (1) { +} else if (2) { + +/* Case 2 */ +} + +// Prettier main +/* Case 1 */ +if (1) { +} + +/* Case 2 */ +else if (2) { +} +``` diff --git a/src/language-js/comments/attach/handle-if-statement-comments.js b/src/language-js/comments/attach/handle-if-statement-comments.js index 6efcc5acf..1f62e38ec 100644 --- a/src/language-js/comments/attach/handle-if-statement-comments.js +++ b/src/language-js/comments/attach/handle-if-statement-comments.js @@ -93,12 +93,14 @@ function handleCommentsBetween({ return true; } + const isConsequentBlockStatement = precedingNode.type === "BlockStatement"; + // Comments before `else`: // - treat as trailing comments of the consequent, if it's a BlockStatement // - treat as a dangling comment otherwise - if ( - isSingleLineComment(comment, text) && + !isConsequentBlockStatement && + isSingleLineComment && // Comment and `precedingNode` are on same line !hasNewlineInRange(text, locEnd(precedingNode), locStart(comment)) ) { @@ -106,6 +108,7 @@ function handleCommentsBetween({ // if (cond1) expr1; // comment A // else if (cond2) expr2; // comment A // else expr3; + addTrailingComment(precedingNode, comment); return true; } diff --git a/src/language-js/print/if-statement.js b/src/language-js/print/if-statement.js index 936d971e5..60fb832db 100644 --- a/src/language-js/print/if-statement.js +++ b/src/language-js/print/if-statement.js @@ -1,6 +1,9 @@ import { group, hardline } from "../../document/index.js"; import { printDanglingComments } from "../../main/comments/print.js"; -import { CommentCheckFlags, hasComment } from "../utilities/comments.js"; +import hasNewline from "../../utilities/has-newline.js"; +import { locEnd, locStart } from "../loc.js"; +import { CommentCheckFlags, getComments } from "../utilities/comments.js"; +import { isPreviousLineEmpty } from "../utilities/is-previous-line-empty.js"; import { needsHardlineAfterDanglingComment } from "../utilities/needs-hardline-after-dangling-comment.js"; import { printIfStatementAlternate, @@ -15,36 +18,60 @@ import { printIfStatementCondition } from "./miscellaneous.js"; function printIfStatement(path, options, print) { const { node } = path; - const consequent = printIfStatementConsequent(path, options, print); const opening = group([ "if (", printIfStatementCondition(path, options, print), ")", - consequent, + printIfStatementConsequent(path, options, print), ]); - /** @type{Doc[]} */ + + if (!node.alternate) { + return opening; + } + + const { consequent } = node; + const isConsequentBlockStatement = consequent.type === "BlockStatement"; + + /** @type {Doc[]} */ const parts = [opening]; + let needSpace = isConsequentBlockStatement; + if (!isConsequentBlockStatement) { + parts.push(hardline); + needSpace = false; + } - if (node.alternate) { - const commentOnOwnLine = - hasComment( - node.consequent, - CommentCheckFlags.Trailing | CommentCheckFlags.Line, - ) || needsHardlineAfterDanglingComment(node); - const elseOnSameLine = - node.consequent.type === "BlockStatement" && !commentOnOwnLine; - parts.push(elseOnSameLine ? " " : hardline); + const danglingComments = getComments(node, CommentCheckFlags.Dangling); + if (danglingComments.length > 0) { + const [firstComment] = danglingComments; - if (hasComment(node, CommentCheckFlags.Dangling)) { - parts.push( - printDanglingComments(path, options), - commentOnOwnLine ? hardline : " ", - ); + if (isPreviousLineEmpty(firstComment, options)) { + parts.push(isConsequentBlockStatement ? [hardline, hardline] : hardline); + } else if ( + hasNewline(options.originalText, locStart(firstComment), { + backwards: true, + }) + ) { + parts.push(isConsequentBlockStatement ? hardline : ""); + } else { + parts.push(" "); } - parts.push("else", group(printIfStatementAlternate(path, options, print))); + parts.push( + printDanglingComments(path, options), + needsHardlineAfterDanglingComment(node) || + hasNewline(options.originalText, locEnd(danglingComments.at(-1))) + ? hardline + : " ", + ); + needSpace = false; } + parts.push( + needSpace ? " " : "", + "else", + group(printIfStatementAlternate(path, options, print)), + ); + return parts; } diff --git a/src/language-js/utilities/comments.js b/src/language-js/utilities/comments.js index 9424628fd..b23d70bc6 100644 --- a/src/language-js/utilities/comments.js +++ b/src/language-js/utilities/comments.js @@ -54,8 +54,8 @@ const getCommentTestFunction = (flags, fn) => { }; /** * @param {Node} node - * @param {number | function} [flags] - * @param {function} [fn] + * @param {number | ((comment: Comment) => boolean)} [flags] + * @param {(comment: Comment) => boolean} [fn] * @returns {boolean} */ function hasComment(node, flags, fn) { @@ -68,8 +68,8 @@ function hasComment(node, flags, fn) { /** * @param {Node} node - * @param {number | function} [flags] - * @param {function} [fn] + * @param {number | ((comment: Comment) => boolean)} [flags] + * @param {(comment: Comment) => boolean} [fn] * @returns {Comment[]} */ function getComments(node, flags, fn) { diff --git a/src/language-js/utilities/is-previous-line-empty.js b/src/language-js/utilities/is-previous-line-empty.js new file mode 100644 index 000000000..b20f26ba7 --- /dev/null +++ b/src/language-js/utilities/is-previous-line-empty.js @@ -0,0 +1,18 @@ +import isPreviousLineEmptyBeforeIndex from "../../utilities/is-previous-line-empty.js"; +import { locStart } from "../loc.js"; + +/** +@import { + Node, + Comment, +} from "../types/estree.js"; +*/ + +/** + * @param {Node | Comment} node + * @returns {boolean} + */ +const isPreviousLineEmpty = (node, { originalText }) => + isPreviousLineEmptyBeforeIndex(originalText, locStart(node)); + +export { isPreviousLineEmpty }; diff --git a/tests/format/js/if/__snapshots__/format.test.js.snap b/tests/format/js/if/__snapshots__/format.test.js.snap index 84df0c0aa..e661335ef 100644 --- a/tests/format/js/if/__snapshots__/format.test.js.snap +++ b/tests/format/js/if/__snapshots__/format.test.js.snap @@ -1,5 +1,141 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +exports[`blank-lines.js format 1`] = ` +====================================options===================================== +parsers: ["babel", "flow", "typescript"] +printWidth: 80 + | printWidth +=====================================input====================================== +// 11 +if (1) { + a(); +} + +// 12 +else if (2) { + a(); +} + +// 13 +else { + a(); +} + +/* 21 */ +if (1) { + a(); +} + +/* 22 */ +else if (2) { + a(); +} + +/* 23 */ +else { + a(); +} + +// 31 +if (1) + a(); + +// 32 +else if (2) + a(); + +// 33 +else + a(); + +// 31 +if (1) + a(); + +// 32 +else if (2) + a(); + +// 33 +else + a(); + +/* 41 */ if (1) { + a(); +} + +/* 42 */ else if (2) { + a(); +} + +/* 43 */ else { + a(); +} + +=====================================output===================================== +// 11 +if (1) { + a(); +} + +// 12 +else if (2) { + a(); +} + +// 13 +else { + a(); +} + +/* 21 */ +if (1) { + a(); +} + +/* 22 */ +else if (2) { + a(); +} + +/* 23 */ +else { + a(); +} + +// 31 +if (1) a(); + +// 32 +else if (2) a(); + +// 33 +else a(); + +// 31 +if (1) a(); + +// 32 +else if (2) a(); + +// 33 +else a(); + +/* 41 */ if (1) { + a(); +} + +/* 42 */ else if (2) { + a(); +} + +/* 43 */ else { + a(); +} + +================================================================================ +`; + exports[`comment_before_else.js format 1`] = ` ====================================options===================================== parsers: ["babel", "flow", "typescript"] diff --git a/tests/format/js/if/blank-lines.js b/tests/format/js/if/blank-lines.js new file mode 100644 index 000000000..b988cae8c --- /dev/null +++ b/tests/format/js/if/blank-lines.js @@ -0,0 +1,65 @@ +// 11 +if (1) { + a(); +} + +// 12 +else if (2) { + a(); +} + +// 13 +else { + a(); +} + +/* 21 */ +if (1) { + a(); +} + +/* 22 */ +else if (2) { + a(); +} + +/* 23 */ +else { + a(); +} + +// 31 +if (1) + a(); + +// 32 +else if (2) + a(); + +// 33 +else + a(); + +// 31 +if (1) + a(); + +// 32 +else if (2) + a(); + +// 33 +else + a(); + +/* 41 */ if (1) { + a(); +} + +/* 42 */ else if (2) { + a(); +} + +/* 43 */ else { + a(); +}