• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
12const astUtils = require("./utils/ast-utils");
13const eslintUtils = require("eslint-utils");
14
15const precedence = astUtils.getPrecedence;
16
17//------------------------------------------------------------------------------
18// Rule Definition
19//------------------------------------------------------------------------------
20
21module.exports = {
22    meta: {
23        type: "suggestion",
24
25        docs: {
26            description: "disallow unnecessary boolean casts",
27            category: "Possible Errors",
28            recommended: true,
29            url: "https://eslint.org/docs/rules/no-extra-boolean-cast"
30        },
31
32        schema: [{
33            type: "object",
34            properties: {
35                enforceForLogicalOperands: {
36                    type: "boolean",
37                    default: false
38                }
39            },
40            additionalProperties: false
41        }],
42        fixable: "code",
43
44        messages: {
45            unexpectedCall: "Redundant Boolean call.",
46            unexpectedNegation: "Redundant double negation."
47        }
48    },
49
50    create(context) {
51        const sourceCode = context.getSourceCode();
52
53        // Node types which have a test which will coerce values to booleans.
54        const BOOLEAN_NODE_TYPES = [
55            "IfStatement",
56            "DoWhileStatement",
57            "WhileStatement",
58            "ConditionalExpression",
59            "ForStatement"
60        ];
61
62        /**
63         * Check if a node is a Boolean function or constructor.
64         * @param {ASTNode} node the node
65         * @returns {boolean} If the node is Boolean function or constructor
66         */
67        function isBooleanFunctionOrConstructorCall(node) {
68
69            // Boolean(<bool>) and new Boolean(<bool>)
70            return (node.type === "CallExpression" || node.type === "NewExpression") &&
71                    node.callee.type === "Identifier" &&
72                        node.callee.name === "Boolean";
73        }
74
75        /**
76         * Checks whether the node is a logical expression and that the option is enabled
77         * @param {ASTNode} node the node
78         * @returns {boolean} if the node is a logical expression and option is enabled
79         */
80        function isLogicalContext(node) {
81            return node.type === "LogicalExpression" &&
82            (node.operator === "||" || node.operator === "&&") &&
83            (context.options.length && context.options[0].enforceForLogicalOperands === true);
84
85        }
86
87
88        /**
89         * Check if a node is in a context where its value would be coerced to a boolean at runtime.
90         * @param {ASTNode} node The node
91         * @returns {boolean} If it is in a boolean context
92         */
93        function isInBooleanContext(node) {
94            return (
95                (isBooleanFunctionOrConstructorCall(node.parent) &&
96                node === node.parent.arguments[0]) ||
97
98                (BOOLEAN_NODE_TYPES.indexOf(node.parent.type) !== -1 &&
99                    node === node.parent.test) ||
100
101                // !<bool>
102                (node.parent.type === "UnaryExpression" &&
103                    node.parent.operator === "!")
104            );
105        }
106
107        /**
108         * Checks whether the node is a context that should report an error
109         * Acts recursively if it is in a logical context
110         * @param {ASTNode} node the node
111         * @returns {boolean} If the node is in one of the flagged contexts
112         */
113        function isInFlaggedContext(node) {
114            if (node.parent.type === "ChainExpression") {
115                return isInFlaggedContext(node.parent);
116            }
117
118            return isInBooleanContext(node) ||
119            (isLogicalContext(node.parent) &&
120
121            // For nested logical statements
122            isInFlaggedContext(node.parent)
123            );
124        }
125
126
127        /**
128         * Check if a node has comments inside.
129         * @param {ASTNode} node The node to check.
130         * @returns {boolean} `true` if it has comments inside.
131         */
132        function hasCommentsInside(node) {
133            return Boolean(sourceCode.getCommentsInside(node).length);
134        }
135
136        /**
137         * Checks if the given node is wrapped in grouping parentheses. Parentheses for constructs such as if() don't count.
138         * @param {ASTNode} node The node to check.
139         * @returns {boolean} `true` if the node is parenthesized.
140         * @private
141         */
142        function isParenthesized(node) {
143            return eslintUtils.isParenthesized(1, node, sourceCode);
144        }
145
146        /**
147         * Determines whether the given node needs to be parenthesized when replacing the previous node.
148         * It assumes that `previousNode` is the node to be reported by this rule, so it has a limited list
149         * of possible parent node types. By the same assumption, the node's role in a particular parent is already known.
150         * For example, if the parent is `ConditionalExpression`, `previousNode` must be its `test` child.
151         * @param {ASTNode} previousNode Previous node.
152         * @param {ASTNode} node The node to check.
153         * @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
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};
317