• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/**
2 * @fileoverview Rule to disallow `parseInt()` in favor of binary, octal, and hexadecimal literals
3 * @author Annie Zhang, Henry Zhu
4 */
5
6"use strict";
7
8//------------------------------------------------------------------------------
9// Requirements
10//------------------------------------------------------------------------------
11
12const astUtils = require("./utils/ast-utils");
13
14//------------------------------------------------------------------------------
15// Helpers
16//------------------------------------------------------------------------------
17
18const radixMap = new Map([
19    [2, { system: "binary", literalPrefix: "0b" }],
20    [8, { system: "octal", literalPrefix: "0o" }],
21    [16, { system: "hexadecimal", literalPrefix: "0x" }]
22]);
23
24/**
25 * Checks to see if a CallExpression's callee node is `parseInt` or
26 * `Number.parseInt`.
27 * @param {ASTNode} calleeNode The callee node to evaluate.
28 * @returns {boolean} True if the callee is `parseInt` or `Number.parseInt`,
29 * false otherwise.
30 */
31function isParseInt(calleeNode) {
32    return (
33        astUtils.isSpecificId(calleeNode, "parseInt") ||
34        astUtils.isSpecificMemberAccess(calleeNode, "Number", "parseInt")
35    );
36}
37
38//------------------------------------------------------------------------------
39// Rule Definition
40//------------------------------------------------------------------------------
41
42module.exports = {
43    meta: {
44        type: "suggestion",
45
46        docs: {
47            description: "disallow `parseInt()` and `Number.parseInt()` in favor of binary, octal, and hexadecimal literals",
48            category: "ECMAScript 6",
49            recommended: false,
50            url: "https://eslint.org/docs/rules/prefer-numeric-literals"
51        },
52
53        schema: [],
54
55        messages: {
56            useLiteral: "Use {{system}} literals instead of {{functionName}}()."
57        },
58
59        fixable: "code"
60    },
61
62    create(context) {
63        const sourceCode = context.getSourceCode();
64
65        //----------------------------------------------------------------------
66        // Public
67        //----------------------------------------------------------------------
68
69        return {
70
71            "CallExpression[arguments.length=2]"(node) {
72                const [strNode, radixNode] = node.arguments,
73                    str = astUtils.getStaticStringValue(strNode),
74                    radix = radixNode.value;
75
76                if (
77                    str !== null &&
78                    astUtils.isStringLiteral(strNode) &&
79                    radixNode.type === "Literal" &&
80                    typeof radix === "number" &&
81                    radixMap.has(radix) &&
82                    isParseInt(node.callee)
83                ) {
84
85                    const { system, literalPrefix } = radixMap.get(radix);
86
87                    context.report({
88                        node,
89                        messageId: "useLiteral",
90                        data: {
91                            system,
92                            functionName: sourceCode.getText(node.callee)
93                        },
94                        fix(fixer) {
95                            if (sourceCode.getCommentsInside(node).length) {
96                                return null;
97                            }
98
99                            const replacement = `${literalPrefix}${str}`;
100
101                            if (+replacement !== parseInt(str, radix)) {
102
103                                /*
104                                 * If the newly-produced literal would be invalid, (e.g. 0b1234),
105                                 * or it would yield an incorrect parseInt result for some other reason, don't make a fix.
106                                 *
107                                 * If `str` had numeric separators, `+replacement` will evaluate to `NaN` because unary `+`
108                                 * per the specification doesn't support numeric separators. Thus, the above condition will be `true`
109                                 * (`NaN !== anything` is always `true`) regardless of the `parseInt(str, radix)` value.
110                                 * Consequently, no autofixes will be made. This is correct behavior because `parseInt` also
111                                 * doesn't support numeric separators, but it does parse part of the string before the first `_`,
112                                 * so the autofix would be invalid:
113                                 *
114                                 *   parseInt("1_1", 2) // === 1
115                                 *   0b1_1 // === 3
116                                 */
117                                return null;
118                            }
119
120                            const tokenBefore = sourceCode.getTokenBefore(node),
121                                tokenAfter = sourceCode.getTokenAfter(node);
122                            let prefix = "",
123                                suffix = "";
124
125                            if (
126                                tokenBefore &&
127                                tokenBefore.range[1] === node.range[0] &&
128                                !astUtils.canTokensBeAdjacent(tokenBefore, replacement)
129                            ) {
130                                prefix = " ";
131                            }
132
133                            if (
134                                tokenAfter &&
135                                node.range[1] === tokenAfter.range[0] &&
136                                !astUtils.canTokensBeAdjacent(replacement, tokenAfter)
137                            ) {
138                                suffix = " ";
139                            }
140
141                            return fixer.replaceText(node, `${prefix}${replacement}${suffix}`);
142                        }
143                    });
144                }
145            }
146        };
147    }
148};
149