Generator preferred quote style (#20434)

This commit is contained in:
Shahar Naveh 2025-09-18 12:57:21 +02:00 committed by GitHub
parent 50bd3943da
commit 48ada2d359
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -3,6 +3,7 @@
use std::fmt::Write; use std::fmt::Write;
use std::ops::Deref; use std::ops::Deref;
use ruff_python_ast::str::Quote;
use ruff_python_ast::{ use ruff_python_ast::{
self as ast, Alias, AnyStringFlags, ArgOrKeyword, BoolOp, BytesLiteralFlags, CmpOp, self as ast, Alias, AnyStringFlags, ArgOrKeyword, BoolOp, BytesLiteralFlags, CmpOp,
Comprehension, ConversionFlag, DebugText, ExceptHandler, Expr, Identifier, MatchCase, Operator, Comprehension, ConversionFlag, DebugText, ExceptHandler, Expr, Identifier, MatchCase, Operator,
@ -67,6 +68,8 @@ pub struct Generator<'a> {
indent: &'a Indentation, indent: &'a Indentation,
/// The line ending to use. /// The line ending to use.
line_ending: LineEnding, line_ending: LineEnding,
/// Preferred quote style to use. For more info see [`Generator::with_preferred_quote`].
preferred_quote: Option<Quote>,
buffer: String, buffer: String,
indent_depth: usize, indent_depth: usize,
num_newlines: usize, num_newlines: usize,
@ -78,6 +81,7 @@ impl<'a> From<&'a Stylist<'a>> for Generator<'a> {
Self { Self {
indent: stylist.indentation(), indent: stylist.indentation(),
line_ending: stylist.line_ending(), line_ending: stylist.line_ending(),
preferred_quote: None,
buffer: String::new(), buffer: String::new(),
indent_depth: 0, indent_depth: 0,
num_newlines: 0, num_newlines: 0,
@ -92,6 +96,7 @@ impl<'a> Generator<'a> {
// Style preferences. // Style preferences.
indent, indent,
line_ending, line_ending,
preferred_quote: None,
// Internal state. // Internal state.
buffer: String::new(), buffer: String::new(),
indent_depth: 0, indent_depth: 0,
@ -100,6 +105,16 @@ impl<'a> Generator<'a> {
} }
} }
/// Set a preferred quote style for generated source code.
///
/// - If [`None`], the generator will attempt to preserve the existing quote style whenever possible.
/// - If [`Some`], the generator will prefer the specified quote style, ignoring the one found in the source.
#[must_use]
pub fn with_preferred_quote(mut self, quote: Option<Quote>) -> Self {
self.preferred_quote = quote;
self
}
/// Generate source code from a [`Stmt`]. /// Generate source code from a [`Stmt`].
pub fn stmt(mut self, stmt: &Stmt) -> String { pub fn stmt(mut self, stmt: &Stmt) -> String {
self.unparse_stmt(stmt); self.unparse_stmt(stmt);
@ -158,7 +173,8 @@ impl<'a> Generator<'a> {
return; return;
} }
} }
let escape = AsciiEscape::with_preferred_quote(s, flags.quote_style()); let quote_style = self.preferred_quote.unwrap_or_else(|| flags.quote_style());
let escape = AsciiEscape::with_preferred_quote(s, quote_style);
if let Some(len) = escape.layout().len { if let Some(len) = escape.layout().len {
self.buffer.reserve(len); self.buffer.reserve(len);
} }
@ -176,7 +192,9 @@ impl<'a> Generator<'a> {
return; return;
} }
self.p(flags.prefix().as_str()); self.p(flags.prefix().as_str());
let escape = UnicodeEscape::with_preferred_quote(s, flags.quote_style());
let quote_style = self.preferred_quote.unwrap_or_else(|| flags.quote_style());
let escape = UnicodeEscape::with_preferred_quote(s, quote_style);
if let Some(len) = escape.layout().len { if let Some(len) = escape.layout().len {
self.buffer.reserve(len); self.buffer.reserve(len);
} }
@ -1506,7 +1524,9 @@ impl<'a> Generator<'a> {
self.buffer += &s; self.buffer += &s;
return; return;
} }
let escape = UnicodeEscape::with_preferred_quote(&s, flags.quote_style());
let quote_style = self.preferred_quote.unwrap_or_else(|| flags.quote_style());
let escape = UnicodeEscape::with_preferred_quote(&s, quote_style);
if let Some(len) = escape.layout().len { if let Some(len) = escape.layout().len {
self.buffer.reserve(len); self.buffer.reserve(len);
} }
@ -1531,6 +1551,9 @@ impl<'a> Generator<'a> {
flags: AnyStringFlags, flags: AnyStringFlags,
) { ) {
self.p(flags.prefix().as_str()); self.p(flags.prefix().as_str());
let flags =
flags.with_quote_style(self.preferred_quote.unwrap_or_else(|| flags.quote_style()));
self.p(flags.quote_str()); self.p(flags.quote_str());
self.unparse_interpolated_string_body(values, flags); self.unparse_interpolated_string_body(values, flags);
self.p(flags.quote_str()); self.p(flags.quote_str());
@ -1563,6 +1586,7 @@ impl<'a> Generator<'a> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use ruff_python_ast::str::Quote;
use ruff_python_ast::{Mod, ModModule}; use ruff_python_ast::{Mod, ModModule};
use ruff_python_parser::{self, Mode, ParseOptions, parse_module}; use ruff_python_parser::{self, Mode, ParseOptions, parse_module};
use ruff_source_file::LineEnding; use ruff_source_file::LineEnding;
@ -1580,15 +1604,17 @@ mod tests {
generator.generate() generator.generate()
} }
/// Like [`round_trip`] but configure the [`Generator`] with the requested `indentation` and /// Like [`round_trip`] but configure the [`Generator`] with the requested
/// `line_ending` settings. /// `indentation`, `line_ending` and `preferred_quote` settings.
fn round_trip_with( fn round_trip_with(
indentation: &Indentation, indentation: &Indentation,
line_ending: LineEnding, line_ending: LineEnding,
preferred_quote: Option<Quote>,
contents: &str, contents: &str,
) -> String { ) -> String {
let module = parse_module(contents).unwrap(); let module = parse_module(contents).unwrap();
let mut generator = Generator::new(indentation, line_ending); let mut generator =
Generator::new(indentation, line_ending).with_preferred_quote(preferred_quote);
generator.unparse_suite(module.suite()); generator.unparse_suite(module.suite());
generator.generate() generator.generate()
} }
@ -1974,6 +2000,7 @@ if True:
round_trip_with( round_trip_with(
&Indentation::new(" ".to_string()), &Indentation::new(" ".to_string()),
LineEnding::default(), LineEnding::default(),
None,
r" r"
if True: if True:
pass pass
@ -1991,6 +2018,7 @@ if True:
round_trip_with( round_trip_with(
&Indentation::new(" ".to_string()), &Indentation::new(" ".to_string()),
LineEnding::default(), LineEnding::default(),
None,
r" r"
if True: if True:
pass pass
@ -2008,6 +2036,7 @@ if True:
round_trip_with( round_trip_with(
&Indentation::new("\t".to_string()), &Indentation::new("\t".to_string()),
LineEnding::default(), LineEnding::default(),
None,
r" r"
if True: if True:
pass pass
@ -2029,6 +2058,7 @@ if True:
round_trip_with( round_trip_with(
&Indentation::default(), &Indentation::default(),
LineEnding::Lf, LineEnding::Lf,
None,
"if True:\n print(42)", "if True:\n print(42)",
), ),
"if True:\n print(42)", "if True:\n print(42)",
@ -2038,6 +2068,7 @@ if True:
round_trip_with( round_trip_with(
&Indentation::default(), &Indentation::default(),
LineEnding::CrLf, LineEnding::CrLf,
None,
"if True:\n print(42)", "if True:\n print(42)",
), ),
"if True:\r\n print(42)", "if True:\r\n print(42)",
@ -2047,9 +2078,32 @@ if True:
round_trip_with( round_trip_with(
&Indentation::default(), &Indentation::default(),
LineEnding::Cr, LineEnding::Cr,
None,
"if True:\n print(42)", "if True:\n print(42)",
), ),
"if True:\r print(42)", "if True:\r print(42)",
); );
} }
#[test_case::test_case(r#""'hello'""#, r#""'hello'""#, Quote::Single ; "basic str ignored")]
#[test_case::test_case(r#"b"'hello'""#, r#"b"'hello'""#, Quote::Single ; "basic bytes ignored")]
#[test_case::test_case("'hello'", r#""hello""#, Quote::Double ; "basic str double")]
#[test_case::test_case(r#""hello""#, "'hello'", Quote::Single ; "basic str single")]
#[test_case::test_case("b'hello'", r#"b"hello""#, Quote::Double ; "basic bytes double")]
#[test_case::test_case(r#"b"hello""#, "b'hello'", Quote::Single ; "basic bytes single")]
#[test_case::test_case(r#""hello""#, r#""hello""#, Quote::Double ; "remain str double")]
#[test_case::test_case("'hello'", "'hello'", Quote::Single ; "remain str single")]
#[test_case::test_case("x: list['str']", r#"x: list["str"]"#, Quote::Double ; "type ann double")]
#[test_case::test_case(r#"x: list["str"]"#, "x: list['str']", Quote::Single ; "type ann single")]
#[test_case::test_case("f'hello'", r#"f"hello""#, Quote::Double ; "basic fstring double")]
#[test_case::test_case(r#"f"hello""#, "f'hello'", Quote::Single ; "basic fstring single")]
fn preferred_quote(inp: &str, out: &str, quote: Quote) {
let got = round_trip_with(
&Indentation::default(),
LineEnding::default(),
Some(quote),
inp,
);
assert_eq!(got, out);
}
} }