Add support for Union declarations without | to PYI016 (#5598)
Previously, PYI016 only supported reporting violations for unions defined with `|`. Now, union declarations with `typing.Union` are supported.
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import typing
|
||||
|
||||
# Shouldn't affect non-union field types.
|
||||
field1: str
|
||||
|
||||
@@ -30,3 +32,42 @@ field10: (str | int) | str # PYI016: Duplicate union member `str`
|
||||
|
||||
# Should emit for nested unions.
|
||||
field11: dict[int | int, str]
|
||||
|
||||
# Should emit for unions with more than two cases
|
||||
field12: int | int | int # Error
|
||||
field13: int | int | int | int # Error
|
||||
|
||||
# Should emit for unions with more than two cases, even if not directly adjacent
|
||||
field14: int | int | str | int # Error
|
||||
|
||||
# Should emit for duplicate literal types; also covered by PYI030
|
||||
field15: typing.Literal[1] | typing.Literal[1] # Error
|
||||
|
||||
# Shouldn't emit if in new parent type
|
||||
field16: int | dict[int, str] # OK
|
||||
|
||||
# Shouldn't emit if not in a union parent
|
||||
field17: dict[int, int] # OK
|
||||
|
||||
# Should emit in cases with newlines
|
||||
field18: typing.Union[
|
||||
set[
|
||||
int # foo
|
||||
],
|
||||
set[
|
||||
int # bar
|
||||
],
|
||||
] # Error, newline and comment will not be emitted in message
|
||||
|
||||
|
||||
# Should emit in cases with `typing.Union` instead of `|`
|
||||
field19: typing.Union[int, int] # Error
|
||||
|
||||
# Should emit in cases with nested `typing.Union`
|
||||
field20: typing.Union[int, typing.Union[int, str]] # Error
|
||||
|
||||
# Should emit in cases with mixed `typing.Union` and `|`
|
||||
field21: typing.Union[int, int | str] # Error
|
||||
|
||||
# Should emit only once in cases with multiple nested `typing.Union`
|
||||
field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error
|
||||
|
||||
@@ -2196,18 +2196,24 @@ where
|
||||
}
|
||||
|
||||
// Ex) Union[...]
|
||||
if self.enabled(Rule::UnnecessaryLiteralUnion) {
|
||||
let mut check = true;
|
||||
if self.any_enabled(&[Rule::UnnecessaryLiteralUnion, Rule::DuplicateUnionMember]) {
|
||||
// Determine if the current expression is an union
|
||||
// Avoid duplicate checks if the parent is an `Union[...]` since these rules traverse nested unions
|
||||
let is_unchecked_union = self
|
||||
.semantic
|
||||
.expr_grandparent()
|
||||
.and_then(Expr::as_subscript_expr)
|
||||
.map_or(true, |parent| {
|
||||
!self.semantic.match_typing_expr(&parent.value, "Union")
|
||||
});
|
||||
|
||||
// Avoid duplicate checks if the parent is an `Union[...]`
|
||||
if let Some(Expr::Subscript(ast::ExprSubscript { value, .. })) =
|
||||
self.semantic.expr_grandparent()
|
||||
{
|
||||
check = !self.semantic.match_typing_expr(value, "Union");
|
||||
}
|
||||
|
||||
if check {
|
||||
flake8_pyi::rules::unnecessary_literal_union(self, expr);
|
||||
if is_unchecked_union {
|
||||
if self.enabled(Rule::UnnecessaryLiteralUnion) {
|
||||
flake8_pyi::rules::unnecessary_literal_union(self, expr);
|
||||
}
|
||||
if self.enabled(Rule::DuplicateUnionMember) {
|
||||
flake8_pyi::rules::duplicate_union_member(self, expr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
54
crates/ruff/src/rules/flake8_pyi/helpers.rs
Normal file
54
crates/ruff/src/rules/flake8_pyi/helpers.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use rustpython_parser::ast::{self, Expr, Operator};
|
||||
|
||||
/// Traverse a "union" type annotation, applying `func` to each union member.
|
||||
/// Supports traversal of `Union` and `|` union expressions.
|
||||
/// The function is called with each expression in the union (excluding declarations of nested unions)
|
||||
/// and the parent expression (if any).
|
||||
pub(super) fn traverse_union<'a, F>(
|
||||
func: &mut F,
|
||||
semantic: &SemanticModel,
|
||||
expr: &'a Expr,
|
||||
parent: Option<&'a Expr>,
|
||||
) where
|
||||
F: FnMut(&'a Expr, Option<&'a Expr>),
|
||||
{
|
||||
// Ex) x | y
|
||||
if let Expr::BinOp(ast::ExprBinOp {
|
||||
op: Operator::BitOr,
|
||||
left,
|
||||
right,
|
||||
range: _,
|
||||
}) = expr
|
||||
{
|
||||
// The union data structure usually looks like this:
|
||||
// a | b | c -> (a | b) | c
|
||||
//
|
||||
// However, parenthesized expressions can coerce it into any structure:
|
||||
// a | (b | c)
|
||||
//
|
||||
// So we have to traverse both branches in order (left, then right), to report members
|
||||
// in the order they appear in the source code.
|
||||
|
||||
// Traverse the left then right arms
|
||||
traverse_union(func, semantic, left, Some(expr));
|
||||
traverse_union(func, semantic, right, Some(expr));
|
||||
return;
|
||||
}
|
||||
|
||||
// Ex) Union[x, y]
|
||||
if let Expr::Subscript(ast::ExprSubscript { value, slice, .. }) = expr {
|
||||
if semantic.match_typing_expr(value, "Union") {
|
||||
if let Expr::Tuple(ast::ExprTuple { elts, .. }) = slice.as_ref() {
|
||||
// Traverse each element of the tuple within the union recursively to handle cases
|
||||
// such as `Union[..., Union[...]]
|
||||
elts.iter()
|
||||
.for_each(|elt| traverse_union(func, semantic, elt, Some(expr)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, call the function on expression
|
||||
func(expr, parent);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
//! Rules from [flake8-pyi](https://pypi.org/project/flake8-pyi/).
|
||||
mod helpers;
|
||||
pub(crate) mod rules;
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,89 +1,77 @@
|
||||
use rustc_hash::FxHashSet;
|
||||
use rustpython_parser::ast::{self, Expr, Operator, Ranged};
|
||||
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::comparable::ComparableExpr;
|
||||
use rustpython_parser::ast::{self, Expr, Ranged};
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::AsRule;
|
||||
use crate::rules::flake8_pyi::helpers::traverse_union;
|
||||
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::comparable::ComparableExpr;
|
||||
|
||||
#[violation]
|
||||
pub struct DuplicateUnionMember {
|
||||
duplicate_name: String,
|
||||
}
|
||||
|
||||
impl AlwaysAutofixableViolation for DuplicateUnionMember {
|
||||
impl Violation for DuplicateUnionMember {
|
||||
const AUTOFIX: AutofixKind = AutofixKind::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Duplicate union member `{}`", self.duplicate_name)
|
||||
}
|
||||
|
||||
fn autofix_title(&self) -> String {
|
||||
format!("Remove duplicate union member `{}`", self.duplicate_name)
|
||||
fn autofix_title(&self) -> Option<String> {
|
||||
Some(format!(
|
||||
"Remove duplicate union member `{}`",
|
||||
self.duplicate_name
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// PYI016
|
||||
pub(crate) fn duplicate_union_member(checker: &mut Checker, expr: &Expr) {
|
||||
let mut seen_nodes = FxHashSet::default();
|
||||
traverse_union(&mut seen_nodes, checker, expr, None);
|
||||
}
|
||||
pub(crate) fn duplicate_union_member<'a>(checker: &mut Checker, expr: &'a Expr) {
|
||||
let mut seen_nodes: HashSet<ComparableExpr<'_>, _> = FxHashSet::default();
|
||||
let mut diagnostics: Vec<Diagnostic> = Vec::new();
|
||||
|
||||
fn traverse_union<'a>(
|
||||
seen_nodes: &mut FxHashSet<ComparableExpr<'a>>,
|
||||
checker: &mut Checker,
|
||||
expr: &'a Expr,
|
||||
parent: Option<&'a Expr>,
|
||||
) {
|
||||
// The union data structure usually looks like this:
|
||||
// a | b | c -> (a | b) | c
|
||||
//
|
||||
// However, parenthesized expressions can coerce it into any structure:
|
||||
// a | (b | c)
|
||||
//
|
||||
// So we have to traverse both branches in order (left, then right), to report duplicates
|
||||
// in the order they appear in the source code.
|
||||
if let Expr::BinOp(ast::ExprBinOp {
|
||||
op: Operator::BitOr,
|
||||
left,
|
||||
right,
|
||||
range: _,
|
||||
}) = expr
|
||||
{
|
||||
// Traverse left subtree, then the right subtree, propagating the previous node.
|
||||
traverse_union(seen_nodes, checker, left, Some(expr));
|
||||
traverse_union(seen_nodes, checker, right, Some(expr));
|
||||
}
|
||||
// Adds a member to `literal_exprs` if it is a `Literal` annotation
|
||||
let mut check_for_duplicate_members = |expr: &'a Expr, parent: Option<&'a Expr>| {
|
||||
// If we've already seen this union member, raise a violation.
|
||||
if !seen_nodes.insert(expr.into()) {
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
DuplicateUnionMember {
|
||||
duplicate_name: checker.generator().expr(expr),
|
||||
},
|
||||
expr.range(),
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
// Delete the "|" character as well as the duplicate value by reconstructing the
|
||||
// parent without the duplicate.
|
||||
|
||||
// If we've already seen this union member, raise a violation.
|
||||
if !seen_nodes.insert(expr.into()) {
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
DuplicateUnionMember {
|
||||
duplicate_name: checker.generator().expr(expr),
|
||||
},
|
||||
expr.range(),
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
// Delete the "|" character as well as the duplicate value by reconstructing the
|
||||
// parent without the duplicate.
|
||||
|
||||
// SAFETY: impossible to have a duplicate without a `parent` node.
|
||||
let parent = parent.expect("Parent node must exist");
|
||||
|
||||
// SAFETY: Parent node must have been a `BinOp` in order for us to have traversed it.
|
||||
let Expr::BinOp(ast::ExprBinOp { left, right, .. }) = parent else {
|
||||
panic!("Parent node must be a BinOp");
|
||||
};
|
||||
|
||||
// Replace the parent with its non-duplicate child.
|
||||
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
|
||||
checker
|
||||
.generator()
|
||||
.expr(if expr == left.as_ref() { right } else { left }),
|
||||
parent.range(),
|
||||
)));
|
||||
// If the parent node is not a `BinOp` we will not perform a fix
|
||||
if let Some(Expr::BinOp(ast::ExprBinOp { left, right, .. })) = parent {
|
||||
// Replace the parent with its non-duplicate child.
|
||||
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
|
||||
checker
|
||||
.generator()
|
||||
.expr(if expr == left.as_ref() { right } else { left }),
|
||||
parent.unwrap().range(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
};
|
||||
|
||||
// Traverse the union, collect all diagnostic members
|
||||
traverse_union(
|
||||
&mut check_for_duplicate_members,
|
||||
checker.semantic(),
|
||||
expr,
|
||||
None,
|
||||
);
|
||||
|
||||
// Add all diagnostics to the checker
|
||||
checker.diagnostics.append(&mut diagnostics);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use rustpython_parser::ast::{self, Expr, Operator, Ranged};
|
||||
use rustpython_parser::ast::{self, Expr, Ranged};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::rules::flake8_pyi::helpers::traverse_union;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for the presence of multiple literal types in a union.
|
||||
@@ -47,7 +47,7 @@ pub(crate) fn unnecessary_literal_union<'a>(checker: &mut Checker, expr: &'a Exp
|
||||
let mut literal_exprs = SmallVec::<[&Box<Expr>; 1]>::new();
|
||||
|
||||
// Adds a member to `literal_exprs` if it is a `Literal` annotation
|
||||
let mut collect_literal_expr = |expr: &'a Expr| {
|
||||
let mut collect_literal_expr = |expr: &'a Expr, _| {
|
||||
if let Expr::Subscript(ast::ExprSubscript { value, slice, .. }) = expr {
|
||||
if checker.semantic().match_typing_expr(value, "Literal") {
|
||||
literal_exprs.push(slice);
|
||||
@@ -56,7 +56,7 @@ pub(crate) fn unnecessary_literal_union<'a>(checker: &mut Checker, expr: &'a Exp
|
||||
};
|
||||
|
||||
// Traverse the union, collect all literal members
|
||||
traverse_union(&mut collect_literal_expr, expr, checker.semantic());
|
||||
traverse_union(&mut collect_literal_expr, checker.semantic(), expr, None);
|
||||
|
||||
// Raise a violation if more than one
|
||||
if literal_exprs.len() > 1 {
|
||||
@@ -73,48 +73,3 @@ pub(crate) fn unnecessary_literal_union<'a>(checker: &mut Checker, expr: &'a Exp
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
/// Traverse a "union" type annotation, calling `func` on each expression in the union.
|
||||
fn traverse_union<'a, F>(func: &mut F, expr: &'a Expr, semantic: &SemanticModel)
|
||||
where
|
||||
F: FnMut(&'a Expr),
|
||||
{
|
||||
// Ex) x | y
|
||||
if let Expr::BinOp(ast::ExprBinOp {
|
||||
op: Operator::BitOr,
|
||||
left,
|
||||
right,
|
||||
range: _,
|
||||
}) = expr
|
||||
{
|
||||
// The union data structure usually looks like this:
|
||||
// a | b | c -> (a | b) | c
|
||||
//
|
||||
// However, parenthesized expressions can coerce it into any structure:
|
||||
// a | (b | c)
|
||||
//
|
||||
// So we have to traverse both branches in order (left, then right), to report members
|
||||
// in the order they appear in the source code.
|
||||
|
||||
// Traverse the left then right arms
|
||||
traverse_union(func, left, semantic);
|
||||
traverse_union(func, right, semantic);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ex) Union[x, y]
|
||||
if let Expr::Subscript(ast::ExprSubscript { value, slice, .. }) = expr {
|
||||
if semantic.match_typing_expr(value, "Union") {
|
||||
if let Expr::Tuple(ast::ExprTuple { elts, .. }) = slice.as_ref() {
|
||||
// Traverse each element of the tuple within the union recursively to handle cases
|
||||
// such as `Union[..., Union[...]]
|
||||
elts.iter()
|
||||
.for_each(|elt| traverse_union(func, elt, semantic));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, call the function on expression
|
||||
func(expr);
|
||||
}
|
||||
|
||||
@@ -1,219 +1,462 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
PYI016.pyi:5:15: PYI016 [*] Duplicate union member `str`
|
||||
PYI016.pyi:7:15: PYI016 [*] Duplicate union member `str`
|
||||
|
|
||||
4 | # Should emit for duplicate field types.
|
||||
5 | field2: str | str # PYI016: Duplicate union member `str`
|
||||
6 | # Should emit for duplicate field types.
|
||||
7 | field2: str | str # PYI016: Duplicate union member `str`
|
||||
| ^^^ PYI016
|
||||
6 |
|
||||
7 | # Should emit for union types in arguments.
|
||||
8 |
|
||||
9 | # Should emit for union types in arguments.
|
||||
|
|
||||
= help: Remove duplicate union member `str`
|
||||
|
||||
ℹ Fix
|
||||
2 2 | field1: str
|
||||
3 3 |
|
||||
4 4 | # Should emit for duplicate field types.
|
||||
5 |-field2: str | str # PYI016: Duplicate union member `str`
|
||||
5 |+field2: str # PYI016: Duplicate union member `str`
|
||||
6 6 |
|
||||
7 7 | # Should emit for union types in arguments.
|
||||
8 8 | def func1(arg1: int | int): # PYI016: Duplicate union member `int`
|
||||
4 4 | field1: str
|
||||
5 5 |
|
||||
6 6 | # Should emit for duplicate field types.
|
||||
7 |-field2: str | str # PYI016: Duplicate union member `str`
|
||||
7 |+field2: str # PYI016: Duplicate union member `str`
|
||||
8 8 |
|
||||
9 9 | # Should emit for union types in arguments.
|
||||
10 10 | def func1(arg1: int | int): # PYI016: Duplicate union member `int`
|
||||
|
||||
PYI016.pyi:8:23: PYI016 [*] Duplicate union member `int`
|
||||
|
|
||||
7 | # Should emit for union types in arguments.
|
||||
8 | def func1(arg1: int | int): # PYI016: Duplicate union member `int`
|
||||
| ^^^ PYI016
|
||||
9 | print(arg1)
|
||||
|
|
||||
= help: Remove duplicate union member `int`
|
||||
|
||||
ℹ Fix
|
||||
5 5 | field2: str | str # PYI016: Duplicate union member `str`
|
||||
6 6 |
|
||||
7 7 | # Should emit for union types in arguments.
|
||||
8 |-def func1(arg1: int | int): # PYI016: Duplicate union member `int`
|
||||
8 |+def func1(arg1: int): # PYI016: Duplicate union member `int`
|
||||
9 9 | print(arg1)
|
||||
10 10 |
|
||||
11 11 | # Should emit for unions in return types.
|
||||
|
||||
PYI016.pyi:12:22: PYI016 [*] Duplicate union member `str`
|
||||
PYI016.pyi:10:23: PYI016 [*] Duplicate union member `int`
|
||||
|
|
||||
11 | # Should emit for unions in return types.
|
||||
12 | def func2() -> str | str: # PYI016: Duplicate union member `str`
|
||||
| ^^^ PYI016
|
||||
13 | return "my string"
|
||||
|
|
||||
= help: Remove duplicate union member `str`
|
||||
|
||||
ℹ Fix
|
||||
9 9 | print(arg1)
|
||||
10 10 |
|
||||
11 11 | # Should emit for unions in return types.
|
||||
12 |-def func2() -> str | str: # PYI016: Duplicate union member `str`
|
||||
12 |+def func2() -> str: # PYI016: Duplicate union member `str`
|
||||
13 13 | return "my string"
|
||||
14 14 |
|
||||
15 15 | # Should emit in longer unions, even if not directly adjacent.
|
||||
|
||||
PYI016.pyi:16:15: PYI016 [*] Duplicate union member `str`
|
||||
|
|
||||
15 | # Should emit in longer unions, even if not directly adjacent.
|
||||
16 | field3: str | str | int # PYI016: Duplicate union member `str`
|
||||
| ^^^ PYI016
|
||||
17 | field4: int | int | str # PYI016: Duplicate union member `int`
|
||||
18 | field5: str | int | str # PYI016: Duplicate union member `str`
|
||||
|
|
||||
= help: Remove duplicate union member `str`
|
||||
|
||||
ℹ Fix
|
||||
13 13 | return "my string"
|
||||
14 14 |
|
||||
15 15 | # Should emit in longer unions, even if not directly adjacent.
|
||||
16 |-field3: str | str | int # PYI016: Duplicate union member `str`
|
||||
16 |+field3: str | int # PYI016: Duplicate union member `str`
|
||||
17 17 | field4: int | int | str # PYI016: Duplicate union member `int`
|
||||
18 18 | field5: str | int | str # PYI016: Duplicate union member `str`
|
||||
19 19 | field6: int | bool | str | int # PYI016: Duplicate union member `int`
|
||||
|
||||
PYI016.pyi:17:15: PYI016 [*] Duplicate union member `int`
|
||||
|
|
||||
15 | # Should emit in longer unions, even if not directly adjacent.
|
||||
16 | field3: str | str | int # PYI016: Duplicate union member `str`
|
||||
17 | field4: int | int | str # PYI016: Duplicate union member `int`
|
||||
| ^^^ PYI016
|
||||
18 | field5: str | int | str # PYI016: Duplicate union member `str`
|
||||
19 | field6: int | bool | str | int # PYI016: Duplicate union member `int`
|
||||
9 | # Should emit for union types in arguments.
|
||||
10 | def func1(arg1: int | int): # PYI016: Duplicate union member `int`
|
||||
| ^^^ PYI016
|
||||
11 | print(arg1)
|
||||
|
|
||||
= help: Remove duplicate union member `int`
|
||||
|
||||
ℹ Fix
|
||||
14 14 |
|
||||
15 15 | # Should emit in longer unions, even if not directly adjacent.
|
||||
16 16 | field3: str | str | int # PYI016: Duplicate union member `str`
|
||||
17 |-field4: int | int | str # PYI016: Duplicate union member `int`
|
||||
17 |+field4: int | str # PYI016: Duplicate union member `int`
|
||||
18 18 | field5: str | int | str # PYI016: Duplicate union member `str`
|
||||
19 19 | field6: int | bool | str | int # PYI016: Duplicate union member `int`
|
||||
20 20 |
|
||||
7 7 | field2: str | str # PYI016: Duplicate union member `str`
|
||||
8 8 |
|
||||
9 9 | # Should emit for union types in arguments.
|
||||
10 |-def func1(arg1: int | int): # PYI016: Duplicate union member `int`
|
||||
10 |+def func1(arg1: int): # PYI016: Duplicate union member `int`
|
||||
11 11 | print(arg1)
|
||||
12 12 |
|
||||
13 13 | # Should emit for unions in return types.
|
||||
|
||||
PYI016.pyi:18:21: PYI016 [*] Duplicate union member `str`
|
||||
PYI016.pyi:14:22: PYI016 [*] Duplicate union member `str`
|
||||
|
|
||||
16 | field3: str | str | int # PYI016: Duplicate union member `str`
|
||||
17 | field4: int | int | str # PYI016: Duplicate union member `int`
|
||||
18 | field5: str | int | str # PYI016: Duplicate union member `str`
|
||||
| ^^^ PYI016
|
||||
19 | field6: int | bool | str | int # PYI016: Duplicate union member `int`
|
||||
13 | # Should emit for unions in return types.
|
||||
14 | def func2() -> str | str: # PYI016: Duplicate union member `str`
|
||||
| ^^^ PYI016
|
||||
15 | return "my string"
|
||||
|
|
||||
= help: Remove duplicate union member `str`
|
||||
|
||||
ℹ Fix
|
||||
15 15 | # Should emit in longer unions, even if not directly adjacent.
|
||||
16 16 | field3: str | str | int # PYI016: Duplicate union member `str`
|
||||
17 17 | field4: int | int | str # PYI016: Duplicate union member `int`
|
||||
18 |-field5: str | int | str # PYI016: Duplicate union member `str`
|
||||
18 |+field5: str | int # PYI016: Duplicate union member `str`
|
||||
19 19 | field6: int | bool | str | int # PYI016: Duplicate union member `int`
|
||||
20 20 |
|
||||
21 21 | # Shouldn't emit for non-type unions.
|
||||
11 11 | print(arg1)
|
||||
12 12 |
|
||||
13 13 | # Should emit for unions in return types.
|
||||
14 |-def func2() -> str | str: # PYI016: Duplicate union member `str`
|
||||
14 |+def func2() -> str: # PYI016: Duplicate union member `str`
|
||||
15 15 | return "my string"
|
||||
16 16 |
|
||||
17 17 | # Should emit in longer unions, even if not directly adjacent.
|
||||
|
||||
PYI016.pyi:19:28: PYI016 [*] Duplicate union member `int`
|
||||
PYI016.pyi:18:15: PYI016 [*] Duplicate union member `str`
|
||||
|
|
||||
17 | field4: int | int | str # PYI016: Duplicate union member `int`
|
||||
18 | field5: str | int | str # PYI016: Duplicate union member `str`
|
||||
19 | field6: int | bool | str | int # PYI016: Duplicate union member `int`
|
||||
17 | # Should emit in longer unions, even if not directly adjacent.
|
||||
18 | field3: str | str | int # PYI016: Duplicate union member `str`
|
||||
| ^^^ PYI016
|
||||
19 | field4: int | int | str # PYI016: Duplicate union member `int`
|
||||
20 | field5: str | int | str # PYI016: Duplicate union member `str`
|
||||
|
|
||||
= help: Remove duplicate union member `str`
|
||||
|
||||
ℹ Fix
|
||||
15 15 | return "my string"
|
||||
16 16 |
|
||||
17 17 | # Should emit in longer unions, even if not directly adjacent.
|
||||
18 |-field3: str | str | int # PYI016: Duplicate union member `str`
|
||||
18 |+field3: str | int # PYI016: Duplicate union member `str`
|
||||
19 19 | field4: int | int | str # PYI016: Duplicate union member `int`
|
||||
20 20 | field5: str | int | str # PYI016: Duplicate union member `str`
|
||||
21 21 | field6: int | bool | str | int # PYI016: Duplicate union member `int`
|
||||
|
||||
PYI016.pyi:19:15: PYI016 [*] Duplicate union member `int`
|
||||
|
|
||||
17 | # Should emit in longer unions, even if not directly adjacent.
|
||||
18 | field3: str | str | int # PYI016: Duplicate union member `str`
|
||||
19 | field4: int | int | str # PYI016: Duplicate union member `int`
|
||||
| ^^^ PYI016
|
||||
20 | field5: str | int | str # PYI016: Duplicate union member `str`
|
||||
21 | field6: int | bool | str | int # PYI016: Duplicate union member `int`
|
||||
|
|
||||
= help: Remove duplicate union member `int`
|
||||
|
||||
ℹ Fix
|
||||
16 16 |
|
||||
17 17 | # Should emit in longer unions, even if not directly adjacent.
|
||||
18 18 | field3: str | str | int # PYI016: Duplicate union member `str`
|
||||
19 |-field4: int | int | str # PYI016: Duplicate union member `int`
|
||||
19 |+field4: int | str # PYI016: Duplicate union member `int`
|
||||
20 20 | field5: str | int | str # PYI016: Duplicate union member `str`
|
||||
21 21 | field6: int | bool | str | int # PYI016: Duplicate union member `int`
|
||||
22 22 |
|
||||
|
||||
PYI016.pyi:20:21: PYI016 [*] Duplicate union member `str`
|
||||
|
|
||||
18 | field3: str | str | int # PYI016: Duplicate union member `str`
|
||||
19 | field4: int | int | str # PYI016: Duplicate union member `int`
|
||||
20 | field5: str | int | str # PYI016: Duplicate union member `str`
|
||||
| ^^^ PYI016
|
||||
21 | field6: int | bool | str | int # PYI016: Duplicate union member `int`
|
||||
|
|
||||
= help: Remove duplicate union member `str`
|
||||
|
||||
ℹ Fix
|
||||
17 17 | # Should emit in longer unions, even if not directly adjacent.
|
||||
18 18 | field3: str | str | int # PYI016: Duplicate union member `str`
|
||||
19 19 | field4: int | int | str # PYI016: Duplicate union member `int`
|
||||
20 |-field5: str | int | str # PYI016: Duplicate union member `str`
|
||||
20 |+field5: str | int # PYI016: Duplicate union member `str`
|
||||
21 21 | field6: int | bool | str | int # PYI016: Duplicate union member `int`
|
||||
22 22 |
|
||||
23 23 | # Shouldn't emit for non-type unions.
|
||||
|
||||
PYI016.pyi:21:28: PYI016 [*] Duplicate union member `int`
|
||||
|
|
||||
19 | field4: int | int | str # PYI016: Duplicate union member `int`
|
||||
20 | field5: str | int | str # PYI016: Duplicate union member `str`
|
||||
21 | field6: int | bool | str | int # PYI016: Duplicate union member `int`
|
||||
| ^^^ PYI016
|
||||
20 |
|
||||
21 | # Shouldn't emit for non-type unions.
|
||||
22 |
|
||||
23 | # Shouldn't emit for non-type unions.
|
||||
|
|
||||
= help: Remove duplicate union member `int`
|
||||
|
||||
ℹ Fix
|
||||
16 16 | field3: str | str | int # PYI016: Duplicate union member `str`
|
||||
17 17 | field4: int | int | str # PYI016: Duplicate union member `int`
|
||||
18 18 | field5: str | int | str # PYI016: Duplicate union member `str`
|
||||
19 |-field6: int | bool | str | int # PYI016: Duplicate union member `int`
|
||||
19 |+field6: int | bool | str # PYI016: Duplicate union member `int`
|
||||
20 20 |
|
||||
21 21 | # Shouldn't emit for non-type unions.
|
||||
22 22 | field7 = str | str
|
||||
18 18 | field3: str | str | int # PYI016: Duplicate union member `str`
|
||||
19 19 | field4: int | int | str # PYI016: Duplicate union member `int`
|
||||
20 20 | field5: str | int | str # PYI016: Duplicate union member `str`
|
||||
21 |-field6: int | bool | str | int # PYI016: Duplicate union member `int`
|
||||
21 |+field6: int | bool | str # PYI016: Duplicate union member `int`
|
||||
22 22 |
|
||||
23 23 | # Shouldn't emit for non-type unions.
|
||||
24 24 | field7 = str | str
|
||||
|
||||
PYI016.pyi:25:22: PYI016 [*] Duplicate union member `int`
|
||||
PYI016.pyi:27:22: PYI016 [*] Duplicate union member `int`
|
||||
|
|
||||
24 | # Should emit for strangely-bracketed unions.
|
||||
25 | field8: int | (str | int) # PYI016: Duplicate union member `int`
|
||||
26 | # Should emit for strangely-bracketed unions.
|
||||
27 | field8: int | (str | int) # PYI016: Duplicate union member `int`
|
||||
| ^^^ PYI016
|
||||
26 |
|
||||
27 | # Should handle user brackets when fixing.
|
||||
28 |
|
||||
29 | # Should handle user brackets when fixing.
|
||||
|
|
||||
= help: Remove duplicate union member `int`
|
||||
|
||||
ℹ Fix
|
||||
22 22 | field7 = str | str
|
||||
23 23 |
|
||||
24 24 | # Should emit for strangely-bracketed unions.
|
||||
25 |-field8: int | (str | int) # PYI016: Duplicate union member `int`
|
||||
25 |+field8: int | (str) # PYI016: Duplicate union member `int`
|
||||
26 26 |
|
||||
27 27 | # Should handle user brackets when fixing.
|
||||
28 28 | field9: int | (int | str) # PYI016: Duplicate union member `int`
|
||||
24 24 | field7 = str | str
|
||||
25 25 |
|
||||
26 26 | # Should emit for strangely-bracketed unions.
|
||||
27 |-field8: int | (str | int) # PYI016: Duplicate union member `int`
|
||||
27 |+field8: int | (str) # PYI016: Duplicate union member `int`
|
||||
28 28 |
|
||||
29 29 | # Should handle user brackets when fixing.
|
||||
30 30 | field9: int | (int | str) # PYI016: Duplicate union member `int`
|
||||
|
||||
PYI016.pyi:28:16: PYI016 [*] Duplicate union member `int`
|
||||
PYI016.pyi:30:16: PYI016 [*] Duplicate union member `int`
|
||||
|
|
||||
27 | # Should handle user brackets when fixing.
|
||||
28 | field9: int | (int | str) # PYI016: Duplicate union member `int`
|
||||
29 | # Should handle user brackets when fixing.
|
||||
30 | field9: int | (int | str) # PYI016: Duplicate union member `int`
|
||||
| ^^^ PYI016
|
||||
29 | field10: (str | int) | str # PYI016: Duplicate union member `str`
|
||||
31 | field10: (str | int) | str # PYI016: Duplicate union member `str`
|
||||
|
|
||||
= help: Remove duplicate union member `int`
|
||||
|
||||
ℹ Fix
|
||||
25 25 | field8: int | (str | int) # PYI016: Duplicate union member `int`
|
||||
26 26 |
|
||||
27 27 | # Should handle user brackets when fixing.
|
||||
28 |-field9: int | (int | str) # PYI016: Duplicate union member `int`
|
||||
28 |+field9: int | (str) # PYI016: Duplicate union member `int`
|
||||
29 29 | field10: (str | int) | str # PYI016: Duplicate union member `str`
|
||||
30 30 |
|
||||
31 31 | # Should emit for nested unions.
|
||||
27 27 | field8: int | (str | int) # PYI016: Duplicate union member `int`
|
||||
28 28 |
|
||||
29 29 | # Should handle user brackets when fixing.
|
||||
30 |-field9: int | (int | str) # PYI016: Duplicate union member `int`
|
||||
30 |+field9: int | (str) # PYI016: Duplicate union member `int`
|
||||
31 31 | field10: (str | int) | str # PYI016: Duplicate union member `str`
|
||||
32 32 |
|
||||
33 33 | # Should emit for nested unions.
|
||||
|
||||
PYI016.pyi:29:24: PYI016 [*] Duplicate union member `str`
|
||||
PYI016.pyi:31:24: PYI016 [*] Duplicate union member `str`
|
||||
|
|
||||
27 | # Should handle user brackets when fixing.
|
||||
28 | field9: int | (int | str) # PYI016: Duplicate union member `int`
|
||||
29 | field10: (str | int) | str # PYI016: Duplicate union member `str`
|
||||
29 | # Should handle user brackets when fixing.
|
||||
30 | field9: int | (int | str) # PYI016: Duplicate union member `int`
|
||||
31 | field10: (str | int) | str # PYI016: Duplicate union member `str`
|
||||
| ^^^ PYI016
|
||||
30 |
|
||||
31 | # Should emit for nested unions.
|
||||
32 |
|
||||
33 | # Should emit for nested unions.
|
||||
|
|
||||
= help: Remove duplicate union member `str`
|
||||
|
||||
ℹ Fix
|
||||
26 26 |
|
||||
27 27 | # Should handle user brackets when fixing.
|
||||
28 28 | field9: int | (int | str) # PYI016: Duplicate union member `int`
|
||||
29 |-field10: (str | int) | str # PYI016: Duplicate union member `str`
|
||||
29 |+field10: str | int # PYI016: Duplicate union member `str`
|
||||
30 30 |
|
||||
31 31 | # Should emit for nested unions.
|
||||
32 32 | field11: dict[int | int, str]
|
||||
28 28 |
|
||||
29 29 | # Should handle user brackets when fixing.
|
||||
30 30 | field9: int | (int | str) # PYI016: Duplicate union member `int`
|
||||
31 |-field10: (str | int) | str # PYI016: Duplicate union member `str`
|
||||
31 |+field10: str | int # PYI016: Duplicate union member `str`
|
||||
32 32 |
|
||||
33 33 | # Should emit for nested unions.
|
||||
34 34 | field11: dict[int | int, str]
|
||||
|
||||
PYI016.pyi:32:21: PYI016 [*] Duplicate union member `int`
|
||||
PYI016.pyi:34:21: PYI016 [*] Duplicate union member `int`
|
||||
|
|
||||
31 | # Should emit for nested unions.
|
||||
32 | field11: dict[int | int, str]
|
||||
33 | # Should emit for nested unions.
|
||||
34 | field11: dict[int | int, str]
|
||||
| ^^^ PYI016
|
||||
35 |
|
||||
36 | # Should emit for unions with more than two cases
|
||||
|
|
||||
= help: Remove duplicate union member `int`
|
||||
|
||||
ℹ Fix
|
||||
29 29 | field10: (str | int) | str # PYI016: Duplicate union member `str`
|
||||
30 30 |
|
||||
31 31 | # Should emit for nested unions.
|
||||
32 |-field11: dict[int | int, str]
|
||||
32 |+field11: dict[int, str]
|
||||
31 31 | field10: (str | int) | str # PYI016: Duplicate union member `str`
|
||||
32 32 |
|
||||
33 33 | # Should emit for nested unions.
|
||||
34 |-field11: dict[int | int, str]
|
||||
34 |+field11: dict[int, str]
|
||||
35 35 |
|
||||
36 36 | # Should emit for unions with more than two cases
|
||||
37 37 | field12: int | int | int # Error
|
||||
|
||||
PYI016.pyi:37:16: PYI016 [*] Duplicate union member `int`
|
||||
|
|
||||
36 | # Should emit for unions with more than two cases
|
||||
37 | field12: int | int | int # Error
|
||||
| ^^^ PYI016
|
||||
38 | field13: int | int | int | int # Error
|
||||
|
|
||||
= help: Remove duplicate union member `int`
|
||||
|
||||
ℹ Fix
|
||||
34 34 | field11: dict[int | int, str]
|
||||
35 35 |
|
||||
36 36 | # Should emit for unions with more than two cases
|
||||
37 |-field12: int | int | int # Error
|
||||
37 |+field12: int | int # Error
|
||||
38 38 | field13: int | int | int | int # Error
|
||||
39 39 |
|
||||
40 40 | # Should emit for unions with more than two cases, even if not directly adjacent
|
||||
|
||||
PYI016.pyi:37:22: PYI016 [*] Duplicate union member `int`
|
||||
|
|
||||
36 | # Should emit for unions with more than two cases
|
||||
37 | field12: int | int | int # Error
|
||||
| ^^^ PYI016
|
||||
38 | field13: int | int | int | int # Error
|
||||
|
|
||||
= help: Remove duplicate union member `int`
|
||||
|
||||
ℹ Fix
|
||||
34 34 | field11: dict[int | int, str]
|
||||
35 35 |
|
||||
36 36 | # Should emit for unions with more than two cases
|
||||
37 |-field12: int | int | int # Error
|
||||
37 |+field12: int | int # Error
|
||||
38 38 | field13: int | int | int | int # Error
|
||||
39 39 |
|
||||
40 40 | # Should emit for unions with more than two cases, even if not directly adjacent
|
||||
|
||||
PYI016.pyi:38:16: PYI016 [*] Duplicate union member `int`
|
||||
|
|
||||
36 | # Should emit for unions with more than two cases
|
||||
37 | field12: int | int | int # Error
|
||||
38 | field13: int | int | int | int # Error
|
||||
| ^^^ PYI016
|
||||
39 |
|
||||
40 | # Should emit for unions with more than two cases, even if not directly adjacent
|
||||
|
|
||||
= help: Remove duplicate union member `int`
|
||||
|
||||
ℹ Fix
|
||||
35 35 |
|
||||
36 36 | # Should emit for unions with more than two cases
|
||||
37 37 | field12: int | int | int # Error
|
||||
38 |-field13: int | int | int | int # Error
|
||||
38 |+field13: int | int | int # Error
|
||||
39 39 |
|
||||
40 40 | # Should emit for unions with more than two cases, even if not directly adjacent
|
||||
41 41 | field14: int | int | str | int # Error
|
||||
|
||||
PYI016.pyi:38:22: PYI016 [*] Duplicate union member `int`
|
||||
|
|
||||
36 | # Should emit for unions with more than two cases
|
||||
37 | field12: int | int | int # Error
|
||||
38 | field13: int | int | int | int # Error
|
||||
| ^^^ PYI016
|
||||
39 |
|
||||
40 | # Should emit for unions with more than two cases, even if not directly adjacent
|
||||
|
|
||||
= help: Remove duplicate union member `int`
|
||||
|
||||
ℹ Fix
|
||||
35 35 |
|
||||
36 36 | # Should emit for unions with more than two cases
|
||||
37 37 | field12: int | int | int # Error
|
||||
38 |-field13: int | int | int | int # Error
|
||||
38 |+field13: int | int | int # Error
|
||||
39 39 |
|
||||
40 40 | # Should emit for unions with more than two cases, even if not directly adjacent
|
||||
41 41 | field14: int | int | str | int # Error
|
||||
|
||||
PYI016.pyi:38:28: PYI016 [*] Duplicate union member `int`
|
||||
|
|
||||
36 | # Should emit for unions with more than two cases
|
||||
37 | field12: int | int | int # Error
|
||||
38 | field13: int | int | int | int # Error
|
||||
| ^^^ PYI016
|
||||
39 |
|
||||
40 | # Should emit for unions with more than two cases, even if not directly adjacent
|
||||
|
|
||||
= help: Remove duplicate union member `int`
|
||||
|
||||
ℹ Fix
|
||||
35 35 |
|
||||
36 36 | # Should emit for unions with more than two cases
|
||||
37 37 | field12: int | int | int # Error
|
||||
38 |-field13: int | int | int | int # Error
|
||||
38 |+field13: int | int | int # Error
|
||||
39 39 |
|
||||
40 40 | # Should emit for unions with more than two cases, even if not directly adjacent
|
||||
41 41 | field14: int | int | str | int # Error
|
||||
|
||||
PYI016.pyi:41:16: PYI016 [*] Duplicate union member `int`
|
||||
|
|
||||
40 | # Should emit for unions with more than two cases, even if not directly adjacent
|
||||
41 | field14: int | int | str | int # Error
|
||||
| ^^^ PYI016
|
||||
42 |
|
||||
43 | # Should emit for duplicate literal types; also covered by PYI030
|
||||
|
|
||||
= help: Remove duplicate union member `int`
|
||||
|
||||
ℹ Fix
|
||||
38 38 | field13: int | int | int | int # Error
|
||||
39 39 |
|
||||
40 40 | # Should emit for unions with more than two cases, even if not directly adjacent
|
||||
41 |-field14: int | int | str | int # Error
|
||||
41 |+field14: int | str | int # Error
|
||||
42 42 |
|
||||
43 43 | # Should emit for duplicate literal types; also covered by PYI030
|
||||
44 44 | field15: typing.Literal[1] | typing.Literal[1] # Error
|
||||
|
||||
PYI016.pyi:41:28: PYI016 [*] Duplicate union member `int`
|
||||
|
|
||||
40 | # Should emit for unions with more than two cases, even if not directly adjacent
|
||||
41 | field14: int | int | str | int # Error
|
||||
| ^^^ PYI016
|
||||
42 |
|
||||
43 | # Should emit for duplicate literal types; also covered by PYI030
|
||||
|
|
||||
= help: Remove duplicate union member `int`
|
||||
|
||||
ℹ Fix
|
||||
38 38 | field13: int | int | int | int # Error
|
||||
39 39 |
|
||||
40 40 | # Should emit for unions with more than two cases, even if not directly adjacent
|
||||
41 |-field14: int | int | str | int # Error
|
||||
41 |+field14: int | int | str # Error
|
||||
42 42 |
|
||||
43 43 | # Should emit for duplicate literal types; also covered by PYI030
|
||||
44 44 | field15: typing.Literal[1] | typing.Literal[1] # Error
|
||||
|
||||
PYI016.pyi:44:30: PYI016 [*] Duplicate union member `typing.Literal[1]`
|
||||
|
|
||||
43 | # Should emit for duplicate literal types; also covered by PYI030
|
||||
44 | field15: typing.Literal[1] | typing.Literal[1] # Error
|
||||
| ^^^^^^^^^^^^^^^^^ PYI016
|
||||
45 |
|
||||
46 | # Shouldn't emit if in new parent type
|
||||
|
|
||||
= help: Remove duplicate union member `typing.Literal[1]`
|
||||
|
||||
ℹ Fix
|
||||
41 41 | field14: int | int | str | int # Error
|
||||
42 42 |
|
||||
43 43 | # Should emit for duplicate literal types; also covered by PYI030
|
||||
44 |-field15: typing.Literal[1] | typing.Literal[1] # Error
|
||||
44 |+field15: typing.Literal[1] # Error
|
||||
45 45 |
|
||||
46 46 | # Shouldn't emit if in new parent type
|
||||
47 47 | field16: int | dict[int, str] # OK
|
||||
|
||||
PYI016.pyi:57:5: PYI016 Duplicate union member `set[int]`
|
||||
|
|
||||
55 | int # foo
|
||||
56 | ],
|
||||
57 | set[
|
||||
| _____^
|
||||
58 | | int # bar
|
||||
59 | | ],
|
||||
| |_____^ PYI016
|
||||
60 | ] # Error, newline and comment will not be emitted in message
|
||||
|
|
||||
= help: Remove duplicate union member `set[int]`
|
||||
|
||||
PYI016.pyi:64:28: PYI016 Duplicate union member `int`
|
||||
|
|
||||
63 | # Should emit in cases with `typing.Union` instead of `|`
|
||||
64 | field19: typing.Union[int, int] # Error
|
||||
| ^^^ PYI016
|
||||
65 |
|
||||
66 | # Should emit in cases with nested `typing.Union`
|
||||
|
|
||||
= help: Remove duplicate union member `int`
|
||||
|
||||
PYI016.pyi:67:41: PYI016 Duplicate union member `int`
|
||||
|
|
||||
66 | # Should emit in cases with nested `typing.Union`
|
||||
67 | field20: typing.Union[int, typing.Union[int, str]] # Error
|
||||
| ^^^ PYI016
|
||||
68 |
|
||||
69 | # Should emit in cases with mixed `typing.Union` and `|`
|
||||
|
|
||||
= help: Remove duplicate union member `int`
|
||||
|
||||
PYI016.pyi:70:28: PYI016 [*] Duplicate union member `int`
|
||||
|
|
||||
69 | # Should emit in cases with mixed `typing.Union` and `|`
|
||||
70 | field21: typing.Union[int, int | str] # Error
|
||||
| ^^^ PYI016
|
||||
71 |
|
||||
72 | # Should emit only once in cases with multiple nested `typing.Union`
|
||||
|
|
||||
= help: Remove duplicate union member `int`
|
||||
|
||||
ℹ Fix
|
||||
67 67 | field20: typing.Union[int, typing.Union[int, str]] # Error
|
||||
68 68 |
|
||||
69 69 | # Should emit in cases with mixed `typing.Union` and `|`
|
||||
70 |-field21: typing.Union[int, int | str] # Error
|
||||
70 |+field21: typing.Union[int, str] # Error
|
||||
71 71 |
|
||||
72 72 | # Should emit only once in cases with multiple nested `typing.Union`
|
||||
73 73 | field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error
|
||||
|
||||
PYI016.pyi:73:41: PYI016 Duplicate union member `int`
|
||||
|
|
||||
72 | # Should emit only once in cases with multiple nested `typing.Union`
|
||||
73 | field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error
|
||||
| ^^^ PYI016
|
||||
|
|
||||
= help: Remove duplicate union member `int`
|
||||
|
||||
PYI016.pyi:73:59: PYI016 Duplicate union member `int`
|
||||
|
|
||||
72 | # Should emit only once in cases with multiple nested `typing.Union`
|
||||
73 | field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error
|
||||
| ^^^ PYI016
|
||||
|
|
||||
= help: Remove duplicate union member `int`
|
||||
|
||||
PYI016.pyi:73:64: PYI016 Duplicate union member `int`
|
||||
|
|
||||
72 | # Should emit only once in cases with multiple nested `typing.Union`
|
||||
73 | field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error
|
||||
| ^^^ PYI016
|
||||
|
|
||||
= help: Remove duplicate union member `int`
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user