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 //------------------------------------------------------------------------------
26 description: "disallow unnecessary boolean casts",
27 category: "Possible Errors",
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 = [
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.indexOf(node.parent.type) !== -1 &&
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 * @returns {boolean} `true` if the node needs to be parenthesized.
155 function needsParens(previousNode, node) {
156 if (previousNode.parent.type === "ChainExpression") {
157 return needsParens(previousNode.parent, node);
159 if (isParenthesized(previousNode)) {
161 // parentheses around the previous node will stay, so there is no need for an additional pair
165 // parent of the previous node will become parent of the replacement node
166 const parent = previousNode.parent;
168 switch (parent.type) {
169 case "CallExpression":
170 case "NewExpression":
171 return node.type === "SequenceExpression";
173 case "DoWhileStatement":
174 case "WhileStatement":
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)) {
185 if (previousNode === parent.left) {
186 return precedence(node) < precedence(parent);
188 return precedence(node) <= precedence(parent);
190 /* istanbul ignore next */
192 throw new Error(`Unexpected parent type: ${parent.type}`);
197 UnaryExpression(node) {
198 const parent = node.parent;
201 // Exit early if it's guaranteed not to match
202 if (node.operator !== "!" ||
203 parent.type !== "UnaryExpression" ||
204 parent.operator !== "!") {
209 if (isInFlaggedContext(parent)) {
212 messageId: "unexpectedNegation",
214 if (hasCommentsInside(parent)) {
218 if (needsParens(parent, node.argument)) {
219 return fixer.replaceText(parent, `(${sourceCode.getText(node.argument)})`);
223 const tokenBefore = sourceCode.getTokenBefore(parent);
224 const firstReplacementToken = sourceCode.getFirstToken(node.argument);
228 tokenBefore.range[1] === parent.range[0] &&
229 !astUtils.canTokensBeAdjacent(tokenBefore, firstReplacementToken)
234 return fixer.replaceText(parent, prefix + sourceCode.getText(node.argument));
240 CallExpression(node) {
241 if (node.callee.type !== "Identifier" || node.callee.name !== "Boolean") {
245 if (isInFlaggedContext(node)) {
248 messageId: "unexpectedCall",
250 const parent = node.parent;
252 if (node.arguments.length === 0) {
253 if (parent.type === "UnaryExpression" && parent.operator === "!") {
259 if (hasCommentsInside(parent)) {
263 const replacement = "true";
265 const tokenBefore = sourceCode.getTokenBefore(parent);
269 tokenBefore.range[1] === parent.range[0] &&
270 !astUtils.canTokensBeAdjacent(tokenBefore, replacement)
275 return fixer.replaceText(parent, prefix + replacement);
282 if (hasCommentsInside(node)) {
286 return fixer.replaceText(node, "false");
289 if (node.arguments.length === 1) {
290 const argument = node.arguments[0];
292 if (argument.type === "SpreadElement" || hasCommentsInside(node)) {
297 * Boolean(expression) -> expression
300 if (needsParens(node, argument)) {
301 return fixer.replaceText(node, `(${sourceCode.getText(argument)})`);
304 return fixer.replaceText(node, sourceCode.getText(argument));
307 // two or more arguments