]>
Commit | Line | Data |
---|---|---|
eb39fafa DC |
1 | /** |
2 | * @fileoverview Rule to flag statements that use magic numbers (adapted from https://github.com/danielstjules/buddy.js) | |
3 | * @author Vincent Lemeunier | |
4 | */ | |
5 | ||
6 | "use strict"; | |
7 | ||
6f036462 | 8 | const astUtils = require("./utils/ast-utils"); |
eb39fafa DC |
9 | |
10 | // Maximum array length by the ECMAScript Specification. | |
11 | const MAX_ARRAY_LENGTH = 2 ** 32 - 1; | |
12 | ||
13 | //------------------------------------------------------------------------------ | |
14 | // Rule Definition | |
15 | //------------------------------------------------------------------------------ | |
16 | ||
17 | /** | |
18 | * Convert the value to bigint if it's a string. Otherwise return the value as-is. | |
19 | * @param {bigint|number|string} x The value to normalize. | |
20 | * @returns {bigint|number} The normalized value. | |
21 | */ | |
22 | function normalizeIgnoreValue(x) { | |
23 | if (typeof x === "string") { | |
24 | return BigInt(x.slice(0, -1)); | |
25 | } | |
26 | return x; | |
27 | } | |
28 | ||
34eeec05 | 29 | /** @type {import('../shared/types').Rule} */ |
eb39fafa DC |
30 | module.exports = { |
31 | meta: { | |
32 | type: "suggestion", | |
33 | ||
34 | docs: { | |
8f9d1d4d | 35 | description: "Disallow magic numbers", |
eb39fafa DC |
36 | recommended: false, |
37 | url: "https://eslint.org/docs/rules/no-magic-numbers" | |
38 | }, | |
39 | ||
40 | schema: [{ | |
41 | type: "object", | |
42 | properties: { | |
43 | detectObjects: { | |
44 | type: "boolean", | |
45 | default: false | |
46 | }, | |
47 | enforceConst: { | |
48 | type: "boolean", | |
49 | default: false | |
50 | }, | |
51 | ignore: { | |
52 | type: "array", | |
53 | items: { | |
54 | anyOf: [ | |
55 | { type: "number" }, | |
56 | { type: "string", pattern: "^[+-]?(?:0|[1-9][0-9]*)n$" } | |
57 | ] | |
58 | }, | |
59 | uniqueItems: true | |
60 | }, | |
61 | ignoreArrayIndexes: { | |
62 | type: "boolean", | |
63 | default: false | |
6f036462 TL |
64 | }, |
65 | ignoreDefaultValues: { | |
66 | type: "boolean", | |
67 | default: false | |
eb39fafa DC |
68 | } |
69 | }, | |
70 | additionalProperties: false | |
71 | }], | |
72 | ||
73 | messages: { | |
74 | useConst: "Number constants declarations must use 'const'.", | |
75 | noMagic: "No magic number: {{raw}}." | |
76 | } | |
77 | }, | |
78 | ||
79 | create(context) { | |
80 | const config = context.options[0] || {}, | |
81 | detectObjects = !!config.detectObjects, | |
82 | enforceConst = !!config.enforceConst, | |
8f9d1d4d | 83 | ignore = new Set((config.ignore || []).map(normalizeIgnoreValue)), |
6f036462 TL |
84 | ignoreArrayIndexes = !!config.ignoreArrayIndexes, |
85 | ignoreDefaultValues = !!config.ignoreDefaultValues; | |
eb39fafa DC |
86 | |
87 | const okTypes = detectObjects ? [] : ["ObjectExpression", "Property", "AssignmentExpression"]; | |
88 | ||
89 | /** | |
90 | * Returns whether the rule is configured to ignore the given value | |
91 | * @param {bigint|number} value The value to check | |
92 | * @returns {boolean} true if the value is ignored | |
93 | */ | |
94 | function isIgnoredValue(value) { | |
8f9d1d4d | 95 | return ignore.has(value); |
eb39fafa DC |
96 | } |
97 | ||
6f036462 TL |
98 | /** |
99 | * Returns whether the number is a default value assignment. | |
100 | * @param {ASTNode} fullNumberNode `Literal` or `UnaryExpression` full number node | |
101 | * @returns {boolean} true if the number is a default value | |
102 | */ | |
103 | function isDefaultValue(fullNumberNode) { | |
104 | const parent = fullNumberNode.parent; | |
105 | ||
106 | return parent.type === "AssignmentPattern" && parent.right === fullNumberNode; | |
107 | } | |
108 | ||
eb39fafa DC |
109 | /** |
110 | * Returns whether the given node is used as a radix within parseInt() or Number.parseInt() | |
111 | * @param {ASTNode} fullNumberNode `Literal` or `UnaryExpression` full number node | |
112 | * @returns {boolean} true if the node is radix | |
113 | */ | |
114 | function isParseIntRadix(fullNumberNode) { | |
115 | const parent = fullNumberNode.parent; | |
116 | ||
117 | return parent.type === "CallExpression" && fullNumberNode === parent.arguments[1] && | |
118 | ( | |
6f036462 TL |
119 | astUtils.isSpecificId(parent.callee, "parseInt") || |
120 | astUtils.isSpecificMemberAccess(parent.callee, "Number", "parseInt") | |
eb39fafa DC |
121 | ); |
122 | } | |
123 | ||
124 | /** | |
125 | * Returns whether the given node is a direct child of a JSX node. | |
126 | * In particular, it aims to detect numbers used as prop values in JSX tags. | |
127 | * Example: <input maxLength={10} /> | |
128 | * @param {ASTNode} fullNumberNode `Literal` or `UnaryExpression` full number node | |
129 | * @returns {boolean} true if the node is a JSX number | |
130 | */ | |
131 | function isJSXNumber(fullNumberNode) { | |
132 | return fullNumberNode.parent.type.indexOf("JSX") === 0; | |
133 | } | |
134 | ||
135 | /** | |
136 | * Returns whether the given node is used as an array index. | |
137 | * Value must coerce to a valid array index name: "0", "1", "2" ... "4294967294". | |
138 | * | |
139 | * All other values, like "-1", "2.5", or "4294967295", are just "normal" object properties, | |
140 | * which can be created and accessed on an array in addition to the array index properties, | |
141 | * but they don't affect array's length and are not considered by methods such as .map(), .forEach() etc. | |
142 | * | |
143 | * The maximum array length by the specification is 2 ** 32 - 1 = 4294967295, | |
144 | * thus the maximum valid index is 2 ** 32 - 2 = 4294967294. | |
145 | * | |
146 | * All notations are allowed, as long as the value coerces to one of "0", "1", "2" ... "4294967294". | |
147 | * | |
148 | * Valid examples: | |
149 | * a[0], a[1], a[1.2e1], a[0xAB], a[0n], a[1n] | |
150 | * a[-0] (same as a[0] because -0 coerces to "0") | |
151 | * a[-0n] (-0n evaluates to 0n) | |
152 | * | |
153 | * Invalid examples: | |
154 | * a[-1], a[-0xAB], a[-1n], a[2.5], a[1.23e1], a[12e-1] | |
155 | * a[4294967295] (above the max index, it's an access to a regular property a["4294967295"]) | |
156 | * a[999999999999999999999] (even if it wasn't above the max index, it would be a["1e+21"]) | |
157 | * a[1e310] (same as a["Infinity"]) | |
158 | * @param {ASTNode} fullNumberNode `Literal` or `UnaryExpression` full number node | |
159 | * @param {bigint|number} value Value expressed by the fullNumberNode | |
160 | * @returns {boolean} true if the node is a valid array index | |
161 | */ | |
162 | function isArrayIndex(fullNumberNode, value) { | |
163 | const parent = fullNumberNode.parent; | |
164 | ||
165 | return parent.type === "MemberExpression" && parent.property === fullNumberNode && | |
166 | (Number.isInteger(value) || typeof value === "bigint") && | |
167 | value >= 0 && value < MAX_ARRAY_LENGTH; | |
168 | } | |
169 | ||
170 | return { | |
171 | Literal(node) { | |
6f036462 | 172 | if (!astUtils.isNumericLiteral(node)) { |
eb39fafa DC |
173 | return; |
174 | } | |
175 | ||
176 | let fullNumberNode; | |
177 | let value; | |
178 | let raw; | |
179 | ||
180 | // Treat unary minus as a part of the number | |
181 | if (node.parent.type === "UnaryExpression" && node.parent.operator === "-") { | |
182 | fullNumberNode = node.parent; | |
183 | value = -node.value; | |
184 | raw = `-${node.raw}`; | |
185 | } else { | |
186 | fullNumberNode = node; | |
187 | value = node.value; | |
188 | raw = node.raw; | |
189 | } | |
190 | ||
6f036462 TL |
191 | const parent = fullNumberNode.parent; |
192 | ||
eb39fafa DC |
193 | // Always allow radix arguments and JSX props |
194 | if ( | |
195 | isIgnoredValue(value) || | |
6f036462 | 196 | (ignoreDefaultValues && isDefaultValue(fullNumberNode)) || |
eb39fafa DC |
197 | isParseIntRadix(fullNumberNode) || |
198 | isJSXNumber(fullNumberNode) || | |
199 | (ignoreArrayIndexes && isArrayIndex(fullNumberNode, value)) | |
200 | ) { | |
201 | return; | |
202 | } | |
203 | ||
eb39fafa DC |
204 | if (parent.type === "VariableDeclarator") { |
205 | if (enforceConst && parent.parent.kind !== "const") { | |
206 | context.report({ | |
207 | node: fullNumberNode, | |
208 | messageId: "useConst" | |
209 | }); | |
210 | } | |
211 | } else if ( | |
8f9d1d4d | 212 | !okTypes.includes(parent.type) || |
eb39fafa DC |
213 | (parent.type === "AssignmentExpression" && parent.left.type === "Identifier") |
214 | ) { | |
215 | context.report({ | |
216 | node: fullNumberNode, | |
217 | messageId: "noMagic", | |
218 | data: { | |
219 | raw | |
220 | } | |
221 | }); | |
222 | } | |
223 | } | |
224 | }; | |
225 | } | |
226 | }; |