Skip to content

Commit

Permalink
Preserve backslash in raw string literal (#6152)
Browse files Browse the repository at this point in the history
  • Loading branch information
harupy authored Jul 31, 2023
1 parent a540933 commit 0274de1
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 11 deletions.
72 changes: 67 additions & 5 deletions crates/ruff_python_formatter/src/expression/string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,11 +203,17 @@ impl Format<PyFormatContext<'_>> 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 = preferred_quotes(raw_content, quotes, f.options().quote_style());
let is_raw_string = prefix.is_raw_string();
let preferred_quotes = if is_raw_string {
preferred_quotes_raw(raw_content, quotes, f.options().quote_style())
} else {
preferred_quotes(raw_content, quotes, f.options().quote_style())
};

write!(f, [prefix, preferred_quotes])?;

let (normalized, contains_newlines) = normalize_string(raw_content, preferred_quotes);
let (normalized, contains_newlines) =
normalize_string(raw_content, preferred_quotes, is_raw_string);

match normalized {
Cow::Borrowed(_) => {
Expand All @@ -223,7 +229,7 @@ impl Format<PyFormatContext<'_>> for FormatStringPart {
}

bitflags! {
#[derive(Copy, Clone, Debug)]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub(super) struct StringPrefix: u8 {
const UNICODE = 0b0000_0001;
/// `r"test"`
Expand Down Expand Up @@ -264,6 +270,10 @@ impl StringPrefix {
pub(super) const fn text_len(self) -> TextSize {
TextSize::new(self.bits().count_ones())
}

pub(super) const fn is_raw_string(self) -> bool {
matches!(self, StringPrefix::RAW | StringPrefix::RAW_UPPER)
}
}

impl Format<PyFormatContext<'_>> for StringPrefix {
Expand All @@ -290,6 +300,54 @@ impl Format<PyFormatContext<'_>> for StringPrefix {
}
}

/// Detects the preferred quotes for raw string `input`.
/// The configured quote style is preferred unless `input` contains unescaped quotes of the
/// configured style. For example, `r"foo"` is preferred over `r'foo'` if the configured
/// quote style is double quotes.
fn preferred_quotes_raw(
input: &str,
quotes: StringQuotes,
configured_style: QuoteStyle,
) -> StringQuotes {
let configured_quote_char = configured_style.as_char();
let mut chars = input.chars().peekable();
let contains_unescaped_configured_quotes = loop {
match chars.next() {
Some('\\') => {
// Ignore escaped characters
chars.next();
}
// `"` or `'`
Some(c) if c == configured_quote_char => {
if !quotes.triple {
break true;
}

if chars.peek() == Some(&configured_quote_char) {
// `""` or `''`
chars.next();

if chars.peek() == Some(&configured_quote_char) {
// `"""` or `'''`
break true;
}
}
}
Some(_) => continue,
None => break false,
}
};

StringQuotes {
triple: quotes.triple,
style: if contains_unescaped_configured_quotes {
quotes.style
} else {
configured_style
},
}
}

/// Detects the preferred quotes for `input`.
/// * single quoted strings: The preferred quote style is the one that requires less escape sequences.
/// * triple quoted strings: Use double quotes except the string contains a sequence of `"""`.
Expand Down Expand Up @@ -434,7 +492,11 @@ impl Format<PyFormatContext<'_>> for StringQuotes {
/// with the provided `style`.
///
/// Returns the normalized string and whether it contains new lines.
fn normalize_string(input: &str, quotes: StringQuotes) -> (Cow<str>, ContainsNewlines) {
fn normalize_string(
input: &str,
quotes: StringQuotes,
is_raw: bool,
) -> (Cow<str>, ContainsNewlines) {
// The normalized string if `input` is not yet normalized.
// `output` must remain empty if `input` is already normalized.
let mut output = String::new();
Expand Down Expand Up @@ -467,7 +529,7 @@ fn normalize_string(input: &str, quotes: StringQuotes) -> (Cow<str>, ContainsNew
newlines = ContainsNewlines::Yes;
} else if c == '\n' {
newlines = ContainsNewlines::Yes;
} else if !quotes.triple {
} else if !quotes.triple && !is_raw {
if c == '\\' {
if let Some(next) = input.as_bytes().get(index + 1).copied().map(char::from) {
#[allow(clippy::if_same_then_else)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,11 @@ f"\"{a}\"{'hello' * b}\"{c}\""
+f"NOT_YET_IMPLEMENTED_ExprJoinedStr"
+f"NOT_YET_IMPLEMENTED_ExprJoinedStr"
r"raw string ftw"
-r"Date d\'expiration:(.*)"
+r"Date d'expiration:(.*)"
r"Date d\'expiration:(.*)"
r'Tricky "quote'
-r"Not-so-tricky \"quote"
r"Not-so-tricky \"quote"
-rf"{yay}"
-"\nThe \"quick\"\nbrown fox\njumps over\nthe 'lazy' dog.\n"
+r'Not-so-tricky "quote'
+f"NOT_YET_IMPLEMENTED_ExprJoinedStr"
+"\n\
+The \"quick\"\n\
Expand Down Expand Up @@ -147,9 +145,9 @@ f"NOT_YET_IMPLEMENTED_ExprJoinedStr"
f"NOT_YET_IMPLEMENTED_ExprJoinedStr"
f"NOT_YET_IMPLEMENTED_ExprJoinedStr"
r"raw string ftw"
r"Date d'expiration:(.*)"
r"Date d\'expiration:(.*)"
r'Tricky "quote'
r'Not-so-tricky "quote'
r"Not-so-tricky \"quote"
f"NOT_YET_IMPLEMENTED_ExprJoinedStr"
"\n\
The \"quick\"\n\
Expand Down

0 comments on commit 0274de1

Please sign in to comment.