UNPKG

8.65 kBJavaScriptView Raw
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
8const astUtils = require("./utils/ast-utils");
9
10// Maximum array length by the ECMAScript Specification.
11const 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 */
22function normalizeIgnoreValue(x) {
23 if (typeof x === "string") {
24 return BigInt(x.slice(0, -1));
25 }
26 return x;
27}
28
29module.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};