]>
Commit | Line | Data |
---|---|---|
eb39fafa DC |
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 | ||
12 | const astUtils = require("./utils/ast-utils"); | |
13 | ||
14 | //------------------------------------------------------------------------------ | |
15 | // Helpers | |
16 | //------------------------------------------------------------------------------ | |
17 | ||
18 | const 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 | */ | |
31 | function isParseInt(calleeNode) { | |
6f036462 TL |
32 | return ( |
33 | astUtils.isSpecificId(calleeNode, "parseInt") || | |
34 | astUtils.isSpecificMemberAccess(calleeNode, "Number", "parseInt") | |
35 | ); | |
eb39fafa DC |
36 | } |
37 | ||
38 | //------------------------------------------------------------------------------ | |
39 | // Rule Definition | |
40 | //------------------------------------------------------------------------------ | |
41 | ||
34eeec05 | 42 | /** @type {import('../shared/types').Rule} */ |
eb39fafa DC |
43 | module.exports = { |
44 | meta: { | |
45 | type: "suggestion", | |
46 | ||
47 | docs: { | |
8f9d1d4d | 48 | description: "Disallow `parseInt()` and `Number.parseInt()` in favor of binary, octal, and hexadecimal literals", |
eb39fafa | 49 | recommended: false, |
f2a92ac6 | 50 | url: "https://eslint.org/docs/latest/rules/prefer-numeric-literals" |
eb39fafa DC |
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) { | |
f2a92ac6 | 63 | const sourceCode = context.sourceCode; |
eb39fafa DC |
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. | |
6f036462 TL |
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 | |
eb39fafa DC |
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 | }; |