diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.options.json b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.options.json new file mode 100644 index 0000000000..7d6d0512c2 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.options.json @@ -0,0 +1,8 @@ +[ + { + "quote_style": "double" + }, + { + "quote_style": "single" + } +] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.py index 7ae9032c80..7ddb0f1cdb 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.py @@ -12,7 +12,7 @@ # Prefer double quotes for string with equal amount of single and double quotes '" \' " " \'\'' -"' \" '' \" \" '" +"' \" '' \" \"" "\\' \"\"" '\\\' ""' @@ -47,6 +47,16 @@ String "" String """ ''' +'''Multiline +String "''' + +"""Multiline +String ''' +""" + +"""Multiline +String '""" + '''Multiline String \"\"\" ''' diff --git a/crates/ruff_python_formatter/src/expression/string.rs b/crates/ruff_python_formatter/src/expression/string.rs index 116226fc1d..9d27ab87c1 100644 --- a/crates/ruff_python_formatter/src/expression/string.rs +++ b/crates/ruff_python_formatter/src/expression/string.rs @@ -58,7 +58,8 @@ 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); + let (preferred_quotes, contains_newlines) = + preferred_quotes(raw_content, quotes, f.options().quote_style()); write!(f, [prefix, preferred_quotes])?; @@ -148,14 +149,20 @@ impl Format> for StringPrefix { /// 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 `"""`. -fn preferred_quotes(input: &str, quotes: StringQuotes) -> (StringQuotes, ContainsNewlines) { +fn preferred_quotes( + input: &str, + quotes: StringQuotes, + configured_style: QuoteStyle, +) -> (StringQuotes, ContainsNewlines) { let mut contains_newlines = ContainsNewlines::No; let preferred_style = if quotes.triple { - let mut use_single_quotes = false; + // True if the string contains a triple quote sequence of the configured quote style. + let mut uses_triple_quotes = false; let mut chars = input.chars().peekable(); while let Some(c) = chars.next() { + let configured_quote_char = configured_style.as_char(); match c { '\n' | '\r' => contains_newlines = ContainsNewlines::Yes, '\\' => { @@ -163,24 +170,25 @@ fn preferred_quotes(input: &str, quotes: StringQuotes) -> (StringQuotes, Contain chars.next(); } } - '"' => { + // `"` or `'` + c if c == configured_quote_char => { match chars.peek().copied() { - Some('"') => { - // `""` + Some(c) if c == configured_quote_char => { + // `""` or `''` chars.next(); - if chars.peek().copied() == Some('"') { - // `"""` + if chars.peek().copied() == Some(configured_quote_char) { + // `"""` or `'''` chars.next(); - use_single_quotes = true; + uses_triple_quotes = true; } } Some(_) => { - // Single quote, this is ok + // A single quote char, this is ok } None => { // Trailing quote at the end of the comment - use_single_quotes = true; + uses_triple_quotes = true; } } } @@ -188,10 +196,12 @@ fn preferred_quotes(input: &str, quotes: StringQuotes) -> (StringQuotes, Contain } } - if use_single_quotes { - QuoteStyle::Single + if uses_triple_quotes { + // String contains a triple quote sequence of the configured quote style. + // Keep the existing quote style. + quotes.style } else { - QuoteStyle::Double + configured_style } } else { let mut single_quotes = 0u32; @@ -215,10 +225,21 @@ fn preferred_quotes(input: &str, quotes: StringQuotes) -> (StringQuotes, Contain } } - if double_quotes > single_quotes { - QuoteStyle::Single - } else { - QuoteStyle::Double + match configured_style { + QuoteStyle::Single => { + if single_quotes > double_quotes { + QuoteStyle::Double + } else { + QuoteStyle::Single + } + } + QuoteStyle::Double => { + if double_quotes > single_quotes { + QuoteStyle::Single + } else { + QuoteStyle::Double + } + } } }; @@ -286,7 +307,7 @@ fn normalize_quotes(input: &str, quotes: StringQuotes) -> Cow { let style = quotes.style; let preferred_quote = style.as_char(); - let opposite_quote = style.opposite().as_char(); + let opposite_quote = style.invert().as_char(); let mut chars = input.char_indices(); diff --git a/crates/ruff_python_formatter/src/options.rs b/crates/ruff_python_formatter/src/options.rs index e886a30b60..65bcf65552 100644 --- a/crates/ruff_python_formatter/src/options.rs +++ b/crates/ruff_python_formatter/src/options.rs @@ -109,7 +109,7 @@ impl QuoteStyle { } #[must_use] - pub const fn opposite(self) -> QuoteStyle { + pub const fn invert(self) -> QuoteStyle { match self { QuoteStyle::Single => QuoteStyle::Double, QuoteStyle::Double => QuoteStyle::Single, diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap index 7943becf02..6dd87f155f 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap @@ -18,7 +18,7 @@ input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression # Prefer double quotes for string with equal amount of single and double quotes '" \' " " \'\'' -"' \" '' \" \" '" +"' \" '' \" \"" "\\' \"\"" '\\\' ""' @@ -53,12 +53,30 @@ String "" String """ ''' +'''Multiline +String "''' + +"""Multiline +String ''' +""" + +"""Multiline +String '""" + '''Multiline String \"\"\" ''' ``` -## Output +## Outputs +### Output 1 +``` +indent-style = Spaces, size: 4 +line-width = 88 +quote-style = Double +magic-trailing-comma = Respect +``` + ```py "' test" '" test' @@ -74,7 +92,7 @@ String \"\"\" # Prefer double quotes for string with equal amount of single and double quotes "\" ' \" \" ''" -"' \" '' \" \" '" +"' \" '' \" \"" '\\\' ""' '\\\' ""' @@ -109,10 +127,94 @@ String "" String """ ''' +'''Multiline +String "''' + +"""Multiline +String ''' +""" + +"""Multiline +String '""" + """Multiline String \"\"\" """ ``` +### Output 2 +``` +indent-style = Spaces, size: 4 +line-width = 88 +quote-style = Single +magic-trailing-comma = Respect +``` + +```py +"' test" +'" test' + +'" test' +"' test" + +# Prefer single quotes for string with more double quotes +'\' " " \'\' " " \'' + +# Prefer double quotes for string with more single quotes +'\' " " \'\' " " \'' + +# Prefer double quotes for string with equal amount of single and double quotes +'" \' " " \'\'' +'\' " \'\' " "' + +'\\\' ""' +'\\\' ""' + + +'Test' +'Test' + +r'Test' +R'Test' + +'This string will not include \ +backslashes or newline characters.' + +if True: + 'This string will not include \ + backslashes or newline characters.' + +'''Multiline +String \" +''' + +'''Multiline +String \' +''' + +'''Multiline +String "" +''' + +'''Multiline +String """ +''' + +'''Multiline +String "''' + +"""Multiline +String ''' +""" + +"""Multiline +String '""" + +'''Multiline +String \"\"\" +''' +``` + +