Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions crates/oxc_formatter/src/formatter/builders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,8 +297,7 @@ impl std::fmt::Debug for Token {

/// Creates a text from a dynamic string and a range of the input source
pub fn text(text: &str) -> Text<'_> {
// FIXME
// debug_assert_no_newlines(text);
debug_assert_no_cr_line_break(text);
Text { text, width: None }
}

Expand Down Expand Up @@ -334,12 +333,16 @@ impl std::fmt::Debug for Text<'_> {
}
}

/// Debug assert that the given text contains no `\r` line terminator characters.
//
// `#[inline(always)]` because this is a no-op in release mode
#[inline(always)]
#[expect(clippy::inline_always)]
#[track_caller]
#[expect(unused)]
fn debug_assert_no_newlines(text: &str) {
fn debug_assert_no_cr_line_break(text: &str) {
debug_assert!(
!text.contains('\r'),
"The content '{text}' contains an unsupported '\\r' line terminator character but text must only use line feeds '\\n' as line separator. Use '\\n' instead of '\\r' and '\\r\\n' to insert a line break in strings."
"The content `{text}` contains an unsupported `\\r` line terminator character but text must only use line feeds `\\n` as line separator. Use `\\n` instead of `\\r` and `\\r\\n` to insert a line break in strings."
);
}

Expand Down
44 changes: 33 additions & 11 deletions crates/oxc_formatter/src/utils/string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ impl<'a> Format<'a> for FormatLiteralStringToken<'a> {
///
/// - escaping `preferred_quote`
/// - unescape alternate quotes of `preferred_quote` if `quotes_will_change`
/// - normalize the new lines by replacing `\r\n` with `\n`.
/// - normalize the new lines by replacing `\r\n` and `\r` with `\n`.
///
/// The function allocates a new string only if at least one change is performed.
///
Expand All @@ -332,31 +332,44 @@ pub fn normalize_string(
let preferred_quote = preferred_quote.as_byte();
let mut reduced_string = String::new();
let mut copy_start = 0;
let mut bytes = raw_content.bytes().enumerate();
let mut bytes = raw_content.bytes().enumerate().peekable();
while let Some((byte_index, byte)) = bytes.next() {
match byte {
// If the next character is escaped
b'\\' => {
if let Some((escaped_index, escaped)) = bytes.next() {
if let Some(&(escaped_index, escaped)) = bytes.peek() {
if escaped == b'\r' {
// If we encounter the sequence "\r\n", then skip '\r'
if let Some((next_byte_index, b'\n')) = bytes.next() {
reduced_string.push_str(&raw_content[copy_start..escaped_index]);
copy_start = next_byte_index;
bytes.next(); // consume the \r
// Copy up to (not including) the \r
reduced_string.push_str(&raw_content[copy_start..escaped_index]);
if bytes.next_if(|(_, b)| *b == b'\n').is_some() {
// \\\r\n -> keep \\ and \n, skip \r
// The \n will be included when we copy from copy_start
} else {
// \\\r -> convert \r to \n
reduced_string.push('\n');
}
copy_start = escaped_index + 1;
} else if quotes_will_change && escaped == alternate_quote {
bytes.next(); // consume the escaped character
// Unescape alternate quotes if quotes are changing
reduced_string.push_str(&raw_content[copy_start..byte_index]);
copy_start = escaped_index;
} else {
bytes.next(); // consume the escaped character
}
}
}
// If we encounter the sequence "\r\n", then skip '\r'
// Normalize \r\n and \r to \n
b'\r' => {
if let Some((next_byte_index, b'\n')) = bytes.next() {
reduced_string.push_str(&raw_content[copy_start..byte_index]);
copy_start = next_byte_index;
reduced_string.push_str(&raw_content[copy_start..byte_index]);
if bytes.next_if(|(_, b)| *b == b'\n').is_some() {
// \r\n -> skip \r, the \n will be included when we copy from copy_start
} else {
// Single \r -> convert to \n
reduced_string.push('\n');
}
copy_start = byte_index + 1;
}
_ => {
// If we encounter a preferred quote and it's not escaped, we have to replace it with
Expand Down Expand Up @@ -386,9 +399,18 @@ mod tests {

#[test]
fn normalize_newline() {
// \n unchanged
assert_eq!(normalize_string("a\nb", QuoteStyle::Double, true), "a\nb");
// \r\n -> \n
assert_eq!(normalize_string("a\r\nb", QuoteStyle::Double, true), "a\nb");
// \r -> \n (single CR)
assert_eq!(normalize_string("a\rb", QuoteStyle::Double, true), "a\nb");
assert_eq!(normalize_string("a\r", QuoteStyle::Double, true), "a\n");
assert_eq!(normalize_string("\rb", QuoteStyle::Double, true), "\nb");
// escaped \r\n -> escaped \n
assert_eq!(normalize_string("a\\\r\nb", QuoteStyle::Double, true), "a\\\nb");
// escaped \r -> escaped \n (single CR)
assert_eq!(normalize_string("a\\\rb", QuoteStyle::Double, true), "a\\\nb");
}

#[test]
Expand Down
Loading