Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions crates/oxc_codegen/src/gen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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'`');
}
Expand Down Expand Up @@ -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'`');
}
Expand Down
183 changes: 183 additions & 0 deletions crates/oxc_codegen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,76 @@ impl<'a> Codegen<'a> {
}
}

/// Print template literal content, escaping:
/// - `` ` `` → `` \` `` (unescaped backticks)
/// - `${` → `\${` (unescaped interpolation markers)
/// - `</script` → `<\/script` (HTML script close tag)
///
/// For backticks and `${`, we only escape if they're not already escaped
/// (i.e., not preceded by an odd number of backslashes).
pub fn print_template_literal_str(&mut self, s: &str) {
let bytes = s.as_bytes();
let len = bytes.len();
let mut consumed = 0;

let mut i = 0;
while i < len {
let byte = bytes[i];

match byte {
b'`' => {
// 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;
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When escaping ${, only the $ is being replaced with \\$, but consumed is set to skip only the $ character (i + 1), leaving the { to be printed in the next flush. This will result in \\${ being output as \\$ followed by {, which produces the incorrect output \\${ when it should just be \\${. The logic should either skip both characters (set consumed = i + 2) or only output \\ and let the next iteration handle ${.

Suggested change
consumed = i + 1;
consumed = i + 2;

Copilot uses AI. Check for mistakes.
}
}
b'<' => {
// Check for `</script` (case-insensitive)
if i + 8 <= len && is_script_close_tag(&bytes[i..i + 8]) {
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The </script escaping in template literals uses a simpler byte-by-byte scan compared to the SIMD-optimized approach in print_str_escaping_script_close_tag. While </script is rare in template literals, consider whether the performance difference is acceptable or if SIMD optimization should be applied here as well for consistency.

Copilot uses AI. Check for mistakes.
// Flush bytes up to and including `<`, then write `\/`
// SAFETY: `consumed` and `i + 1` are valid indices
unsafe {
self.code.print_bytes_unchecked(bytes.get_unchecked(consumed..=i));
}
self.print_str("\\/");
consumed = i + 2; // Skip past `</`
i += 1; // Extra increment to skip `/`
}
}
Comment on lines +433 to +445
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new implementation uses a simple byte-by-byte loop for all patterns including </script. The original print_str_escaping_script_close_tag function uses SIMD optimizations to search for < in chunks of 16 bytes for better performance. Consider whether the simpler approach is acceptable for template literals, or if SIMD optimization should also be applied here since template literals are commonly used and can be quite long.

Copilot uses AI. Check for mistakes.
_ => {}
}

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]
Expand All @@ -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 {
Expand Down Expand Up @@ -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() {
// </script should be escaped to <\/script
assert_eq!(escape_template_str("</script"), "<\\/script");
assert_eq!(escape_template_str("</SCRIPT"), "<\\/SCRIPT");
assert_eq!(escape_template_str("</ScRiPt"), "<\\/ScRiPt");
assert_eq!(escape_template_str("a</script>b"), "a<\\/script>b");
}

#[test]
fn test_combined_escaping() {
// Multiple escaping scenarios combined
assert_eq!(escape_template_str("a`b${c}</script>d"), "a\\`b\\${c}<\\/script>d");
}
}
Loading