diff --git a/crates/oxc_ecmascript/src/side_effects/context.rs b/crates/oxc_ecmascript/src/side_effects/context.rs index 877aab4bb3dc7..a4a27fbd76b9c 100644 --- a/crates/oxc_ecmascript/src/side_effects/context.rs +++ b/crates/oxc_ecmascript/src/side_effects/context.rs @@ -1,3 +1,5 @@ +use oxc_ast::ast::Expression; + use crate::is_global_reference::IsGlobalReference; pub trait MayHaveSideEffectsContext: IsGlobalReference { @@ -6,4 +8,10 @@ pub trait MayHaveSideEffectsContext: IsGlobalReference { /// Pure annotations are the comments that marks that a expression is pure. /// For example, `/* @__PURE__ */`, `/* #__NO_SIDE_EFFECTS__ */`. fn respect_annotations(&self) -> bool; + + /// Whether to treat this function call as pure. + /// + /// This function is called for normal function calls, new calls, and + /// tagged template calls (`foo()`, `new Foo()`, ``foo`b` ``). + fn is_pure_call(&self, callee: &Expression) -> bool; } 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 2dae9dd32e6de..d8b691cd24ba3 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 @@ -62,6 +62,7 @@ impl MayHaveSideEffects for Expression<'_> { } Expression::CallExpression(e) => e.may_have_side_effects(ctx), Expression::NewExpression(e) => e.may_have_side_effects(ctx), + Expression::TaggedTemplateExpression(e) => e.may_have_side_effects(ctx), _ => true, } } @@ -486,7 +487,7 @@ fn get_array_minimum_length(arr: &ArrayExpression) -> usize { impl MayHaveSideEffects for CallExpression<'_> { fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext) -> bool { - if self.pure && ctx.respect_annotations() { + if (self.pure && ctx.respect_annotations()) || ctx.is_pure_call(&self.callee) { self.arguments.iter().any(|e| e.may_have_side_effects(ctx)) } else { true @@ -496,7 +497,7 @@ impl MayHaveSideEffects for CallExpression<'_> { impl MayHaveSideEffects for NewExpression<'_> { fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext) -> bool { - if self.pure && ctx.respect_annotations() { + if (self.pure && ctx.respect_annotations()) || ctx.is_pure_call(&self.callee) { self.arguments.iter().any(|e| e.may_have_side_effects(ctx)) } else { true @@ -504,6 +505,12 @@ impl MayHaveSideEffects for NewExpression<'_> { } } +impl MayHaveSideEffects for TaggedTemplateExpression<'_> { + fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext) -> bool { + if ctx.is_pure_call(&self.tag) { self.quasi.may_have_side_effects(ctx) } else { true } + } +} + impl MayHaveSideEffects for Argument<'_> { fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext) -> bool { match self { diff --git a/crates/oxc_minifier/src/ctx.rs b/crates/oxc_minifier/src/ctx.rs index f03cc6e14d413..b0e9f48e09328 100644 --- a/crates/oxc_minifier/src/ctx.rs +++ b/crates/oxc_minifier/src/ctx.rs @@ -29,6 +29,10 @@ impl oxc_ecmascript::side_effects::MayHaveSideEffectsContext for Ctx<'_, '_> { fn respect_annotations(&self) -> bool { true } + + fn is_pure_call(&self, _callee: &Expression) -> bool { + false + } } impl<'a> ConstantEvaluationCtx<'a> for Ctx<'a, '_> { diff --git a/crates/oxc_minifier/tests/ecmascript/may_have_side_effects.rs b/crates/oxc_minifier/tests/ecmascript/may_have_side_effects.rs index 03baaea871a67..5e6fecec16eb8 100644 --- a/crates/oxc_minifier/tests/ecmascript/may_have_side_effects.rs +++ b/crates/oxc_minifier/tests/ecmascript/may_have_side_effects.rs @@ -1,5 +1,5 @@ use oxc_allocator::Allocator; -use oxc_ast::ast::{IdentifierReference, Statement}; +use oxc_ast::ast::{Expression, IdentifierReference, Statement}; use oxc_ecmascript::{ is_global_reference::IsGlobalReference, side_effects::{MayHaveSideEffects, MayHaveSideEffectsContext}, @@ -10,6 +10,12 @@ use oxc_span::SourceType; struct Ctx { global_variable_names: Vec, annotation: bool, + pure_function_names: Vec, +} +impl Default for Ctx { + fn default() -> Self { + Self { global_variable_names: vec![], annotation: true, pure_function_names: vec![] } + } } impl IsGlobalReference for Ctx { fn is_global_reference(&self, ident: &IdentifierReference<'_>) -> Option { @@ -20,10 +26,18 @@ impl MayHaveSideEffectsContext for Ctx { fn respect_annotations(&self) -> bool { self.annotation } + + fn is_pure_call(&self, callee: &Expression) -> bool { + if let Expression::Identifier(id) = callee { + self.pure_function_names.iter().any(|name| name == id.name.as_str()) + } else { + false + } + } } fn test(source_text: &str, expected: bool) { - let ctx = Ctx { global_variable_names: vec![], annotation: true }; + let ctx = Ctx::default(); test_with_ctx(source_text, &ctx, expected); } @@ -32,7 +46,7 @@ fn test_with_global_variables( global_variable_names: Vec, expected: bool, ) { - let ctx = Ctx { global_variable_names, annotation: true }; + let ctx = Ctx { global_variable_names, ..Default::default() }; test_with_ctx(source_text, &ctx, expected); } @@ -711,11 +725,31 @@ fn test_call_like_expressions() { test("/* #__PURE__ */ new Foo(...`${bar()}`)", true); test("/* #__PURE__ */ new class { constructor() { foo() } }()", false); - let ctx = Ctx { global_variable_names: vec![], annotation: false }; + let ctx = Ctx { annotation: false, ..Default::default() }; test_with_ctx("/* #__PURE__ */ foo()", &ctx, true); test_with_ctx("/* #__PURE__ */ new Foo()", &ctx, true); } +#[test] +fn test_is_pure_call_support() { + let ctx = Ctx { + pure_function_names: vec!["foo".to_string(), "Foo".to_string()], + ..Default::default() + }; + test_with_ctx("foo()", &ctx, false); + test_with_ctx("foo(1)", &ctx, false); + test_with_ctx("foo(bar())", &ctx, true); + test_with_ctx("bar()", &ctx, true); + test_with_ctx("new Foo()", &ctx, false); + test_with_ctx("new Foo(1)", &ctx, false); + test_with_ctx("new Foo(bar())", &ctx, true); + test_with_ctx("new Bar()", &ctx, true); + test_with_ctx("foo``", &ctx, false); + test_with_ctx("foo`1`", &ctx, false); + test_with_ctx("foo`${bar()}`", &ctx, true); + test_with_ctx("bar``", &ctx, true); +} + #[test] fn test_object_with_to_primitive_related_properties_overridden() { test("+{}", false);