blob: cb061dac5d543c08eafad776ab8fd5b471d7664c [file] [log] [blame]
Yang Guo4fd355c2019-09-19 10:59:03 +02001/**
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
12const astUtils = require("./utils/ast-utils");
Tim van der Lippe16aca392020-11-13 11:37:13 +000013const eslintUtils = require("eslint-utils");
14
15const precedence = astUtils.getPrecedence;
Yang Guo4fd355c2019-09-19 10:59:03 +020016
17//------------------------------------------------------------------------------
18// Rule Definition
19//------------------------------------------------------------------------------
20
21module.exports = {
22 meta: {
23 type: "suggestion",
24
25 docs: {
26 description: "disallow unnecessary boolean casts",
Yang Guo4fd355c2019-09-19 10:59:03 +020027 recommended: true,
28 url: "https://eslint.org/docs/rules/no-extra-boolean-cast"
29 },
30
Tim van der Lippe16aca392020-11-13 11:37:13 +000031 schema: [{
32 type: "object",
33 properties: {
34 enforceForLogicalOperands: {
35 type: "boolean",
36 default: false
37 }
38 },
39 additionalProperties: false
40 }],
Yang Guo4fd355c2019-09-19 10:59:03 +020041 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 /**
Tim van der Lippe16aca392020-11-13 11:37:13 +000062 * 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 /**
Yang Guo4fd355c2019-09-19 10:59:03 +020088 * Check if a node is in a context where its value would be coerced to a boolean at runtime.
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +010089 * @param {ASTNode} node The node
Yang Guo4fd355c2019-09-19 10:59:03 +020090 * @returns {boolean} If it is in a boolean context
91 */
Tim van der Lippe16aca392020-11-13 11:37:13 +000092 function isInBooleanContext(node) {
Yang Guo4fd355c2019-09-19 10:59:03 +020093 return (
Tim van der Lippe16aca392020-11-13 11:37:13 +000094 (isBooleanFunctionOrConstructorCall(node.parent) &&
95 node === node.parent.arguments[0]) ||
96
97 (BOOLEAN_NODE_TYPES.indexOf(node.parent.type) !== -1 &&
98 node === node.parent.test) ||
Yang Guo4fd355c2019-09-19 10:59:03 +020099
100 // !<bool>
Tim van der Lippe16aca392020-11-13 11:37:13 +0000101 (node.parent.type === "UnaryExpression" &&
102 node.parent.operator === "!")
Yang Guo4fd355c2019-09-19 10:59:03 +0200103 );
104 }
105
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100106 /**
Tim van der Lippe16aca392020-11-13 11:37:13 +0000107 * 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) {
113 if (node.parent.type === "ChainExpression") {
114 return isInFlaggedContext(node.parent);
115 }
116
117 return isInBooleanContext(node) ||
118 (isLogicalContext(node.parent) &&
119
120 // For nested logical statements
121 isInFlaggedContext(node.parent)
122 );
123 }
124
125
126 /**
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100127 * 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 }
Yang Guo4fd355c2019-09-19 10:59:03 +0200134
Tim van der Lippe16aca392020-11-13 11:37:13 +0000135 /**
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.
Tim van der Lippe0fb47802021-11-08 16:23:10 +0000152 * @throws {Error} (Unreachable.)
Tim van der Lippe16aca392020-11-13 11:37:13 +0000153 * @returns {boolean} `true` if the node needs to be parenthesized.
154 */
155 function needsParens(previousNode, node) {
156 if (previousNode.parent.type === "ChainExpression") {
157 return needsParens(previousNode.parent, node);
158 }
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":
182 if (astUtils.isMixedLogicalAndCoalesceExpressions(node, parent)) {
183 return true;
184 }
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
Yang Guo4fd355c2019-09-19 10:59:03 +0200196 return {
197 UnaryExpression(node) {
Tim van der Lippe16aca392020-11-13 11:37:13 +0000198 const parent = node.parent;
199
Yang Guo4fd355c2019-09-19 10:59:03 +0200200
201 // Exit early if it's guaranteed not to match
202 if (node.operator !== "!" ||
Tim van der Lippe16aca392020-11-13 11:37:13 +0000203 parent.type !== "UnaryExpression" ||
204 parent.operator !== "!") {
Yang Guo4fd355c2019-09-19 10:59:03 +0200205 return;
206 }
207
Yang Guo4fd355c2019-09-19 10:59:03 +0200208
Tim van der Lippe16aca392020-11-13 11:37:13 +0000209 if (isInFlaggedContext(parent)) {
Yang Guo4fd355c2019-09-19 10:59:03 +0200210 context.report({
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100211 node: parent,
Yang Guo4fd355c2019-09-19 10:59:03 +0200212 messageId: "unexpectedNegation",
Tim van der Lippe16aca392020-11-13 11:37:13 +0000213 fix(fixer) {
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100214 if (hasCommentsInside(parent)) {
215 return null;
216 }
217
Tim van der Lippe16aca392020-11-13 11:37:13 +0000218 if (needsParens(parent, node.argument)) {
219 return fixer.replaceText(parent, `(${sourceCode.getText(node.argument)})`);
220 }
221
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100222 let prefix = "";
223 const tokenBefore = sourceCode.getTokenBefore(parent);
224 const firstReplacementToken = sourceCode.getFirstToken(node.argument);
225
Tim van der Lippe16aca392020-11-13 11:37:13 +0000226 if (
227 tokenBefore &&
228 tokenBefore.range[1] === parent.range[0] &&
229 !astUtils.canTokensBeAdjacent(tokenBefore, firstReplacementToken)
230 ) {
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100231 prefix = " ";
232 }
233
234 return fixer.replaceText(parent, prefix + sourceCode.getText(node.argument));
235 }
Yang Guo4fd355c2019-09-19 10:59:03 +0200236 });
237 }
238 },
Yang Guo4fd355c2019-09-19 10:59:03 +0200239
Tim van der Lippe16aca392020-11-13 11:37:13 +0000240 CallExpression(node) {
Yang Guo4fd355c2019-09-19 10:59:03 +0200241 if (node.callee.type !== "Identifier" || node.callee.name !== "Boolean") {
242 return;
243 }
244
Tim van der Lippe16aca392020-11-13 11:37:13 +0000245 if (isInFlaggedContext(node)) {
Yang Guo4fd355c2019-09-19 10:59:03 +0200246 context.report({
247 node,
248 messageId: "unexpectedCall",
Tim van der Lippe16aca392020-11-13 11:37:13 +0000249 fix(fixer) {
250 const parent = node.parent;
251
252 if (node.arguments.length === 0) {
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100253 if (parent.type === "UnaryExpression" && parent.operator === "!") {
254
Tim van der Lippe16aca392020-11-13 11:37:13 +0000255 /*
256 * !Boolean() -> true
257 */
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100258
259 if (hasCommentsInside(parent)) {
260 return null;
261 }
262
263 const replacement = "true";
264 let prefix = "";
265 const tokenBefore = sourceCode.getTokenBefore(parent);
266
Tim van der Lippe16aca392020-11-13 11:37:13 +0000267 if (
268 tokenBefore &&
269 tokenBefore.range[1] === parent.range[0] &&
270 !astUtils.canTokensBeAdjacent(tokenBefore, replacement)
271 ) {
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100272 prefix = " ";
273 }
274
275 return fixer.replaceText(parent, prefix + replacement);
276 }
277
Tim van der Lippe16aca392020-11-13 11:37:13 +0000278 /*
279 * Boolean() -> false
280 */
281
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100282 if (hasCommentsInside(node)) {
283 return null;
284 }
Tim van der Lippe16aca392020-11-13 11:37:13 +0000285
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100286 return fixer.replaceText(node, "false");
Yang Guo4fd355c2019-09-19 10:59:03 +0200287 }
288
Tim van der Lippe16aca392020-11-13 11:37:13 +0000289 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));
Yang Guo4fd355c2019-09-19 10:59:03 +0200305 }
306
Tim van der Lippe16aca392020-11-13 11:37:13 +0000307 // two or more arguments
308 return null;
Yang Guo4fd355c2019-09-19 10:59:03 +0200309 }
310 });
311 }
312 }
313 };
314
315 }
316};