From e3e9999edd7d0967d0f325750c18bc6e2c88e2af Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Tue, 4 Feb 2025 15:49:36 +0000 Subject: [PATCH] feat(ecmascript): complete may_have_side_effects (#8855) Ported tests from closure compiler and reviewed the behavior of may_have_side_effects and added additional tests. --- .../src/constant_evaluation/mod.rs | 4 + .../src/side_effects/may_have_side_effects.rs | 89 +++- crates/oxc_minifier/README.md | 2 + .../minimize_conditional_expression.rs | 2 +- .../src/peephole/statement_fusion.rs | 2 +- .../peephole/substitute_alternate_syntax.rs | 2 +- .../tests/ecmascript/may_have_side_effects.rs | 496 ++++++++++++++++++ crates/oxc_minifier/tests/ecmascript/mod.rs | 1 + 8 files changed, 581 insertions(+), 17 deletions(-) create mode 100644 crates/oxc_minifier/tests/ecmascript/may_have_side_effects.rs diff --git a/crates/oxc_ecmascript/src/constant_evaluation/mod.rs b/crates/oxc_ecmascript/src/constant_evaluation/mod.rs index 67e0516d90505..9c930a7aeae51 100644 --- a/crates/oxc_ecmascript/src/constant_evaluation/mod.rs +++ b/crates/oxc_ecmascript/src/constant_evaluation/mod.rs @@ -327,6 +327,10 @@ pub trait ConstantEvaluation<'a>: MayHaveSideEffects { if matches!(name, "Object" | "Number" | "Boolean" | "String") && self.is_global_reference(right_ident) { + let left_ty = ValueType::from(left); + if left_ty.is_undetermined() { + return None; + } return Some(ConstantValue::Boolean( name == "Object" && ValueType::from(left).is_object(), )); diff --git a/crates/oxc_ecmascript/src/side_effects/may_have_side_effects.rs b/crates/oxc_ecmascript/src/side_effects/may_have_side_effects.rs index 403524d7b1421..fd76a004f95c5 100644 --- a/crates/oxc_ecmascript/src/side_effects/may_have_side_effects.rs +++ b/crates/oxc_ecmascript/src/side_effects/may_have_side_effects.rs @@ -2,16 +2,23 @@ use oxc_ast::ast::*; /// Returns true if subtree changes application state. /// +/// This trait assumes the following: +/// - `.toString()`, `.valueOf()`, and `[Symbol.toPrimitive]()` are side-effect free. +/// - Errors thrown when creating a String or an Array that exceeds the maximum length does not happen. +/// - TDZ errors does not happen. +/// /// Ported from [closure-compiler](https://github.com/google/closure-compiler/blob/f3ce5ed8b630428e311fe9aa2e20d36560d975e2/src/com/google/javascript/jscomp/AstAnalyzer.java#L94) pub trait MayHaveSideEffects { fn is_global_reference(&self, ident: &IdentifierReference<'_>) -> bool; fn expression_may_have_side_effects(&self, e: &Expression<'_>) -> bool { match e { - // Reference read can have a side effect. Expression::Identifier(ident) => match ident.name.as_str() { - "NaN" | "Infinity" | "undefined" => !self.is_global_reference(ident), - _ => true, + "NaN" | "Infinity" | "undefined" => false, + // Reading global variables may have a side effect. + // NOTE: It should also return true when the reference might refer to a reference value created by a with statement + // NOTE: we ignore TDZ errors + _ => self.is_global_reference(ident), }, Expression::NumericLiteral(_) | Expression::BooleanLiteral(_) @@ -22,11 +29,13 @@ pub trait MayHaveSideEffects { | Expression::MetaProperty(_) | Expression::ThisExpression(_) | Expression::ArrowFunctionExpression(_) - | Expression::FunctionExpression(_) => false, + | Expression::FunctionExpression(_) + | Expression::Super(_) => false, Expression::TemplateLiteral(template) => { template.expressions.iter().any(|e| self.expression_may_have_side_effects(e)) } Expression::UnaryExpression(e) => self.unary_expression_may_have_side_effects(e), + Expression::LogicalExpression(e) => self.logical_expression_may_have_side_effects(e), Expression::ParenthesizedExpression(e) => { self.expression_may_have_side_effects(&e.expression) } @@ -43,10 +52,9 @@ pub trait MayHaveSideEffects { .properties .iter() .any(|property| self.object_property_kind_may_have_side_effects(property)), - Expression::ArrayExpression(e) => e - .elements - .iter() - .any(|element| self.array_expression_element_may_have_side_effects(element)), + Expression::ArrayExpression(e) => self.array_expression_may_have_side_effects(e), + Expression::ClassExpression(e) => self.class_may_have_side_effects(e), + // NOTE: private in can throw `TypeError` _ => true, } } @@ -56,6 +64,9 @@ pub trait MayHaveSideEffects { fn is_simple_unary_operator(operator: UnaryOperator) -> bool { operator != UnaryOperator::Delete } + if e.operator == UnaryOperator::Typeof && matches!(&e.argument, Expression::Identifier(_)) { + return false; + } if is_simple_unary_operator(e.operator) { return self.expression_may_have_side_effects(&e.argument); } @@ -71,14 +82,32 @@ pub trait MayHaveSideEffects { || self.expression_may_have_side_effects(&e.right) } + fn logical_expression_may_have_side_effects(&self, e: &LogicalExpression<'_>) -> bool { + self.expression_may_have_side_effects(&e.left) + || self.expression_may_have_side_effects(&e.right) + } + + fn array_expression_may_have_side_effects(&self, e: &ArrayExpression<'_>) -> bool { + e.elements + .iter() + .any(|element| self.array_expression_element_may_have_side_effects(element)) + } + fn array_expression_element_may_have_side_effects( &self, e: &ArrayExpressionElement<'_>, ) -> bool { match e { - ArrayExpressionElement::SpreadElement(e) => { - self.expression_may_have_side_effects(&e.argument) - } + ArrayExpressionElement::SpreadElement(e) => match &e.argument { + Expression::ArrayExpression(arr) => { + self.array_expression_may_have_side_effects(arr) + } + Expression::StringLiteral(_) => false, + Expression::TemplateLiteral(t) => { + t.expressions.iter().any(|e| self.expression_may_have_side_effects(e)) + } + _ => true, + }, match_expression!(ArrayExpressionElement) => { self.expression_may_have_side_effects(e.to_expression()) } @@ -89,9 +118,16 @@ pub trait MayHaveSideEffects { fn object_property_kind_may_have_side_effects(&self, e: &ObjectPropertyKind<'_>) -> bool { match e { ObjectPropertyKind::ObjectProperty(o) => self.object_property_may_have_side_effects(o), - ObjectPropertyKind::SpreadProperty(e) => { - self.expression_may_have_side_effects(&e.argument) - } + ObjectPropertyKind::SpreadProperty(e) => match &e.argument { + Expression::ArrayExpression(arr) => { + self.array_expression_may_have_side_effects(arr) + } + Expression::StringLiteral(_) => false, + Expression::TemplateLiteral(t) => { + t.expressions.iter().any(|e| self.expression_may_have_side_effects(e)) + } + _ => true, + }, } } @@ -108,4 +144,29 @@ pub trait MayHaveSideEffects { } } } + + fn class_may_have_side_effects(&self, class: &Class<'_>) -> bool { + class.body.body.iter().any(|element| self.class_element_may_have_side_effects(element)) + } + + fn class_element_may_have_side_effects(&self, e: &ClassElement<'_>) -> bool { + match e { + // TODO: check side effects inside the block + ClassElement::StaticBlock(block) => !block.body.is_empty(), + ClassElement::MethodDefinition(e) => { + e.r#static && self.property_key_may_have_side_effects(&e.key) + } + ClassElement::PropertyDefinition(e) => { + e.r#static + && (self.property_key_may_have_side_effects(&e.key) + || e.value + .as_ref() + .is_some_and(|v| self.expression_may_have_side_effects(v))) + } + ClassElement::AccessorProperty(e) => { + e.r#static && self.property_key_may_have_side_effects(&e.key) + } + ClassElement::TSIndexSignature(_) => false, + } + } } diff --git a/crates/oxc_minifier/README.md b/crates/oxc_minifier/README.md index 3ab1181bc393e..e263d12c759cc 100644 --- a/crates/oxc_minifier/README.md +++ b/crates/oxc_minifier/README.md @@ -29,6 +29,8 @@ The compressor is responsible for rewriting statements and expressions for minim - Examples that breaks this assumption: `(() => { console.log(v); let v; })()` - `with` statement is not used - Examples that breaks this assumption: `with (Math) { console.log(PI); }` +- `.toString()`, `.valueOf()`, `[Symbol.toPrimitive]()` are side-effect free + - Examples that breaks this assumption: `{ toString() { console.log('sideeffect') } }` - Errors thrown when creating a String or an Array that exceeds the maximum length can disappear or moved - Examples that breaks this assumption: `try { new Array(Number(2n**53n)) } catch { console.log('log') }` diff --git a/crates/oxc_minifier/src/peephole/minimize_conditional_expression.rs b/crates/oxc_minifier/src/peephole/minimize_conditional_expression.rs index 861710f269b35..fa08ba031ec42 100644 --- a/crates/oxc_minifier/src/peephole/minimize_conditional_expression.rs +++ b/crates/oxc_minifier/src/peephole/minimize_conditional_expression.rs @@ -609,7 +609,7 @@ mod test { test("var x; (x && false) && y()", "var x; x && !1"); test("(x && true) && y()", "x && y()"); test("(x && false) && y()", "x && !1"); - test("var x; (x || true) && y()", "var x; x || !0, y()"); + test("var x; (x || true) && y()", "var x; y()"); test("var x; (x || false) && y()", "var x; x && y()"); test("(x || true) && y()", "x || !0, y()"); diff --git a/crates/oxc_minifier/src/peephole/statement_fusion.rs b/crates/oxc_minifier/src/peephole/statement_fusion.rs index 2b3700922a546..ea6a3ad752fdb 100644 --- a/crates/oxc_minifier/src/peephole/statement_fusion.rs +++ b/crates/oxc_minifier/src/peephole/statement_fusion.rs @@ -113,7 +113,7 @@ mod test { // Never fuse a statement into a block that contains let/const/class declarations, or you risk // colliding variable names. (unless the AST is normalized). test("a; {b;}", "a,b"); - test("a; {b; var a = 1;}", "a, b; var a = 1;"); + test("a; {b; var a = 1;}", "b; var a = 1;"); test_same("a; { b; let a = 1; }"); test("a; { b; const a = 1; }", "a; { b; let a = 1; }"); test_same("a; { b; class a {} }"); diff --git a/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs b/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs index b79646f6148dc..1502628099b7d 100644 --- a/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs +++ b/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs @@ -1385,7 +1385,7 @@ mod test { #[test] fn test_fold_arrow_function_return() { test("const foo = () => { return 'baz' }", "const foo = () => 'baz'"); - test("const foo = () => { foo; return 'baz' }", "const foo = () => (foo, 'baz')"); + test("const foo = () => { foo.foo; return 'baz' }", "const foo = () => (foo.foo, 'baz')"); } #[test] diff --git a/crates/oxc_minifier/tests/ecmascript/may_have_side_effects.rs b/crates/oxc_minifier/tests/ecmascript/may_have_side_effects.rs new file mode 100644 index 0000000000000..d668cbdd97e71 --- /dev/null +++ b/crates/oxc_minifier/tests/ecmascript/may_have_side_effects.rs @@ -0,0 +1,496 @@ +use oxc_allocator::Allocator; +use oxc_ast::ast::{IdentifierReference, Statement}; +use oxc_ecmascript::side_effects::MayHaveSideEffects; +use oxc_parser::Parser; +use oxc_span::SourceType; + +struct SideEffectChecker { + global_variable_names: Vec, +} +impl MayHaveSideEffects for SideEffectChecker { + fn is_global_reference(&self, ident: &IdentifierReference<'_>) -> bool { + self.global_variable_names.iter().any(|name| name == ident.name.as_str()) + } +} + +fn test(source_text: &str, expected: bool) { + test_with_global_variables(source_text, vec![], expected); +} + +fn test_with_global_variables( + source_text: &str, + global_variable_names: Vec, + 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 side_effect_checker = SideEffectChecker { global_variable_names }; + + let Some(Statement::ExpressionStatement(stmt)) = &ret.program.body.first() else { + panic!("should have a expression statement body: {source_text}"); + }; + assert_eq!( + side_effect_checker.expression_may_have_side_effects(&stmt.expression), + expected, + "{source_text}" + ); +} + +/// +#[test] +fn closure_compiler_tests() { + test("[1]", false); + test("[1, 2]", false); + test("i++", true); + test("[b, [a, i++]]", true); + test("i=3", true); + test("[0, i=3]", true); + test("b()", true); + test("[1, b()]", true); + test("b.b=4", true); + test("b.b--", true); + test("i--", true); + test("a[0][i=4]", true); + test("a += 3", true); + test("a, b, z += 4", true); + test("a ? c : d++", true); + test("a ?? b++", true); + test("a + c++", true); + test("a + c - d()", true); + test("a + c - d()", true); + // test("function foo() {}", true); + // test("class Foo {}", true); + // test("while(true);", true); + // test("if(true){a()}", true); + // test("if(true){a}", false); + test("(function() { })", false); + test("(function() { i++ })", false); + test("[function a(){}]", false); + test("(class { })", false); + test("(class { method() { i++ } })", false); + test("(class { [computedName()]() {} })", false); // computedName is called when constructed + test("(class { [computedName]() {} })", false); + test("(class Foo extends Bar { })", false); + test("(class extends foo() { })", false); // foo() is called when constructed + test("a", false); + test("a.b", true); + test("a.b.c", true); + test("[b, c, [d, [e]]]", false); + test("({a: x, b: y, c: z})", false); + test("({a, b, c})", false); + test("/abc/gi", false); + test("('a')", false); // wrapped with parentheses to avoid treated as a directive + test("0", false); + test("a + c", false); + test("'c' + a[0]", true); + test("a[0][1]", true); + test("'a' + c", false); + test("'a' + a.name", true); + test("1, 2, 3", false); + test("a, b, 3", false); + test("(function(a, b) { })", false); + test("a ? c : d", false); + test("a ?? b", false); + // test("'1' + navigator.userAgent", false); + test("`template`", false); + test("`template${name}`", false); + test("`${name}template`", false); + test("`${naming()}template`", true); + test("templateFunction`template`", true); + test("st = `${name}template`", true); + test("tempFunc = templateFunction`template`", true); + // test("new RegExp('foobar', 'i')", false); + test("new RegExp(SomethingWacky(), 'i')", true); + // test("new Array()", false); + // test("new Array", false); + // test("new Array(4)", false); + // test("new Array('a', 'b', 'c')", false); + test("new SomeClassINeverHeardOf()", true); + test("new SomeClassINeverHeardOf()", true); + // test("({}).foo = 4", false); + // test("([]).foo = 4", false); + // test("(function() {}).foo = 4", false); + test("this.foo = 4", true); + test("a.foo = 4", true); + test("(function() { return n; })().foo = 4", true); + test("([]).foo = bar()", true); + test("undefined", false); + test("void 0", false); + test("void foo()", true); + test("-Infinity", false); + test("Infinity", false); + test("NaN", false); + // test("({}||[]).foo = 2;", false); + // test("(true ? {} : []).foo = 2;", false); + // test("({},[]).foo = 2;", false); + test("delete a.b", true); + // test("Math.random();", false); + test("Math.random(seed);", true); + // test("[1, 1].foo;", false); + // test("export var x = 0;", true); + // test("export let x = 0;", true); + // test("export const x = 0;", true); + // test("export class X {};", true); + // test("export function x() {};", true); + // test("export {x};", true); + + // ARRAYLIT-ITER_SPREAD + test("[...[]]", false); + test("[...[1]]", false); + test("[...[i++]]", true); + test("[...'string']", false); + test("[...`templatelit`]", false); + test("[...`templatelit ${safe}`]", false); + test("[...`templatelit ${unsafe()}`]", true); + test("[...f()]", true); + test("[...5]", true); + test("[...null]", true); + test("[...true]", true); + + // CALL-ITER_SPREAD + // test("Math.sin(...[])", false); + // test("Math.sin(...[1])", false); + test("Math.sin(...[i++])", true); + // test("Math.sin(...'string')", false); + // test("Math.sin(...`templatelit`)", false); + // test("Math.sin(...`templatelit ${safe}`)", false); + test("Math.sin(...`templatelit ${unsafe()}`)", true); + test("Math.sin(...f())", true); + test("Math.sin(...5)", true); + test("Math.sin(...null)", true); + test("Math.sin(...true)", true); + + // NEW-ITER_SPREAD + // test("new Object(...[])", false); + // test("new Object(...[1])", false); + test("new Object(...[i++])", true); + // test("new Object(...'string')", false); + // test("new Object(...`templatelit`)", false); + // test("new Object(...`templatelit ${safe}`)", false); + test("new Object(...`templatelit ${unsafe()}`)", true); + test("new Object(...f())", true); + test("new Object(...5)", true); + test("new Object(...null)", true); + test("new Object(...true)", true); + + // OBJECT_SPREAD + // These could all invoke getters. + test("({...x})", true); + test("({...{}})", true); + test("({...{a:1}})", true); + test("({...{a:i++}})", true); + test("({...{a:f()}})", true); + test("({...f()})", true); + + // OBJECT_REST + // This could invoke getters. + test("({...x} = something)", true); + // the presence of `a` affects what goes into `x` + test("({a, ...x} = something)", true); + + // ITER_REST + // We currently assume all iterable-rests are side-effectful. + test("([...x] = 'safe')", true); + test("(function(...x) { })", false); + + // Context switch + // test("async function f() { await 0; }", true); + // test("(async()=>{ for await (let x of []) {} })", true); + // test("function f() { throw 'something'; }", true); + // test("function* f() { yield 'something'; }", true); + // test("function* f() { yield* 'something'; }", true); + + // Enhanced for loop + // These edge cases are actually side-effect free. We include them to confirm we just give + // up on enhanced for loops. + // test("for (const x in []) { }", true); + // test("for (const x of []) { }", true); + + // COMPUTED_PROP - OBJECTLIT + test("({[a]: x})", false); + test("({[a()]: x})", true); + test("({[a]: x()})", true); + + // computed property getters and setters are modeled as COMPUTED_PROP with an + // annotation to indicate getter or setter. + test("({ get [a]() {} })", false); + test("({ get [a()]() {} })", true); + test("({ set [a](x) {} })", false); + test("({ set [a()](x) {} })", true); + + // COMPUTED_PROP - CLASS + test("(class C { [a]() {} })", false); + test("(class C { [a()]() {} })", false); // a is called when constructed + + // computed property getters and setters are modeled as COMPUTED_PROP with an + // annotation to indicate getter or setter. + test("(class C { get [a]() {} })", false); + test("(class C { get [a()]() {} })", false); // a is called when constructed + test("(class C { set [a](x) {} })", false); + test("(class C { set [a()](x) {} })", false); // a is called when constructed + + // GETTER_DEF + test("({ get a() {} })", false); + test("(class C { get a() {} })", false); + + // Getter use + test("x.normal;", true); + test("x?.normal;", true); + test("({normal} = foo());", true); + + // SETTER_DEF + test("({ set a(x) {} })", false); + test("(class C { set a(x) {} })", false); + + // SETTER_USE + test("x.normal = 0;", true); + + // MEMBER_FUNCTION_DEF + test("({ a(x) {} })", false); + test("(class C { a(x) {} })", false); + + // MEMBER_FIELD_DEF + test("(class C { x=2; })", false); + test("(class C { x; })", false); + test("(class C { x })", false); + test("(class C { x \n y })", false); + test("(class C { static x=2; })", false); + test("(class C { static x; })", false); + test("(class C { static x })", false); + test("(class C { static x \n static y })", false); + test("(class C { x = alert(1); })", false); + test("(class C { static x = alert(1); })", true); + + // COMPUTED_FIELD_DEF + test("(class C { [x]; })", false); + test("(class C { ['x']=2; })", false); + test("(class C { 'x'=2; })", false); + test("(class C { 1=2; })", false); + test("(class C { static [x]; })", false); + test("(class C { static ['x']=2; })", false); + test("(class C { static 'x'=2; })", false); + test("(class C { static 1=2; })", false); + test("(class C { ['x'] = alert(1); })", false); + test("(class C { static ['x'] = alert(1); })", true); + test("(class C { static [alert(1)] = 2; })", true); + + // CLASS_STATIC_BLOCK + test("(class C { static {} })", false); + // test("(class C { static { [1]; } })", false); + test("(class C { static { let x; } })", true); + test("(class C { static { const x =1 ; } })", true); + test("(class C { static { var x; } })", true); + test("(class C { static { this.x = 1; } })", true); + test("(class C { static { function f() { } } })", true); + // test("(class C { static { (function () {} )} })", false); + // test("(class C { static { ()=>{} } })", false); + + // SUPER calls + test("super()", true); + test("super.foo()", true); + + // A RegExp Object by itself doesn't have any side-effects + test("/abc/gi", false); + + // RegExp instance methods have global side-effects, so whether they are + // considered side-effect free depends on whether the global properties + // are referenced. + test("(/abc/gi).test('')", true); + test("(/abc/gi).test(a)", true); + test("(/abc/gi).exec('')", true); + + // Some RegExp object method that may have side-effects. + test("(/abc/gi).foo('')", true); + + // Try the string RegExp ops. + test("''.match('a')", true); + test("''.match(/(a)/)", true); + test("''.replace('a')", true); + test("''.search('a')", true); + test("''.split('a')", true); + + // Some non-RegExp string op that may have side-effects. + test("''.foo('a')", true); + + // 'a' might be a RegExp object with the 'g' flag, in which case + // the state might change by running any of the string ops. + // Specifically, using these methods resets the "lastIndex" if used + // in combination with a RegExp instance "exec" method. + test("''.match(a)", true); + + // Dynamic import changes global state + test("import('./module.js')", true); +} + +#[test] +fn test_identifier_reference() { + // accessing global variables may have a side effect + test_with_global_variables("a", vec!["a".to_string()], true); + // accessing known globals are side-effect free + test_with_global_variables("NaN", vec!["NaN".to_string()], false); +} + +#[test] +fn test_simple_expressions() { + test("1n", false); + test("true", false); + test("this", false); + test("import.meta", false); + test("(() => {})", false); +} + +#[test] +fn test_unary_expressions() { + test("+'foo'", false); + test("+foo()", true); + test("-'foo'", false); + test("-foo()", true); + test("!'foo'", false); + test("!foo()", true); + test("~'foo'", false); + test("~foo()", true); + test("typeof 'foo'", false); + test_with_global_variables("typeof a", vec!["a".to_string()], false); + test("typeof foo()", true); + test("void 'foo'", false); + test("void foo()", true); + test("delete 'foo'", true); + test("delete foo()", true); +} + +#[test] +fn test_logical_expressions() { + test("a || b", false); + test("a() || b", true); + test("a && b", false); + test("a() && b", true); + test("a ?? b", false); + test("a() ?? b", true); +} + +#[test] +fn test_other_expressions() { + test("(foo)", false); + test("(foo())", true); + + test("a ? b : c", false); + test("a() ? b : c", true); + + test("a, b", false); + test("a(), b", true); + test("a, b()", true); +} + +#[test] +fn test_binary_expressions() { + test("a == b", false); + test("a() == b", true); + test("a < b", false); + test("a() < b", true); + test("a + b", false); + test("a() + b", true); + + // b maybe not a object + // b maybe a proxy that has a side effectful "has" trap + test("a in b", true); + // b maybe not a function + // b[Symbol.hasInstance] may have a side effect + // a maybe a proxy that has a side effectful "getPrototypeOf" trap + test("a instanceof b", true); +} + +#[test] +fn test_object_expression() { + // wrapped with parentheses to avoid treated as a block statement + test("({})", false); + test("({a: 1})", false); + test("({a: foo()})", true); + test("({1: 1})", false); + test("({[1]: 1})", false); + test("({[1n]: 1})", false); + test("({['1']: 1})", false); + test("({[foo()]: 1 })", true); + test("({a: foo()})", true); + test("({...a})", true); + test("({...[]})", false); + test("({...[...a]})", true); + test("({...'foo'})", false); + test("({...`foo`})", false); + test("({...`foo${foo()}`})", true); + test("({...foo()})", true); +} + +#[test] +fn test_array_expression() { + test("[]", false); + test("[1]", false); + test("[foo()]", true); + test("[,]", false); + test("[...a]", true); + test("[...[]]", false); + test("[...[...a]]", true); + test("[...'foo']", false); + test("[...`foo`]", false); + test("[...`foo${foo()}`]", true); + test("[...foo()]", true); +} + +#[test] +fn test_class_expression() { + test("(class {})", false); + test("(class extends a {})", false); + test("(class extends foo() {})", false); // foo() is called when constructed + test("(class { static {} })", false); + test("(class { static { foo(); } })", true); + test("(class { a; })", false); + test("(class { 1; })", false); + test("(class { [1]; })", false); + test("(class { [1n]; })", false); + test("(class { #a; })", false); + test("(class { [foo()] = 1 })", false); // foo() is called when constructed + test("(class { a = foo() })", false); // foo() is called when constructed + test("(class { static a; })", false); + test("(class { static 1; })", false); + test("(class { static [1]; })", false); + test("(class { static [1n]; })", false); + test("(class { static #a; })", false); + test("(class { static [foo()] = 1 })", true); + test("(class { static a = foo() })", true); + test("(class { accessor [foo()]; })", false); + test("(class { static accessor [foo()]; })", true); +} + +#[test] +fn test_side_effectful_expressions() { + test("a.b", true); + test("a[0]", true); + test("a?.b", true); +} + +#[test] +fn tests() { + // This actually have a side effect, but this treated as side-effect free. + test("'' + { toString() { console.log('sideeffect') } }", false); + test("'' + { valueOf() { console.log('sideeffect') } }", false); + test("'' + { [s]() { console.log('sideeffect') } }", false); // assuming s is Symbol.toPrimitive + + // FIXME: actually these have a side effect, but it's ignored now + test("+s", false); // assuming s is a Symbol + test("+b", false); // assuming b is a BitInt + test("+{ valueOf() { return Symbol() } }", false); + test("~s", false); // assuming s is a Symbol + + // FIXME: actually these have a side effect, but it's ignored now + // same for -, *, /, %, **, <<, >> + test("'' + s", false); // assuming s is Symbol + test("0 + b", false); // assuming b is a BitInt + + // FIXME: actually these throws an error, but it's ignored now + test("1n ** (-1n)", false); + test("1n / 0n", false); + test("1n % 0n", false); + test("0n >>> 1n", false); // >>> throws an error even when both operands are bigint +} diff --git a/crates/oxc_minifier/tests/ecmascript/mod.rs b/crates/oxc_minifier/tests/ecmascript/mod.rs index fd7746917462e..55577a9eedbfe 100644 --- a/crates/oxc_minifier/tests/ecmascript/mod.rs +++ b/crates/oxc_minifier/tests/ecmascript/mod.rs @@ -1,2 +1,3 @@ mod array_join; +mod may_have_side_effects; mod prop_name;