From 6af657708ae4046ab47d5735a2c154726d953d2e Mon Sep 17 00:00:00 2001 From: Jakka Prihatna Date: Wed, 18 Jan 2023 12:43:54 +0700 Subject: [PATCH 1/5] feat(eslint-plugin): [member-ordering] add support for grouping readonly fields --- .../docs/rules/member-ordering.md | 39 +- .../src/rules/member-ordering.ts | 72 +++- .../tests/rules/member-ordering.test.ts | 404 ++++++++++++++++++ 3 files changed, 505 insertions(+), 10 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/member-ordering.md b/packages/eslint-plugin/docs/rules/member-ordering.md index 919ec88186f..d276d5472cd 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'`) Member attributes may be joined with a `'-'` to combine into more specific groups. For example, `'public-field'` would come before `'private-field'`. @@ -1017,34 +1017,56 @@ The most explicit and granular form is the following: // 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 +1312,21 @@ 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 + // No grouping for index 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 81e0ae6484e..a0561d5ef30 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'; + type MemberKind = | 'call-signature' | 'constructor' + | ReadonlyType | 'field' | 'get' | 'method' @@ -19,7 +22,7 @@ type MemberKind = | 'signature' | 'static-initialization'; -type DecoratedMemberKind = 'field' | 'method' | 'get' | 'set'; +type DecoratedMemberKind = ReadonlyType | 'field' | 'method' | 'get' | 'set'; type NonCallableMemberKind = Exclude; @@ -259,6 +262,7 @@ const allMemberTypes = Array.from( ( [ 'signature', + 'readonly-field', 'field', 'method', 'call-signature', @@ -284,7 +288,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') @@ -329,6 +334,8 @@ const functionExpressions = [ * @param node the node to be evaluated. */ function getNodeType(node: Member): MemberKind | null { + const readonly = 'readonly' in node && node.readonly === true; + switch (node.type) { case AST_NODE_TYPES.TSAbstractMethodDefinition: case AST_NODE_TYPES.MethodDefinition: @@ -340,13 +347,15 @@ function getNodeType(node: Member): MemberKind | null { case AST_NODE_TYPES.TSConstructSignatureDeclaration: return 'constructor'; case AST_NODE_TYPES.TSAbstractPropertyDefinition: - return 'field'; + return readonly ? 'readonly-field' : 'field'; case AST_NODE_TYPES.PropertyDefinition: return node.value && functionExpressions.includes(node.value.type) ? 'method' + : readonly + ? 'readonly-field' : 'field'; case AST_NODE_TYPES.TSPropertySignature: - return 'field'; + return readonly ? 'readonly-field' : 'field'; case AST_NODE_TYPES.TSIndexSignature: return 'signature'; case AST_NODE_TYPES.StaticBlock: @@ -514,13 +523,20 @@ 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') { + // if its readonly-field, push also its superset type + memberGroups.push(`${accessibility}-decorated-field`); + memberGroups.push(`decorated-field`); + } } if (type !== 'signature' && type !== 'static-initialization') { @@ -528,13 +544,27 @@ function getRank( // Constructors have no scope memberGroups.push(`${accessibility}-${scope}-${type}`); memberGroups.push(`${scope}-${type}`); + + if (type === 'readonly-field') { + // if its readonly-field, push also its superset type + memberGroups.push(`${accessibility}-${scope}-field`); + memberGroups.push(`${scope}-field`); + } } memberGroups.push(`${accessibility}-${type}`); + if (type === 'readonly-field') { + // if its readonly-field, push also its superset type + memberGroups.push(`${accessibility}-field`); + } } } memberGroups.push(type); + if (type === 'readonly-field') { + // if its readonly-field, push also its superset type + memberGroups.push('field'); + } // ...then get the rank order for those member groups based on the node return getRankOrder(memberGroups, orderConfig); @@ -621,15 +651,39 @@ export default util.createRule({ interfaces: { oneOf: [ neverConfig, - arrayConfig(['signature', 'field', 'method', 'constructor']), - objectConfig(['signature', 'field', 'method', 'constructor']), + arrayConfig([ + 'signature', + 'readonly-field', + 'field', + 'method', + 'constructor', + ]), + objectConfig([ + 'signature', + 'readonly-field', + 'field', + 'method', + 'constructor', + ]), ], }, typeLiterals: { oneOf: [ neverConfig, - arrayConfig(['signature', 'field', 'method', 'constructor']), - objectConfig(['signature', 'field', 'method', 'constructor']), + arrayConfig([ + 'signature', + 'readonly-field', + 'field', + 'method', + 'constructor', + ]), + objectConfig([ + '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..1ce9ae0b76f 100644 --- a/packages/eslint-plugin/tests/rules/member-ordering.test.ts +++ b/packages/eslint-plugin/tests/rules/member-ordering.test.ts @@ -1648,6 +1648,291 @@ 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'], + ], + }, + ], + }, ], invalid: [ { @@ -4410,6 +4695,125 @@ 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, + }, + ], + }, ], }; From 6b95451fb5a8c184d19944c71577fdb3b687d299 Mon Sep 17 00:00:00 2001 From: Jakka Prihatna Date: Tue, 24 Jan 2023 08:24:37 +0700 Subject: [PATCH 2/5] refactor: inline readonly assertions --- packages/eslint-plugin/src/rules/member-ordering.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/eslint-plugin/src/rules/member-ordering.ts b/packages/eslint-plugin/src/rules/member-ordering.ts index a0561d5ef30..dd818e813de 100644 --- a/packages/eslint-plugin/src/rules/member-ordering.ts +++ b/packages/eslint-plugin/src/rules/member-ordering.ts @@ -334,8 +334,6 @@ const functionExpressions = [ * @param node the node to be evaluated. */ function getNodeType(node: Member): MemberKind | null { - const readonly = 'readonly' in node && node.readonly === true; - switch (node.type) { case AST_NODE_TYPES.TSAbstractMethodDefinition: case AST_NODE_TYPES.MethodDefinition: @@ -347,15 +345,15 @@ function getNodeType(node: Member): MemberKind | null { case AST_NODE_TYPES.TSConstructSignatureDeclaration: return 'constructor'; case AST_NODE_TYPES.TSAbstractPropertyDefinition: - return readonly ? 'readonly-field' : 'field'; + return node.readonly ? 'readonly-field' : 'field'; case AST_NODE_TYPES.PropertyDefinition: return node.value && functionExpressions.includes(node.value.type) ? 'method' - : readonly + : node.readonly ? 'readonly-field' : 'field'; case AST_NODE_TYPES.TSPropertySignature: - return readonly ? 'readonly-field' : 'field'; + return node.readonly ? 'readonly-field' : 'field'; case AST_NODE_TYPES.TSIndexSignature: return 'signature'; case AST_NODE_TYPES.StaticBlock: From 58ec08ac14e69bfb5c7644304300fc821d565bd0 Mon Sep 17 00:00:00 2001 From: Jakka Prihatna Date: Tue, 24 Jan 2023 08:26:55 +0700 Subject: [PATCH 3/5] chore: remove comments --- packages/eslint-plugin/src/rules/member-ordering.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/eslint-plugin/src/rules/member-ordering.ts b/packages/eslint-plugin/src/rules/member-ordering.ts index dd818e813de..36068b53842 100644 --- a/packages/eslint-plugin/src/rules/member-ordering.ts +++ b/packages/eslint-plugin/src/rules/member-ordering.ts @@ -531,7 +531,6 @@ function getRank( memberGroups.push(`decorated-${type}`); if (type === 'readonly-field') { - // if its readonly-field, push also its superset type memberGroups.push(`${accessibility}-decorated-field`); memberGroups.push(`decorated-field`); } @@ -544,7 +543,6 @@ function getRank( memberGroups.push(`${scope}-${type}`); if (type === 'readonly-field') { - // if its readonly-field, push also its superset type memberGroups.push(`${accessibility}-${scope}-field`); memberGroups.push(`${scope}-field`); } @@ -552,7 +550,6 @@ function getRank( memberGroups.push(`${accessibility}-${type}`); if (type === 'readonly-field') { - // if its readonly-field, push also its superset type memberGroups.push(`${accessibility}-field`); } } @@ -560,7 +557,6 @@ function getRank( memberGroups.push(type); if (type === 'readonly-field') { - // if its readonly-field, push also its superset type memberGroups.push('field'); } From 0278a34a8d5cc252f6047b49aa2d1334e90f5459 Mon Sep 17 00:00:00 2001 From: Jakka Prihatna Date: Tue, 24 Jan 2023 10:04:34 +0700 Subject: [PATCH 4/5] test: add more tests for readonly TSAbstractPropertyDefinition and TSPropertySignature --- .../tests/rules/member-ordering.test.ts | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/member-ordering.test.ts b/packages/eslint-plugin/tests/rules/member-ordering.test.ts index 1ce9ae0b76f..ccaeeb79fd7 100644 --- a/packages/eslint-plugin/tests/rules/member-ordering.test.ts +++ b/packages/eslint-plugin/tests/rules/member-ordering.test.ts @@ -1933,6 +1933,22 @@ abstract class Foo { }, ], }, + { + code: ` +interface Foo { + readonly A: string; + readonly B: string; + + C: string; + D: string; +} + `, + options: [ + { + default: ['readonly-field', 'field'], + }, + ], + }, ], invalid: [ { @@ -4814,6 +4830,94 @@ abstract class Foo { }, ], }, + { + 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, + }, + ], + }, ], }; From 9f2e32c4c8ff96701f0a69ca13e7ccdd367d8dff Mon Sep 17 00:00:00 2001 From: Jakka Prihatna Date: Tue, 24 Jan 2023 12:55:01 +0700 Subject: [PATCH 5/5] feat: add support for readonly signatures --- .../docs/rules/member-ordering.md | 6 +- .../src/rules/member-ordering.ts | 35 +++++-- .../tests/rules/member-ordering.test.ts | 99 +++++++++++++++++++ 3 files changed, 131 insertions(+), 9 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/member-ordering.md b/packages/eslint-plugin/docs/rules/member-ordering.md index d276d5472cd..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' | 'readonly-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,6 +1014,7 @@ The most explicit and granular form is the following: [ // Index signature "signature", + "readonly-signature", // Fields "public-static-field", @@ -1319,7 +1320,8 @@ It is possible to group fields by their `readonly` modifiers. ```jsonc [ // Index signature - // No grouping for 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"] diff --git a/packages/eslint-plugin/src/rules/member-ordering.ts b/packages/eslint-plugin/src/rules/member-ordering.ts index 36068b53842..9044073ab52 100644 --- a/packages/eslint-plugin/src/rules/member-ordering.ts +++ b/packages/eslint-plugin/src/rules/member-ordering.ts @@ -9,7 +9,7 @@ export type MessageIds = | 'incorrectOrder' | 'incorrectRequiredMembersOrder'; -type ReadonlyType = 'readonly-field'; +type ReadonlyType = 'readonly-field' | 'readonly-signature'; type MemberKind = | 'call-signature' @@ -22,9 +22,17 @@ type MemberKind = | 'signature' | 'static-initialization'; -type DecoratedMemberKind = ReadonlyType | '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'; @@ -34,7 +42,7 @@ type BaseMemberType = | MemberKind | `${Accessibility}-${Exclude< MemberKind, - 'signature' | 'static-initialization' + 'signature' | 'readonly-signature' | 'static-initialization' >}` | `${Accessibility}-decorated-${DecoratedMemberKind}` | `decorated-${DecoratedMemberKind}` @@ -261,6 +269,7 @@ export const defaultOrder: MemberType[] = [ const allMemberTypes = Array.from( ( [ + 'readonly-signature', 'signature', 'readonly-field', 'field', @@ -277,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' && @@ -300,6 +310,7 @@ const allMemberTypes = Array.from( if ( type !== 'constructor' && + type !== 'readonly-signature' && type !== 'signature' && type !== 'call-signature' ) { @@ -355,7 +366,7 @@ function getNodeType(node: Member): MemberKind | null { case AST_NODE_TYPES.TSPropertySignature: 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: @@ -536,7 +547,11 @@ function getRank( } } - 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}`); @@ -556,7 +571,9 @@ function getRank( } memberGroups.push(type); - if (type === 'readonly-field') { + if (type === 'readonly-signature') { + memberGroups.push('signature'); + } else if (type === 'readonly-field') { memberGroups.push('field'); } @@ -646,6 +663,7 @@ export default util.createRule({ oneOf: [ neverConfig, arrayConfig([ + 'readonly-signature', 'signature', 'readonly-field', 'field', @@ -653,6 +671,7 @@ export default util.createRule({ 'constructor', ]), objectConfig([ + 'readonly-signature', 'signature', 'readonly-field', 'field', @@ -665,6 +684,7 @@ export default util.createRule({ oneOf: [ neverConfig, arrayConfig([ + 'readonly-signature', 'signature', 'readonly-field', 'field', @@ -672,6 +692,7 @@ export default util.createRule({ 'constructor', ]), objectConfig([ + 'readonly-signature', 'signature', 'readonly-field', 'field', diff --git a/packages/eslint-plugin/tests/rules/member-ordering.test.ts b/packages/eslint-plugin/tests/rules/member-ordering.test.ts index ccaeeb79fd7..89f499d697b 100644 --- a/packages/eslint-plugin/tests/rules/member-ordering.test.ts +++ b/packages/eslint-plugin/tests/rules/member-ordering.test.ts @@ -1949,6 +1949,64 @@ interface Foo { }, ], }, + { + 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: [ { @@ -4918,6 +4976,47 @@ interface Foo { }, ], }, + { + 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, + }, + ], + }, ], };