From 6acb08f0544ecbb9c5a63cfae64670f76245329b Mon Sep 17 00:00:00 2001 From: Boshen Date: Thu, 9 Mar 2023 20:33:45 +0800 Subject: [PATCH] feat(linter): implement no_constant_binary_expression --- crates/oxc_ast/src/ast/js.rs | 34 + crates/oxc_linter/src/ast_util.rs | 166 +++++ crates/oxc_linter/src/context.rs | 9 +- crates/oxc_linter/src/globals.rs | 75 ++ crates/oxc_linter/src/lib.rs | 1 + crates/oxc_linter/src/rules.rs | 1 + .../rules/no_constant_binary_expression.rs | 675 ++++++++++++++++++ 7 files changed, 960 insertions(+), 1 deletion(-) create mode 100644 crates/oxc_linter/src/globals.rs create mode 100644 crates/oxc_linter/src/rules/no_constant_binary_expression.rs diff --git a/crates/oxc_ast/src/ast/js.rs b/crates/oxc_ast/src/ast/js.rs index 4ac9980c2..d0738be1b 100644 --- a/crates/oxc_ast/src/ast/js.rs +++ b/crates/oxc_ast/src/ast/js.rs @@ -1,5 +1,6 @@ use std::fmt::Display; +use num_bigint::BigUint; use oxc_allocator::{Box, Vec}; use serde::Serialize; @@ -125,6 +126,24 @@ impl<'a> Expression<'a> { matches!(self, Self::Identifier(ident) if ident.name == "undefined") } + /// Determines whether the given expr is a `void 0` + #[must_use] + pub fn is_void_0(&self) -> bool { + matches!(self, Self::UnaryExpression(expr) if expr.operator == UnaryOperator::Void) + } + + /// Determines whether the given expr evaluate to `undefined` + #[must_use] + pub fn evaluate_to_undefined(&self) -> bool { + self.is_undefined() || self.is_void_0() + } + + /// Determines whether the given expr is a `null` or `undefined` or `void 0` + #[must_use] + pub fn is_null_or_undefined(&self) -> bool { + self.is_null() || self.evaluate_to_undefined() + } + /// Remove nested parentheses from this expression. #[must_use] pub fn without_parenthesized(&self) -> &Self { @@ -168,6 +187,21 @@ impl<'a> Expression<'a> { pub fn is_function(&self) -> bool { matches!(self, Expression::FunctionExpression(_) | Expression::ArrowFunctionExpression(_)) } + + /// Returns literal's value converted to the Boolean type + /// returns `true` when node is truthy, `false` when node is falsy, `None` when it cannot be determined. + #[must_use] + pub fn get_boolean_value(&self) -> Option { + match self { + Self::BooleanLiteral(lit) => Some(lit.value), + Self::NullLiteral(_) => Some(false), + Self::NumberLiteral(lit) => Some(lit.value != 0.0), + Self::BigintLiteral(lit) => Some(lit.value != BigUint::new(vec![])), + Self::RegExpLiteral(_) => Some(true), + Self::StringLiteral(lit) => Some(!lit.value.is_empty()), + _ => None, + } + } } /// Section 12.6 `IdentifierName` diff --git a/crates/oxc_linter/src/ast_util.rs b/crates/oxc_linter/src/ast_util.rs index 64f7ad243..f9a2515e4 100644 --- a/crates/oxc_linter/src/ast_util.rs +++ b/crates/oxc_linter/src/ast_util.rs @@ -1,5 +1,9 @@ +#[allow(clippy::wildcard_imports)] +use oxc_ast::ast::*; use phf::{phf_set, Set}; +use crate::context::LintContext; + pub const STRICT_MODE_NAMES: Set<&'static str> = phf_set! { "implements", "interface", @@ -11,3 +15,165 @@ pub const STRICT_MODE_NAMES: Set<&'static str> = phf_set! { "static", "yield", }; + +/// Test if an AST node is a boolean value that never changes. Specifically we +/// test for: +/// 1. Literal booleans (`true` or `false`) +/// 2. Unary `!` expressions with a constant value +/// 3. Constant booleans created via the `Boolean` global function +pub fn is_static_boolean<'a>(expr: &Expression<'a>, ctx: &LintContext<'a>) -> bool { + match expr { + Expression::BooleanLiteral(_) => true, + Expression::CallExpression(call_expr) => call_expr.is_constant(true, ctx), + Expression::UnaryExpression(unary_expr) => { + unary_expr.operator == UnaryOperator::LogicalNot + && unary_expr.argument.is_constant(true, ctx) + } + _ => false, + } +} + +/// Checks if a branch node of `LogicalExpression` short circuits the whole condition +fn is_logical_identity(op: LogicalOperator, expr: &Expression) -> bool { + match expr { + expr if expr.is_literal_expression() => { + let boolean_value = expr.get_boolean_value(); + (op == LogicalOperator::Or && boolean_value == Some(true)) + || (op == LogicalOperator::And && boolean_value == Some(false)) + } + Expression::UnaryExpression(unary_expr) => { + op == LogicalOperator::And && unary_expr.operator == UnaryOperator::Void + } + Expression::LogicalExpression(logical_expr) => { + op == logical_expr.operator + && (is_logical_identity(logical_expr.operator, &logical_expr.left) + || is_logical_identity(logical_expr.operator, &logical_expr.right)) + } + Expression::AssignmentExpression(assign_expr) => { + matches!( + assign_expr.operator, + AssignmentOperator::LogicalAnd | AssignmentOperator::LogicalOr + ) && ((op == LogicalOperator::And + && assign_expr.operator == AssignmentOperator::LogicalAnd) + || (op == LogicalOperator::Or + && assign_expr.operator == AssignmentOperator::LogicalOr)) + && is_logical_identity(op, &assign_expr.right) + } + Expression::ParenthesizedExpression(expr) => is_logical_identity(op, &expr.expression), + _ => false, + } +} + +/// Checks if a node has a constant truthiness value. +/// `inBooleanPosition`: +/// `true` if checking the test of a condition. +/// `false` in all other cases. +/// When `false`, checks if -- for both string and number -- +/// if coerced to that type, the value will be constant. +pub trait IsConstant<'a, 'b> { + fn is_constant(&self, in_boolean_position: bool, ctx: &LintContext<'a>) -> bool; +} + +impl<'a, 'b> IsConstant<'a, 'b> for Expression<'a> { + fn is_constant(&self, in_boolean_position: bool, ctx: &LintContext<'a>) -> bool { + match self { + Self::ArrowFunctionExpression(_) + | Self::FunctionExpression(_) + | Self::ClassExpression(_) + | Self::ObjectExpression(_) => true, + Self::TemplateLiteral(template) => { + let test_quasis = in_boolean_position + && template.quasis.iter().any(|quasi| { + quasi.value.cooked.as_ref().map_or(false, |cooked| !cooked.is_empty()) + }); + let test_expressions = + template.expressions.iter().all(|expr| expr.is_constant(false, ctx)); + test_quasis || test_expressions + } + Self::ArrayExpression(expr) => { + if in_boolean_position { + return true; + } + expr.elements + .iter() + .all(|element| element.as_ref().map_or(true, |e| e.is_constant(false, ctx))) + } + Self::UnaryExpression(expr) => match expr.operator { + UnaryOperator::Void => true, + UnaryOperator::Typeof if in_boolean_position => true, + UnaryOperator::LogicalNot => expr.argument.is_constant(true, ctx), + _ => expr.argument.is_constant(false, ctx), + }, + Self::BinaryExpression(expr) => { + expr.operator != BinaryOperator::In + && expr.left.is_constant(false, ctx) + && expr.right.is_constant(false, ctx) + } + Self::LogicalExpression(expr) => { + let is_left_constant = expr.left.is_constant(in_boolean_position, ctx); + let is_right_constant = expr.right.is_constant(in_boolean_position, ctx); + let is_left_short_circuit = + is_left_constant && is_logical_identity(expr.operator, &expr.left); + let is_right_short_circuit = in_boolean_position + && is_right_constant + && is_logical_identity(expr.operator, &expr.right); + (is_left_constant && is_right_constant) + || is_left_short_circuit + || is_right_short_circuit + } + Self::NewExpression(_) => in_boolean_position, + Self::AssignmentExpression(expr) => match expr.operator { + AssignmentOperator::Assign => expr.right.is_constant(in_boolean_position, ctx), + AssignmentOperator::LogicalAnd if in_boolean_position => { + is_logical_identity(LogicalOperator::And, &expr.right) + } + AssignmentOperator::LogicalOr if in_boolean_position => { + is_logical_identity(LogicalOperator::Or, &expr.right) + } + _ => false, + }, + Self::SequenceExpression(sequence_expr) => sequence_expr + .expressions + .iter() + .last() + .map_or(false, |last| last.is_constant(in_boolean_position, ctx)), + Self::CallExpression(call_expr) => call_expr.is_constant(in_boolean_position, ctx), + Self::ParenthesizedExpression(paren_expr) => { + paren_expr.expression.is_constant(in_boolean_position, ctx) + } + Self::Identifier(ident) => { + ident.name == "undefined" && ctx.is_reference_to_global_variable(ident) + } + _ if self.is_literal_expression() => true, + _ => false, + } + } +} + +impl<'a, 'b> IsConstant<'a, 'b> for CallExpression<'a> { + fn is_constant(&self, _in_boolean_position: bool, ctx: &LintContext<'a>) -> bool { + if let Expression::Identifier(ident) = &self.callee { + if ident.name == "Boolean" + && self.arguments.iter().next().map_or(true, |first| first.is_constant(true, ctx)) + { + return ctx.is_reference_to_global_variable(ident); + } + } + false + } +} + +impl<'a, 'b> IsConstant<'a, 'b> for Argument<'a> { + fn is_constant(&self, in_boolean_position: bool, ctx: &LintContext<'a>) -> bool { + match self { + Self::SpreadElement(element) => element.is_constant(in_boolean_position, ctx), + Self::Expression(expr) => expr.is_constant(in_boolean_position, ctx), + } + } +} + +impl<'a, 'b> IsConstant<'a, 'b> for SpreadElement<'a> { + fn is_constant(&self, in_boolean_position: bool, ctx: &LintContext<'a>) -> bool { + self.argument.is_constant(in_boolean_position, ctx) + } +} diff --git a/crates/oxc_linter/src/context.rs b/crates/oxc_linter/src/context.rs index bafcaa496..c3be64673 100644 --- a/crates/oxc_linter/src/context.rs +++ b/crates/oxc_linter/src/context.rs @@ -1,7 +1,7 @@ use std::{cell::RefCell, rc::Rc}; use indextree::{Ancestors, NodeId}; -use oxc_ast::{AstKind, SourceType}; +use oxc_ast::{ast::IdentifierReference, AstKind, SourceType}; use oxc_diagnostics::Error; use oxc_semantic::{AstNodes, Scope, ScopeTree, Semantic, SemanticNode}; @@ -108,4 +108,11 @@ impl<'a> LintContext<'a> { let scope = self.scope(node); node.get().strict_mode(scope) } + + /* Symbols */ + + #[allow(clippy::unused_self)] + pub fn is_reference_to_global_variable(&self, _ident: &IdentifierReference) -> bool { + false + } } diff --git a/crates/oxc_linter/src/globals.rs b/crates/oxc_linter/src/globals.rs new file mode 100644 index 000000000..77935b0a4 --- /dev/null +++ b/crates/oxc_linter/src/globals.rs @@ -0,0 +1,75 @@ +//! [Globals](https://github.com/sindresorhus/globals/blob/main/globals.json) +//! Each global is given a value of true or false. +//! A value of true indicates that the variable may be overwritten. +//! A value of false indicates that the variable should be considered read-only. + +use phf::{phf_map, Map}; + +pub const BUILTINS: Map<&'static str, bool> = phf_map! { + "AggregateError" => false, + "Array" => false, + "ArrayBuffer" => false, + "Atomics" => false, + "BigInt" => false, + "BigInt64Array" => false, + "BigUint64Array" => false, + "Boolean" => false, + "constructor" => false, + "DataView" => false, + "Date" => false, + "decodeURI" => false, + "decodeURIComponent" => false, + "encodeURI" => false, + "encodeURIComponent" => false, + "Error" => false, + "escape" => false, + "eval" => false, + "EvalError" => false, + "FinalizationRegistry" => false, + "Float32Array" => false, + "Float64Array" => false, + "Function" => false, + "globalThis" => false, + "hasOwnProperty" => false, + "Infinity" => false, + "Int16Array" => false, + "Int32Array" => false, + "Int8Array" => false, + "isFinite" => false, + "isNaN" => false, + "isPrototypeOf" => false, + "JSON" => false, + "Map" => false, + "Math" => false, + "NaN" => false, + "Number" => false, + "Object" => false, + "parseFloat" => false, + "parseInt" => false, + "Promise" => false, + "propertyIsEnumerable" => false, + "Proxy" => false, + "RangeError" => false, + "ReferenceError" => false, + "Reflect" => false, + "RegExp" => false, + "Set" => false, + "SharedArrayBuffer" => false, + "String" => false, + "Symbol" => false, + "Diagnostic" => false, + "toLocaleString" => false, + "toString" => false, + "TypeError" => false, + "Uint16Array" => false, + "Uint32Array" => false, + "Uint8Array" => false, + "Uint8ClampedArray" => false, + "undefined" => false, + "unescape" => false, + "URIError" => false, + "valueOf" => false, + "WeakMap" => false, + "WeakRef" => false, + "WeakSet" => false +}; diff --git a/crates/oxc_linter/src/lib.rs b/crates/oxc_linter/src/lib.rs index 2d20da575..e20c71b88 100644 --- a/crates/oxc_linter/src/lib.rs +++ b/crates/oxc_linter/src/lib.rs @@ -6,6 +6,7 @@ mod tester; mod ast_util; mod context; mod fixer; +mod globals; pub mod rule; mod rules; diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 48d248b3a..8e953751b 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -11,5 +11,6 @@ oxc_macros::declare_all_lint_rules! { no_empty, no_empty_pattern, no_mixed_operators, + no_constant_binary_expression, deepscan::uninvoked_array_callback, } diff --git a/crates/oxc_linter/src/rules/no_constant_binary_expression.rs b/crates/oxc_linter/src/rules/no_constant_binary_expression.rs new file mode 100644 index 000000000..8bb433934 --- /dev/null +++ b/crates/oxc_linter/src/rules/no_constant_binary_expression.rs @@ -0,0 +1,675 @@ +#[allow(clippy::wildcard_imports)] +use oxc_ast::{ast::*, AstKind, Span}; +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::Error, +}; +use oxc_macros::declare_oxc_lint; + +use crate::{ + ast_util::{self, IsConstant}, + context::LintContext, + globals::BUILTINS, + rule::Rule, + AstNode, +}; + +/// `https://eslint.org/docs/latest/rules/no-constant-binary-expression` +/// Original Author: Jordan Eldredge +#[derive(Debug, Default, Clone)] +pub struct NoConstantBinaryExpression; + +declare_oxc_lint!( + /// ### What it does + /// Disallow expressions where the operation doesn't affect the value + /// + /// ### Why is this bad? + /// Comparisons which will always evaluate to true or false and logical expressions (||, &&, ??) which either always + /// short-circuit or never short-circuit are both likely indications of programmer error. + /// + /// These errors are especially common in complex expressions where operator precedence is easy to misjudge. + /// + /// Additionally, this rule detects comparisons to newly constructed objects/arrays/functions/etc. + /// In JavaScript, where objects are compared by reference, a newly constructed object can never === any other value. + /// This can be surprising for programmers coming from languages where objects are compared by value. + /// + /// ### Example + /// ```javascript + /// // One might think this would evaluate as `a + (b ?? c)`: + /// const x = a + b ?? c; + /// + /// // But it actually evaluates as `(a + b) ?? c`. Since `a + b` can never be null, + /// // the `?? c` has no effect. + /// + /// // Programmers coming from a language where objects are compared by value might expect this to work: + /// const isEmpty = x === []; + /// + /// // However, this will always result in `isEmpty` being `false`. + /// ``` + NoConstantBinaryExpression, + nursery +); + +#[derive(Debug, Error, Diagnostic)] +#[error( + "eslint(no-constant-binary-expression): Disallow expressions where the operation doesn't affect the value" +)] +#[diagnostic()] +struct NoConstantBinaryExpressionDiagnostic(#[label] pub Span); + +#[derive(Debug, Error, Diagnostic)] +#[error( + "eslint(no-constant-binary-expression): Unexpected constant {0:?} on the left-hand side of a `{1:?}` expression" +)] +#[diagnostic(severity(warning))] +struct ConstantShortCircuit( + &'static str, // property + String, // operator + #[label("This expression always evaluates to the constant on the left-hand side")] Span, +); + +#[derive(Debug, Error, Diagnostic)] +#[error("eslint(no-constant-binary-expression): Unexpected constant binary expression")] +#[diagnostic(severity(warning))] +struct ConstantBinaryOperand( + &'static str, // otherSide + String, // operator + #[label("This compares constantly with the {0}-hand side of the `{1}`")] Span, +); + +#[derive(Debug, Error, Diagnostic)] +#[error("eslint(no-constant-binary-expression): Unexpected comparison to newly constructed object")] +#[diagnostic(severity(warning))] +struct ConstantAlwaysNew(#[label("These two values can never be equal")] Span); + +#[derive(Debug, Error, Diagnostic)] +#[error( + "eslint(no-constant-binary-expression): Unexpected comparison of two newly constructed objects" +)] +#[diagnostic(severity(warning))] +struct ConstantBothAlwaysNew(#[label("These two values can never be equal")] Span); + +impl Rule for NoConstantBinaryExpression { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + match node.get().kind() { + AstKind::LogicalExpression(expr) => match expr.operator { + LogicalOperator::Or | LogicalOperator::And if expr.left.is_constant(true, ctx) => { + ctx.diagnostic(ConstantShortCircuit( + "truthiness", + expr.operator.to_string(), + expr.span, + )); + } + LogicalOperator::Coalesce + if Self::has_constant_nullishness(&expr.left, false, ctx) => + { + ctx.diagnostic(ConstantShortCircuit( + "nullishness", + expr.operator.to_string(), + expr.span, + )); + } + _ => {} + }, + AstKind::BinaryExpression(expr) => { + let left = &expr.left; + let right = &expr.right; + let operator = expr.operator; + + let right_constant_operand = + Self::find_binary_expression_constant_operand(left, right, operator, ctx); + + let left_constant_operand = + Self::find_binary_expression_constant_operand(right, left, operator, ctx); + + if right_constant_operand.is_some() { + ctx.diagnostic(ConstantBinaryOperand("left", operator.to_string(), expr.span)); + return; + } + + if left_constant_operand.is_some() { + ctx.diagnostic(ConstantBinaryOperand("right", operator.to_string(), expr.span)); + return; + } + + if matches!( + operator, + BinaryOperator::StrictEquality | BinaryOperator::StrictInequality + ) && (Self::is_always_new(left, ctx) || Self::is_always_new(right, ctx)) + { + ctx.diagnostic(ConstantAlwaysNew(expr.span)); + return; + } + + if matches!(operator, BinaryOperator::Equality | BinaryOperator::Inequality) + && Self::is_always_new(left, ctx) + && Self::is_always_new(right, ctx) + { + ctx.diagnostic(ConstantBothAlwaysNew(expr.span)); + } + } + _ => {} + } + } +} + +impl NoConstantBinaryExpression { + /// Test if an AST node has a statically knowable constant nullishness. Meaning, + /// it will always resolve to a constant value of either: `null`, `undefined` + /// or not `null` _or_ `undefined`. An expression that can vary between those + /// three states at runtime would return `false`. + fn has_constant_nullishness<'a>( + expr: &Expression<'a>, + non_nullish: bool, + ctx: &LintContext<'a>, + ) -> bool { + if non_nullish && (expr.is_null() || expr.evaluate_to_undefined()) { + return false; + } + match expr.get_inner_expression() { + Expression::ObjectExpression(_) + | Expression::ArrayExpression(_) + | Expression::ArrowFunctionExpression(_) + | Expression::FunctionExpression(_) + | Expression::ClassExpression(_) + | Expression::NewExpression(_) + | Expression::TemplateLiteral(_) + | Expression::UpdateExpression(_) + | Expression::BinaryExpression(_) + | Expression::UnaryExpression(_) => true, + expr if expr.is_literal_expression() => true, + Expression::CallExpression(call_expr) => { + if let Expression::Identifier(ident) = &call_expr.callee { + return ["Boolean", "String", "Number"].contains(&ident.name.as_str()) + && ctx.is_reference_to_global_variable(ident); + } + false + } + Expression::LogicalExpression(logical_expr) + if logical_expr.operator == LogicalOperator::Coalesce => + { + Self::has_constant_nullishness(&logical_expr.right, true, ctx) + } + Expression::AssignmentExpression(assign_expr) => match assign_expr.operator { + AssignmentOperator::Assign => { + Self::has_constant_nullishness(&assign_expr.right, non_nullish, ctx) + } + op if op.is_logical_operator() => false, + _ => true, + }, + Expression::SequenceExpression(sequence_expr) => sequence_expr + .expressions + .iter() + .last() + .map_or(false, |last| Self::has_constant_nullishness(last, non_nullish, ctx)), + Expression::Identifier(_) => expr.evaluate_to_undefined(), + _ => false, + } + } + + /// Checks if one operand will cause the result to be constant. + fn find_binary_expression_constant_operand<'a>( + a: &'a Expression<'a>, + b: &'a Expression<'a>, + operator: BinaryOperator, + ctx: &LintContext<'a>, + ) -> Option<&'a Expression<'a>> { + match operator { + BinaryOperator::Equality | BinaryOperator::Inequality => { + if (a.is_null_or_undefined() && Self::has_constant_nullishness(b, false, ctx)) + || (ast_util::is_static_boolean(a, ctx) + && Self::has_constant_loose_boolean_comparison(b, ctx)) + { + return Some(b); + } + } + BinaryOperator::StrictEquality | BinaryOperator::StrictInequality => { + if (a.is_null_or_undefined() && Self::has_constant_nullishness(b, false, ctx)) + || (ast_util::is_static_boolean(a, ctx) + && Self::has_constant_strict_boolean_comparison(b, ctx)) + { + return Some(b); + } + } + _ => {} + } + None + } + + /// Test if an AST node will always give the same result when compared to a + /// boolean value. Note that comparison to boolean values is different than + /// truthiness. + /// `https://262.ecma-international.org/5.1/#sec-11.9.3` + fn has_constant_loose_boolean_comparison<'a>( + expr: &Expression<'a>, + ctx: &LintContext<'a>, + ) -> bool { + match expr { + Expression::ObjectExpression(_) + | Expression::ClassExpression(_) + | Expression::ArrowFunctionExpression(_) + | Expression::FunctionExpression(_) => true, + Expression::ArrayExpression(array_expr) => { + array_expr.elements.is_empty() + || array_expr + .elements + .iter() + .filter(|e| matches!(e, Some(Argument::Expression(_)))) + .count() + > 1 + } + Expression::UnaryExpression(unary_expr) => match unary_expr.operator { + UnaryOperator::Void | UnaryOperator::Typeof => true, + UnaryOperator::LogicalNot => unary_expr.argument.is_constant(true, ctx), + _ => false, + }, + Expression::CallExpression(call_expr) => call_expr.is_constant(true, ctx), + Expression::TemplateLiteral(lit) => lit.expressions.is_empty(), + Expression::AssignmentExpression(assignment_expr) => { + assignment_expr.operator == AssignmentOperator::Assign + && Self::has_constant_loose_boolean_comparison(&assignment_expr.right, ctx) + } + Expression::SequenceExpression(sequence_expr) => sequence_expr + .expressions + .iter() + .last() + .map_or(false, |last| Self::has_constant_loose_boolean_comparison(last, ctx)), + Expression::ParenthesizedExpression(paren_expr) => { + Self::has_constant_loose_boolean_comparison(&paren_expr.expression, ctx) + } + expr if expr.is_literal_expression() => true, + expr if expr.evaluate_to_undefined() => true, + _ => false, + } + } + + /// Test if an AST node will always give the same result when _strictly_ compared + /// to a boolean value. This can happen if the expression can never be boolean, or + /// if it is always the same boolean value. + fn has_constant_strict_boolean_comparison<'a>( + expr: &Expression<'a>, + ctx: &LintContext<'a>, + ) -> bool { + match expr { + Expression::ObjectExpression(_) + | Expression::ArrayExpression(_) + | Expression::ArrowFunctionExpression(_) + | Expression::FunctionExpression(_) + | Expression::NewExpression(_) + | Expression::TemplateLiteral(_) + | Expression::UpdateExpression(_) => true, + expr if expr.is_literal_expression() => true, + Expression::BinaryExpression(binary_expr) => { + binary_expr.operator.is_numeric_or_string_binary_operator() + } + Expression::UnaryExpression(unary_expr) => match unary_expr.operator { + UnaryOperator::Delete => false, + UnaryOperator::LogicalNot => unary_expr.argument.is_constant(true, ctx), + _ => true, + }, + Expression::CallExpression(call_expr) => { + if let Expression::Identifier(ident) = &call_expr.callee { + if ident.name == "String" + || ident.name == "Number" && ctx.is_reference_to_global_variable(ident) + { + return true; + } + + if ident.name == "Boolean" && ctx.is_reference_to_global_variable(ident) { + return call_expr + .arguments + .iter() + .next() + .map_or(true, |first| first.is_constant(true, ctx)); + } + } + false + } + Expression::AssignmentExpression(assign_expr) => match assign_expr.operator { + AssignmentOperator::Assign => { + Self::has_constant_strict_boolean_comparison(&assign_expr.right, ctx) + } + op if op.is_logical_operator() => false, + _ => true, + }, + Expression::SequenceExpression(sequence_expr) => sequence_expr + .expressions + .iter() + .last() + .map_or(false, |last| Self::has_constant_strict_boolean_comparison(last, ctx)), + Expression::ParenthesizedExpression(paren_expr) => { + Self::has_constant_strict_boolean_comparison(&paren_expr.expression, ctx) + } + Expression::Identifier(_) => expr.evaluate_to_undefined(), + _ => false, + } + } + + /// Test if an AST node will always result in a newly constructed object + fn is_always_new<'a>(expr: &Expression<'a>, ctx: &LintContext<'a>) -> bool { + match expr { + Expression::ObjectExpression(_) + | Expression::ArrayExpression(_) + | Expression::ArrowFunctionExpression(_) + | Expression::FunctionExpression(_) + | Expression::ClassExpression(_) + | Expression::RegExpLiteral(_) => true, + Expression::NewExpression(call_expr) => { + if let Expression::Identifier(ident) = &call_expr.callee { + return BUILTINS.contains_key(ident.name.as_str()) + && ctx.is_reference_to_global_variable(ident); + } + false + } + Expression::SequenceExpression(sequence_expr) => sequence_expr + .expressions + .iter() + .last() + .map_or(false, |last| Self::is_always_new(last, ctx)), + Expression::AssignmentExpression(assignment_expr) + if assignment_expr.operator == AssignmentOperator::Assign => + { + Self::is_always_new(&assignment_expr.right, ctx) + } + Expression::ConditionalExpression(cond_expr) => { + Self::is_always_new(&cond_expr.consequent, ctx) + && Self::is_always_new(&cond_expr.alternate, ctx) + } + Expression::ParenthesizedExpression(paren_expr) => { + Self::is_always_new(&paren_expr.expression, ctx) + } + _ => false, + } + } +} + +#[test] +#[allow(clippy::too_many_lines)] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + // While this _would_ be a constant condition in React, ESLint has a policy of not attributing any specific behavior to JSX. + ("

