/** * @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-utils"); const precedence = astUtils.getPrecedence; //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ /** @type {import('../shared/types').Rule} */ module.exports = { meta: { type: "suggestion", docs: { description: "Disallow unnecessary boolean casts", recommended: true, url: "https://eslint.org/docs/rules/no-extra-boolean-cast" }, schema: [{ type: "object", properties: { enforceForLogicalOperands: { type: "boolean", default: false } }, additionalProperties: false }], fixable: "code", messages: { unexpectedCall: "Redundant Boolean call.", unexpectedNegation: "Redundant double negation." } }, create(context) { const sourceCode = context.getSourceCode(); // 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"; } /** * Checks whether the node is a logical expression and that the option is enabled * @param {ASTNode} node the node * @returns {boolean} if the node is a logical expression and option is enabled */ function isLogicalContext(node) { return node.type === "LogicalExpression" && (node.operator === "||" || node.operator === "&&") && (context.options.length && context.options[0].enforceForLogicalOperands === true); } /** * 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); } return isInBooleanContext(node) || (isLogicalContext(node.parent) && // For nested logical statements isInFlaggedContext(node.parent) ); } /** * 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. * For example, if the parent is `ConditionalExpression`, `previousNode` must be its `test` child. * @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": return false; case "ConditionalExpression": return precedence(node) <= precedence(parent); 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; } }); } } }; } };