mirror of
https://github.com/aphrody-labs/oxc.git
synced 2026-10-14 22:29:17 +00:00
fix(ecmascript): do not treat lone-surrogate strings as constants (#25084)
The value of a StringLiteral and the cooked value of a template quasi encode lone surrogates with `\u{FFFD}` escapes and mark the node with a `lone_surrogates` flag. ToJsString and constant evaluation returned the encoded text, and consumers materialized it into new string literals without the flag, printing the escape encoding as literal text: `"\ud800" + "y"` folded to a string containing a literal `d800`.
Return `None` for such strings and template quasis so folds keep the original nodes.
```js
// source
console.log("\ud800" + "y"); // node prints: �y
// before (miscompiled)
console.log(`�d800y`); // node prints: �d800y
// after
console.log(`\ud800`+`y`); // node prints: �y
```
This commit is contained in:
1 parent
fb402b010c
commit
8a47ff3743
4 files changed
+25
No files matched your search
@@ -162,6 +162,13 @@ impl<'a> ConstantEvaluation<'a> for Expression<'a> {
|
||||
Expression::BooleanLiteral(lit) => Some(ConstantValue::Boolean(lit.value)),
|
||||
Expression::BigIntLiteral(lit) => lit.to_big_int(ctx).map(ConstantValue::BigInt),
|
||||
Expression::StringLiteral(lit) => {
|
||||
// The value of a string with lone surrogates encodes them with
|
||||
// `\u{FFFD}` escapes. Consumers materialize the returned value
|
||||
// into new string literals without the `lone_surrogates` flag,
|
||||
// which would print the escape encoding as literal text.
|
||||
if lit.lone_surrogates {
|
||||
return None;
|
||||
}
|
||||
Some(ConstantValue::String(Cow::Borrowed(lit.value.as_str())))
|
||||
}
|
||||
Expression::StaticMemberExpression(e) => e.evaluate_value_to(ctx, target_ty),
|
||||
|
||||
@@ -53,6 +53,13 @@ impl<'a> ToJsString<'a> for ArrayExpressionElement<'a> {
|
||||
|
||||
impl<'a> ToJsString<'a> for StringLiteral<'a> {
|
||||
fn to_js_string(&self, _ctx: &impl GlobalContext<'a>) -> Option<Cow<'a, str>> {
|
||||
// The value of a string with lone surrogates encodes them with
|
||||
// `\u{FFFD}` escapes. Consumers build new string literals from the
|
||||
// returned value without carrying the `lone_surrogates` flag over,
|
||||
// which would print the escape encoding as literal text.
|
||||
if self.lone_surrogates {
|
||||
return None;
|
||||
}
|
||||
Some(Cow::Borrowed(self.value.as_str()))
|
||||
}
|
||||
}
|
||||
@@ -61,6 +68,11 @@ impl<'a> ToJsString<'a> for TemplateLiteral<'a> {
|
||||
fn to_js_string(&self, ctx: &impl GlobalContext<'a>) -> Option<Cow<'a, str>> {
|
||||
let mut str = String::new();
|
||||
for (i, quasi) in self.quasis.iter().enumerate() {
|
||||
// See the `StringLiteral` impl: the cooked value encodes lone
|
||||
// surrogates with `\u{FFFD}` escapes.
|
||||
if quasi.lone_surrogates {
|
||||
return None;
|
||||
}
|
||||
str.push_str(quasi.value.cooked.as_ref()?);
|
||||
|
||||
if i < self.expressions.len() {
|
||||
|
||||
@@ -927,6 +927,9 @@ fn test_fold_bit_shifts() {
|
||||
#[test]
|
||||
fn test_string_add() {
|
||||
fold("x = 'a' + 'bc'", "x = 'abc'");
|
||||
// Lone surrogates are stored escaped in the string value; folding would
|
||||
// materialize the escape encoding as literal text.
|
||||
fold_same("x = '\\ud800' + 'y'");
|
||||
fold("x = 'a' + 5", "x = 'a5'");
|
||||
fold("x = 5 + 'a'", "x = '5a'");
|
||||
fold("x = 'a' + 5n", "x = 'a5'");
|
||||
|
||||
@@ -328,6 +328,9 @@ fn test_string_array_splitting() {
|
||||
#[test]
|
||||
fn test_template_string_to_string() {
|
||||
test("x = `abcde`", "x = 'abcde'");
|
||||
// Lone surrogates are stored escaped in the cooked value; converting to a
|
||||
// string literal would materialize the escape encoding as literal text.
|
||||
test_same("x = `\\ud800y`");
|
||||
test("x = `ab cd ef`", "x = 'ab cd ef'");
|
||||
test_same("x = `hello ${name}`");
|
||||
test_same("tag `hello ${name}`");
|
||||
|
||||
Reference in new issue
Block a user