mirror of
https://github.com/astral-sh/ruff.git
synced 2025-07-23 13:05:06 +00:00
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:
parent
e994970538
commit
9bf138c45a
24 changed files with 550 additions and 343 deletions
|
@ -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]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue