From 9bf138c45a7b8dfd677b37d479bddb68d18829ee Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Mon, 27 Jan 2025 13:41:03 -0500 Subject: [PATCH] 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 --- .../test/fixtures/flake8_simplify/SIM905.py | 40 +- .../test/fixtures/pyupgrade/UP007.py | 8 + .../resources/test/fixtures/ruff/RUF055_0.py | 5 + .../src/checkers/ast/analyze/expression.rs | 2 +- crates/ruff_linter/src/checkers/ast/mod.rs | 14 +- .../flake8_pytest_style/rules/parametrize.rs | 25 +- .../rules/flake8_simplify/rules/ast_expr.rs | 10 +- .../rules/split_static_string.rs | 36 +- ...ke8_simplify__tests__SIM905_SIM905.py.snap | 380 +++++++++--------- .../src/rules/flake8_type_checking/helpers.rs | 55 ++- .../rules/runtime_cast_value.rs | 1 + .../runtime_import_in_type_checking_block.rs | 1 + .../rules/type_alias_quotes.rs | 1 + .../rules/typing_only_runtime_import.rs | 1 + .../flynt/rules/static_join_to_fstring.rs | 8 +- .../pylint/rules/unspecified_encoding.rs | 19 +- .../rules/pyupgrade/rules/native_literals.rs | 13 +- ...er__rules__pyupgrade__tests__UP007.py.snap | 21 +- .../ruff/rules/assert_with_print_message.rs | 17 +- ...f__tests__preview__RUF055_RUF055_0.py.snap | 61 ++- crates/ruff_python_ast/src/nodes.rs | 53 ++- crates/ruff_python_codegen/src/generator.rs | 114 +++--- .../ruff_python_formatter/tests/normalizer.rs | 6 +- crates/ruff_python_parser/src/lib.rs | 2 +- 24 files changed, 550 insertions(+), 343 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM905.py b/crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM905.py index e050ece623..ad7da0b856 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM905.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM905.py @@ -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 diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP007.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP007.py index f933a4a995..cbc3e79b29 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP007.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP007.py @@ -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) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF055_0.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF055_0.py index 14c70f190f..6274bd4e3c 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF055_0.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF055_0.py @@ -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) diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index 8601165cb7..e2ea12bc5e 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -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" { diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 80a7880b29..b9f1e2bdf7 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -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. /// diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/parametrize.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/parametrize.rs index 9323ab6d78..fa26390f34 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/parametrize.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/parametrize.rs @@ -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 { +fn elts_to_csv(elts: &[Expr], generator: Generator, flags: StringLiteralFlags) -> Option { if !elts.iter().all(Expr::is_string_literal_expr) { return None; } @@ -298,7 +298,8 @@ fn elts_to_csv(elts: &[Expr], generator: Generator) -> Option { 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(), diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_expr.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_expr.rs index 50c17cfeba..f47c97a87b 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_expr.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_expr.rs @@ -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( diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/split_static_string.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/split_static_string.rs index 375d30b491..23e85f45a4 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/split_static_string.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/split_static_string.rs @@ -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 { +fn split_default(str_value: &StringLiteralValue, max_split: i32) -> Option { // 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 { 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. diff --git a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM905_SIM905.py.snap b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM905_SIM905.py.snap index 54d6ace3b9..b842a85e23 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM905_SIM905.py.snap +++ b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM905_SIM905.py.snap @@ -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 | diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/helpers.rs b/crates/ruff_linter/src/rules/flake8_type_checking/helpers.rs index c6881536f9..e9193d1ae7 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/helpers.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/helpers.rs @@ -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); + } +} diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/rules/runtime_cast_value.rs b/crates/ruff_linter/src/rules/flake8_type_checking/rules/runtime_cast_value.rs index f2de99c612..d627829e66 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/rules/runtime_cast_value.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/rules/runtime_cast_value.rs @@ -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)); diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs b/crates/ruff_linter/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs index f5f9114b71..a61c7bcec1 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs @@ -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 diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs b/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs index 980fb06a5d..af7fa05b14 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs @@ -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 { diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs b/crates/ruff_linter/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs index 79418145ab..d49c465c7b 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs @@ -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 diff --git a/crates/ruff_linter/src/rules/flynt/rules/static_join_to_fstring.rs b/crates/ruff_linter/src/rules/flynt/rules/static_join_to_fstring.rs index af100938e9..32cadc5851 100644 --- a/crates/ruff_linter/src/rules/flynt/rules/static_join_to_fstring.rs +++ b/crates/ruff_linter/src/rules/flynt/rules/static_join_to_fstring.rs @@ -63,11 +63,16 @@ fn is_static_length(elts: &[Expr]) -> bool { fn build_fstring(joiner: &str, joinees: &[Expr]) -> Option { // 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 { }) .join(joiner) .into_boxed_str(), - ..ast::StringLiteral::default() + flags: flags?, + range: TextRange::default(), }; return Some(node.into()); } diff --git a/crates/ruff_linter/src/rules/pylint/rules/unspecified_encoding.rs b/crates/ruff_linter/src/rules/pylint/rules/unspecified_encoding.rs index 1aa1ea934a..a87793a924 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/unspecified_encoding.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/unspecified_encoding.rs @@ -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,16 +158,11 @@ 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(), - }), - range: TextRange::default(), - })) + checker.generator().expr(&Expr::from(ast::StringLiteral { + value: Box::from("utf-8"), + flags: checker.default_string_flags(), + range: TextRange::default(), + })) ), &call.arguments, checker.comment_ranges(), diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/native_literals.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/native_literals.rs index 4e71ee73ae..32b1a81527 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/native_literals.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/native_literals.rs @@ -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, diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP007.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP007.py.snap index c238f46be4..a2b50cedc1 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP007.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP007.py.snap @@ -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) diff --git a/crates/ruff_linter/src/rules/ruff/rules/assert_with_print_message.rs b/crates/ruff_linter/src/rules/ruff/rules/assert_with_print_message.rs index da9b3de4fc..cbd0721da4 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/assert_with_print_message.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/assert_with_print_message.rs @@ -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, + flags: StringLiteralFlags, ) -> Option> { 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>, sep: impl ExactSizeIterator, + flags: StringLiteralFlags, ) -> Option { // 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::>>(), ) 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 { + pub(super) fn to_expr(arguments: &Arguments, flags: StringLiteralFlags) -> Option { // 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())) } } diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF055_RUF055_0.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF055_RUF055_0.py.snap index a2a3ee2304..d019fe88a2 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF055_RUF055_0.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF055_RUF055_0.py.snap @@ -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''', "") diff --git a/crates/ruff_python_ast/src/nodes.rs b/crates/ruff_python_ast/src/nodes.rs index ac779c56a6..1d4d9c78b6 100644 --- a/crates/ruff_python_ast/src/nodes.rs +++ b/crates/ruff_python_ast/src/nodes.rs @@ -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, @@ -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 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() { diff --git a/crates/ruff_python_codegen/src/generator.rs b/crates/ruff_python_codegen/src/generator.rs index 847038ff1c..410a6ef68c 100644 --- a/crates/ruff_python_codegen/src/generator.rs +++ b/crates/ruff_python_codegen/src/generator.rs @@ -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] diff --git a/crates/ruff_python_formatter/tests/normalizer.rs b/crates/ruff_python_formatter/tests/normalizer.rs index b83a8cee0c..cd745171a2 100644 --- a/crates/ruff_python_formatter/tests/normalizer.rs +++ b/crates/ruff_python_formatter/tests/normalizer.rs @@ -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(), }); } } diff --git a/crates/ruff_python_parser/src/lib.rs b/crates/ruff_python_parser/src/lib.rs index 3571804bad..53f96a8d73 100644 --- a/crates/ruff_python_parser/src/lib.rs +++ b/crates/ruff_python_parser/src/lib.rs @@ -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);