Skip to content

Commit 6183b8e

Browse files
[refurb] Implement regex-flag-alias with fix (FURB167) (#9516)
## Summary add [`FURB167`/`use-long-regex-flag`](https://github.com/dosisod/refurb/blob/master/refurb/checks/regex/use_long_flag.py) with autofix See: #1348 ## Test Plan `cargo test` --------- Co-authored-by: Charlie Marsh <[email protected]>
1 parent 0c0d3db commit 6183b8e

File tree

8 files changed

+211
-0
lines changed

8 files changed

+211
-0
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
def func():
2+
import re
3+
4+
# OK
5+
if re.match("^hello", "hello world", re.IGNORECASE):
6+
pass
7+
8+
9+
def func():
10+
import re
11+
12+
# FURB167
13+
if re.match("^hello", "hello world", re.I):
14+
pass
15+
16+
17+
def func():
18+
from re import match, I
19+
20+
# FURB167
21+
if match("^hello", "hello world", I):
22+
pass

crates/ruff_linter/src/checkers/ast/analyze/expression.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
170170
if checker.enabled(Rule::CollectionsNamedTuple) {
171171
flake8_pyi::rules::collections_named_tuple(checker, expr);
172172
}
173+
if checker.enabled(Rule::RegexFlagAlias) {
174+
refurb::rules::regex_flag_alias(checker, expr);
175+
}
173176

174177
// Ex) List[...]
175178
if checker.any_enabled(&[
@@ -293,6 +296,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
293296
}
294297
}
295298
}
299+
if checker.enabled(Rule::RegexFlagAlias) {
300+
refurb::rules::regex_flag_alias(checker, expr);
301+
}
296302
if checker.enabled(Rule::DatetimeTimezoneUTC) {
297303
if checker.settings.target_version >= PythonVersion::Py311 {
298304
pyupgrade::rules::datetime_utc_alias(checker, expr);

crates/ruff_linter/src/codes.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -987,6 +987,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
987987
(Refurb, "152") => (RuleGroup::Preview, rules::refurb::rules::MathConstant),
988988
(Refurb, "161") => (RuleGroup::Preview, rules::refurb::rules::BitCount),
989989
(Refurb, "163") => (RuleGroup::Preview, rules::refurb::rules::RedundantLogBase),
990+
(Refurb, "167") => (RuleGroup::Preview, rules::refurb::rules::RegexFlagAlias),
990991
(Refurb, "168") => (RuleGroup::Preview, rules::refurb::rules::IsinstanceTypeNone),
991992
(Refurb, "169") => (RuleGroup::Preview, rules::refurb::rules::TypeNoneComparison),
992993
(Refurb, "171") => (RuleGroup::Preview, rules::refurb::rules::SingleItemMembershipTest),

crates/ruff_linter/src/rules/refurb/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ mod tests {
2828
#[test_case(Rule::ImplicitCwd, Path::new("FURB177.py"))]
2929
#[test_case(Rule::SingleItemMembershipTest, Path::new("FURB171.py"))]
3030
#[test_case(Rule::BitCount, Path::new("FURB161.py"))]
31+
#[test_case(Rule::RegexFlagAlias, Path::new("FURB167.py"))]
3132
#[test_case(Rule::IsinstanceTypeNone, Path::new("FURB168.py"))]
3233
#[test_case(Rule::TypeNoneComparison, Path::new("FURB169.py"))]
3334
#[test_case(Rule::RedundantLogBase, Path::new("FURB163.py"))]

crates/ruff_linter/src/rules/refurb/rules/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pub(crate) use math_constant::*;
99
pub(crate) use print_empty_string::*;
1010
pub(crate) use read_whole_file::*;
1111
pub(crate) use redundant_log_base::*;
12+
pub(crate) use regex_flag_alias::*;
1213
pub(crate) use reimplemented_operator::*;
1314
pub(crate) use reimplemented_starmap::*;
1415
pub(crate) use repeated_append::*;
@@ -28,6 +29,7 @@ mod math_constant;
2829
mod print_empty_string;
2930
mod read_whole_file;
3031
mod redundant_log_base;
32+
mod regex_flag_alias;
3133
mod reimplemented_operator;
3234
mod reimplemented_starmap;
3335
mod repeated_append;
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
2+
use ruff_macros::{derive_message_formats, violation};
3+
use ruff_python_ast::Expr;
4+
use ruff_text_size::Ranged;
5+
6+
use crate::checkers::ast::Checker;
7+
use crate::importer::ImportRequest;
8+
9+
/// ## What it does
10+
/// Checks for the use of shorthand aliases for regular expression flags
11+
/// (e.g., `re.I` instead of `re.IGNORECASE`).
12+
///
13+
/// ## Why is this bad?
14+
/// The regular expression module provides descriptive names for each flag,
15+
/// along with single-letter aliases. Prefer the descriptive names, as they
16+
/// are more readable and self-documenting.
17+
///
18+
/// ## Example
19+
/// ```python
20+
/// import re
21+
///
22+
/// if re.match("^hello", "hello world", re.I):
23+
/// ...
24+
/// ```
25+
///
26+
/// Use instead:
27+
/// ```python
28+
/// import re
29+
///
30+
/// if re.match("^hello", "hello world", re.IGNORECASE):
31+
/// ...
32+
/// ```
33+
///
34+
#[violation]
35+
pub struct RegexFlagAlias {
36+
alias: &'static str,
37+
full_name: &'static str,
38+
}
39+
40+
impl AlwaysFixableViolation for RegexFlagAlias {
41+
#[derive_message_formats]
42+
fn message(&self) -> String {
43+
let RegexFlagAlias { alias, .. } = self;
44+
format!("Use of regular expression alias `re.{alias}`")
45+
}
46+
47+
fn fix_title(&self) -> String {
48+
let RegexFlagAlias { full_name, .. } = self;
49+
format!("Replace with `re.{full_name}`")
50+
}
51+
}
52+
53+
/// FURB167
54+
pub(crate) fn regex_flag_alias(checker: &mut Checker, expr: &Expr) {
55+
let Some(flag) =
56+
checker
57+
.semantic()
58+
.resolve_call_path(expr)
59+
.and_then(|call_path| match call_path.as_slice() {
60+
["re", "A"] => Some(RegexFlag::Ascii),
61+
["re", "I"] => Some(RegexFlag::IgnoreCase),
62+
["re", "L"] => Some(RegexFlag::Locale),
63+
["re", "M"] => Some(RegexFlag::Multiline),
64+
["re", "S"] => Some(RegexFlag::DotAll),
65+
["re", "T"] => Some(RegexFlag::Template),
66+
["re", "U"] => Some(RegexFlag::Unicode),
67+
["re", "X"] => Some(RegexFlag::Verbose),
68+
_ => None,
69+
})
70+
else {
71+
return;
72+
};
73+
74+
let mut diagnostic = Diagnostic::new(
75+
RegexFlagAlias {
76+
alias: flag.alias(),
77+
full_name: flag.full_name(),
78+
},
79+
expr.range(),
80+
);
81+
diagnostic.try_set_fix(|| {
82+
let (edit, binding) = checker.importer().get_or_import_symbol(
83+
&ImportRequest::import("re", flag.full_name()),
84+
expr.start(),
85+
checker.semantic(),
86+
)?;
87+
Ok(Fix::safe_edits(
88+
Edit::range_replacement(binding, expr.range()),
89+
[edit],
90+
))
91+
});
92+
checker.diagnostics.push(diagnostic);
93+
}
94+
95+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96+
enum RegexFlag {
97+
Ascii,
98+
IgnoreCase,
99+
Locale,
100+
Multiline,
101+
DotAll,
102+
Template,
103+
Unicode,
104+
Verbose,
105+
}
106+
107+
impl RegexFlag {
108+
fn alias(self) -> &'static str {
109+
match self {
110+
Self::Ascii => "A",
111+
Self::IgnoreCase => "I",
112+
Self::Locale => "L",
113+
Self::Multiline => "M",
114+
Self::DotAll => "S",
115+
Self::Template => "T",
116+
Self::Unicode => "U",
117+
Self::Verbose => "X",
118+
}
119+
}
120+
121+
fn full_name(self) -> &'static str {
122+
match self {
123+
Self::Ascii => "ASCII",
124+
Self::IgnoreCase => "IGNORECASE",
125+
Self::Locale => "LOCALE",
126+
Self::Multiline => "MULTILINE",
127+
Self::DotAll => "DOTALL",
128+
Self::Template => "TEMPLATE",
129+
Self::Unicode => "UNICODE",
130+
Self::Verbose => "VERBOSE",
131+
}
132+
}
133+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
source: crates/ruff_linter/src/rules/refurb/mod.rs
3+
---
4+
FURB167.py:13:42: FURB167 [*] Use of regular expression alias `re.I`
5+
|
6+
12 | # FURB167
7+
13 | if re.match("^hello", "hello world", re.I):
8+
| ^^^^ FURB167
9+
14 | pass
10+
|
11+
= help: Replace with `re.IGNORECASE`
12+
13+
Safe fix
14+
10 10 | import re
15+
11 11 |
16+
12 12 | # FURB167
17+
13 |- if re.match("^hello", "hello world", re.I):
18+
13 |+ if re.match("^hello", "hello world", re.IGNORECASE):
19+
14 14 | pass
20+
15 15 |
21+
16 16 |
22+
23+
FURB167.py:21:39: FURB167 [*] Use of regular expression alias `re.I`
24+
|
25+
20 | # FURB167
26+
21 | if match("^hello", "hello world", I):
27+
| ^ FURB167
28+
22 | pass
29+
|
30+
= help: Replace with `re.IGNORECASE`
31+
32+
Safe fix
33+
1 |+import re
34+
1 2 | def func():
35+
2 3 | import re
36+
3 4 |
37+
--------------------------------------------------------------------------------
38+
18 19 | from re import match, I
39+
19 20 |
40+
20 21 | # FURB167
41+
21 |- if match("^hello", "hello world", I):
42+
22 |+ if match("^hello", "hello world", re.IGNORECASE):
43+
22 23 | pass
44+
45+

ruff.schema.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)