From c95044feb103fb4bdc1e1a258b542a77484024f6 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Wed, 11 Dec 2024 21:51:04 +0200 Subject: [PATCH 1/3] initial implementation --- .../rules/consistent-indexed-object-style.ts | 41 ++++++++++++++++--- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts b/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts index e5ddecdd815f..dfd89e7a62f9 100644 --- a/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts +++ b/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts @@ -5,7 +5,10 @@ import { AST_NODE_TYPES, ASTUtils } from '@typescript-eslint/utils'; import { createRule, isParenthesized, nullThrows } from '../util'; -type MessageIds = 'preferIndexSignature' | 'preferRecord'; +type MessageIds = + | 'preferIndexSignature' + | 'preferIndexSignatureSuggestion' + | 'preferRecord'; type Options = ['index-signature' | 'record']; export default createRule({ @@ -17,8 +20,11 @@ export default createRule({ recommended: 'stylistic', }, fixable: 'code', + hasSuggestions: true, messages: { preferIndexSignature: 'An index signature is preferred over a record.', + preferIndexSignatureSuggestion: + 'Change into an index signature instead of a record.', preferRecord: 'A record is preferred over an index signature.', }, schema: [ @@ -113,14 +119,37 @@ export default createRule({ return; } + const indexParam = params[0]; + + const shouldAutoFix = + indexParam.type === AST_NODE_TYPES.TSStringKeyword || + indexParam.type === AST_NODE_TYPES.TSNumberKeyword || + indexParam.type === AST_NODE_TYPES.TSSymbolKeyword; + + const fix: TSESLint.ReportFixFunction = fixer => { + const key = context.sourceCode.getText(params[0]); + const type = context.sourceCode.getText(params[1]); + return fixer.replaceText(node, `{ [key: ${key}]: ${type} }`); + }; + context.report({ node, messageId: 'preferIndexSignature', - fix(fixer) { - const key = context.sourceCode.getText(params[0]); - const type = context.sourceCode.getText(params[1]); - return fixer.replaceText(node, `{ [key: ${key}]: ${type} }`); - }, + fix: shouldAutoFix + ? fixer => { + const key = context.sourceCode.getText(params[0]); + const type = context.sourceCode.getText(params[1]); + return fixer.replaceText(node, `{ [key: ${key}]: ${type} }`); + } + : null, + suggest: shouldAutoFix + ? null + : [ + { + messageId: 'preferIndexSignatureSuggestion', + fix, + }, + ], }); }, }), From 2a046b6e84964f45e96cf3306dd24fb360011501 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Wed, 11 Dec 2024 21:59:55 +0200 Subject: [PATCH 2/3] add tests --- .../consistent-indexed-object-style.test.ts | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/consistent-indexed-object-style.test.ts b/packages/eslint-plugin/tests/rules/consistent-indexed-object-style.test.ts index e2a1ac63ba13..8441a720e326 100644 --- a/packages/eslint-plugin/tests/rules/consistent-indexed-object-style.test.ts +++ b/packages/eslint-plugin/tests/rules/consistent-indexed-object-style.test.ts @@ -401,6 +401,57 @@ interface Foo { output: 'type Foo = Generic<{ [key: string]: any }>;', }, + // Record with an index node that may potentially break index-signature style + { + code: 'type Foo = Record;', + errors: [ + { + column: 12, + line: 1, + messageId: 'preferIndexSignature', + suggestions: [ + { + messageId: 'preferIndexSignatureSuggestion', + output: 'type Foo = { [key: string | number]: any };', + }, + ], + }, + ], + options: ['index-signature'], + }, + { + code: "type Foo = Record, any>;", + errors: [ + { + column: 12, + line: 1, + messageId: 'preferIndexSignature', + suggestions: [ + { + messageId: 'preferIndexSignatureSuggestion', + output: + "type Foo = { [key: Exclude<'a' | 'b' | 'c', 'a'>]: any };", + }, + ], + }, + ], + options: ['index-signature'], + }, + + // Record with valid index node should use an auto-fix + { + code: 'type Foo = Record;', + errors: [{ column: 12, line: 1, messageId: 'preferIndexSignature' }], + options: ['index-signature'], + output: 'type Foo = { [key: number]: any };', + }, + { + code: 'type Foo = Record;', + errors: [{ column: 12, line: 1, messageId: 'preferIndexSignature' }], + options: ['index-signature'], + output: 'type Foo = { [key: symbol]: any };', + }, + // Function types { code: 'function foo(arg: Record) {}', From 023265db171019fb8aa45d84a5c54a71b381e2b5 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Tue, 17 Dec 2024 22:10:36 +0200 Subject: [PATCH 3/3] use getFixOrSuggest --- .../rules/consistent-indexed-object-style.ts | 36 +++++++++---------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts b/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts index dfd89e7a62f9..15ab691b38dd 100644 --- a/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts +++ b/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts @@ -3,7 +3,12 @@ import type { ReportFixFunction } from '@typescript-eslint/utils/ts-eslint'; import { AST_NODE_TYPES, ASTUtils } from '@typescript-eslint/utils'; -import { createRule, isParenthesized, nullThrows } from '../util'; +import { + createRule, + getFixOrSuggest, + isParenthesized, + nullThrows, +} from '../util'; type MessageIds = | 'preferIndexSignature' @@ -20,6 +25,7 @@ export default createRule({ recommended: 'stylistic', }, fixable: 'code', + // eslint-disable-next-line eslint-plugin/require-meta-has-suggestions -- suggestions are exposed through a helper. hasSuggestions: true, messages: { preferIndexSignature: 'An index signature is preferred over a record.', @@ -121,35 +127,25 @@ export default createRule({ const indexParam = params[0]; - const shouldAutoFix = + const shouldFix = indexParam.type === AST_NODE_TYPES.TSStringKeyword || indexParam.type === AST_NODE_TYPES.TSNumberKeyword || indexParam.type === AST_NODE_TYPES.TSSymbolKeyword; - const fix: TSESLint.ReportFixFunction = fixer => { - const key = context.sourceCode.getText(params[0]); - const type = context.sourceCode.getText(params[1]); - return fixer.replaceText(node, `{ [key: ${key}]: ${type} }`); - }; - context.report({ node, messageId: 'preferIndexSignature', - fix: shouldAutoFix - ? fixer => { + ...getFixOrSuggest({ + fixOrSuggest: shouldFix ? 'fix' : 'suggest', + suggestion: { + messageId: 'preferIndexSignatureSuggestion', + fix: fixer => { const key = context.sourceCode.getText(params[0]); const type = context.sourceCode.getText(params[1]); return fixer.replaceText(node, `{ [key: ${key}]: ${type} }`); - } - : null, - suggest: shouldAutoFix - ? null - : [ - { - messageId: 'preferIndexSignatureSuggestion', - fix, - }, - ], + }, + }, + }), }); }, }),