Skip to content

Commit 0f773ef

Browse files
authored
feat: support TS syntax in no-magic-numbers (#19561)
1 parent 43d3975 commit 0f773ef

File tree

4 files changed

+1314
-67
lines changed

4 files changed

+1314
-67
lines changed

docs/src/rules/no-magic-numbers.md

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ data[100] = a;
128128
f(data[0]);
129129

130130
a = data[-0]; // same as data[0], -0 will be coerced to "0"
131+
a = data[+1]; // same as data[1], +1 will be coerced to "1"
131132

132133
a = data[0xAB];
133134

@@ -207,6 +208,7 @@ Examples of **correct** code for the `{ "ignoreClassFieldInitialValues": true }`
207208
class C {
208209
foo = 2;
209210
bar = -3;
211+
tux = +1;
210212
#baz = 4;
211213
static qux = 5;
212214
}
@@ -290,3 +292,131 @@ const dutyFreePrice = 100,
290292
```
291293

292294
:::
295+
296+
### ignoreEnums (TypeScript only)
297+
298+
Whether enums used in TypeScript are considered okay. `false` by default.
299+
300+
Examples of **incorrect** code for the `{ "ignoreEnums": false }` option:
301+
302+
::: incorrect
303+
304+
```ts
305+
/*eslint no-magic-numbers: ["error", { "ignoreEnums": false }]*/
306+
307+
enum foo {
308+
SECOND = 1000,
309+
}
310+
```
311+
312+
:::
313+
314+
Examples of **correct** code for the `{ "ignoreEnums": true }` option:
315+
316+
::: correct
317+
318+
```ts
319+
/*eslint no-magic-numbers: ["error", { "ignoreEnums": true }]*/
320+
321+
enum foo {
322+
SECOND = 1000,
323+
}
324+
```
325+
326+
:::
327+
328+
### ignoreNumericLiteralTypes (TypeScript only)
329+
330+
Whether numbers used in TypeScript numeric literal types are considered okay. `false` by default.
331+
332+
Examples of **incorrect** code for the `{ "ignoreNumericLiteralTypes": false }` option:
333+
334+
::: incorrect
335+
336+
```ts
337+
/*eslint no-magic-numbers: ["error", { "ignoreNumericLiteralTypes": false }]*/
338+
339+
type Foo = 1 | 2 | 3;
340+
```
341+
342+
:::
343+
344+
Examples of **correct** code for the `{ "ignoreNumericLiteralTypes": true }` option:
345+
346+
::: correct
347+
348+
```ts
349+
/*eslint no-magic-numbers: ["error", { "ignoreNumericLiteralTypes": true }]*/
350+
351+
type Foo = 1 | 2 | 3;
352+
```
353+
354+
:::
355+
356+
### ignoreReadonlyClassProperties (TypeScript only)
357+
358+
Whether numbers used in TypeScript readonly class properties are considered okay. `false` by default.
359+
360+
Examples of **incorrect** code for the `{ "ignoreReadonlyClassProperties": false }` option:
361+
362+
::: incorrect
363+
364+
```ts
365+
/*eslint no-magic-numbers: ["error", { "ignoreReadonlyClassProperties": false }]*/
366+
367+
class Foo {
368+
readonly A = 1;
369+
readonly B = 2;
370+
public static readonly C = 1;
371+
static readonly D = 1;
372+
}
373+
```
374+
375+
:::
376+
377+
Examples of **correct** code for the `{ "ignoreReadonlyClassProperties": true }` option:
378+
379+
::: correct
380+
381+
```ts
382+
/*eslint no-magic-numbers: ["error", { "ignoreReadonlyClassProperties": true }]*/
383+
384+
class Foo {
385+
readonly A = 1;
386+
readonly B = 2;
387+
public static readonly C = 1;
388+
static readonly D = 1;
389+
}
390+
```
391+
392+
:::
393+
394+
### ignoreTypeIndexes (TypeScript only)
395+
396+
Whether numbers used to index types are okay. `false` by default.
397+
398+
Examples of **incorrect** code for the `{ "ignoreTypeIndexes": false }` option:
399+
400+
::: incorrect
401+
402+
```ts
403+
/*eslint no-magic-numbers: ["error", { "ignoreTypeIndexes": false }]*/
404+
405+
type Foo = Bar[0];
406+
type Baz = Parameters<Foo>[2];
407+
```
408+
409+
:::
410+
411+
Examples of **correct** code for the `{ "ignoreTypeIndexes": true }` option:
412+
413+
::: correct
414+
415+
```ts
416+
/*eslint no-magic-numbers: ["error", { "ignoreTypeIndexes": true }]*/
417+
418+
type Foo = Bar[0];
419+
type Baz = Parameters<Foo>[2];
420+
```
421+
422+
:::

lib/rules/no-magic-numbers.js

Lines changed: 98 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,73 @@ function normalizeIgnoreValue(x) {
2626
return x;
2727
}
2828

29+
/**
30+
* Checks if the node parent is a TypeScript enum member
31+
* @param {ASTNode} node The node to be validated
32+
* @returns {boolean} True if the node parent is a TypeScript enum member
33+
*/
34+
function isParentTSEnumDeclaration(node) {
35+
return node.parent.type === "TSEnumMember";
36+
}
37+
38+
/**
39+
* Checks if the node is a valid TypeScript numeric literal type.
40+
* @param {ASTNode} node The node to be validated
41+
* @returns {boolean} True if the node is a TypeScript numeric literal type
42+
*/
43+
function isTSNumericLiteralType(node) {
44+
let ancestor = node.parent;
45+
46+
// Go up while we're part of a type union
47+
while (ancestor.parent.type === "TSUnionType") {
48+
ancestor = ancestor.parent;
49+
}
50+
51+
// Check if the final ancestor is in a type alias declaration
52+
return ancestor.parent.type === "TSTypeAliasDeclaration";
53+
}
54+
55+
/**
56+
* Checks if the node parent is a readonly class property
57+
* @param {ASTNode} node The node to be validated
58+
* @returns {boolean} True if the node parent is a readonly class property
59+
*/
60+
function isParentTSReadonlyPropertyDefinition(node) {
61+
if (node.parent?.type === "PropertyDefinition" && node.parent.readonly) {
62+
return true;
63+
}
64+
65+
return false;
66+
}
67+
68+
/**
69+
* Checks if the node is part of a type indexed access (eg. Foo[4])
70+
* @param {ASTNode} node The node to be validated
71+
* @returns {boolean} True if the node is part of an indexed access
72+
*/
73+
function isAncestorTSIndexedAccessType(node) {
74+
let ancestor = node.parent;
75+
76+
/*
77+
* Go up another level while we're part of a type union (eg. 1 | 2) or
78+
* intersection (eg. 1 & 2)
79+
*/
80+
while (
81+
ancestor.parent.type === "TSUnionType" ||
82+
ancestor.parent.type === "TSIntersectionType"
83+
) {
84+
ancestor = ancestor.parent;
85+
}
86+
87+
return ancestor.parent.type === "TSIndexedAccessType";
88+
}
89+
2990
/** @type {import('../types').Rule.RuleModule} */
3091
module.exports = {
3192
meta: {
3293
type: "suggestion",
94+
dialects: ["typescript", "javascript"],
95+
language: "javascript",
3396

3497
docs: {
3598
description: "Disallow magic numbers",
@@ -75,6 +138,22 @@ module.exports = {
75138
type: "boolean",
76139
default: false,
77140
},
141+
ignoreEnums: {
142+
type: "boolean",
143+
default: false,
144+
},
145+
ignoreNumericLiteralTypes: {
146+
type: "boolean",
147+
default: false,
148+
},
149+
ignoreReadonlyClassProperties: {
150+
type: "boolean",
151+
default: false,
152+
},
153+
ignoreTypeIndexes: {
154+
type: "boolean",
155+
default: false,
156+
},
78157
},
79158
additionalProperties: false,
80159
},
@@ -94,7 +173,12 @@ module.exports = {
94173
ignoreArrayIndexes = !!config.ignoreArrayIndexes,
95174
ignoreDefaultValues = !!config.ignoreDefaultValues,
96175
ignoreClassFieldInitialValues =
97-
!!config.ignoreClassFieldInitialValues;
176+
!!config.ignoreClassFieldInitialValues,
177+
ignoreEnums = !!config.ignoreEnums,
178+
ignoreNumericLiteralTypes = !!config.ignoreNumericLiteralTypes,
179+
ignoreReadonlyClassProperties =
180+
!!config.ignoreReadonlyClassProperties,
181+
ignoreTypeIndexes = !!config.ignoreTypeIndexes;
98182

99183
const okTypes = detectObjects
100184
? []
@@ -217,14 +301,15 @@ module.exports = {
217301
let value;
218302
let raw;
219303

220-
// Treat unary minus as a part of the number
304+
// Treat unary minus/plus as a part of the number
221305
if (
222306
node.parent.type === "UnaryExpression" &&
223-
node.parent.operator === "-"
307+
["-", "+"].includes(node.parent.operator)
224308
) {
225309
fullNumberNode = node.parent;
226-
value = -node.value;
227-
raw = `-${node.raw}`;
310+
value =
311+
node.parent.operator === "-" ? -node.value : node.value;
312+
raw = `${node.parent.operator}${node.raw}`;
228313
} else {
229314
fullNumberNode = node;
230315
value = node.value;
@@ -239,6 +324,14 @@ module.exports = {
239324
(ignoreDefaultValues && isDefaultValue(fullNumberNode)) ||
240325
(ignoreClassFieldInitialValues &&
241326
isClassFieldInitialValue(fullNumberNode)) ||
327+
(ignoreEnums &&
328+
isParentTSEnumDeclaration(fullNumberNode)) ||
329+
(ignoreNumericLiteralTypes &&
330+
isTSNumericLiteralType(fullNumberNode)) ||
331+
(ignoreTypeIndexes &&
332+
isAncestorTSIndexedAccessType(fullNumberNode)) ||
333+
(ignoreReadonlyClassProperties &&
334+
isParentTSReadonlyPropertyDefinition(fullNumberNode)) ||
242335
isParseIntRadix(fullNumberNode) ||
243336
isJSXNumber(fullNumberNode) ||
244337
(ignoreArrayIndexes && isArrayIndex(fullNumberNode, value))

lib/types/rules.d.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2858,6 +2858,22 @@ export interface ESLintRules extends Linter.RulesRecord {
28582858
* @default false
28592859
*/
28602860
detectObjects: boolean;
2861+
/**
2862+
* @default false
2863+
*/
2864+
ignoreEnums: boolean;
2865+
/**
2866+
* @default false
2867+
*/
2868+
ignoreNumericLiteralTypes: boolean;
2869+
/**
2870+
* @default false
2871+
*/
2872+
ignoreReadonlyClassProperties: boolean;
2873+
/**
2874+
* @default false
2875+
*/
2876+
ignoreTypeIndexes: boolean;
28612877
}>,
28622878
]
28632879
>;

0 commit comments

Comments
 (0)