| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420 |
- /**
- * @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('../types').Rule.RuleModule} */
- module.exports = {
- meta: {
- type: "suggestion",
- defaultOptions: [{}],
- docs: {
- description: "Disallow unnecessary boolean casts",
- recommended: true,
- frozen: true,
- url: "https://eslint.org/docs/latest/rules/no-extra-boolean-cast",
- },
- schema: [
- {
- anyOf: [
- {
- type: "object",
- properties: {
- enforceForInnerExpressions: {
- type: "boolean",
- },
- },
- additionalProperties: false,
- },
- // deprecated
- {
- type: "object",
- properties: {
- enforceForLogicalOperands: {
- type: "boolean",
- },
- },
- additionalProperties: false,
- },
- ],
- },
- ],
- fixable: "code",
- messages: {
- unexpectedCall: "Redundant Boolean call.",
- unexpectedNegation: "Redundant double negation.",
- },
- },
- create(context) {
- const sourceCode = context.sourceCode;
- const [{ enforceForLogicalOperands, enforceForInnerExpressions }] =
- context.options;
- // 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"
- );
- }
- /**
- * 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);
- }
- /*
- * legacy behavior - enforceForLogicalOperands will only recurse on
- * logical expressions, not on other contexts.
- * enforceForInnerExpressions will recurse on logical expressions
- * as well as the other recursive syntaxes.
- */
- if (enforceForLogicalOperands || enforceForInnerExpressions) {
- if (node.parent.type === "LogicalExpression") {
- if (
- node.parent.operator === "||" ||
- node.parent.operator === "&&"
- ) {
- return isInFlaggedContext(node.parent);
- }
- // Check the right hand side of a `??` operator.
- if (
- enforceForInnerExpressions &&
- node.parent.operator === "??" &&
- node.parent.right === node
- ) {
- return isInFlaggedContext(node.parent);
- }
- }
- }
- if (enforceForInnerExpressions) {
- if (
- node.parent.type === "ConditionalExpression" &&
- (node.parent.consequent === node ||
- node.parent.alternate === node)
- ) {
- return isInFlaggedContext(node.parent);
- }
- /*
- * Check last expression only in a sequence, i.e. if ((1, 2, Boolean(3))) {}, since
- * the others don't affect the result of the expression.
- */
- if (
- node.parent.type === "SequenceExpression" &&
- node.parent.expressions.at(-1) === node
- ) {
- return isInFlaggedContext(node.parent);
- }
- }
- return isInBooleanContext(node);
- }
- /**
- * 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.
- * @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":
- case "SequenceExpression":
- return false;
- case "ConditionalExpression":
- if (previousNode === parent.test) {
- return precedence(node) <= precedence(parent);
- }
- if (
- previousNode === parent.consequent ||
- previousNode === parent.alternate
- ) {
- return (
- precedence(node) <
- precedence({ type: "AssignmentExpression" })
- );
- }
- /* c8 ignore next */
- throw new Error(
- "Ternary child must be test, consequent, or alternate.",
- );
- 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;
- },
- });
- }
- },
- };
- },
- };
|