radix.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  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. module.exports = {
  66. meta: {
  67. type: "suggestion",
  68. docs: {
  69. description: "enforce the consistent use of the radix argument when using `parseInt()`",
  70. category: "Best Practices",
  71. recommended: false,
  72. url: "https://eslint.org/docs/rules/radix"
  73. },
  74. schema: [
  75. {
  76. enum: ["always", "as-needed"]
  77. }
  78. ],
  79. messages: {
  80. missingParameters: "Missing parameters.",
  81. redundantRadix: "Redundant radix parameter.",
  82. missingRadix: "Missing radix parameter.",
  83. invalidRadix: "Invalid radix parameter, must be an integer between 2 and 36."
  84. }
  85. },
  86. create(context) {
  87. const mode = context.options[0] || MODE_ALWAYS;
  88. /**
  89. * Checks the arguments of a given CallExpression node and reports it if it
  90. * offends this rule.
  91. * @param {ASTNode} node A CallExpression node to check.
  92. * @returns {void}
  93. */
  94. function checkArguments(node) {
  95. const args = node.arguments;
  96. switch (args.length) {
  97. case 0:
  98. context.report({
  99. node,
  100. messageId: "missingParameters"
  101. });
  102. break;
  103. case 1:
  104. if (mode === MODE_ALWAYS) {
  105. context.report({
  106. node,
  107. messageId: "missingRadix"
  108. });
  109. }
  110. break;
  111. default:
  112. if (mode === MODE_AS_NEEDED && isDefaultRadix(args[1])) {
  113. context.report({
  114. node,
  115. messageId: "redundantRadix"
  116. });
  117. } else if (!isValidRadix(args[1])) {
  118. context.report({
  119. node,
  120. messageId: "invalidRadix"
  121. });
  122. }
  123. break;
  124. }
  125. }
  126. return {
  127. "Program:exit"() {
  128. const scope = context.getScope();
  129. let variable;
  130. // Check `parseInt()`
  131. variable = astUtils.getVariableByName(scope, "parseInt");
  132. if (variable && !isShadowed(variable)) {
  133. variable.references.forEach(reference => {
  134. const node = reference.identifier;
  135. if (astUtils.isCallee(node)) {
  136. checkArguments(node.parent);
  137. }
  138. });
  139. }
  140. // Check `Number.parseInt()`
  141. variable = astUtils.getVariableByName(scope, "Number");
  142. if (variable && !isShadowed(variable)) {
  143. variable.references.forEach(reference => {
  144. const node = reference.identifier.parent;
  145. const maybeCallee = node.parent.type === "ChainExpression"
  146. ? node.parent
  147. : node;
  148. if (isParseIntMethod(node) && astUtils.isCallee(maybeCallee)) {
  149. checkArguments(maybeCallee.parent);
  150. }
  151. });
  152. }
  153. }
  154. };
  155. }
  156. };