mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-01 14:21:53 +00:00
Normalize '\r' in string literals to '\n'
<!-- Thank you for contributing to Ruff! To help us out with reviewing, please consider the following: - Does this pull request include a summary of the change? (See below.) - Does this pull request include a descriptive title? - Does this pull request include references to any relevant issues? --> ## Summary This PR normalizes line endings inside of strings to `\n` as required by the printer. <!-- What's the purpose of the change? What does it do, and why? --> ## Test Plan I added a new test using `\r\n` and ran the ecosystem check. There are no remaining end of line panics. https://gist.github.com/MichaReiser/8f36b1391ca7b48475b3a4f592d74ff4 <!-- How was it tested? -->
This commit is contained in:
parent
dc65007fe9
commit
f9129e435a
5 changed files with 93 additions and 41 deletions
2
crates/ruff_python_formatter/resources/test/fixtures/ruff/carriage_return/.editorconfig
vendored
Normal file
2
crates/ruff_python_formatter/resources/test/fixtures/ruff/carriage_return/.editorconfig
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
[*.py]
|
||||||
|
end_of_line = crlf
|
1
crates/ruff_python_formatter/resources/test/fixtures/ruff/carriage_return/.gitattributes
vendored
Normal file
1
crates/ruff_python_formatter/resources/test/fixtures/ruff/carriage_return/.gitattributes
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
* text=auto eol=crlf
|
6
crates/ruff_python_formatter/resources/test/fixtures/ruff/carriage_return/string.py
vendored
Normal file
6
crates/ruff_python_formatter/resources/test/fixtures/ruff/carriage_return/string.py
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
'This string will not include \
|
||||||
|
backslashes or newline characters.'
|
||||||
|
|
||||||
|
"""Multiline
|
||||||
|
String \"
|
||||||
|
"""
|
|
@ -199,12 +199,11 @@ impl Format<PyFormatContext<'_>> for FormatStringPart {
|
||||||
let raw_content_range = relative_raw_content_range + self.part_range.start();
|
let raw_content_range = relative_raw_content_range + self.part_range.start();
|
||||||
|
|
||||||
let raw_content = &string_content[relative_raw_content_range];
|
let raw_content = &string_content[relative_raw_content_range];
|
||||||
let (preferred_quotes, contains_newlines) =
|
let preferred_quotes = preferred_quotes(raw_content, quotes, f.options().quote_style());
|
||||||
preferred_quotes(raw_content, quotes, f.options().quote_style());
|
|
||||||
|
|
||||||
write!(f, [prefix, preferred_quotes])?;
|
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 {
|
match normalized {
|
||||||
Cow::Borrowed(_) => {
|
Cow::Borrowed(_) => {
|
||||||
|
@ -294,9 +293,7 @@ fn preferred_quotes(
|
||||||
input: &str,
|
input: &str,
|
||||||
quotes: StringQuotes,
|
quotes: StringQuotes,
|
||||||
configured_style: QuoteStyle,
|
configured_style: QuoteStyle,
|
||||||
) -> (StringQuotes, ContainsNewlines) {
|
) -> StringQuotes {
|
||||||
let mut contains_newlines = ContainsNewlines::No;
|
|
||||||
|
|
||||||
let preferred_style = if quotes.triple {
|
let preferred_style = if quotes.triple {
|
||||||
// True if the string contains a triple quote sequence of the configured quote style.
|
// True if the string contains a triple quote sequence of the configured quote style.
|
||||||
let mut uses_triple_quotes = false;
|
let mut uses_triple_quotes = false;
|
||||||
|
@ -305,7 +302,6 @@ fn preferred_quotes(
|
||||||
while let Some(c) = chars.next() {
|
while let Some(c) = chars.next() {
|
||||||
let configured_quote_char = configured_style.as_char();
|
let configured_quote_char = configured_style.as_char();
|
||||||
match c {
|
match c {
|
||||||
'\n' | '\r' => contains_newlines = ContainsNewlines::Yes,
|
|
||||||
'\\' => {
|
'\\' => {
|
||||||
if matches!(chars.peek(), Some('"' | '\\')) {
|
if matches!(chars.peek(), Some('"' | '\\')) {
|
||||||
chars.next();
|
chars.next();
|
||||||
|
@ -358,10 +354,6 @@ fn preferred_quotes(
|
||||||
double_quotes += 1;
|
double_quotes += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
'\n' | '\r' => {
|
|
||||||
contains_newlines = ContainsNewlines::Yes;
|
|
||||||
}
|
|
||||||
|
|
||||||
_ => continue,
|
_ => continue,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -384,13 +376,10 @@ fn preferred_quotes(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
(
|
StringQuotes {
|
||||||
StringQuotes {
|
triple: quotes.triple,
|
||||||
triple: quotes.triple,
|
style: preferred_style,
|
||||||
style: preferred_style,
|
}
|
||||||
},
|
|
||||||
contains_newlines,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
@ -435,30 +424,56 @@ impl Format<PyFormatContext<'_>> for StringQuotes {
|
||||||
|
|
||||||
/// Adds the necessary quote escapes and removes unnecessary escape sequences when quoting `input`
|
/// Adds the necessary quote escapes and removes unnecessary escape sequences when quoting `input`
|
||||||
/// with the provided `style`.
|
/// with the provided `style`.
|
||||||
fn normalize_quotes(input: &str, quotes: StringQuotes) -> Cow<str> {
|
///
|
||||||
if quotes.triple {
|
/// Returns the normalized string and whether it contains new lines.
|
||||||
Cow::Borrowed(input)
|
fn normalize_string(input: &str, quotes: StringQuotes) -> (Cow<str>, ContainsNewlines) {
|
||||||
} else {
|
// The normalized string if `input` is not yet normalized.
|
||||||
// The normalized string if `input` is not yet normalized.
|
// `output` must remain empty if `input` is already normalized.
|
||||||
// `output` must remain empty if `input` is already normalized.
|
let mut output = String::new();
|
||||||
let mut output = String::new();
|
// Tracks the last index of `input` that has been written to `output`.
|
||||||
// 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.
|
||||||
// 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 last_index = 0;
|
|
||||||
|
|
||||||
let style = quotes.style;
|
let mut newlines = ContainsNewlines::No;
|
||||||
let preferred_quote = style.as_char();
|
|
||||||
let opposite_quote = style.invert().as_char();
|
|
||||||
|
|
||||||
let mut chars = input.char_indices();
|
let style = quotes.style;
|
||||||
|
let preferred_quote = style.as_char();
|
||||||
|
let opposite_quote = style.invert().as_char();
|
||||||
|
|
||||||
while let Some((index, c)) = chars.next() {
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
last_index = index + '\r'.len_utf8();
|
||||||
|
newlines = ContainsNewlines::Yes;
|
||||||
|
} else if c == '\n' {
|
||||||
|
newlines = ContainsNewlines::Yes;
|
||||||
|
} else if !quotes.triple {
|
||||||
if c == '\\' {
|
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 {
|
if next == opposite_quote {
|
||||||
// Remove the escape by ending before the backslash and starting again with the quote
|
// Remove the escape by ending before the backslash and starting again with the quote
|
||||||
|
chars.next();
|
||||||
output.push_str(&input[last_index..index]);
|
output.push_str(&input[last_index..index]);
|
||||||
last_index = index + '\\'.len_utf8();
|
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 {
|
} else if c == preferred_quote {
|
||||||
|
@ -469,12 +484,14 @@ fn normalize_quotes(input: &str, quotes: StringQuotes) -> Cow<str> {
|
||||||
last_index = index + preferred_quote.len_utf8();
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 \"
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue