From 9d3bdfcb2fe3d2b2c0b82d9587d52f0e2ef4e344 Mon Sep 17 00:00:00 2001 From: Jakka Prihatna Date: Mon, 13 Mar 2023 12:48:07 +0700 Subject: [PATCH] feat(eslint-plugin): [member-ordering] add support for grouping readonly fields (#6349) * feat(eslint-plugin): [member-ordering] add support for grouping readonly fields * refactor: inline readonly assertions * chore: remove comments * test: add more tests for readonly TSAbstractPropertyDefinition and TSPropertySignature * feat: add support for readonly signatures --------- Co-authored-by: Josh Goldberg --- .../docs/rules/member-ordering.md | 41 +- .../src/rules/member-ordering.ts | 95 ++- .../tests/rules/member-ordering.test.ts | 607 ++++++++++++++++++ 3 files changed, 729 insertions(+), 14 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/member-ordering.md b/packages/eslint-plugin/docs/rules/member-ordering.md index 919ec88186f..b4d1e217b2c 100644 --- a/packages/eslint-plugin/docs/rules/member-ordering.md +++ b/packages/eslint-plugin/docs/rules/member-ordering.md @@ -58,7 +58,7 @@ The supported member attributes are, in order: - **Accessibility** (`'public' | 'protected' | 'private' | '#private'`) - **Decoration** (`'decorated'`): Whether the member has an explicit accessibility decorator -- **Kind** (`'call-signature' | 'constructor' | 'field' | 'get' | 'method' | 'set' | 'signature'`) +- **Kind** (`'call-signature' | 'constructor' | 'field' | 'readonly-field' | 'get' | 'method' | 'set' | 'signature' | 'readonly-signature'`) Member attributes may be joined with a `'-'` to combine into more specific groups. For example, `'public-field'` would come before `'private-field'`. @@ -1014,37 +1014,60 @@ The most explicit and granular form is the following: [ // Index signature "signature", + "readonly-signature", // Fields "public-static-field", + "public-static-readonly-field", "protected-static-field", + "protected-static-readonly-field", "private-static-field", + "private-static-readonly-field", "#private-static-field", + "#private-static-readonly-field", "public-decorated-field", + "public-decorated-readonly-field", "protected-decorated-field", + "protected-decorated-readonly-field", "private-decorated-field", + "private-decorated-readonly-field", "public-instance-field", + "public-instance-readonly-field", "protected-instance-field", + "protected-instance-readonly-field", "private-instance-field", + "private-instance-readonly-field", "#private-instance-field", + "#private-instance-readonly-field", "public-abstract-field", + "public-abstract-readonly-field", "protected-abstract-field", + "protected-abstract-readonly-field", "public-field", + "public-readonly-field", "protected-field", + "protected-readonly-field", "private-field", + "private-readonly-field" "#private-field", + "#private-readonly-field" "static-field", + "static-readonly-field", "instance-field", + "instance-readonly-field" "abstract-field", + "abstract-readonly-field", "decorated-field", + "decorated-readonly-field", "field", + "readonly-field", // Static initialization "static-initialization", @@ -1290,6 +1313,22 @@ The third grouping option is to ignore both scope and accessibility. ] ``` +### Member Group Types (Readonly Fields) + +It is possible to group fields by their `readonly` modifiers. + +```jsonc +[ + // Index signature + "readonly-signature", + "signature", + + // Fields + "readonly-field", // = ["public-static-readonly-field", "protected-static-readonly-field", "private-static-readonly-field", "public-instance-readonly-field", "protected-instance-readonly-field", "private-instance-readonly-field", "public-abstract-readonly-field", "protected-abstract-readonly-field"] + "field" // = ["public-static-field", "protected-static-field", "private-static-field", "public-instance-field", "protected-instance-field", "private-instance-field", "public-abstract-field", "protected-abstract-field"] +] +``` + ### Grouping Different Member Types at the Same Rank It is also possible to group different member types at the same rank. diff --git a/packages/eslint-plugin/src/rules/member-ordering.ts b/packages/eslint-plugin/src/rules/member-ordering.ts index f8db5ade0ca..f6b76c4bfad 100644 --- a/packages/eslint-plugin/src/rules/member-ordering.ts +++ b/packages/eslint-plugin/src/rules/member-ordering.ts @@ -9,9 +9,12 @@ export type MessageIds = | 'incorrectOrder' | 'incorrectRequiredMembersOrder'; +type ReadonlyType = 'readonly-field' | 'readonly-signature'; + type MemberKind = | 'call-signature' | 'constructor' + | ReadonlyType | 'field' | 'get' | 'method' @@ -19,9 +22,17 @@ type MemberKind = | 'signature' | 'static-initialization'; -type DecoratedMemberKind = 'field' | 'method' | 'get' | 'set'; +type DecoratedMemberKind = + | Exclude + | 'field' + | 'method' + | 'get' + | 'set'; -type NonCallableMemberKind = Exclude; +type NonCallableMemberKind = Exclude< + MemberKind, + 'constructor' | 'signature' | 'readonly-signature' +>; type MemberScope = 'static' | 'instance' | 'abstract'; @@ -31,7 +42,7 @@ type BaseMemberType = | MemberKind | `${Accessibility}-${Exclude< MemberKind, - 'signature' | 'static-initialization' + 'signature' | 'readonly-signature' | 'static-initialization' >}` | `${Accessibility}-decorated-${DecoratedMemberKind}` | `decorated-${DecoratedMemberKind}` @@ -258,7 +269,9 @@ export const defaultOrder: MemberType[] = [ const allMemberTypes = Array.from( ( [ + 'readonly-signature', 'signature', + 'readonly-field', 'field', 'method', 'call-signature', @@ -273,6 +286,7 @@ const allMemberTypes = Array.from( (['public', 'protected', 'private', '#private'] as const).forEach( accessibility => { if ( + type !== 'readonly-signature' && type !== 'signature' && type !== 'static-initialization' && type !== 'call-signature' && @@ -284,7 +298,8 @@ const allMemberTypes = Array.from( // Only class instance fields, methods, get and set can have decorators attached to them if ( accessibility !== '#private' && - (type === 'field' || + (type === 'readonly-field' || + type === 'field' || type === 'method' || type === 'get' || type === 'set') @@ -295,6 +310,7 @@ const allMemberTypes = Array.from( if ( type !== 'constructor' && + type !== 'readonly-signature' && type !== 'signature' && type !== 'call-signature' ) { @@ -340,15 +356,17 @@ function getNodeType(node: Member): MemberKind | null { case AST_NODE_TYPES.TSConstructSignatureDeclaration: return 'constructor'; case AST_NODE_TYPES.TSAbstractPropertyDefinition: - return 'field'; + return node.readonly ? 'readonly-field' : 'field'; case AST_NODE_TYPES.PropertyDefinition: return node.value && functionExpressions.includes(node.value.type) ? 'method' + : node.readonly + ? 'readonly-field' : 'field'; case AST_NODE_TYPES.TSPropertySignature: - return 'field'; + return node.readonly ? 'readonly-field' : 'field'; case AST_NODE_TYPES.TSIndexSignature: - return 'signature'; + return node.readonly ? 'readonly-signature' : 'signature'; case AST_NODE_TYPES.StaticBlock: return 'static-initialization'; default: @@ -514,27 +532,50 @@ function getRank( const decorated = 'decorators' in node && node.decorators!.length > 0; if ( decorated && - (type === 'field' || + (type === 'readonly-field' || + type === 'field' || type === 'method' || type === 'get' || type === 'set') ) { memberGroups.push(`${accessibility}-decorated-${type}`); memberGroups.push(`decorated-${type}`); + + if (type === 'readonly-field') { + memberGroups.push(`${accessibility}-decorated-field`); + memberGroups.push(`decorated-field`); + } } - if (type !== 'signature' && type !== 'static-initialization') { + if ( + type !== 'readonly-signature' && + type !== 'signature' && + type !== 'static-initialization' + ) { if (type !== 'constructor') { // Constructors have no scope memberGroups.push(`${accessibility}-${scope}-${type}`); memberGroups.push(`${scope}-${type}`); + + if (type === 'readonly-field') { + memberGroups.push(`${accessibility}-${scope}-field`); + memberGroups.push(`${scope}-field`); + } } memberGroups.push(`${accessibility}-${type}`); + if (type === 'readonly-field') { + memberGroups.push(`${accessibility}-field`); + } } } memberGroups.push(type); + if (type === 'readonly-signature') { + memberGroups.push('signature'); + } else if (type === 'readonly-field') { + memberGroups.push('field'); + } // ...then get the rank order for those member groups based on the node return getRankOrder(memberGroups, orderConfig); @@ -621,15 +662,43 @@ export default util.createRule({ interfaces: { oneOf: [ neverConfig, - arrayConfig(['signature', 'field', 'method', 'constructor']), - objectConfig(['signature', 'field', 'method', 'constructor']), + arrayConfig([ + 'readonly-signature', + 'signature', + 'readonly-field', + 'field', + 'method', + 'constructor', + ]), + objectConfig([ + 'readonly-signature', + 'signature', + 'readonly-field', + 'field', + 'method', + 'constructor', + ]), ], }, typeLiterals: { oneOf: [ neverConfig, - arrayConfig(['signature', 'field', 'method', 'constructor']), - objectConfig(['signature', 'field', 'method', 'constructor']), + arrayConfig([ + 'readonly-signature', + 'signature', + 'readonly-field', + 'field', + 'method', + 'constructor', + ]), + objectConfig([ + 'readonly-signature', + 'signature', + 'readonly-field', + 'field', + 'method', + 'constructor', + ]), ], }, }, diff --git a/packages/eslint-plugin/tests/rules/member-ordering.test.ts b/packages/eslint-plugin/tests/rules/member-ordering.test.ts index 3305bebfa8c..89f499d697b 100644 --- a/packages/eslint-plugin/tests/rules/member-ordering.test.ts +++ b/packages/eslint-plugin/tests/rules/member-ordering.test.ts @@ -1648,6 +1648,365 @@ class Foo { }, ], }, + { + code: ` +class Foo { + readonly B: string; + readonly A: string; + constructor() {} + D: string; + C: string; + E(): void; + F(): void; +} + `, + options: [{ default: ['readonly-field', 'field'] }], + }, + { + code: ` +class Foo { + A: string; + B: string; + private readonly C: string; + private D: string; +} + `, + options: [ + { + default: ['public-field', 'private-readonly-field', 'private-field'], + }, + ], + }, + { + code: ` +class Foo { + private readonly A: string; + constructor() {} + private B: string; +} + `, + options: [ + { + default: ['private-readonly-field', 'constructor', 'private-field'], + }, + ], + }, + { + code: ` +class Foo { + public A: string; + private readonly B: string; +} + `, + options: [ + { + default: ['private-readonly-field', 'public-instance-field'], + classes: ['public-instance-field', 'private-readonly-field'], + }, + ], + }, + // class + ignore readonly + { + code: ` +class Foo { + public A(): string; + public B(): string; + public C(): string; + + d: string; + readonly e: string; + f: string; +} + `, + options: [ + { + default: ['public-method', 'field'], + }, + ], + }, + { + code: ` +class Foo { + private readonly A: string; + readonly B: string; + C: string; + constructor() {} + @Dec() private D: string; + private E(): void; + set F() {} + G(): void; +} + `, + options: [ + { + default: [ + 'readonly-field', + 'public-field', + 'constructor', + ['private-decorated-field', 'public-set', 'private-method'], + 'public-method', + ], + }, + ], + }, + { + code: ` +abstract class Foo { + public static readonly SA: string; + protected static readonly SB: string; + private static readonly SC: string; + static readonly #SD: string; + + public readonly IA: string; + protected readonly IB: string; + private readonly IC: string; + readonly #ID: string; + + public abstract readonly AA: string; + protected abstract readonly AB: string; + + @Dec public readonly DA: string; + @Dec protected readonly DB: string; + @Dec private readonly DC: string; +} + `, + options: [ + { + default: [ + 'public-static-readonly-field', + 'protected-static-readonly-field', + 'private-static-readonly-field', + '#private-static-readonly-field', + + 'static-readonly-field', + + 'public-instance-readonly-field', + 'protected-instance-readonly-field', + 'private-instance-readonly-field', + '#private-instance-readonly-field', + + 'instance-readonly-field', + + 'public-readonly-field', + 'protected-readonly-field', + 'private-readonly-field', + '#private-readonly-field', + + 'readonly-field', + + 'public-abstract-readonly-field', + 'protected-abstract-readonly-field', + + 'abstract-readonly-field', + + 'public-decorated-readonly-field', + 'protected-decorated-readonly-field', + 'private-decorated-readonly-field', + 'decorated-readonly-field', + ], + }, + ], + }, + { + code: ` +abstract class Foo { + @Dec public readonly DA: string; + @Dec protected readonly DB: string; + @Dec private readonly DC: string; + + public static readonly SA: string; + protected static readonly SB: string; + private static readonly SC: string; + static readonly #SD: string; + + public readonly IA: string; + protected readonly IB: string; + private readonly IC: string; + readonly #ID: string; + + public abstract readonly AA: string; + protected abstract readonly AB: string; +} + `, + options: [ + { + default: [ + 'decorated-readonly-field', + 'static-readonly-field', + 'instance-readonly-field', + 'abstract-readonly-field', + ], + }, + ], + }, + { + code: ` +abstract class Foo { + @Dec public readonly DA: string; + @Dec protected readonly DB: string; + @Dec private readonly DC: string; + + public static readonly SA: string; + public readonly IA: string; + public abstract readonly AA: string; + + protected static readonly SB: string; + protected readonly IB: string; + protected abstract readonly AB: string; + + private static readonly SC: string; + private readonly IC: string; + + static readonly #SD: string; + readonly #ID: string; +} + `, + options: [ + { + default: [ + 'decorated-readonly-field', + 'public-readonly-field', + 'protected-readonly-field', + 'private-readonly-field', + ], + }, + ], + }, + { + code: ` +abstract class Foo { + @Dec public readonly DA: string; + @Dec protected readonly DB: string; + @Dec private readonly DC: string; + + public static readonly SA: string; + public readonly IA: string; + static readonly #SD: string; + readonly #ID: string; + + protected static readonly SB: string; + protected readonly IB: string; + + private static readonly SC: string; + private readonly IC: string; + + public abstract readonly AA: string; + protected abstract readonly AB: string; +} + `, + options: [ + { + default: [ + 'decorated-readonly-field', + ['public-readonly-field', 'readonly-field'], + 'protected-readonly-field', + 'private-readonly-field', + 'abstract-readonly-field', + ], + }, + ], + }, + { + code: ` +abstract class Foo { + @Dec public readonly A: string; + @Dec public B: string; + + public readonly C: string; + public static readonly D: string; + public E: string; + public static F: string; + + static readonly #G: string; + readonly #H: string; + static #I: string; + #J: string; +} + `, + options: [ + { + default: [ + ['decorated-field', 'decorated-readonly-field'], + ['field', 'readonly-field'], + ['#private-field', '#private-readonly-field'], + ], + }, + ], + }, + { + code: ` +interface Foo { + readonly A: string; + readonly B: string; + + C: string; + D: string; +} + `, + options: [ + { + default: ['readonly-field', 'field'], + }, + ], + }, + { + code: ` +interface Foo { + readonly [i: string]: string; + readonly A: string; + + [i: number]: string; + B: string; +} + `, + options: [ + { + default: [ + 'readonly-signature', + 'readonly-field', + 'signature', + 'field', + ], + }, + ], + }, + { + code: ` +interface Foo { + readonly [i: string]: string; + [i: number]: string; + + readonly A: string; + B: string; +} + `, + options: [ + { + default: [ + 'readonly-signature', + 'signature', + 'readonly-field', + 'field', + ], + }, + ], + }, + { + code: ` +interface Foo { + readonly A: string; + B: string; + + [i: number]: string; + readonly [i: string]: string; +} + `, + options: [ + { + default: ['readonly-field', 'field', 'signature'], + }, + ], + }, ], invalid: [ { @@ -4410,6 +4769,254 @@ class Foo { }, ], }, + { + code: ` +// no accessibility === public +class Foo { + B: string; + readonly A: string = ''; + C: string = ''; + constructor() {} + D() {} + E() {} +} + `, + options: [{ default: ['readonly-field', 'field'] }], + errors: [ + { + messageId: 'incorrectGroupOrder', + data: { + name: 'A', + rank: 'field', + }, + line: 5, + column: 3, + }, + ], + }, + { + code: ` +class Foo { + A: string; + private C(): void; + constructor() {} + private readonly B: string; + set D() {} + E(): void; +} + `, + options: [ + { + default: [ + 'private-readonly-field', + 'public-field', + 'constructor', + ['public-set', 'private-method'], + 'public-method', + ], + }, + ], + errors: [ + { + messageId: 'incorrectGroupOrder', + data: { + name: 'constructor', + rank: 'public set, private method', + }, + line: 5, + column: 3, + }, + { + messageId: 'incorrectGroupOrder', + data: { + name: 'B', + rank: 'public field', + }, + line: 6, + column: 3, + }, + ], + }, + { + code: ` +abstract class Foo { + @Dec public readonly A: string; + public readonly B: string; + public static readonly C: string; + static readonly #D: string; + readonly #E: string; + + @Dec public F: string; + public G: string; + public static H: string; + static readonly #I: string; + readonly #J: string; +} + `, + options: [ + { + default: ['decorated-field', 'readonly-field', 'field'], + }, + ], + errors: [ + { + messageId: 'incorrectGroupOrder', + data: { + name: 'F', + rank: 'readonly field', + }, + line: 9, + column: 3, + }, + { + messageId: 'incorrectGroupOrder', + data: { + name: 'I', + rank: 'field', + }, + line: 12, + column: 3, + }, + { + messageId: 'incorrectGroupOrder', + data: { + name: 'J', + rank: 'field', + }, + line: 13, + column: 3, + }, + ], + }, + { + code: ` +abstract class Foo { + @Dec public readonly DA: string; + @Dec protected readonly DB: string; + @Dec private readonly DC: string; + + public static readonly SA: string; + protected static readonly SB: string; + private static readonly SC: string; + static readonly #SD: string; + + public readonly IA: string; + protected readonly IB: string; + private readonly IC: string; + readonly #ID: string; + + public abstract readonly AA: string; + protected abstract readonly AB: string; +} + `, + options: [ + { + default: [ + 'decorated-readonly-field', + 'abstract-readonly-field', + 'static-readonly-field', + 'instance-readonly-field', + ], + }, + ], + errors: [ + { + messageId: 'incorrectGroupOrder', + data: { + name: 'AA', + rank: 'static readonly field', + }, + line: 17, + column: 3, + }, + { + messageId: 'incorrectGroupOrder', + data: { + name: 'AB', + rank: 'static readonly field', + }, + line: 18, + column: 3, + }, + ], + }, + { + code: ` +interface Foo { + readonly A: string; + readonly B: string; + + C: string; + D: string; +} + `, + options: [ + { + default: ['field', 'readonly-field'], + }, + ], + errors: [ + { + messageId: 'incorrectGroupOrder', + data: { + name: 'C', + rank: 'readonly field', + }, + line: 6, + column: 3, + }, + { + messageId: 'incorrectGroupOrder', + data: { + name: 'D', + rank: 'readonly field', + }, + line: 7, + column: 3, + }, + ], + }, + { + code: ` +interface Foo { + [i: number]: string; + readonly [i: string]: string; + + A: string; + readonly B: string; +} + `, + options: [ + { + default: [ + 'readonly-signature', + 'signature', + 'readonly-field', + 'field', + ], + }, + ], + errors: [ + { + messageId: 'incorrectGroupOrder', + data: { + name: 'i', + rank: 'signature', + }, + line: 4, + column: 3, + }, + { + messageId: 'incorrectGroupOrder', + data: { + name: 'B', + rank: 'field', + }, + line: 7, + column: 3, + }, + ], + }, ], };