]>
Commit | Line | Data |
---|---|---|
eb39fafa DC |
1 | /** |
2 | * @fileoverview Rule to flag use of parseInt without a radix argument | |
3 | * @author James Allardice | |
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 MODE_ALWAYS = "always", | |
19 | MODE_AS_NEEDED = "as-needed"; | |
20 | ||
21 | const validRadixValues = new Set(Array.from({ length: 37 - 2 }, (_, index) => index + 2)); | |
22 | ||
23 | /** | |
24 | * Checks whether a given variable is shadowed or not. | |
25 | * @param {eslint-scope.Variable} variable A variable to check. | |
26 | * @returns {boolean} `true` if the variable is shadowed. | |
27 | */ | |
28 | function isShadowed(variable) { | |
29 | return variable.defs.length >= 1; | |
30 | } | |
31 | ||
32 | /** | |
33 | * Checks whether a given node is a MemberExpression of `parseInt` method or not. | |
34 | * @param {ASTNode} node A node to check. | |
35 | * @returns {boolean} `true` if the node is a MemberExpression of `parseInt` | |
36 | * method. | |
37 | */ | |
38 | function isParseIntMethod(node) { | |
39 | return ( | |
40 | node.type === "MemberExpression" && | |
41 | !node.computed && | |
42 | node.property.type === "Identifier" && | |
43 | node.property.name === "parseInt" | |
44 | ); | |
45 | } | |
46 | ||
47 | /** | |
48 | * Checks whether a given node is a valid value of radix or not. | |
49 | * | |
50 | * The following values are invalid. | |
51 | * | |
52 | * - A literal except integers between 2 and 36. | |
53 | * - undefined. | |
54 | * @param {ASTNode} radix A node of radix to check. | |
55 | * @returns {boolean} `true` if the node is valid. | |
56 | */ | |
57 | function isValidRadix(radix) { | |
58 | return !( | |
59 | (radix.type === "Literal" && !validRadixValues.has(radix.value)) || | |
60 | (radix.type === "Identifier" && radix.name === "undefined") | |
61 | ); | |
62 | } | |
63 | ||
64 | /** | |
65 | * Checks whether a given node is a default value of radix or not. | |
66 | * @param {ASTNode} radix A node of radix to check. | |
67 | * @returns {boolean} `true` if the node is the literal node of `10`. | |
68 | */ | |
69 | function isDefaultRadix(radix) { | |
70 | return radix.type === "Literal" && radix.value === 10; | |
71 | } | |
72 | ||
73 | //------------------------------------------------------------------------------ | |
74 | // Rule Definition | |
75 | //------------------------------------------------------------------------------ | |
76 | ||
77 | module.exports = { | |
78 | meta: { | |
79 | type: "suggestion", | |
80 | ||
81 | docs: { | |
82 | description: "enforce the consistent use of the radix argument when using `parseInt()`", | |
83 | category: "Best Practices", | |
84 | recommended: false, | |
85 | url: "https://eslint.org/docs/rules/radix" | |
86 | }, | |
87 | ||
88 | schema: [ | |
89 | { | |
90 | enum: ["always", "as-needed"] | |
91 | } | |
92 | ], | |
93 | ||
94 | messages: { | |
95 | missingParameters: "Missing parameters.", | |
96 | redundantRadix: "Redundant radix parameter.", | |
97 | missingRadix: "Missing radix parameter.", | |
98 | invalidRadix: "Invalid radix parameter, must be an integer between 2 and 36." | |
99 | } | |
100 | }, | |
101 | ||
102 | create(context) { | |
103 | const mode = context.options[0] || MODE_ALWAYS; | |
104 | ||
105 | /** | |
106 | * Checks the arguments of a given CallExpression node and reports it if it | |
107 | * offends this rule. | |
108 | * @param {ASTNode} node A CallExpression node to check. | |
109 | * @returns {void} | |
110 | */ | |
111 | function checkArguments(node) { | |
112 | const args = node.arguments; | |
113 | ||
114 | switch (args.length) { | |
115 | case 0: | |
116 | context.report({ | |
117 | node, | |
118 | messageId: "missingParameters" | |
119 | }); | |
120 | break; | |
121 | ||
122 | case 1: | |
123 | if (mode === MODE_ALWAYS) { | |
124 | context.report({ | |
125 | node, | |
126 | messageId: "missingRadix" | |
127 | }); | |
128 | } | |
129 | break; | |
130 | ||
131 | default: | |
132 | if (mode === MODE_AS_NEEDED && isDefaultRadix(args[1])) { | |
133 | context.report({ | |
134 | node, | |
135 | messageId: "redundantRadix" | |
136 | }); | |
137 | } else if (!isValidRadix(args[1])) { | |
138 | context.report({ | |
139 | node, | |
140 | messageId: "invalidRadix" | |
141 | }); | |
142 | } | |
143 | break; | |
144 | } | |
145 | } | |
146 | ||
147 | return { | |
148 | "Program:exit"() { | |
149 | const scope = context.getScope(); | |
150 | let variable; | |
151 | ||
152 | // Check `parseInt()` | |
153 | variable = astUtils.getVariableByName(scope, "parseInt"); | |
154 | if (variable && !isShadowed(variable)) { | |
155 | variable.references.forEach(reference => { | |
156 | const node = reference.identifier; | |
157 | ||
158 | if (astUtils.isCallee(node)) { | |
159 | checkArguments(node.parent); | |
160 | } | |
161 | }); | |
162 | } | |
163 | ||
164 | // Check `Number.parseInt()` | |
165 | variable = astUtils.getVariableByName(scope, "Number"); | |
166 | if (variable && !isShadowed(variable)) { | |
167 | variable.references.forEach(reference => { | |
168 | const node = reference.identifier.parent; | |
6f036462 TL |
169 | const maybeCallee = node.parent.type === "ChainExpression" |
170 | ? node.parent | |
171 | : node; | |
eb39fafa | 172 | |
6f036462 TL |
173 | if (isParseIntMethod(node) && astUtils.isCallee(maybeCallee)) { |
174 | checkArguments(maybeCallee.parent); | |
eb39fafa DC |
175 | } |
176 | }); | |
177 | } | |
178 | } | |
179 | }; | |
180 | } | |
181 | }; |