radix.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. /**
  2. * @fileoverview Rule to flag use of parseInt without a radix argument
  3. * @author James Allardice
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const astUtils = require("./utils/ast-utils");
  10. //------------------------------------------------------------------------------
  11. // Helpers
  12. //------------------------------------------------------------------------------
  13. const MODE_ALWAYS = "always",
  14. MODE_AS_NEEDED = "as-needed";
  15. const validRadixValues = new Set(
  16. Array.from({ length: 37 - 2 }, (_, index) => index + 2),
  17. );
  18. /**
  19. * Checks whether a given variable is shadowed or not.
  20. * @param {eslint-scope.Variable} variable A variable to check.
  21. * @returns {boolean} `true` if the variable is shadowed.
  22. */
  23. function isShadowed(variable) {
  24. return variable.defs.length >= 1;
  25. }
  26. /**
  27. * Checks whether a given node is a MemberExpression of `parseInt` method or not.
  28. * @param {ASTNode} node A node to check.
  29. * @returns {boolean} `true` if the node is a MemberExpression of `parseInt`
  30. * method.
  31. */
  32. function isParseIntMethod(node) {
  33. return (
  34. node.type === "MemberExpression" &&
  35. !node.computed &&
  36. node.property.type === "Identifier" &&
  37. node.property.name === "parseInt"
  38. );
  39. }
  40. /**
  41. * Checks whether a given node is a valid value of radix or not.
  42. *
  43. * The following values are invalid.
  44. *
  45. * - A literal except integers between 2 and 36.
  46. * - undefined.
  47. * @param {ASTNode} radix A node of radix to check.
  48. * @returns {boolean} `true` if the node is valid.
  49. */
  50. function isValidRadix(radix) {
  51. return !(
  52. (radix.type === "Literal" && !validRadixValues.has(radix.value)) ||
  53. (radix.type === "Identifier" && radix.name === "undefined")
  54. );
  55. }
  56. /**
  57. * Checks whether a given node is a default value of radix or not.
  58. * @param {ASTNode} radix A node of radix to check.
  59. * @returns {boolean} `true` if the node is the literal node of `10`.
  60. */
  61. function isDefaultRadix(radix) {
  62. return radix.type === "Literal" && radix.value === 10;
  63. }
  64. //------------------------------------------------------------------------------
  65. // Rule Definition
  66. //------------------------------------------------------------------------------
  67. /** @type {import('../types').Rule.RuleModule} */
  68. module.exports = {
  69. meta: {
  70. type: "suggestion",
  71. defaultOptions: [MODE_ALWAYS],
  72. docs: {
  73. description:
  74. "Enforce the consistent use of the radix argument when using `parseInt()`",
  75. recommended: false,
  76. url: "https://eslint.org/docs/latest/rules/radix",
  77. },
  78. hasSuggestions: true,
  79. schema: [
  80. {
  81. enum: ["always", "as-needed"],
  82. },
  83. ],
  84. messages: {
  85. missingParameters: "Missing parameters.",
  86. redundantRadix: "Redundant radix parameter.",
  87. missingRadix: "Missing radix parameter.",
  88. invalidRadix:
  89. "Invalid radix parameter, must be an integer between 2 and 36.",
  90. addRadixParameter10:
  91. "Add radix parameter `10` for parsing decimal numbers.",
  92. },
  93. },
  94. create(context) {
  95. const [mode] = context.options;
  96. const sourceCode = context.sourceCode;
  97. /**
  98. * Checks the arguments of a given CallExpression node and reports it if it
  99. * offends this rule.
  100. * @param {ASTNode} node A CallExpression node to check.
  101. * @returns {void}
  102. */
  103. function checkArguments(node) {
  104. const args = node.arguments;
  105. switch (args.length) {
  106. case 0:
  107. context.report({
  108. node,
  109. messageId: "missingParameters",
  110. });
  111. break;
  112. case 1:
  113. if (mode === MODE_ALWAYS) {
  114. context.report({
  115. node,
  116. messageId: "missingRadix",
  117. suggest: [
  118. {
  119. messageId: "addRadixParameter10",
  120. fix(fixer) {
  121. const tokens =
  122. sourceCode.getTokens(node);
  123. const lastToken = tokens.at(-1); // Parenthesis.
  124. const secondToLastToken = tokens.at(-2); // May or may not be a comma.
  125. const hasTrailingComma =
  126. secondToLastToken.type ===
  127. "Punctuator" &&
  128. secondToLastToken.value === ",";
  129. return fixer.insertTextBefore(
  130. lastToken,
  131. hasTrailingComma ? " 10," : ", 10",
  132. );
  133. },
  134. },
  135. ],
  136. });
  137. }
  138. break;
  139. default:
  140. if (mode === MODE_AS_NEEDED && isDefaultRadix(args[1])) {
  141. context.report({
  142. node,
  143. messageId: "redundantRadix",
  144. });
  145. } else if (!isValidRadix(args[1])) {
  146. context.report({
  147. node,
  148. messageId: "invalidRadix",
  149. });
  150. }
  151. break;
  152. }
  153. }
  154. return {
  155. "Program:exit"(node) {
  156. const scope = sourceCode.getScope(node);
  157. let variable;
  158. // Check `parseInt()`
  159. variable = astUtils.getVariableByName(scope, "parseInt");
  160. if (variable && !isShadowed(variable)) {
  161. variable.references.forEach(reference => {
  162. const idNode = reference.identifier;
  163. if (astUtils.isCallee(idNode)) {
  164. checkArguments(idNode.parent);
  165. }
  166. });
  167. }
  168. // Check `Number.parseInt()`
  169. variable = astUtils.getVariableByName(scope, "Number");
  170. if (variable && !isShadowed(variable)) {
  171. variable.references.forEach(reference => {
  172. const parentNode = reference.identifier.parent;
  173. const maybeCallee =
  174. parentNode.parent.type === "ChainExpression"
  175. ? parentNode.parent
  176. : parentNode;
  177. if (
  178. isParseIntMethod(parentNode) &&
  179. astUtils.isCallee(maybeCallee)
  180. ) {
  181. checkArguments(maybeCallee.parent);
  182. }
  183. });
  184. }
  185. },
  186. };
  187. },
  188. };