no-magic-numbers.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  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. module.exports = {
  24. meta: {
  25. type: "suggestion",
  26. docs: {
  27. description: "disallow magic numbers",
  28. category: "Best Practices",
  29. recommended: false,
  30. url: "https://eslint.org/docs/rules/no-magic-numbers"
  31. },
  32. schema: [{
  33. type: "object",
  34. properties: {
  35. detectObjects: {
  36. type: "boolean",
  37. default: false
  38. },
  39. enforceConst: {
  40. type: "boolean",
  41. default: false
  42. },
  43. ignore: {
  44. type: "array",
  45. items: {
  46. anyOf: [
  47. { type: "number" },
  48. { type: "string", pattern: "^[+-]?(?:0|[1-9][0-9]*)n$" }
  49. ]
  50. },
  51. uniqueItems: true
  52. },
  53. ignoreArrayIndexes: {
  54. type: "boolean",
  55. default: false
  56. },
  57. ignoreDefaultValues: {
  58. type: "boolean",
  59. default: false
  60. }
  61. },
  62. additionalProperties: false
  63. }],
  64. messages: {
  65. useConst: "Number constants declarations must use 'const'.",
  66. noMagic: "No magic number: {{raw}}."
  67. }
  68. },
  69. create(context) {
  70. const config = context.options[0] || {},
  71. detectObjects = !!config.detectObjects,
  72. enforceConst = !!config.enforceConst,
  73. ignore = (config.ignore || []).map(normalizeIgnoreValue),
  74. ignoreArrayIndexes = !!config.ignoreArrayIndexes,
  75. ignoreDefaultValues = !!config.ignoreDefaultValues;
  76. const okTypes = detectObjects ? [] : ["ObjectExpression", "Property", "AssignmentExpression"];
  77. /**
  78. * Returns whether the rule is configured to ignore the given value
  79. * @param {bigint|number} value The value to check
  80. * @returns {boolean} true if the value is ignored
  81. */
  82. function isIgnoredValue(value) {
  83. return ignore.indexOf(value) !== -1;
  84. }
  85. /**
  86. * Returns whether the number is a default value assignment.
  87. * @param {ASTNode} fullNumberNode `Literal` or `UnaryExpression` full number node
  88. * @returns {boolean} true if the number is a default value
  89. */
  90. function isDefaultValue(fullNumberNode) {
  91. const parent = fullNumberNode.parent;
  92. return parent.type === "AssignmentPattern" && parent.right === fullNumberNode;
  93. }
  94. /**
  95. * Returns whether the given node is used as a radix within parseInt() or Number.parseInt()
  96. * @param {ASTNode} fullNumberNode `Literal` or `UnaryExpression` full number node
  97. * @returns {boolean} true if the node is radix
  98. */
  99. function isParseIntRadix(fullNumberNode) {
  100. const parent = fullNumberNode.parent;
  101. return parent.type === "CallExpression" && fullNumberNode === parent.arguments[1] &&
  102. (
  103. astUtils.isSpecificId(parent.callee, "parseInt") ||
  104. astUtils.isSpecificMemberAccess(parent.callee, "Number", "parseInt")
  105. );
  106. }
  107. /**
  108. * Returns whether the given node is a direct child of a JSX node.
  109. * In particular, it aims to detect numbers used as prop values in JSX tags.
  110. * Example: <input maxLength={10} />
  111. * @param {ASTNode} fullNumberNode `Literal` or `UnaryExpression` full number node
  112. * @returns {boolean} true if the node is a JSX number
  113. */
  114. function isJSXNumber(fullNumberNode) {
  115. return fullNumberNode.parent.type.indexOf("JSX") === 0;
  116. }
  117. /**
  118. * Returns whether the given node is used as an array index.
  119. * Value must coerce to a valid array index name: "0", "1", "2" ... "4294967294".
  120. *
  121. * All other values, like "-1", "2.5", or "4294967295", are just "normal" object properties,
  122. * which can be created and accessed on an array in addition to the array index properties,
  123. * but they don't affect array's length and are not considered by methods such as .map(), .forEach() etc.
  124. *
  125. * The maximum array length by the specification is 2 ** 32 - 1 = 4294967295,
  126. * thus the maximum valid index is 2 ** 32 - 2 = 4294967294.
  127. *
  128. * All notations are allowed, as long as the value coerces to one of "0", "1", "2" ... "4294967294".
  129. *
  130. * Valid examples:
  131. * a[0], a[1], a[1.2e1], a[0xAB], a[0n], a[1n]
  132. * a[-0] (same as a[0] because -0 coerces to "0")
  133. * a[-0n] (-0n evaluates to 0n)
  134. *
  135. * Invalid examples:
  136. * a[-1], a[-0xAB], a[-1n], a[2.5], a[1.23e1], a[12e-1]
  137. * a[4294967295] (above the max index, it's an access to a regular property a["4294967295"])
  138. * a[999999999999999999999] (even if it wasn't above the max index, it would be a["1e+21"])
  139. * a[1e310] (same as a["Infinity"])
  140. * @param {ASTNode} fullNumberNode `Literal` or `UnaryExpression` full number node
  141. * @param {bigint|number} value Value expressed by the fullNumberNode
  142. * @returns {boolean} true if the node is a valid array index
  143. */
  144. function isArrayIndex(fullNumberNode, value) {
  145. const parent = fullNumberNode.parent;
  146. return parent.type === "MemberExpression" && parent.property === fullNumberNode &&
  147. (Number.isInteger(value) || typeof value === "bigint") &&
  148. value >= 0 && value < MAX_ARRAY_LENGTH;
  149. }
  150. return {
  151. Literal(node) {
  152. if (!astUtils.isNumericLiteral(node)) {
  153. return;
  154. }
  155. let fullNumberNode;
  156. let value;
  157. let raw;
  158. // Treat unary minus as a part of the number
  159. if (node.parent.type === "UnaryExpression" && node.parent.operator === "-") {
  160. fullNumberNode = node.parent;
  161. value = -node.value;
  162. raw = `-${node.raw}`;
  163. } else {
  164. fullNumberNode = node;
  165. value = node.value;
  166. raw = node.raw;
  167. }
  168. const parent = fullNumberNode.parent;
  169. // Always allow radix arguments and JSX props
  170. if (
  171. isIgnoredValue(value) ||
  172. (ignoreDefaultValues && isDefaultValue(fullNumberNode)) ||
  173. isParseIntRadix(fullNumberNode) ||
  174. isJSXNumber(fullNumberNode) ||
  175. (ignoreArrayIndexes && isArrayIndex(fullNumberNode, value))
  176. ) {
  177. return;
  178. }
  179. if (parent.type === "VariableDeclarator") {
  180. if (enforceConst && parent.parent.kind !== "const") {
  181. context.report({
  182. node: fullNumberNode,
  183. messageId: "useConst"
  184. });
  185. }
  186. } else if (
  187. okTypes.indexOf(parent.type) === -1 ||
  188. (parent.type === "AssignmentExpression" && parent.left.type === "Identifier")
  189. ) {
  190. context.report({
  191. node: fullNumberNode,
  192. messageId: "noMagic",
  193. data: {
  194. raw
  195. }
  196. });
  197. }
  198. }
  199. };
  200. }
  201. };