--- /dev/null
+/**
+ * @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
+//------------------------------------------------------------------------------
+
+module.exports = {
+ meta: {
+ type: "suggestion",
+
+ docs: {
+ description: "disallow unnecessary boolean casts",
+ category: "Possible Errors",
+ 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 = [
+ "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.indexOf(node.parent.type) !== -1 &&
+ 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.
+ * @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;
+ }
+ });
+ }
+ }
+ };
+
+ }
+};