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..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,9 +3,17 @@ 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' | 'preferRecord'; +type MessageIds = + | 'preferIndexSignature' + | 'preferIndexSignatureSuggestion' + | 'preferRecord'; type Options = ['index-signature' | 'record']; export default createRule({ @@ -17,8 +25,12 @@ 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.', + preferIndexSignatureSuggestion: + 'Change into an index signature instead of a record.', preferRecord: 'A record is preferred over an index signature.', }, schema: [ @@ -113,14 +125,27 @@ export default createRule({ return; } + const indexParam = params[0]; + + const shouldFix = + indexParam.type === AST_NODE_TYPES.TSStringKeyword || + indexParam.type === AST_NODE_TYPES.TSNumberKeyword || + indexParam.type === AST_NODE_TYPES.TSSymbolKeyword; + 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} }`); - }, + ...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} }`); + }, + }, + }), }); }, }), 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) {}',