diff --git a/crates/oxc_minifier/src/peephole/fold_constants.rs b/crates/oxc_minifier/src/peephole/fold_constants.rs index 3db74ad5d23cf..dce5999ac15f2 100644 --- a/crates/oxc_minifier/src/peephole/fold_constants.rs +++ b/crates/oxc_minifier/src/peephole/fold_constants.rs @@ -2,6 +2,7 @@ use oxc_ast::ast::*; use oxc_ecmascript::{ constant_evaluation::{ConstantEvaluation, ConstantValue, DetermineValueType, ValueType}, side_effects::MayHaveSideEffects, + ToJsString, }; use oxc_span::GetSpan; use oxc_syntax::operator::{BinaryOperator, LogicalOperator}; @@ -16,6 +17,10 @@ impl<'a> PeepholeOptimizations { /// /// pub fn fold_constants_exit_expression(&mut self, expr: &mut Expression<'a>, ctx: Ctx<'a, '_>) { + if let Expression::TemplateLiteral(t) = expr { + self.try_inline_values_in_template_literal(t, ctx); + } + if let Some(folded_expr) = match expr { Expression::BinaryExpression(e) => Self::try_fold_binary_expr(e, ctx) .or_else(|| Self::try_fold_binary_typeof_comparison(e, ctx)), @@ -507,6 +512,64 @@ impl<'a> PeepholeOptimizations { } None } + + /// Inline constant values in template literals + /// + /// - `foo${1}bar${i}` => `foo1bar${i}` + fn try_inline_values_in_template_literal( + &mut self, + t: &mut TemplateLiteral<'a>, + ctx: Ctx<'a, '_>, + ) { + let has_expr_to_inline = t + .expressions + .iter() + .any(|expr| !expr.may_have_side_effects(&ctx) && expr.to_js_string(&ctx).is_some()); + if !has_expr_to_inline { + return; + } + + let mut inline_exprs = Vec::new(); + let new_exprs = + ctx.ast.vec_from_iter(t.expressions.drain(..).enumerate().filter_map(|(idx, expr)| { + if expr.may_have_side_effects(&ctx) { + Some(expr) + } else if let Some(str) = expr.to_js_string(&ctx) { + inline_exprs.push((idx, str)); + None + } else { + Some(expr) + } + })); + t.expressions = new_exprs; + + // inline the extracted inline-able expressions into quasis + // "current_quasis + extracted_value + next_quasis" + for (i, (idx, str)) in inline_exprs.into_iter().enumerate() { + let idx = idx - i; + let next_quasi = (idx + 1 < t.quasis.len()).then(|| t.quasis.remove(idx + 1)); + let quasi = &mut t.quasis[idx]; + let new_raw = quasi.value.raw.into_string() + + &Self::escape_string_for_template_literal(&str) + + next_quasi.as_ref().map(|q| q.value.raw.as_str()).unwrap_or_default(); + quasi.value.raw = ctx.ast.atom(&new_raw); + let new_cooked = if let (Some(cooked1), Some(cooked2)) = + (quasi.value.cooked, next_quasi.as_ref().map(|q| q.value.cooked)) + { + let v = + cooked1.into_string() + &str + cooked2.map(|c| c.as_str()).unwrap_or_default(); + Some(ctx.ast.atom(&v)) + } else { + None + }; + quasi.value.cooked = new_cooked; + if next_quasi.is_some_and(|q| q.tail) { + quasi.tail = true; + } + } + + self.mark_current_function_as_changed(); + } } /// @@ -1643,6 +1706,18 @@ mod test { fold("+(void unknown())", "+void unknown()"); } + #[test] + fn test_inline_values_in_template_literal() { + fold("`foo${1}`", "'foo1'"); + fold("`foo${1}bar`", "'foo1bar'"); + fold("`foo${1}bar${2}baz`", "'foo1bar2baz'"); + fold("`foo${1}bar${2}baz${3}qux`", "'foo1bar2baz3qux'"); + fold("`foo${1}${i}`", "`foo1${i}`"); + fold("`foo${'${}'}`", "'foo${}'"); + fold("`foo${'${}'}${i}`", "`foo\\${}${i}`"); + fold_same("foo`foo${1}bar`"); + } + mod bigint { use super::{ fold, fold_same, MAX_SAFE_FLOAT, MAX_SAFE_INT, NEG_MAX_SAFE_FLOAT, NEG_MAX_SAFE_INT, diff --git a/crates/oxc_minifier/src/peephole/replace_known_methods.rs b/crates/oxc_minifier/src/peephole/replace_known_methods.rs index 6f895580eeed9..ddea9da67877f 100644 --- a/crates/oxc_minifier/src/peephole/replace_known_methods.rs +++ b/crates/oxc_minifier/src/peephole/replace_known_methods.rs @@ -739,11 +739,7 @@ impl<'a> PeepholeOptimizations { SPAN, false, TemplateElementValue { - raw: s - .cow_replace("\\", "\\\\") - .cow_replace("`", "\\`") - .cow_replace("${", "\\${") - .cow_replace("\r\n", "\\r\n") + raw: Self::escape_string_for_template_literal(&s) .into_in(ctx.ast.allocator), cooked: Some(cooked), }, @@ -760,6 +756,20 @@ impl<'a> PeepholeOptimizations { } } + pub fn escape_string_for_template_literal(s: &str) -> Cow<'_, str> { + if s.contains(['\\', '`', '$', '\r']) { + Cow::Owned( + s.cow_replace("\\", "\\\\") + .cow_replace("`", "\\`") + .cow_replace("${", "\\${") + .cow_replace("\r\n", "\\r\n") + .into_owned(), + ) + } else { + Cow::Borrowed(s) + } + } + fn try_fold_known_property_access(&mut self, node: &mut Expression<'a>, ctx: Ctx<'a, '_>) { let (name, object, span) = match &node { Expression::StaticMemberExpression(member) if !member.optional => { @@ -1761,7 +1771,7 @@ mod test { test("x = ''.concat(a, 'b', c)", "x = `${a}b${c}`"); test("x = ''.concat('a', b, 'c')", "x = `a${b}c`"); test("x = ''.concat('a', b, 'c', d, 'e', f, 'g', h, 'i', j, 'k', l, 'm', n, 'o', p, 'q', r, 's', t)", "x = `a${b}c${d}e${f}g${h}i${j}k${l}m${n}o${p}q${r}s${t}`"); - test("x = ''.concat(a, 1)", "x = `${a}${1}`"); // inlining 1 is not implemented yet + test("x = ''.concat(a, 1)", "x = `${a}1`"); test("x = '\\\\s'.concat(a)", "x = `\\\\s${a}`"); test("x = '`'.concat(a)", "x = `\\`${a}`");