Skip to content

Commit bf592e5

Browse files
committed
Avoid inserting redundant None elements
1 parent 033a4fb commit bf592e5

3 files changed

Lines changed: 155 additions & 10 deletions

File tree

crates/ruff_linter/resources/test/fixtures/pyupgrade/UP045.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,12 @@ class ServiceRefOrValue:
8282
int
8383
# text
8484
] = None
85+
86+
87+
# Regression test for: https://github.com/astral-sh/ruff/issues/23429
88+
# Optional[None | X] should not produce None | None
89+
bar: None | Optional[None | int] = None
90+
bar: Optional[None | int] = None
91+
bar: Optional[int | None] = None
92+
bar: Optional[None | int | str] = None
93+
bar: Optional[None | None] = None

crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_annotation.rs

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use ruff_macros::{ViolationMetadata, derive_message_formats};
22
use ruff_python_ast::PythonVersion;
33
use ruff_python_ast::helpers::{pep_604_optional, pep_604_union};
4-
use ruff_python_ast::{self as ast, Expr};
4+
use ruff_python_ast::{self as ast, Expr, Operator};
55
use ruff_python_semantic::analyze::typing::{Pep604Operator, to_pep604_operator};
66
use ruff_source_file::LineRanges;
77
use ruff_text_size::Ranged;
@@ -190,17 +190,40 @@ pub(crate) fn non_pep604_annotation(
190190
}
191191
}
192192

