Skip to content

Commit a6a50e2

Browse files
committed
Limit RUF036 to typing contexts; make it unsafe for non-typing-only
1 parent 78a6efd commit a6a50e2

5 files changed

Lines changed: 72 additions & 27 deletions

File tree

crates/ruff_linter/resources/test/fixtures/ruff/RUF036.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,13 @@ def func14(arg: None | int | str | bytes):
6666
...
6767

6868

69-
# Preserve token boundaries when fixing non-annotation expressions.
70-
print(None | (int)and 2)
71-
print(2 or(None) | int)
69+
# Preserve token boundaries when fixing annotations.
70+
def func15(arg: None | (int)and 2):
71+
...
72+
73+
74+
def func16(arg: 2 or(None) | int):
75+
...
7276

7377

7478
# Ok

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ mod tests {
2222
use crate::rules::pydocstyle::settings::Settings as PydocstyleSettings;
2323
use crate::settings::LinterSettings;
2424
use crate::settings::types::{CompiledPerFileIgnoreList, PerFileIgnore, PreviewMode};
25-
use crate::test::{test_path, test_resource_path};
25+
use crate::test::{test_path, test_resource_path, test_snippet};
2626
use crate::{assert_diagnostics, assert_diagnostics_diff, settings};
2727

2828
#[test_case(Rule::CollectionLiteralConcatenation, Path::new("RUF005.py"))]
@@ -228,6 +228,23 @@ mod tests {
228228
Ok(())
229229
}
230230

231+
#[test]
232+
fn none_not_at_end_of_union_py313() {
233+
let diagnostics = test_snippet(
234+
r"
235+
def func(arg: None | int):
236+
...
237+
238+
print(None | (int)and 2)
239+
",
240+
&settings::LinterSettings {
241+
unresolved_target_version: PythonVersion::PY313.into(),
242+
..settings::LinterSettings::for_rule(Rule::NoneNotAtEndOfUnion)
243+
},
244+
);
245+
assert_diagnostics!("PY313_RUF036_runtime_evaluated", diagnostics);
246+
}
247+
231248
#[test]
232249
fn access_annotations_from_class_dict_py310() -> Result<()> {
233250
let diagnostics = test_path(

crates/ruff_linter/src/rules/ruff/rules/none_not_at_end_of_union.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ fn is_union_expr(semantic: &SemanticModel, expr: &Expr) -> bool {
9191
/// RUF036
9292
pub(crate) fn none_not_at_end_of_union<'a>(checker: &Checker, union: &'a Expr) {
9393
let semantic = checker.semantic();
94+
95+
if !semantic.in_type_definition() {
96+
return;
97+
}
98+
9499
let mut none_exprs: Vec<&Expr> = Vec::new();
95100
let mut other_exprs: Vec<&Expr> = Vec::new();
96101

@@ -150,10 +155,12 @@ fn generate_fix(
150155
annotation: &Expr,
151156
is_pep604: bool,
152157
) -> Option<Fix> {
153-
let applicability = if checker.comment_ranges().intersects(annotation.range()) {
154-
Applicability::Unsafe
155-
} else {
158+
let applicability = if checker.semantic().in_typing_only_annotation()
159+
&& !checker.comment_ranges().intersects(annotation.range())
160+
{
156161
Applicability::Safe
162+
} else {
163+
Applicability::Unsafe
157164
};
158165

159166
let reordered: Vec<Expr> = other_exprs
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
source: crates/ruff_linter/src/rules/ruff/mod.rs
3+
---
4+
RUF036 [*] `None` not at the end of the type union.
5+
--> <filename>:2:15
6+
|
7+
2 | def func(arg: None | int):
8+
| ^^^^^^^^^^
9+
3 | ...
10+
|
11+
help: Move `None` to the end of the type union
12+
1 |
13+
- def func(arg: None | int):
14+
2 + def func(arg: int | None):
15+
3 | ...
16+
4 |
17+
5 | print(None | (int)and 2)
18+
note: This is an unsafe fix and may change runtime behavior

crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF036_RUF036.py.snap

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -241,37 +241,36 @@ help: Move `None` to the end of the type union
241241
68 |
242242

243243
RUF036 [*] `None` not at the end of the type union.
244-
--> RUF036.py:70:7
244+
--> RUF036.py:70:17
245245
|
246-
69 | # Preserve token boundaries when fixing non-annotation expressions.
247-
70 | print(None | (int)and 2)
248-
| ^^^^^^^^^^^^
249-
71 | print(2 or(None) | int)
246+
69 | # Preserve token boundaries when fixing annotations.
247+
70 | def func15(arg: None | (int)and 2):
248+
| ^^^^^^^^^^^^
249+
71 | ...
250250
|
251251
help: Move `None` to the end of the type union
252252
67 |
253253
68 |
254-
69 | # Preserve token boundaries when fixing non-annotation expressions.
255-
- print(None | (int)and 2)
256-
70 + print(int | None and 2)
257-
71 | print(2 or(None) | int)
254+
69 | # Preserve token boundaries when fixing annotations.
255+
- def func15(arg: None | (int)and 2):
256+
70 + def func15(arg: int | None and 2):
257+
71 | ...
258258
72 |
259259
73 |
260260

261261
RUF036 [*] `None` not at the end of the type union.
262-
--> RUF036.py:71:11
262+
--> RUF036.py:74:21
263263
|
264-
69 | # Preserve token boundaries when fixing non-annotation expressions.
265-
70 | print(None | (int)and 2)
266-
71 | print(2 or(None) | int)
267-
| ^^^^^^^^^^^^
264+
74 | def func16(arg: 2 or(None) | int):
265+
| ^^^^^^^^^^^^
266+
75 | ...
268267
|
269268
help: Move `None` to the end of the type union
270-
68 |
271-
69 | # Preserve token boundaries when fixing non-annotation expressions.
272-
70 | print(None | (int)and 2)
273-
- print(2 or(None) | int)
274-
71 + print(2 or int | None)
269+
71 | ...
275270
72 |
276271
73 |
277-
74 | # Ok
272+
- def func16(arg: 2 or(None) | int):
273+
74 + def func16(arg: 2 or int | None):
274+
75 | ...
275+
76 |
276+
77 |

0 commit comments

Comments
 (0)