blob: 336f601d1652de4c7d8e6172bf47870dcdf0c018 [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");
13
14//------------------------------------------------------------------------------
15// Rule Definition
16//------------------------------------------------------------------------------
17
18module.exports = {
19 meta: {
20 type: "suggestion",
21
22 docs: {
23 description: "disallow unnecessary boolean casts",
24 category: "Possible Errors",
25 recommended: true,
26 url: "https://eslint.org/docs/rules/no-extra-boolean-cast"
27 },
28
29 schema: [],
30 fixable: "code",
31
32 messages: {
33 unexpectedCall: "Redundant Boolean call.",
34 unexpectedNegation: "Redundant double negation."
35 }
36 },
37
38 create(context) {
39 const sourceCode = context.getSourceCode();
40
41 // Node types which have a test which will coerce values to booleans.
42 const BOOLEAN_NODE_TYPES = [
43 "IfStatement",
44 "DoWhileStatement",
45 "WhileStatement",
46 "ConditionalExpression",
47 "ForStatement"
48 ];
49
50 /**
51 * 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 +010052 * @param {ASTNode} node The node
53 * @param {ASTNode} parent Its parent
Yang Guo4fd355c2019-09-19 10:59:03 +020054 * @returns {boolean} If it is in a boolean context
55 */
56 function isInBooleanContext(node, parent) {
57 return (
58 (BOOLEAN_NODE_TYPES.indexOf(parent.type) !== -1 &&
59 node === parent.test) ||
60
61 // !<bool>
62 (parent.type === "UnaryExpression" &&
63 parent.operator === "!")
64 );
65 }
66
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +010067 /**
68 * Check if a node has comments inside.
69 * @param {ASTNode} node The node to check.
70 * @returns {boolean} `true` if it has comments inside.
71 */
72 function hasCommentsInside(node) {
73 return Boolean(sourceCode.getCommentsInside(node).length);
74 }
Yang Guo4fd355c2019-09-19 10:59:03 +020075
76 return {
77 UnaryExpression(node) {
78 const ancestors = context.getAncestors(),
79 parent = ancestors.pop(),
80 grandparent = ancestors.pop();
81
82 // Exit early if it's guaranteed not to match
83 if (node.operator !== "!" ||
84 parent.type !== "UnaryExpression" ||
85 parent.operator !== "!") {
86 return;
87 }
88
89 if (isInBooleanContext(parent, grandparent) ||
90
91 // Boolean(<bool>) and new Boolean(<bool>)
92 ((grandparent.type === "CallExpression" || grandparent.type === "NewExpression") &&
93 grandparent.callee.type === "Identifier" &&
94 grandparent.callee.name === "Boolean")
95 ) {
96 context.report({
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +010097 node: parent,
Yang Guo4fd355c2019-09-19 10:59:03 +020098 messageId: "unexpectedNegation",
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +010099 fix: fixer => {
100 if (hasCommentsInside(parent)) {
101 return null;
102 }
103
104 let prefix = "";
105 const tokenBefore = sourceCode.getTokenBefore(parent);
106 const firstReplacementToken = sourceCode.getFirstToken(node.argument);
107
108 if (tokenBefore && tokenBefore.range[1] === parent.range[0] &&
109 !astUtils.canTokensBeAdjacent(tokenBefore, firstReplacementToken)) {
110 prefix = " ";
111 }
112
113 return fixer.replaceText(parent, prefix + sourceCode.getText(node.argument));
114 }
Yang Guo4fd355c2019-09-19 10:59:03 +0200115 });
116 }
117 },
118 CallExpression(node) {
119 const parent = node.parent;
120
121 if (node.callee.type !== "Identifier" || node.callee.name !== "Boolean") {
122 return;
123 }
124
125 if (isInBooleanContext(node, parent)) {
126 context.report({
127 node,
128 messageId: "unexpectedCall",
129 fix: fixer => {
130 if (!node.arguments.length) {
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100131 if (parent.type === "UnaryExpression" && parent.operator === "!") {
132
133 // !Boolean() -> true
134
135 if (hasCommentsInside(parent)) {
136 return null;
137 }
138
139 const replacement = "true";
140 let prefix = "";
141 const tokenBefore = sourceCode.getTokenBefore(parent);
142
143 if (tokenBefore && tokenBefore.range[1] === parent.range[0] &&
144 !astUtils.canTokensBeAdjacent(tokenBefore, replacement)) {
145 prefix = " ";
146 }
147
148 return fixer.replaceText(parent, prefix + replacement);
149 }
150
151 // Boolean() -> false
152 if (hasCommentsInside(node)) {
153 return null;
154 }
155 return fixer.replaceText(node, "false");
Yang Guo4fd355c2019-09-19 10:59:03 +0200156 }
157
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100158 if (node.arguments.length > 1 || node.arguments[0].type === "SpreadElement" ||
159 hasCommentsInside(node)) {
Yang Guo4fd355c2019-09-19 10:59:03 +0200160 return null;
161 }
162
163 const argument = node.arguments[0];
164
165 if (astUtils.getPrecedence(argument) < astUtils.getPrecedence(node.parent)) {
166 return fixer.replaceText(node, `(${sourceCode.getText(argument)})`);
167 }
168 return fixer.replaceText(node, sourceCode.getText(argument));
169 }
170 });
171 }
172 }
173 };
174
175 }
176};