diff --git a/packages/eslint-plugin/docs/rules/no-magic-numbers.md b/packages/eslint-plugin/docs/rules/no-magic-numbers.md index b67aeae6788..936d9dd686e 100644 --- a/packages/eslint-plugin/docs/rules/no-magic-numbers.md +++ b/packages/eslint-plugin/docs/rules/no-magic-numbers.md @@ -36,6 +36,7 @@ interface Options extends BaseNoMagicNumbersOptions { ignoreEnums?: boolean; ignoreNumericLiteralTypes?: boolean; ignoreReadonlyClassProperties?: boolean; + ignoreTypeIndexes?: boolean; } const defaultOptions: Options = { @@ -43,6 +44,7 @@ const defaultOptions: Options = { ignoreEnums: false, ignoreNumericLiteralTypes: false, ignoreReadonlyClassProperties: false, + ignoreTypeIndexes: false, }; ``` @@ -118,6 +120,28 @@ class Foo { } ``` +### `ignoreTypeIndexes` + +A boolean to specify if numbers used to index types are okay. `false` by default. + +Examples of **incorrect** code for the `{ "ignoreTypeIndexes": false }` option: + +```ts +/*eslint @typescript-eslint/no-magic-numbers: ["error", { "ignoreTypeIndexes": false }]*/ + +type Foo = Bar[0]; +type Baz = Parameters[2]; +``` + +Examples of **correct** code for the `{ "ignoreTypeIndexes": true }` option: + +```ts +/*eslint @typescript-eslint/no-magic-numbers: ["error", { "ignoreTypeIndexes": true }]*/ + +type Foo = Bar[0]; +type Baz = Parameters[2]; +``` + Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/main/docs/rules/no-magic-numbers.md) diff --git a/packages/eslint-plugin/src/rules/no-magic-numbers.ts b/packages/eslint-plugin/src/rules/no-magic-numbers.ts index 7310123b85c..3307298cb54 100644 --- a/packages/eslint-plugin/src/rules/no-magic-numbers.ts +++ b/packages/eslint-plugin/src/rules/no-magic-numbers.ts @@ -24,6 +24,9 @@ const schema = util.deepMerge( ignoreReadonlyClassProperties: { type: 'boolean', }, + ignoreTypeIndexes: { + type: 'boolean', + }, }, }, ); @@ -56,29 +59,40 @@ export default util.createRule({ return { Literal(node): void { - // Check if the node is a TypeScript enum declaration - if (options.ignoreEnums && isParentTSEnumDeclaration(node)) { + // If it’s not a numeric literal we’re not interested + if (typeof node.value !== 'number' && typeof node.value !== 'bigint') { return; } + // This will be `true` if we’re configured to ignore this case (eg. it’s + // an enum and `ignoreEnums` is `true`). It will be `false` if we’re not + // configured to ignore this case. It will remain `undefined` if this is + // not one of our exception cases, and we’ll fall back to the base rule. + let isAllowed: boolean | undefined; + + // Check if the node is a TypeScript enum declaration + if (isParentTSEnumDeclaration(node)) { + isAllowed = options.ignoreEnums === true; + } // Check TypeScript specific nodes for Numeric Literal - if ( - options.ignoreNumericLiteralTypes && - typeof node.value === 'number' && - isTSNumericLiteralType(node) - ) { - return; + else if (isTSNumericLiteralType(node)) { + isAllowed = options.ignoreNumericLiteralTypes === true; + } + // Check if the node is a type index + else if (isAncestorTSIndexedAccessType(node)) { + isAllowed = options.ignoreTypeIndexes === true; } - // Check if the node is a readonly class property - if ( - (typeof node.value === 'number' || typeof node.value === 'bigint') && - isParentTSReadonlyPropertyDefinition(node) - ) { - if (options.ignoreReadonlyClassProperties) { - return; - } + else if (isParentTSReadonlyPropertyDefinition(node)) { + isAllowed = options.ignoreReadonlyClassProperties === true; + } + // If we’ve hit a case where the ignore option is true we can return now + if (isAllowed === true) { + return; + } + // If the ignore option is *not* set we can report it now + else if (isAllowed === false) { let fullNumberNode: TSESTree.Literal | TSESTree.UnaryExpression = node; let raw = node.raw; @@ -216,3 +230,25 @@ function isParentTSReadonlyPropertyDefinition(node: TSESTree.Literal): boolean { return false; } + +/** + * Checks if the node is part of a type indexed access (eg. Foo[4]) + * @param node the node to be validated. + * @returns true if the node is part of an indexed access + * @private + */ +function isAncestorTSIndexedAccessType(node: TSESTree.Literal): boolean { + // Handle unary expressions (eg. -4) + let ancestor = getLiteralParent(node); + + // Go up another level while we’re part of a type union (eg. 1 | 2) or + // intersection (eg. 1 & 2) + while ( + ancestor?.parent?.type === AST_NODE_TYPES.TSUnionType || + ancestor?.parent?.type === AST_NODE_TYPES.TSIntersectionType + ) { + ancestor = ancestor.parent; + } + + return ancestor?.parent?.type === AST_NODE_TYPES.TSIndexedAccessType; +} diff --git a/packages/eslint-plugin/tests/rules/no-magic-numbers.test.ts b/packages/eslint-plugin/tests/rules/no-magic-numbers.test.ts index 2bf16e99796..3be7590742a 100644 --- a/packages/eslint-plugin/tests/rules/no-magic-numbers.test.ts +++ b/packages/eslint-plugin/tests/rules/no-magic-numbers.test.ts @@ -58,6 +58,64 @@ class Foo { `, options: [{ ignoreReadonlyClassProperties: true }], }, + { + code: 'type Foo = Bar[0];', + options: [{ ignoreTypeIndexes: true }], + }, + { + code: 'type Foo = Bar[-1];', + options: [{ ignoreTypeIndexes: true }], + }, + { + code: 'type Foo = Bar[0xab];', + options: [{ ignoreTypeIndexes: true }], + }, + { + code: 'type Foo = Bar[5.6e1];', + options: [{ ignoreTypeIndexes: true }], + }, + { + code: 'type Foo = Bar[10n];', + options: [{ ignoreTypeIndexes: true }], + }, + { + code: 'type Foo = Bar[1 | -2];', + options: [{ ignoreTypeIndexes: true }], + }, + { + code: 'type Foo = Bar[1 & -2];', + options: [{ ignoreTypeIndexes: true }], + }, + { + code: 'type Foo = Bar[1 & number];', + options: [{ ignoreTypeIndexes: true }], + }, + { + code: 'type Foo = Bar[((1 & -2) | 3) | 4];', + options: [{ ignoreTypeIndexes: true }], + }, + { + code: 'type Foo = Parameters[2];', + options: [{ ignoreTypeIndexes: true }], + }, + { + code: "type Foo = Bar['baz'];", + options: [{ ignoreTypeIndexes: true }], + }, + { + code: "type Foo = Bar['baz'];", + options: [{ ignoreTypeIndexes: false }], + }, + { + code: ` +type Others = [['a'], ['b']]; + +type Foo = { + [K in keyof Others[0]]: Others[K]; +}; + `, + options: [{ ignoreTypeIndexes: true }], + }, ], invalid: [ @@ -268,5 +326,277 @@ class Foo { }, ], }, + { + code: 'type Foo = Bar[0];', + options: [{ ignoreTypeIndexes: false }], + errors: [ + { + messageId: 'noMagic', + data: { + raw: '0', + }, + line: 1, + column: 16, + }, + ], + }, + { + code: 'type Foo = Bar[-1];', + options: [{ ignoreTypeIndexes: false }], + errors: [ + { + messageId: 'noMagic', + data: { + raw: '-1', + }, + line: 1, + column: 16, + }, + ], + }, + { + code: 'type Foo = Bar[0xab];', + options: [{ ignoreTypeIndexes: false }], + errors: [ + { + messageId: 'noMagic', + data: { + raw: '0xab', + }, + line: 1, + column: 16, + }, + ], + }, + { + code: 'type Foo = Bar[5.6e1];', + options: [{ ignoreTypeIndexes: false }], + errors: [ + { + messageId: 'noMagic', + data: { + raw: '5.6e1', + }, + line: 1, + column: 16, + }, + ], + }, + { + code: 'type Foo = Bar[10n];', + options: [{ ignoreTypeIndexes: false }], + errors: [ + { + messageId: 'noMagic', + data: { + raw: '10n', + }, + line: 1, + column: 16, + }, + ], + }, + { + code: 'type Foo = Bar[1 | -2];', + options: [{ ignoreTypeIndexes: false }], + errors: [ + { + messageId: 'noMagic', + data: { + raw: '1', + }, + line: 1, + column: 16, + }, + { + messageId: 'noMagic', + data: { + raw: '-2', + }, + line: 1, + column: 20, + }, + ], + }, + { + code: 'type Foo = Bar[1 & -2];', + options: [{ ignoreTypeIndexes: false }], + errors: [ + { + messageId: 'noMagic', + data: { + raw: '1', + }, + line: 1, + column: 16, + }, + { + messageId: 'noMagic', + data: { + raw: '-2', + }, + line: 1, + column: 20, + }, + ], + }, + { + code: 'type Foo = Bar[1 & number];', + options: [{ ignoreTypeIndexes: false }], + errors: [ + { + messageId: 'noMagic', + data: { + raw: '1', + }, + line: 1, + column: 16, + }, + ], + }, + { + code: 'type Foo = Bar[((1 & -2) | 3) | 4];', + options: [{ ignoreTypeIndexes: false }], + errors: [ + { + messageId: 'noMagic', + data: { + raw: '1', + }, + line: 1, + column: 18, + }, + { + messageId: 'noMagic', + data: { + raw: '-2', + }, + line: 1, + column: 22, + }, + { + messageId: 'noMagic', + data: { + raw: '3', + }, + line: 1, + column: 28, + }, + { + messageId: 'noMagic', + data: { + raw: '4', + }, + line: 1, + column: 33, + }, + ], + }, + { + code: 'type Foo = Parameters[2];', + options: [{ ignoreTypeIndexes: false }], + errors: [ + { + messageId: 'noMagic', + data: { + raw: '2', + }, + line: 1, + column: 28, + }, + ], + }, + { + code: ` +type Others = [['a'], ['b']]; + +type Foo = { + [K in keyof Others[0]]: Others[K]; +}; + `, + options: [{ ignoreTypeIndexes: false }], + errors: [ + { + messageId: 'noMagic', + data: { + raw: '0', + }, + line: 5, + column: 22, + }, + ], + }, + { + code: ` +type Other = { + [0]: 3; +}; + +type Foo = { + [K in keyof Other]: \`\${K & number}\`; +}; + `, + options: [{ ignoreTypeIndexes: true }], + errors: [ + { + messageId: 'noMagic', + data: { + raw: '0', + }, + line: 3, + column: 4, + }, + { + messageId: 'noMagic', + data: { + raw: '3', + }, + line: 3, + column: 8, + }, + ], + }, + { + code: ` +type Foo = { + [K in 0 | 1 | 2]: 0; +}; + `, + options: [{ ignoreTypeIndexes: true }], + errors: [ + { + messageId: 'noMagic', + data: { + raw: '0', + }, + line: 3, + column: 9, + }, + { + messageId: 'noMagic', + data: { + raw: '1', + }, + line: 3, + column: 13, + }, + { + messageId: 'noMagic', + data: { + raw: '2', + }, + line: 3, + column: 17, + }, + { + messageId: 'noMagic', + data: { + raw: '0', + }, + line: 3, + column: 21, + }, + ], + }, ], }); diff --git a/packages/eslint-plugin/typings/eslint-rules.d.ts b/packages/eslint-plugin/typings/eslint-rules.d.ts index 4c7b71a67ae..08f17271735 100644 --- a/packages/eslint-plugin/typings/eslint-rules.d.ts +++ b/packages/eslint-plugin/typings/eslint-rules.d.ts @@ -319,6 +319,7 @@ declare module 'eslint/lib/rules/no-magic-numbers' { ignoreNumericLiteralTypes?: boolean; ignoreEnums?: boolean; ignoreReadonlyClassProperties?: boolean; + ignoreTypeIndexes?: boolean; }, ], {