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

@ -29,47 +29,47 @@ no_sep = None
' 1 2 3 '.split()
'1<>2<>3<4'.split('<>')
" a*a a*a a ".split("*", -1) # [' a', 'a a', 'a a ']
" a*a a*a a ".split("*", -1) # [" a", "a a", "a a "]
"".split() # []
"""
""".split() # []
" ".split() # []
"/abc/".split() # ['/abc/']
"/abc/".split() # ["/abc/"]
("a,b,c"
# comment
.split()
) # ['a,b,c']
) # ["a,b,c"]
("a,b,c"
# comment1
.split(",")
) # ['a', 'b', 'c']
) # ["a", "b", "c"]
("a,"
# comment
"b,"
"c"
.split(",")
) # ['a', 'b', 'c']
) # ["a", "b", "c"]
"hello "\
"world".split()
# ['hello', 'world']
# ["hello", "world"]
# prefixes and isc
u"a b".split() # ['a', 'b']
r"a \n b".split() # ['a', '\\n', 'b']
("a " "b").split() # ['a', 'b']
"a " "b".split() # ['a', 'b']
u"a " "b".split() # ['a', 'b']
"a " u"b".split() # ['a', 'b']
u"a " r"\n".split() # ['a', '\\n']
r"\n " u"\n".split() # ['\\n']
r"\n " "\n".split() # ['\\n']
"a " r"\n".split() # ['a', '\\n']
u"a b".split() # [u"a", u"b"]
r"a \n b".split() # [r"a", r"\n", r"b"]
("a " "b").split() # ["a", "b"]
"a " "b".split() # ["a", "b"]
u"a " "b".split() # [u"a", u"b"]
"a " u"b".split() # ["a", "b"]
u"a " r"\n".split() # [u"a", u"\\n"]
r"\n " u"\n".split() # [r"\n"]
r"\n " "\n".split() # [r"\n"]
"a " r"\n".split() # ["a", "\\n"]
"a,b,c".split(',', maxsplit=0) # ['a,b,c']
"a,b,c".split(',', maxsplit=-1) # ['a', 'b', 'c']
"a,b,c".split(',', maxsplit=-2) # ['a', 'b', 'c']
"a,b,c".split(',', maxsplit=-0) # ['a,b,c']
"a,b,c".split(',', maxsplit=0) # ["a,b,c"]
"a,b,c".split(',', maxsplit=-1) # ["a", "b", "c"]
"a,b,c".split(',', maxsplit=-2) # ["a", "b", "c"]
"a,b,c".split(',', maxsplit=-0) # ["a,b,c"]
# negatives

View file

@ -82,3 +82,11 @@ class Collection(Protocol[*_B0]):
# Regression test for: https://github.com/astral-sh/ruff/issues/8609
def f(x: Union[int, str, bytes]) -> None:
...
# Regression test for https://github.com/astral-sh/ruff/issues/14132
class AClass:
...
def myfunc(param: "tuple[Union[int, 'AClass', None], str]"):
print(param)

View file

@ -93,3 +93,8 @@ re.sub(r"a", r"\a", "a")
re.sub(r"a", "\?", "a")
re.sub(r"a", r"\?", "a")
# these double as tests for preserving raw string quoting style
re.sub(r'abc', "", s)
re.sub(r"""abc""", "", s)
re.sub(r'''abc''', "", s)

View file

