diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/carriage_return/.editorconfig b/crates/ruff_python_formatter/resources/test/fixtures/ruff/carriage_return/.editorconfig new file mode 100644 index 0000000000000..cafa748cf3f88 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/carriage_return/.editorconfig @@ -0,0 +1,2 @@ +[*.py] +end_of_line = crlf diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/carriage_return/.gitattributes b/crates/ruff_python_formatter/resources/test/fixtures/ruff/carriage_return/.gitattributes new file mode 100644 index 0000000000000..0c42f3cc294d6 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/carriage_return/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=crlf diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/carriage_return/string.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/carriage_return/string.py new file mode 100644 index 0000000000000..45f9dacc386f0 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/carriage_return/string.py @@ -0,0 +1,6 @@ +'This string will not include \ +backslashes or newline characters.' + +"""Multiline +String \" +""" diff --git a/crates/ruff_python_formatter/src/expression/string.rs b/crates/ruff_python_formatter/src/expression/string.rs index 0362aff00509f..7b8369d0883fa 100644 --- a/crates/ruff_python_formatter/src/expression/string.rs +++ b/crates/ruff_python_formatter/src/expression/string.rs @@ -199,12 +199,11 @@ impl Format> for FormatStringPart { let raw_content_range = relative_raw_content_range + self.part_range.start(); let raw_content = &string_content[relative_raw_content_range]; - let (preferred_quotes, contains_newlines) = - preferred_quotes(raw_content, quotes, f.options().quote_style()); + let preferred_quotes = preferred_quotes(raw_content, quotes, f.options().quote_style()); write!(f, [prefix, preferred_quotes])?; - let normalized = normalize_quotes(raw_content, preferred_quotes); + let (normalized, contains_newlines) = normalize_string(raw_content, preferred_quotes); match normalized { Cow::Borrowed(_) => { @@ -294,9 +293,7 @@ fn preferred_quotes( input: &str, quotes: StringQuotes, configured_style: QuoteStyle, -) -> (StringQuotes, ContainsNewlines) { - let mut contains_newlines = ContainsNewlines::No; - +) -> StringQuotes { let preferred_style = if quotes.triple { // True if the string contains a triple quote sequence of the configured quote style. let mut uses_triple_quotes = false; @@ -305,7 +302,6 @@ fn preferred_quotes( while let Some(c) = chars.next() { let configured_quote_char = configured_style.as_char(); match c { - '\n' | '\r' => contains_newlines = ContainsNewlines::Yes, '\\' => { if matches!(chars.peek(), Some('"' | '\\')) { chars.next(); @@ -358,10 +354,6 @@ fn preferred_quotes( double_quotes += 1; } - '\n' | '\r' => { - contains_newlines = ContainsNewlines::Yes; - } - _ => continue, } } @@ -384,13 +376,10 @@ fn preferred_quotes( } }; - ( - StringQuotes { - triple: quotes.triple, - style: preferred_style, - }, - contains_newlines, - ) + StringQuotes { + triple: quotes.triple, + style: preferred_style, + } } #[derive(Copy, Clone, Debug)] @@ -435,30 +424,56 @@ impl Format> for StringQuotes { /// Adds the necessary quote escapes and removes unnecessary escape sequences when quoting `input` /// with the provided `style`. -fn normalize_quotes(input: &str, quotes: StringQuotes) -> Cow { - if quotes.triple { - Cow::Borrowed(input) - } else { - // The normalized string if `input` is not yet normalized. - // `output` must remain empty if `input` is already normalized. - let mut output = String::new(); - // Tracks the last index of `input` that has been written to `output`. - // If `last_index` is `0` at the end, then the input is already normalized and can be returned as is. - let mut last_index = 0; - - let style = quotes.style; - let preferred_quote = style.as_char(); - let opposite_quote = style.invert().as_char(); - - let mut chars = input.char_indices(); +/// +/// Returns the normalized string and whether it contains new lines. +fn normalize_string(input: &str, quotes: StringQuotes) -> (Cow, ContainsNewlines) { + // The normalized string if `input` is not yet normalized. + // `output` must remain empty if `input` is already normalized. + let mut output = String::new(); + // Tracks the last index of `input` that has been written to `output`. + // If `last_index` is `0` at the end, then the input is already normalized and can be returned as is. + let mut last_index = 0; + + let mut newlines = ContainsNewlines::No; + + let style = quotes.style; + let preferred_quote = style.as_char(); + let opposite_quote = style.invert().as_char(); + + let mut chars = input.char_indices(); + + while let Some((index, c)) = chars.next() { + if c == '\r' { + output.push_str(&input[last_index..index]); + + // Skip over the '\r' character, keep the `\n` + if input.as_bytes().get(index + 1).copied() == Some(b'\n') { + chars.next(); + } + // Replace the `\r` with a `\n` + else { + output.push('\n'); + } - while let Some((index, c)) = chars.next() { + last_index = index + '\r'.len_utf8(); + newlines = ContainsNewlines::Yes; + } else if c == '\n' { + newlines = ContainsNewlines::Yes; + } else if !quotes.triple { if c == '\\' { - if let Some((_, next)) = chars.next() { + if let Some(next) = input.as_bytes().get(index + 1).copied().map(char::from) { + #[allow(clippy::if_same_then_else)] if next == opposite_quote { // Remove the escape by ending before the backslash and starting again with the quote + chars.next(); output.push_str(&input[last_index..index]); last_index = index + '\\'.len_utf8(); + } else if next == preferred_quote { + // Quote is already escaped, skip over it. + chars.next(); + } else if next == '\\' { + // Skip over escaped backslashes + chars.next(); } } } else if c == preferred_quote { @@ -469,12 +484,14 @@ fn normalize_quotes(input: &str, quotes: StringQuotes) -> Cow { last_index = index + preferred_quote.len_utf8(); } } - - if last_index == 0 { - Cow::Borrowed(input) - } else { - output.push_str(&input[last_index..]); - Cow::Owned(output) - } } + + let normalized = if last_index == 0 { + Cow::Borrowed(input) + } else { + output.push_str(&input[last_index..]); + Cow::Owned(output) + }; + + (normalized, newlines) } diff --git a/crates/ruff_python_formatter/tests/snapshots/format@carriage_return__string.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@carriage_return__string.py.snap new file mode 100644 index 0000000000000..522538378af1f --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@carriage_return__string.py.snap @@ -0,0 +1,26 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/carriage_return/string.py +--- +## Input +```py +'This string will not include \ +backslashes or newline characters.' + +"""Multiline +String \" +""" +``` + +## Output +```py +"This string will not include \ +backslashes or newline characters." + +"""Multiline +String \" +""" +``` + + +