eslint 9.27.0 → 9.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/cli.js +14 -12
- package/lib/config/config-loader.js +32 -21
- package/lib/config/config.js +34 -11
- package/lib/eslint/eslint.js +5 -4
- package/lib/languages/js/source-code/source-code.js +30 -0
- package/lib/linter/linter.js +27 -58
- package/lib/linter/{node-event-generator.js → source-code-traverser.js} +143 -87
- package/lib/options.js +7 -0
- package/lib/rules/func-style.js +57 -7
- package/lib/rules/no-implicit-globals.js +31 -15
- package/lib/rules/no-magic-numbers.js +98 -5
- package/lib/rules/no-shadow.js +262 -6
- package/lib/rules/no-unassigned-vars.js +14 -6
- package/lib/rules/no-use-before-define.js +97 -1
- package/lib/rules/prefer-arrow-callback.js +9 -0
- package/lib/services/warning-service.js +85 -0
- package/lib/types/index.d.ts +10 -5
- package/lib/types/rules.d.ts +47 -2
- package/package.json +7 -5
@@ -1,6 +1,6 @@
|
|
1
1
|
/**
|
2
|
-
* @fileoverview
|
3
|
-
* @author
|
2
|
+
* @fileoverview Traverser for SourceCode objects.
|
3
|
+
* @author Nicholas C. Zakas
|
4
4
|
*/
|
5
5
|
|
6
6
|
"use strict";
|
@@ -10,6 +10,7 @@
|
|
10
10
|
//------------------------------------------------------------------------------
|
11
11
|
|
12
12
|
const { parse, matches } = require("./esquery");
|
13
|
+
const vk = require("eslint-visitor-keys");
|
13
14
|
|
14
15
|
//-----------------------------------------------------------------------------
|
15
16
|
// Typedefs
|
@@ -17,12 +18,16 @@ const { parse, matches } = require("./esquery");
|
|
17
18
|
|
18
19
|
/**
|
19
20
|
* @import { ESQueryParsedSelector } from "./esquery.js";
|
21
|
+
* @import { Language, SourceCode } from "@eslint/core";
|
20
22
|
*/
|
21
23
|
|
22
24
|
//-----------------------------------------------------------------------------
|
23
25
|
// Helpers
|
24
26
|
//-----------------------------------------------------------------------------
|
25
27
|
|
28
|
+
const STEP_KIND_VISIT = 1;
|
29
|
+
const STEP_KIND_CALL = 2;
|
30
|
+
|
26
31
|
/**
|
27
32
|
* Compares two ESQuery selectors by specificity.
|
28
33
|
* @param {ESQueryParsedSelector} a The first selector to compare.
|
@@ -33,69 +38,10 @@ function compareSpecificity(a, b) {
|
|
33
38
|
return a.compare(b);
|
34
39
|
}
|
35
40
|
|
36
|
-
//------------------------------------------------------------------------------
|
37
|
-
// Public Interface
|
38
|
-
//------------------------------------------------------------------------------
|
39
|
-
|
40
41
|
/**
|
41
|
-
*
|
42
|
-
* This implements below interface.
|
43
|
-
*
|
44
|
-
* ```ts
|
45
|
-
* interface EventGenerator {
|
46
|
-
* emitter: SafeEmitter;
|
47
|
-
* enterNode(node: ASTNode): void;
|
48
|
-
* leaveNode(node: ASTNode): void;
|
49
|
-
* }
|
50
|
-
* ```
|
42
|
+
* Helper to wrap ESQuery operations.
|
51
43
|
*/
|
52
|
-
class
|
53
|
-
/**
|
54
|
-
* The emitter to use during traversal.
|
55
|
-
* @type {SafeEmitter}
|
56
|
-
*/
|
57
|
-
emitter;
|
58
|
-
|
59
|
-
/**
|
60
|
-
* The options for `esquery` to use during matching.
|
61
|
-
* @type {ESQueryOptions}
|
62
|
-
*/
|
63
|
-
esqueryOptions;
|
64
|
-
|
65
|
-
/**
|
66
|
-
* The ancestry of the currently visited node.
|
67
|
-
* @type {ASTNode[]}
|
68
|
-
*/
|
69
|
-
currentAncestry = [];
|
70
|
-
|
71
|
-
/**
|
72
|
-
* A map of node type to selectors targeting that node type on the
|
73
|
-
* enter phase of traversal.
|
74
|
-
* @type {Map<string, ESQueryParsedSelector[]>}
|
75
|
-
*/
|
76
|
-
enterSelectorsByNodeType = new Map();
|
77
|
-
|
78
|
-
/**
|
79
|
-
* A map of node type to selectors targeting that node type on the
|
80
|
-
* exit phase of traversal.
|
81
|
-
* @type {Map<string, ESQueryParsedSelector[]>}
|
82
|
-
*/
|
83
|
-
exitSelectorsByNodeType = new Map();
|
84
|
-
|
85
|
-
/**
|
86
|
-
* An array of selectors that match any node type on the
|
87
|
-
* enter phase of traversal.
|
88
|
-
* @type {ESQueryParsedSelector[]}
|
89
|
-
*/
|
90
|
-
anyTypeEnterSelectors = [];
|
91
|
-
|
92
|
-
/**
|
93
|
-
* An array of selectors that match any node type on the
|
94
|
-
* exit phase of traversal.
|
95
|
-
* @type {ESQueryParsedSelector[]}
|
96
|
-
*/
|
97
|
-
anyTypeExitSelectors = [];
|
98
|
-
|
44
|
+
class ESQueryHelper {
|
99
45
|
/**
|
100
46
|
* @param {SafeEmitter} emitter
|
101
47
|
* An SafeEmitter which is the destination of events. This emitter must already
|
@@ -105,9 +51,46 @@ class NodeEventGenerator {
|
|
105
51
|
* @returns {NodeEventGenerator} new instance
|
106
52
|
*/
|
107
53
|
constructor(emitter, esqueryOptions) {
|
54
|
+
/**
|
55
|
+
* The emitter to use during traversal.
|
56
|
+
* @type {SafeEmitter}
|
57
|
+
*/
|
108
58
|
this.emitter = emitter;
|
59
|
+
|
60
|
+
/**
|
61
|
+
* The options for `esquery` to use during matching.
|
62
|
+
* @type {ESQueryOptions}
|
63
|
+
*/
|
109
64
|
this.esqueryOptions = esqueryOptions;
|
110
65
|
|
66
|
+
/**
|
67
|
+
* A map of node type to selectors targeting that node type on the
|
68
|
+
* enter phase of traversal.
|
69
|
+
* @type {Map<string, ESQueryParsedSelector[]>}
|
70
|
+
*/
|
71
|
+
this.enterSelectorsByNodeType = new Map();
|
72
|
+
|
73
|
+
/**
|
74
|
+
* A map of node type to selectors targeting that node type on the
|
75
|
+
* exit phase of traversal.
|
76
|
+
* @type {Map<string, ESQueryParsedSelector[]>}
|
77
|
+
*/
|
78
|
+
this.exitSelectorsByNodeType = new Map();
|
79
|
+
|
80
|
+
/**
|
81
|
+
* An array of selectors that match any node type on the
|
82
|
+
* enter phase of traversal.
|
83
|
+
* @type {ESQueryParsedSelector[]}
|
84
|
+
*/
|
85
|
+
this.anyTypeEnterSelectors = [];
|
86
|
+
|
87
|
+
/**
|
88
|
+
* An array of selectors that match any node type on the
|
89
|
+
* exit phase of traversal.
|
90
|
+
* @type {ESQueryParsedSelector[]}
|
91
|
+
*/
|
92
|
+
this.anyTypeExitSelectors = [];
|
93
|
+
|
111
94
|
emitter.eventNames().forEach(rawSelector => {
|
112
95
|
const selector = parse(rawSelector);
|
113
96
|
|
@@ -156,18 +139,12 @@ class NodeEventGenerator {
|
|
156
139
|
/**
|
157
140
|
* Checks a selector against a node, and emits it if it matches
|
158
141
|
* @param {ASTNode} node The node to check
|
142
|
+
* @param {ASTNode[]} ancestry The ancestry of the node being checked.
|
159
143
|
* @param {ESQueryParsedSelector} selector An AST selector descriptor
|
160
144
|
* @returns {void}
|
161
145
|
*/
|
162
|
-
applySelector(node, selector) {
|
163
|
-
if (
|
164
|
-
matches(
|
165
|
-
node,
|
166
|
-
selector.root,
|
167
|
-
this.currentAncestry,
|
168
|
-
this.esqueryOptions,
|
169
|
-
)
|
170
|
-
) {
|
146
|
+
#applySelector(node, ancestry, selector) {
|
147
|
+
if (matches(node, selector.root, ancestry, this.esqueryOptions)) {
|
171
148
|
this.emitter.emit(selector.source, node);
|
172
149
|
}
|
173
150
|
}
|
@@ -175,10 +152,11 @@ class NodeEventGenerator {
|
|
175
152
|
/**
|
176
153
|
* Applies all appropriate selectors to a node, in specificity order
|
177
154
|
* @param {ASTNode} node The node to check
|
155
|
+
* @param {ASTNode[]} ancestry The ancestry of the node being checked.
|
178
156
|
* @param {boolean} isExit `false` if the node is currently being entered, `true` if it's currently being exited
|
179
157
|
* @returns {void}
|
180
158
|
*/
|
181
|
-
applySelectors(node, isExit) {
|
159
|
+
applySelectors(node, ancestry, isExit) {
|
182
160
|
const nodeTypeKey = this.esqueryOptions?.nodeTypeKey || "type";
|
183
161
|
|
184
162
|
/*
|
@@ -218,39 +196,117 @@ class NodeEventGenerator {
|
|
218
196
|
selectorsByNodeType[selectorsByNodeTypeIndex],
|
219
197
|
) < 0)
|
220
198
|
) {
|
221
|
-
this
|
199
|
+
this.#applySelector(
|
222
200
|
node,
|
201
|
+
ancestry,
|
223
202
|
anyTypeSelectors[anyTypeSelectorsIndex++],
|
224
203
|
);
|
225
204
|
} else {
|
226
205
|
// otherwise apply the node type selector
|
227
|
-
this
|
206
|
+
this.#applySelector(
|
228
207
|
node,
|
208
|
+
ancestry,
|
229
209
|
selectorsByNodeType[selectorsByNodeTypeIndex++],
|
230
210
|
);
|
231
211
|
}
|
232
212
|
}
|
233
213
|
}
|
214
|
+
}
|
215
|
+
|
216
|
+
//------------------------------------------------------------------------------
|
217
|
+
// Public Interface
|
218
|
+
//------------------------------------------------------------------------------
|
234
219
|
|
220
|
+
/**
|
221
|
+
* Traverses source code and ensures that visitor methods are called when
|
222
|
+
* entering and leaving each node.
|
223
|
+
*/
|
224
|
+
class SourceCodeTraverser {
|
235
225
|
/**
|
236
|
-
*
|
237
|
-
* @
|
238
|
-
|
226
|
+
* The language of the source code being traversed.
|
227
|
+
* @type {Language}
|
228
|
+
*/
|
229
|
+
#language;
|
230
|
+
|
231
|
+
/**
|
232
|
+
* Map of languages to instances of this class.
|
233
|
+
* @type {WeakMap<Language, SourceCodeTraverser>}
|
234
|
+
*/
|
235
|
+
static instances = new WeakMap();
|
236
|
+
|
237
|
+
/**
|
238
|
+
* Creates a new instance.
|
239
|
+
* @param {Language} language The language of the source code being traversed.
|
239
240
|
*/
|
240
|
-
|
241
|
-
this
|
242
|
-
|
241
|
+
constructor(language) {
|
242
|
+
this.#language = language;
|
243
|
+
}
|
244
|
+
|
245
|
+
static getInstance(language) {
|
246
|
+
if (!this.instances.has(language)) {
|
247
|
+
this.instances.set(language, new this(language));
|
248
|
+
}
|
249
|
+
|
250
|
+
return this.instances.get(language);
|
243
251
|
}
|
244
252
|
|
245
253
|
/**
|
246
|
-
*
|
247
|
-
* @param {
|
254
|
+
* Traverses the given source code synchronously.
|
255
|
+
* @param {SourceCode} sourceCode The source code to traverse.
|
256
|
+
* @param {SafeEmitter} emitter The emitter to use for events.
|
257
|
+
* @param {Object} options Options for traversal.
|
258
|
+
* @param {ReturnType<SourceCode["traverse"]>} options.steps The steps to take during traversal.
|
248
259
|
* @returns {void}
|
260
|
+
* @throws {Error} If an error occurs during traversal.
|
249
261
|
*/
|
250
|
-
|
251
|
-
|
252
|
-
|
262
|
+
traverseSync(sourceCode, emitter, { steps } = {}) {
|
263
|
+
const esquery = new ESQueryHelper(emitter, {
|
264
|
+
visitorKeys: sourceCode.visitorKeys ?? this.#language.visitorKeys,
|
265
|
+
fallback: vk.getKeys,
|
266
|
+
matchClass: this.#language.matchesSelectorClass ?? (() => false),
|
267
|
+
nodeTypeKey: this.#language.nodeTypeKey,
|
268
|
+
});
|
269
|
+
|
270
|
+
const currentAncestry = [];
|
271
|
+
|
272
|
+
for (const step of steps ?? sourceCode.traverse()) {
|
273
|
+
switch (step.kind) {
|
274
|
+
case STEP_KIND_VISIT: {
|
275
|
+
try {
|
276
|
+
if (step.phase === 1) {
|
277
|
+
esquery.applySelectors(
|
278
|
+
step.target,
|
279
|
+
currentAncestry,
|
280
|
+
false,
|
281
|
+
);
|
282
|
+
currentAncestry.unshift(step.target);
|
283
|
+
} else {
|
284
|
+
currentAncestry.shift();
|
285
|
+
esquery.applySelectors(
|
286
|
+
step.target,
|
287
|
+
currentAncestry,
|
288
|
+
true,
|
289
|
+
);
|
290
|
+
}
|
291
|
+
} catch (err) {
|
292
|
+
err.currentNode = step.target;
|
293
|
+
throw err;
|
294
|
+
}
|
295
|
+
break;
|
296
|
+
}
|
297
|
+
|
298
|
+
case STEP_KIND_CALL: {
|
299
|
+
emitter.emit(step.target, ...step.args);
|
300
|
+
break;
|
301
|
+
}
|
302
|
+
|
303
|
+
default:
|
304
|
+
throw new Error(
|
305
|
+
`Invalid traversal step found: "${step.kind}".`,
|
306
|
+
);
|
307
|
+
}
|
308
|
+
}
|
253
309
|
}
|
254
310
|
}
|
255
311
|
|
256
|
-
module.exports =
|
312
|
+
module.exports = { SourceCodeTraverser };
|
package/lib/options.js
CHANGED
@@ -66,6 +66,7 @@ const optionator = require("optionator");
|
|
66
66
|
* @property {string[]} [suppressRule] Suppress specific rules
|
67
67
|
* @property {string} [suppressionsLocation] Path to the suppressions file or directory
|
68
68
|
* @property {boolean} [pruneSuppressions] Prune unused suppressions
|
69
|
+
* @property {boolean} [passOnUnprunedSuppressions] Ignore unused suppressions
|
69
70
|
*/
|
70
71
|
|
71
72
|
//------------------------------------------------------------------------------
|
@@ -449,6 +450,12 @@ module.exports = function (usingFlatConfig) {
|
|
449
450
|
default: "false",
|
450
451
|
description: "Prune unused suppressions",
|
451
452
|
},
|
453
|
+
{
|
454
|
+
option: "pass-on-unpruned-suppressions",
|
455
|
+
type: "Boolean",
|
456
|
+
default: "false",
|
457
|
+
description: "Ignore unused suppressions",
|
458
|
+
},
|
452
459
|
{
|
453
460
|
heading: "Miscellaneous",
|
454
461
|
},
|
package/lib/rules/func-style.js
CHANGED
@@ -11,12 +11,15 @@
|
|
11
11
|
/** @type {import('../types').Rule.RuleModule} */
|
12
12
|
module.exports = {
|
13
13
|
meta: {
|
14
|
+
dialects: ["javascript", "typescript"],
|
15
|
+
language: "javascript",
|
14
16
|
type: "suggestion",
|
15
17
|
|
16
18
|
defaultOptions: [
|
17
19
|
"expression",
|
18
20
|
{
|
19
21
|
allowArrowFunctions: false,
|
22
|
+
allowTypeAnnotation: false,
|
20
23
|
overrides: {},
|
21
24
|
},
|
22
25
|
],
|
@@ -39,6 +42,9 @@ module.exports = {
|
|
39
42
|
allowArrowFunctions: {
|
40
43
|
type: "boolean",
|
41
44
|
},
|
45
|
+
allowTypeAnnotation: {
|
46
|
+
type: "boolean",
|
47
|
+
},
|
42
48
|
overrides: {
|
43
49
|
type: "object",
|
44
50
|
properties: {
|
@@ -60,11 +66,49 @@ module.exports = {
|
|
60
66
|
},
|
61
67
|
|
62
68
|
create(context) {
|
63
|
-
const [style, { allowArrowFunctions, overrides }] =
|
69
|
+
const [style, { allowArrowFunctions, allowTypeAnnotation, overrides }] =
|
70
|
+
context.options;
|
64
71
|
const enforceDeclarations = style === "declaration";
|
65
72
|
const { namedExports: exportFunctionStyle } = overrides;
|
66
73
|
const stack = [];
|
67
74
|
|
75
|
+
/**
|
76
|
+
* Checks if a function declaration is part of an overloaded function
|
77
|
+
* @param {ASTNode} node The function declaration node to check
|
78
|
+
* @returns {boolean} True if the function is overloaded
|
79
|
+
*/
|
80
|
+
function isOverloadedFunction(node) {
|
81
|
+
const functionName = node.id.name;
|
82
|
+
|
83
|
+
if (node.parent.type === "ExportNamedDeclaration") {
|
84
|
+
return node.parent.parent.body.some(
|
85
|
+
member =>
|
86
|
+
member.type === "ExportNamedDeclaration" &&
|
87
|
+
member.declaration?.type === "TSDeclareFunction" &&
|
88
|
+
member.declaration.id.name === functionName,
|
89
|
+
);
|
90
|
+
}
|
91
|
+
|
92
|
+
if (node.parent.type === "SwitchCase") {
|
93
|
+
return node.parent.parent.cases.some(switchCase =>
|
94
|
+
switchCase.consequent.some(
|
95
|
+
member =>
|
96
|
+
member.type === "TSDeclareFunction" &&
|
97
|
+
member.id.name === functionName,
|
98
|
+
),
|
99
|
+
);
|
100
|
+
}
|
101
|
+
|
102
|
+
return (
|
103
|
+
Array.isArray(node.parent.body) &&
|
104
|
+
node.parent.body.some(
|
105
|
+
member =>
|
106
|
+
member.type === "TSDeclareFunction" &&
|
107
|
+
member.id.name === functionName,
|
108
|
+
)
|
109
|
+
);
|
110
|
+
}
|
111
|
+
|
68
112
|
const nodesToCheck = {
|
69
113
|
FunctionDeclaration(node) {
|
70
114
|
stack.push(false);
|
@@ -73,14 +117,16 @@ module.exports = {
|
|
73
117
|
!enforceDeclarations &&
|
74
118
|
node.parent.type !== "ExportDefaultDeclaration" &&
|
75
119
|
(typeof exportFunctionStyle === "undefined" ||
|
76
|
-
node.parent.type !== "ExportNamedDeclaration")
|
120
|
+
node.parent.type !== "ExportNamedDeclaration") &&
|
121
|
+
!isOverloadedFunction(node)
|
77
122
|
) {
|
78
123
|
context.report({ node, messageId: "expression" });
|
79
124
|
}
|
80
125
|
|
81
126
|
if (
|
82
127
|
node.parent.type === "ExportNamedDeclaration" &&
|
83
|
-
exportFunctionStyle === "expression"
|
128
|
+
exportFunctionStyle === "expression" &&
|
129
|
+
!isOverloadedFunction(node)
|
84
130
|
) {
|
85
131
|
context.report({ node, messageId: "expression" });
|
86
132
|
}
|
@@ -97,7 +143,8 @@ module.exports = {
|
|
97
143
|
node.parent.type === "VariableDeclarator" &&
|
98
144
|
(typeof exportFunctionStyle === "undefined" ||
|
99
145
|
node.parent.parent.parent.type !==
|
100
|
-
"ExportNamedDeclaration")
|
146
|
+
"ExportNamedDeclaration") &&
|
147
|
+
!(allowTypeAnnotation && node.parent.id.typeAnnotation)
|
101
148
|
) {
|
102
149
|
context.report({
|
103
150
|
node: node.parent,
|
@@ -109,7 +156,8 @@ module.exports = {
|
|
109
156
|
node.parent.type === "VariableDeclarator" &&
|
110
157
|
node.parent.parent.parent.type ===
|
111
158
|
"ExportNamedDeclaration" &&
|
112
|
-
exportFunctionStyle === "declaration"
|
159
|
+
exportFunctionStyle === "declaration" &&
|
160
|
+
!(allowTypeAnnotation && node.parent.id.typeAnnotation)
|
113
161
|
) {
|
114
162
|
context.report({
|
115
163
|
node: node.parent,
|
@@ -144,7 +192,8 @@ module.exports = {
|
|
144
192
|
enforceDeclarations &&
|
145
193
|
(typeof exportFunctionStyle === "undefined" ||
|
146
194
|
node.parent.parent.parent.type !==
|
147
|
-
"ExportNamedDeclaration")
|
195
|
+
"ExportNamedDeclaration") &&
|
196
|
+
!(allowTypeAnnotation && node.parent.id.typeAnnotation)
|
148
197
|
) {
|
149
198
|
context.report({
|
150
199
|
node: node.parent,
|
@@ -155,7 +204,8 @@ module.exports = {
|
|
155
204
|
if (
|
156
205
|
node.parent.parent.parent.type ===
|
157
206
|
"ExportNamedDeclaration" &&
|
158
|
-
exportFunctionStyle === "declaration"
|
207
|
+
exportFunctionStyle === "declaration" &&
|
208
|
+
!(allowTypeAnnotation && node.parent.id.typeAnnotation)
|
159
209
|
) {
|
160
210
|
context.report({
|
161
211
|
node: node.parent,
|
@@ -5,6 +5,12 @@
|
|
5
5
|
|
6
6
|
"use strict";
|
7
7
|
|
8
|
+
const ASSIGNMENT_NODES = new Set([
|
9
|
+
"AssignmentExpression",
|
10
|
+
"ForInStatement",
|
11
|
+
"ForOfStatement",
|
12
|
+
]);
|
13
|
+
|
8
14
|
//------------------------------------------------------------------------------
|
9
15
|
// Rule Definition
|
10
16
|
//------------------------------------------------------------------------------
|
@@ -142,27 +148,37 @@ module.exports = {
|
|
142
148
|
}
|
143
149
|
}
|
144
150
|
});
|
145
|
-
});
|
146
151
|
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
152
|
+
if (
|
153
|
+
isReadonlyEslintGlobalVariable &&
|
154
|
+
variable.defs.length === 0
|
155
|
+
) {
|
156
|
+
variable.references.forEach(reference => {
|
157
|
+
if (reference.isWrite() && !reference.isRead()) {
|
158
|
+
let assignmentParent =
|
159
|
+
reference.identifier.parent;
|
160
|
+
|
161
|
+
while (
|
162
|
+
assignmentParent &&
|
163
|
+
!ASSIGNMENT_NODES.has(assignmentParent.type)
|
164
|
+
) {
|
165
|
+
assignmentParent = assignmentParent.parent;
|
166
|
+
}
|
151
167
|
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
} else {
|
159
|
-
// Reference to an unknown variable, possible global leak.
|
160
|
-
messageId = "globalVariableLeak";
|
168
|
+
report(
|
169
|
+
assignmentParent ?? reference.identifier,
|
170
|
+
"assignmentToReadonlyGlobal",
|
171
|
+
);
|
172
|
+
}
|
173
|
+
});
|
161
174
|
}
|
175
|
+
});
|
162
176
|
|
177
|
+
// Undeclared assigned variables.
|
178
|
+
scope.implicit.variables.forEach(variable => {
|
163
179
|
// def.node is an AssignmentExpression, ForInStatement or ForOfStatement.
|
164
180
|
variable.defs.forEach(def => {
|
165
|
-
report(def.node,
|
181
|
+
report(def.node, "globalVariableLeak");
|
166
182
|
});
|
167
183
|
});
|
168
184
|
},
|
@@ -26,10 +26,73 @@ function normalizeIgnoreValue(x) {
|
|
26
26
|
return x;
|
27
27
|
}
|
28
28
|
|
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
|
+
|
29
90
|
/** @type {import('../types').Rule.RuleModule} */
|
30
91
|
module.exports = {
|
31
92
|
meta: {
|
32
93
|
type: "suggestion",
|
94
|
+
dialects: ["typescript", "javascript"],
|
95
|
+
language: "javascript",
|
33
96
|
|
34
97
|
docs: {
|
35
98
|
description: "Disallow magic numbers",
|
@@ -75,6 +138,22 @@ module.exports = {
|
|
75
138
|
type: "boolean",
|
76
139
|
default: false,
|
77
140
|
},
|
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
|
+
},
|
78
157
|
},
|
79
158
|
additionalProperties: false,
|
80
159
|
},
|
@@ -94,7 +173,12 @@ module.exports = {
|
|
94
173
|
ignoreArrayIndexes = !!config.ignoreArrayIndexes,
|
95
174
|
ignoreDefaultValues = !!config.ignoreDefaultValues,
|
96
175
|
ignoreClassFieldInitialValues =
|
97
|
-
!!config.ignoreClassFieldInitialValues
|
176
|
+
!!config.ignoreClassFieldInitialValues,
|
177
|
+
ignoreEnums = !!config.ignoreEnums,
|
178
|
+
ignoreNumericLiteralTypes = !!config.ignoreNumericLiteralTypes,
|
179
|
+
ignoreReadonlyClassProperties =
|
180
|
+
!!config.ignoreReadonlyClassProperties,
|
181
|
+
ignoreTypeIndexes = !!config.ignoreTypeIndexes;
|
98
182
|
|
99
183
|
const okTypes = detectObjects
|
100
184
|
? []
|
@@ -217,14 +301,15 @@ module.exports = {
|
|
217
301
|
let value;
|
218
302
|
let raw;
|
219
303
|
|
220
|
-
// Treat unary minus as a part of the number
|
304
|
+
// Treat unary minus/plus as a part of the number
|
221
305
|
if (
|
222
306
|
node.parent.type === "UnaryExpression" &&
|
223
|
-
node.parent.operator
|
307
|
+
["-", "+"].includes(node.parent.operator)
|
224
308
|
) {
|
225
309
|
fullNumberNode = node.parent;
|
226
|
-
value =
|
227
|
-
|
310
|
+
value =
|
311
|
+
node.parent.operator === "-" ? -node.value : node.value;
|
312
|
+
raw = `${node.parent.operator}${node.raw}`;
|
228
313
|
} else {
|
229
314
|
fullNumberNode = node;
|
230
315
|
value = node.value;
|
@@ -239,6 +324,14 @@ module.exports = {
|
|
239
324
|
(ignoreDefaultValues && isDefaultValue(fullNumberNode)) ||
|
240
325
|
(ignoreClassFieldInitialValues &&
|
241
326
|
isClassFieldInitialValue(fullNumberNode)) ||
|
327
|
+
(ignoreEnums &&
|
328
|
+
isParentTSEnumDeclaration(fullNumberNode)) ||
|
329
|
+
(ignoreNumericLiteralTypes &&
|
330
|
+
isTSNumericLiteralType(fullNumberNode)) ||
|
331
|
+
(ignoreTypeIndexes &&
|
332
|
+
isAncestorTSIndexedAccessType(fullNumberNode)) ||
|
333
|
+
(ignoreReadonlyClassProperties &&
|
334
|
+
isParentTSReadonlyPropertyDefinition(fullNumberNode)) ||
|
242
335
|
isParseIntRadix(fullNumberNode) ||
|
243
336
|
isJSXNumber(fullNumberNode) ||
|
244
337
|
(ignoreArrayIndexes && isArrayIndex(fullNumberNode, value))
|