@ -506,7 +506,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
checker,
attr,
call,
string_value.to_str(),
string_value,
);
}
} else if attr == "format" {

View file

@ -296,11 +296,23 @@ impl<'a> Checker<'a> {
pub(crate) fn generator(&self) -> Generator {
Generator::new(
self.stylist.indentation(),
self.f_string_quote_style().unwrap_or(self.stylist.quote()),
self.preferred_quote(),
self.stylist.line_ending(),
)
}
/// Return the preferred quote for a generated `StringLiteral` node, given where we are in the
/// AST.
fn preferred_quote(&self) -> Quote {
self.f_string_quote_style().unwrap_or(self.stylist.quote())
}
/// Return the default string flags a generated `StringLiteral` node should use, given where we
/// are in the AST.
pub(crate) fn default_string_flags(&self) -> ast::StringLiteralFlags {
ast::StringLiteralFlags::empty().with_quote_style(self.preferred_quote())
}
/// Returns the appropriate quoting for f-string by reversing the one used outside of
/// the f-string.
///

View file

@ -4,7 +4,7 @@ use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::{self as ast, Expr, ExprCall, ExprContext};
use ruff_python_ast::{self as ast, Expr, ExprCall, ExprContext, StringLiteralFlags};
use ruff_python_codegen::Generator;
use ruff_python_trivia::CommentRanges;
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
@ -280,7 +280,7 @@ impl Violation for PytestDuplicateParametrizeTestCases {
}
}
fn elts_to_csv(elts: &[Expr], generator: Generator) -> Option<String> {
fn elts_to_csv(elts: &[Expr], generator: Generator, flags: StringLiteralFlags) -> Option<String> {
if !elts.iter().all(Expr::is_string_literal_expr) {
return None;
}
@ -298,7 +298,8 @@ fn elts_to_csv(elts: &[Expr], generator: Generator) -> Option<String> {
acc
})
.into_boxed_str(),
..ast::StringLiteral::default()
range: TextRange::default(),
flags,
});
Some(generator.expr(&node))
}
@ -358,8 +359,9 @@ fn check_names(checker: &mut Checker, call: &ExprCall, expr: &Expr, argvalues: &
.iter()
.map(|name| {
Expr::from(ast::StringLiteral {
value: (*name).to_string().into_boxed_str(),
..ast::StringLiteral::default()
value: Box::from(*name),
range: TextRange::default(),
flags: checker.default_string_flags(),
})
})
.collect(),
@ -393,8 +395,9 @@ fn check_names(checker: &mut Checker, call: &ExprCall, expr: &Expr, argvalues: &
.iter()
.map(|name| {
Expr::from(ast::StringLiteral {
value: (*name).to_string().into_boxed_str(),
..ast::StringLiteral::default()
value: Box::from(*name),
range: TextRange::default(),
flags: checker.default_string_flags(),
})
})
.collect(),
@ -444,7 +447,9 @@ fn check_names(checker: &mut Checker, call: &ExprCall, expr: &Expr, argvalues: &
},
expr.range(),
);
if let Some(content) = elts_to_csv(elts, checker.generator()) {
if let Some(content) =
elts_to_csv(elts, checker.generator(), checker.default_string_flags())
{
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
content,
expr.range(),
@ -489,7 +494,9 @@ fn check_names(checker: &mut Checker, call: &ExprCall, expr: &Expr, argvalues: &
},
expr.range(),
);
if let Some(content) = elts_to_csv(elts, checker.generator()) {
if let Some(content) =
elts_to_csv(elts, checker.generator(), checker.default_string_flags())
{
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
content,
expr.range(),

View file

@ -1,7 +1,5 @@
use ruff_python_ast::{
self as ast, str_prefix::StringLiteralPrefix, Arguments, Expr, StringLiteralFlags,
};
use ruff_text_size::Ranged;
use ruff_python_ast::{self as ast, str_prefix::StringLiteralPrefix, Arguments, Expr};
use ruff_text_size::{Ranged, TextRange};
use crate::fix::snippet::SourceCodeSnippet;
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix, FixAvailability, Violation};
@ -220,14 +218,14 @@ fn check_os_environ_subscript(checker: &mut Checker, expr: &Expr) {
);
let node = ast::StringLiteral {
value: capital_env_var.into_boxed_str(),
flags: StringLiteralFlags::default().with_prefix({
flags: checker.default_string_flags().with_prefix({
if env_var.is_unicode() {
StringLiteralPrefix::Unicode
} else {
StringLiteralPrefix::Empty
}
}),
..ast::StringLiteral::default()
range: TextRange::default(),
};
let new_env_var = node.into();
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(

View file

@ -62,7 +62,7 @@ pub(crate) fn split_static_string(
checker: &mut Checker,
attr: &str,
call: &ExprCall,
str_value: &str,
str_value: &StringLiteralValue,
) {
let ExprCall { arguments, .. } = call;
@ -115,16 +115,16 @@ pub(crate) fn split_static_string(
checker.diagnostics.push(diagnostic);
}
fn construct_replacement(elts: &[&str]) -> Expr {
fn construct_replacement(elts: &[&str], flags: StringLiteralFlags) -> Expr {
Expr::List(ExprList {
elts: elts
.iter()
.map(|elt| {
Expr::StringLiteral(ExprStringLiteral {
value: StringLiteralValue::single(StringLiteral {
value: (*elt).to_string().into_boxed_str(),
value: Box::from(*elt),
range: TextRange::default(),
flags: StringLiteralFlags::default(),
flags,
}),
range: TextRange::default(),
})
@ -135,7 +135,7 @@ fn construct_replacement(elts: &[&str]) -> Expr {
})
}
fn split_default(str_value: &str, max_split: i32) -> Option<Expr> {
fn split_default(str_value: &StringLiteralValue, max_split: i32) -> Option<Expr> {
// From the Python documentation:
// > If sep is not specified or is None, a different splitting algorithm is applied: runs of
// > consecutive whitespace are regarded as a single separator, and the result will contain
@ -151,30 +151,36 @@ fn split_default(str_value: &str, max_split: i32) -> Option<Expr> {
None
}
Ordering::Equal => {
let list_items: Vec<&str> = vec![str_value];
Some(construct_replacement(&list_items))
let list_items: Vec<&str> = vec![str_value.to_str()];
Some(construct_replacement(&list_items, str_value.flags()))
}
Ordering::Less => {
let list_items: Vec<&str> = str_value.split_whitespace().collect();
Some(construct_replacement(&list_items))
let list_items: Vec<&str> = str_value.to_str().split_whitespace().collect();
Some(construct_replacement(&list_items, str_value.flags()))
}
}
}
fn split_sep(str_value: &str, sep_value: &str, max_split: i32, direction: Direction) -> Expr {
fn split_sep(
str_value: &StringLiteralValue,
sep_value: &str,
max_split: i32,
direction: Direction,
) -> Expr {
let value = str_value.to_str();
let list_items: Vec<&str> = if let Ok(split_n) = usize::try_from(max_split) {
match direction {
Direction::Left => str_value.splitn(split_n + 1, sep_value).collect(),
Direction::Right => str_value.rsplitn(split_n + 1, sep_value).collect(),
Direction::Left => value.splitn(split_n + 1, sep_value).collect(),
Direction::Right => value.rsplitn(split_n + 1, sep_value).collect(),
}
} else {
match direction {
Direction::Left => str_value.split(sep_value).collect(),
Direction::Right => str_value.rsplit(sep_value).collect(),
Direction::Left => value.split(sep_value).collect(),
Direction::Right => value.rsplit(sep_value).collect(),
}
};
construct_replacement(&list_items)
construct_replacement(&list_items, str_value.flags())
}
/// Returns the value of the `maxsplit` argument as an `i32`, if it is a numeric value.

View file

@ -319,10 +319,10 @@ SIM905.py:29:1: SIM905 [*] Consider using a list literal instead of `str.split`
27 27 | "a,b,c,d".split(maxsplit=0)
28 28 | "VERB AUX PRON ADP DET".split(" ")
29 |-' 1 2 3 '.split()
29 |+["1", "2", "3"]
29 |+['1', '2', '3']
30 30 | '1<>2<>3<4'.split('<>')
31 31 |
32 32 | " a*a a*a a ".split("*", -1) # [' a', 'a a', 'a a ']
32 32 | " a*a a*a a ".split("*", -1) # [" a", "a a", "a a "]
SIM905.py:30:1: SIM905 [*] Consider using a list literal instead of `str.split`
|
@ -331,7 +331,7 @@ SIM905.py:30:1: SIM905 [*] Consider using a list literal instead of `str.split`
30 | '1<>2<>3<4'.split('<>')
| ^^^^^^^^^^^^^^^^^^^^^^^ SIM905
31 |
32 | " a*a a*a a ".split("*", -1) # [' a', 'a a', 'a a ']
32 | " a*a a*a a ".split("*", -1) # [" a", "a a", "a a "]
|
= help: Replace with list literal
@ -340,16 +340,16 @@ SIM905.py:30:1: SIM905 [*] Consider using a list literal instead of `str.split`
28 28 | "VERB AUX PRON ADP DET".split(" ")
29 29 | ' 1 2 3 '.split()
30 |-'1<>2<>3<4'.split('<>')
30 |+["1", "2", "3<4"]
30 |+['1', '2', '3<4']
31 31 |
32 32 | " a*a a*a a ".split("*", -1) # [' a', 'a a', 'a a ']
32 32 | " a*a a*a a ".split("*", -1) # [" a", "a a", "a a "]
33 33 | "".split() # []
SIM905.py:32:1: SIM905 [*] Consider using a list literal instead of `str.split`
|
30 | '1<>2<>3<4'.split('<>')
31 |
32 | " a*a a*a a ".split("*", -1) # [' a', 'a a', 'a a ']
32 | " a*a a*a a ".split("*", -1) # [" a", "a a", "a a "]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM905
33 | "".split() # []
34 | """
@ -360,15 +360,15 @@ SIM905.py:32:1: SIM905 [*] Consider using a list literal instead of `str.split`
29 29 | ' 1 2 3 '.split()
30 30 | '1<>2<>3<4'.split('<>')
31 31 |
32 |-" a*a a*a a ".split("*", -1) # [' a', 'a a', 'a a ']
32 |+[" a", "a a", "a a "] # [' a', 'a a', 'a a ']
32 |-" a*a a*a a ".split("*", -1) # [" a", "a a", "a a "]
32 |+[" a", "a a", "a a "] # [" a", "a a", "a a "]
33 33 | "".split() # []
34 34 | """
35 35 | """.split() # []
SIM905.py:33:1: SIM905 [*] Consider using a list literal instead of `str.split`
|
32 | " a*a a*a a ".split("*", -1) # [' a', 'a a', 'a a ']
32 | " a*a a*a a ".split("*", -1) # [" a", "a a", "a a "]
33 | "".split() # []
| ^^^^^^^^^^ SIM905
34 | """
@ -379,7 +379,7 @@ SIM905.py:33:1: SIM905 [*] Consider using a list literal instead of `str.split`
Safe fix
30 30 | '1<>2<>3<4'.split('<>')
31 31 |
32 32 | " a*a a*a a ".split("*", -1) # [' a', 'a a', 'a a ']
32 32 | " a*a a*a a ".split("*", -1) # [" a", "a a", "a a "]
33 |-"".split() # []
33 |+[] # []
34 34 | """
@ -388,25 +388,25 @@ SIM905.py:33:1: SIM905 [*] Consider using a list literal instead of `str.split`
SIM905.py:34:1: SIM905 [*] Consider using a list literal instead of `str.split`
|
32 | " a*a a*a a ".split("*", -1) # [' a', 'a a', 'a a ']
32 | " a*a a*a a ".split("*", -1) # [" a", "a a", "a a "]
33 | "".split() # []
34 | / """
35 | | """.split() # []
| |___________^ SIM905
36 | " ".split() # []
37 | "/abc/".split() # ['/abc/']
37 | "/abc/".split() # ["/abc/"]
|
= help: Replace with list literal
Safe fix
31 31 |
32 32 | " a*a a*a a ".split("*", -1) # [' a', 'a a', 'a a ']
32 32 | " a*a a*a a ".split("*", -1) # [" a", "a a", "a a "]
33 33 | "".split() # []
34 |-"""
35 |-""".split() # []
34 |+[] # []
36 35 | " ".split() # []
37 36 | "/abc/".split() # ['/abc/']
37 36 | "/abc/".split() # ["/abc/"]
38 37 | ("a,b,c"
SIM905.py:36:1: SIM905 [*] Consider using a list literal instead of `str.split`
@ -415,7 +415,7 @@ SIM905.py:36:1: SIM905 [*] Consider using a list literal instead of `str.split`
35 | """.split() # []
36 | " ".split() # []
| ^^^^^^^^^^^^^^^^^ SIM905
37 | "/abc/".split() # ['/abc/']
37 | "/abc/".split() # ["/abc/"]
38 | ("a,b,c"
|
= help: Replace with list literal
@ -426,7 +426,7 @@ SIM905.py:36:1: SIM905 [*] Consider using a list literal instead of `str.split`
35 35 | """.split() # []
36 |-" ".split() # []
36 |+[] # []
37 37 | "/abc/".split() # ['/abc/']
37 37 | "/abc/".split() # ["/abc/"]
38 38 | ("a,b,c"
39 39 | # comment
@ -434,7 +434,7 @@ SIM905.py:37:1: SIM905 [*] Consider using a list literal instead of `str.split`
|
35 | """.split() # []
36 | " ".split() # []
37 | "/abc/".split() # ['/abc/']
37 | "/abc/".split() # ["/abc/"]
| ^^^^^^^^^^^^^^^ SIM905
38 | ("a,b,c"
39 | # comment
@ -445,8 +445,8 @@ SIM905.py:37:1: SIM905 [*] Consider using a list literal instead of `str.split`
34 34 | """
35 35 | """.split() # []
36 36 | " ".split() # []
37 |-"/abc/".split() # ['/abc/']
37 |+["/abc/"] # ['/abc/']
37 |-"/abc/".split() # ["/abc/"]
37 |+["/abc/"] # ["/abc/"]
38 38 | ("a,b,c"
39 39 | # comment
40 40 | .split()
@ -454,13 +454,13 @@ SIM905.py:37:1: SIM905 [*] Consider using a list literal instead of `str.split`
SIM905.py:38:2: SIM905 [*] Consider using a list literal instead of `str.split`
|
36 | " ".split() # []
37 | "/abc/".split() # ['/abc/']
37 | "/abc/".split() # ["/abc/"]
38 | ("a,b,c"
| __^
39 | | # comment
40 | | .split()
| |________^ SIM905
41 | ) # ['a,b,c']
41 | ) # ["a,b,c"]
42 | ("a,b,c"
|
= help: Replace with list literal
@ -468,25 +468,25 @@ SIM905.py:38:2: SIM905 [*] Consider using a list literal instead of `str.split`
Unsafe fix
35 35 | """.split() # []
36 36 | " ".split() # []
37 37 | "/abc/".split() # ['/abc/']
37 37 | "/abc/".split() # ["/abc/"]
38 |-("a,b,c"
39 |-# comment
40 |-.split()
38 |+(["a,b,c"]
41 39 | ) # ['a,b,c']
41 39 | ) # ["a,b,c"]
42 40 | ("a,b,c"
43 41 | # comment1
SIM905.py:42:2: SIM905 [*] Consider using a list literal instead of `str.split`
|
40 | .split()
41 | ) # ['a,b,c']
41 | ) # ["a,b,c"]
42 | ("a,b,c"
| __^
43 | | # comment1
44 | | .split(",")
| |___________^ SIM905
45 | ) # ['a', 'b', 'c']
45 | ) # ["a", "b", "c"]
46 | ("a,"
|
= help: Replace with list literal
@ -494,19 +494,19 @@ SIM905.py:42:2: SIM905 [*] Consider using a list literal instead of `str.split`
Unsafe fix
39 39 | # comment
40 40 | .split()
41 41 | ) # ['a,b,c']
41 41 | ) # ["a,b,c"]
42 |-("a,b,c"
43 |-# comment1
44 |-.split(",")
42 |+(["a", "b", "c"]
45 43 | ) # ['a', 'b', 'c']
45 43 | ) # ["a", "b", "c"]
46 44 | ("a,"
47 45 | # comment
SIM905.py:46:2: SIM905 [*] Consider using a list literal instead of `str.split`
|
44 | .split(",")
45 | ) # ['a', 'b', 'c']
45 | ) # ["a", "b", "c"]
46 | ("a,"
| __^
47 | | # comment
@ -514,320 +514,320 @@ SIM905.py:46:2: SIM905 [*] Consider using a list literal instead of `str.split`
49 | | "c"
50 | | .split(",")
| |___________^ SIM905
51 | ) # ['a', 'b', 'c']
51 | ) # ["a", "b", "c"]
|
= help: Replace with list literal
Unsafe fix
43 43 | # comment1
44 44 | .split(",")
45 45 | ) # ['a', 'b', 'c']
45 45 | ) # ["a", "b", "c"]
46 |-("a,"
47 |-# comment
48 |-"b,"
49 |-"c"
50 |-.split(",")
46 |+(["a", "b", "c"]
51 47 | ) # ['a', 'b', 'c']
51 47 | ) # ["a", "b", "c"]
52 48 |
53 49 | "hello "\
SIM905.py:53:1: SIM905 [*] Consider using a list literal instead of `str.split`
|
51 | ) # ['a', 'b', 'c']
51 | ) # ["a", "b", "c"]
52 |
53 | / "hello "\
54 | | "world".split()
| |___________________^ SIM905
55 | # ['hello', 'world']
55 | # ["hello", "world"]
|
= help: Replace with list literal
Safe fix
50 50 | .split(",")
51 51 | ) # ['a', 'b', 'c']
51 51 | ) # ["a", "b", "c"]
52 52 |
53 |-"hello "\
54 |- "world".split()
53 |+["hello", "world"]
55 54 | # ['hello', 'world']
55 54 | # ["hello", "world"]
56 55 |
57 56 | # prefixes and isc
SIM905.py:58:1: SIM905 [*] Consider using a list literal instead of `str.split`
|
57 | # prefixes and isc
58 | u"a b".split() # ['a', 'b']
58 | u"a b".split() # [u"a", u"b"]
| ^^^^^^^^^^^^^^ SIM905
59 | r"a \n b".split() # ['a', '\\n', 'b']
60 | ("a " "b").split() # ['a', 'b']
59 | r"a \n b".split() # [r"a", r"\n", r"b"]
60 | ("a " "b").split() # ["a", "b"]
|
= help: Replace with list literal
Safe fix
55 55 | # ['hello', 'world']
55 55 | # ["hello", "world"]
56 56 |
57 57 | # prefixes and isc
58 |-u"a b".split() # ['a', 'b']
58 |+["a", "b"] # ['a', 'b']
59 59 | r"a \n b".split() # ['a', '\\n', 'b']
60 60 | ("a " "b").split() # ['a', 'b']
61 61 | "a " "b".split() # ['a', 'b']
58 |-u"a b".split() # [u"a", u"b"]
58 |+[u"a", u"b"] # [u"a", u"b"]
59 59 | r"a \n b".split() # [r"a", r"\n", r"b"]
60 60 | ("a " "b").split() # ["a", "b"]
61 61 | "a " "b".split() # ["a", "b"]
SIM905.py:59:1: SIM905 [*] Consider using a list literal instead of `str.split`
|
57 | # prefixes and isc
58 | u"a b".split() # ['a', 'b']
59 | r"a \n b".split() # ['a', '\\n', 'b']
58 | u"a b".split() # [u"a", u"b"]
59 | r"a \n b".split() # [r"a", r"\n", r"b"]
| ^^^^^^^^^^^^^^^^^ SIM905
60 | ("a " "b").split() # ['a', 'b']
61 | "a " "b".split() # ['a', 'b']
60 | ("a " "b").split() # ["a", "b"]
61 | "a " "b".split() # ["a", "b"]
|
= help: Replace with list literal
Safe fix
56 56 |
57 57 | # prefixes and isc
58 58 | u"a b".split() # ['a', 'b']
59 |-r"a \n b".split() # ['a', '\\n', 'b']
59 |+["a", "\\n", "b"] # ['a', '\\n', 'b']
60 60 | ("a " "b").split() # ['a', 'b']
61 61 | "a " "b".split() # ['a', 'b']
62 62 | u"a " "b".split() # ['a', 'b']
58 58 | u"a b".split() # [u"a", u"b"]
59 |-r"a \n b".split() # [r"a", r"\n", r"b"]
59 |+[r"a", r"\n", r"b"] # [r"a", r"\n", r"b"]
60 60 | ("a " "b").split() # ["a", "b"]
61 61 | "a " "b".split() # ["a", "b"]
62 62 | u"a " "b".split() # [u"a", u"b"]
SIM905.py:60:1: SIM905 [*] Consider using a list literal instead of `str.split`
|
58 | u"a b".split() # ['a', 'b']
59 | r"a \n b".split() # ['a', '\\n', 'b']
60 | ("a " "b").split() # ['a', 'b']
58 | u"a b".split() # [u"a", u"b"]
59 | r"a \n b".split() # [r"a", r"\n", r"b"]
60 | ("a " "b").split() # ["a", "b"]
| ^^^^^^^^^^^^^^^^^^ SIM905
61 | "a " "b".split() # ['a', 'b']
62 | u"a " "b".split() # ['a', 'b']
61 | "a " "b".split() # ["a", "b"]
62 | u"a " "b".split() # [u"a", u"b"]
|
= help: Replace with list literal
Safe fix
57 57 | # prefixes and isc
58 58 | u"a b".split() # ['a', 'b']
59 59 | r"a \n b".split() # ['a', '\\n', 'b']
60 |-("a " "b").split() # ['a', 'b']
60 |+["a", "b"] # ['a', 'b']
61 61 | "a " "b".split() # ['a', 'b']
62 62 | u"a " "b".split() # ['a', 'b']
63 63 | "a " u"b".split() # ['a', 'b']
58 58 | u"a b".split() # [u"a", u"b"]
59 59 | r"a \n b".split() # [r"a", r"\n", r"b"]
60 |-("a " "b").split() # ["a", "b"]
60 |+["a", "b"] # ["a", "b"]
61 61 | "a " "b".split() # ["a", "b"]
62 62 | u"a " "b".split() # [u"a", u"b"]
63 63 | "a " u"b".split() # ["a", "b"]
SIM905.py:61:1: SIM905 [*] Consider using a list literal instead of `str.split`
|
59 | r"a \n b".split() # ['a', '\\n', 'b']
60 | ("a " "b").split() # ['a', 'b']
61 | "a " "b".split() # ['a', 'b']
59 | r"a \n b".split() # [r"a", r"\n", r"b"]
60 | ("a " "b").split() # ["a", "b"]
61 | "a " "b".split() # ["a", "b"]
| ^^^^^^^^^^^^^^^^ SIM905
62 | u"a " "b".split() # ['a', 'b']
63 | "a " u"b".split() # ['a', 'b']
62 | u"a " "b".split() # [u"a", u"b"]
63 | "a " u"b".split() # ["a", "b"]
|
= help: Replace with list literal
Safe fix
58 58 | u"a b".split() # ['a', 'b']
59 59 | r"a \n b".split() # ['a', '\\n', 'b']
60 60 | ("a " "b").split() # ['a', 'b']
61 |-"a " "b".split() # ['a', 'b']
61 |+["a", "b"] # ['a', 'b']
62 62 | u"a " "b".split() # ['a', 'b']
63 63 | "a " u"b".split() # ['a', 'b']
64 64 | u"a " r"\n".split() # ['a', '\\n']
58 58 | u"a b".split() # [u"a", u"b"]
59 59 | r"a \n b".split() # [r"a", r"\n", r"b"]
60 60 | ("a " "b").split() # ["a", "b"]
61 |-"a " "b".split() # ["a", "b"]
61 |+["a", "b"] # ["a", "b"]
62 62 | u"a " "b".split() # [u"a", u"b"]
63 63 | "a " u"b".split() # ["a", "b"]
64 64 | u"a " r"\n".split() # [u"a", u"\\n"]
SIM905.py:62:1: SIM905 [*] Consider using a list literal instead of `str.split`
|
60 | ("a " "b").split() # ['a', 'b']
61 | "a " "b".split() # ['a', 'b']
62 | u"a " "b".split() # ['a', 'b']
60 | ("a " "b").split() # ["a", "b"]
61 | "a " "b".split() # ["a", "b"]
62 | u"a " "b".split() # [u"a", u"b"]
| ^^^^^^^^^^^^^^^^^ SIM905
63 | "a " u"b".split() # ['a', 'b']
64 | u"a " r"\n".split() # ['a', '\\n']
63 | "a " u"b".split() # ["a", "b"]
64 | u"a " r"\n".split() # [u"a", u"\\n"]
|
= help: Replace with list literal
Safe fix
59 59 | r"a \n b".split() # ['a', '\\n', 'b']
60 60 | ("a " "b").split() # ['a', 'b']
61 61 | "a " "b".split() # ['a', 'b']
62 |-u"a " "b".split() # ['a', 'b']
62 |+["a", "b"] # ['a', 'b']
63 63 | "a " u"b".split() # ['a', 'b']
64 64 | u"a " r"\n".split() # ['a', '\\n']
65 65 | r"\n " u"\n".split() # ['\\n']
59 59 | r"a \n b".split() # [r"a", r"\n", r"b"]
60 60 | ("a " "b").split() # ["a", "b"]
61 61 | "a " "b".split() # ["a", "b"]
62 |-u"a " "b".split() # [u"a", u"b"]
62 |+[u"a", u"b"] # [u"a", u"b"]
63 63 | "a " u"b".split() # ["a", "b"]
64 64 | u"a " r"\n".split() # [u"a", u"\\n"]
65 65 | r"\n " u"\n".split() # [r"\n"]
SIM905.py:63:1: SIM905 [*] Consider using a list literal instead of `str.split`
|
61 | "a " "b".split() # ['a', 'b']
62 | u"a " "b".split() # ['a', 'b']
63 | "a " u"b".split() # ['a', 'b']
61 | "a " "b".split() # ["a", "b"]
62 | u"a " "b".split() # [u"a", u"b"]
63 | "a " u"b".split() # ["a", "b"]
| ^^^^^^^^^^^^^^^^^ SIM905
64 | u"a " r"\n".split() # ['a', '\\n']
65 | r"\n " u"\n".split() # ['\\n']
64 | u"a " r"\n".split() # [u"a", u"\\n"]
65 | r"\n " u"\n".split() # [r"\n"]
|
= help: Replace with list literal
Safe fix
60 60 | ("a " "b").split() # ['a', 'b']
61 61 | "a " "b".split() # ['a', 'b']
62 62 | u"a " "b".split() # ['a', 'b']
63 |-"a " u"b".split() # ['a', 'b']
63 |+["a", "b"] # ['a', 'b']
64 64 | u"a " r"\n".split() # ['a', '\\n']
65 65 | r"\n " u"\n".split() # ['\\n']
66 66 | r"\n " "\n".split() # ['\\n']
60 60 | ("a " "b").split() # ["a", "b"]
61 61 | "a " "b".split() # ["a", "b"]
62 62 | u"a " "b".split() # [u"a", u"b"]
63 |-"a " u"b".split() # ["a", "b"]
63 |+["a", "b"] # ["a", "b"]
64 64 | u"a " r"\n".split() # [u"a", u"\\n"]
65 65 | r"\n " u"\n".split() # [r"\n"]
66 66 | r"\n " "\n".split() # [r"\n"]
SIM905.py:64:1: SIM905 [*] Consider using a list literal instead of `str.split`
|
62 | u"a " "b".split() # ['a', 'b']
63 | "a " u"b".split() # ['a', 'b']
64 | u"a " r"\n".split() # ['a', '\\n']
62 | u"a " "b".split() # [u"a", u"b"]
63 | "a " u"b".split() # ["a", "b"]
64 | u"a " r"\n".split() # [u"a", u"\\n"]
| ^^^^^^^^^^^^^^^^^^^ SIM905
65 | r"\n " u"\n".split() # ['\\n']
66 | r"\n " "\n".split() # ['\\n']
65 | r"\n " u"\n".split() # [r"\n"]
66 | r"\n " "\n".split() # [r"\n"]
|
= help: Replace with list literal
Safe fix
61 61 | "a " "b".split() # ['a', 'b']
62 62 | u"a " "b".split() # ['a', 'b']
63 63 | "a " u"b".split() # ['a', 'b']
64 |-u"a " r"\n".split() # ['a', '\\n']
64 |+["a", "\\n"] # ['a', '\\n']
65 65 | r"\n " u"\n".split() # ['\\n']
66 66 | r"\n " "\n".split() # ['\\n']
67 67 | "a " r"\n".split() # ['a', '\\n']
61 61 | "a " "b".split() # ["a", "b"]
62 62 | u"a " "b".split() # [u"a", u"b"]
63 63 | "a " u"b".split() # ["a", "b"]
64 |-u"a " r"\n".split() # [u"a", u"\\n"]
64 |+[u"a", u"\\n"] # [u"a", u"\\n"]
65 65 | r"\n " u"\n".split() # [r"\n"]
66 66 | r"\n " "\n".split() # [r"\n"]
67 67 | "a " r"\n".split() # ["a", "\\n"]
SIM905.py:65:1: SIM905 [*] Consider using a list literal instead of `str.split`
|
63 | "a " u"b".split() # ['a', 'b']
64 | u"a " r"\n".split() # ['a', '\\n']
65 | r"\n " u"\n".split() # ['\\n']
63 | "a " u"b".split() # ["a", "b"]
64 | u"a " r"\n".split() # [u"a", u"\\n"]
65 | r"\n " u"\n".split() # [r"\n"]
| ^^^^^^^^^^^^^^^^^^^^ SIM905
66 | r"\n " "\n".split() # ['\\n']
67 | "a " r"\n".split() # ['a', '\\n']
66 | r"\n " "\n".split() # [r"\n"]
67 | "a " r"\n".split() # ["a", "\\n"]
|
= help: Replace with list literal
Safe fix
62 62 | u"a " "b".split() # ['a', 'b']
63 63 | "a " u"b".split() # ['a', 'b']
64 64 | u"a " r"\n".split() # ['a', '\\n']
65 |-r"\n " u"\n".split() # ['\\n']
65 |+["\\n"] # ['\\n']
66 66 | r"\n " "\n".split() # ['\\n']
67 67 | "a " r"\n".split() # ['a', '\\n']
62 62 | u"a " "b".split() # [u"a", u"b"]
63 63 | "a " u"b".split() # ["a", "b"]
64 64 | u"a " r"\n".split() # [u"a", u"\\n"]
65 |-r"\n " u"\n".split() # [r"\n"]
65 |+[r"\n"] # [r"\n"]
66 66 | r"\n " "\n".split() # [r"\n"]
67 67 | "a " r"\n".split() # ["a", "\\n"]
68 68 |
SIM905.py:66:1: SIM905 [*] Consider using a list literal instead of `str.split`
|
64 | u"a " r"\n".split() # ['a', '\\n']
65 | r"\n " u"\n".split() # ['\\n']
66 | r"\n " "\n".split() # ['\\n']
64 | u"a " r"\n".split() # [u"a", u"\\n"]
65 | r"\n " u"\n".split() # [r"\n"]
66 | r"\n " "\n".split() # [r"\n"]
| ^^^^^^^^^^^^^^^^^^^ SIM905
67 | "a " r"\n".split() # ['a', '\\n']
67 | "a " r"\n".split() # ["a", "\\n"]
|
= help: Replace with list literal
Safe fix
63 63 | "a " u"b".split() # ['a', 'b']
64 64 | u"a " r"\n".split() # ['a', '\\n']
65 65 | r"\n " u"\n".split() # ['\\n']
66 |-r"\n " "\n".split() # ['\\n']
66 |+["\\n"] # ['\\n']
67 67 | "a " r"\n".split() # ['a', '\\n']
63 63 | "a " u"b".split() # ["a", "b"]
64 64 | u"a " r"\n".split() # [u"a", u"\\n"]
65 65 | r"\n " u"\n".split() # [r"\n"]
66 |-r"\n " "\n".split() # [r"\n"]
66 |+[r"\n"] # [r"\n"]
67 67 | "a " r"\n".split() # ["a", "\\n"]
68 68 |
69 69 | "a,b,c".split(',', maxsplit=0) # ['a,b,c']
69 69 | "a,b,c".split(',', maxsplit=0) # ["a,b,c"]
SIM905.py:67:1: SIM905 [*] Consider using a list literal instead of `str.split`
|
65 | r"\n " u"\n".split() # ['\\n']
66 | r"\n " "\n".split() # ['\\n']
67 | "a " r"\n".split() # ['a', '\\n']
65 | r"\n " u"\n".split() # [r"\n"]
66 | r"\n " "\n".split() # [r"\n"]
67 | "a " r"\n".split() # ["a", "\\n"]
| ^^^^^^^^^^^^^^^^^^ SIM905
68 |
69 | "a,b,c".split(',', maxsplit=0) # ['a,b,c']
69 | "a,b,c".split(',', maxsplit=0) # ["a,b,c"]
|
= help: Replace with list literal
Safe fix
64 64 | u"a " r"\n".split() # ['a', '\\n']
65 65 | r"\n " u"\n".split() # ['\\n']
66 66 | r"\n " "\n".split() # ['\\n']
67 |-"a " r"\n".split() # ['a', '\\n']
67 |+["a", "\\n"] # ['a', '\\n']
64 64 | u"a " r"\n".split() # [u"a", u"\\n"]
65 65 | r"\n " u"\n".split() # [r"\n"]
66 66 | r"\n " "\n".split() # [r"\n"]
67 |-"a " r"\n".split() # ["a", "\\n"]
67 |+["a", "\\n"] # ["a", "\\n"]
68 68 |
69 69 | "a,b,c".split(',', maxsplit=0) # ['a,b,c']
70 70 | "a,b,c".split(',', maxsplit=-1) # ['a', 'b', 'c']
69 69 | "a,b,c".split(',', maxsplit=0) # ["a,b,c"]
70 70 | "a,b,c".split(',', maxsplit=-1) # ["a", "b", "c"]
SIM905.py:69:1: SIM905 [*] Consider using a list literal instead of `str.split`
|
67 | "a " r"\n".split() # ['a', '\\n']
67 | "a " r"\n".split() # ["a", "\\n"]
68 |
69 | "a,b,c".split(',', maxsplit=0) # ['a,b,c']
69 | "a,b,c".split(',', maxsplit=0) # ["a,b,c"]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM905
70 | "a,b,c".split(',', maxsplit=-1) # ['a', 'b', 'c']
71 | "a,b,c".split(',', maxsplit=-2) # ['a', 'b', 'c']
70 | "a,b,c".split(',', maxsplit=-1) # ["a", "b", "c"]
71 | "a,b,c".split(',', maxsplit=-2) # ["a", "b", "c"]
|
= help: Replace with list literal
Safe fix
66 66 | r"\n " "\n".split() # ['\\n']
67 67 | "a " r"\n".split() # ['a', '\\n']
66 66 | r"\n " "\n".split() # [r"\n"]
67 67 | "a " r"\n".split() # ["a", "\\n"]
68 68 |
69 |-"a,b,c".split(',', maxsplit=0) # ['a,b,c']
69 |+["a,b,c"] # ['a,b,c']
70 70 | "a,b,c".split(',', maxsplit=-1) # ['a', 'b', 'c']
71 71 | "a,b,c".split(',', maxsplit=-2) # ['a', 'b', 'c']
72 72 | "a,b,c".split(',', maxsplit=-0) # ['a,b,c']
69 |-"a,b,c".split(',', maxsplit=0) # ["a,b,c"]
69 |+["a,b,c"] # ["a,b,c"]
70 70 | "a,b,c".split(',', maxsplit=-1) # ["a", "b", "c"]
71 71 | "a,b,c".split(',', maxsplit=-2) # ["a", "b", "c"]
72 72 | "a,b,c".split(',', maxsplit=-0) # ["a,b,c"]
SIM905.py:70:1: SIM905 [*] Consider using a list literal instead of `str.split`
|
69 | "a,b,c".split(',', maxsplit=0) # ['a,b,c']
70 | "a,b,c".split(',', maxsplit=-1) # ['a', 'b', 'c']
69 | "a,b,c".split(',', maxsplit=0) # ["a,b,c"]
70 | "a,b,c".split(',', maxsplit=-1) # ["a", "b", "c"]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM905
71 | "a,b,c".split(',', maxsplit=-2) # ['a', 'b', 'c']
72 | "a,b,c".split(',', maxsplit=-0) # ['a,b,c']
71 | "a,b,c".split(',', maxsplit=-2) # ["a", "b", "c"]
72 | "a,b,c".split(',', maxsplit=-0) # ["a,b,c"]
|
= help: Replace with list literal
Safe fix
67 67 | "a " r"\n".split() # ['a', '\\n']
67 67 | "a " r"\n".split() # ["a", "\\n"]
68 68 |
69 69 | "a,b,c".split(',', maxsplit=0) # ['a,b,c']
70 |-"a,b,c".split(',', maxsplit=-1) # ['a', 'b', 'c']
70 |+["a", "b", "c"] # ['a', 'b', 'c']
71 71 | "a,b,c".split(',', maxsplit=-2) # ['a', 'b', 'c']
72 72 | "a,b,c".split(',', maxsplit=-0) # ['a,b,c']
69 69 | "a,b,c".split(',', maxsplit=0) # ["a,b,c"]
70 |-"a,b,c".split(',', maxsplit=-1) # ["a", "b", "c"]
70 |+["a", "b", "c"] # ["a", "b", "c"]
71 71 | "a,b,c".split(',', maxsplit=-2) # ["a", "b", "c"]
72 72 | "a,b,c".split(',', maxsplit=-0) # ["a,b,c"]
73 73 |
SIM905.py:71:1: SIM905 [*] Consider using a list literal instead of `str.split`
|
69 | "a,b,c".split(',', maxsplit=0) # ['a,b,c']
70 | "a,b,c".split(',', maxsplit=-1) # ['a', 'b', 'c']
71 | "a,b,c".split(',', maxsplit=-2) # ['a', 'b', 'c']
69 | "a,b,c".split(',', maxsplit=0) # ["a,b,c"]
70 | "a,b,c".split(',', maxsplit=-1) # ["a", "b", "c"]
71 | "a,b,c".split(',', maxsplit=-2) # ["a", "b", "c"]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM905
72 | "a,b,c".split(',', maxsplit=-0) # ['a,b,c']
72 | "a,b,c".split(',', maxsplit=-0) # ["a,b,c"]
|
= help: Replace with list literal
Safe fix
68 68 |
69 69 | "a,b,c".split(',', maxsplit=0) # ['a,b,c']
70 70 | "a,b,c".split(',', maxsplit=-1) # ['a', 'b', 'c']
71 |-"a,b,c".split(',', maxsplit=-2) # ['a', 'b', 'c']
71 |+["a", "b", "c"] # ['a', 'b', 'c']
72 72 | "a,b,c".split(',', maxsplit=-0) # ['a,b,c']
69 69 | "a,b,c".split(',', maxsplit=0) # ["a,b,c"]
70 70 | "a,b,c".split(',', maxsplit=-1) # ["a", "b", "c"]
71 |-"a,b,c".split(',', maxsplit=-2) # ["a", "b", "c"]
71 |+["a", "b", "c"] # ["a", "b", "c"]
72 72 | "a,b,c".split(',', maxsplit=-0) # ["a,b,c"]
73 73 |
74 74 | # negatives
SIM905.py:72:1: SIM905 [*] Consider using a list literal instead of `str.split`
|
70 | "a,b,c".split(',', maxsplit=-1) # ['a', 'b', 'c']
71 | "a,b,c".split(',', maxsplit=-2) # ['a', 'b', 'c']
72 | "a,b,c".split(',', maxsplit=-0) # ['a,b,c']
70 | "a,b,c".split(',', maxsplit=-1) # ["a", "b", "c"]
71 | "a,b,c".split(',', maxsplit=-2) # ["a", "b", "c"]
72 | "a,b,c".split(',', maxsplit=-0) # ["a,b,c"]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM905
73 |
74 | # negatives
@ -835,11 +835,11 @@ SIM905.py:72:1: SIM905 [*] Consider using a list literal instead of `str.split`
= help: Replace with list literal
Safe fix
69 69 | "a,b,c".split(',', maxsplit=0) # ['a,b,c']
70 70 | "a,b,c".split(',', maxsplit=-1) # ['a', 'b', 'c']
71 71 | "a,b,c".split(',', maxsplit=-2) # ['a', 'b', 'c']
72 |-"a,b,c".split(',', maxsplit=-0) # ['a,b,c']
72 |+["a,b,c"] # ['a,b,c']
69 69 | "a,b,c".split(',', maxsplit=0) # ["a,b,c"]
70 70 | "a,b,c".split(',', maxsplit=-1) # ["a", "b", "c"]
71 71 | "a,b,c".split(',', maxsplit=-2) # ["a", "b", "c"]
72 |-"a,b,c".split(',', maxsplit=-0) # ["a,b,c"]
72 |+["a,b,c"] # ["a,b,c"]
73 73 |
74 74 | # negatives
75 75 |

View file

@ -3,8 +3,9 @@ use std::cmp::Reverse;
use ruff_diagnostics::Edit;
use ruff_python_ast::helpers::{map_callable, map_subscript};
use ruff_python_ast::name::QualifiedName;
use ruff_python_ast::str::Quote;
use ruff_python_ast::visitor::transformer::{walk_expr, Transformer};
use ruff_python_ast::{self as ast, Decorator, Expr};
use ruff_python_ast::{self as ast, Decorator, Expr, StringLiteralFlags};
use ruff_python_codegen::{Generator, Stylist};
use ruff_python_parser::typing::parse_type_annotation;
use ruff_python_semantic::{
@ -249,6 +250,7 @@ pub(crate) fn quote_annotation(
semantic: &SemanticModel,
stylist: &Stylist,
locator: &Locator,
flags: StringLiteralFlags,
) -> Edit {
let expr = semantic.expression(node_id).expect("Expression not found");
if let Some(parent_id) = semantic.parent_expression_id(node_id) {
@ -258,7 +260,7 @@ pub(crate) fn quote_annotation(
// If we're quoting the value of a subscript, we need to quote the entire
// expression. For example, when quoting `DataFrame` in `DataFrame[int]`, we
// should generate `"DataFrame[int]"`.
return quote_annotation(parent_id, semantic, stylist, locator);
return quote_annotation(parent_id, semantic, stylist, locator, flags);
}
}
Some(Expr::Attribute(parent)) => {
@ -266,7 +268,7 @@ pub(crate) fn quote_annotation(
// If we're quoting the value of an attribute, we need to quote the entire
// expression. For example, when quoting `DataFrame` in `pd.DataFrame`, we
// should generate `"pd.DataFrame"`.
return quote_annotation(parent_id, semantic, stylist, locator);
return quote_annotation(parent_id, semantic, stylist, locator, flags);
}
}
Some(Expr::Call(parent)) => {
@ -274,7 +276,7 @@ pub(crate) fn quote_annotation(
// If we're quoting the function of a call, we need to quote the entire
// expression. For example, when quoting `DataFrame` in `DataFrame()`, we
// should generate `"DataFrame()"`.
return quote_annotation(parent_id, semantic, stylist, locator);
return quote_annotation(parent_id, semantic, stylist, locator, flags);
}
}
Some(Expr::BinOp(parent)) => {
@ -282,14 +284,14 @@ pub(crate) fn quote_annotation(
// If we're quoting the left or right side of a binary operation, we need to
// quote the entire expression. For example, when quoting `DataFrame` in
// `DataFrame | Series`, we should generate `"DataFrame | Series"`.
return quote_annotation(parent_id, semantic, stylist, locator);
return quote_annotation(parent_id, semantic, stylist, locator, flags);
}
}
_ => {}
}
}
quote_type_expression(expr, semantic, stylist, locator)
quote_type_expression(expr, semantic, stylist, locator, flags)
}
/// Wrap a type expression in quotes.
@ -305,9 +307,10 @@ pub(crate) fn quote_type_expression(
semantic: &SemanticModel,
stylist: &Stylist,
locator: &Locator,
flags: StringLiteralFlags,
) -> Edit {
// Quote the entire expression.
let quote_annotator = QuoteAnnotator::new(semantic, stylist, locator);
let quote_annotator = QuoteAnnotator::new(semantic, stylist, locator, flags);
Edit::range_replacement(quote_annotator.into_annotation(expr), expr.range())
}
@ -336,6 +339,7 @@ pub(crate) struct QuoteAnnotator<'a> {
semantic: &'a SemanticModel<'a>,
stylist: &'a Stylist<'a>,
locator: &'a Locator<'a>,
flags: StringLiteralFlags,
}
impl<'a> QuoteAnnotator<'a> {
@ -343,11 +347,13 @@ impl<'a> QuoteAnnotator<'a> {
semantic: &'a SemanticModel<'a>,
stylist: &'a Stylist<'a>,
locator: &'a Locator<'a>,
flags: StringLiteralFlags,
) -> Self {
Self {
semantic,
stylist,
locator,
flags,
}
}
@ -366,7 +372,7 @@ impl<'a> QuoteAnnotator<'a> {
generator.expr(&Expr::from(ast::StringLiteral {
range: TextRange::default(),
value: annotation.into_boxed_str(),
flags: ast::StringLiteralFlags::default(),
flags: self.flags,
}))
}
@ -376,6 +382,12 @@ impl<'a> QuoteAnnotator<'a> {
if let Expr::Tuple(ast::ExprTuple { elts, .. }) = slice {
if !elts.is_empty() {
self.visit_expr(&mut elts[0]);
// The outer annotation will use the preferred quote.
// As such, any quotes found in metadata elements inside an `Annotated` slice
// should use the opposite quote to the preferred quote.
for elt in elts.iter_mut().skip(1) {
QuoteRewriter::new(self.stylist).visit_expr(elt);
}
}
}
}
@ -390,8 +402,10 @@ impl Transformer for QuoteAnnotator<'_> {
.semantic
.match_typing_qualified_name(&qualified_name, "Literal")
{
// we don't want to modify anything inside `Literal`
// so skip visiting this subscripts' slice
// The outer annotation will use the preferred quote.
// As such, any quotes found inside a `Literal` slice
// should use the opposite quote to the preferred quote.
QuoteRewriter::new(self.stylist).visit_expr(slice);
} else if self
.semantic
.match_typing_qualified_name(&qualified_name, "Annotated")
@ -420,3 +434,24 @@ impl Transformer for QuoteAnnotator<'_> {
}
}
}
/// A [`Transformer`] struct that rewrites all strings in an expression
/// to use a specified quotation style
#[derive(Debug)]
struct QuoteRewriter {
preferred_inner_quote: Quote,
}
impl QuoteRewriter {
fn new(stylist: &Stylist) -> Self {
Self {
preferred_inner_quote: stylist.quote().opposite(),
}
}
}
impl Transformer for QuoteRewriter {
fn visit_string_literal(&self, literal: &mut ast::StringLiteral) {
literal.flags = literal.flags.with_quote_style(self.preferred_inner_quote);
}
}

View file

@ -68,6 +68,7 @@ pub(crate) fn runtime_cast_value(checker: &mut Checker, type_expr: &Expr) {
checker.semantic(),
checker.stylist(),
checker.locator(),
checker.default_string_flags(),
);
if checker.comment_ranges().intersects(type_expr.range()) {
diagnostic.set_fix(Fix::unsafe_edit(edit));

View file

@ -278,6 +278,7 @@ fn quote_imports(checker: &Checker, node_id: NodeId, imports: &[ImportBinding])
checker.semantic(),
checker.stylist(),
checker.locator(),
checker.default_string_flags(),
))
} else {
None

View file

@ -175,6 +175,7 @@ pub(crate) fn unquoted_type_alias(checker: &Checker, binding: &Binding) -> Optio
checker.semantic(),
checker.stylist(),
checker.locator(),
checker.default_string_flags(),
);
let mut diagnostics = Vec::with_capacity(names.len());
for name in names {

View file

@ -510,6 +510,7 @@ fn fix_imports(checker: &Checker, node_id: NodeId, imports: &[ImportBinding]) ->
checker.semantic(),
checker.stylist(),
checker.locator(),
checker.default_string_flags(),
))
} else {
None

View file

@ -63,11 +63,16 @@ fn is_static_length(elts: &[Expr]) -> bool {
fn build_fstring(joiner: &str, joinees: &[Expr]) -> Option<Expr> {
// If all elements are string constants, join them into a single string.
if joinees.iter().all(Expr::is_string_literal_expr) {
let mut flags = None;
let node = ast::StringLiteral {
value: joinees
.iter()
.filter_map(|expr| {
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = expr {
if flags.is_none() {
// take the flags from the first Expr
flags = Some(value.flags());
}
Some(value.to_str())
} else {
None
@ -75,7 +80,8 @@ fn build_fstring(joiner: &str, joinees: &[Expr]) -> Option<Expr> {
})
.join(joiner)
.into_boxed_str(),
..ast::StringLiteral::default()
flags: flags?,
range: TextRange::default(),
};
return Some(node.into());
}

View file

@ -3,7 +3,7 @@ use std::fmt::{Display, Formatter};
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::name::QualifiedName;
use ruff_python_ast::{self as ast, Expr, StringLiteralFlags};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_semantic::SemanticModel;
use ruff_text_size::{Ranged, TextRange};
@ -24,7 +24,7 @@ use crate::fix::edits::add_argument;
/// encoding. [PEP 597] recommends the use of `encoding="utf-8"` as a default,
/// and suggests that it may become the default in future versions of Python.
///
/// If a local-specific encoding is intended, use `encoding="local"` on
/// If a locale-specific encoding is intended, use `encoding="locale"` on
/// Python 3.10 and later, or `locale.getpreferredencoding()` on earlier versions,
/// to make the encoding explicit.
///
@ -158,14 +158,9 @@ fn generate_keyword_fix(checker: &Checker, call: &ast::ExprCall) -> Fix {
Fix::unsafe_edit(add_argument(
&format!(
"encoding={}",
checker
.generator()
.expr(&Expr::StringLiteral(ast::ExprStringLiteral {
value: ast::StringLiteralValue::single(ast::StringLiteral {
value: "utf-8".to_string().into_boxed_str(),
flags: StringLiteralFlags::default(),
range: TextRange::default(),
}),
checker.generator().expr(&Expr::from(ast::StringLiteral {
value: Box::from("utf-8"),
flags: checker.default_string_flags(),
range: TextRange::default(),
}))
),

View file

@ -3,7 +3,7 @@ use std::str::FromStr;
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::{self as ast, Expr, Int, LiteralExpressionRef, UnaryOp};
use ruff_python_ast::{self as ast, Expr, Int, LiteralExpressionRef, StringLiteralFlags, UnaryOp};
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
@ -33,9 +33,14 @@ impl FromStr for LiteralType {
}
impl LiteralType {
fn as_zero_value_expr(self) -> Expr {
fn as_zero_value_expr(self, flags: StringLiteralFlags) -> Expr {
match self {
LiteralType::Str => ast::ExprStringLiteral::default().into(),
LiteralType::Str => ast::StringLiteral {
value: Box::default(),
range: TextRange::default(),
flags,
}
.into(),
LiteralType::Bytes => ast::ExprBytesLiteral::default().into(),
LiteralType::Int => ast::ExprNumberLiteral {
value: ast::Number::Int(Int::from(0u8)),
@ -186,7 +191,7 @@ pub(crate) fn native_literals(
return;
}
let expr = literal_type.as_zero_value_expr();
let expr = literal_type.as_zero_value_expr(checker.default_string_flags());
let content = checker.generator().expr(&expr);
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
content,

View file

@ -1,6 +1,5 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
snapshot_kind: text
---
UP007.py:5:10: UP007 [*] Use `X | Y` for type annotations
|
@ -295,3 +294,23 @@ UP007.py:83:10: UP007 [*] Use `X | Y` for type annotations
83 |-def f(x: Union[int, str, bytes]) -> None:
83 |+def f(x: int | str | bytes) -> None:
84 84 | ...
85 85 |
86 86 |
UP007.py:91:26: UP007 [*] Use `X | Y` for type annotations
|
89 | ...
90 |
91 | def myfunc(param: "tuple[Union[int, 'AClass', None], str]"):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ UP007
92 | print(param)
|
= help: Convert to `X | Y`
Safe fix
88 88 | class AClass:
89 89 | ...
90 90 |
91 |-def myfunc(param: "tuple[Union[int, 'AClass', None], str]"):
91 |+def myfunc(param: "tuple[int | 'AClass' | None, str]"):
92 92 | print(param)

View file

@ -66,7 +66,8 @@ pub(crate) fn assert_with_print_message(checker: &mut Checker, stmt: &ast::StmtA
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
checker.generator().stmt(&Stmt::Assert(ast::StmtAssert {
test: stmt.test.clone(),
msg: print_arguments::to_expr(&call.arguments).map(Box::new),
msg: print_arguments::to_expr(&call.arguments, checker.default_string_flags())
.map(Box::new),
range: TextRange::default(),
})),
// We have to replace the entire statement,
@ -140,12 +141,13 @@ mod print_arguments {
/// literals.
fn fstring_elements_to_string_literals<'a>(
mut elements: impl ExactSizeIterator<Item = &'a FStringElement>,
flags: StringLiteralFlags,
) -> Option<Vec<StringLiteral>> {
elements.try_fold(Vec::with_capacity(elements.len()), |mut acc, element| {
if let FStringElement::Literal(literal) = element {
acc.push(StringLiteral {
value: literal.value.clone(),
flags: StringLiteralFlags::default(),
flags,
range: TextRange::default(),
});
Some(acc)
@ -162,6 +164,7 @@ mod print_arguments {
fn args_to_string_literal_expr<'a>(
args: impl ExactSizeIterator<Item = &'a Vec<FStringElement>>,
sep: impl ExactSizeIterator<Item = &'a FStringElement>,
flags: StringLiteralFlags,
) -> Option<Expr> {
// If there are no arguments, short-circuit and return `None`
if args.len() == 0 {
@ -174,8 +177,8 @@ mod print_arguments {
// of a concatenated string literal. (e.g. "text", "text" "text") The `sep` will
// be inserted only between the outer Vecs.
let (Some(sep), Some(args)) = (
fstring_elements_to_string_literals(sep),
args.map(|arg| fstring_elements_to_string_literals(arg.iter()))
fstring_elements_to_string_literals(sep, flags),
args.map(|arg| fstring_elements_to_string_literals(arg.iter(), flags))
.collect::<Option<Vec<_>>>(),
) else {
// If any of the arguments are not string literals, return None
@ -203,7 +206,7 @@ mod print_arguments {
range: TextRange::default(),
value: StringLiteralValue::single(StringLiteral {
value: combined_string.into(),
flags: StringLiteralFlags::default(),
flags,
range: TextRange::default(),
}),
}))
@ -256,7 +259,7 @@ mod print_arguments {
/// - [`Some`]<[`Expr::StringLiteral`]> if all arguments including `sep` are string literals.
/// - [`Some`]<[`Expr::FString`]> if any of the arguments are not string literals.
/// - [`None`] if the `print` contains no positional arguments at all.
pub(super) fn to_expr(arguments: &Arguments) -> Option<Expr> {
pub(super) fn to_expr(arguments: &Arguments, flags: StringLiteralFlags) -> Option<Expr> {
// Convert the `sep` argument into `FStringElement`s
let sep = arguments
.find_keyword("sep")
@ -286,7 +289,7 @@ mod print_arguments {
// Attempt to convert the `sep` and `args` arguments to a string literal,
// falling back to an f-string if the arguments are not all string literals.
args_to_string_literal_expr(args.iter(), sep.iter())
args_to_string_literal_expr(args.iter(), sep.iter(), flags)
.or_else(|| args_to_fstring_expr(args.into_iter(), sep.into_iter()))
}
}

View file

@ -1,6 +1,5 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
snapshot_kind: text
---
RUF055_0.py:6:1: RUF055 [*] Plain string pattern passed to `re` function
|
@ -211,12 +210,16 @@ RUF055_0.py:94:1: RUF055 [*] Plain string pattern passed to `re` function
94 |-re.sub(r"a", "\?", "a")
94 |+"a".replace(r"a", "\\?")
95 95 | re.sub(r"a", r"\?", "a")
96 96 |
97 97 | # these double as tests for preserving raw string quoting style
RUF055_0.py:95:1: RUF055 [*] Plain string pattern passed to `re` function
|
94 | re.sub(r"a", "\?", "a")
95 | re.sub(r"a", r"\?", "a")
| ^^^^^^^^^^^^^^^^^^^^^^^^ RUF055
96 |
97 | # these double as tests for preserving raw string quoting style
|
= help: Replace with `"a".replace(r"a", r"\?")`
@ -226,3 +229,59 @@ RUF055_0.py:95:1: RUF055 [*] Plain string pattern passed to `re` function
94 94 | re.sub(r"a", "\?", "a")
95 |-re.sub(r"a", r"\?", "a")
95 |+"a".replace(r"a", r"\?")
96 96 |
97 97 | # these double as tests for preserving raw string quoting style
98 98 | re.sub(r'abc', "", s)
RUF055_0.py:98:1: RUF055 [*] Plain string pattern passed to `re` function
|
97 | # these double as tests for preserving raw string quoting style
98 | re.sub(r'abc', "", s)
| ^^^^^^^^^^^^^^^^^^^^^ RUF055
99 | re.sub(r"""abc""", "", s)
100 | re.sub(r'''abc''', "", s)
|
= help: Replace with `s.replace(r'abc', "")`
Safe fix
95 95 | re.sub(r"a", r"\?", "a")
96 96 |
97 97 | # these double as tests for preserving raw string quoting style
98 |-re.sub(r'abc', "", s)
98 |+s.replace(r'abc', "")
99 99 | re.sub(r"""abc""", "", s)
100 100 | re.sub(r'''abc''', "", s)
RUF055_0.py:99:1: RUF055 [*] Plain string pattern passed to `re` function
|
97 | # these double as tests for preserving raw string quoting style
98 | re.sub(r'abc', "", s)
99 | re.sub(r"""abc""", "", s)
| ^^^^^^^^^^^^^^^^^^^^^^^^^ RUF055
100 | re.sub(r'''abc''', "", s)
|
= help: Replace with `s.replace(r"""abc""", "")`
Safe fix
96 96 |
97 97 | # these double as tests for preserving raw string quoting style
98 98 | re.sub(r'abc', "", s)
99 |-re.sub(r"""abc""", "", s)
99 |+s.replace(r"""abc""", "")
100 100 | re.sub(r'''abc''', "", s)
RUF055_0.py:100:1: RUF055 [*] Plain string pattern passed to `re` function
|
98 | re.sub(r'abc', "", s)
99 | re.sub(r"""abc""", "", s)
100 | re.sub(r'''abc''', "", s)
| ^^^^^^^^^^^^^^^^^^^^^^^^^ RUF055
|
= help: Replace with `s.replace(r'''abc''', "")`
Safe fix
97 97 | # these double as tests for preserving raw string quoting style
98 98 | re.sub(r'abc', "", s)
99 99 | re.sub(r"""abc""", "", s)
100 |-re.sub(r'''abc''', "", s)
100 |+s.replace(r'''abc''', "")

View file

@ -1221,14 +1221,14 @@ impl fmt::Debug for FStringElements {
/// An AST node that represents either a single string literal or an implicitly
/// concatenated string literals.
#[derive(Clone, Debug, Default, PartialEq)]
#[derive(Clone, Debug, PartialEq)]
pub struct ExprStringLiteral {
pub range: TextRange,
pub value: StringLiteralValue,
}
/// The value representing a [`ExprStringLiteral`].
#[derive(Clone, Debug, Default, PartialEq)]
#[derive(Clone, Debug, PartialEq)]
pub struct StringLiteralValue {
inner: StringLiteralValueInner,
}
@ -1241,6 +1241,18 @@ impl StringLiteralValue {
}
}
/// Returns the [`StringLiteralFlags`] associated with this string literal.
///
/// For an implicitly concatenated string, it returns the flags for the first literal.
pub fn flags(&self) -> StringLiteralFlags {
self.iter()
.next()
.expect(
"There should always be at least one string literal in an `ExprStringLiteral` node",
)
.flags
}
/// Creates a new string literal with the given values that represents an
/// implicitly concatenated strings.
///
@ -1371,12 +1383,6 @@ enum StringLiteralValueInner {
Concatenated(ConcatenatedStringLiteral),
}
impl Default for StringLiteralValueInner {
fn default() -> Self {
Self::Single(StringLiteral::default())
}
}
bitflags! {
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash)]
struct StringLiteralFlagsInner: u8 {
@ -1414,10 +1420,33 @@ bitflags! {
/// Flags that can be queried to obtain information
/// regarding the prefixes and quotes used for a string literal.
#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
///
/// ## Notes on usage
///
/// If you're using a `Generator` from the `ruff_python_codegen` crate to generate a lint-rule fix
/// from an existing string literal, consider passing along the [`StringLiteral::flags`] field or
/// the result of the [`StringLiteralValue::flags`] method. If you don't have an existing string but
/// have a `Checker` from the `ruff_linter` crate available, consider using
/// `Checker::default_string_flags` to create instances of this struct; this method will properly
/// handle surrounding f-strings. For usage that doesn't fit into one of these categories, the
/// public constructor [`StringLiteralFlags::empty`] can be used.
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub struct StringLiteralFlags(StringLiteralFlagsInner);
impl StringLiteralFlags {
/// Construct a new [`StringLiteralFlags`] with **no flags set**.
///
/// See [`StringLiteralFlags::with_quote_style`], [`StringLiteralFlags::with_triple_quotes`],
/// and [`StringLiteralFlags::with_prefix`] for ways of setting the quote style (single or
/// double), enabling triple quotes, and adding prefixes (such as `r` or `u`), respectively.
///
/// See the documentation for [`StringLiteralFlags`] for additional caveats on this constructor,
/// and situations in which alternative ways to construct this struct should be used, especially
/// when writing lint rules.
pub fn empty() -> Self {
Self(StringLiteralFlagsInner::empty())
}
#[must_use]
pub fn with_quote_style(mut self, quote_style: Quote) -> Self {
self.0
@ -1520,7 +1549,7 @@ impl fmt::Debug for StringLiteralFlags {
/// An AST node that represents a single string literal which is part of an
/// [`ExprStringLiteral`].
#[derive(Clone, Debug, Default, PartialEq)]
#[derive(Clone, Debug, PartialEq)]
pub struct StringLiteral {
pub range: TextRange,
pub value: Box<str>,
@ -1546,7 +1575,7 @@ impl StringLiteral {
Self {
range,
value: "".into(),
flags: StringLiteralFlags::default().with_invalid(),
flags: StringLiteralFlags::empty().with_invalid(),
}
}
}
@ -2115,7 +2144,7 @@ impl From<AnyStringFlags> for StringLiteralFlags {
value.prefix()
)
};
let new = StringLiteralFlags::default()
let new = StringLiteralFlags::empty()
.with_quote_style(value.quote_style())
.with_prefix(prefix);
if value.is_triple_quoted() {

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() {
macro_rules! round_trip_with {
($quote:expr, $start:expr, $end:expr) => {
assert_eq!(
round_trip_with(
&Indentation::default(),
Quote::Double,
$quote,
LineEnding::default(),
r#""hello""#
$start
),
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'"
$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]

View file

@ -4,12 +4,12 @@ use {
regex::Regex,
};
use ruff_python_ast::visitor::transformer;
use ruff_python_ast::visitor::transformer::Transformer;
use ruff_python_ast::{
self as ast, BytesLiteralFlags, Expr, FStringElement, FStringFlags, FStringLiteralElement,
FStringPart, Stmt, StringFlags, StringLiteralFlags,
FStringPart, Stmt, StringFlags,
};
use ruff_python_ast::{visitor::transformer, StringLiteralFlags};
use ruff_text_size::{Ranged, TextRange};
/// A struct to normalize AST nodes for the purpose of comparing formatted representations for
@ -81,7 +81,7 @@ impl Transformer for Normalizer {
string.value = ast::StringLiteralValue::single(ast::StringLiteral {
value: string.value.to_str().to_string().into_boxed_str(),
range: string.range,
flags: StringLiteralFlags::default(),
flags: StringLiteralFlags::empty(),
});
}
}

View file

@ -205,7 +205,7 @@ pub fn parse_parenthesized_expression_range(
///
/// let string = StringLiteral {
/// value: "'''\n int | str'''".to_string().into_boxed_str(),
/// flags: StringLiteralFlags::default(),
/// flags: StringLiteralFlags::empty(),
/// range: TextRange::new(TextSize::new(0), TextSize::new(16)),
/// };
/// let parsed = parse_string_annotation("'''\n int | str'''", &string);