1
1
use ast:: ExprContext ;
2
- use ruff_diagnostics:: { Diagnostic , Edit , Fix , FixAvailability , Violation } ;
2
+ use ruff_diagnostics:: { Applicability , Diagnostic , Edit , Fix , FixAvailability , Violation } ;
3
3
use ruff_macros:: { derive_message_formats, violation} ;
4
4
use ruff_python_ast:: helpers:: pep_604_union;
5
5
use ruff_python_ast:: name:: Name ;
@@ -25,21 +25,28 @@ use crate::checkers::ast::Checker;
25
25
/// ```pyi
26
26
/// field: type[int | float] | str
27
27
/// ```
28
+ ///
29
+ /// ## Fix safety
30
+ ///
31
+ /// This rule's fix is marked as safe in most cases; however, the fix will
32
+ /// flatten nested unions type expressions into a single top-level union.
33
+ ///
34
+ /// The fix is marked as unsafe when comments are present within the type
35
+ /// expression.
28
36
#[ violation]
29
37
pub struct UnnecessaryTypeUnion {
30
38
members : Vec < Name > ,
31
- is_pep604_union : bool ,
39
+ union_kind : UnionKind ,
32
40
}
33
41
34
42
impl Violation for UnnecessaryTypeUnion {
35
43
const FIX_AVAILABILITY : FixAvailability = FixAvailability :: Sometimes ;
36
44
37
45
#[ derive_message_formats]
38
46
fn message ( & self ) -> String {
39
- let union_str = if self . is_pep604_union {
40
- self . members . join ( " | " )
41
- } else {
42
- format ! ( "Union[{}]" , self . members. join( ", " ) )
47
+ let union_str = match self . union_kind {
48
+ UnionKind :: PEP604 => self . members . join ( " | " ) ,
49
+ UnionKind :: TypingUnion => format ! ( "Union[{}]" , self . members. join( ", " ) ) ,
43
50
} ;
44
51
45
52
format ! (
@@ -63,43 +70,85 @@ pub(crate) fn unnecessary_type_union<'a>(checker: &mut Checker, union: &'a Expr)
63
70
64
71
// Check if `union` is a PEP604 union (e.g. `float | int`) or a `typing.Union[float, int]`
65
72
let subscript = union. as_subscript_expr ( ) ;
66
- if subscript. is_some_and ( |subscript| !semantic. match_typing_expr ( & subscript. value , "Union" ) ) {
67
- return ;
68
- }
73
+ let mut union_kind = match subscript {
74
+ Some ( subscript) => {
75
+ if !semantic. match_typing_expr ( & subscript. value , "Union" ) {
76
+ return ;
77
+ }
78
+ UnionKind :: TypingUnion
79
+ }
80
+ None => UnionKind :: PEP604 ,
81
+ } ;
69
82
70
83
let mut type_exprs: Vec < & Expr > = Vec :: new ( ) ;
71
84
let mut other_exprs: Vec < & Expr > = Vec :: new ( ) ;
72
85
73
- let mut collect_type_exprs = |expr : & ' a Expr , _parent : & ' a Expr | match expr {
74
- Expr :: Subscript ( ast:: ExprSubscript { slice, value, .. } ) => {
75
- if semantic. match_builtin_expr ( value, "type" ) {
76
- type_exprs. push ( slice) ;
77
- } else {
78
- other_exprs. push ( expr) ;
86
+ let mut collect_type_exprs = |expr : & ' a Expr , parent : & ' a Expr | {
87
+ // If a PEP604-style union is used within a `typing.Union`, then the fix can
88
+ // use PEP604-style unions.
89
+ if matches ! ( parent, Expr :: BinOp ( _) ) {
90
+ union_kind = UnionKind :: PEP604 ;
91
+ }
92
+ match expr {
93
+ Expr :: Subscript ( ast:: ExprSubscript { slice, value, .. } ) => {
94
+ if semantic. match_builtin_expr ( value, "type" ) {
95
+ type_exprs. push ( slice) ;
96
+ } else {
97
+ other_exprs. push ( expr) ;
98
+ }
79
99
}
100
+ _ => other_exprs. push ( expr) ,
80
101
}
81
- _ => other_exprs. push ( expr) ,
82
102
} ;
83
103
84
104
traverse_union ( & mut collect_type_exprs, semantic, union) ;
85
105
86
- if type_exprs. len ( ) > 1 {
87
- let type_members: Vec < Name > = type_exprs
88
- . clone ( )
89
- . into_iter ( )
90
- . map ( |type_expr| Name :: new ( checker. locator ( ) . slice ( type_expr) ) )
91
- . collect ( ) ;
92
-
93
- let mut diagnostic = Diagnostic :: new (
94
- UnnecessaryTypeUnion {
95
- members : type_members. clone ( ) ,
96
- is_pep604_union : subscript. is_none ( ) ,
97
- } ,
98
- union. range ( ) ,
99
- ) ;
100
-
101
- if semantic. has_builtin_binding ( "type" ) {
102
- let content = if let Some ( subscript) = subscript {
106
+ // Return if zero or one `type` expressions are found.
107
+ if type_exprs. len ( ) <= 1 {
108
+ return ;
109
+ }
110
+
111
+ let type_members: Vec < Name > = type_exprs
112
+ . iter ( )
113
+ . map ( |type_expr| Name :: new ( checker. locator ( ) . slice ( type_expr) ) )
114
+ . collect ( ) ;
115
+
116
+ let mut diagnostic = Diagnostic :: new (
117
+ UnnecessaryTypeUnion {
118
+ members : type_members. clone ( ) ,
119
+ union_kind,
120
+ } ,
121
+ union. range ( ) ,
122
+ ) ;
123
+
124
+ if semantic. has_builtin_binding ( "type" ) {
125
+ // Construct the content for the [`Fix`] based on if we encountered a PEP604 union.
126
+ let content = match union_kind {
127
+ UnionKind :: PEP604 => {
128
+ let elts: Vec < Expr > = type_exprs. into_iter ( ) . cloned ( ) . collect ( ) ;
129
+ let types = Expr :: Subscript ( ast:: ExprSubscript {
130
+ value : Box :: new ( Expr :: Name ( ast:: ExprName {
131
+ id : Name :: new_static ( "type" ) ,
132
+ ctx : ExprContext :: Load ,
133
+ range : TextRange :: default ( ) ,
134
+ } ) ) ,
135
+ slice : Box :: new ( pep_604_union ( & elts) ) ,
136
+ ctx : ExprContext :: Load ,
137
+ range : TextRange :: default ( ) ,
138
+ } ) ;
139
+
140
+ if other_exprs. is_empty ( ) {
141
+ checker. generator ( ) . expr ( & types)
142
+ } else {
143
+ let elts: Vec < Expr > = std:: iter:: once ( types)
144
+ . chain ( other_exprs. into_iter ( ) . cloned ( ) )
145
+ . collect ( ) ;
146
+ checker. generator ( ) . expr ( & pep_604_union ( & elts) )
147
+ }
148
+ }
149
+ UnionKind :: TypingUnion => {
150
+ // When subscript is None, it uses the pervious match case.
151
+ let subscript = subscript. unwrap ( ) ;
103
152
let types = & Expr :: Subscript ( ast:: ExprSubscript {
104
153
value : Box :: new ( Expr :: Name ( ast:: ExprName {
105
154
id : Name :: new_static ( "type" ) ,
@@ -151,35 +200,29 @@ pub(crate) fn unnecessary_type_union<'a>(checker: &mut Checker, union: &'a Expr)
151
200
152
201
checker. generator ( ) . expr ( & union)
153
202
}
154
- } else {
155
- let elts: Vec < Expr > = type_exprs. into_iter ( ) . cloned ( ) . collect ( ) ;
156
- let types = Expr :: Subscript ( ast:: ExprSubscript {
157
- value : Box :: new ( Expr :: Name ( ast:: ExprName {
158
- id : Name :: new_static ( "type" ) ,
159
- ctx : ExprContext :: Load ,
160
- range : TextRange :: default ( ) ,
161
- } ) ) ,
162
- slice : Box :: new ( pep_604_union ( & elts) ) ,
163
- ctx : ExprContext :: Load ,
164
- range : TextRange :: default ( ) ,
165
- } ) ;
166
-
167
- if other_exprs. is_empty ( ) {
168
- checker. generator ( ) . expr ( & types)
169
- } else {
170
- let elts: Vec < Expr > = std:: iter:: once ( types)
171
- . chain ( other_exprs. into_iter ( ) . cloned ( ) )
172
- . collect ( ) ;
173
- checker. generator ( ) . expr ( & pep_604_union ( & elts) )
174
- }
175
- } ;
203
+ }
204
+ } ;
176
205
177
- diagnostic. set_fix ( Fix :: safe_edit ( Edit :: range_replacement (
178
- content,
179
- union. range ( ) ,
180
- ) ) ) ;
181
- }
206
+ // Mark [`Fix`] as unsafe when comments are in range.
207
+ let applicability = if checker. comment_ranges ( ) . intersects ( union. range ( ) ) {
208
+ Applicability :: Unsafe
209
+ } else {
210
+ Applicability :: Safe
211
+ } ;
182
212
183
- checker. diagnostics . push ( diagnostic) ;
213
+ diagnostic. set_fix ( Fix :: applicable_edit (
214
+ Edit :: range_replacement ( content, union. range ( ) ) ,
215
+ applicability,
216
+ ) ) ;
184
217
}
218
+
219
+ checker. diagnostics . push ( diagnostic) ;
220
+ }
221
+
222
+ #[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
223
+ enum UnionKind {
224
+ /// E.g., `typing.Union[int, str]`
225
+ TypingUnion ,
226
+ /// E.g., `int | str`
227
+ PEP604 ,
185
228
}
0 commit comments