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;
 | |
|                         }
 | |
|                     });
 | |
|                 }
 | |
|             }
 | |
|         };
 | |
| 
 | |
|     }
 | |
| };
 |