From 6966c7e488015d83983fea2cd6baf474d584f6d3 Mon Sep 17 00:00:00 2001 From: fisker Cheung Date: Thu, 29 Jan 2026 20:27:28 +0800 Subject: [PATCH] Print decorators of `ClassExpression` on it's own (#18788) --- src/language-js/print/class.js | 18 +- src/language-js/print/ignored.js | 6 + src/language-js/print/index.js | 24 +-- .../config/format-test/failed-format-tests.js | 5 + .../ignore/__snapshots__/format.test.js.snap | 71 ------- tests/format/js/ignore/format.test.js | 7 +- .../semi/__snapshots__/format.test.js.snap | 180 ++++++++++++++++++ .../{ => semi}/class-expression-decorator.js | 0 tests/format/js/ignore/semi/format.test.js | 14 +- tests/format/js/ignore/semi/head-ignored.js | 4 + 10 files changed, 234 insertions(+), 95 deletions(-) rename tests/format/js/ignore/{ => semi}/class-expression-decorator.js (100%) create mode 100644 tests/format/js/ignore/semi/head-ignored.js diff --git a/src/language-js/print/class.js b/src/language-js/print/class.js index 612fde284..061f6abeb 100644 --- a/src/language-js/print/class.js +++ b/src/language-js/print/class.js @@ -14,13 +14,14 @@ import { } from "../../main/comments/print.js"; import createGroupIdMapper from "../../utilities/create-group-id-mapper.js"; import isNonEmptyArray from "../../utilities/is-non-empty-array.js"; +import needsParentheses from "../parentheses/needs-parentheses.js"; import { isNonEmptyClassBody } from "../utilities/class-members.js"; import { CommentCheckFlags, hasComment } from "../utilities/comments.js"; import { createTypeCheckFunction } from "../utilities/create-type-check-function.js"; import { isMemberExpression } from "../utilities/node-types.js"; import { stripChainElementWrappers } from "../utilities/strip-chain-element-wrappers.js"; import { printAssignment } from "./assignment.js"; -import { printClassMemberDecorators } from "./decorators.js"; +import { printClassMemberDecorators, printDecorators } from "./decorators.js"; import { printMethod } from "./function.js"; import { printKey } from "./key.js"; import { @@ -56,6 +57,21 @@ const isInterface = createTypeCheckFunction([ - `TSInterfaceDeclaration`(TypeScript) */ function printClass(path, options, print) { + const doc = printClassWithoutDecorators(path, options, print); + + const { node } = path; + if (node.type === "ClassExpression" && isNonEmptyArray(node.decorators)) { + const decoratorsDoc = printDecorators(path, options, print); + const needsParens = needsParentheses(path, options); + return needsParens + ? [indent([softline, decoratorsDoc, doc]), softline] + : [decoratorsDoc, doc]; + } + + return doc; +} + +function printClassWithoutDecorators(path, options, print) { const { node } = path; const isPrintingInterface = isInterface(node); const isPrintingRecord = node.type === "RecordDeclaration"; diff --git a/src/language-js/print/ignored.js b/src/language-js/print/ignored.js index 404c16f8f..19565493d 100644 --- a/src/language-js/print/ignored.js +++ b/src/language-js/print/ignored.js @@ -1,3 +1,5 @@ +import { indent, softline } from "../../document/index.js"; +import isNonEmptyArray from "../../utilities/is-non-empty-array.js"; import { locEnd, locStart } from "../loc.js"; import { shouldExpressionStatementPrintLeadingSemicolon } from "../semicolon/semicolon.js"; @@ -23,6 +25,10 @@ function printIgnored(path, options /* , print*/) { text = `;${text}`; } + if (node.type === "ClassExpression" && isNonEmptyArray(node.decorators)) { + return [indent([softline, text]), softline]; + } + return text; } diff --git a/src/language-js/print/index.js b/src/language-js/print/index.js index d49f63c52..baeb639e6 100644 --- a/src/language-js/print/index.js +++ b/src/language-js/print/index.js @@ -1,10 +1,4 @@ -import { - group, - indent, - inheritLabel, - line, - softline, -} from "../../document/index.js"; +import { group, indent, inheritLabel, softline } from "../../document/index.js"; import { printComments } from "../../main/comments/print.js"; import isNonEmptyArray from "../../utilities/is-non-empty-array.js"; import needsParentheses from "../parentheses/needs-parentheses.js"; @@ -83,13 +77,11 @@ function print(path, options, print, args) { doc = printCommentsForFunction(path, options, doc); - const hasDecorators = isNonEmptyArray(node.decorators); - const decoratorsDoc = printDecorators(path, options, print); - const isClassExpression = node.type === "ClassExpression"; - // Nodes (except `ClassExpression`) with decorators can't have parentheses and don't need leading semicolons - if (hasDecorators && !isClassExpression) { - return inheritLabel(doc, (doc) => group([decoratorsDoc, doc])); - } + const decoratorsDoc = + // `ClassExpression` prints own decorators + node.type !== "ClassExpression" && isNonEmptyArray(node.decorators) + ? printDecorators(path, options, print) + : ""; const needsParens = needsParentheses(path, options); @@ -99,9 +91,7 @@ function print(path, options, print, args) { return inheritLabel(doc, (doc) => [ needsParens ? "(" : "", - needsParens && isClassExpression && hasDecorators - ? [indent([line, decoratorsDoc, doc]), line] - : [decoratorsDoc, doc], + decoratorsDoc ? group([decoratorsDoc, doc]) : doc, needsParens ? ")" : "", ]); } diff --git a/tests/config/format-test/failed-format-tests.js b/tests/config/format-test/failed-format-tests.js index afa7b125c..d5b42245c 100644 --- a/tests/config/format-test/failed-format-tests.js +++ b/tests/config/format-test/failed-format-tests.js @@ -5,6 +5,11 @@ import { FORMAT_TEST_DIRECTORY } from "./constants.js"; const unstableTests = new Map( [ ["js/identifier/parentheses/let.js", (options) => options.semi === false], + [ + "js/ignore/semi/class-expression-decorator.js", + (options) => options.semi === false, + ], + ["js/ignore/semi/head-ignored.js", (options) => options.semi === false], "js/comments/return-statement.js", "js/comments/tagged-template-literal.js", "js/for/9812-unstable.js", diff --git a/tests/format/js/ignore/__snapshots__/format.test.js.snap b/tests/format/js/ignore/__snapshots__/format.test.js.snap index 56a97f8db..3108269fb 100644 --- a/tests/format/js/ignore/__snapshots__/format.test.js.snap +++ b/tests/format/js/ignore/__snapshots__/format.test.js.snap @@ -1,76 +1,5 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing -exports[`class-expression-decorator.js format 1`] = ` -====================================options===================================== -parsers: ["babel", "flow", "typescript"] -printWidth: 80 - | printWidth -=====================================input====================================== -( - // prettier-ignore - @decorator - class {} -); - -=====================================output===================================== -// prettier-ignore -( - @decorator - class {} -); - -================================================================================ -`; - -exports[`class-expression-decorator.js format[acorn] 1`] = ` -"Unexpected character '@' (3:3) - 1 | ( - 2 | // prettier-ignore -> 3 | @decorator - | ^ - 4 | class {} - 5 | ); - 6 | -Cause: Unexpected character '@' (3:2)" -`; - -exports[`class-expression-decorator.js format[espree] 1`] = ` -"Unexpected character '@' (3:3) - 1 | ( - 2 | // prettier-ignore -> 3 | @decorator - | ^ - 4 | class {} - 5 | ); - 6 | -Cause: Unexpected character '@'" -`; - -exports[`class-expression-decorator.js format[flow] 1`] = ` -"Unexpected token \`@\` (3:3) - 1 | ( - 2 | // prettier-ignore -> 3 | @decorator - | ^ - 4 | class {} - 5 | ); - 6 |" -`; - -exports[`class-expression-decorator.js format[hermes] 1`] = ` -"unrecognized character '@' (3:2) - 1 | ( - 2 | // prettier-ignore -> 3 | @decorator - | ^ - 4 | class {} - 5 | ); - 6 | -Cause: unrecognized character '@' (3:2) - @decorator - ^" -`; - exports[`decorator.js format 1`] = ` ====================================options===================================== parsers: ["babel", "flow", "typescript"] diff --git a/tests/format/js/ignore/format.test.js b/tests/format/js/ignore/format.test.js index d7e7dd559..b3b329173 100644 --- a/tests/format/js/ignore/format.test.js +++ b/tests/format/js/ignore/format.test.js @@ -1,8 +1,7 @@ runFormatTest(import.meta, ["babel", "flow", "typescript"], { errors: { - acorn: ["decorator.js", "class-expression-decorator.js"], - espree: ["decorator.js", "class-expression-decorator.js"], - flow: ["class-expression-decorator.js"], - hermes: ["decorator.js", "class-expression-decorator.js"], + acorn: ["decorator.js"], + espree: ["decorator.js"], + hermes: ["decorator.js"], }, }); diff --git a/tests/format/js/ignore/semi/__snapshots__/format.test.js.snap b/tests/format/js/ignore/semi/__snapshots__/format.test.js.snap index e31ebaa98..3ff48401c 100644 --- a/tests/format/js/ignore/semi/__snapshots__/format.test.js.snap +++ b/tests/format/js/ignore/semi/__snapshots__/format.test.js.snap @@ -37,6 +37,149 @@ foo; ================================================================================ `; +exports[`class-expression-decorator.js - {"semi":false} format 1`] = ` +====================================options===================================== +parsers: ["babel", "flow", "typescript"] +printWidth: 80 +semi: false + | printWidth +=====================================input====================================== +( + // prettier-ignore + @decorator + class {} +); + +=====================================output===================================== +;// prettier-ignore +( + @decorator + class {} +) + +================================================================================ +`; + +exports[`class-expression-decorator.js - {"semi":false} format[acorn] 1`] = ` +"Unexpected character '@' (3:3) + 1 | ( + 2 | // prettier-ignore +> 3 | @decorator + | ^ + 4 | class {} + 5 | ); + 6 | +Cause: Unexpected character '@' (3:2)" +`; + +exports[`class-expression-decorator.js - {"semi":false} format[espree] 1`] = ` +"Unexpected character '@' (3:3) + 1 | ( + 2 | // prettier-ignore +> 3 | @decorator + | ^ + 4 | class {} + 5 | ); + 6 | +Cause: Unexpected character '@'" +`; + +exports[`class-expression-decorator.js - {"semi":false} format[flow] 1`] = ` +"Unexpected token \`@\` (3:3) + 1 | ( + 2 | // prettier-ignore +> 3 | @decorator + | ^ + 4 | class {} + 5 | ); + 6 |" +`; + +exports[`class-expression-decorator.js - {"semi":false} format[hermes] 1`] = ` +"unrecognized character '@' (3:2) + 1 | ( + 2 | // prettier-ignore +> 3 | @decorator + | ^ + 4 | class {} + 5 | ); + 6 | +Cause: unrecognized character '@' (3:2) + @decorator + ^" +`; + +exports[`class-expression-decorator.js format 1`] = ` +====================================options===================================== +parsers: ["babel", "flow", "typescript"] +printWidth: 80 + | printWidth +=====================================input====================================== +( + // prettier-ignore + @decorator + class {} +); + +=====================================output===================================== +// prettier-ignore +( + @decorator + class {} +); + +================================================================================ +`; + +exports[`class-expression-decorator.js format[acorn] 1`] = ` +"Unexpected character '@' (3:3) + 1 | ( + 2 | // prettier-ignore +> 3 | @decorator + | ^ + 4 | class {} + 5 | ); + 6 | +Cause: Unexpected character '@' (3:2)" +`; + +exports[`class-expression-decorator.js format[espree] 1`] = ` +"Unexpected character '@' (3:3) + 1 | ( + 2 | // prettier-ignore +> 3 | @decorator + | ^ + 4 | class {} + 5 | ); + 6 | +Cause: Unexpected character '@'" +`; + +exports[`class-expression-decorator.js format[flow] 1`] = ` +"Unexpected token \`@\` (3:3) + 1 | ( + 2 | // prettier-ignore +> 3 | @decorator + | ^ + 4 | class {} + 5 | ); + 6 |" +`; + +exports[`class-expression-decorator.js format[hermes] 1`] = ` +"unrecognized character '@' (3:2) + 1 | ( + 2 | // prettier-ignore +> 3 | @decorator + | ^ + 4 | class {} + 5 | ); + 6 | +Cause: unrecognized character '@' (3:2) + @decorator + ^" +`; + exports[`directive.js - {"semi":false} format 1`] = ` ====================================options===================================== parsers: ["babel", "flow", "typescript"] @@ -97,3 +240,40 @@ function foo() { ================================================================================ `; + +exports[`head-ignored.js - {"semi":false} format 1`] = ` +====================================options===================================== +parsers: ["babel", "flow", "typescript"] +printWidth: 80 +semi: false + | printWidth +=====================================input====================================== +( + // prettier-ignore +[] +).forEach() + +=====================================output===================================== +;// prettier-ignore +[].forEach() + +================================================================================ +`; + +exports[`head-ignored.js format 1`] = ` +====================================options===================================== +parsers: ["babel", "flow", "typescript"] +printWidth: 80 + | printWidth +=====================================input====================================== +( + // prettier-ignore +[] +).forEach() + +=====================================output===================================== +// prettier-ignore +[].forEach(); + +================================================================================ +`; diff --git a/tests/format/js/ignore/class-expression-decorator.js b/tests/format/js/ignore/semi/class-expression-decorator.js similarity index 100% rename from tests/format/js/ignore/class-expression-decorator.js rename to tests/format/js/ignore/semi/class-expression-decorator.js diff --git a/tests/format/js/ignore/semi/format.test.js b/tests/format/js/ignore/semi/format.test.js index 5f0ff42da..305204c73 100644 --- a/tests/format/js/ignore/semi/format.test.js +++ b/tests/format/js/ignore/semi/format.test.js @@ -1,2 +1,12 @@ -runFormatTest(import.meta, ["babel", "flow", "typescript"]); -runFormatTest(import.meta, ["babel", "flow", "typescript"], { semi: false }); +const errors = { + acorn: ["class-expression-decorator.js"], + espree: ["class-expression-decorator.js"], + flow: ["class-expression-decorator.js"], + hermes: ["class-expression-decorator.js"], +}; + +runFormatTest(import.meta, ["babel", "flow", "typescript"], { errors }); +runFormatTest(import.meta, ["babel", "flow", "typescript"], { + semi: false, + errors, +}); diff --git a/tests/format/js/ignore/semi/head-ignored.js b/tests/format/js/ignore/semi/head-ignored.js new file mode 100644 index 000000000..66b1be552 --- /dev/null +++ b/tests/format/js/ignore/semi/head-ignored.js @@ -0,0 +1,4 @@ +( + // prettier-ignore +[] +).forEach()