diff --git a/packages/eslint-plugin/docs/rules/member-ordering.md b/packages/eslint-plugin/docs/rules/member-ordering.md index 7adde7ba9a6..454463bbb39 100644 --- a/packages/eslint-plugin/docs/rules/member-ordering.md +++ b/packages/eslint-plugin/docs/rules/member-ordering.md @@ -24,6 +24,7 @@ type OrderConfig = MemberType[] | SortedOrderConfig | 'never'; interface SortedOrderConfig { memberTypes?: MemberType[] | 'never'; + optionalityOrder?: 'optional-first' | 'required-first'; order: | 'alphabetically' | 'alphabetically-case-insensitive' @@ -44,9 +45,10 @@ You can configure `OrderConfig` options for: - **`interfaces`**?: override ordering specifically for interfaces - **`typeLiterals`**?: override ordering specifically for type literals -The `OrderConfig` settings for each kind of construct may configure sorting on one or both two levels: +The `OrderConfig` settings for each kind of construct may configure sorting on up to three levels: - **`memberTypes`**: organizing on member type groups such as methods vs. properties +- **`optionalityOrder`**: whether to put all optional members first or all required members first - **`order`**: organizing based on member names, such as alphabetically ### Groups @@ -902,6 +904,96 @@ interface Foo { } ``` +#### Sorting Optional Members First or Last + +The `optionalityOrder` option may be enabled to place all optional members in a group at the beginning or end of that group. + +This config places all optional members before all required members: + +```jsonc +// .eslintrc.json +{ + "rules": { + "@typescript-eslint/member-ordering": [ + "error", + { + "default": { + "optionalityOrder": "optional-first", + "order": "alphabetically" + } + } + ] + } +} +``` + + + +##### ❌ Incorrect + +```ts +interface Foo { + a: boolean; + b?: number; + c: string; +} +``` + +##### ✅ Correct + +```ts +interface Foo { + b?: number; + a: boolean; + c: string; +} +``` + + + +This config places all required members before all optional members: + +```jsonc +// .eslintrc.json +{ + "rules": { + "@typescript-eslint/member-ordering": [ + "error", + { + "default": { + "optionalityOrder": "required-first", + "order": "alphabetically" + } + } + ] + } +} +``` + + + +##### ❌ Incorrect + +```ts +interface Foo { + a: boolean; + b?: number; + c: string; +} +``` + +##### ✅ Correct + +```ts +interface Foo { + a: boolean; + c: string; + b?: number; +} +``` + + + ## All Supported Options ### Member Types (Granular Form) diff --git a/packages/eslint-plugin/src/rules/member-ordering.ts b/packages/eslint-plugin/src/rules/member-ordering.ts index 055ae1a3a63..f37d3620004 100644 --- a/packages/eslint-plugin/src/rules/member-ordering.ts +++ b/packages/eslint-plugin/src/rules/member-ordering.ts @@ -48,13 +48,15 @@ type Order = AlphabeticalOrder | 'as-written'; interface SortedOrderConfig { memberTypes?: MemberType[] | 'never'; + optionalityOrder?: OptionalityOrder; order: Order; - required?: 'first' | 'last'; } type OrderConfig = MemberType[] | SortedOrderConfig | 'never'; type Member = TSESTree.ClassElement | TSESTree.TypeElement; +type OptionalityOrder = 'optional-first' | 'required-first'; + export type Options = [ { default?: OrderConfig; @@ -103,9 +105,9 @@ const objectConfig = (memberTypes: MemberType[]): JSONSchema.JSONSchema4 => ({ 'natural-case-insensitive', ], }, - required: { + optionalityOrder: { type: 'string', - enum: ['first', 'last'], + enum: ['optional-first', 'required-first'], }, }, additionalProperties: false, @@ -723,12 +725,13 @@ export default util.createRule({ * on the given 'required' parameter. * * @param members Members to be validated. + * @param optionalityOrder Where to place optional members, if not intermixed. * * @return True if all required and optional members are correctly sorted. */ function checkRequiredOrder( members: Member[], - required: 'first' | 'last' | undefined, + optionalityOrder: OptionalityOrder | undefined, ): boolean { const switchIndex = members.findIndex( (member, i) => @@ -741,14 +744,18 @@ export default util.createRule({ loc: member.loc, data: { member: getMemberName(member, context.getSourceCode()), - optionalOrRequired: required === 'first' ? 'required' : 'optional', + optionalOrRequired: + optionalityOrder === 'optional-first' ? 'required' : 'optional', }, }); - // if the optionality of the first item is correct (based on required) + // if the optionality of the first item is correct (based on optionalityOrder) // then the first 0 inclusive to switchIndex exclusive members all // have the correct optionality - if (isMemberOptional(members[0]) !== (required === 'last')) { + if ( + isMemberOptional(members[0]) !== + (optionalityOrder === 'required-first') + ) { report(members[0]); return false; } @@ -785,7 +792,7 @@ export default util.createRule({ // Standardize config let order: Order | undefined; let memberTypes: string | MemberType[] | undefined; - let required: 'first' | 'last' | undefined; + let optionalityOrder: OptionalityOrder | undefined; // returns true if everything is good and false if an error was reported const checkOrder = (memberSet: Member[]): boolean => { @@ -821,10 +828,10 @@ export default util.createRule({ } else { order = orderConfig.order; memberTypes = orderConfig.memberTypes; - required = orderConfig.required; + optionalityOrder = orderConfig.optionalityOrder; } - if (!required) { + if (!optionalityOrder) { checkOrder(members); return; } @@ -835,7 +842,7 @@ export default util.createRule({ ); if (switchIndex !== -1) { - if (!checkRequiredOrder(members, required)) { + if (!checkRequiredOrder(members, optionalityOrder)) { return; } checkOrder(members.slice(0, switchIndex)); diff --git a/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-required.test.ts b/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-optionalMembers.test.ts similarity index 86% rename from packages/eslint-plugin/tests/rules/member-ordering/member-ordering-required.test.ts rename to packages/eslint-plugin/tests/rules/member-ordering/member-ordering-optionalMembers.test.ts index 6e88af70e04..9c72fc7322a 100644 --- a/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-required.test.ts +++ b/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-optionalMembers.test.ts @@ -10,7 +10,7 @@ const ruleTester = new RuleTester({ const grouped: TSESLint.RunTests = { valid: [ - // required - first + // optionalityOrder - optional-first { code: ` interface X { @@ -24,7 +24,7 @@ interface X { default: { memberTypes: 'never', order: 'alphabetically', - required: 'first', + optionalityOrder: 'optional-first', }, }, ], @@ -42,7 +42,7 @@ interface X { default: { memberTypes: 'never', order: 'as-written', - required: 'first', + optionalityOrder: 'optional-first', }, }, ], @@ -60,7 +60,7 @@ interface X { default: { memberTypes: 'never', order: 'as-written', - required: 'first', + optionalityOrder: 'optional-first', }, }, ], @@ -78,7 +78,7 @@ class X { default: { memberTypes: 'never', order: 'alphabetically', - required: 'first', + optionalityOrder: 'optional-first', }, }, ], @@ -96,7 +96,7 @@ class X { default: { memberTypes: 'never', order: 'alphabetically', - required: 'first', + optionalityOrder: 'optional-first', }, }, ], @@ -114,7 +114,7 @@ class X { default: { memberTypes: 'never', order: 'alphabetically', - required: 'first', + optionalityOrder: 'optional-first', }, }, ], @@ -132,7 +132,7 @@ class X { default: { memberTypes: 'never', order: 'alphabetically', - required: 'first', + optionalityOrder: 'optional-first', }, }, ], @@ -150,7 +150,7 @@ interface X { default: { memberTypes: 'never', order: 'alphabetically', - required: 'first', + optionalityOrder: 'optional-first', }, }, ], @@ -169,12 +169,12 @@ interface X { default: { memberTypes: 'never', order: 'alphabetically', - required: 'first', + optionalityOrder: 'optional-first', }, }, ], }, - // required - last + // optionalityOrder - required-first { code: ` interface X { @@ -188,7 +188,7 @@ interface X { default: { memberTypes: 'never', order: 'alphabetically', - required: 'last', + optionalityOrder: 'required-first', }, }, ], @@ -206,7 +206,7 @@ interface X { default: { memberTypes: 'never', order: 'as-written', - required: 'last', + optionalityOrder: 'required-first', }, }, ], @@ -224,7 +224,7 @@ interface X { default: { memberTypes: 'never', order: 'as-written', - required: 'last', + optionalityOrder: 'required-first', }, }, ], @@ -242,13 +242,13 @@ class X { default: { memberTypes: 'never', order: 'alphabetically', - required: 'last', + optionalityOrder: 'required-first', }, }, ], }, ], - // required - first + // optionalityOrder - optional-first invalid: [ { code: ` @@ -263,7 +263,7 @@ interface X { default: { memberTypes: 'never', order: 'alphabetically', - required: 'first', + optionalityOrder: 'optional-first', }, }, ], @@ -288,7 +288,7 @@ interface X { default: { memberTypes: ['call-signature', 'field', 'method'], order: 'as-written', - required: 'first', + optionalityOrder: 'optional-first', }, }, ], @@ -317,7 +317,7 @@ class X { default: { memberTypes: 'never', order: 'as-written', - required: 'first', + optionalityOrder: 'optional-first', }, }, ], @@ -333,7 +333,7 @@ class X { }, ], }, - // required - last + // optionalityOrder - required-first { code: ` interface X { @@ -347,7 +347,7 @@ interface X { default: { memberTypes: 'never', order: 'alphabetically', - required: 'last', + optionalityOrder: 'required-first', }, }, ], @@ -372,7 +372,7 @@ interface X { default: { memberTypes: ['call-signature', 'field', 'method'], order: 'as-written', - required: 'last', + optionalityOrder: 'required-first', }, }, ], @@ -405,7 +405,7 @@ class Test { default: { memberTypes: 'never', order: 'as-written', - required: 'last', + optionalityOrder: 'required-first', }, }, ], @@ -436,7 +436,7 @@ class Test { default: { memberTypes: 'never', order: 'as-written', - required: 'last', + optionalityOrder: 'required-first', }, }, ],