no-magic-numbers.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  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. "use strict";
  6. const astUtils = require("./utils/ast-utils");
  7. // Maximum array length by the ECMAScript Specification.
  8. const MAX_ARRAY_LENGTH = 2 ** 32 - 1;
  9. //------------------------------------------------------------------------------
  10. // Rule Definition
  11. //------------------------------------------------------------------------------
  12. /**
  13. * Convert the value to bigint if it's a string. Otherwise return the value as-is.
  14. * @param {bigint|number|string} x The value to normalize.
  15. * @returns {bigint|number} The normalized value.
  16. */
  17. function normalizeIgnoreValue(x) {
  18. if (typeof x === "string") {
  19. return BigInt(x.slice(0, -1));
  20. }
  21. return x;
  22. }
  23. /**
  24. * Checks if the node parent is a TypeScript enum member
  25. * @param {ASTNode} node The node to be validated
  26. * @returns {boolean} True if the node parent is a TypeScript enum member
  27. */
  28. function isParentTSEnumDeclaration(node) {
  29. return node.parent.type === "TSEnumMember";
  30. }
  31. /**
  32. * Checks if the node is a valid TypeScript numeric literal type.
  33. * @param {ASTNode} node The node to be validated
  34. * @returns {boolean} True if the node is a TypeScript numeric literal type
  35. */
  36. function isTSNumericLiteralType(node) {
  37. let ancestor = node.parent;
  38. // Go up while we're part of a type union
  39. while (ancestor.parent.type === "TSUnionType") {
  40. ancestor = ancestor.parent;
  41. }
  42. // Check if the final ancestor is in a type alias declaration
  43. return ancestor.parent.type === "TSTypeAliasDeclaration";
  44. }
  45. /**
  46. * Checks if the node parent is a readonly class property
  47. * @param {ASTNode} node The node to be validated
  48. * @returns {boolean} True if the node parent is a readonly class property
  49. */
  50. function isParentTSReadonlyPropertyDefinition(node) {
  51. if (node.parent?.type === "PropertyDefinition" && node.parent.readonly) {
  52. return true;
  53. }
  54. return false;
  55. }
  56. /**
  57. * Checks if the node is part of a type indexed access (eg. Foo[4])
  58. * @param {ASTNode} node The node to be validated
  59. * @returns {boolean} True if the node is part of an indexed access
  60. */
  61. function isAncestorTSIndexedAccessType(node) {
  62. let ancestor = node.parent;
  63. /*
  64. * Go up another level while we're part of a type union (eg. 1 | 2) or
  65. * intersection (eg. 1 & 2)
  66. */
  67. while (
  68. ancestor.parent.type === "TSUnionType" ||
  69. ancestor.parent.type === "TSIntersectionType"
  70. ) {
  71. ancestor = ancestor.parent;
  72. }
  73. return ancestor.parent.type === "TSIndexedAccessType";
  74. }
  75. /** @type {import('../types').Rule.RuleModule} */
  76. module.exports = {
  77. meta: {
  78. type: "suggestion",
  79. dialects: ["typescript", "javascript"],
  80. language: "javascript",
  81. docs: {
  82. description: "Disallow magic numbers",
  83. recommended: false,
  84. frozen: true,
  85. url: "https://eslint.org/docs/latest/rules/no-magic-numbers",
  86. },
  87. schema: [
  88. {
  89. type: "object",
  90. properties: {
  91. detectObjects: {
  92. type: "boolean",
  93. default: false,
  94. },
  95. enforceConst: {
  96. type: "boolean",
  97. default: false,
  98. },
  99. ignore: {
  100. type: "array",
  101. items: {
  102. anyOf: [
  103. { type: "number" },
  104. {
  105. type: "string",
  106. pattern: "^[+-]?(?:0|[1-9][0-9]*)n$",
  107. },
  108. ],
  109. },
  110. uniqueItems: true,
  111. },
  112. ignoreArrayIndexes: {
  113. type: "boolean",
  114. default: false,
  115. },
  116. ignoreDefaultValues: {
  117. type: "boolean",
  118. default: false,
  119. },
  120. ignoreClassFieldInitialValues: {
  121. type: "boolean",
  122. default: false,
  123. },
  124. ignoreEnums: {
  125. type: "boolean",
  126. default: false,
  127. },
  128. ignoreNumericLiteralTypes: {
  129. type: "boolean",
  130. default: false,
  131. },
  132. ignoreReadonlyClassProperties: {
  133. type: "boolean",
  134. default: false,
  135. },
  136. ignoreTypeIndexes: {
  137. type: "boolean",
  138. default: false,
  139. },
  140. },
  141. additionalProperties: false,
  142. },
  143. ],
  144. messages: {
  145. useConst: "Number constants declarations must use 'const'.",
  146. noMagic: "No magic number: {{raw}}.",
  147. },
  148. },
  149. create(context) {
  150. const config = context.options[0] || {},
  151. detectObjects = !!config.detectObjects,
  152. enforceConst = !!config.enforceConst,
  153. ignore = new Set((config.ignore || []).map(normalizeIgnoreValue)),
  154. ignoreArrayIndexes = !!config.ignoreArrayIndexes,
  155. ignoreDefaultValues = !!config.ignoreDefaultValues,
  156. ignoreClassFieldInitialValues =
  157. !!config.ignoreClassFieldInitialValues,
  158. ignoreEnums = !!config.ignoreEnums,
  159. ignoreNumericLiteralTypes = !!config.ignoreNumericLiteralTypes,
  160. ignoreReadonlyClassProperties =
  161. !!config.ignoreReadonlyClassProperties,
  162. ignoreTypeIndexes = !!config.ignoreTypeIndexes;
  163. const okTypes = detectObjects
  164. ? []
  165. : ["ObjectExpression", "Property", "AssignmentExpression"];
  166. /**
  167. * Returns whether the rule is configured to ignore the given value
  168. * @param {bigint|number} value The value to check
  169. * @returns {boolean} true if the value is ignored
  170. */
  171. function isIgnoredValue(value) {
  172. return ignore.has(value);
  173. }
  174. /**
  175. * Returns whether the number is a default value assignment.
  176. * @param {ASTNode} fullNumberNode `Literal` or `UnaryExpression` full number node
  177. * @returns {boolean} true if the number is a default value
  178. */
  179. function isDefaultValue(fullNumberNode) {
  180. const parent = fullNumberNode.parent;
  181. return (
  182. parent.type === "AssignmentPattern" &&
  183. parent.right === fullNumberNode
  184. );
  185. }
  186. /**
  187. * Returns whether the number is the initial value of a class field.
  188. * @param {ASTNode} fullNumberNode `Literal` or `UnaryExpression` full number node
  189. * @returns {boolean} true if the number is the initial value of a class field.
  190. */
  191. function isClassFieldInitialValue(fullNumberNode) {
  192. const parent = fullNumberNode.parent;
  193. return (
  194. parent.type === "PropertyDefinition" &&
  195. parent.value === fullNumberNode
  196. );
  197. }
  198. /**
  199. * Returns whether the given node is used as a radix within parseInt() or Number.parseInt()
  200. * @param {ASTNode} fullNumberNode `Literal` or `UnaryExpression` full number node
  201. * @returns {boolean} true if the node is radix
  202. */
  203. function isParseIntRadix(fullNumberNode) {
  204. const parent = fullNumberNode.parent;
  205. return (
  206. parent.type === "CallExpression" &&
  207. fullNumberNode === parent.arguments[1] &&
  208. (astUtils.isSpecificId(parent.callee, "parseInt") ||
  209. astUtils.isSpecificMemberAccess(
  210. parent.callee,
  211. "Number",
  212. "parseInt",
  213. ))
  214. );
  215. }
  216. /**
  217. * Returns whether the given node is a direct child of a JSX node.
  218. * In particular, it aims to detect numbers used as prop values in JSX tags.
  219. * Example: <input maxLength={10} />
  220. * @param {ASTNode} fullNumberNode `Literal` or `UnaryExpression` full number node
  221. * @returns {boolean} true if the node is a JSX number
  222. */
  223. function isJSXNumber(fullNumberNode) {
  224. return fullNumberNode.parent.type.indexOf("JSX") === 0;
  225. }
  226. /**
  227. * Returns whether the given node is used as an array index.
  228. * Value must coerce to a valid array index name: "0", "1", "2" ... "4294967294".
  229. *
  230. * All other values, like "-1", "2.5", or "4294967295", are just "normal" object properties,
  231. * which can be created and accessed on an array in addition to the array index properties,
  232. * but they don't affect array's length and are not considered by methods such as .map(), .forEach() etc.
  233. *
  234. * The maximum array length by the specification is 2 ** 32 - 1 = 4294967295,
  235. * thus the maximum valid index is 2 ** 32 - 2 = 4294967294.
  236. *
  237. * All notations are allowed, as long as the value coerces to one of "0", "1", "2" ... "4294967294".
  238. *
  239. * Valid examples:
  240. * a[0], a[1], a[1.2e1], a[0xAB], a[0n], a[1n]
  241. * a[-0] (same as a[0] because -0 coerces to "0")
  242. * a[-0n] (-0n evaluates to 0n)
  243. *
  244. * Invalid examples:
  245. * a[-1], a[-0xAB], a[-1n], a[2.5], a[1.23e1], a[12e-1]
  246. * a[4294967295] (above the max index, it's an access to a regular property a["4294967295"])
  247. * a[999999999999999999999] (even if it wasn't above the max index, it would be a["1e+21"])
  248. * a[1e310] (same as a["Infinity"])
  249. * @param {ASTNode} fullNumberNode `Literal` or `UnaryExpression` full number node
  250. * @param {bigint|number} value Value expressed by the fullNumberNode
  251. * @returns {boolean} true if the node is a valid array index
  252. */
  253. function isArrayIndex(fullNumberNode, value) {
  254. const parent = fullNumberNode.parent;
  255. return (
  256. parent.type === "MemberExpression" &&
  257. parent.property === fullNumberNode &&
  258. (Number.isInteger(value) || typeof value === "bigint") &&
  259. value >= 0 &&
  260. value < MAX_ARRAY_LENGTH
  261. );
  262. }
  263. return {
  264. Literal(node) {
  265. if (!astUtils.isNumericLiteral(node)) {
  266. return;
  267. }
  268. let fullNumberNode;
  269. let value;
  270. let raw;
  271. // Treat unary minus/plus as a part of the number
  272. if (
  273. node.parent.type === "UnaryExpression" &&
  274. ["-", "+"].includes(node.parent.operator)
  275. ) {
  276. fullNumberNode = node.parent;
  277. value =
  278. node.parent.operator === "-" ? -node.value : node.value;
  279. raw = `${node.parent.operator}${node.raw}`;
  280. } else {
  281. fullNumberNode = node;
  282. value = node.value;
  283. raw = node.raw;
  284. }
  285. const parent = fullNumberNode.parent;
  286. // Always allow radix arguments and JSX props
  287. if (
  288. isIgnoredValue(value) ||
  289. (ignoreDefaultValues && isDefaultValue(fullNumberNode)) ||
  290. (ignoreClassFieldInitialValues &&
  291. isClassFieldInitialValue(fullNumberNode)) ||
  292. (ignoreEnums &&
  293. isParentTSEnumDeclaration(fullNumberNode)) ||
  294. (ignoreNumericLiteralTypes &&
  295. isTSNumericLiteralType(fullNumberNode)) ||
  296. (ignoreTypeIndexes &&
  297. isAncestorTSIndexedAccessType(fullNumberNode)) ||
  298. (ignoreReadonlyClassProperties &&
  299. isParentTSReadonlyPropertyDefinition(fullNumberNode)) ||
  300. isParseIntRadix(fullNumberNode) ||
  301. isJSXNumber(fullNumberNode) ||
  302. (ignoreArrayIndexes && isArrayIndex(fullNumberNode, value))
  303. ) {
  304. return;
  305. }
  306. if (parent.type === "VariableDeclarator") {
  307. if (enforceConst && parent.parent.kind !== "const") {
  308. context.report({
  309. node: fullNumberNode,
  310. messageId: "useConst",
  311. });
  312. }
  313. } else if (
  314. !okTypes.includes(parent.type) ||
  315. (parent.type === "AssignmentExpression" &&
  316. parent.left.type === "Identifier")
  317. ) {
  318. context.report({
  319. node: fullNumberNode,
  320. messageId: "noMagic",
  321. data: {
  322. raw,
  323. },
  324. });
  325. }
  326. },
  327. };
  328. },
  329. };