[flake8-pyi] Expand Optional[A] to A | None (PYI016) (#18572)
Some checks are pending
CI / cargo fuzz build (push) Blocked by required conditions
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 (linux, release) (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) Waiting to run
CI / cargo build (msrv) (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 / Fuzz for new ty panics (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 / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run

## Summary
Under preview 🧪 I've expanded rule `PYI016` to also flag type
union duplicates containing `None` and `Optional`.

## Test Plan
Examples/tests have been added. I've made sure that the existing
examples did not change unless preview is enabled.

## Relevant Issues
* https://github.com/astral-sh/ruff/issues/18508 (discussing
introducing/extending a rule to flag `Optional[None]`)
* https://github.com/astral-sh/ruff/issues/18546 (where I discussed this
addition with @AlexWaygood)

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
This commit is contained in:
Robsdedude 2025-06-27 15:43:11 +00:00 committed by GitHub
parent 96f3c8d1ab
commit 6802c4702f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 2747 additions and 25 deletions

View file

@ -428,12 +428,52 @@ pub fn is_sys_version_block(stmt: &ast::StmtIf, semantic: &SemanticModel) -> boo
pub fn traverse_union<'a, F>(func: &mut F, semantic: &SemanticModel, expr: &'a Expr)
where
F: FnMut(&'a Expr, &'a Expr),
{
traverse_union_options(func, semantic, expr, UnionTraversalOptions::default());
}
/// Traverse a "union" type annotation, applying `func` to each union member.
///
/// Supports traversal of `Union`, `|`, and `Optional` union expressions.
///
/// The function is called with each expression in the union (excluding declarations of nested
/// unions) and the parent expression.
pub fn traverse_union_and_optional<'a, F>(func: &mut F, semantic: &SemanticModel, expr: &'a Expr)
where
F: FnMut(&'a Expr, &'a Expr),
{
traverse_union_options(
func,
semantic,
expr,
UnionTraversalOptions {
traverse_optional: true,
},
);
}
#[derive(Debug, Clone, Copy, Default)]
/// Options for traversing union types.
///
/// See also [`traverse_union_options`].
struct UnionTraversalOptions {
traverse_optional: bool,
}
fn traverse_union_options<'a, F>(
func: &mut F,
semantic: &SemanticModel,
expr: &'a Expr,
options: UnionTraversalOptions,
) where
F: FnMut(&'a Expr, &'a Expr),
{
fn inner<'a, F>(
func: &mut F,
semantic: &SemanticModel,
expr: &'a Expr,
parent: Option<&'a Expr>,
options: UnionTraversalOptions,
) where
F: FnMut(&'a Expr, &'a Expr),
{
@ -456,25 +496,31 @@ where
// in the order they appear in the source code.
// Traverse the left then right arms
inner(func, semantic, left, Some(expr));
inner(func, semantic, right, Some(expr));
inner(func, semantic, left, Some(expr), options);
inner(func, semantic, right, Some(expr), options);
return;
}
// Ex) `Union[x, y]`
if let Expr::Subscript(ast::ExprSubscript { value, slice, .. }) = expr {
// Ex) `Union[x, y]`
if semantic.match_typing_expr(value, "Union") {
if let Expr::Tuple(tuple) = &**slice {
// Traverse each element of the tuple within the union recursively to handle cases
// such as `Union[..., Union[...]]`
tuple
.iter()
.for_each(|elem| inner(func, semantic, elem, Some(expr)));
.for_each(|elem| inner(func, semantic, elem, Some(expr), options));
return;
}
// Ex) `Union[Union[a, b]]` and `Union[a | b | c]`
inner(func, semantic, slice, Some(expr));
inner(func, semantic, slice, Some(expr), options);
return;
}
// Ex) `Optional[x]`
if options.traverse_optional && semantic.match_typing_expr(value, "Optional") {
inner(func, semantic, value, Some(expr), options);
inner(func, semantic, slice, Some(expr), options);
return;
}
}
@ -485,7 +531,7 @@ where
}
}
inner(func, semantic, expr, None);
inner(func, semantic, expr, None, options);
}
/// Traverse a "literal" type annotation, applying `func` to each literal member.

View file

@ -1604,7 +1604,7 @@ impl<'a> SemanticModel<'a> {
let mut parent_expressions = self.current_expressions().skip(1);
match parent_expressions.next() {
// The parent expression is of the inner union is a single `typing.Union`.
// The parent expression of the inner union is a single `typing.Union`.
// Ex) `Union[Union[a, b]]`
Some(Expr::Subscript(parent)) => self.match_typing_expr(&parent.value, "Union"),
// The parent expression is of the inner union is a tuple with two or more
@ -1624,6 +1624,18 @@ impl<'a> SemanticModel<'a> {
}
}
/// Return `true` if the model is directly inside an Optional (e.g., the inner `Union` in
/// `Optional[Union[int, str]]`).
pub fn inside_optional(&self) -> bool {
let mut parent_expressions = self.current_expressions().skip(1);
matches!(
parent_expressions.next(),
// The parent expression is a single `typing.Optional`.
// Ex) `Optional[EXPR]`
Some(Expr::Subscript(parent)) if self.match_typing_expr(&parent.value, "Optional")
)
}
/// Return `true` if the model is in a nested literal expression (e.g., the inner `Literal` in
/// `Literal[Literal[int, str], float]`).
pub fn in_nested_literal(&self) -> bool {