prefer-numeric-literals.js 5.7 KB
/**
 * @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}`);
                        }
                    });
                }
            }
        };
    }
};