mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
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:
1 parent
ff71551a4c
commit
4c353629a6
8 files changed
+101
-143
No files matched your search
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user