318 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			318 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/**
 | 
						|
 * @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('../shared/types').Rule} */
 | 
						|
module.exports = {
 | 
						|
    meta: {
 | 
						|
        type: "suggestion",
 | 
						|
 | 
						|
        docs: {
 | 
						|
            description: "Disallow unnecessary boolean casts",
 | 
						|
            recommended: true,
 | 
						|
            url: "https://eslint.org/docs/latest/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.sourceCode;
 | 
						|
 | 
						|
        // 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(<bool>) and new Boolean(<bool>)
 | 
						|
            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) ||
 | 
						|
 | 
						|
                // !<bool>
 | 
						|
                (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;
 | 
						|
                        }
 | 
						|
                    });
 | 
						|
                }
 | 
						|
            }
 | 
						|
        };
 | 
						|
 | 
						|
    }
 | 
						|
};
 |