diff --git a/crates/oxc_codegen/src/gen.rs b/crates/oxc_codegen/src/gen.rs index 5b54eebadebf2..6874a82fc241e 100644 --- a/crates/oxc_codegen/src/gen.rs +++ b/crates/oxc_codegen/src/gen.rs @@ -2171,13 +2171,13 @@ impl Gen for TemplateLiteral<'_> { p.print_ascii_byte(b'`'); debug_assert_eq!(self.quasis.len(), self.expressions.len() + 1); let (first_quasi, remaining_quasis) = self.quasis.split_first().unwrap(); - p.print_str_escaping_script_close_tag(first_quasi.value.raw.as_str()); + p.print_template_literal_str(first_quasi.value.raw.as_str()); for (expr, quasi) in self.expressions.iter().zip(remaining_quasis) { p.print_str("${"); p.print_expression(expr); p.print_ascii_byte(b'}'); p.add_source_mapping(quasi.span); - p.print_str_escaping_script_close_tag(quasi.value.raw.as_str()); + p.print_template_literal_str(quasi.value.raw.as_str()); } p.print_ascii_byte(b'`'); } @@ -3278,7 +3278,7 @@ impl Gen for TSTemplateLiteralType<'_> { types.print(p, ctx); p.print_ascii_byte(b'}'); } - p.print_str(item.value.raw.as_str()); + p.print_template_literal_str(item.value.raw.as_str()); } p.print_ascii_byte(b'`'); } diff --git a/crates/oxc_codegen/src/lib.rs b/crates/oxc_codegen/src/lib.rs index 56c6c56290b6a..4e0eb4038cd5a 100644 --- a/crates/oxc_codegen/src/lib.rs +++ b/crates/oxc_codegen/src/lib.rs @@ -386,6 +386,76 @@ impl<'a> Codegen<'a> { } } + /// Print template literal content, escaping: + /// - `` ` `` → `` \` `` (unescaped backticks) + /// - `${` → `\${` (unescaped interpolation markers) + /// - ` { + // Check if preceded by odd number of backslashes (already escaped) + if !is_preceded_by_odd_backslashes(bytes, i) { + // Flush bytes up to (not including) the backtick + // SAFETY: `consumed` and `i` are valid indices + unsafe { + self.code.print_bytes_unchecked(bytes.get_unchecked(consumed..i)); + } + self.print_str("\\`"); + consumed = i + 1; + } + } + b'$' => { + // Check if followed by `{` and not already escaped + if i + 1 < len + && bytes[i + 1] == b'{' + && !is_preceded_by_odd_backslashes(bytes, i) + { + // Flush bytes up to (not including) the dollar sign + // SAFETY: `consumed` and `i` are valid indices + unsafe { + self.code.print_bytes_unchecked(bytes.get_unchecked(consumed..i)); + } + self.print_str("\\$"); + consumed = i + 1; + } + } + b'<' => { + // Check for ` {} + } + + i += 1; + } + + // Flush remaining bytes + // SAFETY: `consumed` is a valid index + unsafe { + self.code.print_bytes_unchecked(bytes.get_unchecked(consumed..)); + } + } + /// Print a single [`Expression`], adding it to the code generator's /// internal buffer. Unlike [`Codegen::build`], this does not consume `self`. #[inline] @@ -394,6 +464,23 @@ impl<'a> Codegen<'a> { } } +/// Returns true if position `i` in `bytes` is preceded by an odd number of backslashes. +/// This indicates the character at position `i` is escaped. +#[inline] +fn is_preceded_by_odd_backslashes(bytes: &[u8], i: usize) -> bool { + let mut count = 0; + let mut j = i; + while j > 0 { + j -= 1; + if bytes[j] == b'\\' { + count += 1; + } else { + break; + } + } + count % 2 == 1 +} + // Private APIs impl<'a> Codegen<'a> { fn code(&self) -> &CodeBuffer { @@ -938,3 +1025,99 @@ impl<'a> Codegen<'a> { #[inline] fn add_source_mapping_for_name(&mut self, _span: Span, _name: &str) {} } + +#[cfg(test)] +mod template_literal_escaping_tests { + use super::*; + + fn escape_template_str(input: &str) -> String { + let mut codegen = Codegen::new(); + codegen.print_template_literal_str(input); + codegen.into_source_text() + } + + #[test] + fn test_no_escaping_needed() { + // Regular strings should pass through unchanged + assert_eq!(escape_template_str("hello world"), "hello world"); + assert_eq!(escape_template_str("foo bar baz"), "foo bar baz"); + assert_eq!(escape_template_str(""), ""); + } + + #[test] + fn test_backtick_escaping() { + // Unescaped backticks should be escaped + assert_eq!(escape_template_str("hello`world"), "hello\\`world"); + assert_eq!(escape_template_str("`"), "\\`"); + assert_eq!(escape_template_str("``"), "\\`\\`"); + assert_eq!(escape_template_str("a`b`c"), "a\\`b\\`c"); + } + + #[test] + fn test_already_escaped_backtick() { + // Already escaped backticks (preceded by odd backslashes) should NOT be double-escaped + assert_eq!(escape_template_str("hello\\`world"), "hello\\`world"); + assert_eq!(escape_template_str("\\`"), "\\`"); + assert_eq!(escape_template_str("a\\`b"), "a\\`b"); + } + + #[test] + fn test_double_backslash_before_backtick() { + // Double backslash + backtick: the backslashes escape each other, backtick is NOT escaped + // So we need to escape the backtick + assert_eq!(escape_template_str("\\\\`"), "\\\\\\`"); + assert_eq!(escape_template_str("a\\\\`b"), "a\\\\\\`b"); + } + + #[test] + fn test_triple_backslash_before_backtick() { + // Triple backslash + backtick: \\ (escaped backslash) + \` (escaped backtick) + // The backtick IS already escaped, so no change needed + assert_eq!(escape_template_str("\\\\\\`"), "\\\\\\`"); + } + + #[test] + fn test_dollar_brace_escaping() { + // Unescaped ${ should be escaped + assert_eq!(escape_template_str("hello${world"), "hello\\${world"); + assert_eq!(escape_template_str("${"), "\\${"); + assert_eq!(escape_template_str("${foo}${bar}"), "\\${foo}\\${bar}"); + } + + #[test] + fn test_dollar_without_brace() { + // $ not followed by { should NOT be escaped + assert_eq!(escape_template_str("$"), "$"); + assert_eq!(escape_template_str("$a"), "$a"); + assert_eq!(escape_template_str("$ {"), "$ {"); + assert_eq!(escape_template_str("$}"), "$}"); + } + + #[test] + fn test_already_escaped_dollar_brace() { + // Already escaped ${ should NOT be double-escaped + assert_eq!(escape_template_str("\\${"), "\\${"); + assert_eq!(escape_template_str("a\\${b"), "a\\${b"); + } + + #[test] + fn test_double_backslash_before_dollar_brace() { + // Double backslash + ${: the backslashes escape each other, ${ is NOT escaped + assert_eq!(escape_template_str("\\\\${"), "\\\\\\${"); + } + + #[test] + fn test_script_close_tag_escaping() { + // b"), "a<\\/script>b"); + } + + #[test] + fn test_combined_escaping() { + // Multiple escaping scenarios combined + assert_eq!(escape_template_str("a`b${c}d"), "a\\`b\\${c}<\\/script>d"); + } +}