diff --git a/changelog_unreleased/javascript/19652.md b/changelog_unreleased/javascript/19652.md new file mode 100644 index 000000000..a5d7a3731 --- /dev/null +++ b/changelog_unreleased/javascript/19652.md @@ -0,0 +1,13 @@ +#### Fix misplaced type cast comments (#19652 by @fisker) + + +```jsx +// Input +foo = a ? b : (/** @type {Foo} */ (foo) ?? bar); + +// Prettier stable +foo = a ? b : /** @type {Foo} */ ((foo) ?? bar); + +// Prettier main +foo = a ? b : (/** @type {Foo} */ (foo) ?? bar); +``` diff --git a/src/language-js/comments/handle-comments.js b/src/language-js/comments/handle-comments.js index 5014e3d24..86009bf5b 100644 --- a/src/language-js/comments/handle-comments.js +++ b/src/language-js/comments/handle-comments.js @@ -15,6 +15,7 @@ import { isBlockComment, isLineComment } from "../utilities/comment-types.js"; import { createTypeCheckFunction } from "../utilities/create-type-check-function.js"; import { getFunctionParameters } from "../utilities/function-parameters.js"; import { isMethod } from "../utilities/is-method.js"; +import { isNullishCoalescing } from "../utilities/is-nullish-coalescing.js"; import { isObjectProperty } from "../utilities/is-object-property.js"; import { isPrettierIgnoreComment } from "../utilities/is-prettier-ignore-comment.js"; import { isTypeCastComment } from "../utilities/is-type-cast-comment.js"; @@ -70,6 +71,7 @@ function handleOwnLineComment(context) { return [ handleCommentInEmptyParens, handleIgnoreComments, + handleClosureTypeCastComments, handleConditionalExpressionComments, handleLastFunctionParameterComments, handleMemberExpressionComments, @@ -135,6 +137,7 @@ function handleRemainingComment(context) { return [ handleCommentInEmptyParens, handleIgnoreComments, + handleClosureTypeCastComments, handleIfStatementComments, handleWhileLikeComments, handleSwitchStatementComments, @@ -152,9 +155,21 @@ function handleRemainingComment(context) { ].some((fn) => fn(context)); } -function handleClosureTypeCastComments({ comment, followingNode }) { +function handleClosureTypeCastComments({ + comment, + followingNode, + enclosingNode, +}) { if (followingNode && isTypeCastComment(comment)) { - addLeadingComment(followingNode, comment); + addLeadingComment( + // We'll add parentheses to the nullish coalescing expression, + // The comment need attach to the left side, so the comments can print inside it + enclosingNode?.type === "ConditionalExpression" && + isNullishCoalescing(followingNode) + ? followingNode.left + : followingNode, + comment, + ); return true; } return false; diff --git a/tests/format/js/comments-closure-typecast/__snapshots__/format.test.js.snap b/tests/format/js/comments-closure-typecast/__snapshots__/format.test.js.snap index 2f73c8ce5..b04c90822 100644 --- a/tests/format/js/comments-closure-typecast/__snapshots__/format.test.js.snap +++ b/tests/format/js/comments-closure-typecast/__snapshots__/format.test.js.snap @@ -566,6 +566,42 @@ const w1 = /** @typefoo Foo */ value; ================================================================================ `; +exports[`nullish-coalescing.js format 1`] = ` +====================================options===================================== +parsers: ["babel"] + printWidth: 80 (default) | +=====================================input====================================== +target = a + ? + b + : ( + /** @type {A} */ (c) + ?? d + ) + +target = a + ? b + : (/** @type {A} */ (root).head ?? root.ownerDocument.head); + +// https://github.com/sveltejs/svelte/blob/602a873b6b82bba4e6edc91b039e2f6defbe3fc4/packages/svelte/src/internal/client/dom/css.js#L15-L17 +target = /** @type {ShadowRoot} */ (root).host + ? /** @type {ShadowRoot} */ (root) + : (/** @type {Document} */ (root).head ?? /** @type {Document} */ (root.ownerDocument).head); + +=====================================output===================================== +target = a ? b : (/** @type {A} */ (c) ?? d); + +target = a ? b : (/** @type {A} */ (root).head ?? root.ownerDocument.head); + +// https://github.com/sveltejs/svelte/blob/602a873b6b82bba4e6edc91b039e2f6defbe3fc4/packages/svelte/src/internal/client/dom/css.js#L15-L17 +target = /** @type {ShadowRoot} */ (root).host + ? /** @type {ShadowRoot} */ (root) + : (/** @type {Document} */ (root).head ?? + /** @type {Document} */ (root.ownerDocument).head); + +================================================================================ +`; + exports[`object-with-comment.js format 1`] = ` ====================================options===================================== parsers: ["babel"] diff --git a/tests/format/js/comments-closure-typecast/nullish-coalescing.js b/tests/format/js/comments-closure-typecast/nullish-coalescing.js new file mode 100644 index 000000000..c3cd854da --- /dev/null +++ b/tests/format/js/comments-closure-typecast/nullish-coalescing.js @@ -0,0 +1,16 @@ +target = a + ? + b + : ( + /** @type {A} */ (c) + ?? d + ) + +target = a + ? b + : (/** @type {A} */ (root).head ?? root.ownerDocument.head); + +// https://github.com/sveltejs/svelte/blob/602a873b6b82bba4e6edc91b039e2f6defbe3fc4/packages/svelte/src/internal/client/dom/css.js#L15-L17 +target = /** @type {ShadowRoot} */ (root).host + ? /** @type {ShadowRoot} */ (root) + : (/** @type {Document} */ (root).head ?? /** @type {Document} */ (root.ownerDocument).head); diff --git a/tests/format/js/nullish-coalescing/__snapshots__/format.test.js.snap b/tests/format/js/nullish-coalescing/__snapshots__/format.test.js.snap index 5530d2a9f..71cda8f83 100644 --- a/tests/format/js/nullish-coalescing/__snapshots__/format.test.js.snap +++ b/tests/format/js/nullish-coalescing/__snapshots__/format.test.js.snap @@ -1,6 +1,119 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing -exports[`nullish_coalesing_operator.js format 1`] = ` +exports[`comments.js format 1`] = ` +====================================options===================================== +parsers: ["babel", "flow", "typescript"] + printWidth: 80 (default) | +=====================================input====================================== +errorDetails = + errorJSON === '{}' + ? // regular JS error case: print stacktrace + (error.stack ?? 'N/A') + : // MDX error: print extra attributes + stacktrace + \`\${errorJSON}\\n\${error.stack}\`; + +_ = { + children: keepChildren + ? instance.children + : // $FlowFixMe[incompatible-type] We're not typing immutable instances. + (children ?? []) +} + +_ = ( + + + + {isPending ? ( + loading + ) : data == null || data.length === 0 || total === 0 ? ( + /** + * Once loaded, the data for the list may be empty. + * Instead of displaying the table header + * with zero data rows, the DataTable + * displays the empty component. + */ + empty ?? null + ) : ( + /** + * After the initial load, if the data for the list + * isn't empty, and even if the data is refreshing + * (e.g. after a filter change), the DataTable + * displays the current data. + */ + + + + + {children} + + + + + )} + + + + ); + +=====================================output===================================== +errorDetails = + errorJSON === "{}" + ? // regular JS error case: print stacktrace + (error.stack ?? "N/A") + : // MDX error: print extra attributes + stacktrace + \`\${errorJSON}\\n\${error.stack}\`; + +_ = { + children: keepChildren + ? instance.children + : // $FlowFixMe[incompatible-type] We're not typing immutable instances. + (children ?? []), +}; + +_ = ( + + + + {isPending ? ( + loading + ) : data == null || data.length === 0 || total === 0 ? ( + /** + * Once loaded, the data for the list may be empty. + * Instead of displaying the table header + * with zero data rows, the DataTable + * displays the empty component. + */ + (empty ?? null) + ) : ( + /** + * After the initial load, if the data for the list + * isn't empty, and even if the data is refreshing + * (e.g. after a filter change), the DataTable + * displays the current data. + */ + + + + + {children} + + + + + )} + + + +); + +================================================================================ +`; + +exports[`nullish-coalesing-operator.js format 1`] = ` ====================================options===================================== parsers: ["babel", "flow", "typescript"] printWidth: 80 (default) | diff --git a/tests/format/js/nullish-coalescing/comments.js b/tests/format/js/nullish-coalescing/comments.js new file mode 100644 index 000000000..0815fa010 --- /dev/null +++ b/tests/format/js/nullish-coalescing/comments.js @@ -0,0 +1,53 @@ +errorDetails = + errorJSON === '{}' + ? // regular JS error case: print stacktrace + (error.stack ?? 'N/A') + : // MDX error: print extra attributes + stacktrace + `${errorJSON}\n${error.stack}`; + +_ = { + children: keepChildren + ? instance.children + : // $FlowFixMe[incompatible-type] We're not typing immutable instances. + (children ?? []) +} + +_ = ( + + + + {isPending ? ( + loading + ) : data == null || data.length === 0 || total === 0 ? ( + /** + * Once loaded, the data for the list may be empty. + * Instead of displaying the table header + * with zero data rows, the DataTable + * displays the empty component. + */ + empty ?? null + ) : ( + /** + * After the initial load, if the data for the list + * isn't empty, and even if the data is refreshing + * (e.g. after a filter change), the DataTable + * displays the current data. + */ + + + + + {children} + + + + + )} + + + + ); diff --git a/tests/format/js/nullish-coalescing/nullish_coalesing_operator.js b/tests/format/js/nullish-coalescing/nullish-coalesing-operator.js similarity index 100% rename from tests/format/js/nullish-coalescing/nullish_coalesing_operator.js rename to tests/format/js/nullish-coalescing/nullish-coalesing-operator.js