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