Preserve quote style in generated code (#15726)

## Summary

This is a first step toward fixing #7799 by using the quoting style
stored in the `flags` field on `ast::StringLiteral`s to select a quoting
style. This PR does not include support for f-strings or byte strings.

Several rules also needed small updates to pass along existing quoting
styles instead of using `StringLiteralFlags::default()`. The remaining
snapshot changes are intentional and should preserve the quotes from the
input strings.

## Test Plan

Existing tests with some accepted updates, plus a few new RUF055 tests
for raw strings.

---------

Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
This commit is contained in:
Brent Westbrook 2025-01-27 13:41:03 -05:00 committed by GitHub
parent e994970538
commit 9bf138c45a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 550 additions and 343 deletions

View file

@ -6,8 +6,8 @@ use ruff_python_ast::str::Quote;
use ruff_python_ast::{
self as ast, Alias, ArgOrKeyword, BoolOp, CmpOp, Comprehension, ConversionFlag, DebugText,
ExceptHandler, Expr, Identifier, MatchCase, Operator, Parameter, Parameters, Pattern,
Singleton, Stmt, Suite, TypeParam, TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple,
WithItem,
Singleton, Stmt, StringFlags, Suite, TypeParam, TypeParamParamSpec, TypeParamTypeVar,
TypeParamTypeVarTuple, WithItem,
};
use ruff_python_ast::{ParameterWithDefault, TypeParams};
use ruff_python_literal::escape::{AsciiEscape, Escape, UnicodeEscape};
@ -65,7 +65,10 @@ mod precedence {
pub struct Generator<'a> {
/// The indentation style to use.
indent: &'a Indentation,
/// The quote style to use for string literals.
/// 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,
@ -158,8 +161,8 @@ impl<'a> Generator<'a> {
escape.bytes_repr().write(&mut self.buffer).unwrap(); // write to string doesn't fail
}
fn p_str_repr(&mut self, s: &str) {
let escape = UnicodeEscape::with_preferred_quote(s, self.quote);
fn p_str_repr(&mut self, s: &str, quote: Quote) {
let escape = UnicodeEscape::with_preferred_quote(s, quote);
if let Some(len) = escape.layout().len {
self.buffer.reserve(len);
}
@ -1288,14 +1291,14 @@ impl<'a> Generator<'a> {
// replacement here
if flags.prefix().is_raw() {
self.p(flags.prefix().as_str());
self.p(self.quote.as_str());
self.p(flags.quote_str());
self.p(value);
self.p(self.quote.as_str());
self.p(flags.quote_str());
} else {
if flags.prefix().is_unicode() {
self.p("u");
}
self.p_str_repr(value);
self.p_str_repr(value, flags.quote_style());
}
}
@ -1403,7 +1406,7 @@ impl<'a> Generator<'a> {
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.p_str_repr(body, self.quote);
}
}
@ -1444,6 +1447,24 @@ mod tests {
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'"#
/// );
/// ```
fn round_trip_with(
indentation: &Indentation,
quote: Quote,
@ -1719,14 +1740,14 @@ class Foo:
#[test]
fn quote() {
assert_eq!(round_trip(r#""hello""#), r#""hello""#);
assert_eq!(round_trip(r"'hello'"), r#""hello""#);
assert_eq!(round_trip(r"u'hello'"), r#"u"hello""#);
assert_eq!(round_trip(r"r'hello'"), r#"r"hello""#);
assert_round_trip!(r"'hello'");
assert_round_trip!(r"u'hello'");
assert_round_trip!(r"r'hello'");
assert_eq!(round_trip(r"b'hello'"), r#"b"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}""#);
assert_eq!(round_trip(r#"f'abc{"def"}{1}'"#), r#"f"abc{'def'}{1}""#);
assert_round_trip!(r#"f'abc{"def"}{1}'"#);
}
#[test]
@ -1773,42 +1794,37 @@ if True:
#[test]
fn set_quote() {
assert_eq!(
round_trip_with(
&Indentation::default(),
Quote::Double,
LineEnding::default(),
r#""hello""#
),
r#""hello""#
);
assert_eq!(
round_trip_with(
&Indentation::default(),
Quote::Single,
LineEnding::default(),
r#""hello""#
),
r"'hello'"
);
assert_eq!(
round_trip_with(
&Indentation::default(),
Quote::Double,
LineEnding::default(),
r"'hello'"
),
r#""hello""#
);
assert_eq!(
round_trip_with(
&Indentation::default(),
Quote::Single,
LineEnding::default(),
r"'hello'"
),
r"'hello'"
);
macro_rules! round_trip_with {
($quote:expr, $start:expr, $end:expr) => {
assert_eq!(
round_trip_with(
&Indentation::default(),
$quote,
LineEnding::default(),
$start
),
$end,
);
};
}
// setting Generator::quote works for bytestrings
round_trip_with!(Quote::Double, r#"b"hello""#, r#"b"hello""#);
round_trip_with!(Quote::Single, r#"b"hello""#, r"b'hello'");
round_trip_with!(Quote::Double, r"b'hello'", r#"b"hello""#);
round_trip_with!(Quote::Single, r"b'hello'", r"b'hello'");
// and 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 string literals, where the `Quote` is taken directly from their flags
round_trip_with!(Quote::Double, r#""hello""#, r#""hello""#);
round_trip_with!(Quote::Single, r#""hello""#, r#""hello""#); // no effect
round_trip_with!(Quote::Double, r"'hello'", r#"'hello'"#); // no effect
round_trip_with!(Quote::Single, r"'hello'", r"'hello'");
}
#[test]