| /** | |
|  * @fileoverview Rule to flag use of parseInt without a radix argument | |
|  * @author James Allardice | |
|  */ | |
| 
 | |
| "use strict"; | |
| 
 | |
| //------------------------------------------------------------------------------ | |
| // Requirements | |
| //------------------------------------------------------------------------------ | |
|  | |
| const astUtils = require("./utils/ast-utils"); | |
| 
 | |
| //------------------------------------------------------------------------------ | |
| // Helpers | |
| //------------------------------------------------------------------------------ | |
|  | |
| const MODE_ALWAYS = "always", | |
|     MODE_AS_NEEDED = "as-needed"; | |
| 
 | |
| const validRadixValues = new Set(Array.from({ length: 37 - 2 }, (_, index) => index + 2)); | |
| 
 | |
| /** | |
|  * Checks whether a given variable is shadowed or not. | |
|  * @param {eslint-scope.Variable} variable A variable to check. | |
|  * @returns {boolean} `true` if the variable is shadowed. | |
|  */ | |
| function isShadowed(variable) { | |
|     return variable.defs.length >= 1; | |
| } | |
| 
 | |
| /** | |
|  * Checks whether a given node is a MemberExpression of `parseInt` method or not. | |
|  * @param {ASTNode} node A node to check. | |
|  * @returns {boolean} `true` if the node is a MemberExpression of `parseInt` | |
|  *      method. | |
|  */ | |
| function isParseIntMethod(node) { | |
|     return ( | |
|         node.type === "MemberExpression" && | |
|         !node.computed && | |
|         node.property.type === "Identifier" && | |
|         node.property.name === "parseInt" | |
|     ); | |
| } | |
| 
 | |
| /** | |
|  * Checks whether a given node is a valid value of radix or not. | |
|  * | |
|  * The following values are invalid. | |
|  * | |
|  * - A literal except integers between 2 and 36. | |
|  * - undefined. | |
|  * @param {ASTNode} radix A node of radix to check. | |
|  * @returns {boolean} `true` if the node is valid. | |
|  */ | |
| function isValidRadix(radix) { | |
|     return !( | |
|         (radix.type === "Literal" && !validRadixValues.has(radix.value)) || | |
|         (radix.type === "Identifier" && radix.name === "undefined") | |
|     ); | |
| } | |
| 
 | |
| /** | |
|  * Checks whether a given node is a default value of radix or not. | |
|  * @param {ASTNode} radix A node of radix to check. | |
|  * @returns {boolean} `true` if the node is the literal node of `10`. | |
|  */ | |
| function isDefaultRadix(radix) { | |
|     return radix.type === "Literal" && radix.value === 10; | |
| } | |
| 
 | |
| //------------------------------------------------------------------------------ | |
| // Rule Definition | |
| //------------------------------------------------------------------------------ | |
|  | |
| /** @type {import('../shared/types').Rule} */ | |
| module.exports = { | |
|     meta: { | |
|         type: "suggestion", | |
| 
 | |
|         docs: { | |
|             description: "Enforce the consistent use of the radix argument when using `parseInt()`", | |
|             recommended: false, | |
|             url: "https://eslint.org/docs/latest/rules/radix" | |
|         }, | |
| 
 | |
|         hasSuggestions: true, | |
| 
 | |
|         schema: [ | |
|             { | |
|                 enum: ["always", "as-needed"] | |
|             } | |
|         ], | |
| 
 | |
|         messages: { | |
|             missingParameters: "Missing parameters.", | |
|             redundantRadix: "Redundant radix parameter.", | |
|             missingRadix: "Missing radix parameter.", | |
|             invalidRadix: "Invalid radix parameter, must be an integer between 2 and 36.", | |
|             addRadixParameter10: "Add radix parameter `10` for parsing decimal numbers." | |
|         } | |
|     }, | |
| 
 | |
|     create(context) { | |
|         const mode = context.options[0] || MODE_ALWAYS; | |
|         const sourceCode = context.sourceCode; | |
| 
 | |
|         /** | |
|          * Checks the arguments of a given CallExpression node and reports it if it | |
|          * offends this rule. | |
|          * @param {ASTNode} node A CallExpression node to check. | |
|          * @returns {void} | |
|          */ | |
|         function checkArguments(node) { | |
|             const args = node.arguments; | |
| 
 | |
|             switch (args.length) { | |
|                 case 0: | |
|                     context.report({ | |
|                         node, | |
|                         messageId: "missingParameters" | |
|                     }); | |
|                     break; | |
| 
 | |
|                 case 1: | |
|                     if (mode === MODE_ALWAYS) { | |
|                         context.report({ | |
|                             node, | |
|                             messageId: "missingRadix", | |
|                             suggest: [ | |
|                                 { | |
|                                     messageId: "addRadixParameter10", | |
|                                     fix(fixer) { | |
|                                         const tokens = sourceCode.getTokens(node); | |
|                                         const lastToken = tokens[tokens.length - 1]; // Parenthesis. | |
|                                         const secondToLastToken = tokens[tokens.length - 2]; // May or may not be a comma. | |
|                                         const hasTrailingComma = secondToLastToken.type === "Punctuator" && secondToLastToken.value === ","; | |
| 
 | |
|                                         return fixer.insertTextBefore(lastToken, hasTrailingComma ? " 10," : ", 10"); | |
|                                     } | |
|                                 } | |
|                             ] | |
|                         }); | |
|                     } | |
|                     break; | |
| 
 | |
|                 default: | |
|                     if (mode === MODE_AS_NEEDED && isDefaultRadix(args[1])) { | |
|                         context.report({ | |
|                             node, | |
|                             messageId: "redundantRadix" | |
|                         }); | |
|                     } else if (!isValidRadix(args[1])) { | |
|                         context.report({ | |
|                             node, | |
|                             messageId: "invalidRadix" | |
|                         }); | |
|                     } | |
|                     break; | |
|             } | |
|         } | |
| 
 | |
|         return { | |
|             "Program:exit"(node) { | |
|                 const scope = sourceCode.getScope(node); | |
|                 let variable; | |
| 
 | |
|                 // Check `parseInt()` | |
|                 variable = astUtils.getVariableByName(scope, "parseInt"); | |
|                 if (variable && !isShadowed(variable)) { | |
|                     variable.references.forEach(reference => { | |
|                         const idNode = reference.identifier; | |
| 
 | |
|                         if (astUtils.isCallee(idNode)) { | |
|                             checkArguments(idNode.parent); | |
|                         } | |
|                     }); | |
|                 } | |
| 
 | |
|                 // Check `Number.parseInt()` | |
|                 variable = astUtils.getVariableByName(scope, "Number"); | |
|                 if (variable && !isShadowed(variable)) { | |
|                     variable.references.forEach(reference => { | |
|                         const parentNode = reference.identifier.parent; | |
|                         const maybeCallee = parentNode.parent.type === "ChainExpression" | |
|                             ? parentNode.parent | |
|                             : parentNode; | |
| 
 | |
|                         if (isParseIntMethod(parentNode) && astUtils.isCallee(maybeCallee)) { | |
|                             checkArguments(maybeCallee.parent); | |
|                         } | |
|                     }); | |
|                 } | |
|             } | |
|         }; | |
|     } | |
| };
 |