]>
Commit | Line | Data |
---|---|---|
eb39fafa DC |
1 | /** |
2 | * @fileoverview Rule to flag unnecessary double negation in Boolean contexts | |
3 | * @author Brandon Mills | |
4 | */ | |
5 | ||
6 | "use strict"; | |
7 | ||
8 | //------------------------------------------------------------------------------ | |
9 | // Requirements | |
10 | //------------------------------------------------------------------------------ | |
11 | ||
12 | const astUtils = require("./utils/ast-utils"); | |
13 | const eslintUtils = require("eslint-utils"); | |
14 | ||
15 | const precedence = astUtils.getPrecedence; | |
16 | ||
17 | //------------------------------------------------------------------------------ | |
18 | // Rule Definition | |
19 | //------------------------------------------------------------------------------ | |
20 | ||
21 | module.exports = { | |
22 | meta: { | |
23 | type: "suggestion", | |
24 | ||
25 | docs: { | |
26 | description: "disallow unnecessary boolean casts", | |
eb39fafa DC |
27 | recommended: true, |
28 | url: "https://eslint.org/docs/rules/no-extra-boolean-cast" | |
29 | }, | |
30 | ||
31 | schema: [{ | |
32 | type: "object", | |
33 | properties: { | |
34 | enforceForLogicalOperands: { | |
35 | type: "boolean", | |
36 | default: false | |
37 | } | |
38 | }, | |
39 | additionalProperties: false | |
40 | }], | |
41 | fixable: "code", | |
42 | ||
43 | messages: { | |
44 | unexpectedCall: "Redundant Boolean call.", | |
45 | unexpectedNegation: "Redundant double negation." | |
46 | } | |
47 | }, | |
48 | ||
49 | create(context) { | |
50 | const sourceCode = context.getSourceCode(); | |
51 | ||
52 | // Node types which have a test which will coerce values to booleans. | |
53 | const BOOLEAN_NODE_TYPES = [ | |
54 | "IfStatement", | |
55 | "DoWhileStatement", | |
56 | "WhileStatement", | |
57 | "ConditionalExpression", | |
58 | "ForStatement" | |
59 | ]; | |
60 | ||
61 | /** | |
62 | * Check if a node is a Boolean function or constructor. | |
63 | * @param {ASTNode} node the node | |
64 | * @returns {boolean} If the node is Boolean function or constructor | |
65 | */ | |
66 | function isBooleanFunctionOrConstructorCall(node) { | |
67 | ||
68 | // Boolean(<bool>) and new Boolean(<bool>) | |
69 | return (node.type === "CallExpression" || node.type === "NewExpression") && | |
70 | node.callee.type === "Identifier" && | |
71 | node.callee.name === "Boolean"; | |
72 | } | |
73 | ||
74 | /** | |
75 | * Checks whether the node is a logical expression and that the option is enabled | |
76 | * @param {ASTNode} node the node | |
77 | * @returns {boolean} if the node is a logical expression and option is enabled | |
78 | */ | |
79 | function isLogicalContext(node) { | |
80 | return node.type === "LogicalExpression" && | |
81 | (node.operator === "||" || node.operator === "&&") && | |
82 | (context.options.length && context.options[0].enforceForLogicalOperands === true); | |
83 | ||
84 | } | |
85 | ||
86 | ||
87 | /** | |
88 | * Check if a node is in a context where its value would be coerced to a boolean at runtime. | |
89 | * @param {ASTNode} node The node | |
90 | * @returns {boolean} If it is in a boolean context | |
91 | */ | |
92 | function isInBooleanContext(node) { | |
93 | return ( | |
94 | (isBooleanFunctionOrConstructorCall(node.parent) && | |
95 | node === node.parent.arguments[0]) || | |
96 | ||
97 | (BOOLEAN_NODE_TYPES.indexOf(node.parent.type) !== -1 && | |
98 | node === node.parent.test) || | |
99 | ||
100 | // !<bool> | |
101 | (node.parent.type === "UnaryExpression" && | |
102 | node.parent.operator === "!") | |
103 | ); | |
104 | } | |
105 | ||
106 | /** | |
107 | * Checks whether the node is a context that should report an error | |
108 | * Acts recursively if it is in a logical context | |
109 | * @param {ASTNode} node the node | |
110 | * @returns {boolean} If the node is in one of the flagged contexts | |
111 | */ | |
112 | function isInFlaggedContext(node) { | |
6f036462 TL |
113 | if (node.parent.type === "ChainExpression") { |
114 | return isInFlaggedContext(node.parent); | |
115 | } | |
116 | ||
eb39fafa DC |
117 | return isInBooleanContext(node) || |
118 | (isLogicalContext(node.parent) && | |
119 | ||
120 | // For nested logical statements | |
121 | isInFlaggedContext(node.parent) | |
122 | ); | |
123 | } | |
124 | ||
125 | ||
126 | /** | |
127 | * Check if a node has comments inside. | |
128 | * @param {ASTNode} node The node to check. | |
129 | * @returns {boolean} `true` if it has comments inside. | |
130 | */ | |
131 | function hasCommentsInside(node) { | |
132 | return Boolean(sourceCode.getCommentsInside(node).length); | |
133 | } | |
134 | ||
135 | /** | |
136 | * Checks if the given node is wrapped in grouping parentheses. Parentheses for constructs such as if() don't count. | |
137 | * @param {ASTNode} node The node to check. | |
138 | * @returns {boolean} `true` if the node is parenthesized. | |
139 | * @private | |
140 | */ | |
141 | function isParenthesized(node) { | |
142 | return eslintUtils.isParenthesized(1, node, sourceCode); | |
143 | } | |
144 | ||
145 | /** | |
146 | * Determines whether the given node needs to be parenthesized when replacing the previous node. | |
147 | * It assumes that `previousNode` is the node to be reported by this rule, so it has a limited list | |
148 | * of possible parent node types. By the same assumption, the node's role in a particular parent is already known. | |
149 | * For example, if the parent is `ConditionalExpression`, `previousNode` must be its `test` child. | |
150 | * @param {ASTNode} previousNode Previous node. | |
151 | * @param {ASTNode} node The node to check. | |
609c276f | 152 | * @throws {Error} (Unreachable.) |
eb39fafa DC |
153 | * @returns {boolean} `true` if the node needs to be parenthesized. |
154 | */ | |
155 | function needsParens(previousNode, node) { | |
6f036462 TL |
156 | if (previousNode.parent.type === "ChainExpression") { |
157 | return needsParens(previousNode.parent, node); | |
158 | } | |
eb39fafa DC |
159 | if (isParenthesized(previousNode)) { |
160 | ||
161 | // parentheses around the previous node will stay, so there is no need for an additional pair | |
162 | return false; | |
163 | } | |
164 | ||
165 | // parent of the previous node will become parent of the replacement node | |
166 | const parent = previousNode.parent; | |
167 | ||
168 | switch (parent.type) { | |
169 | case "CallExpression": | |
170 | case "NewExpression": | |
171 | return node.type === "SequenceExpression"; | |
172 | case "IfStatement": | |
173 | case "DoWhileStatement": | |
174 | case "WhileStatement": | |
175 | case "ForStatement": | |
176 | return false; | |
177 | case "ConditionalExpression": | |
178 | return precedence(node) <= precedence(parent); | |
179 | case "UnaryExpression": | |
180 | return precedence(node) < precedence(parent); | |
181 | case "LogicalExpression": | |
d3726936 TL |
182 | if (astUtils.isMixedLogicalAndCoalesceExpressions(node, parent)) { |
183 | return true; | |
184 | } | |
eb39fafa DC |
185 | if (previousNode === parent.left) { |
186 | return precedence(node) < precedence(parent); | |
187 | } | |
188 | return precedence(node) <= precedence(parent); | |
189 | ||
190 | /* istanbul ignore next */ | |
191 | default: | |
192 | throw new Error(`Unexpected parent type: ${parent.type}`); | |
193 | } | |
194 | } | |
195 | ||
196 | return { | |
197 | UnaryExpression(node) { | |
198 | const parent = node.parent; | |
199 | ||
200 | ||
201 | // Exit early if it's guaranteed not to match | |
202 | if (node.operator !== "!" || | |
203 | parent.type !== "UnaryExpression" || | |
204 | parent.operator !== "!") { | |
205 | return; | |
206 | } | |
207 | ||
208 | ||
209 | if (isInFlaggedContext(parent)) { | |
210 | context.report({ | |
211 | node: parent, | |
212 | messageId: "unexpectedNegation", | |
213 | fix(fixer) { | |
214 | if (hasCommentsInside(parent)) { | |
215 | return null; | |
216 | } | |
217 | ||
218 | if (needsParens(parent, node.argument)) { | |
219 | return fixer.replaceText(parent, `(${sourceCode.getText(node.argument)})`); | |
220 | } | |
221 | ||
222 | let prefix = ""; | |
223 | const tokenBefore = sourceCode.getTokenBefore(parent); | |
224 | const firstReplacementToken = sourceCode.getFirstToken(node.argument); | |
225 | ||
226 | if ( | |
227 | tokenBefore && | |
228 | tokenBefore.range[1] === parent.range[0] && | |
229 | !astUtils.canTokensBeAdjacent(tokenBefore, firstReplacementToken) | |
230 | ) { | |
231 | prefix = " "; | |
232 | } | |
233 | ||
234 | return fixer.replaceText(parent, prefix + sourceCode.getText(node.argument)); | |
235 | } | |
236 | }); | |
237 | } | |
238 | }, | |
239 | ||
240 | CallExpression(node) { | |
241 | if (node.callee.type !== "Identifier" || node.callee.name !== "Boolean") { | |
242 | return; | |
243 | } | |
244 | ||
245 | if (isInFlaggedContext(node)) { | |
246 | context.report({ | |
247 | node, | |
248 | messageId: "unexpectedCall", | |
249 | fix(fixer) { | |
250 | const parent = node.parent; | |
251 | ||
252 | if (node.arguments.length === 0) { | |
253 | if (parent.type === "UnaryExpression" && parent.operator === "!") { | |
254 | ||
255 | /* | |
256 | * !Boolean() -> true | |
257 | */ | |
258 | ||
259 | if (hasCommentsInside(parent)) { | |
260 | return null; | |
261 | } | |
262 | ||
263 | const replacement = "true"; | |
264 | let prefix = ""; | |
265 | const tokenBefore = sourceCode.getTokenBefore(parent); | |
266 | ||
267 | if ( | |
268 | tokenBefore && | |
269 | tokenBefore.range[1] === parent.range[0] && | |
270 | !astUtils.canTokensBeAdjacent(tokenBefore, replacement) | |
271 | ) { | |
272 | prefix = " "; | |
273 | } | |
274 | ||
275 | return fixer.replaceText(parent, prefix + replacement); | |
276 | } | |
277 | ||
278 | /* | |
279 | * Boolean() -> false | |
280 | */ | |
281 | ||
282 | if (hasCommentsInside(node)) { | |
283 | return null; | |
284 | } | |
285 | ||
286 | return fixer.replaceText(node, "false"); | |
287 | } | |
288 | ||
289 | if (node.arguments.length === 1) { | |
290 | const argument = node.arguments[0]; | |
291 | ||
292 | if (argument.type === "SpreadElement" || hasCommentsInside(node)) { | |
293 | return null; | |
294 | } | |
295 | ||
296 | /* | |
297 | * Boolean(expression) -> expression | |
298 | */ | |
299 | ||
300 | if (needsParens(node, argument)) { | |
301 | return fixer.replaceText(node, `(${sourceCode.getText(argument)})`); | |
302 | } | |
303 | ||
304 | return fixer.replaceText(node, sourceCode.getText(argument)); | |
305 | } | |
306 | ||
307 | // two or more arguments | |
308 | return null; | |
309 | } | |
310 | }); | |
311 | } | |
312 | } | |
313 | }; | |
314 | ||
315 | } | |
316 | }; |