[ruff] Add support for more re patterns (RUF055) (#15764)

## 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/).
This commit is contained in:
Garrett Reynolds 2025-01-29 09:14:44 -06:00 committed by GitHub
parent 0f1035b930
commit 6c1e19592e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 345 additions and 47 deletions

View file

@ -2,7 +2,7 @@ import re
s = "str" s = "str"
# this should be replaced with s.replace("abc", "") # this should be replaced with `s.replace("abc", "")`
re.sub("abc", "", s) re.sub("abc", "", s)
@ -17,7 +17,7 @@ def dashrepl(matchobj):
re.sub("-", dashrepl, "pro----gram-files") 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 # used in an if context for its truth value
if re.match("abc", s): if re.match("abc", s):
pass pass
@ -25,17 +25,17 @@ if m := re.match("abc", s): # this should *not* be replaced
pass pass
re.match("abc", s) # this should not be replaced because match returns a Match 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): if re.search("abc", s):
pass pass
re.search("abc", s) # this should not be replaced 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): if re.fullmatch("abc", s):
pass pass
re.fullmatch("abc", s) # this should not be replaced 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) re.split("abc", s)
# these currently should not be modified because the patterns contain regex # these currently should not be modified because the patterns contain regex

View file

@ -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

View file

@ -422,6 +422,7 @@ mod tests {
#[test_case(Rule::UnrawRePattern, Path::new("RUF039_concat.py"))] #[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_0.py"))]
#[test_case(Rule::UnnecessaryRegularExpression, Path::new("RUF055_1.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::UnnecessaryCastToInt, Path::new("RUF046.py"))]
#[test_case(Rule::PytestRaisesAmbiguousPattern, Path::new("RUF043.py"))] #[test_case(Rule::PytestRaisesAmbiguousPattern, Path::new("RUF043.py"))]
#[test_case(Rule::UnnecessaryRound, Path::new("RUF057.py"))] #[test_case(Rule::UnnecessaryRound, Path::new("RUF057.py"))]

View file

@ -3,7 +3,7 @@ use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Vi
use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::{ use ruff_python_ast::{
Arguments, CmpOp, Expr, ExprAttribute, ExprCall, ExprCompare, ExprContext, ExprStringLiteral, 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::analyze::typing::find_binding_value;
use ruff_python_semantic::{Modules, SemanticModel}; use ruff_python_semantic::{Modules, SemanticModel};
@ -111,8 +111,8 @@ pub(crate) fn unnecessary_regular_expression(checker: &mut Checker, call: &ExprC
return; return;
} }
// Here we know the pattern is a string literal with no metacharacters, so // Now we know the pattern is a string literal with no metacharacters, so
// we can proceed with the str method replacement // we can proceed with the str method replacement.
let new_expr = re_func.replacement(); let new_expr = re_func.replacement();
let repl = new_expr.map(|expr| checker.generator().expr(&expr)); 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 { UnnecessaryRegularExpression {
replacement: repl.clone(), replacement: repl.clone(),
}, },
call.range, re_func.range,
); );
if let Some(repl) = repl { if let Some(repl) = repl {
diagnostic.set_fix(Fix::applicable_edit( diagnostic.set_fix(Fix::applicable_edit(
Edit::range_replacement(repl, call.range), Edit::range_replacement(repl, re_func.range),
if checker if checker.comment_ranges().intersects(re_func.range) {
.comment_ranges()
.has_comments(call, checker.source())
{
Applicability::Unsafe Applicability::Unsafe
} else { } else {
Applicability::Safe Applicability::Safe
@ -156,6 +153,8 @@ struct ReFunc<'a> {
kind: ReFuncKind<'a>, kind: ReFuncKind<'a>,
pattern: &'a Expr, pattern: &'a Expr,
string: &'a Expr, string: &'a Expr,
comparison_to_none: Option<ComparisonToNone>,
range: TextRange,
} }
impl<'a> ReFunc<'a> { impl<'a> ReFunc<'a> {
@ -165,8 +164,14 @@ impl<'a> ReFunc<'a> {
func_name: &str, func_name: &str,
) -> Option<Self> { ) -> Option<Self> {
// the proposed fixes for match, search, and fullmatch rely on the // the proposed fixes for match, search, and fullmatch rely on the
// return value only being used for its truth value // return value only being used for its truth value or being compared to None
let in_if_context = semantic.in_boolean_test(); 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()) { match (func_name, call.arguments.len()) {
// `split` is the safest of these to fix, as long as metacharacters // `split` is the safest of these to fix, as long as metacharacters
@ -175,6 +180,8 @@ impl<'a> ReFunc<'a> {
kind: ReFuncKind::Split, kind: ReFuncKind::Split,
pattern: call.arguments.find_argument_value("pattern", 0)?, pattern: call.arguments.find_argument_value("pattern", 0)?,
string: call.arguments.find_argument_value("string", 1)?, 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 // `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 // 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)?, pattern: call.arguments.find_argument_value("pattern", 0)?,
string: call.arguments.find_argument_value("string", 2)?, 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, kind: ReFuncKind::Match,
pattern: call.arguments.find_argument_value("pattern", 0)?, pattern: call.arguments.find_argument_value("pattern", 0)?,
string: call.arguments.find_argument_value("string", 1)?, 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, kind: ReFuncKind::Search,
pattern: call.arguments.find_argument_value("pattern", 0)?, pattern: call.arguments.find_argument_value("pattern", 0)?,
string: call.arguments.find_argument_value("string", 1)?, 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, kind: ReFuncKind::Fullmatch,
pattern: call.arguments.find_argument_value("pattern", 0)?, pattern: call.arguments.find_argument_value("pattern", 0)?,
string: call.arguments.find_argument_value("string", 1)?, string: call.arguments.find_argument_value("string", 1)?,
comparison_to_none,
range,
}), }),
_ => None, _ => 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<Expr> { fn replacement(&self) -> Option<Expr> {
match self.kind { match (&self.kind, &self.comparison_to_none) {
// string.replace(pattern, repl) // string.replace(pattern, repl)
ReFuncKind::Sub { repl } => repl (ReFuncKind::Sub { repl }, _) => repl
.cloned() .cloned()
.map(|repl| self.method_expr("replace", vec![self.pattern.clone(), repl])), .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()])),
// pattern in string
ReFuncKind::Search => Some(self.compare_expr(CmpOp::In)),
// 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) // string.split(pattern)
ReFuncKind::Split => Some(self.method_expr("split", vec![self.pattern.clone()])), (ReFuncKind::Split, _) => Some(self.method_expr("split", vec![self.pattern.clone()])),
// pattern in string
(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, 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` /// Return a new compare expr of the form `left op right`
fn compare_expr(&self, op: CmpOp) -> Expr { fn compare_expr(left: &Expr, op: CmpOp, right: &Expr) -> Expr {
Expr::Compare(ExprCompare { Expr::Compare(ExprCompare {
left: Box::new(self.pattern.clone()), left: Box::new(left.clone()),
ops: Box::new([op]), ops: Box::new([op]),
comparators: Box::new([self.string.clone()]), comparators: Box::new([right.clone()]),
range: TextRange::default(), range: TextRange::default(),
}) })
} }
@ -302,3 +345,35 @@ fn resolve_string_literal<'a>(
None 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,
}
}

View file

@ -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 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) 6 | re.sub("abc", "", s)
| ^^^^^^^^^^^^^^^^^^^^ RUF055 | ^^^^^^^^^^^^^^^^^^^^ RUF055
| |
@ -12,7 +12,7 @@ RUF055_0.py:6:1: RUF055 [*] Plain string pattern passed to `re` function
Safe fix Safe fix
3 3 | s = "str" 3 3 | s = "str"
4 4 | 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 |-re.sub("abc", "", s)
6 |+s.replace("abc", "") 6 |+s.replace("abc", "")
7 7 | 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 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 21 | # used in an if context for its truth value
22 | if re.match("abc", s): 22 | if re.match("abc", s):
| ^^^^^^^^^^^^^^^^^^ RUF055 | ^^^^^^^^^^^^^^^^^^ RUF055
@ -32,7 +32,7 @@ RUF055_0.py:22:4: RUF055 [*] Plain string pattern passed to `re` function
Safe fix Safe fix
19 19 | 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 21 21 | # used in an if context for its truth value
22 |-if re.match("abc", s): 22 |-if re.match("abc", s):
22 |+if s.startswith("abc"): 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 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): 29 | if re.search("abc", s):
| ^^^^^^^^^^^^^^^^^^^ RUF055 | ^^^^^^^^^^^^^^^^^^^ RUF055
30 | pass 30 | pass
@ -53,7 +53,7 @@ RUF055_0.py:29:4: RUF055 [*] Plain string pattern passed to `re` function
Safe fix Safe fix
26 26 | re.match("abc", s) # this should not be replaced because match returns a Match 26 26 | re.match("abc", s) # this should not be replaced because match returns a Match
27 27 | 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 re.search("abc", s):
29 |+if "abc" in s: 29 |+if "abc" in s:
30 30 | pass 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 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): 34 | if re.fullmatch("abc", s):
| ^^^^^^^^^^^^^^^^^^^^^^ RUF055 | ^^^^^^^^^^^^^^^^^^^^^^ RUF055
35 | pass 35 | pass
@ -73,7 +73,7 @@ RUF055_0.py:34:4: RUF055 [*] Plain string pattern passed to `re` function
Safe fix Safe fix
31 31 | re.search("abc", s) # this should not be replaced 31 31 | re.search("abc", s) # this should not be replaced
32 32 | 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 re.fullmatch("abc", s):
34 |+if s == "abc": 34 |+if s == "abc":
35 35 | pass 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 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) 39 | re.split("abc", s)
| ^^^^^^^^^^^^^^^^^^ RUF055 | ^^^^^^^^^^^^^^^^^^ RUF055
40 | 40 |
@ -93,7 +93,7 @@ RUF055_0.py:39:1: RUF055 [*] Plain string pattern passed to `re` function
Safe fix Safe fix
36 36 | re.fullmatch("abc", s) # this should not be replaced 36 36 | re.fullmatch("abc", s) # this should not be replaced
37 37 | 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 |-re.split("abc", s)
39 |+s.split("abc") 39 |+s.split("abc")
40 40 | 40 40 |

View file

@ -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