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 | ;
|
7 |
|
8 | const astUtils = require("./utils/ast-utils");
|
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 |
|
29 | module.exports = {
|
30 | meta: {
|
31 | type: "suggestion",
|
32 |
|
33 | docs: {
|
34 | description: "disallow magic numbers",
|
35 | category: "Best Practices",
|
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
|
64 | },
|
65 | ignoreDefaultValues: {
|
66 | type: "boolean",
|
67 | default: false
|
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,
|
83 | ignore = (config.ignore || []).map(normalizeIgnoreValue),
|
84 | ignoreArrayIndexes = !!config.ignoreArrayIndexes,
|
85 | ignoreDefaultValues = !!config.ignoreDefaultValues;
|
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) {
|
95 | return ignore.indexOf(value) !== -1;
|
96 | }
|
97 |
|
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 |
|
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 | (
|
119 | astUtils.isSpecificId(parent.callee, "parseInt") ||
|
120 | astUtils.isSpecificMemberAccess(parent.callee, "Number", "parseInt")
|
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) {
|
172 | if (!astUtils.isNumericLiteral(node)) {
|
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 |
|
191 | const parent = fullNumberNode.parent;
|
192 |
|
193 | // Always allow radix arguments and JSX props
|
194 | if (
|
195 | isIgnoredValue(value) ||
|
196 | (ignoreDefaultValues && isDefaultValue(fullNumberNode)) ||
|
197 | isParseIntRadix(fullNumberNode) ||
|
198 | isJSXNumber(fullNumberNode) ||
|
199 | (ignoreArrayIndexes && isArrayIndex(fullNumberNode, value))
|
200 | ) {
|
201 | return;
|
202 | }
|
203 |
|
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 (
|
212 | okTypes.indexOf(parent.type) === -1 ||
|
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 | };
|