/** * @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; }, }); } }, }; }, };