193-
diagnostic.set_fix(Fix::applicable_edit(
194-
Edit::range_replacement(
195-
pad(
196-
checker.generator().expr(&pep_604_optional(inner)),
193+
// If the inner expression is a `BitOr` union that already
194+
// contains `None`, strip it out and re-add it only at the end.
195+
// This avoids generating `None | None` which is a runtime
196+
// `TypeError`. For example, `Optional[None | int]` should
197+
// become `int | None`, not `None | int | None`.
198+
let fix_expr = if let Expr::BinOp(ast::ExprBinOp {
199+
op: Operator::BitOr,
200+
..
201+
}) = inner
202+
{
203+
let elements = collect_non_none(inner);
204+
if elements.is_empty() {
205+
// All elements were `None`; don't provide a fix.
206+
None
207+
} else {
208+
Some(pep_604_optional(&pep_604_union(&elements)))
209+
}
210+
} else {
211+
Some(pep_604_optional(inner))
212+
};
213+
214+
if let Some(fix_expr) = fix_expr {
215+
diagnostic.set_fix(Fix::applicable_edit(
216+
Edit::range_replacement(
217+
pad(
218+
checker.generator().expr(&fix_expr),
219+
expr.range(),
220+
checker.locator(),
221+
),
197222
expr.range(),
198-
checker.locator(),
199223
),
200-
expr.range(),
201-
),
202-
applicability,
203-
));
224+
applicability,
225+
));
226+
}
204227
}
205228
}
206229
}
@@ -332,3 +355,27 @@ fn is_named_tuple(checker: &Checker, expr: &Expr) -> bool {
332355
fn is_optional_none(operator: Pep604Operator, slice: &Expr) -> bool {
333356
matches!(operator, Pep604Operator::Optional) && matches!(slice, Expr::NoneLiteral(_))
334357
}
358+
359+
/// Collect all non-`None` leaf elements of a chain of `BitOr` binary operations.
360+
///
361+
/// For example, `a | None | b` is collected as `[a, b]`.
362+
fn collect_non_none(expr: &Expr) -> Vec<Expr> {
363+
fn inner(expr: &Expr, elements: &mut Vec<Expr>) {
364+
if let Expr::BinOp(ast::ExprBinOp {
365+
left,
366+
op: Operator::BitOr,
367+
right,
368+
..
369+
}) = expr
370+
{
371+
inner(left, elements);
372+
inner(right, elements);
373+
} else if !expr.is_none_literal_expr() {
374+
elements.push(expr.clone());
375+
}
376+
}
377+
378+
let mut elements = Vec::new();
379+
inner(expr, &mut elements);
380+
elements
381+
}

crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP045.py.snap

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,4 +311,93 @@ help: Convert to `X | None`
311311
- # text
312312
- ] = None
313313
81 + foo: int | None = None
314+
82 |
315+
83 |
316+
84 | # Regression test for: https://github.com/astral-sh/ruff/issues/23429
314317
note: This is an unsafe fix and may change runtime behavior
318+
319+
UP045 [*] Use `X | None` for type annotations
320+
--> UP045.py:89:13
321+
|
322+
87 | # Regression test for: https://github.com/astral-sh/ruff/issues/23429
323+
88 | # Optional[None | X] should not produce None | None
324+
89 | bar: None | Optional[None | int] = None
325+
| ^^^^^^^^^^^^^^^^^^^^
326+
90 | bar: Optional[None | int] = None
327+
91 | bar: Optional[int | None] = None
328+
|
329+
help: Convert to `X | None`
330+
86 |
331+
87 | # Regression test for: https://github.com/astral-sh/ruff/issues/23429
332+
88 | # Optional[None | X] should not produce None | None
333+
- bar: None | Optional[None | int] = None
334+
89 + bar: None | int | None = None
335+
90 | bar: Optional[None | int] = None
336+
91 | bar: Optional[int | None] = None
337+
92 | bar: Optional[None | int | str] = None
338+
339+
UP045 [*] Use `X | None` for type annotations
340+
--> UP045.py:90:6
341+
|
342+
88 | # Optional[None | X] should not produce None | None
343+
89 | bar: None | Optional[None | int] = None
344+
90 | bar: Optional[None | int] = None
345+
| ^^^^^^^^^^^^^^^^^^^^
346+
91 | bar: Optional[int | None] = None
347+
92 | bar: Optional[None | int | str] = None
348+
|
349+
help: Convert to `X | None`
350+
87 | # Regression test for: https://github.com/astral-sh/ruff/issues/23429
351+
88 | # Optional[None | X] should not produce None | None
352+
89 | bar: None | Optional[None | int] = None
353+
- bar: Optional[None | int] = None
354+
90 + bar: int | None = None
355+
91 | bar: Optional[int | None] = None
356+
92 | bar: Optional[None | int | str] = None
357+
93 | bar: Optional[None | None] = None
358+
359+
UP045 [*] Use `X | None` for type annotations
360+
--> UP045.py:91:6
361+
|
362+
89 | bar: None | Optional[None | int] = None
363+
90 | bar: Optional[None | int] = None
364+
91 | bar: Optional[int | None] = None
365+
| ^^^^^^^^^^^^^^^^^^^^
366+
92 | bar: Optional[None | int | str] = None
367+
93 | bar: Optional[None | None] = None
368+
|
369+
help: Convert to `X | None`
370+
88 | # Optional[None | X] should not produce None | None
371+
89 | bar: None | Optional[None | int] = None
372+
90 | bar: Optional[None | int] = None
373+
- bar: Optional[int | None] = None
374+
91 + bar: int | None = None
375+
92 | bar: Optional[None | int | str] = None
376+
93 | bar: Optional[None | None] = None
377+
378+
UP045 [*] Use `X | None` for type annotations
379+
--> UP045.py:92:6
380+
|
381+
90 | bar: Optional[None | int] = None
382+
91 | bar: Optional[int | None] = None
383+
92 | bar: Optional[None | int | str] = None
384+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
385+
93 | bar: Optional[None | None] = None
386+
|
387+
help: Convert to `X | None`
388+
89 | bar: None | Optional[None | int] = None
389+
90 | bar: Optional[None | int] = None
390+
91 | bar: Optional[int | None] = None
391+
- bar: Optional[None | int | str] = None
392+
92 + bar: int | str | None = None
393+
93 | bar: Optional[None | None] = None
394+
395+
UP045 Use `X | None` for type annotations
396+
--> UP045.py:93:6
397+
|
398+
91 | bar: Optional[int | None] = None
399+
92 | bar: Optional[None | int | str] = None
400+
93 | bar: Optional[None | None] = None
401+
| ^^^^^^^^^^^^^^^^^^^^^
402+
|
403+
help: Convert to `X | None`

0 commit comments

Comments
 (0)