Add "preserve" quote-style to mimic Black's skip-string-normalization (#8822)

Co-authored-by: Micha Reiser <micha@reiser.io>
This commit is contained in:
Samuel Cormier-Iijima 2023-12-07 18:59:22 -05:00 committed by GitHub
parent 6bbabceead
commit 2414298289
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 482 additions and 96 deletions

View file

@ -1,5 +1,6 @@
use crate::comments::Comments;
use crate::{PyFormatOptions, QuoteStyle};
use crate::expression::string::QuoteChar;
use crate::PyFormatOptions;
use ruff_formatter::{Buffer, FormatContext, GroupId, SourceCode};
use ruff_source_file::Locator;
use std::fmt::{Debug, Formatter};
@ -12,14 +13,14 @@ pub struct PyFormatContext<'a> {
comments: Comments<'a>,
node_level: NodeLevel,
/// Set to a non-None value when the formatter is running on a code
/// snippet within a docstring. The value should be the quote style of the
/// snippet within a docstring. The value should be the quote character of the
/// docstring containing the code snippet.
///
/// Various parts of the formatter may inspect this state to change how it
/// works. For example, multi-line strings will always be written with a
/// quote style that is inverted from the one here in order to ensure that
/// the formatted Python code will be valid.
docstring: Option<QuoteStyle>,
docstring: Option<QuoteChar>,
}
impl<'a> PyFormatContext<'a> {
@ -57,20 +58,20 @@ impl<'a> PyFormatContext<'a> {
/// Returns a non-None value only if the formatter is running on a code
/// snippet within a docstring.
///
/// The quote style returned corresponds to the quoting used for the
/// The quote character returned corresponds to the quoting used for the
/// docstring containing the code snippet currently being formatted.
pub(crate) fn docstring(&self) -> Option<QuoteStyle> {
pub(crate) fn docstring(&self) -> Option<QuoteChar> {
self.docstring
}
/// Return a new context suitable for formatting code snippets within a
/// docstring.
///
/// The quote style given should correspond to the style of quoting used
/// The quote character given should correspond to the quote character used
/// for the docstring containing the code snippets.
pub(crate) fn in_docstring(self, style: QuoteStyle) -> PyFormatContext<'a> {
pub(crate) fn in_docstring(self, quote: QuoteChar) -> PyFormatContext<'a> {
PyFormatContext {
docstring: Some(style),
docstring: Some(quote),
..self
}
}

View file

@ -13,9 +13,9 @@ use {
ruff_text_size::{Ranged, TextLen, TextRange, TextSize},
};
use crate::{prelude::*, FormatModuleError, QuoteStyle};
use crate::{prelude::*, FormatModuleError};
use super::NormalizedString;
use super::{NormalizedString, QuoteChar};
/// Format a docstring by trimming whitespace and adjusting the indentation.
///
@ -139,7 +139,7 @@ pub(super) fn format(normalized: &NormalizedString, f: &mut PyFormatter) -> Form
// Edge case: The first line is `""" "content`, so we need to insert chaperone space that keep
// inner quotes and closing quotes from getting to close to avoid `""""content`
if trim_both.starts_with(normalized.quotes.style.as_char()) {
if trim_both.starts_with(normalized.quotes.quote_char.as_char()) {
space().fmt(f)?;
}
@ -192,7 +192,7 @@ pub(super) fn format(normalized: &NormalizedString, f: &mut PyFormatter) -> Form
offset,
stripped_indentation_length,
already_normalized,
quote_style: normalized.quotes.style,
quote_char: normalized.quotes.quote_char,
code_example: CodeExample::default(),
}
.add_iter(lines)?;
@ -250,8 +250,8 @@ struct DocstringLinePrinter<'ast, 'buf, 'fmt, 'src> {
/// is, the formatter can take a fast path.
already_normalized: bool,
/// The quote style used by the docstring being printed.
quote_style: QuoteStyle,
/// The quote character used by the docstring being printed.
quote_char: QuoteChar,
/// The current code example detected in the docstring.
code_example: CodeExample<'src>,
@ -476,7 +476,7 @@ impl<'ast, 'buf, 'fmt, 'src> DocstringLinePrinter<'ast, 'buf, 'fmt, 'src> {
// instead of later, and as a result, get more consistent
// results.
.with_indent_style(IndentStyle::Space);
let printed = match docstring_format_source(options, self.quote_style, &codeblob) {
let printed = match docstring_format_source(options, self.quote_char, &codeblob) {
Ok(printed) => printed,
Err(FormatModuleError::FormatError(err)) => return Err(err),
Err(
@ -498,9 +498,11 @@ impl<'ast, 'buf, 'fmt, 'src> DocstringLinePrinter<'ast, 'buf, 'fmt, 'src> {
// a docstring. As we fix corner cases over time, we can perhaps
// remove this check. See the `doctest_invalid_skipped` tests in
// `docstring_code_examples.py` for when this check is relevant.
let wrapped = match self.quote_style {
QuoteStyle::Single => std::format!("'''{}'''", printed.as_code()),
QuoteStyle::Double => std::format!(r#""""{}""""#, printed.as_code()),
let wrapped = match self.quote_char {
QuoteChar::Single => std::format!("'''{}'''", printed.as_code()),
QuoteChar::Double => {
std::format!(r#""""{}""""#, printed.as_code())
}
};
let result = ruff_python_parser::parse(
&wrapped,
@ -1483,7 +1485,7 @@ enum CodeExampleAddAction<'src> {
/// inside of a docstring.
fn docstring_format_source(
options: crate::PyFormatOptions,
docstring_quote_style: QuoteStyle,
docstring_quote_style: QuoteChar,
source: &str,
) -> Result<Printed, FormatModuleError> {
use ruff_python_parser::AsMode;
@ -1510,7 +1512,7 @@ fn docstring_format_source(
/// that avoids `content""""` and `content\"""`. This does only applies to un-escaped backslashes,
/// so `content\\ """` doesn't need a space while `content\\\ """` does.
fn needs_chaperone_space(normalized: &NormalizedString, trim_end: &str) -> bool {
trim_end.ends_with(normalized.quotes.style.as_char())
trim_end.ends_with(normalized.quotes.quote_char.as_char())
|| trim_end.chars().rev().take_while(|c| *c == '\\').count() % 2 == 1
}

View file

@ -325,7 +325,7 @@ impl StringPart {
quoting: Quoting,
locator: &'a Locator,
configured_style: QuoteStyle,
parent_docstring_quote_style: Option<QuoteStyle>,
parent_docstring_quote_char: Option<QuoteChar>,
) -> NormalizedString<'a> {
// Per PEP 8, always prefer double quotes for triple-quoted strings.
let preferred_style = if self.quotes.triple {
@ -374,8 +374,8 @@ impl StringPart {
// Overall this is a bit of a corner case and just inverting the
// style from what the parent ultimately decided upon works, even
// if it doesn't have perfect alignment with PEP8.
if let Some(style) = parent_docstring_quote_style {
style.invert()
if let Some(quote) = parent_docstring_quote_char {
QuoteStyle::from(quote.invert())
} else {
QuoteStyle::Double
}
@ -388,10 +388,14 @@ impl StringPart {
let quotes = match quoting {
Quoting::Preserve => self.quotes,
Quoting::CanChange => {
if self.prefix.is_raw_string() {
choose_quotes_raw(raw_content, self.quotes, preferred_style)
if let Some(preferred_quote) = QuoteChar::from_style(preferred_style) {
if self.prefix.is_raw_string() {
choose_quotes_raw(raw_content, self.quotes, preferred_quote)
} else {
choose_quotes(raw_content, self.quotes, preferred_quote)
}
} else {
choose_quotes(raw_content, self.quotes, preferred_style)
self.quotes
}
}
};
@ -526,9 +530,9 @@ impl Format<PyFormatContext<'_>> for StringPrefix {
fn choose_quotes_raw(
input: &str,
quotes: StringQuotes,
preferred_style: QuoteStyle,
preferred_quote: QuoteChar,
) -> StringQuotes {
let preferred_quote_char = preferred_style.as_char();
let preferred_quote_char = preferred_quote.as_char();
let mut chars = input.chars().peekable();
let contains_unescaped_configured_quotes = loop {
match chars.next() {
@ -566,10 +570,10 @@ fn choose_quotes_raw(
StringQuotes {
triple: quotes.triple,
style: if contains_unescaped_configured_quotes {
quotes.style
quote_char: if contains_unescaped_configured_quotes {
quotes.quote_char
} else {
preferred_style
preferred_quote
},
}
}
@ -582,14 +586,14 @@ fn choose_quotes_raw(
/// For triple quoted strings, the preferred quote style is always used, unless the string contains
/// a triplet of the quote character (e.g., if double quotes are preferred, double quotes will be
/// used unless the string contains `"""`).
fn choose_quotes(input: &str, quotes: StringQuotes, preferred_style: QuoteStyle) -> StringQuotes {
let style = if quotes.triple {
fn choose_quotes(input: &str, quotes: StringQuotes, preferred_quote: QuoteChar) -> StringQuotes {
let quote = if quotes.triple {
// 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 preferred_quote_char = preferred_style.as_char();
let preferred_quote_char = preferred_quote.as_char();
match c {
'\\' => {
if matches!(chars.peek(), Some('"' | '\\')) {
@ -637,9 +641,9 @@ fn choose_quotes(input: &str, quotes: StringQuotes, preferred_style: QuoteStyle)
if uses_triple_quotes {
// String contains a triple quote sequence of the configured quote style.
// Keep the existing quote style.
quotes.style
quotes.quote_char
} else {
preferred_style
preferred_quote
}
} else {
let mut single_quotes = 0u32;
@ -659,19 +663,19 @@ fn choose_quotes(input: &str, quotes: StringQuotes, preferred_style: QuoteStyle)
}
}
match preferred_style {
QuoteStyle::Single => {
match preferred_quote {
QuoteChar::Single => {
if single_quotes > double_quotes {
QuoteStyle::Double
QuoteChar::Double
} else {
QuoteStyle::Single
QuoteChar::Single
}
}
QuoteStyle::Double => {
QuoteChar::Double => {
if double_quotes > single_quotes {
QuoteStyle::Single
QuoteChar::Single
} else {
QuoteStyle::Double
QuoteChar::Double
}
}
}
@ -679,14 +683,14 @@ fn choose_quotes(input: &str, quotes: StringQuotes, preferred_style: QuoteStyle)
StringQuotes {
triple: quotes.triple,
style,
quote_char: quote,
}
}
#[derive(Copy, Clone, Debug)]
pub(super) struct StringQuotes {
triple: bool,
style: QuoteStyle,
quote_char: QuoteChar,
}
impl StringQuotes {
@ -694,11 +698,14 @@ impl StringQuotes {
let mut chars = input.chars();
let quote_char = chars.next()?;
let style = QuoteStyle::try_from(quote_char).ok()?;
let quote = QuoteChar::try_from(quote_char).ok()?;
let triple = chars.next() == Some(quote_char) && chars.next() == Some(quote_char);
Some(Self { triple, style })
Some(Self {
triple,
quote_char: quote,
})
}
pub(super) const fn is_triple(self) -> bool {
@ -716,17 +723,74 @@ impl StringQuotes {
impl Format<PyFormatContext<'_>> for StringQuotes {
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
let quotes = match (self.style, self.triple) {
(QuoteStyle::Single, false) => "'",
(QuoteStyle::Single, true) => "'''",
(QuoteStyle::Double, false) => "\"",
(QuoteStyle::Double, true) => "\"\"\"",
let quotes = match (self.quote_char, self.triple) {
(QuoteChar::Single, false) => "'",
(QuoteChar::Single, true) => "'''",
(QuoteChar::Double, false) => "\"",
(QuoteChar::Double, true) => "\"\"\"",
};
token(quotes).fmt(f)
}
}
/// The quotation character used to quote a string, byte, or fstring literal.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum QuoteChar {
/// A single quote: `'`
Single,
/// A double quote: '"'
Double,
}
impl QuoteChar {
pub const fn as_char(self) -> char {
match self {
QuoteChar::Single => '\'',
QuoteChar::Double => '"',
}
}
#[must_use]
pub const fn invert(self) -> QuoteChar {
match self {
QuoteChar::Single => QuoteChar::Double,
QuoteChar::Double => QuoteChar::Single,
}
}
#[must_use]
pub const fn from_style(style: QuoteStyle) -> Option<QuoteChar> {
match style {
QuoteStyle::Single => Some(QuoteChar::Single),
QuoteStyle::Double => Some(QuoteChar::Double),
QuoteStyle::Preserve => None,
}
}
}
impl From<QuoteChar> for QuoteStyle {
fn from(value: QuoteChar) -> Self {
match value {
QuoteChar::Single => QuoteStyle::Single,
QuoteChar::Double => QuoteStyle::Double,
}
}
}
impl TryFrom<char> for QuoteChar {
type Error = ();
fn try_from(value: char) -> Result<Self, Self::Error> {
match value {
'\'' => Ok(QuoteChar::Single),
'"' => Ok(QuoteChar::Double),
_ => Err(()),
}
}
}
/// Adds the necessary quote escapes and removes unnecessary escape sequences when quoting `input`
/// with the provided [`StringQuotes`] style.
///
@ -739,9 +803,9 @@ fn normalize_string(input: &str, quotes: StringQuotes, prefix: StringPrefix) ->
// 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 quote = quotes.quote_char;
let preferred_quote = quote.as_char();
let opposite_quote = quote.invert().as_char();
let mut chars = input.char_indices().peekable();

View file

@ -207,35 +207,7 @@ pub enum QuoteStyle {
Single,
#[default]
Double,
}
impl QuoteStyle {
pub const fn as_char(self) -> char {
match self {
QuoteStyle::Single => '\'',
QuoteStyle::Double => '"',
}
}
#[must_use]
pub const fn invert(self) -> QuoteStyle {
match self {
QuoteStyle::Single => QuoteStyle::Double,
QuoteStyle::Double => QuoteStyle::Single,
}
}
}
impl TryFrom<char> for QuoteStyle {
type Error = ();
fn try_from(value: char) -> std::result::Result<Self, Self::Error> {
match value {
'\'' => Ok(QuoteStyle::Single),
'"' => Ok(QuoteStyle::Double),
_ => Err(()),
}
}
Preserve,
}
impl FromStr for QuoteStyle {
@ -245,6 +217,7 @@ impl FromStr for QuoteStyle {
match s {
"\"" | "double" | "Double" => Ok(Self::Double),
"'" | "single" | "Single" => Ok(Self::Single),
"preserve" | "Preserve" => Ok(Self::Preserve),
// TODO: replace this error with a diagnostic
_ => Err("Value not supported for QuoteStyle"),
}