mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-10 13:48:18 +00:00
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:
parent
6bbabceead
commit
2414298289
10 changed files with 482 additions and 96 deletions
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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"),
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue