From 6c1e19592e2647d91fec152afacf7b60b3f81b34 Mon Sep 17 00:00:00 2001 From: Garrett Reynolds Date: Wed, 29 Jan 2025 09:14:44 -0600 Subject: [PATCH] [`ruff`] Add support for more `re` patterns (`RUF055`) (#15764) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Implements some of #14738, by adding support for 6 new patterns: ```py re.search("abc", s) is None # ⇒ "abc" not in s re.search("abc", s) is not None # ⇒ "abc" in s re.match("abc", s) is None # ⇒ not s.startswith("abc") re.match("abc", s) is not None # ⇒ s.startswith("abc") re.fullmatch("abc", s) is None # ⇒ s != "abc" re.fullmatch("abc", s) is not None # ⇒ s == "abc" ``` ## Test Plan ```shell cargo nextest run cargo insta review ``` And ran the fix on my startup's repo. ## Note One minor limitation here: ```py if not re.match('abc', s) is None: pass ``` will get fixed to this (technically correct, just not nice): ```py if not not s.startswith('abc'): pass ``` This seems fine given that Ruff has this covered: the initial code should be caught by [E714](https://docs.astral.sh/ruff/rules/not-is-test/) and the fixed code should be caught by [SIM208](https://docs.astral.sh/ruff/rules/double-negation/). --- .../resources/test/fixtures/ruff/RUF055_0.py | 10 +- .../resources/test/fixtures/ruff/RUF055_2.py | 52 ++++++ crates/ruff_linter/src/rules/ruff/mod.rs | 1 + .../rules/unnecessary_regular_expression.rs | 137 ++++++++++---- ...f__tests__preview__RUF055_RUF055_0.py.snap | 20 +-- ...f__tests__preview__RUF055_RUF055_2.py.snap | 170 ++++++++++++++++++ 6 files changed, 344 insertions(+), 46 deletions(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/ruff/RUF055_2.py create mode 100644 crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF055_RUF055_2.py.snap 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 6274bd4e3c7195..608ea2ef22862c 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF055_0.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF055_0.py @@ -2,7 +2,7 @@ s = "str" -# this should be replaced with s.replace("abc", "") +# this should be replaced with `s.replace("abc", "")` re.sub("abc", "", s) @@ -17,7 +17,7 @@ def dashrepl(matchobj): re.sub("-", dashrepl, "pro----gram-files") -# this one should be replaced with s.startswith("abc") because the Match is +# this one should be replaced with `s.startswith("abc")` because the Match is # used in an if context for its truth value if re.match("abc", s): pass @@ -25,17 +25,17 @@ def dashrepl(matchobj): pass re.match("abc", s) # this should not be replaced because match returns a Match -# this should be replaced with "abc" in s +# this should be replaced with `"abc" in s` if re.search("abc", s): pass re.search("abc", s) # this should not be replaced -# this should be replaced with "abc" == s +# this should be replaced with `"abc" == s` if re.fullmatch("abc", s): pass re.fullmatch("abc", s) # this should not be replaced -# this should be replaced with s.split("abc") +# this should be replaced with `s.split("abc")` re.split("abc", s) # these currently should not be modified because the patterns contain regex diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF055_2.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF055_2.py new file mode 100644 index 00000000000000..8b1a2d57840810 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF055_2.py @@ -0,0 +1,52 @@ +"""Patterns that don't just involve the call, but rather the parent expression""" +import re + +s = "str" + +# this should be replaced with `"abc" not in s` +re.search("abc", s) is None + + +# this should be replaced with `"abc" in s` +re.search("abc", s) is not None + + +# this should be replaced with `not s.startswith("abc")` +re.match("abc", s) is None + + +# this should be replaced with `s.startswith("abc")` +re.match("abc", s) is not None + + +# this should be replaced with `s != "abc"` +re.fullmatch("abc", s) is None + + +# this should be replaced with `s == "abc"` +re.fullmatch("abc", s) is not None + + +# this should trigger an unsafe fix because of the presence of a comment within the +# expression being replaced (which we'd lose) +if ( + re.fullmatch( + "a really really really really long string", + s, + ) + # with a comment here + is None +): + pass + + +# this should trigger a safe fix (comments are preserved given they're outside the +# expression) +if ( # leading + re.fullmatch( + "a really really really really long string", + s, + ) + is None # trailing +): + pass diff --git a/crates/ruff_linter/src/rules/ruff/mod.rs b/crates/ruff_linter/src/rules/ruff/mod.rs index 6a575a5bd35f88..5ed899c945f254 100644 --- a/crates/ruff_linter/src/rules/ruff/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/mod.rs @@ -422,6 +422,7 @@ mod tests { #[test_case(Rule::UnrawRePattern, Path::new("RUF039_concat.py"))] #[test_case(Rule::UnnecessaryRegularExpression, Path::new("RUF055_0.py"))] #[test_case(Rule::UnnecessaryRegularExpression, Path::new("RUF055_1.py"))] + #[test_case(Rule::UnnecessaryRegularExpression, Path::new("RUF055_2.py"))] #[test_case(Rule::UnnecessaryCastToInt, Path::new("RUF046.py"))] #[test_case(Rule::PytestRaisesAmbiguousPattern, Path::new("RUF043.py"))] #[test_case(Rule::UnnecessaryRound, Path::new("RUF057.py"))] diff --git a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_regular_expression.rs b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_regular_expression.rs index fe263f1926bb23..f52466e6d9568d 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_regular_expression.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_regular_expression.rs @@ -3,7 +3,7 @@ use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Vi use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_python_ast::{ Arguments, CmpOp, Expr, ExprAttribute, ExprCall, ExprCompare, ExprContext, ExprStringLiteral, - Identifier, + ExprUnaryOp, Identifier, UnaryOp, }; use ruff_python_semantic::analyze::typing::find_binding_value; use ruff_python_semantic::{Modules, SemanticModel}; @@ -111,8 +111,8 @@ pub(crate) fn unnecessary_regular_expression(checker: &mut Checker, call: &ExprC return; } - // Here we know the pattern is a string literal with no metacharacters, so - // we can proceed with the str method replacement + // Now we know the pattern is a string literal with no metacharacters, so + // we can proceed with the str method replacement. let new_expr = re_func.replacement(); let repl = new_expr.map(|expr| checker.generator().expr(&expr)); @@ -120,16 +120,13 @@ pub(crate) fn unnecessary_regular_expression(checker: &mut Checker, call: &ExprC UnnecessaryRegularExpression { replacement: repl.clone(), }, - call.range, + re_func.range, ); if let Some(repl) = repl { diagnostic.set_fix(Fix::applicable_edit( - Edit::range_replacement(repl, call.range), - if checker - .comment_ranges() - .has_comments(call, checker.source()) - { + Edit::range_replacement(repl, re_func.range), + if checker.comment_ranges().intersects(re_func.range) { Applicability::Unsafe } else { Applicability::Safe @@ -156,6 +153,8 @@ struct ReFunc<'a> { kind: ReFuncKind<'a>, pattern: &'a Expr, string: &'a Expr, + comparison_to_none: Option, + range: TextRange, } impl<'a> ReFunc<'a> { @@ -165,8 +164,14 @@ impl<'a> ReFunc<'a> { func_name: &str, ) -> Option { // the proposed fixes for match, search, and fullmatch rely on the - // return value only being used for its truth value - let in_if_context = semantic.in_boolean_test(); + // return value only being used for its truth value or being compared to None + let comparison_to_none = get_comparison_to_none(semantic); + let in_truthy_context = semantic.in_boolean_test() || comparison_to_none.is_some(); + + let (comparison_to_none, range) = match comparison_to_none { + Some((cmp, range)) => (Some(cmp), range), + None => (None, call.range), + }; match (func_name, call.arguments.len()) { // `split` is the safest of these to fix, as long as metacharacters @@ -175,6 +180,8 @@ impl<'a> ReFunc<'a> { kind: ReFuncKind::Split, pattern: call.arguments.find_argument_value("pattern", 0)?, string: call.arguments.find_argument_value("string", 1)?, + comparison_to_none, + range, }), // `sub` is only safe to fix if `repl` is a string. `re.sub` also // allows it to be a function, which will *not* work in the str @@ -209,55 +216,91 @@ impl<'a> ReFunc<'a> { }, pattern: call.arguments.find_argument_value("pattern", 0)?, string: call.arguments.find_argument_value("string", 2)?, + comparison_to_none, + range, }) } - ("match", 2) if in_if_context => Some(ReFunc { + ("match", 2) if in_truthy_context => Some(ReFunc { kind: ReFuncKind::Match, pattern: call.arguments.find_argument_value("pattern", 0)?, string: call.arguments.find_argument_value("string", 1)?, + comparison_to_none, + range, }), - ("search", 2) if in_if_context => Some(ReFunc { + ("search", 2) if in_truthy_context => Some(ReFunc { kind: ReFuncKind::Search, pattern: call.arguments.find_argument_value("pattern", 0)?, string: call.arguments.find_argument_value("string", 1)?, + comparison_to_none, + range, }), - ("fullmatch", 2) if in_if_context => Some(ReFunc { + ("fullmatch", 2) if in_truthy_context => Some(ReFunc { kind: ReFuncKind::Fullmatch, pattern: call.arguments.find_argument_value("pattern", 0)?, string: call.arguments.find_argument_value("string", 1)?, + comparison_to_none, + range, }), _ => None, } } + /// Get replacement for the call or parent expression. + /// + /// Examples: + /// `re.search("abc", s) is None` => `"abc" not in s` + /// `re.search("abc", s)` => `"abc" in s` fn replacement(&self) -> Option { - match self.kind { + match (&self.kind, &self.comparison_to_none) { // string.replace(pattern, repl) - ReFuncKind::Sub { repl } => repl + (ReFuncKind::Sub { repl }, _) => repl .cloned() .map(|repl| self.method_expr("replace", vec![self.pattern.clone(), repl])), - // string.startswith(pattern) - ReFuncKind::Match => Some(self.method_expr("startswith", vec![self.pattern.clone()])), + // string.split(pattern) + (ReFuncKind::Split, _) => Some(self.method_expr("split", vec![self.pattern.clone()])), // pattern in string - ReFuncKind::Search => Some(self.compare_expr(CmpOp::In)), + (ReFuncKind::Search, None | Some(ComparisonToNone::IsNot)) => { + Some(ReFunc::compare_expr(self.pattern, CmpOp::In, self.string)) + } + // pattern not in string + (ReFuncKind::Search, Some(ComparisonToNone::Is)) => Some(ReFunc::compare_expr( + self.pattern, + CmpOp::NotIn, + self.string, + )), + // string.startswith(pattern) + (ReFuncKind::Match, None | Some(ComparisonToNone::IsNot)) => { + Some(self.method_expr("startswith", vec![self.pattern.clone()])) + } + // not string.startswith(pattern) + (ReFuncKind::Match, Some(ComparisonToNone::Is)) => { + let expr = self.method_expr("startswith", vec![self.pattern.clone()]); + let negated_expr = Expr::UnaryOp(ExprUnaryOp { + op: UnaryOp::Not, + operand: Box::new(expr), + range: TextRange::default(), + }); + Some(negated_expr) + } // string == pattern - ReFuncKind::Fullmatch => Some(Expr::Compare(ExprCompare { - range: TextRange::default(), - left: Box::new(self.string.clone()), - ops: Box::new([CmpOp::Eq]), - comparators: Box::new([self.pattern.clone()]), - })), - // string.split(pattern) - ReFuncKind::Split => Some(self.method_expr("split", vec![self.pattern.clone()])), + (ReFuncKind::Fullmatch, None | Some(ComparisonToNone::IsNot)) => { + Some(ReFunc::compare_expr(self.string, CmpOp::Eq, self.pattern)) + } + // string != pattern + (ReFuncKind::Fullmatch, Some(ComparisonToNone::Is)) => Some(ReFunc::compare_expr( + self.string, + CmpOp::NotEq, + self.pattern, + )), } } - /// Return a new compare expr of the form `self.pattern op self.string` - fn compare_expr(&self, op: CmpOp) -> Expr { + /// Return a new compare expr of the form `left op right` + fn compare_expr(left: &Expr, op: CmpOp, right: &Expr) -> Expr { Expr::Compare(ExprCompare { - left: Box::new(self.pattern.clone()), + left: Box::new(left.clone()), ops: Box::new([op]), - comparators: Box::new([self.string.clone()]), + comparators: Box::new([right.clone()]), range: TextRange::default(), }) } @@ -302,3 +345,35 @@ fn resolve_string_literal<'a>( None } + +#[derive(Clone, Copy, Debug)] +enum ComparisonToNone { + Is, + IsNot, +} + +/// If the regex call is compared to `None`, return the comparison and its range. +/// Example: `re.search("abc", s) is None` +fn get_comparison_to_none(semantic: &SemanticModel) -> Option<(ComparisonToNone, TextRange)> { + let parent_expr = semantic.current_expression_parent()?; + + let Expr::Compare(ExprCompare { + ops, + comparators, + range, + .. + }) = parent_expr + else { + return None; + }; + + let Some(Expr::NoneLiteral(_)) = comparators.first() else { + return None; + }; + + match ops.as_ref() { + [CmpOp::Is] => Some((ComparisonToNone::Is, *range)), + [CmpOp::IsNot] => Some((ComparisonToNone::IsNot, *range)), + _ => None, + } +} 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 d019fe88a262eb..0ea320d36a28ff 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 @@ -3,7 +3,7 @@ source: crates/ruff_linter/src/rules/ruff/mod.rs --- RUF055_0.py:6:1: RUF055 [*] Plain string pattern passed to `re` function | -5 | # this should be replaced with s.replace("abc", "") +5 | # this should be replaced with `s.replace("abc", "")` 6 | re.sub("abc", "", s) | ^^^^^^^^^^^^^^^^^^^^ RUF055 | @@ -12,7 +12,7 @@ RUF055_0.py:6:1: RUF055 [*] Plain string pattern passed to `re` function ℹ Safe fix 3 3 | s = "str" 4 4 | -5 5 | # this should be replaced with s.replace("abc", "") +5 5 | # this should be replaced with `s.replace("abc", "")` 6 |-re.sub("abc", "", s) 6 |+s.replace("abc", "") 7 7 | @@ -21,7 +21,7 @@ RUF055_0.py:6:1: RUF055 [*] Plain string pattern passed to `re` function RUF055_0.py:22:4: RUF055 [*] Plain string pattern passed to `re` function | -20 | # this one should be replaced with s.startswith("abc") because the Match is +20 | # this one should be replaced with `s.startswith("abc")` because the Match is 21 | # used in an if context for its truth value 22 | if re.match("abc", s): | ^^^^^^^^^^^^^^^^^^ RUF055 @@ -32,7 +32,7 @@ RUF055_0.py:22:4: RUF055 [*] Plain string pattern passed to `re` function ℹ Safe fix 19 19 | -20 20 | # this one should be replaced with s.startswith("abc") because the Match is +20 20 | # this one should be replaced with `s.startswith("abc")` because the Match is 21 21 | # used in an if context for its truth value 22 |-if re.match("abc", s): 22 |+if s.startswith("abc"): @@ -42,7 +42,7 @@ RUF055_0.py:22:4: RUF055 [*] Plain string pattern passed to `re` function RUF055_0.py:29:4: RUF055 [*] Plain string pattern passed to `re` function | -28 | # this should be replaced with "abc" in s +28 | # this should be replaced with `"abc" in s` 29 | if re.search("abc", s): | ^^^^^^^^^^^^^^^^^^^ RUF055 30 | pass @@ -53,7 +53,7 @@ RUF055_0.py:29:4: RUF055 [*] Plain string pattern passed to `re` function ℹ Safe fix 26 26 | re.match("abc", s) # this should not be replaced because match returns a Match 27 27 | -28 28 | # this should be replaced with "abc" in s +28 28 | # this should be replaced with `"abc" in s` 29 |-if re.search("abc", s): 29 |+if "abc" in s: 30 30 | pass @@ -62,7 +62,7 @@ RUF055_0.py:29:4: RUF055 [*] Plain string pattern passed to `re` function RUF055_0.py:34:4: RUF055 [*] Plain string pattern passed to `re` function | -33 | # this should be replaced with "abc" == s +33 | # this should be replaced with `"abc" == s` 34 | if re.fullmatch("abc", s): | ^^^^^^^^^^^^^^^^^^^^^^ RUF055 35 | pass @@ -73,7 +73,7 @@ RUF055_0.py:34:4: RUF055 [*] Plain string pattern passed to `re` function ℹ Safe fix 31 31 | re.search("abc", s) # this should not be replaced 32 32 | -33 33 | # this should be replaced with "abc" == s +33 33 | # this should be replaced with `"abc" == s` 34 |-if re.fullmatch("abc", s): 34 |+if s == "abc": 35 35 | pass @@ -82,7 +82,7 @@ RUF055_0.py:34:4: RUF055 [*] Plain string pattern passed to `re` function RUF055_0.py:39:1: RUF055 [*] Plain string pattern passed to `re` function | -38 | # this should be replaced with s.split("abc") +38 | # this should be replaced with `s.split("abc")` 39 | re.split("abc", s) | ^^^^^^^^^^^^^^^^^^ RUF055 40 | @@ -93,7 +93,7 @@ RUF055_0.py:39:1: RUF055 [*] Plain string pattern passed to `re` function ℹ Safe fix 36 36 | re.fullmatch("abc", s) # this should not be replaced 37 37 | -38 38 | # this should be replaced with s.split("abc") +38 38 | # this should be replaced with `s.split("abc")` 39 |-re.split("abc", s) 39 |+s.split("abc") 40 40 | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF055_RUF055_2.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF055_RUF055_2.py.snap new file mode 100644 index 00000000000000..b2d2b8017c46a1 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF055_RUF055_2.py.snap @@ -0,0 +1,170 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +snapshot_kind: text +--- +RUF055_2.py:7:1: RUF055 [*] Plain string pattern passed to `re` function + | +6 | # this should be replaced with `"abc" not in s` +7 | re.search("abc", s) is None + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF055 + | + = help: Replace with `"abc" not in s` + +ℹ Safe fix +4 4 | s = "str" +5 5 | +6 6 | # this should be replaced with `"abc" not in s` +7 |-re.search("abc", s) is None + 7 |+"abc" not in s +8 8 | +9 9 | +10 10 | # this should be replaced with `"abc" in s` + +RUF055_2.py:11:1: RUF055 [*] Plain string pattern passed to `re` function + | +10 | # this should be replaced with `"abc" in s` +11 | re.search("abc", s) is not None + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF055 + | + = help: Replace with `"abc" in s` + +ℹ Safe fix +8 8 | +9 9 | +10 10 | # this should be replaced with `"abc" in s` +11 |-re.search("abc", s) is not None + 11 |+"abc" in s +12 12 | +13 13 | +14 14 | # this should be replaced with `not s.startswith("abc")` + +RUF055_2.py:15:1: RUF055 [*] Plain string pattern passed to `re` function + | +14 | # this should be replaced with `not s.startswith("abc")` +15 | re.match("abc", s) is None + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF055 + | + = help: Replace with `not s.startswith("abc")` + +ℹ Safe fix +12 12 | +13 13 | +14 14 | # this should be replaced with `not s.startswith("abc")` +15 |-re.match("abc", s) is None + 15 |+not s.startswith("abc") +16 16 | +17 17 | +18 18 | # this should be replaced with `s.startswith("abc")` + +RUF055_2.py:19:1: RUF055 [*] Plain string pattern passed to `re` function + | +18 | # this should be replaced with `s.startswith("abc")` +19 | re.match("abc", s) is not None + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF055 + | + = help: Replace with `s.startswith("abc")` + +ℹ Safe fix +16 16 | +17 17 | +18 18 | # this should be replaced with `s.startswith("abc")` +19 |-re.match("abc", s) is not None + 19 |+s.startswith("abc") +20 20 | +21 21 | +22 22 | # this should be replaced with `s != "abc"` + +RUF055_2.py:23:1: RUF055 [*] Plain string pattern passed to `re` function + | +22 | # this should be replaced with `s != "abc"` +23 | re.fullmatch("abc", s) is None + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF055 + | + = help: Replace with `s != "abc"` + +ℹ Safe fix +20 20 | +21 21 | +22 22 | # this should be replaced with `s != "abc"` +23 |-re.fullmatch("abc", s) is None + 23 |+s != "abc" +24 24 | +25 25 | +26 26 | # this should be replaced with `s == "abc"` + +RUF055_2.py:27:1: RUF055 [*] Plain string pattern passed to `re` function + | +26 | # this should be replaced with `s == "abc"` +27 | re.fullmatch("abc", s) is not None + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF055 + | + = help: Replace with `s == "abc"` + +ℹ Safe fix +24 24 | +25 25 | +26 26 | # this should be replaced with `s == "abc"` +27 |-re.fullmatch("abc", s) is not None + 27 |+s == "abc" +28 28 | +29 29 | +30 30 | # this should trigger an unsafe fix because of the presence of a comment within the + +RUF055_2.py:33:5: RUF055 [*] Plain string pattern passed to `re` function + | +31 | # expression being replaced (which we'd lose) +32 | if ( +33 | / re.fullmatch( +34 | | "a really really really really long string", +35 | | s, +36 | | ) +37 | | # with a comment here +38 | | is None + | |___________^ RUF055 +39 | ): +40 | pass + | + = help: Replace with `s != "a really really really really long string"` + +ℹ Unsafe fix +30 30 | # this should trigger an unsafe fix because of the presence of a comment within the +31 31 | # expression being replaced (which we'd lose) +32 32 | if ( +33 |- re.fullmatch( +34 |- "a really really really really long string", +35 |- s, +36 |- ) +37 |- # with a comment here +38 |- is None + 33 |+ s != "a really really really really long string" +39 34 | ): +40 35 | pass +41 36 | + +RUF055_2.py:46:5: RUF055 [*] Plain string pattern passed to `re` function + | +44 | # expression) +45 | if ( # leading +46 | / re.fullmatch( +47 | | "a really really really really long string", +48 | | s, +49 | | ) +50 | | is None # trailing + | |___________^ RUF055 +51 | ): +52 | pass + | + = help: Replace with `s != "a really really really really long string"` + +ℹ Safe fix +43 43 | # this should trigger a safe fix (comments are preserved given they're outside the +44 44 | # expression) +45 45 | if ( # leading +46 |- re.fullmatch( +47 |- "a really really really really long string", +48 |- s, +49 |- ) +50 |- is None # trailing + 46 |+ s != "a really really really really long string" # trailing +51 47 | ): +52 48 | pass