[ruff] Auto-add r prefix when string has no backslashes for unraw-re-pattern (RUF039) (#14536)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Blocked by required conditions
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions

This PR adds a sometimes-available, safe autofix for [unraw-re-pattern
(RUF039)](https://docs.astral.sh/ruff/rules/unraw-re-pattern/#unraw-re-pattern-ruf039),
which prepends an `r` prefix. It is used only when the string in
question has no backslahses (and also does not have a `u` prefix, since
that causes a syntax error.)

Closes #14527

Notes: 
- Test fixture unchanged, but snapshot changed to include fix messages.
- This fix is automatically only available in preview since the rule
itself is in preview
This commit is contained in:
Dylan 2024-11-22 15:09:53 -06:00 committed by GitHub
parent 931fa06d85
commit 3fda2d17c7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 294 additions and 29 deletions

View file

@ -1,13 +1,15 @@
use std::fmt::{Display, Formatter};
use std::str::FromStr;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{
BytesLiteral, Expr, ExprBytesLiteral, ExprCall, ExprStringLiteral, StringLiteral,
};
use ruff_python_semantic::{Modules, SemanticModel};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
/// ## What it does
@ -41,6 +43,7 @@ pub struct UnrawRePattern {
}
impl Violation for UnrawRePattern {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
let Self { module, func, kind } = &self;
@ -158,8 +161,22 @@ fn check_string(checker: &mut Checker, literal: &StringLiteral, module: RegexMod
let kind = PatternKind::String;
let func = func.to_string();
let range = literal.range;
let diagnostic = Diagnostic::new(UnrawRePattern { module, func, kind }, range);
let mut diagnostic = Diagnostic::new(UnrawRePattern { module, func, kind }, range);
if
// The (no-op) `u` prefix is a syntax error when combined with `r`
!literal.flags.prefix().is_unicode()
// We are looking for backslash characters
// in the raw source code here, because `\n`
// gets converted to a single character already
// at the lexing stage.
&&!checker.locator().slice(literal.range()).contains('\\')
{
diagnostic.set_fix(Fix::safe_edit(Edit::insertion(
"r".to_string(),
literal.range().start(),
)));
}
checker.diagnostics.push(diagnostic);
}

View file

@ -1,8 +1,7 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
snapshot_kind: text
---
RUF039.py:5:12: RUF039 First argument to `re.compile()` is not raw string
RUF039.py:5:12: RUF039 [*] First argument to `re.compile()` is not raw string
|
4 | # Errors
5 | re.compile('single free-spacing', flags=re.X)
@ -12,6 +11,16 @@ RUF039.py:5:12: RUF039 First argument to `re.compile()` is not raw string
|
= help: Replace with raw string
Safe fix
2 2 | import regex
3 3 |
4 4 | # Errors
5 |-re.compile('single free-spacing', flags=re.X)
5 |+re.compile(r'single free-spacing', flags=re.X)
6 6 | re.findall('si\ngle')
7 7 | re.finditer("dou\ble")
8 8 | re.fullmatch('''t\riple single''')
RUF039.py:6:12: RUF039 First argument to `re.findall()` is not raw string
|
4 | # Errors
@ -56,7 +65,7 @@ RUF039.py:9:10: RUF039 First argument to `re.match()` is not raw string
|
= help: Replace with raw string
RUF039.py:10:11: RUF039 First argument to `re.search()` is not raw string
RUF039.py:10:11: RUF039 [*] First argument to `re.search()` is not raw string
|
8 | re.fullmatch('''t\riple single''')
9 | re.match("""\triple double""")
@ -67,7 +76,17 @@ RUF039.py:10:11: RUF039 First argument to `re.search()` is not raw string
|
= help: Replace with raw string
RUF039.py:11:10: RUF039 First argument to `re.split()` is not raw string
Safe fix
7 7 | re.finditer("dou\ble")
8 8 | re.fullmatch('''t\riple single''')
9 9 | re.match("""\triple double""")
10 |-re.search('two', 'args')
10 |+re.search(r'two', 'args')
11 11 | re.split("raw", r'second')
12 12 | re.sub(u'''nicode''', u"f(?i)rst")
13 13 | re.subn(b"""ytes are""", f"\u006e")
RUF039.py:11:10: RUF039 [*] First argument to `re.split()` is not raw string
|
9 | re.match("""\triple double""")
10 | re.search('two', 'args')
@ -78,6 +97,16 @@ RUF039.py:11:10: RUF039 First argument to `re.split()` is not raw string
|
= help: Replace with raw string
Safe fix
8 8 | re.fullmatch('''t\riple single''')
9 9 | re.match("""\triple double""")
10 10 | re.search('two', 'args')
11 |-re.split("raw", r'second')
11 |+re.split(r"raw", r'second')
12 12 | re.sub(u'''nicode''', u"f(?i)rst")
13 13 | re.subn(b"""ytes are""", f"\u006e")
14 14 |
RUF039.py:12:8: RUF039 First argument to `re.sub()` is not raw string
|
10 | re.search('two', 'args')
@ -99,7 +128,7 @@ RUF039.py:13:9: RUF039 First argument to `re.subn()` is not raw bytes literal
|
= help: Replace with raw bytes literal
RUF039.py:15:15: RUF039 First argument to `regex.compile()` is not raw string
RUF039.py:15:15: RUF039 [*] First argument to `regex.compile()` is not raw string
|
13 | re.subn(b"""ytes are""", f"\u006e")
14 |
@ -110,6 +139,16 @@ RUF039.py:15:15: RUF039 First argument to `regex.compile()` is not raw string
|
= help: Replace with raw string
Safe fix
12 12 | re.sub(u'''nicode''', u"f(?i)rst")
13 13 | re.subn(b"""ytes are""", f"\u006e")
14 14 |
15 |-regex.compile('single free-spacing', flags=regex.X)
15 |+regex.compile(r'single free-spacing', flags=regex.X)
16 16 | regex.findall('si\ngle')
17 17 | regex.finditer("dou\ble")
18 18 | regex.fullmatch('''t\riple single''')
RUF039.py:16:15: RUF039 First argument to `regex.findall()` is not raw string
|
15 | regex.compile('single free-spacing', flags=regex.X)
@ -153,7 +192,7 @@ RUF039.py:19:13: RUF039 First argument to `regex.match()` is not raw string
|
= help: Replace with raw string
RUF039.py:20:14: RUF039 First argument to `regex.search()` is not raw string
RUF039.py:20:14: RUF039 [*] First argument to `regex.search()` is not raw string
|
18 | regex.fullmatch('''t\riple single''')
19 | regex.match("""\triple double""")
@ -164,7 +203,17 @@ RUF039.py:20:14: RUF039 First argument to `regex.search()` is not raw string
|
= help: Replace with raw string
RUF039.py:21:13: RUF039 First argument to `regex.split()` is not raw string
Safe fix
17 17 | regex.finditer("dou\ble")
18 18 | regex.fullmatch('''t\riple single''')
19 19 | regex.match("""\triple double""")
20 |-regex.search('two', 'args')
20 |+regex.search(r'two', 'args')
21 21 | regex.split("raw", r'second')
22 22 | regex.sub(u'''nicode''', u"f(?i)rst")
23 23 | regex.subn(b"""ytes are""", f"\u006e")
RUF039.py:21:13: RUF039 [*] First argument to `regex.split()` is not raw string
|
19 | regex.match("""\triple double""")
20 | regex.search('two', 'args')
@ -175,6 +224,16 @@ RUF039.py:21:13: RUF039 First argument to `regex.split()` is not raw string
|
= help: Replace with raw string
Safe fix
18 18 | regex.fullmatch('''t\riple single''')
19 19 | regex.match("""\triple double""")
20 20 | regex.search('two', 'args')
21 |-regex.split("raw", r'second')
21 |+regex.split(r"raw", r'second')
22 22 | regex.sub(u'''nicode''', u"f(?i)rst")
23 23 | regex.subn(b"""ytes are""", f"\u006e")
24 24 |
RUF039.py:22:11: RUF039 First argument to `regex.sub()` is not raw string
|
20 | regex.search('two', 'args')
@ -196,7 +255,7 @@ RUF039.py:23:12: RUF039 First argument to `regex.subn()` is not raw bytes litera
|
= help: Replace with raw bytes literal
RUF039.py:25:16: RUF039 First argument to `regex.template()` is not raw string
RUF039.py:25:16: RUF039 [*] First argument to `regex.template()` is not raw string
|
23 | regex.subn(b"""ytes are""", f"\u006e")
24 |
@ -209,3 +268,13 @@ RUF039.py:25:16: RUF039 First argument to `regex.template()` is not raw string
| |___^ RUF039
|
= help: Replace with raw string
Safe fix
22 22 | regex.sub(u'''nicode''', u"f(?i)rst")
23 23 | regex.subn(b"""ytes are""", f"\u006e")
24 24 |
25 |-regex.template("""(?m)
25 |+regex.template(r"""(?m)
26 26 | (?:ulti)?
27 27 | (?=(?<!(?<=(?!l)))
28 28 | l(?i:ne)

View file

@ -1,8 +1,7 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
snapshot_kind: text
---
RUF039_concat.py:5:5: RUF039 First argument to `re.compile()` is not raw string
RUF039_concat.py:5:5: RUF039 [*] First argument to `re.compile()` is not raw string
|
4 | re.compile(
5 | 'implicit'
@ -12,7 +11,17 @@ RUF039_concat.py:5:5: RUF039 First argument to `re.compile()` is not raw string
|
= help: Replace with raw string
RUF039_concat.py:6:5: RUF039 First argument to `re.compile()` is not raw string
Safe fix
2 2 |
3 3 |
4 4 | re.compile(
5 |- 'implicit'
5 |+ r'implicit'
6 6 | 'concatenation'
7 7 | )
8 8 | re.findall(
RUF039_concat.py:6:5: RUF039 [*] First argument to `re.compile()` is not raw string
|
4 | re.compile(
5 | 'implicit'
@ -23,7 +32,17 @@ RUF039_concat.py:6:5: RUF039 First argument to `re.compile()` is not raw string
|
= help: Replace with raw string
RUF039_concat.py:12:5: RUF039 First argument to `re.findall()` is not raw string
Safe fix
3 3 |
4 4 | re.compile(
5 5 | 'implicit'
6 |- 'concatenation'
6 |+ r'concatenation'
7 7 | )
8 8 | re.findall(
9 9 | r'''
RUF039_concat.py:12:5: RUF039 [*] First argument to `re.findall()` is not raw string
|
10 | multiline
11 | '''
@ -37,6 +56,16 @@ RUF039_concat.py:12:5: RUF039 First argument to `re.findall()` is not raw string
|
= help: Replace with raw string
Safe fix
9 9 | r'''
10 10 | multiline
11 11 | '''
12 |- """
12 |+ r"""
13 13 | concatenation
14 14 | """
15 15 | )
RUF039_concat.py:26:5: RUF039 First argument to `re.match()` is not raw bytes literal
|
24 | )
@ -59,7 +88,7 @@ RUF039_concat.py:30:8: RUF039 First argument to `re.search()` is not raw string
|
= help: Replace with raw string
RUF039_concat.py:31:5: RUF039 First argument to `re.search()` is not raw string
RUF039_concat.py:31:5: RUF039 [*] First argument to `re.search()` is not raw string
|
29 | re.search(
30 | r''u''
@ -70,7 +99,17 @@ RUF039_concat.py:31:5: RUF039 First argument to `re.search()` is not raw string
|
= help: Replace with raw string
RUF039_concat.py:33:10: RUF039 First argument to `re.split()` is not raw string
Safe fix
28 28 | )
29 29 | re.search(
30 30 | r''u''
31 |- '''okay?'''
31 |+ r'''okay?'''
32 32 | )
33 33 | re.split(''U"""w"""U'')
34 34 | re.sub(
RUF039_concat.py:33:10: RUF039 [*] First argument to `re.split()` is not raw string
|
31 | '''okay?'''
32 | )
@ -81,6 +120,16 @@ RUF039_concat.py:33:10: RUF039 First argument to `re.split()` is not raw string
|
= help: Replace with raw string
Safe fix
30 30 | r''u''
31 31 | '''okay?'''
32 32 | )
33 |-re.split(''U"""w"""U'')
33 |+re.split(r''U"""w"""U'')
34 34 | re.sub(
35 35 | "I''m o"
36 36 | 'utta ideas'
RUF039_concat.py:33:12: RUF039 First argument to `re.split()` is not raw string
|
31 | '''okay?'''
@ -103,7 +152,7 @@ RUF039_concat.py:33:20: RUF039 First argument to `re.split()` is not raw string
|
= help: Replace with raw string
RUF039_concat.py:35:5: RUF039 First argument to `re.sub()` is not raw string
RUF039_concat.py:35:5: RUF039 [*] First argument to `re.sub()` is not raw string
|
33 | re.split(''U"""w"""U'')
34 | re.sub(
@ -114,7 +163,17 @@ RUF039_concat.py:35:5: RUF039 First argument to `re.sub()` is not raw string
|
= help: Replace with raw string
RUF039_concat.py:36:5: RUF039 First argument to `re.sub()` is not raw string
Safe fix
32 32 | )
33 33 | re.split(''U"""w"""U'')
34 34 | re.sub(
35 |- "I''m o"
35 |+ r"I''m o"
36 36 | 'utta ideas'
37 37 | )
38 38 | re.subn("()"r' am I'"??")
RUF039_concat.py:36:5: RUF039 [*] First argument to `re.sub()` is not raw string
|
34 | re.sub(
35 | "I''m o"
@ -125,7 +184,17 @@ RUF039_concat.py:36:5: RUF039 First argument to `re.sub()` is not raw string
|
= help: Replace with raw string
RUF039_concat.py:38:9: RUF039 First argument to `re.subn()` is not raw string
Safe fix
33 33 | re.split(''U"""w"""U'')
34 34 | re.sub(
35 35 | "I''m o"
36 |- 'utta ideas'
36 |+ r'utta ideas'
37 37 | )
38 38 | re.subn("()"r' am I'"??")
39 39 |
RUF039_concat.py:38:9: RUF039 [*] First argument to `re.subn()` is not raw string
|
36 | 'utta ideas'
37 | )
@ -134,7 +203,17 @@ RUF039_concat.py:38:9: RUF039 First argument to `re.subn()` is not raw string
|
= help: Replace with raw string
RUF039_concat.py:38:21: RUF039 First argument to `re.subn()` is not raw string
Safe fix
35 35 | "I''m o"
36 36 | 'utta ideas'
37 37 | )
38 |-re.subn("()"r' am I'"??")
38 |+re.subn(r"()"r' am I'"??")
39 39 |
40 40 |
41 41 | import regex
RUF039_concat.py:38:21: RUF039 [*] First argument to `re.subn()` is not raw string
|
36 | 'utta ideas'
37 | )
@ -143,7 +222,17 @@ RUF039_concat.py:38:21: RUF039 First argument to `re.subn()` is not raw string
|
= help: Replace with raw string
RUF039_concat.py:45:5: RUF039 First argument to `regex.compile()` is not raw string
Safe fix
35 35 | "I''m o"
36 36 | 'utta ideas'
37 37 | )
38 |-re.subn("()"r' am I'"??")
38 |+re.subn("()"r' am I'r"??")
39 39 |
40 40 |
41 41 | import regex
RUF039_concat.py:45:5: RUF039 [*] First argument to `regex.compile()` is not raw string
|
44 | regex.compile(
45 | 'implicit'
@ -153,7 +242,17 @@ RUF039_concat.py:45:5: RUF039 First argument to `regex.compile()` is not raw str
|
= help: Replace with raw string
RUF039_concat.py:46:5: RUF039 First argument to `regex.compile()` is not raw string
Safe fix
42 42 |
43 43 |
44 44 | regex.compile(
45 |- 'implicit'
45 |+ r'implicit'
46 46 | 'concatenation'
47 47 | )
48 48 | regex.findall(
RUF039_concat.py:46:5: RUF039 [*] First argument to `regex.compile()` is not raw string
|
44 | regex.compile(
45 | 'implicit'
@ -164,7 +263,17 @@ RUF039_concat.py:46:5: RUF039 First argument to `regex.compile()` is not raw str
|
= help: Replace with raw string
RUF039_concat.py:52:5: RUF039 First argument to `regex.findall()` is not raw string
Safe fix
43 43 |
44 44 | regex.compile(
45 45 | 'implicit'
46 |- 'concatenation'
46 |+ r'concatenation'
47 47 | )
48 48 | regex.findall(
49 49 | r'''
RUF039_concat.py:52:5: RUF039 [*] First argument to `regex.findall()` is not raw string
|
50 | multiline
51 | '''
@ -178,6 +287,16 @@ RUF039_concat.py:52:5: RUF039 First argument to `regex.findall()` is not raw str
|
= help: Replace with raw string
Safe fix
49 49 | r'''
50 50 | multiline
51 51 | '''
52 |- """
52 |+ r"""
53 53 | concatenation
54 54 | """
55 55 | )
RUF039_concat.py:66:5: RUF039 First argument to `regex.match()` is not raw bytes literal
|
64 | )
@ -200,7 +319,7 @@ RUF039_concat.py:70:8: RUF039 First argument to `regex.search()` is not raw stri
|
= help: Replace with raw string
RUF039_concat.py:71:5: RUF039 First argument to `regex.search()` is not raw string
RUF039_concat.py:71:5: RUF039 [*] First argument to `regex.search()` is not raw string
|
69 | regex.search(
70 | r''u''
@ -211,7 +330,17 @@ RUF039_concat.py:71:5: RUF039 First argument to `regex.search()` is not raw stri
|
= help: Replace with raw string
RUF039_concat.py:73:13: RUF039 First argument to `regex.split()` is not raw string
Safe fix
68 68 | )
69 69 | regex.search(
70 70 | r''u''
71 |- '''okay?'''
71 |+ r'''okay?'''
72 72 | )
73 73 | regex.split(''U"""w"""U'')
74 74 | regex.sub(
RUF039_concat.py:73:13: RUF039 [*] First argument to `regex.split()` is not raw string
|
71 | '''okay?'''
72 | )
@ -222,6 +351,16 @@ RUF039_concat.py:73:13: RUF039 First argument to `regex.split()` is not raw stri
|
= help: Replace with raw string
Safe fix
70 70 | r''u''
71 71 | '''okay?'''
72 72 | )
73 |-regex.split(''U"""w"""U'')
73 |+regex.split(r''U"""w"""U'')
74 74 | regex.sub(
75 75 | "I''m o"
76 76 | 'utta ideas'
RUF039_concat.py:73:15: RUF039 First argument to `regex.split()` is not raw string
|
71 | '''okay?'''
@ -244,7 +383,7 @@ RUF039_concat.py:73:23: RUF039 First argument to `regex.split()` is not raw stri
|
= help: Replace with raw string
RUF039_concat.py:75:5: RUF039 First argument to `regex.sub()` is not raw string
RUF039_concat.py:75:5: RUF039 [*] First argument to `regex.sub()` is not raw string
|
73 | regex.split(''U"""w"""U'')
74 | regex.sub(
@ -255,7 +394,17 @@ RUF039_concat.py:75:5: RUF039 First argument to `regex.sub()` is not raw string
|
= help: Replace with raw string
RUF039_concat.py:76:5: RUF039 First argument to `regex.sub()` is not raw string
Safe fix
72 72 | )
73 73 | regex.split(''U"""w"""U'')
74 74 | regex.sub(
75 |- "I''m o"
75 |+ r"I''m o"
76 76 | 'utta ideas'
77 77 | )
78 78 | regex.subn("()"r' am I'"??")
RUF039_concat.py:76:5: RUF039 [*] First argument to `regex.sub()` is not raw string
|
74 | regex.sub(
75 | "I''m o"
@ -266,7 +415,17 @@ RUF039_concat.py:76:5: RUF039 First argument to `regex.sub()` is not raw string
|
= help: Replace with raw string
RUF039_concat.py:78:12: RUF039 First argument to `regex.subn()` is not raw string
Safe fix
73 73 | regex.split(''U"""w"""U'')
74 74 | regex.sub(
75 75 | "I''m o"
76 |- 'utta ideas'
76 |+ r'utta ideas'
77 77 | )
78 78 | regex.subn("()"r' am I'"??")
79 79 |
RUF039_concat.py:78:12: RUF039 [*] First argument to `regex.subn()` is not raw string
|
76 | 'utta ideas'
77 | )
@ -275,7 +434,17 @@ RUF039_concat.py:78:12: RUF039 First argument to `regex.subn()` is not raw strin
|
= help: Replace with raw string
RUF039_concat.py:78:24: RUF039 First argument to `regex.subn()` is not raw string
Safe fix
75 75 | "I''m o"
76 76 | 'utta ideas'
77 77 | )
78 |-regex.subn("()"r' am I'"??")
78 |+regex.subn(r"()"r' am I'"??")
79 79 |
80 80 |
81 81 | regex.template(
RUF039_concat.py:78:24: RUF039 [*] First argument to `regex.subn()` is not raw string
|
76 | 'utta ideas'
77 | )
@ -283,3 +452,13 @@ RUF039_concat.py:78:24: RUF039 First argument to `regex.subn()` is not raw strin
| ^^^^ RUF039
|
= help: Replace with raw string
Safe fix
75 75 | "I''m o"
76 76 | 'utta ideas'
77 77 | )
78 |-regex.subn("()"r' am I'"??")
78 |+regex.subn("()"r' am I'r"??")
79 79 |
80 80 |
81 81 | regex.template(