| /** |
| * @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/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; |
| |
| /** |
| * 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 sourceCode = context.getSourceCode(); |
| 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"() { |
| const scope = context.getScope(); |
| let variable; |
| |
| // Check `parseInt()` |
| variable = astUtils.getVariableByName(scope, "parseInt"); |
| if (variable && !isShadowed(variable)) { |
| variable.references.forEach(reference => { |
| const node = reference.identifier; |
| |
| if (astUtils.isCallee(node)) { |
| checkArguments(node.parent); |
| } |
| }); |
| } |
| |
| // Check `Number.parseInt()` |
| variable = astUtils.getVariableByName(scope, "Number"); |
| if (variable && !isShadowed(variable)) { |
| variable.references.forEach(reference => { |
| const node = reference.identifier.parent; |
| const maybeCallee = node.parent.type === "ChainExpression" |
| ? node.parent |
| : node; |
| |
| if (isParseIntMethod(node) && astUtils.isCallee(maybeCallee)) { |
| checkArguments(maybeCallee.parent); |
| } |
| }); |
| } |
| } |
| }; |
| } |
| }; |