blob: 53a515368960fc74765491661b25c49d19b8402a [file] [log] [blame]
/**
* @fileoverview Rule to disallow `parseInt()` in favor of binary, octal, and hexadecimal literals
* @author Annie Zhang, Henry Zhu
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const radixMap = new Map([
[2, { system: "binary", literalPrefix: "0b" }],
[8, { system: "octal", literalPrefix: "0o" }],
[16, { system: "hexadecimal", literalPrefix: "0x" }]
]);
/**
* Checks to see if a CallExpression's callee node is `parseInt` or
* `Number.parseInt`.
* @param {ASTNode} calleeNode The callee node to evaluate.
* @returns {boolean} True if the callee is `parseInt` or `Number.parseInt`,
* false otherwise.
*/
function isParseInt(calleeNode) {
return (
astUtils.isSpecificId(calleeNode, "parseInt") ||
astUtils.isSpecificMemberAccess(calleeNode, "Number", "parseInt")
);
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../shared/types').Rule} */
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "disallow `parseInt()` and `Number.parseInt()` in favor of binary, octal, and hexadecimal literals",
recommended: false,
url: "https://eslint.org/docs/rules/prefer-numeric-literals"
},
schema: [],
messages: {
useLiteral: "Use {{system}} literals instead of {{functionName}}()."
},
fixable: "code"
},
create(context) {
const sourceCode = context.getSourceCode();
//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------
return {
"CallExpression[arguments.length=2]"(node) {
const [strNode, radixNode] = node.arguments,
str = astUtils.getStaticStringValue(strNode),
radix = radixNode.value;
if (
str !== null &&
astUtils.isStringLiteral(strNode) &&
radixNode.type === "Literal" &&
typeof radix === "number" &&
radixMap.has(radix) &&
isParseInt(node.callee)
) {
const { system, literalPrefix } = radixMap.get(radix);
context.report({
node,
messageId: "useLiteral",
data: {
system,
functionName: sourceCode.getText(node.callee)
},
fix(fixer) {
if (sourceCode.getCommentsInside(node).length) {
return null;
}
const replacement = `${literalPrefix}${str}`;
if (+replacement !== parseInt(str, radix)) {
/*
* If the newly-produced literal would be invalid, (e.g. 0b1234),
* or it would yield an incorrect parseInt result for some other reason, don't make a fix.
*
* If `str` had numeric separators, `+replacement` will evaluate to `NaN` because unary `+`
* per the specification doesn't support numeric separators. Thus, the above condition will be `true`
* (`NaN !== anything` is always `true`) regardless of the `parseInt(str, radix)` value.
* Consequently, no autofixes will be made. This is correct behavior because `parseInt` also
* doesn't support numeric separators, but it does parse part of the string before the first `_`,
* so the autofix would be invalid:
*
* parseInt("1_1", 2) // === 1
* 0b1_1 // === 3
*/
return null;
}
const tokenBefore = sourceCode.getTokenBefore(node),
tokenAfter = sourceCode.getTokenAfter(node);
let prefix = "",
suffix = "";
if (
tokenBefore &&
tokenBefore.range[1] === node.range[0] &&
!astUtils.canTokensBeAdjacent(tokenBefore, replacement)
) {
prefix = " ";
}
if (
tokenAfter &&
node.range[1] === tokenAfter.range[0] &&
!astUtils.canTokensBeAdjacent(replacement, tokenAfter)
) {
suffix = " ";
}
return fixer.replaceText(node, `${prefix}${replacement}${suffix}`);
}
});
}
}
};
}
};