diff --git a/crates/oxc_ecmascript/src/constant_evaluation/is_literal_value.rs b/crates/oxc_ecmascript/src/constant_evaluation/is_literal_value.rs index e4deafcd057a0..dd725aac24a00 100644 --- a/crates/oxc_ecmascript/src/constant_evaluation/is_literal_value.rs +++ b/crates/oxc_ecmascript/src/constant_evaluation/is_literal_value.rs @@ -1,5 +1,7 @@ use oxc_ast::ast::*; +use crate::GlobalContext; + /// Returns true if this is a literal value. /// /// We define a literal value as any node that evaluates @@ -15,87 +17,347 @@ use oxc_ast::ast::*; /// However, a function literal with respect to a particular scope is a literal. /// If `include_functions` is true, all function expressions will be treated as literals. pub trait IsLiteralValue<'a, 'b> { - fn is_literal_value(&self, include_functions: bool) -> bool; + fn is_literal_value(&self, include_functions: bool, ctx: &impl GlobalContext<'a>) -> bool; } -pub fn is_immutable_value(expr: &Expression<'_>) -> bool { - match expr { - Expression::BooleanLiteral(_) - | Expression::NullLiteral(_) - | Expression::NumericLiteral(_) - | Expression::RegExpLiteral(_) - | Expression::StringLiteral(_) => true, - Expression::TemplateLiteral(lit) if lit.is_no_substitution_template() => true, - Expression::Identifier(ident) => { - matches!(ident.name.as_str(), "undefined" | "Infinity" | "NaN") +impl<'a> IsLiteralValue<'a, '_> for Expression<'a> { + fn is_literal_value(&self, include_functions: bool, ctx: &impl GlobalContext<'a>) -> bool { + match self { + Self::BooleanLiteral(_) + | Self::NullLiteral(_) + | Self::NumericLiteral(_) + | Self::BigIntLiteral(_) + | Self::RegExpLiteral(_) + | Self::StringLiteral(_) => true, + Self::TemplateLiteral(lit) => lit.is_literal_value(include_functions, ctx), + Self::Identifier(ident) => { + matches!(ident.name.as_str(), "undefined" | "Infinity" | "NaN") + && ctx.is_global_reference(ident) + } + Self::ArrayExpression(expr) => expr.is_literal_value(include_functions, ctx), + Self::ObjectExpression(expr) => expr.is_literal_value(include_functions, ctx), + Self::FunctionExpression(_) | Self::ArrowFunctionExpression(_) => include_functions, + Self::UnaryExpression(e) => e.is_literal_value(include_functions, ctx), + Self::BinaryExpression(e) => e.is_literal_value(include_functions, ctx), + Self::LogicalExpression(e) => { + e.left.is_literal_value(include_functions, ctx) + && e.right.is_literal_value(include_functions, ctx) + } + Self::ConditionalExpression(e) => { + e.test.is_literal_value(include_functions, ctx) + && e.consequent.is_literal_value(include_functions, ctx) + && e.alternate.is_literal_value(include_functions, ctx) + } + Self::ParenthesizedExpression(e) => { + e.expression.is_literal_value(include_functions, ctx) + } + Self::SequenceExpression(e) => { + e.expressions.iter().all(|expr| expr.is_literal_value(include_functions, ctx)) + } + _ => false, } - Expression::UnaryExpression(e) - if matches!( - e.operator, - UnaryOperator::Void | UnaryOperator::LogicalNot | UnaryOperator::UnaryNegation - ) => - { - is_immutable_value(&e.argument) + } +} + +impl<'a> IsLiteralValue<'a, '_> for TemplateLiteral<'a> { + fn is_literal_value(&self, _include_functions: bool, _ctx: &impl GlobalContext<'a>) -> bool { + self.is_no_substitution_template() + } +} + +impl<'a> IsLiteralValue<'a, '_> for ArrayExpression<'a> { + fn is_literal_value(&self, include_functions: bool, ctx: &impl GlobalContext<'a>) -> bool { + self.elements.iter().all(|element| element.is_literal_value(include_functions, ctx)) + } +} + +impl<'a> IsLiteralValue<'a, '_> for ObjectExpression<'a> { + fn is_literal_value(&self, include_functions: bool, ctx: &impl GlobalContext<'a>) -> bool { + self.properties.iter().all(|property| property.is_literal_value(include_functions, ctx)) + } +} + +impl<'a> IsLiteralValue<'a, '_> for UnaryExpression<'a> { + fn is_literal_value(&self, include_functions: bool, ctx: &impl GlobalContext<'a>) -> bool { + match self.operator { + UnaryOperator::Void | UnaryOperator::LogicalNot | UnaryOperator::Typeof => { + self.argument.is_literal_value(include_functions, ctx) + } + UnaryOperator::UnaryPlus => { + can_convert_to_number_transparently(&self.argument, include_functions, ctx) + } + UnaryOperator::UnaryNegation | UnaryOperator::BitwiseNot => { + can_convert_to_number_transparently(&self.argument, include_functions, ctx) + || matches!(self.argument, Expression::BigIntLiteral(_)) + } + UnaryOperator::Delete => false, } - // Operations on bigint can result type error. - // Expression::BigIntLiteral(_) => false, - _ => false, } } -impl<'a> IsLiteralValue<'a, '_> for Expression<'a> { - fn is_literal_value(&self, include_functions: bool) -> bool { - match self { - Self::FunctionExpression(_) | Self::ArrowFunctionExpression(_) => include_functions, - Self::ArrayExpression(expr) => { - expr.elements.iter().all(|element| element.is_literal_value(include_functions)) +impl<'a> IsLiteralValue<'a, '_> for BinaryExpression<'a> { + fn is_literal_value(&self, include_functions: bool, ctx: &impl GlobalContext<'a>) -> bool { + match self.operator { + BinaryOperator::StrictEquality | BinaryOperator::StrictInequality => { + self.left.is_literal_value(include_functions, ctx) + && self.right.is_literal_value(include_functions, ctx) + } + BinaryOperator::Addition => { + if (is_immutable_string(&self.left, include_functions, ctx) + && can_convert_to_string_transparently(&self.right, include_functions, ctx)) + || (is_immutable_string(&self.right, include_functions, ctx) + && can_convert_to_string_transparently(&self.left, include_functions, ctx)) + { + return true; + } + (matches!(&self.left, Expression::NumericLiteral(_)) + && matches!(&self.right, Expression::NumericLiteral(_))) + | (matches!(&self.left, Expression::BigIntLiteral(_)) + && matches!(&self.right, Expression::BigIntLiteral(_))) } - Self::ObjectExpression(expr) => { - expr.properties.iter().all(|property| property.is_literal_value(include_functions)) + BinaryOperator::Subtraction + | BinaryOperator::Multiplication + | BinaryOperator::Division + | BinaryOperator::Remainder + | BinaryOperator::Exponential + | BinaryOperator::ShiftLeft + | BinaryOperator::ShiftRight + | BinaryOperator::ShiftRightZeroFill + | BinaryOperator::BitwiseOR + | BinaryOperator::BitwiseXOR + | BinaryOperator::BitwiseAnd => { + if (matches!(&self.left, Expression::NumericLiteral(_)) + && can_convert_to_number_transparently(&self.right, include_functions, ctx)) + || (matches!(&self.right, Expression::NumericLiteral(_)) + && can_convert_to_number_transparently(&self.left, include_functions, ctx)) + { + return true; + } + let (Expression::BigIntLiteral(_), Expression::BigIntLiteral(right)) = + (&self.left, &self.right) + else { + return false; + }; + // 1n / 0n, 1n % 0n, 1n ** (-1n) throws an error + match self.operator { + BinaryOperator::ShiftRightZeroFill => false, + BinaryOperator::Exponential => !right.is_negative(), + BinaryOperator::Division | BinaryOperator::Remainder => !right.is_zero(), + _ => true, + } } - _ => is_immutable_value(self), + BinaryOperator::LessThan + | BinaryOperator::LessEqualThan + | BinaryOperator::GreaterThan + | BinaryOperator::GreaterEqualThan + | BinaryOperator::Equality + | BinaryOperator::Inequality + | BinaryOperator::In + | BinaryOperator::Instanceof => false, } } } impl<'a> IsLiteralValue<'a, '_> for ArrayExpressionElement<'a> { - fn is_literal_value(&self, include_functions: bool) -> bool { + fn is_literal_value(&self, include_functions: bool, ctx: &impl GlobalContext<'a>) -> bool { match self { - Self::SpreadElement(element) => element.is_literal_value(include_functions), - match_expression!(Self) => self.to_expression().is_literal_value(include_functions), + // spread element triggers `Symbol.iterator` call + Self::SpreadElement(_) => false, Self::Elision(_) => true, + match_expression!(Self) => { + self.to_expression().is_literal_value(include_functions, ctx) + } } } } -impl<'a> IsLiteralValue<'a, '_> for SpreadElement<'a> { - fn is_literal_value(&self, include_functions: bool) -> bool { - self.argument.is_literal_value(include_functions) - } -} - impl<'a> IsLiteralValue<'a, '_> for ObjectPropertyKind<'a> { - fn is_literal_value(&self, include_functions: bool) -> bool { + fn is_literal_value(&self, include_functions: bool, ctx: &impl GlobalContext<'a>) -> bool { match self { - Self::ObjectProperty(method) => method.is_literal_value(include_functions), - Self::SpreadProperty(property) => property.is_literal_value(include_functions), + Self::ObjectProperty(property) => property.is_literal_value(include_functions, ctx), + Self::SpreadProperty(property) => match &property.argument { + Expression::ArrayExpression(expr) => expr.is_literal_value(include_functions, ctx), + Expression::StringLiteral(_) => true, + Expression::TemplateLiteral(lit) => lit.is_literal_value(include_functions, ctx), + Expression::ObjectExpression(expr) => expr.is_literal_value(include_functions, ctx), + _ => false, + }, } } } impl<'a> IsLiteralValue<'a, '_> for ObjectProperty<'a> { - fn is_literal_value(&self, include_functions: bool) -> bool { - self.key.is_literal_value(include_functions) - && self.value.is_literal_value(include_functions) + fn is_literal_value(&self, include_functions: bool, ctx: &impl GlobalContext<'a>) -> bool { + self.key.is_literal_value(include_functions, ctx) + && self.value.is_literal_value(include_functions, ctx) } } impl<'a> IsLiteralValue<'a, '_> for PropertyKey<'a> { - fn is_literal_value(&self, include_functions: bool) -> bool { + fn is_literal_value(&self, include_functions: bool, ctx: &impl GlobalContext<'a>) -> bool { match self { Self::StaticIdentifier(_) => true, Self::PrivateIdentifier(_) => false, - match_expression!(Self) => self.to_expression().is_literal_value(include_functions), + match_expression!(Self) => { + can_convert_to_string_transparently(self.to_expression(), include_functions, ctx) + } + } + } +} + +fn can_convert_to_number_transparently<'a>( + expr: &Expression<'a>, + include_functions: bool, + ctx: &impl GlobalContext<'a>, +) -> bool { + match expr { + Expression::NumericLiteral(_) + | Expression::NullLiteral(_) + | Expression::BooleanLiteral(_) + | Expression::StringLiteral(_) => true, + Expression::TemplateLiteral(lit) => lit.is_literal_value(include_functions, ctx), + Expression::Identifier(ident) => { + matches!(ident.name.as_str(), "undefined" | "Infinity" | "NaN") + && ctx.is_global_reference(ident) + } + Expression::UnaryExpression(e) => match e.operator { + UnaryOperator::Void | UnaryOperator::LogicalNot | UnaryOperator::Typeof => { + e.argument.is_literal_value(include_functions, ctx) + } + UnaryOperator::UnaryPlus | UnaryOperator::UnaryNegation | UnaryOperator::BitwiseNot => { + can_convert_to_number_transparently(&e.argument, include_functions, ctx) + } + UnaryOperator::Delete => false, + }, + Expression::BinaryExpression(e) => match e.operator { + BinaryOperator::StrictEquality | BinaryOperator::StrictInequality => { + e.left.is_literal_value(include_functions, ctx) + && e.right.is_literal_value(include_functions, ctx) + } + BinaryOperator::Addition => { + if (is_immutable_string(&e.left, include_functions, ctx) + && can_convert_to_string_transparently(&e.right, include_functions, ctx)) + || (is_immutable_string(&e.right, include_functions, ctx) + && can_convert_to_string_transparently(&e.left, include_functions, ctx)) + { + return true; + } + (matches!(&e.left, Expression::NumericLiteral(_)) + && matches!(&e.right, Expression::NumericLiteral(_))) + | (matches!(&e.left, Expression::BigIntLiteral(_)) + && matches!(&e.right, Expression::BigIntLiteral(_))) + } + BinaryOperator::Subtraction + | BinaryOperator::Multiplication + | BinaryOperator::Division + | BinaryOperator::Remainder + | BinaryOperator::Exponential + | BinaryOperator::ShiftLeft + | BinaryOperator::ShiftRight + | BinaryOperator::ShiftRightZeroFill + | BinaryOperator::BitwiseOR + | BinaryOperator::BitwiseXOR + | BinaryOperator::BitwiseAnd => { + if (matches!(&e.left, Expression::NumericLiteral(_)) + && can_convert_to_number_transparently(&e.right, include_functions, ctx)) + || (matches!(&e.right, Expression::NumericLiteral(_)) + && can_convert_to_number_transparently(&e.left, include_functions, ctx)) + { + return true; + } + false + } + BinaryOperator::LessThan + | BinaryOperator::LessEqualThan + | BinaryOperator::GreaterThan + | BinaryOperator::GreaterEqualThan + | BinaryOperator::Equality + | BinaryOperator::Inequality + | BinaryOperator::In + | BinaryOperator::Instanceof => false, + }, + Expression::LogicalExpression(e) => { + can_convert_to_number_transparently(&e.left, include_functions, ctx) + && can_convert_to_number_transparently(&e.right, include_functions, ctx) + } + Expression::ConditionalExpression(e) => { + e.test.is_literal_value(include_functions, ctx) + && can_convert_to_number_transparently(&e.consequent, include_functions, ctx) + && can_convert_to_number_transparently(&e.alternate, include_functions, ctx) + } + Expression::ParenthesizedExpression(e) => { + can_convert_to_number_transparently(&e.expression, include_functions, ctx) + } + Expression::SequenceExpression(e) => { + can_convert_to_number_transparently( + e.expressions.last().expect("should have at least one element"), + include_functions, + ctx, + ) && e + .expressions + .iter() + .rev() + .skip(1) + .all(|expr| expr.is_literal_value(include_functions, ctx)) } + _ => false, + } +} + +fn can_convert_to_string_transparently<'a>( + expr: &Expression<'a>, + include_functions: bool, + ctx: &impl GlobalContext<'a>, +) -> bool { + match expr { + Expression::NumericLiteral(_) + | Expression::StringLiteral(_) + | Expression::NullLiteral(_) + | Expression::BooleanLiteral(_) + | Expression::BigIntLiteral(_) => true, + Expression::TemplateLiteral(lit) => lit.is_literal_value(include_functions, ctx), + Expression::Identifier(ident) => { + matches!(ident.name.as_str(), "undefined" | "Infinity" | "NaN") + && ctx.is_global_reference(ident) + } + Expression::UnaryExpression(e) => e.is_literal_value(include_functions, ctx), + Expression::BinaryExpression(e) => e.is_literal_value(include_functions, ctx), + Expression::LogicalExpression(e) => { + e.left.is_literal_value(include_functions, ctx) + && e.right.is_literal_value(include_functions, ctx) + } + Expression::ConditionalExpression(e) => { + e.test.is_literal_value(include_functions, ctx) + && can_convert_to_string_transparently(&e.consequent, include_functions, ctx) + && can_convert_to_string_transparently(&e.alternate, include_functions, ctx) + } + Expression::ParenthesizedExpression(e) => { + can_convert_to_string_transparently(&e.expression, include_functions, ctx) + } + Expression::SequenceExpression(e) => { + can_convert_to_string_transparently( + e.expressions.last().expect("should have at least one element"), + include_functions, + ctx, + ) && e + .expressions + .iter() + .rev() + .skip(1) + .all(|expr| expr.is_literal_value(include_functions, ctx)) + } + _ => false, + } +} + +fn is_immutable_string<'a>( + expr: &Expression<'a>, + include_functions: bool, + ctx: &impl GlobalContext<'a>, +) -> bool { + match expr { + Expression::StringLiteral(_) => true, + Expression::TemplateLiteral(lit) => lit.is_literal_value(include_functions, ctx), + _ => false, } } diff --git a/crates/oxc_minifier/tests/ecmascript/is_literal_value.rs b/crates/oxc_minifier/tests/ecmascript/is_literal_value.rs new file mode 100644 index 0000000000000..72abe6af439c6 --- /dev/null +++ b/crates/oxc_minifier/tests/ecmascript/is_literal_value.rs @@ -0,0 +1,346 @@ +use std::iter; + +use javascript_globals::GLOBALS; + +use rustc_hash::FxHashSet; + +use oxc_allocator::Allocator; +use oxc_ast::ast::{IdentifierReference, Statement}; +use oxc_ecmascript::{GlobalContext, constant_evaluation::IsLiteralValue}; +use oxc_parser::Parser; +use oxc_span::SourceType; + +struct Ctx { + global_variable_names: FxHashSet<&'static str>, +} + +impl Default for Ctx { + fn default() -> Self { + Self { + global_variable_names: GLOBALS["builtin"] + .keys() + .copied() + .chain(iter::once("arguments")) + .collect::>(), + } + } +} + +impl<'a> GlobalContext<'a> for Ctx { + fn is_global_reference(&self, ident: &IdentifierReference<'a>) -> bool { + self.global_variable_names.contains(ident.name.as_str()) + } +} + +#[track_caller] +fn test(source_text: &str, expected: bool) { + let ctx = Ctx::default(); + test_with_ctx(source_text, &ctx, expected); +} + +#[track_caller] +fn test_with_global_variables( + source_text: &str, + global_variable_names: &[&'static str], + expected: bool, +) { + let ctx = Ctx { global_variable_names: global_variable_names.iter().copied().collect() }; + test_with_ctx(source_text, &ctx, expected); +} + +#[track_caller] +fn test_with_ctx(source_text: &str, ctx: &Ctx, expected: bool) { + test_with_ctx_and_functions_option(source_text, false, ctx, expected); +} + +#[track_caller] +fn test_with_ctx_and_functions_option( + source_text: &str, + include_functions: bool, + ctx: &Ctx, + expected: bool, +) { + let allocator = Allocator::default(); + let ret = Parser::new(&allocator, source_text, SourceType::mjs()).parse(); + assert!(!ret.panicked, "{source_text}"); + assert!(ret.errors.is_empty(), "{source_text}"); + + let Some(Statement::ExpressionStatement(stmt)) = &ret.program.body.first() else { + panic!("should have a expression statement body: {source_text}"); + }; + assert_eq!(stmt.expression.is_literal_value(include_functions, ctx), expected, "{source_text}"); +} + +#[test] +fn test_identifier_reference() { + test("NaN", true); + test("undefined", true); + test("Infinity", true); + test_with_global_variables("NaN", &[], false); +} + +#[test] +fn test_simple_expressions() { + test("true", true); + test("null", true); + test("0", true); + test("/foo/", true); + test("('foo')", true); + test("0n", true); + test("this", false); + test("import.meta", false); +} + +#[test] +fn test_template_literal() { + test("``", true); + test("`a`", true); + test("`${1}`", false); // can improve + test("`${[]}`", false); // Array::[Symbol.toPrimitive] might be overridden + test("`${a}`", false); +} + +#[test] +fn test_unary_expressions() { + test("void 'foo'", true); + test("void foo", false); + test("!'foo'", true); + test("!foo", false); + test("typeof 'foo'", true); + test("typeof foo", false); + + test("+0", true); + test("+0n", false); + test("+undefined", true); // NaN + test("+Infinity", true); + test("+NaN", true); + test("+null", true); // 0 + test("+false", true); // 0 + test("+true", true); // 1 + test("+'foo'", true); // NaN + test("+`foo`", true); // NaN + test("+/foo/", false); // RegExp::[Symbol.toPrimitive] might be overridden + test("+[]", false); // Array::[Symbol.toPrimitive] might be overridden + + test("+(void 1)", true); + test("+(void a)", false); + test("+(!1)", true); + test("+(!1n)", true); + test("+(!a)", false); + test("+(typeof 1)", true); + test("+(typeof a)", false); + test("+(+1)", true); + test("+(+1n)", false); + test("+(+a)", false); + test("+(-1)", true); + test("+(-1n)", false); + test("+(-a)", false); + test("+(~1)", true); + test("+(~1n)", false); + test("+(~a)", false); + test("+(delete a)", false); + test("+('' + true)", true); + test("+('' + 2)", true); + test("+(1 + 2)", true); + test("+(1n + 2n)", true); + test("+('' + a)", false); + test("+(1 - 2)", true); + test("+(1 - a)", false); + test("+(1 < 2)", false); + test("+(1 || 2)", true); + test("+(a || 2)", false); + test("+(1 || b)", false); + test("+(1n || 2)", false); + test("+(1 ? 2 : 3)", true); + test("+(1n ? 2 : 3)", true); + test("+(1 ? 2n : 3)", false); + test("+(1 ? 2 : 3n)", false); + test("+(1)", true); + test("+(a)", false); + test("+(1, 2)", true); + test("+(1n, 2)", true); + test("+(1, 2n)", false); + + test("-0", true); + test("-0n", true); + test("-undefined", true); // NaN + test("-Infinity", true); + test("-NaN", true); + test("-null", true); // -0 + test("-false", true); // -0 + test("-true", true); // -1 + test("-'foo'", true); // NaN + test("-`foo`", true); // NaN + test("-/foo/", false); // RegExp::[Symbol.toPrimitive] might be overridden + test("-[]", false); // Array::[Symbol.toPrimitive] might be overridden + + test("~0", true); + test("~'foo'", true); + test("~foo", false); + + test("delete 0", false); + test("delete foo", false); +} + +#[test] +fn test_logical_expressions() { + test("a || b", false); + test("a || 0", false); + test("1 || b", false); + test("1 || 0", true); + test("a && b", false); + test("a && 0", false); + test("1 && b", false); + test("1 && 0", true); + test("a ?? b", false); + test("a ?? 0", false); + test("1 ?? b", false); + test("1 ?? 0", true); +} + +#[test] +fn test_other_expressions() { + test("(1)", true); + test("(foo)", false); + + test("1 ? 2 : 3", true); + test("1 ? 2 : c", false); + test("a ? 2 : 3", false); + test("1 ? b : 3", false); + + test("1, 2", true); + test("a, 2", false); + test("1, b", false); +} + +#[test] +fn test_array_expression() { + test("[]", true); + test("[1]", true); + test("[foo]", false); + test("[,]", true); + test("[...a]", false); + test("[...[]]", false); // Array::[Symbol.iterator] might be overridden + test("[...'foo']", false); // String::[Symbol.iterator] might be overridden +} + +#[test] +fn test_object_expression() { + // wrapped with parentheses to avoid treated as a block statement + test("({})", true); + test("({a: 1})", true); + test("({a: foo})", false); + test("({1: 1})", true); + test("({[1]: 1})", true); + test("({[1n]: 1})", true); + test("({['1']: 1})", true); + test("({[a]: 1})", false); + test("({[[]]: 1})", false); // Array::[Symbol.toPrimitive] might be overridden + + test("({[void 0]: 1})", true); + test("({[void a]: 1})", false); + test("({[!0]: 1})", true); + test("({[!a]: 1})", false); + test("({[typeof 0]: 1})", true); + test("({[typeof a]: 1})", false); + test("({[+0]: 1})", true); + test("({[-0]: 1})", true); + test("({[~0]: 1})", true); + test("({[delete a]: 1})", false); + test("({['' + 2]: 1})", true); + test("({[1 + 2]: 1})", true); + test("({[1n + 2n]: 1})", true); + test("({['' + a]: 1})", false); + test("({[1 - 2]: 1})", true); + test("({[1 - a]: 1})", false); + test("({[1 < 2]: 1})", false); + test("({['foo' || 'bar']: 1})", true); + test("({[a || 'bar']: 1})", false); + test("({['foo' || a]: 1})", false); + test("({[1 ? 'foo' : 'bar']: 1})", true); + test("({[1 ? a : 'bar']: 1})", false); + test("({[1 ? 'foo' : b]: 1})", false); + test("({[('foo')]: 1})", true); + test("({[(a)]: 1})", false); + test("({[(1, 'foo')]: 1})", true); + test("({[(1, b)]: 1})", false); + + test("({...a})", false); + test("({...[]})", true); + test("({...[0]})", true); + test("({...[a]})", false); + test("({...'foo'})", true); + test("({...`foo`})", true); + test("({...`foo${a}`})", false); + test("({...{}})", true); + test("({...{a:1}})", true); + test("({...{a:a}})", false); +} + +#[test] +fn test_binary_expressions() { + test("1 === 2", true); + test("a === 2", false); + test("1 === b", false); + test("a === b", false); + test("1 !== 2", true); + test("a !== b", false); + + test("'' + ''", true); + test("'' + ``", true); + test("'' + null", true); + test("'' + 0", true); + test("'' + a", false); + test("null + ''", true); + test("0 + ''", true); + test("a + ''", false); + test("1 + 2", true); + test("1n + 2n", true); + + test("0n - 1n", true); + test("0n - 0", false); + test("0n - a", false); + test("a - 0n", false); + test("0 - 1", true); + test("0 - a", false); + test("a - 0", false); + test("0 - ''", true); // 0 + test("0 - ``", true); // 0 + test("0 - true", true); // -1 + test("0 - []", false); // Array::[Symbol.toPrimitive] might be overridden + + test("0 * 1", true); + test("0 * a", false); + test("0 / 1", true); + test("0 / a", false); + test("0 % 1", true); + test("0 % a", false); + test("0 ** 1", true); + test("0 ** a", false); + test("0 << 1", true); + test("0 << a", false); + test("0 >> 1", true); + test("0 >> a", false); + test("0 >>> 1", true); + test("0 >>> a", false); + test("0 | 1", true); + test("0 | a", false); + test("0 ^ 1", true); + test("0 ^ a", false); + test("0 & 1", true); + test("0 & a", false); + + test("1n ** (-1n)", false); // `**` throws an error when the right operand is negative + test("1n / 0n", false); // `/` throws an error when the right operand is zero + test("1n % 0n", false); // `%` throws an error when the right operand is zero + test("0n >>> 1n", false); // `>>>` throws an error even when both operands are bigint + + test("1 == 2", false); + test("1 != 2", false); + test("1 < 2", false); + test("1 <= 2", false); + test("1 > 2", false); + test("1 >= 2", false); + test("1 in 2", false); + test("1 instanceof 2", false); +} diff --git a/crates/oxc_minifier/tests/ecmascript/mod.rs b/crates/oxc_minifier/tests/ecmascript/mod.rs index 369ccc4ee00cd..411db7fb6e4f9 100644 --- a/crates/oxc_minifier/tests/ecmascript/mod.rs +++ b/crates/oxc_minifier/tests/ecmascript/mod.rs @@ -1,5 +1,6 @@ mod array_join; mod is_int32_or_uint32; +mod is_literal_value; mod may_have_side_effects; mod prop_name; mod to_boolean;