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:
Zanie
2023-07-10 12:11:54 -05:00
committed by GitHub
parent 8dc06d1035
commit d19839fe0f
7 changed files with 575 additions and 287 deletions

View File

@@ -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

View File

@@ -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);
}
}
}

View 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);
}

View File

@@ -1,4 +1,5 @@
//! Rules from [flake8-pyi](https://pypi.org/project/flake8-pyi/).
mod helpers;
pub(crate) mod rules;
#[cfg(test)]

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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`