Skip to content

Commit

Permalink
refactor(formatter): preserve useless escapes (#5228)
Browse files Browse the repository at this point in the history
  • Loading branch information
Conaclos authored Mar 1, 2025
1 parent 4a08f79 commit 344a131
Show file tree
Hide file tree
Showing 10 changed files with 38 additions and 39 deletions.
7 changes: 7 additions & 0 deletions .changeset/preserve-useless-escapes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@biomejs/biome": major
---

[Prettier 3.4](https://prettier.io/blog/2024/11/26/3.4.0.html) introduced a change in their normalization process of string literals: it no longer unescapes useless escape sequences.
Biome now matches the new behavior of Prettier when formatting code.
This affects the JSON and JavaScript formatters.
4 changes: 2 additions & 2 deletions crates/biome_formatter/src/token/string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ pub fn normalize_string(
false
}
0xE2 => {
// Prserve escaping of Unicode characters U+2028 and U+2029
// Preserve escaping of Unicode characters U+2028 and U+2029
!(matches!(bytes.next(), Some((_, 0x80)))
&& matches!(bytes.next(), Some((_, 0xA8 | 0xA9))))
}
Expand All @@ -98,7 +98,7 @@ pub fn normalize_string(
// So we ignore the current slash and we continue
// to the next iteration
//
// We always unescape alternate quots regardless of `is_escape_preserved`.
// We always unescape alternate quotes regardless of `is_escape_preserved`.
escaped == alternate_quote
|| (escaped != preferred_quote && !is_escape_preserved)
}
Expand Down
15 changes: 5 additions & 10 deletions crates/biome_js_formatter/src/utils/string_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,11 @@ impl<'token> LiteralStringNormaliser<'token> {

fn normalise_string_literal(&self, string_information: StringInformation) -> Cow<'token, str> {
let preferred_quote = string_information.preferred_quote;
let polished_raw_content = self.normalize_string(&string_information);
let polished_raw_content = normalize_string(
self.raw_content(),
string_information.preferred_quote.into(),
true,
);

match polished_raw_content {
Cow::Borrowed(raw_content) => self.swap_quotes(raw_content, &string_information),
Expand All @@ -333,15 +337,6 @@ impl<'token> LiteralStringNormaliser<'token> {
}
}

fn normalize_string(&self, string_information: &StringInformation) -> Cow<'token, str> {
let is_escape_preserved = self.token.token.kind() == JSX_STRING_LITERAL;
normalize_string(
self.raw_content(),
string_information.preferred_quote.into(),
is_escape_preserved,
)
}

/// Returns the string without its quotes.
fn raw_content(&self) -> &'token str {
let content = self.get_token().text_trimmed();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,6 @@ Object.entries({
};

a = {
a: 1,
"\a": 1,
b: 2,
};
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,20 @@
// Unnecessary escapes.
("'");
('"');
("a");
("a");
("hola");
("hola");
("\a");
("\a");
("hol\a");
("hol\a");
("hol\\a (the a is not escaped)");
("hol\\a (the a is not escaped)");
("multiple a unnecessary a escapes");
("multiple a unnecessary a escapes");
("unnecessarily escaped character preceded by escaped backslash \\a");
("unnecessarily escaped character preceded by escaped backslash \\a");
("multiple \a unnecessary \a escapes");
("multiple \a unnecessary \a escapes");
("unnecessarily escaped character preceded by escaped backslash \\\a");
("unnecessarily escaped character preceded by escaped backslash \\\a");
("unescaped character preceded by two escaped backslashes \\\\a");
("unescaped character preceded by two escaped backslashes \\\\a");
("aa"); // consecutive unnecessarily escaped characters
("aa"); // consecutive unnecessarily escaped characters
("\a\a"); // consecutive unnecessarily escaped characters
("\a\a"); // consecutive unnecessarily escaped characters
("escaped \u2030 ‰ (should not stay escaped)");

// Meaningful escapes
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// https://github.com/babel/babel/pull/11852

"8", "9";
"\8", "\9";
() => {
"use strict";
"8", "9";
"\8", "\9";
};
2 changes: 1 addition & 1 deletion crates/biome_json_formatter/src/format_string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ impl Format<JsonFormatContext> for CleanedStringLiteralText<'_> {
let content = self.token.text_trimmed();
let raw_content = &content[1..content.len() - 1];

let text = match normalize_string(raw_content, Quote::Double, false) {
let text = match normalize_string(raw_content, Quote::Double, true) {
Cow::Borrowed(_) => Cow::Borrowed(content),
Cow::Owned(raw_content) => Cow::Owned(std::format!(
"{}{}{}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,14 @@ Object wrap: Preserve

```json
{
"/\\\"\uCAFE\uBABE\uAB98\uFCDE\ubcda\uef4A\b\f\n\r\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?": "A key can be any string",
"/ & /": "/\\\"\uCAFE\uBABE\uAB98\uFCDE\ubcda\uef4A\b\f\n\r\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?",
"slash": "/ & /"
"\/\\\"\uCAFE\uBABE\uAB98\uFCDE\ubcda\uef4A\b\f\n\r\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?": "A key can be any string",
"/ & \/": "\/\\\"\uCAFE\uBABE\uAB98\uFCDE\ubcda\uef4A\b\f\n\r\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?",
"slash": "/ & \/"
}
```

# Lines exceeding max width of 80 characters
```
2: "/\\\"\uCAFE\uBABE\uAB98\uFCDE\ubcda\uef4A\b\f\n\r\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?": "A key can be any string",
3: "/ & /": "/\\\"\uCAFE\uBABE\uAB98\uFCDE\ubcda\uef4A\b\f\n\r\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?",
2: "\/\\\"\uCAFE\uBABE\uAB98\uFCDE\ubcda\uef4A\b\f\n\r\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?": "A key can be any string",
3: "/ & \/": "\/\\\"\uCAFE\uBABE\uAB98\uFCDE\ubcda\uef4A\b\f\n\r\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?",
```
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"quote": "\"",
"backslash": "\\",
"controls": "\b\f\n\r\t",
"slash": "/ & /",
"slash": "/ & \/",
"alpha": "abcdefghijklmnopqrstuvwyz",
"ALPHA": "ABCDEFGHIJKLMNOPQRSTUVWYZ",
"digit": "0123456789",
Expand All @@ -43,7 +43,7 @@
"compact": [1, 2, 3, 4, 5, 6, 7],
"jsontext": "{\"object with 1 member\":[\"array with 1 element\"]}",
"quotes": "&#34; \u0022 %22 0x22 034 &#x22;",
"/\\\"\uCAFE\uBABE\uAB98\uFCDE\ubcda\uef4A\b\f\n\r\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?": "A key can be any string"
"\/\\\"\uCAFE\uBABE\uAB98\uFCDE\ubcda\uef4A\b\f\n\r\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?": "A key can be any string"
},
0.5,
98.6,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
source: crates/biome_formatter_test/src/snapshot_builder.rs
info: json/json/pass1.json
---

# Input

```json
Expand Down Expand Up @@ -107,7 +106,7 @@ info: json/json/pass1.json
"quote": "\"",
"backslash": "\\",
"controls": "\b\f\n\r\t",
"slash": "/ & /",
"slash": "/ & \/",
"alpha": "abcdefghijklmnopqrstuvwyz",
"ALPHA": "ABCDEFGHIJKLMNOPQRSTUVWYZ",
"digit": "0123456789",
Expand All @@ -131,7 +130,7 @@ info: json/json/pass1.json
"compact": [1, 2, 3, 4, 5, 6, 7],
"jsontext": "{\"object with 1 member\":[\"array with 1 element\"]}",
"quotes": "&#34; \u0022 %22 0x22 034 &#x22;",
"/\\\"\uCAFE\uBABE\uAB98\uFCDE\ubcda\uef4A\b\f\n\r\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?": "A key can be any string"
"\/\\\"\uCAFE\uBABE\uAB98\uFCDE\ubcda\uef4A\b\f\n\r\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?": "A key can be any string"
},
0.5,
98.6,
Expand All @@ -150,7 +149,5 @@ info: json/json/pass1.json

# Lines exceeding max width of 80 characters
```
46: "/\\\"\uCAFE\uBABE\uAB98\uFCDE\ubcda\uef4A\b\f\n\r\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?": "A key can be any string"
46: "\/\\\"\uCAFE\uBABE\uAB98\uFCDE\ubcda\uef4A\b\f\n\r\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?": "A key can be any string"
```

0 comments on commit 344a131

Please sign in to comment.