no-magic-numbers.js 9.2 KB

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