Skip to content

Commit 48ada2d

Browse files
authored
Generator preferred quote style (#20434)
1 parent 50bd394 commit 48ada2d

1 file changed

Lines changed: 60 additions & 6 deletions

File tree

crates/ruff_python_codegen/src/generator.rs

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use std::fmt::Write;
44
use std::ops::Deref;
55

6+
use ruff_python_ast::str::Quote;
67
use ruff_python_ast::{
78
self as ast, Alias, AnyStringFlags, ArgOrKeyword, BoolOp, BytesLiteralFlags, CmpOp,
89
Comprehension, ConversionFlag, DebugText, ExceptHandler, Expr, Identifier, MatchCase, Operator,
@@ -67,6 +68,8 @@ pub struct Generator<'a> {
6768
indent: &'a Indentation,
6869
/// The line ending to use.
6970
line_ending: LineEnding,
71+
/// Preferred quote style to use. For more info see [`Generator::with_preferred_quote`].
72+
preferred_quote: Option<Quote>,
7073
buffer: String,
7174
indent_depth: usize,
7275
num_newlines: usize,
@@ -78,6 +81,7 @@ impl<'a> From<&'a Stylist<'a>> for Generator<'a> {
7881
Self {
7982
indent: stylist.indentation(),
8083
line_ending: stylist.line_ending(),
84+
preferred_quote: None,
8185
buffer: String::new(),
8286
indent_depth: 0,
8387
num_newlines: 0,
@@ -92,6 +96,7 @@ impl<'a> Generator<'a> {
9296
// Style preferences.
9397
indent,
9498
line_ending,
99+
preferred_quote: None,
95100
// Internal state.
96101
buffer: String::new(),
97102
indent_depth: 0,
@@ -100,6 +105,16 @@ impl<'a> Generator<'a> {
100105
}
101106
}
102107

108+
/// Set a preferred quote style for generated source code.
109+
///
110+
/// - If [`None`], the generator will attempt to preserve the existing quote style whenever possible.
111+
/// - If [`Some`], the generator will prefer the specified quote style, ignoring the one found in the source.
112+
#[must_use]
113+
pub fn with_preferred_quote(mut self, quote: Option<Quote>) -> Self {
114+
self.preferred_quote = quote;
115+
self
116+
}
117+
103118
/// Generate source code from a [`Stmt`].
104119
pub fn stmt(mut self, stmt: &Stmt) -> String {
105120
self.unparse_stmt(stmt);
@@ -158,7 +173,8 @@ impl<'a> Generator<'a> {
158173
return;
159174
}
160175
}
161-
let escape = AsciiEscape::with_preferred_quote(s, flags.quote_style());
176+
let quote_style = self.preferred_quote.unwrap_or_else(|| flags.quote_style());
177+
let escape = AsciiEscape::with_preferred_quote(s, quote_style);
162178
if let Some(len) = escape.layout().len {
163179
self.buffer.reserve(len);
164180
}
@@ -176,7 +192,9 @@ impl<'a> Generator<'a> {
176192
return;
177193
}
178194
self.p(flags.prefix().as_str());
179-
let escape = UnicodeEscape::with_preferred_quote(s, flags.quote_style());
195+
196+
let quote_style = self.preferred_quote.unwrap_or_else(|| flags.quote_style());
197+
let escape = UnicodeEscape::with_preferred_quote(s, quote_style);
180198
if let Some(len) = escape.layout().len {
181199
self.buffer.reserve(len);
182200
}
@@ -1506,7 +1524,9 @@ impl<'a> Generator<'a> {
15061524
self.buffer += &s;
15071525
return;
15081526
}
1509-
let escape = UnicodeEscape::with_preferred_quote(&s, flags.quote_style());
1527+
1528+
let quote_style = self.preferred_quote.unwrap_or_else(|| flags.quote_style());
1529+
let escape = UnicodeEscape::with_preferred_quote(&s, quote_style);
15101530
if let Some(len) = escape.layout().len {
15111531
self.buffer.reserve(len);
15121532
}
@@ -1531,6 +1551,9 @@ impl<'a> Generator<'a> {
15311551
flags: AnyStringFlags,
15321552
) {
15331553
self.p(flags.prefix().as_str());
1554+
1555+
let flags =
1556+
flags.with_quote_style(self.preferred_quote.unwrap_or_else(|| flags.quote_style()));
15341557
self.p(flags.quote_str());
15351558
self.unparse_interpolated_string_body(values, flags);
15361559
self.p(flags.quote_str());
@@ -1563,6 +1586,7 @@ impl<'a> Generator<'a> {
15631586

15641587
#[cfg(test)]
15651588
mod tests {
1589+
use ruff_python_ast::str::Quote;
15661590
use ruff_python_ast::{Mod, ModModule};
15671591
use ruff_python_parser::{self, Mode, ParseOptions, parse_module};
15681592
use ruff_source_file::LineEnding;
@@ -1580,15 +1604,17 @@ mod tests {
15801604
generator.generate()
15811605
}
15821606

1583-
/// Like [`round_trip`] but configure the [`Generator`] with the requested `indentation` and
1584-
/// `line_ending` settings.
1607+
/// Like [`round_trip`] but configure the [`Generator`] with the requested
1608+
/// `indentation`, `line_ending` and `preferred_quote` settings.
15851609
fn round_trip_with(
15861610
indentation: &Indentation,
15871611
line_ending: LineEnding,
1612+
preferred_quote: Option<Quote>,
15881613
contents: &str,
15891614
) -> String {
15901615
let module = parse_module(contents).unwrap();
1591-
let mut generator = Generator::new(indentation, line_ending);
1616+
let mut generator =
1617+
Generator::new(indentation, line_ending).with_preferred_quote(preferred_quote);
15921618
generator.unparse_suite(module.suite());
15931619
generator.generate()
15941620
}
@@ -1974,6 +2000,7 @@ if True:
19742000
round_trip_with(
19752001
&Indentation::new(" ".to_string()),
19762002
LineEnding::default(),
2003+
None,
19772004
r"
19782005
if True:
19792006
pass
@@ -1991,6 +2018,7 @@ if True:
19912018
round_trip_with(
19922019
&Indentation::new(" ".to_string()),
19932020
LineEnding::default(),
2021+
None,
19942022
r"
19952023
if True:
19962024
pass
@@ -2008,6 +2036,7 @@ if True:
20082036
round_trip_with(
20092037
&Indentation::new("\t".to_string()),
20102038
LineEnding::default(),
2039+
None,
20112040
r"
20122041
if True:
20132042
pass
@@ -2029,6 +2058,7 @@ if True:
20292058
round_trip_with(
20302059
&Indentation::default(),
20312060
LineEnding::Lf,
2061+
None,
20322062
"if True:\n print(42)",
20332063
),
20342064
"if True:\n print(42)",
@@ -2038,6 +2068,7 @@ if True:
20382068
round_trip_with(
20392069
&Indentation::default(),
20402070
LineEnding::CrLf,
2071+
None,
20412072
"if True:\n print(42)",
20422073
),
20432074
"if True:\r\n print(42)",
@@ -2047,9 +2078,32 @@ if True:
20472078
round_trip_with(
20482079
&Indentation::default(),
20492080
LineEnding::Cr,
2081+
None,
20502082
"if True:\n print(42)",
20512083
),
20522084
"if True:\r print(42)",
20532085
);
20542086
}
2087+
2088+
#[test_case::test_case(r#""'hello'""#, r#""'hello'""#, Quote::Single ; "basic str ignored")]
2089+
#[test_case::test_case(r#"b"'hello'""#, r#"b"'hello'""#, Quote::Single ; "basic bytes ignored")]
2090+
#[test_case::test_case("'hello'", r#""hello""#, Quote::Double ; "basic str double")]
2091+
#[test_case::test_case(r#""hello""#, "'hello'", Quote::Single ; "basic str single")]
2092+
#[test_case::test_case("b'hello'", r#"b"hello""#, Quote::Double ; "basic bytes double")]
2093+
#[test_case::test_case(r#"b"hello""#, "b'hello'", Quote::Single ; "basic bytes single")]
2094+
#[test_case::test_case(r#""hello""#, r#""hello""#, Quote::Double ; "remain str double")]
2095+
#[test_case::test_case("'hello'", "'hello'", Quote::Single ; "remain str single")]
2096+
#[test_case::test_case("x: list['str']", r#"x: list["str"]"#, Quote::Double ; "type ann double")]
2097+
#[test_case::test_case(r#"x: list["str"]"#, "x: list['str']", Quote::Single ; "type ann single")]
2098+
#[test_case::test_case("f'hello'", r#"f"hello""#, Quote::Double ; "basic fstring double")]
2099+
#[test_case::test_case(r#"f"hello""#, "f'hello'", Quote::Single ; "basic fstring single")]
2100+
fn preferred_quote(inp: &str, out: &str, quote: Quote) {
2101+
let got = round_trip_with(
2102+
&Indentation::default(),
2103+
LineEnding::default(),
2104+
Some(quote),
2105+
inp,
2106+
);
2107+
assert_eq!(got, out);
2108+
}
20552109
}

0 commit comments

Comments
 (0)