You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

209 lines
7.7 KiB

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