/** * @fileoverview Rule to flag unnecessary double negation in Boolean contexts * @author Brandon Mills */ "use strict"; //------------------------------------------------------------------------------ // Requirements //------------------------------------------------------------------------------ const astUtils = require("./utils/ast-utils"); const eslintUtils = require("@eslint-community/eslint-utils"); const precedence = astUtils.getPrecedence; //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ /** @type {import('../types').Rule.RuleModule} */ module.exports = { meta: { type: "suggestion", defaultOptions: [{}], docs: { description: "Disallow unnecessary boolean casts", recommended: true, frozen: true, url: "https://eslint.org/docs/latest/rules/no-extra-boolean-cast", }, schema: [ { anyOf: [ { type: "object", properties: { enforceForInnerExpressions: { type: "boolean", }, }, additionalProperties: false, }, // deprecated { type: "object", properties: { enforceForLogicalOperands: { type: "boolean", }, }, additionalProperties: false, }, ], }, ], fixable: "code", messages: { unexpectedCall: "Redundant Boolean call.", unexpectedNegation: "Redundant double negation.", }, }, create(context) { const sourceCode = context.sourceCode; const [{ enforceForLogicalOperands, enforceForInnerExpressions }] = context.options; // Node types which have a test which will coerce values to booleans. const BOOLEAN_NODE_TYPES = new Set([ "IfStatement", "DoWhileStatement", "WhileStatement", "ConditionalExpression", "ForStatement", ]); /** * Check if a node is a Boolean function or constructor. * @param {ASTNode} node the node * @returns {boolean} If the node is Boolean function or constructor */ function isBooleanFunctionOrConstructorCall(node) { // Boolean() and new Boolean() return ( (node.type === "CallExpression" || node.type === "NewExpression") && node.callee.type === "Identifier" && node.callee.name === "Boolean" ); } /** * Check if a node is in a context where its value would be coerced to a boolean at runtime. * @param {ASTNode} node The node * @returns {boolean} If it is in a boolean context */ function isInBooleanContext(node) { return ( (isBooleanFunctionOrConstructorCall(node.parent) && node === node.parent.arguments[0]) || (BOOLEAN_NODE_TYPES.has(node.parent.type) && node === node.parent.test) || // ! (node.parent.type === "UnaryExpression" && node.parent.operator === "!") ); } /** * Checks whether the node is a context that should report an error * Acts recursively if it is in a logical context * @param {ASTNode} node the node * @returns {boolean} If the node is in one of the flagged contexts */ function isInFlaggedContext(node) { if (node.parent.type === "ChainExpression") { return isInFlaggedContext(node.parent); } /* * legacy behavior - enforceForLogicalOperands will only recurse on * logical expressions, not on other contexts. * enforceForInnerExpressions will recurse on logical expressions * as well as the other recursive syntaxes. */ if (enforceForLogicalOperands || enforceForInnerExpressions) { if (node.parent.type === "LogicalExpression") { if ( node.parent.operator === "||" || node.parent.operator === "&&" ) { return isInFlaggedContext(node.parent); } // Check the right hand side of a `??` operator. if ( enforceForInnerExpressions && node.parent.operator === "??" && node.parent.right === node ) { return isInFlaggedContext(node.parent); } } } if (enforceForInnerExpressions) { if ( node.parent.type === "ConditionalExpression" && (node.parent.consequent === node || node.parent.alternate === node) ) { return isInFlaggedContext(node.parent); } /* * Check last expression only in a sequence, i.e. if ((1, 2, Boolean(3))) {}, since * the others don't affect the result of the expression. */ if ( node.parent.type === "SequenceExpression" && node.parent.expressions.at(-1) === node ) { return isInFlaggedContext(node.parent); } } return isInBooleanContext(node); } /** * Check if a node has comments inside. * @param {ASTNode} node The node to check. * @returns {boolean} `true` if it has comments inside. */ function hasCommentsInside(node) { return Boolean(sourceCode.getCommentsInside(node).length); } /** * Checks if the given node is wrapped in grouping parentheses. Parentheses for constructs such as if() don't count. * @param {ASTNode} node The node to check. * @returns {boolean} `true` if the node is parenthesized. * @private */ function isParenthesized(node) { return eslintUtils.isParenthesized(1, node, sourceCode); } /** * Determines whether the given node needs to be parenthesized when replacing the previous node. * It assumes that `previousNode` is the node to be reported by this rule, so it has a limited list * of possible parent node types. By the same assumption, the node's role in a particular parent is already known. * @param {ASTNode} previousNode Previous node. * @param {ASTNode} node The node to check. * @throws {Error} (Unreachable.) * @returns {boolean} `true` if the node needs to be parenthesized. */ function needsParens(previousNode, node) { if (previousNode.parent.type === "ChainExpression") { return needsParens(previousNode.parent, node); } if (isParenthesized(previousNode)) { // parentheses around the previous node will stay, so there is no need for an additional pair return false; } // parent of the previous node will become parent of the replacement node const parent = previousNode.parent; switch (parent.type) { case "CallExpression": case "NewExpression": return node.type === "SequenceExpression"; case "IfStatement": case "DoWhileStatement": case "WhileStatement": case "ForStatement": case "SequenceExpression": return false; case "ConditionalExpression": if (previousNode === parent.test) { return precedence(node) <= precedence(parent); } if ( previousNode === parent.consequent || previousNode === parent.alternate ) { return ( precedence(node) < precedence({ type: "AssignmentExpression" }) ); } /* c8 ignore next */ throw new Error( "Ternary child must be test, consequent, or alternate.", ); case "UnaryExpression": return precedence(node) < precedence(parent); case "LogicalExpression": if ( astUtils.isMixedLogicalAndCoalesceExpressions( node, parent, ) ) { return true; } if (previousNode === parent.left) { return precedence(node) < precedence(parent); } return precedence(node) <= precedence(parent); /* c8 ignore next */ default: throw new Error(`Unexpected parent type: ${parent.type}`); } } return { UnaryExpression(node) { const parent = node.parent; // Exit early if it's guaranteed not to match if ( node.operator !== "!" || parent.type !== "UnaryExpression" || parent.operator !== "!" ) { return; } if (isInFlaggedContext(parent)) { context.report({ node: parent, messageId: "unexpectedNegation", fix(fixer) { if (hasCommentsInside(parent)) { return null; } if (needsParens(parent, node.argument)) { return fixer.replaceText( parent, `(${sourceCode.getText(node.argument)})`, ); } let prefix = ""; const tokenBefore = sourceCode.getTokenBefore(parent); const firstReplacementToken = sourceCode.getFirstToken(node.argument); if ( tokenBefore && tokenBefore.range[1] === parent.range[0] && !astUtils.canTokensBeAdjacent( tokenBefore, firstReplacementToken, ) ) { prefix = " "; } return fixer.replaceText( parent, prefix + sourceCode.getText(node.argument), ); }, }); } }, CallExpression(node) { if ( node.callee.type !== "Identifier" || node.callee.name !== "Boolean" ) { return; } if (isInFlaggedContext(node)) { context.report({ node, messageId: "unexpectedCall", fix(fixer) { const parent = node.parent; if (node.arguments.length === 0) { if ( parent.type === "UnaryExpression" && parent.operator === "!" ) { /* * !Boolean() -> true */ if (hasCommentsInside(parent)) { return null; } const replacement = "true"; let prefix = ""; const tokenBefore = sourceCode.getTokenBefore(parent); if ( tokenBefore && tokenBefore.range[1] === parent.range[0] && !astUtils.canTokensBeAdjacent( tokenBefore, replacement, ) ) { prefix = " "; } return fixer.replaceText( parent, prefix + replacement, ); } /* * Boolean() -> false */ if (hasCommentsInside(node)) { return null; } return fixer.replaceText(node, "false"); } if (node.arguments.length === 1) { const argument = node.arguments[0]; if ( argument.type === "SpreadElement" || hasCommentsInside(node) ) { return null; } /* * Boolean(expression) -> expression */ if (needsParens(node, argument)) { return fixer.replaceText( node, `(${sourceCode.getText(argument)})`, ); } return fixer.replaceText( node, sourceCode.getText(argument), ); } // two or more arguments return null; }, }); } }, }; }, };