2 * @fileoverview Rule to flag statements that use magic numbers (adapted from https://github.com/danielstjules/buddy.js)
3 * @author Vincent Lemeunier
8 const astUtils = require("./utils/ast-utils");
10 // Maximum array length by the ECMAScript Specification.
11 const MAX_ARRAY_LENGTH = 2 ** 32 - 1;
13 //------------------------------------------------------------------------------
15 //------------------------------------------------------------------------------
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.
22 function normalizeIgnoreValue(x) {
23 if (typeof x === "string") {
24 return BigInt(x.slice(0, -1));
34 description: "disallow magic numbers",
35 category: "Best Practices",
37 url: "https://eslint.org/docs/rules/no-magic-numbers"
56 { type: "string", pattern: "^[+-]?(?:0|[1-9][0-9]*)n$" }
65 ignoreDefaultValues: {
70 additionalProperties: false
74 useConst: "Number constants declarations must use 'const'.",
75 noMagic: "No magic number: {{raw}}."
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;
87 const okTypes = detectObjects ? [] : ["ObjectExpression", "Property", "AssignmentExpression"];
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
94 function isIgnoredValue(value) {
95 return ignore.indexOf(value) !== -1;
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
103 function isDefaultValue(fullNumberNode) {
104 const parent = fullNumberNode.parent;
106 return parent.type === "AssignmentPattern" && parent.right === fullNumberNode;
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
114 function isParseIntRadix(fullNumberNode) {
115 const parent = fullNumberNode.parent;
117 return parent.type === "CallExpression" && fullNumberNode === parent.arguments[1] &&
119 astUtils.isSpecificId(parent.callee, "parseInt") ||
120 astUtils.isSpecificMemberAccess(parent.callee, "Number", "parseInt")
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
131 function isJSXNumber(fullNumberNode) {
132 return fullNumberNode.parent.type.indexOf("JSX") === 0;
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".
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.
143 * The maximum array length by the specification is 2 ** 32 - 1 = 4294967295,
144 * thus the maximum valid index is 2 ** 32 - 2 = 4294967294.
146 * All notations are allowed, as long as the value coerces to one of "0", "1", "2" ... "4294967294".
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)
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
162 function isArrayIndex(fullNumberNode, value) {
163 const parent = fullNumberNode.parent;
165 return parent.type === "MemberExpression" && parent.property === fullNumberNode &&
166 (Number.isInteger(value) || typeof value === "bigint") &&
167 value >= 0 && value < MAX_ARRAY_LENGTH;
172 if (!astUtils.isNumericLiteral(node)) {
180 // Treat unary minus as a part of the number
181 if (node.parent.type === "UnaryExpression" && node.parent.operator === "-") {
182 fullNumberNode = node.parent;
184 raw = `-${node.raw}`;
186 fullNumberNode = node;
191 const parent = fullNumberNode.parent;
193 // Always allow radix arguments and JSX props
195 isIgnoredValue(value) ||
196 (ignoreDefaultValues && isDefaultValue(fullNumberNode)) ||
197 isParseIntRadix(fullNumberNode) ||
198 isJSXNumber(fullNumberNode) ||
199 (ignoreArrayIndexes && isArrayIndex(fullNumberNode, value))
204 if (parent.type === "VariableDeclarator") {
205 if (enforceConst && parent.parent.kind !== "const") {
207 node: fullNumberNode,
208 messageId: "useConst"
212 okTypes.indexOf(parent.type) === -1 ||
213 (parent.type === "AssignmentExpression" && parent.left.type === "Identifier")
216 node: fullNumberNode,
217 messageId: "noMagic",