From 6ba133e80c3d95635884da8eb72ce096f7721284 Mon Sep 17 00:00:00 2001 From: fisker Cheung Date: Wed, 26 Nov 2025 19:43:08 +0800 Subject: [PATCH] Improve super class (or single heritage) breaking (#18325) --- changelog_unreleased/javascript/18325.md | 15 +++ src/language-js/print/class.js | 77 ++++++++++++--- .../__snapshots__/format.test.js.snap | 98 +++++++++++++++++++ .../flow/class/heritage-break/format.test.js | 1 + .../heritage-break/member-expression-like.js | 18 ++++ .../class/heritage-break/type-arguments.js | 19 ++++ .../__snapshots__/format.test.js.snap | 88 +++++++++++++++++ .../heritage-break/format.test.js | 1 + .../heritage-break/member-expression-like.ts | 18 ++++ .../heritage-break/type-arguments.ts | 14 +++ 10 files changed, 337 insertions(+), 12 deletions(-) create mode 100644 changelog_unreleased/javascript/18325.md create mode 100644 tests/format/flow/class/heritage-break/__snapshots__/format.test.js.snap create mode 100644 tests/format/flow/class/heritage-break/format.test.js create mode 100644 tests/format/flow/class/heritage-break/member-expression-like.js create mode 100644 tests/format/flow/class/heritage-break/type-arguments.js create mode 100644 tests/format/typescript/class-and-interface/heritage-break/__snapshots__/format.test.js.snap create mode 100644 tests/format/typescript/class-and-interface/heritage-break/format.test.js create mode 100644 tests/format/typescript/class-and-interface/heritage-break/member-expression-like.ts create mode 100644 tests/format/typescript/class-and-interface/heritage-break/type-arguments.ts diff --git a/changelog_unreleased/javascript/18325.md b/changelog_unreleased/javascript/18325.md new file mode 100644 index 000000000..9fb15df3d --- /dev/null +++ b/changelog_unreleased/javascript/18325.md @@ -0,0 +1,15 @@ +#### Improve super class format (#18325 by @fisker) + + +```jsx +// Input +class EnsureNoDisposablesAreLeakedInTestSuiteSuite extends eslint.Rule.RuleModule {}; + +// Prettier stable +class EnsureNoDisposablesAreLeakedInTestSuiteSuite extends eslint.Rule + .RuleModule {} + +// Prettier main +class EnsureNoDisposablesAreLeakedInTestSuiteSuite + extends eslint.Rule.RuleModule {} +``` diff --git a/src/language-js/print/class.js b/src/language-js/print/class.js index a548c9bb3..876b033b6 100644 --- a/src/language-js/print/class.js +++ b/src/language-js/print/class.js @@ -18,6 +18,7 @@ import { CommentCheckFlags, createTypeCheckFunction, hasComment, + isMemberExpression, } from "../utils/index.js"; import { printAssignment } from "./assignment.js"; import { printClassMemberDecorators } from "./decorators.js"; @@ -67,11 +68,7 @@ function printClass(path, options, print) { // Keep old behaviour of extends in same line // If there is only on extends and there are not comments - const groupMode = - hasComment(node.id, CommentCheckFlags.Trailing) || - hasComment(node.typeParameters, CommentCheckFlags.Trailing) || - hasComment(node.superClass) || - hasMultipleHeritage(node); + const groupMode = shouldPrintClassInGroupMode(path); const partsGroup = []; const extendsParts = []; @@ -110,14 +107,12 @@ function printClass(path, options, print) { extendsParts.push(" ", printedWithComments); } } else { - extendsParts.push( - printHeritageClauses(path, options, print, "extends", groupMode), - ); + extendsParts.push(printHeritageClauses(path, options, print, "extends")); } extendsParts.push( - printHeritageClauses(path, options, print, "mixins", groupMode), - printHeritageClauses(path, options, print, "implements", groupMode), + printHeritageClauses(path, options, print, "mixins"), + printHeritageClauses(path, options, print, "implements"), ); let heritageGroupId; @@ -170,7 +165,65 @@ function hasMultipleHeritage(node) { return count > 1; } -function printHeritageClauses(path, options, print, listName, groupMode) { +/** +@returns {boolean} +*/ +function shouldPrintClassInGroupModeWithoutCache(path) { + const { node } = path; + if ( + hasComment(node.id, CommentCheckFlags.Trailing) || + hasComment(node.typeParameters, CommentCheckFlags.Trailing) || + hasComment(node.superClass) || + hasMultipleHeritage(node) + ) { + return true; + } + + if (node.superClass) { + if (path.parent.type === "AssignmentExpression") { + return false; + } + + const superTypeArguments = + node.superTypeArguments ?? node.superTypeParameters; + + return !superTypeArguments && isMemberExpression(node.superClass); + } + + const heritage = + node.extends?.[0] ?? node.mixins?.[0] ?? node.implements?.[0]; + + if (!heritage) { + return false; + } + + const groupMode = + // `ClassImplements` seem not allow `QualifiedTypeIdentifier` + (heritage.type === "InterfaceExtends" && + heritage.id.type === "QualifiedTypeIdentifier" && + !heritage.typeParameters) || + ((heritage.type === "TSClassImplements" || + heritage.type === "TSInterfaceHeritage") && + isMemberExpression(heritage.expression) && + !heritage.typeArguments); + + return groupMode; +} + +const shouldPrintClassInGroupModeCache = new WeakMap(); +function shouldPrintClassInGroupMode(path) { + const { node } = path; + if (!shouldPrintClassInGroupModeCache.has(node)) { + shouldPrintClassInGroupModeCache.set( + node, + shouldPrintClassInGroupModeWithoutCache(path), + ); + } + + return shouldPrintClassInGroupModeCache.get(node); +} + +function printHeritageClauses(path, options, print, listName) { const { node } = path; if (!isNonEmptyArray(node[listName])) { return ""; @@ -189,7 +242,7 @@ function printHeritageClauses(path, options, print, listName, groupMode) { printedLeadingComments, heritageClausesDoc, ]; - if (groupMode) { + if (shouldPrintClassInGroupMode(path)) { return [line, group(printed)]; } return [" ", printed]; diff --git a/tests/format/flow/class/heritage-break/__snapshots__/format.test.js.snap b/tests/format/flow/class/heritage-break/__snapshots__/format.test.js.snap new file mode 100644 index 000000000..69277b8a4 --- /dev/null +++ b/tests/format/flow/class/heritage-break/__snapshots__/format.test.js.snap @@ -0,0 +1,98 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`member-expression-like.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] +printWidth: 80 + | printWidth +=====================================input====================================== +class A_long_long_long_long_long_long_long_long_name1 + extends eslint.Rule.RuleModule {} + +class Short + extends eslint.Rule.RuleModule {} + +class A_long_long_long_long_long_long_long_long_name12 + extends eslint.Rule?.RuleModule {} + +interface A_long_long_long_long_long_long_long_long_name2 + extends eslint.Rule.RuleModule {} + +declare class A_long_long_long_long_long_long_long_long_name2 + mixins eslint.Rule.RuleModule {} + +declare class A_long_long_long_long_long_long_long_long_name3 + extends eslint.Rule.RuleModule + mixins eslint.Rule.RuleModule {} + +=====================================output===================================== +class A_long_long_long_long_long_long_long_long_name1 + extends eslint.Rule.RuleModule {} + +class Short extends eslint.Rule.RuleModule {} + +class A_long_long_long_long_long_long_long_long_name12 + extends eslint.Rule?.RuleModule {} + +interface A_long_long_long_long_long_long_long_long_name2 + extends eslint.Rule.RuleModule {} + +declare class A_long_long_long_long_long_long_long_long_name2 + mixins eslint.Rule.RuleModule {} + +declare class A_long_long_long_long_long_long_long_long_name3 + extends eslint.Rule.RuleModule + mixins eslint.Rule.RuleModule {} + +================================================================================ +`; + +exports[`type-arguments.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] +printWidth: 80 + | printWidth +=====================================input====================================== +class CommentBox extends React.Component< + { url: string; pollInterval: number }, + CommentData +> {} + +interface CommentBox extends React.Component< + { url: string; pollInterval: number }, + CommentData +> {} + +declare class CommentBox extends React.Component< + { url: string; pollInterval: number }, + CommentData +> {} + +declare class CommentBox mixins React.Component< + { url: string; pollInterval: number }, + CommentData +> {} + +=====================================output===================================== +class CommentBox extends React.Component< + { url: string, pollInterval: number }, + CommentData, +> {} + +interface CommentBox extends React.Component< + { url: string, pollInterval: number }, + CommentData, +> {} + +declare class CommentBox extends React.Component< + { url: string, pollInterval: number }, + CommentData, +> {} + +declare class CommentBox mixins React.Component< + { url: string, pollInterval: number }, + CommentData, +> {} + +================================================================================ +`; diff --git a/tests/format/flow/class/heritage-break/format.test.js b/tests/format/flow/class/heritage-break/format.test.js new file mode 100644 index 000000000..55c339883 --- /dev/null +++ b/tests/format/flow/class/heritage-break/format.test.js @@ -0,0 +1 @@ +runFormatTest(import.meta, ["flow"]); diff --git a/tests/format/flow/class/heritage-break/member-expression-like.js b/tests/format/flow/class/heritage-break/member-expression-like.js new file mode 100644 index 000000000..73b068e5c --- /dev/null +++ b/tests/format/flow/class/heritage-break/member-expression-like.js @@ -0,0 +1,18 @@ +class A_long_long_long_long_long_long_long_long_name1 + extends eslint.Rule.RuleModule {} + +class Short + extends eslint.Rule.RuleModule {} + +class A_long_long_long_long_long_long_long_long_name12 + extends eslint.Rule?.RuleModule {} + +interface A_long_long_long_long_long_long_long_long_name2 + extends eslint.Rule.RuleModule {} + +declare class A_long_long_long_long_long_long_long_long_name2 + mixins eslint.Rule.RuleModule {} + +declare class A_long_long_long_long_long_long_long_long_name3 + extends eslint.Rule.RuleModule + mixins eslint.Rule.RuleModule {} diff --git a/tests/format/flow/class/heritage-break/type-arguments.js b/tests/format/flow/class/heritage-break/type-arguments.js new file mode 100644 index 000000000..95a6a3a2c --- /dev/null +++ b/tests/format/flow/class/heritage-break/type-arguments.js @@ -0,0 +1,19 @@ +class CommentBox extends React.Component< + { url: string; pollInterval: number }, + CommentData +> {} + +interface CommentBox extends React.Component< + { url: string; pollInterval: number }, + CommentData +> {} + +declare class CommentBox extends React.Component< + { url: string; pollInterval: number }, + CommentData +> {} + +declare class CommentBox mixins React.Component< + { url: string; pollInterval: number }, + CommentData +> {} diff --git a/tests/format/typescript/class-and-interface/heritage-break/__snapshots__/format.test.js.snap b/tests/format/typescript/class-and-interface/heritage-break/__snapshots__/format.test.js.snap new file mode 100644 index 000000000..4cae483f3 --- /dev/null +++ b/tests/format/typescript/class-and-interface/heritage-break/__snapshots__/format.test.js.snap @@ -0,0 +1,88 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`member-expression-like.ts format 1`] = ` +====================================options===================================== +parsers: ["typescript"] +printWidth: 80 + | printWidth +=====================================input====================================== +class A_long_long_long_long_long_long_long_long_name1 + extends eslint.Rule.RuleModule {} + +class Short + extends eslint.Rule.RuleModule {} + +class A_long_long_long_long_long_long_long_long_name12 + extends eslint.Rule?.RuleModule {} + +interface A_long_long_long_long_long_long_long_long_name2 + extends eslint.Rule.RuleModule {} + +class A_long_long_long_long_long_long_long_long_name2 + implements eslint.Rule.RuleModule {} + +class A_long_long_long_long_long_long_long_long_name3 + extends eslint.Rule.RuleModule + implements eslint.Rule.RuleModule {} + +=====================================output===================================== +class A_long_long_long_long_long_long_long_long_name1 + extends eslint.Rule.RuleModule {} + +class Short extends eslint.Rule.RuleModule {} + +class A_long_long_long_long_long_long_long_long_name12 + extends eslint.Rule?.RuleModule {} + +interface A_long_long_long_long_long_long_long_long_name2 + extends eslint.Rule.RuleModule {} + +class A_long_long_long_long_long_long_long_long_name2 + implements eslint.Rule.RuleModule {} + +class A_long_long_long_long_long_long_long_long_name3 + extends eslint.Rule.RuleModule + implements eslint.Rule.RuleModule {} + +================================================================================ +`; + +exports[`type-arguments.ts format 1`] = ` +====================================options===================================== +parsers: ["typescript"] +printWidth: 80 + | printWidth +=====================================input====================================== +class CommentBox extends React.Component< + { url: string; pollInterval: number }, + CommentData +> {} + +interface CommentBox extends React.Component< + { url: string; pollInterval: number }, + CommentData +> {} + +declare class CommentBox implements React.Component< + { url: string; pollInterval: number }, + CommentData +> {} + +=====================================output===================================== +class CommentBox extends React.Component< + { url: string; pollInterval: number }, + CommentData +> {} + +interface CommentBox extends React.Component< + { url: string; pollInterval: number }, + CommentData +> {} + +declare class CommentBox implements React.Component< + { url: string; pollInterval: number }, + CommentData +> {} + +================================================================================ +`; diff --git a/tests/format/typescript/class-and-interface/heritage-break/format.test.js b/tests/format/typescript/class-and-interface/heritage-break/format.test.js new file mode 100644 index 000000000..116dcbd78 --- /dev/null +++ b/tests/format/typescript/class-and-interface/heritage-break/format.test.js @@ -0,0 +1 @@ +runFormatTest(import.meta, ["typescript"]); diff --git a/tests/format/typescript/class-and-interface/heritage-break/member-expression-like.ts b/tests/format/typescript/class-and-interface/heritage-break/member-expression-like.ts new file mode 100644 index 000000000..ed24d9c3f --- /dev/null +++ b/tests/format/typescript/class-and-interface/heritage-break/member-expression-like.ts @@ -0,0 +1,18 @@ +class A_long_long_long_long_long_long_long_long_name1 + extends eslint.Rule.RuleModule {} + +class Short + extends eslint.Rule.RuleModule {} + +class A_long_long_long_long_long_long_long_long_name12 + extends eslint.Rule?.RuleModule {} + +interface A_long_long_long_long_long_long_long_long_name2 + extends eslint.Rule.RuleModule {} + +class A_long_long_long_long_long_long_long_long_name2 + implements eslint.Rule.RuleModule {} + +class A_long_long_long_long_long_long_long_long_name3 + extends eslint.Rule.RuleModule + implements eslint.Rule.RuleModule {} diff --git a/tests/format/typescript/class-and-interface/heritage-break/type-arguments.ts b/tests/format/typescript/class-and-interface/heritage-break/type-arguments.ts new file mode 100644 index 000000000..9900737e3 --- /dev/null +++ b/tests/format/typescript/class-and-interface/heritage-break/type-arguments.ts @@ -0,0 +1,14 @@ +class CommentBox extends React.Component< + { url: string; pollInterval: number }, + CommentData +> {} + +interface CommentBox extends React.Component< + { url: string; pollInterval: number }, + CommentData +> {} + +declare class CommentBox implements React.Component< + { url: string; pollInterval: number }, + CommentData +> {}