blob: f83c762c11d39788c390a4d277f45a75ca0b8492 [file] [log] [blame]
/**
* @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);
}
});
}
}
};
}
};