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