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}; 227