mirror of
https://github.com/aphrody-labs/oxc.git
synced 2026-10-14 22:29:17 +00:00
fix(minifier): avoid invalid octal escapes in template folds (#25495)
Inlining constant template expressions can join a decimal digit onto a preceding raw `\0`. The resulting `\00` is a forbidden octal escape inside template literals, so DCE can emit JavaScript that fails to parse. This surfaced in rolldown/rolldown#10661. When a fold would create that boundary, serialize the null as `\x00` and continue merging the template fragments. This preserves the cooked value and the optimization, matches Terser and esbuild, and leaves escaped-backslash cases unchanged. ## Example Input: ```js return `${messageId}\0${correlationId}\0${0}`; ``` Before: ```js return `${messageId}\0${correlationId}\00`; ``` After: ```js return `${messageId}\0${correlationId}\x000`; ``` Verified with `cargo test -p oxc_minifier` (643 passed, 35 ignored), the DCE example's `--twice` idempotency check, a runtime equivalence check, `just minsize` with unchanged size/allocation snapshots, Clippy with warnings denied, and formatting. `just ready` additionally passed dependency, typo, generation, formatting, workspace checks, and all minifier tests; the remaining workspace run stopped on three unrelated Oxlint ANSI-color snapshots because the environment sets `NO_COLOR=1`, and a color-enabled rerun was blocked by the filesystem running out of space. Refs rolldown/rolldown#10661. Implemented with AI assistance (OpenAI Codex). The contributor remains responsible for reviewing, understanding, and submitting the change under the Oxc AI usage policy.
This commit is contained in:
1 parent
b63e88a0b4
commit
c3e99d160d
2 files changed
+44
-2
No files matched your search
@@ -973,8 +973,22 @@ impl<'a> PeepholeOptimizations {
|
||||
let quasi = &mut t.quasis[idx];
|
||||
let escaped = Self::escape_string_for_template_literal(&str);
|
||||
let next_raw = next_quasi.as_ref().map(|q| q.value.raw.as_str()).unwrap_or_default();
|
||||
quasi.value.raw =
|
||||
Str::from_strs_array_in([quasi.value.raw.as_str(), &escaped, next_raw], ctx);
|
||||
let raw = quasi.value.raw.as_str();
|
||||
let starts_with_digit = escaped
|
||||
.as_bytes()
|
||||
.first()
|
||||
.or_else(|| next_raw.as_bytes().first())
|
||||
.is_some_and(u8::is_ascii_digit);
|
||||
let cooked_ends_with_null =
|
||||
quasi.value.cooked.is_some_and(|cooked| cooked.as_str().ends_with('\0'));
|
||||
quasi.value.raw = if starts_with_digit
|
||||
&& cooked_ends_with_null
|
||||
&& let Some(prefix) = raw.strip_suffix("\\0")
|
||||
{
|
||||
Str::from_strs_array_in([prefix, "\\x00", &escaped, next_raw], ctx)
|
||||
} else {
|
||||
Str::from_strs_array_in([raw, &escaped, next_raw], ctx)
|
||||
};
|
||||
let new_cooked = if let (Some(cooked1), Some(cooked2)) =
|
||||
(quasi.value.cooked, next_quasi.as_ref().map(|q| q.value.cooked))
|
||||
{
|
||||
|
||||
@@ -1061,3 +1061,31 @@ fn dce_keeps_implicitly_observable_bindings() {
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dce_inline_template_literal_does_not_create_octal_escape() {
|
||||
// https://github.com/rolldown/rolldown/issues/10661
|
||||
test(
|
||||
r"export function makeKey(messageId, correlationId) { return `${messageId}\0${correlationId}\0${0}`; }",
|
||||
r"export function makeKey(messageId, correlationId) { return `${messageId}\0${correlationId}\x000`; }",
|
||||
);
|
||||
|
||||
// An escaped backslash before `0` is not a null escape, so this boundary is safe to fold.
|
||||
test(
|
||||
r"export function makeKey(messageId) { return `${messageId}\\0${0}`; }",
|
||||
r"export function makeKey(messageId) { return `${messageId}\\00`; }",
|
||||
);
|
||||
|
||||
// Empty folds can expose the same boundary directly or across adjacent expressions.
|
||||
test(
|
||||
r"export function makeKey(messageId) { return `${messageId}\0${''}0`; }",
|
||||
r"export function makeKey(messageId) { return `${messageId}\x000`; }",
|
||||
);
|
||||
test(
|
||||
r"export function makeKey(messageId) { return `${messageId}\0${''}${0}`; }",
|
||||
r"export function makeKey(messageId) { return `${messageId}\x000`; }",
|
||||
);
|
||||
|
||||
// Expressions with side effects must still remain interpolation expressions.
|
||||
test_same(r"export function makeKey(messageId) { return `${messageId}\0${sideEffect()}`; }");
|
||||
}
|
||||
Reference in new issue
Block a user