&& foo", None), + ("<> && foo", None), + ("

?? foo", None), + ("<> ?? foo", None), + ("arbitraryFunction(n) ?? foo", None), + ("foo.Boolean(n) ?? foo", None), + ("(x += 1) && foo", None), + ("`${bar}` && foo", None), + ("bar && foo", None), + ("delete bar.baz && foo", None), + ("true ? foo : bar", None), // We leave ConditionalExpression for `no-constant-condition` + ("new Foo() == true", None), + ("foo == true", None), + ("`${foo}` == true", None), + ("`${foo}${bar}` == true", None), + ("`0${foo}` == true", None), + ("`00000000${foo}` == true", None), + ("`0${foo}.000` == true", None), + ("[n] == true", None), + ("delete bar.baz === true", None), + ("foo.Boolean(true) && foo", None), + ("function Boolean(n) { return n; }; Boolean(x) ?? foo", None), + ("function String(n) { return n; }; String(x) ?? foo", None), + ("function Number(n) { return n; }; Number(x) ?? foo", None), + ("function Boolean(n) { return Math.random(); }; Boolean(x) === 1", None), + ("function Boolean(n) { return Math.random(); }; Boolean(1) == true", None), + ("new Foo() === x", None), + ("x === new someObj.Promise()", None), + ("Boolean(foo) === true", None), + // ("function foo(undefined) { undefined ?? bar;}", None), + // ("function foo(undefined) { undefined == true;}", None), + // ("function foo(undefined) { undefined === true;}", None), + ("[...arr, 1] == true", None), + ("[,,,] == true", None), + // { code: "new Foo() === bar;", globals: { Foo: "writable" } }, + ("(foo && true) ?? bar", None), + ("foo ?? null ?? bar", None), + ("a ?? (doSomething(), undefined) ?? b", None), + ("a ?? (something = null) ?? b", None), + ]; + + let fail = vec![ + // Error messages + ("[] && greeting", None), + ("[] || greeting", None), + ("[] ?? greeting", None), + ("[] == true", None), + ("true == []", None), + ("[] != true", None), + ("[] === true", None), + ("[] !== true", None), + // Motivating examples from the original proposal https://github.com/eslint/eslint/issues/13752 + ("!foo == null", None), + ("!foo ?? bar", None), + ("(a + b) / 2 ?? bar", None), + ("String(foo.bar) ?? baz", None), + ("'hello' + name ?? ''", None), + ("[foo?.bar ?? ''] ?? []", None), + // Logical expression with constant truthiness + ("true && hello", None), + ("true || hello", None), + ("true && foo", None), + ("'' && foo", None), + ("100 && foo", None), + ("+100 && foo", None), + ("-100 && foo", None), + ("~100 && foo", None), + ("/[a-z]/ && foo", None), + ("Boolean([]) && foo", None), + ("Boolean() && foo", None), + ("Boolean([], n) && foo", None), + ("({}) && foo", None), + ("[] && foo", None), + ("(() => {}) && foo", None), + ("(function() {}) && foo", None), + ("(class {}) && foo", None), + ("(class { valueOf() { return x; } }) && foo", None), + ("(class { [x]() { return x; } }) && foo", None), + ("new Foo() && foo", None), + // (boxed values are always truthy) + ("new Boolean(unknown) && foo", None), + ("(bar = false) && foo", None), + ("(bar.baz = false) && foo", None), + ("(bar[0] = false) && foo", None), + ("`hello ${hello}` && foo", None), + ("void bar && foo", None), + ("!true && foo", None), + ("typeof bar && foo", None), + ("(bar, baz, true) && foo", None), + ("undefined && foo", None), + // Logical expression with constant nullishness + ("({}) ?? foo", None), + ("([]) ?? foo", None), + ("(() => {}) ?? foo", None), + ("(function() {}) ?? foo", None), + ("(class {}) ?? foo", None), + ("new Foo() ?? foo", None), + ("1 ?? foo", None), + ("/[a-z]/ ?? foo", None), + ("`${''}` ?? foo", None), + ("(a = true) ?? foo", None), + ("(a += 1) ?? foo", None), + ("(a -= 1) ?? foo", None), + ("(a *= 1) ?? foo", None), + ("(a /= 1) ?? foo", None), + ("(a %= 1) ?? foo", None), + ("(a <<= 1) ?? foo", None), + ("(a >>= 1) ?? foo", None), + ("(a >>>= 1) ?? foo", None), + ("(a |= 1) ?? foo", None), + ("(a ^= 1) ?? foo", None), + ("(a &= 1) ?? foo", None), + ("undefined ?? foo", None), + ("!bar ?? foo", None), + ("void bar ?? foo", None), + ("typeof bar ?? foo", None), + ("+bar ?? foo", None), + ("-bar ?? foo", None), + ("~bar ?? foo", None), + ("++bar ?? foo", None), + ("bar++ ?? foo", None), + ("--bar ?? foo", None), + ("bar-- ?? foo", None), + ("(x == y) ?? foo", None), + ("(x + y) ?? foo", None), + ("(x / y) ?? foo", None), + ("(x instanceof String) ?? foo", None), + ("(x in y) ?? foo", None), + ("Boolean(x) ?? foo", None), + ("String(x) ?? foo", None), + ("Number(x) ?? foo", None), + // Binary expression with comparison to null + ("({}) != null", None), + ("({}) == null", None), + ("null == ({})", None), + ("({}) == undefined", None), + ("undefined == ({})", None), + // Binary expression with loose comparison to boolean + ("({}) != true", None), + ("({}) == true", None), + ("([]) == true", None), + ("([a, b]) == true", None), + ("(() => {}) == true", None), + ("(function() {}) == true", None), + ("void foo == true", None), + ("typeof foo == true", None), + ("![] == true", None), + ("true == class {}", None), + ("true == 1", None), + ("undefined == true", None), + ("true == undefined", None), + ("`hello` == true", None), + ("/[a-z]/ == true", None), + ("({}) == Boolean({})", None), + ("({}) == Boolean()", None), + ("({}) == Boolean(() => {}, foo)", None), + // Binary expression with strict comparison to boolean + ("({}) !== true", None), + ("({}) == !({})", None), + ("({}) === true", None), + ("([]) === true", None), + ("(function() {}) === true", None), + ("(() => {}) === true", None), + ("!{} === true", None), + ("typeof n === true", None), + ("void n === true", None), + ("+n === true", None), + ("-n === true", None), + ("~n === true", None), + ("true === true", None), + ("1 === true", None), + ("'hello' === true", None), + ("/[a-z]/ === true", None), + ("undefined === true", None), + ("(a = {}) === true", None), + ("(a += 1) === true", None), + ("(a -= 1) === true", None), + ("(a *= 1) === true", None), + ("(a %= 1) === true", None), + ("(a ** b) === true", None), + ("(a << b) === true", None), + ("(a >> b) === true", None), + ("(a >>> b) === true", None), + ("--a === true", None), + ("a-- === true", None), + ("++a === true", None), + ("a++ === true", None), + ("(a + b) === true", None), + ("(a - b) === true", None), + ("(a * b) === true", None), + ("(a / b) === true", None), + ("(a % b) === true", None), + ("(a | b) === true", None), + ("(a ^ b) === true", None), + ("(a & b) === true", None), + ("Boolean(0) === Boolean(1)", None), + ("true === String(x)", None), + ("true === Number(x)", None), + ("Boolean(0) == !({})", None), + // Binary expression with strict comparison to null + ("({}) !== null", None), + ("({}) === null", None), + ("([]) === null", None), + ("(() => {}) === null", None), + ("(function() {}) === null", None), + ("(class {}) === null", None), + ("new Foo() === null", None), + ("`` === null", None), + ("1 === null", None), + ("'hello' === null", None), + ("/[a-z]/ === null", None), + ("true === null", None), + ("null === null", None), + ("a++ === null", None), + ("++a === null", None), + ("--a === null", None), + ("a-- === null", None), + ("!a === null", None), + ("typeof a === null", None), + ("delete a === null", None), + ("void a === null", None), + ("undefined === null", None), + ("(x = {}) === null", None), + ("(x += y) === null", None), + ("(x -= y) === null", None), + ("(a, b, {}) === null", None), + // Binary expression with strict comparison to undefined + ("({}) !== undefined", None), + ("({}) === undefined", None), + ("([]) === undefined", None), + ("(() => {}) === undefined", None), + ("(function() {}) === undefined", None), + ("(class {}) === undefined", None), + ("new Foo() === undefined", None), + ("`` === undefined", None), + ("1 === undefined", None), + ("'hello' === undefined", None), + ("/[a-z]/ === undefined", None), + ("true === undefined", None), + ("null === undefined", None), + ("a++ === undefined", None), + ("++a === undefined", None), + ("--a === undefined", None), + ("a-- === undefined", None), + ("!a === undefined", None), + ("typeof a === undefined", None), + ("delete a === undefined", None), + ("void a === undefined", None), + ("undefined === undefined", None), + ("(x = {}) === undefined", None), + ("(x += y) === undefined", None), + ("(x -= y) === undefined", None), + ("(a, b, {}) === undefined", None), + /* + * If both sides are newly constructed objects, we can tell they will + * never be equal, even with == equality. + */ + ("[a] == [a]", None), + ("[a] != [a]", None), + ("({}) == []", None), + // Comparing to always new objects + ("x === {}", None), + ("x !== {}", None), + ("x === []", None), + ("x === (() => {})", None), + ("x === (function() {})", None), + ("x === (class {})", None), + ("x === new Boolean()", None), + ("x === new Promise()", None), + ("x === new WeakSet()", None), + ("x === (foo, {})", None), + ("x === (y = {})", None), + ("x === (y ? {} : [])", None), + ("x === /[a-z]/", None), + // It's not obvious what this does, but it compares the old value of `x` to the new object. + ("x === (x = {})", None), + ("window.abc && false && anything", None), + ("window.abc || true || anything", None), + ("window.abc ?? 'non-nullish' ?? anything", None), + ]; + + Tester::new(NoConstantBinaryExpression::NAME, pass, fail).test_and_snapshot(); +}