/** * @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 //------------------------------------------------------------------------------ module.exports = { meta: { type: "suggestion", docs: { description: "disallow `parseInt()` and `Number.parseInt()` in favor of binary, octal, and hexadecimal literals", category: "ECMAScript 6", 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}`); } }); } } }; } };