feat(ast)!: add AstBuilder::template_element_escape_raw and template_element_escape_raw_with_lone_surrogates methods (#23047)

Remove the `escape_raw` param from:

- `AstBuilder::template_element`
- `AstBuilder::template_element_with_lone_surrogates`

This param was added in #18121, to allow automatically escaping `raw`field.

Instead, add new methods:

- `AstBuilder::template_element_escape_raw`
- `AstBuilder::template_element_escape_raw_with_lone_surrogates`

These 2 methods perform the same function - doing what `escape_raw: true` did previously.

The rationale for this change is:

1. Simplifies the codegen. My feeling is that codegen should be as mechanical as possible, with behavior controlled by attributes on AST types themselves (e.g. `#[clone_in(default)]`) rather than having weird "special cases" hard-coded in the codegen.
2. Streamline the common use case (parser) where `escape_raw` was always `false`.
This commit is contained in:
overlookmotel committed 2026-06-07 01:48:45 +00:00
1 parent ff71551a4c
commit 4c353629a6
8 files changed
+101 -143

No files matched your search

+94
View File
@@ -278,4 +278,98 @@ impl<'a> AstBuilder<'a> {
NONE,
))
}
/* ---------- Template literals ---------- */
/// Build a [`TemplateElement`], escaping special characters in the raw value.
///
/// Like [`AstBuilder::template_element`], but escapes backticks, `${`, backslashes, and carriage
/// returns in `value.raw` first.
#[inline]
pub fn template_element_escape_raw(
self,
span: Span,
mut value: TemplateElementValue<'a>,
tail: bool,
) -> TemplateElement<'a> {
value.raw = escape_template_element_raw(value.raw.as_str(), self);
self.template_element(span, value, tail)
}
/// Build a [`TemplateElement`] with `lone_surrogates`, escaping special characters in the raw value.
///
/// Like [`AstBuilder::template_element_with_lone_surrogates`], but escapes backticks, `${`,
/// backslashes, and carriage returns in `value.raw` first.
#[inline]
pub fn template_element_escape_raw_with_lone_surrogates(
self,
span: Span,
mut value: TemplateElementValue<'a>,
tail: bool,
lone_surrogates: bool,
) -> TemplateElement<'a> {
value.raw = escape_template_element_raw(value.raw.as_str(), self);
self.template_element_with_lone_surrogates(span, value, tail, lone_surrogates)
}
}
/// Escape special characters for template element raw value.
///
/// Escapes: backticks, `${`, backslashes, and carriage returns.
fn escape_template_element_raw<'a>(raw: &str, ast: AstBuilder<'a>) -> Str<'a> {
let bytes = raw.as_bytes();
// Calculate size needed for escaped string
let mut extra_bytes = 0usize;
for i in 0..bytes.len() {
extra_bytes += match bytes[i] {
b'\\' | b'`' | b'\r' => 1,
b'$' if bytes.get(i + 1) == Some(&b'{') => 1,
_ => 0,
};
}
if extra_bytes == 0 {
return ast.str(raw);
}
// Allocate directly in arena
let len = bytes.len() + extra_bytes;
let layout = std::alloc::Layout::array::<u8>(len).unwrap();
let ptr = ast.allocator.alloc_layout(layout);
// SAFETY: `ptr` points to `len` bytes of valid memory allocated by the arena.
// Input is valid UTF-8, we only escape ASCII bytes, so output is also valid UTF-8.
unsafe {
let escaped = std::slice::from_raw_parts_mut(ptr.as_ptr(), len);
let mut j = 0;
for i in 0..bytes.len() {
match bytes[i] {
b'\\' => {
*escaped.get_unchecked_mut(j) = b'\\';
*escaped.get_unchecked_mut(j + 1) = b'\\';
j += 2;
}
b'`' => {
*escaped.get_unchecked_mut(j) = b'\\';
*escaped.get_unchecked_mut(j + 1) = b'`';
j += 2;
}
b'$' if bytes.get(i + 1) == Some(&b'{') => {
*escaped.get_unchecked_mut(j) = b'\\';
*escaped.get_unchecked_mut(j + 1) = b'$';
j += 2;
}
b'\r' => {
*escaped.get_unchecked_mut(j) = b'\\';
*escaped.get_unchecked_mut(j + 1) = b'r';
j += 2;
}
b => {
*escaped.get_unchecked_mut(j) = b;
j += 1;
}
}
}
Str::from(std::str::from_utf8_unchecked(escaped))
}
}
@@ -1831,16 +1831,7 @@ impl<'a> AstBuilder<'a> {
span: Span,
value: TemplateElementValue<'a>,
tail: bool,
escape_raw: bool,
) -> TemplateElement<'a> {
let value = if escape_raw {
TemplateElementValue {
raw: escape_template_element_raw(value.raw.as_str(), self),
cooked: value.cooked,
}
} else {
value
};
TemplateElement {
node_id: Default::default(),
span,
@@ -1864,16 +1855,7 @@ impl<'a> AstBuilder<'a> {
value: TemplateElementValue<'a>,
tail: bool,
lone_surrogates: bool,
escape_raw: bool,
) -> TemplateElement<'a> {
let value = if escape_raw {
TemplateElementValue {
raw: escape_template_element_raw(value.raw.as_str(), self),
cooked: value.cooked,
}
} else {
value
};
TemplateElement { node_id: Default::default(), span, value, tail, lone_surrogates }
}
@@ -15533,58 +15515,3 @@ impl<'a> AstBuilder<'a> {
Box::new_in(self.js_doc_unknown_type(span), self.allocator)
}
}
/// Escape special characters for template element raw value.
///
/// Escapes: backticks, `${`, backslashes, and carriage returns.
fn escape_template_element_raw<'a>(raw: &str, ast: AstBuilder<'a>) -> Str<'a> {
let bytes = raw.as_bytes();
let mut extra_bytes = 0usize;
for i in 0..bytes.len() {
extra_bytes += match bytes[i] {
b'\\' | b'`' | b'\r' => 1,
b'$' if bytes.get(i + 1) == Some(&b'{') => 1,
_ => 0,
};
}
if extra_bytes == 0 {
return ast.str(raw);
}
let len = bytes.len() + extra_bytes;
let layout = std::alloc::Layout::array::<u8>(len).unwrap();
let ptr = ast.allocator.alloc_layout(layout);
#[expect(clippy::undocumented_unsafe_blocks)]
unsafe {
let escaped = std::slice::from_raw_parts_mut(ptr.as_ptr(), len);
let mut j = 0;
for i in 0..bytes.len() {
match bytes[i] {
b'\\' => {
*escaped.get_unchecked_mut(j) = b'\\';
*escaped.get_unchecked_mut(j + 1) = b'\\';
j += 2;
}
b'`' => {
*escaped.get_unchecked_mut(j) = b'\\';
*escaped.get_unchecked_mut(j + 1) = b'`';
j += 2;
}
b'$' if bytes.get(i + 1) == Some(&b'{') => {
*escaped.get_unchecked_mut(j) = b'\\';
*escaped.get_unchecked_mut(j + 1) = b'$';
j += 2;
}
b'\r' => {
*escaped.get_unchecked_mut(j) = b'\\';
*escaped.get_unchecked_mut(j + 1) = b'r';
j += 2;
}
b => {
*escaped.get_unchecked_mut(j) = b;
j += 1;
}
}
}
Str::from(std::str::from_utf8_unchecked(escaped))
}
}
+2 -2
View File
@@ -831,10 +831,10 @@ fn template_literal_escape_when_building_ast() {
// Create a template literal with special characters that need escaping:
// backtick, ${, and backslash
// Pass escape_raw: true to automatically escape the raw field
// Use `template_element_escape_raw` to automatically escape the raw field
let cooked = "hello`world${foo}\\bar";
let value = TemplateElementValue { raw: ast.str(cooked), cooked: Some(ast.str(cooked)) };
let element = ast.template_element(SPAN, value, true, true); // escape_raw: true
let element = ast.template_element_escape_raw(SPAN, value, true);
let quasis = ast.vec1(element);
let template_literal = ast.template_literal(SPAN, quasis, ast.vec());
@@ -317,7 +317,6 @@ impl<'a> PeepholeOptimizations {
e.span(),
TemplateElementValue { raw: "".into(), cooked: Some("".into()) },
false,
false,
)
})
.take(expressions.len() + 1),
@@ -344,7 +343,6 @@ impl<'a> PeepholeOptimizations {
temp_lit.span,
TemplateElementValue { raw: "".into(), cooked: Some("".into()) },
false,
false,
)
})
.take(expressions.len() + 1),
@@ -316,11 +316,11 @@ impl<'a> PeepholeOptimizations {
let cooked = ast.str(scratch);
let raw_cow = Self::escape_string_for_template_literal(scratch);
let raw = ast.str(&raw_cow);
// `raw` is already escaped
quasis.push(ast.template_element(
SPAN,
TemplateElementValue { raw, cooked: Some(cooked) },
false,
false, // raw is already escaped
));
scratch.clear(); // maintains INVARIANT above
// checked that all the arguments are expression above
@@ -340,11 +340,11 @@ impl<'a> PeepholeOptimizations {
let cooked = ast.str(scratch);
let raw_cow = Self::escape_string_for_template_literal(scratch);
let raw = ast.str(&raw_cow);
// `raw` is already escaped
quasis.push(ast.template_element(
SPAN,
TemplateElementValue { raw, cooked: Some(cooked) },
true, // tail
false,
));
debug_assert_eq!(quasis.len(), expressions.len() + 1);
+1 -1
View File
@@ -645,12 +645,12 @@ impl<'a, C: Config> ParserImpl<'a, C> {
}
let tail = matches!(cur_kind, Kind::TemplateTail | Kind::NoSubstitutionTemplate);
// Parser provides already-escaped values from source, so no escaping needed here
self.ast.template_element_with_lone_surrogates(
span,
TemplateElementValue { raw, cooked },
tail,
lone_surrogates,
false, // escape_raw: parser provides already-escaped values from source
)
}
@@ -1152,7 +1152,6 @@ mod tests {
SPAN,
TemplateElementValue { raw: ast.str(input), cooked: Some(ast.str(input)) },
true,
false,
)),
ast.vec(),
);
+2 -62
View File
@@ -97,47 +97,6 @@ impl Generator for AstBuilderGenerator {
impl<'a> AstBuilder<'a> {
#fns
}
///@@line_break
/// Escape special characters for template element raw value.
///
/// Escapes: backticks, `${`, backslashes, and carriage returns.
fn escape_template_element_raw<'a>(raw: &str, ast: AstBuilder<'a>) -> Str<'a> {
let bytes = raw.as_bytes();
// Calculate size needed for escaped string
let mut extra_bytes = 0usize;
for i in 0..bytes.len() {
extra_bytes += match bytes[i] {
b'\\' | b'`' | b'\r' => 1,
b'$' if bytes.get(i + 1) == Some(&b'{') => 1,
_ => 0,
};
}
if extra_bytes == 0 {
return ast.str(raw);
}
// Allocate directly in arena
let len = bytes.len() + extra_bytes;
let layout = std::alloc::Layout::array::<u8>(len).unwrap();
let ptr = ast.allocator.alloc_layout(layout);
// SAFETY: `ptr` points to `len` bytes of valid memory allocated by the arena.
// Input is valid UTF-8, we only escape ASCII bytes, so output is also valid UTF-8.
#[expect(clippy::undocumented_unsafe_blocks)]
unsafe {
let escaped = std::slice::from_raw_parts_mut(ptr.as_ptr(), len);
let mut j = 0;
for i in 0..bytes.len() {
match bytes[i] {
b'\\' => { *escaped.get_unchecked_mut(j) = b'\\'; *escaped.get_unchecked_mut(j + 1) = b'\\'; j += 2; }
b'`' => { *escaped.get_unchecked_mut(j) = b'\\'; *escaped.get_unchecked_mut(j + 1) = b'`'; j += 2; }
b'$' if bytes.get(i + 1) == Some(&b'{') => { *escaped.get_unchecked_mut(j) = b'\\'; *escaped.get_unchecked_mut(j + 1) = b'$'; j += 2; }
b'\r' => { *escaped.get_unchecked_mut(j) = b'\\'; *escaped.get_unchecked_mut(j + 1) = b'r'; j += 2; }
b => { *escaped.get_unchecked_mut(j) = b; j += 1; }
}
}
Str::from(std::str::from_utf8_unchecked(escaped))
}
}
};
Output::Rust { path: output_path(AST_CRATE_PATH, "ast_builder.rs"), tokens: output }
@@ -303,32 +262,13 @@ fn generate_builder_methods_for_struct_impl(
let params_docs = generate_doc_comment_for_params(params);
// Special case for TemplateElement: add `escape_raw` parameter
let (extra_params, body) = if struct_name == "TemplateElement" {
let extra_params = quote! { , escape_raw: bool };
let body = quote! {
let value = if escape_raw {
TemplateElementValue {
raw: escape_template_element_raw(value.raw.as_str(), self),
cooked: value.cooked,
}
} else {
value
};
#struct_ident { #fields }
};
(extra_params, body)
} else {
(quote! {}, quote! { #struct_ident { #fields } })
};
let method = quote! {
///@@line_break
#fn_docs
#params_docs
#[inline]
pub fn #fn_name #generic_params (self, #fn_params #extra_params) -> #struct_ty #where_clause {
#body
pub fn #fn_name #generic_params (self, #fn_params) -> #struct_ty #where_clause {
#struct_ident { #fields }
}
};