radix.js 6.7 KB

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