prefer-numeric-literals.js 5.07 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) {
    switch (calleeNode.type) {
        case "Identifier":
            return calleeNode.name === "parseInt";
        case "MemberExpression":
            return calleeNode.object.type === "Identifier" &&
                calleeNode.object.name === "Number" &&
                calleeNode.property.type === "Identifier" &&
                calleeNode.property.name === "parseInt";

        // no default
    }

    return false;
}

//------------------------------------------------------------------------------
// 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 = strNode.value,
                    radix = radixNode.value;

                if (
                    strNode.type === "Literal" &&
                    radixNode.type === "Literal" &&
                    typeof str === "string" &&
                    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.
                                 */
                                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}`);
                        }
                    });
                }
            }
        };
    }
};