mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-19 01:51:30 +00:00
Preserve quotes in generated f-strings (#15794)
## Summary This is another follow-up to #15726 and #15778, extending the quote-preserving behavior to f-strings and deleting the now-unused `Generator::quote` field. ## Details I also made one unrelated change to `rules/flynt/helpers.rs` to remove a `to_string` call for making a `Box<str>` and tweaked some arguments to some of the `Generator::unparse_f_string` methods to make the code easier to follow, in my opinion. Happy to revert especially the latter of these if needed. Unfortunately this still does not fix the issue in #9660, which appears to be more of an escaping issue than a quote-preservation issue. After #15726, the result is now `a = f'# {"".join([])}' if 1 else ""` instead of `a = f"# {''.join([])}" if 1 else ""` (single quotes on the outside now), but we still don't have the desired behavior of double quotes everywhere on Python 3.12+. I added a test for this but split it off into another branch since it ended up being unaddressed here, but my `dbg!` statements showed the correct preferred quotes going into [`UnicodeEscape::with_preferred_quote`](https://github.com/astral-sh/ruff/blob/main/crates/ruff_python_literal/src/escape.rs#L54). ## Test Plan Existing rule and `Generator` tests. --------- Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This commit is contained in:
parent
d151ca85d3
commit
23c98849fc
11 changed files with 197 additions and 116 deletions
|
@ -65,11 +65,6 @@ mod precedence {
|
|||
pub struct Generator<'a> {
|
||||
/// The indentation style to use.
|
||||
indent: &'a Indentation,
|
||||
/// The quote style to use for bytestring and f-string literals. For a plain
|
||||
/// [`StringLiteral`](ast::StringLiteral), modify its `flags` field using
|
||||
/// [`StringLiteralFlags::with_quote_style`](ast::StringLiteralFlags::with_quote_style) before
|
||||
/// passing it to the [`Generator`].
|
||||
quote: Quote,
|
||||
/// The line ending to use.
|
||||
line_ending: LineEnding,
|
||||
buffer: String,
|
||||
|
@ -82,7 +77,6 @@ impl<'a> From<&'a Stylist<'a>> for Generator<'a> {
|
|||
fn from(stylist: &'a Stylist<'a>) -> Self {
|
||||
Self {
|
||||
indent: stylist.indentation(),
|
||||
quote: stylist.quote(),
|
||||
line_ending: stylist.line_ending(),
|
||||
buffer: String::new(),
|
||||
indent_depth: 0,
|
||||
|
@ -93,11 +87,10 @@ impl<'a> From<&'a Stylist<'a>> for Generator<'a> {
|
|||
}
|
||||
|
||||
impl<'a> Generator<'a> {
|
||||
pub const fn new(indent: &'a Indentation, quote: Quote, line_ending: LineEnding) -> Self {
|
||||
pub const fn new(indent: &'a Indentation, line_ending: LineEnding) -> Self {
|
||||
Self {
|
||||
// Style preferences.
|
||||
indent,
|
||||
quote,
|
||||
line_ending,
|
||||
// Internal state.
|
||||
buffer: String::new(),
|
||||
|
@ -1091,7 +1084,7 @@ impl<'a> Generator<'a> {
|
|||
self.p(")");
|
||||
}
|
||||
Expr::FString(ast::ExprFString { value, .. }) => {
|
||||
self.unparse_f_string_value(value, false);
|
||||
self.unparse_f_string_value(value);
|
||||
}
|
||||
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => {
|
||||
self.unparse_string_literal_value(value);
|
||||
|
@ -1310,7 +1303,7 @@ impl<'a> Generator<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
fn unparse_f_string_value(&mut self, value: &ast::FStringValue, is_spec: bool) {
|
||||
fn unparse_f_string_value(&mut self, value: &ast::FStringValue) {
|
||||
let mut first = true;
|
||||
for f_string_part in value {
|
||||
self.p_delim(&mut first, " ");
|
||||
|
@ -1319,7 +1312,7 @@ impl<'a> Generator<'a> {
|
|||
self.unparse_string_literal(string_literal);
|
||||
}
|
||||
ast::FStringPart::FString(f_string) => {
|
||||
self.unparse_f_string(&f_string.elements, is_spec);
|
||||
self.unparse_f_string(&f_string.elements, f_string.flags.quote_style());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1338,7 +1331,7 @@ impl<'a> Generator<'a> {
|
|||
conversion: ConversionFlag,
|
||||
spec: Option<&ast::FStringFormatSpec>,
|
||||
) {
|
||||
let mut generator = Generator::new(self.indent, self.quote, self.line_ending);
|
||||
let mut generator = Generator::new(self.indent, self.line_ending);
|
||||
generator.unparse_expr(val, precedence::FORMATTED_VALUE);
|
||||
let brace = if generator.buffer.starts_with('{') {
|
||||
// put a space to avoid escaping the bracket
|
||||
|
@ -1366,7 +1359,7 @@ impl<'a> Generator<'a> {
|
|||
|
||||
if let Some(spec) = spec {
|
||||
self.p(":");
|
||||
self.unparse_f_string(&spec.elements, true);
|
||||
self.unparse_f_string_specifier(&spec.elements);
|
||||
}
|
||||
|
||||
self.p("}");
|
||||
|
@ -1397,17 +1390,18 @@ impl<'a> Generator<'a> {
|
|||
self.p(&s);
|
||||
}
|
||||
|
||||
fn unparse_f_string(&mut self, values: &[ast::FStringElement], is_spec: bool) {
|
||||
if is_spec {
|
||||
self.unparse_f_string_body(values);
|
||||
} else {
|
||||
self.p("f");
|
||||
let mut generator =
|
||||
Generator::new(self.indent, self.quote.opposite(), self.line_ending);
|
||||
generator.unparse_f_string_body(values);
|
||||
let body = &generator.buffer;
|
||||
self.p_str_repr(body, self.quote);
|
||||
}
|
||||
fn unparse_f_string_specifier(&mut self, values: &[ast::FStringElement]) {
|
||||
self.unparse_f_string_body(values);
|
||||
}
|
||||
|
||||
/// Unparse `values` with [`Generator::unparse_f_string_body`], using `quote` as the preferred
|
||||
/// surrounding quote style.
|
||||
fn unparse_f_string(&mut self, values: &[ast::FStringElement], quote: Quote) {
|
||||
self.p("f");
|
||||
let mut generator = Generator::new(self.indent, self.line_ending);
|
||||
generator.unparse_f_string_body(values);
|
||||
let body = &generator.buffer;
|
||||
self.p_str_repr(body, quote);
|
||||
}
|
||||
|
||||
fn unparse_alias(&mut self, alias: &Alias) {
|
||||
|
@ -1429,7 +1423,7 @@ impl<'a> Generator<'a> {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_python_ast::{str::Quote, Mod, ModModule};
|
||||
use ruff_python_ast::{Mod, ModModule};
|
||||
use ruff_python_parser::{self, parse_module, Mode};
|
||||
use ruff_source_file::LineEnding;
|
||||
|
||||
|
@ -1439,47 +1433,28 @@ mod tests {
|
|||
|
||||
fn round_trip(contents: &str) -> String {
|
||||
let indentation = Indentation::default();
|
||||
let quote = Quote::default();
|
||||
let line_ending = LineEnding::default();
|
||||
let module = parse_module(contents).unwrap();
|
||||
let mut generator = Generator::new(&indentation, quote, line_ending);
|
||||
let mut generator = Generator::new(&indentation, line_ending);
|
||||
generator.unparse_suite(module.suite());
|
||||
generator.generate()
|
||||
}
|
||||
|
||||
/// Like [`round_trip`] but configure the [`Generator`] with the requested `indentation`,
|
||||
/// `quote`, and `line_ending` settings.
|
||||
///
|
||||
/// Note that quoting styles for string literals are taken from their [`StringLiteralFlags`],
|
||||
/// not from the [`Generator`] itself, so using this function on a plain string literal can give
|
||||
/// surprising results.
|
||||
///
|
||||
/// ```rust
|
||||
/// assert_eq!(
|
||||
/// round_trip_with(
|
||||
/// &Indentation::default(),
|
||||
/// Quote::Double,
|
||||
/// LineEnding::default(),
|
||||
/// r#"'hello'"#
|
||||
/// ),
|
||||
/// r#"'hello'"#
|
||||
/// );
|
||||
/// ```
|
||||
/// Like [`round_trip`] but configure the [`Generator`] with the requested `indentation` and
|
||||
/// `line_ending` settings.
|
||||
fn round_trip_with(
|
||||
indentation: &Indentation,
|
||||
quote: Quote,
|
||||
line_ending: LineEnding,
|
||||
contents: &str,
|
||||
) -> String {
|
||||
let module = parse_module(contents).unwrap();
|
||||
let mut generator = Generator::new(indentation, quote, line_ending);
|
||||
let mut generator = Generator::new(indentation, line_ending);
|
||||
generator.unparse_suite(module.suite());
|
||||
generator.generate()
|
||||
}
|
||||
|
||||
fn jupyter_round_trip(contents: &str) -> String {
|
||||
let indentation = Indentation::default();
|
||||
let quote = Quote::default();
|
||||
let line_ending = LineEnding::default();
|
||||
let parsed = ruff_python_parser::parse(contents, Mode::Ipython).unwrap();
|
||||
let Mod::Module(ModModule { body, .. }) = parsed.into_syntax() else {
|
||||
|
@ -1488,7 +1463,7 @@ mod tests {
|
|||
let [stmt] = body.as_slice() else {
|
||||
panic!("Expected only one statement in source code")
|
||||
};
|
||||
let mut generator = Generator::new(&indentation, quote, line_ending);
|
||||
let mut generator = Generator::new(&indentation, line_ending);
|
||||
generator.unparse_stmt(stmt);
|
||||
generator.generate()
|
||||
}
|
||||
|
@ -1744,6 +1719,9 @@ class Foo:
|
|||
assert_round_trip!(r"u'hello'");
|
||||
assert_round_trip!(r"r'hello'");
|
||||
assert_round_trip!(r"b'hello'");
|
||||
assert_round_trip!(r#"b"hello""#);
|
||||
assert_round_trip!(r"f'hello'");
|
||||
assert_round_trip!(r#"f"hello""#);
|
||||
assert_eq!(round_trip(r#"("abc" "def" "ghi")"#), r#""abc" "def" "ghi""#);
|
||||
assert_eq!(round_trip(r#""he\"llo""#), r#"'he"llo'"#);
|
||||
assert_eq!(round_trip(r#"f"abc{'def'}{1}""#), r#"f"abc{'def'}{1}""#);
|
||||
|
@ -1792,50 +1770,11 @@ if True:
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_quote() {
|
||||
macro_rules! round_trip_with {
|
||||
($quote:expr, $start:expr, $end:expr) => {
|
||||
assert_eq!(
|
||||
round_trip_with(
|
||||
&Indentation::default(),
|
||||
$quote,
|
||||
LineEnding::default(),
|
||||
$start
|
||||
),
|
||||
$end,
|
||||
);
|
||||
};
|
||||
($quote:expr, $start:expr) => {
|
||||
round_trip_with!($quote, $start, $start);
|
||||
};
|
||||
}
|
||||
|
||||
// setting Generator::quote works for f-strings
|
||||
round_trip_with!(Quote::Double, r#"f"hello""#, r#"f"hello""#);
|
||||
round_trip_with!(Quote::Single, r#"f"hello""#, r"f'hello'");
|
||||
round_trip_with!(Quote::Double, r"f'hello'", r#"f"hello""#);
|
||||
round_trip_with!(Quote::Single, r"f'hello'", r"f'hello'");
|
||||
|
||||
// but not for bytestrings
|
||||
round_trip_with!(Quote::Double, r#"b"hello""#);
|
||||
round_trip_with!(Quote::Single, r#"b"hello""#); // no effect
|
||||
round_trip_with!(Quote::Double, r"b'hello'"); // no effect
|
||||
round_trip_with!(Quote::Single, r"b'hello'");
|
||||
|
||||
// or for string literals, where the `Quote` is taken directly from their flags
|
||||
round_trip_with!(Quote::Double, r#""hello""#);
|
||||
round_trip_with!(Quote::Single, r#""hello""#); // no effect
|
||||
round_trip_with!(Quote::Double, r"'hello'"); // no effect
|
||||
round_trip_with!(Quote::Single, r"'hello'");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_indent() {
|
||||
assert_eq!(
|
||||
round_trip_with(
|
||||
&Indentation::new(" ".to_string()),
|
||||
Quote::default(),
|
||||
LineEnding::default(),
|
||||
r"
|
||||
if True:
|
||||
|
@ -1853,7 +1792,6 @@ if True:
|
|||
assert_eq!(
|
||||
round_trip_with(
|
||||
&Indentation::new(" ".to_string()),
|
||||
Quote::default(),
|
||||
LineEnding::default(),
|
||||
r"
|
||||
if True:
|
||||
|
@ -1871,7 +1809,6 @@ if True:
|
|||
assert_eq!(
|
||||
round_trip_with(
|
||||
&Indentation::new("\t".to_string()),
|
||||
Quote::default(),
|
||||
LineEnding::default(),
|
||||
r"
|
||||
if True:
|
||||
|
@ -1893,7 +1830,6 @@ if True:
|
|||
assert_eq!(
|
||||
round_trip_with(
|
||||
&Indentation::default(),
|
||||
Quote::default(),
|
||||
LineEnding::Lf,
|
||||
"if True:\n print(42)",
|
||||
),
|
||||
|
@ -1903,7 +1839,6 @@ if True:
|
|||
assert_eq!(
|
||||
round_trip_with(
|
||||
&Indentation::default(),
|
||||
Quote::default(),
|
||||
LineEnding::CrLf,
|
||||
"if True:\n print(42)",
|
||||
),
|
||||
|
@ -1913,7 +1848,6 @@ if True:
|
|||
assert_eq!(
|
||||
round_trip_with(
|
||||
&Indentation::default(),
|
||||
Quote::default(),
|
||||
LineEnding::Cr,
|
||||
"if True:\n print(42)",
|
||||
),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue