2 * @fileoverview Rule to flag unnecessary double negation in Boolean contexts
3 * @author Brandon Mills
8 //------------------------------------------------------------------------------
10 //------------------------------------------------------------------------------
12 const astUtils
= require("./utils/ast-utils");
13 const eslintUtils
= require("eslint-utils");
15 const precedence
= astUtils
.getPrecedence
;
17 //------------------------------------------------------------------------------
19 //------------------------------------------------------------------------------
21 /** @type {import('../shared/types').Rule} */
27 description
: "Disallow unnecessary boolean casts",
29 url
: "https://eslint.org/docs/rules/no-extra-boolean-cast"
35 enforceForLogicalOperands
: {
40 additionalProperties
: false
45 unexpectedCall
: "Redundant Boolean call.",
46 unexpectedNegation
: "Redundant double negation."
51 const sourceCode
= context
.getSourceCode();
53 // Node types which have a test which will coerce values to booleans.
54 const BOOLEAN_NODE_TYPES
= new Set([
58 "ConditionalExpression",
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
67 function isBooleanFunctionOrConstructorCall(node
) {
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";
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
80 function isLogicalContext(node
) {
81 return node
.type
=== "LogicalExpression" &&
82 (node
.operator
=== "||" || node
.operator
=== "&&") &&
83 (context
.options
.length
&& context
.options
[0].enforceForLogicalOperands
=== true);
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
93 function isInBooleanContext(node
) {
95 (isBooleanFunctionOrConstructorCall(node
.parent
) &&
96 node
=== node
.parent
.arguments
[0]) ||
98 (BOOLEAN_NODE_TYPES
.has(node
.parent
.type
) &&
99 node
=== node
.parent
.test
) ||
102 (node
.parent
.type
=== "UnaryExpression" &&
103 node
.parent
.operator
=== "!")
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
113 function isInFlaggedContext(node
) {
114 if (node
.parent
.type
=== "ChainExpression") {
115 return isInFlaggedContext(node
.parent
);
118 return isInBooleanContext(node
) ||
119 (isLogicalContext(node
.parent
) &&
121 // For nested logical statements
122 isInFlaggedContext(node
.parent
)
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.
132 function hasCommentsInside(node
) {
133 return Boolean(sourceCode
.getCommentsInside(node
).length
);
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.
142 function isParenthesized(node
) {
143 return eslintUtils
.isParenthesized(1, node
, sourceCode
);
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 * @throws {Error} (Unreachable.)
154 * @returns {boolean} `true` if the node needs to be parenthesized.
156 function needsParens(previousNode
, node
) {
157 if (previousNode
.parent
.type
=== "ChainExpression") {
158 return needsParens(previousNode
.parent
, node
);
160 if (isParenthesized(previousNode
)) {
162 // parentheses around the previous node will stay, so there is no need for an additional pair
166 // parent of the previous node will become parent of the replacement node
167 const parent
= previousNode
.parent
;
169 switch (parent
.type
) {
170 case "CallExpression":
171 case "NewExpression":
172 return node
.type
=== "SequenceExpression";
174 case "DoWhileStatement":
175 case "WhileStatement":
178 case "ConditionalExpression":
179 return precedence(node
) <= precedence(parent
);
180 case "UnaryExpression":
181 return precedence(node
) < precedence(parent
);
182 case "LogicalExpression":
183 if (astUtils
.isMixedLogicalAndCoalesceExpressions(node
, parent
)) {
186 if (previousNode
=== parent
.left
) {
187 return precedence(node
) < precedence(parent
);
189 return precedence(node
) <= precedence(parent
);
193 throw new Error(`Unexpected parent type: ${parent.type}`);
198 UnaryExpression(node
) {
199 const parent
= node
.parent
;
202 // Exit early if it's guaranteed not to match
203 if (node
.operator
!== "!" ||
204 parent
.type
!== "UnaryExpression" ||
205 parent
.operator
!== "!") {
210 if (isInFlaggedContext(parent
)) {
213 messageId
: "unexpectedNegation",
215 if (hasCommentsInside(parent
)) {
219 if (needsParens(parent
, node
.argument
)) {
220 return fixer
.replaceText(parent
, `(${sourceCode.getText(node.argument)})`);
224 const tokenBefore
= sourceCode
.getTokenBefore(parent
);
225 const firstReplacementToken
= sourceCode
.getFirstToken(node
.argument
);
229 tokenBefore
.range
[1] === parent
.range
[0] &&
230 !astUtils
.canTokensBeAdjacent(tokenBefore
, firstReplacementToken
)
235 return fixer
.replaceText(parent
, prefix
+ sourceCode
.getText(node
.argument
));
241 CallExpression(node
) {
242 if (node
.callee
.type
!== "Identifier" || node
.callee
.name
!== "Boolean") {
246 if (isInFlaggedContext(node
)) {
249 messageId
: "unexpectedCall",
251 const parent
= node
.parent
;
253 if (node
.arguments
.length
=== 0) {
254 if (parent
.type
=== "UnaryExpression" && parent
.operator
=== "!") {
260 if (hasCommentsInside(parent
)) {
264 const replacement
= "true";
266 const tokenBefore
= sourceCode
.getTokenBefore(parent
);
270 tokenBefore
.range
[1] === parent
.range
[0] &&
271 !astUtils
.canTokensBeAdjacent(tokenBefore
, replacement
)
276 return fixer
.replaceText(parent
, prefix
+ replacement
);
283 if (hasCommentsInside(node
)) {
287 return fixer
.replaceText(node
, "false");
290 if (node
.arguments
.length
=== 1) {
291 const argument
= node
.arguments
[0];
293 if (argument
.type
=== "SpreadElement" || hasCommentsInside(node
)) {
298 * Boolean(expression) -> expression
301 if (needsParens(node
, argument
)) {
302 return fixer
.replaceText(node
, `(${sourceCode.getText(argument)})`);
305 return fixer
.replaceText(node
, sourceCode
.getText(argument
));
308 // two or more arguments