@@ -9,7 +9,7 @@ use ruff_macros::{derive_message_formats, violation};
9
9
use ruff_python_ast as ast;
10
10
use ruff_python_ast:: { Stmt , StmtImportFrom } ;
11
11
use ruff_python_semantic:: {
12
- AnyImport , BindingKind , Exceptions , Imported , NodeId , Scope , SemanticModel ,
12
+ AnyImport , BindingKind , Exceptions , Imported , NodeId , Scope , SemanticModel , SubmoduleImport ,
13
13
} ;
14
14
use ruff_text_size:: { Ranged , TextRange } ;
15
15
@@ -18,16 +18,6 @@ use crate::fix;
18
18
use crate :: registry:: Rule ;
19
19
use crate :: rules:: { isort, isort:: ImportSection , isort:: ImportType } ;
20
20
21
- #[ derive( Debug , Copy , Clone , Eq , PartialEq ) ]
22
- enum UnusedImportContext {
23
- ExceptHandler ,
24
- Init {
25
- first_party : bool ,
26
- dunder_all_count : usize ,
27
- ignore_init_module_imports : bool ,
28
- } ,
29
- }
30
-
31
21
/// ## What it does
32
22
/// Checks for unused imports.
33
23
///
@@ -111,8 +101,9 @@ pub struct UnusedImport {
111
101
module : String ,
112
102
/// Name of the import binding
113
103
binding : String ,
114
- context : Option < UnusedImportContext > ,
104
+ context : UnusedImportContext ,
115
105
multiple : bool ,
106
+ ignore_init_module_imports : bool ,
116
107
}
117
108
118
109
impl Violation for UnusedImport {
@@ -122,17 +113,17 @@ impl Violation for UnusedImport {
122
113
fn message ( & self ) -> String {
123
114
let UnusedImport { name, context, .. } = self ;
124
115
match context {
125
- Some ( UnusedImportContext :: ExceptHandler ) => {
116
+ UnusedImportContext :: ExceptHandler => {
126
117
format ! (
127
118
"`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability"
128
119
)
129
120
}
130
- Some ( UnusedImportContext :: Init { .. } ) => {
121
+ UnusedImportContext :: DunderInitFirstParty { .. } => {
131
122
format ! (
132
123
"`{name}` imported but unused; consider removing, adding to `__all__`, or using a redundant alias"
133
124
)
134
125
}
135
- None => format ! ( "`{name}` imported but unused" ) ,
126
+ UnusedImportContext :: Other => format ! ( "`{name}` imported but unused" ) ,
136
127
}
137
128
}
138
129
@@ -142,30 +133,91 @@ impl Violation for UnusedImport {
142
133
module,
143
134
binding,
144
135
multiple,
145
- ..
136
+ ignore_init_module_imports,
137
+ context,
146
138
} = self ;
147
- match self . context {
148
- Some ( UnusedImportContext :: Init {
149
- first_party : true ,
150
- dunder_all_count : 1 ,
151
- ignore_init_module_imports : true ,
152
- } ) => Some ( format ! ( "Add unused import `{binding}` to __all__" ) ) ,
153
-
154
- Some ( UnusedImportContext :: Init {
155
- first_party : true ,
156
- dunder_all_count : 0 ,
157
- ignore_init_module_imports : true ,
158
- } ) => Some ( format ! ( "Use an explicit re-export: `{module} as {module}`" ) ) ,
159
-
160
- _ => Some ( if * multiple {
161
- "Remove unused import" . to_string ( )
162
- } else {
163
- format ! ( "Remove unused import: `{name}`" )
164
- } ) ,
139
+ if * ignore_init_module_imports {
140
+ match context {
141
+ UnusedImportContext :: DunderInitFirstParty {
142
+ dunder_all_count : DunderAllCount :: Zero ,
143
+ submodule_import : false ,
144
+ } => return Some ( format ! ( "Use an explicit re-export: `{module} as {module}`" ) ) ,
145
+ UnusedImportContext :: DunderInitFirstParty {
146
+ dunder_all_count : DunderAllCount :: Zero ,
147
+ submodule_import : true ,
148
+ } => {
149
+ return Some ( format ! (
150
+ "Use an explicit re-export: `import {parent} as {parent}; import {binding}`" ,
151
+ parent = binding
152
+ . split( '.' )
153
+ . next( )
154
+ . expect( "Expected all submodule imports to contain a '.'" )
155
+ ) )
156
+ }
157
+ UnusedImportContext :: DunderInitFirstParty {
158
+ dunder_all_count : DunderAllCount :: One ,
159
+ submodule_import : false ,
160
+ } => return Some ( format ! ( "Add unused import `{binding}` to __all__" ) ) ,
161
+ UnusedImportContext :: DunderInitFirstParty {
162
+ dunder_all_count : DunderAllCount :: One ,
163
+ submodule_import : true ,
164
+ } => {
165
+ return Some ( format ! (
166
+ "Add `{}` to __all__" ,
167
+ binding
168
+ . split( '.' )
169
+ . next( )
170
+ . expect( "Expected all submodule imports to contain a '.'" )
171
+ ) )
172
+ }
173
+ UnusedImportContext :: DunderInitFirstParty {
174
+ dunder_all_count : DunderAllCount :: Many ,
175
+ submodule_import : _,
176
+ }
177
+ | UnusedImportContext :: ExceptHandler
178
+ | UnusedImportContext :: Other => { }
179
+ }
180
+ }
181
+ Some ( if * multiple {
182
+ "Remove unused import" . to_string ( )
183
+ } else {
184
+ format ! ( "Remove unused import: `{name}`" )
185
+ } )
186
+ }
187
+ }
188
+
189
+ /// Enumeration providing three possible answers to the question:
190
+ /// "How many `__all__` definitions are there in this file?"
191
+ #[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
192
+ enum DunderAllCount {
193
+ Zero ,
194
+ One ,
195
+ Many ,
196
+ }
197
+
198
+ impl From < usize > for DunderAllCount {
199
+ fn from ( value : usize ) -> Self {
200
+ match value {
201
+ 0 => Self :: Zero ,
202
+ 1 => Self :: One ,
203
+ _ => Self :: Many ,
165
204
}
166
205
}
167
206
}
168
207
208
+ #[ derive( Debug , Copy , Clone , Eq , PartialEq , is_macro:: Is ) ]
209
+ enum UnusedImportContext {
210
+ /// The unused import occurs inside an except handler
211
+ ExceptHandler ,
212
+ /// The unused import is a first-party import in an `__init__.py` file
213
+ DunderInitFirstParty {
214
+ dunder_all_count : DunderAllCount ,
215
+ submodule_import : bool ,
216
+ } ,
217
+ /// The unused import is something else
218
+ Other ,
219
+ }
220
+
169
221
fn is_first_party ( qualified_name : & str , level : u32 , checker : & Checker ) -> bool {
170
222
let category = isort:: categorize (
171
223
qualified_name,
@@ -304,31 +356,20 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut
304
356
. into_iter ( )
305
357
. map ( |binding| {
306
358
let context = if in_except_handler {
307
- Some ( UnusedImportContext :: ExceptHandler )
308
- } else if in_init && !binding. import . is_submodule_import ( ) {
309
- Some ( UnusedImportContext :: Init {
310
- first_party : is_first_party (
311
- & binding. import . qualified_name ( ) . to_string ( ) ,
312
- level,
313
- checker,
314
- ) ,
315
- dunder_all_count : dunder_all_exprs. len ( ) ,
316
- ignore_init_module_imports : !fix_init,
317
- } )
359
+ UnusedImportContext :: ExceptHandler
360
+ } else if in_init
361
+ && is_first_party ( & binding. import . qualified_name ( ) . to_string ( ) , level, checker)
362
+ {
363
+ UnusedImportContext :: DunderInitFirstParty {
364
+ dunder_all_count : DunderAllCount :: from ( dunder_all_exprs. len ( ) ) ,
365
+ submodule_import : binding. import . is_submodule_import ( ) ,
366
+ }
318
367
} else {
319
- None
368
+ UnusedImportContext :: Other
320
369
} ;
321
370
( binding, context)
322
371
} )
323
- . partition ( |( _, context) | {
324
- matches ! (
325
- context,
326
- Some ( UnusedImportContext :: Init {
327
- first_party: true ,
328
- ..
329
- } )
330
- ) && preview_mode
331
- } ) ;
372
+ . partition ( |( _, context) | context. is_dunder_init_first_party ( ) && preview_mode) ;
332
373
333
374
// generate fixes that are shared across bindings in the statement
334
375
let ( fix_remove, fix_reexport) =
@@ -344,7 +385,7 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut
344
385
fix_by_reexporting (
345
386
checker,
346
387
import_statement,
347
- to_reexport. iter ( ) . map ( |( b, _) | b) . collect :: < Vec < _ > > ( ) ,
388
+ to_reexport. iter ( ) . map ( |( b, _) | b) ,
348
389
& dunder_all_exprs,
349
390
)
350
391
. ok ( ) ,
@@ -364,6 +405,7 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut
364
405
binding : binding. name . to_string ( ) ,
365
406
context,
366
407
multiple,
408
+ ignore_init_module_imports : !fix_init,
367
409
} ,
368
410
binding. range ,
369
411
) ;
@@ -387,8 +429,9 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut
387
429
name : binding. import . qualified_name ( ) . to_string ( ) ,
388
430
module : binding. import . member_name ( ) . to_string ( ) ,
389
431
binding : binding. name . to_string ( ) ,
390
- context : None ,
432
+ context : UnusedImportContext :: Other ,
391
433
multiple : false ,
434
+ ignore_init_module_imports : !fix_init,
392
435
} ,
393
436
binding. range ,
394
437
) ;
@@ -412,6 +455,31 @@ struct ImportBinding<'a> {
412
455
parent_range : Option < TextRange > ,
413
456
}
414
457
458
+ impl < ' a > ImportBinding < ' a > {
459
+ /// The symbol that is stored in the outer scope as a result of this import.
460
+ ///
461
+ /// For example:
462
+ /// - `import foo` => `foo` symbol stored in outer scope
463
+ /// - `import foo as bar` => `bar` symbol stored in outer scope
464
+ /// - `from foo import bar` => `bar` symbol stored in outer scope
465
+ /// - `from foo import bar as baz` => `baz` symbol stored in outer scope
466
+ /// - `import foo.bar` => `foo` symbol stored in outer scope
467
+ fn symbol_stored_in_outer_scope ( & self ) -> & str {
468
+ match & self . import {
469
+ AnyImport :: FromImport ( _) => self . name ,
470
+ AnyImport :: Import ( _) => self . name ,
471
+ AnyImport :: SubmoduleImport ( SubmoduleImport { qualified_name } ) => {
472
+ qualified_name. segments ( ) . first ( ) . unwrap_or_else ( || {
473
+ panic ! (
474
+ "Expected an import binding to have a non-empty qualified name;
475
+ got {qualified_name}"
476
+ )
477
+ } )
478
+ }
479
+ }
480
+ }
481
+ }
482
+
415
483
impl Ranged for ImportBinding < ' _ > {
416
484
fn range ( & self ) -> TextRange {
417
485
self . range
@@ -461,29 +529,31 @@ fn fix_by_removing_imports<'a>(
461
529
462
530
/// Generate a [`Fix`] to make bindings in a statement explicit, either by adding them to `__all__`
463
531
/// or changing them from `import a` to `import a as a`.
464
- fn fix_by_reexporting (
532
+ fn fix_by_reexporting < ' a > (
465
533
checker : & Checker ,
466
534
node_id : NodeId ,
467
- mut imports : Vec < & ImportBinding > ,
535
+ imports : impl IntoIterator < Item = & ' a ImportBinding < ' a > > ,
468
536
dunder_all_exprs : & [ & ast:: Expr ] ,
469
537
) -> Result < Fix > {
470
538
let statement = checker. semantic ( ) . statement ( node_id) ;
471
- if imports. is_empty ( ) {
472
- bail ! ( "Expected import bindings" ) ;
473
- }
474
539
475
- imports. sort_by_key ( |b| b. name ) ;
540
+ let imports = {
541
+ let mut imports: Vec < & str > = imports
542
+ . into_iter ( )
543
+ . map ( ImportBinding :: symbol_stored_in_outer_scope)
544
+ . collect ( ) ;
545
+ if imports. is_empty ( ) {
546
+ bail ! ( "Expected import bindings" ) ;
547
+ }
548
+ imports. sort_unstable ( ) ;
549
+ imports
550
+ } ;
476
551
477
552
let edits = match dunder_all_exprs {
478
- [ ] => fix:: edits:: make_redundant_alias (
479
- imports. iter ( ) . map ( |b| b. import . member_name ( ) ) ,
480
- statement,
481
- ) ,
482
- [ dunder_all] => fix:: edits:: add_to_dunder_all (
483
- imports. iter ( ) . map ( |b| b. name ) ,
484
- dunder_all,
485
- checker. stylist ( ) ,
486
- ) ,
553
+ [ ] => fix:: edits:: make_redundant_alias ( imports. into_iter ( ) , statement) ,
554
+ [ dunder_all] => {
555
+ fix:: edits:: add_to_dunder_all ( imports. into_iter ( ) , dunder_all, checker. stylist ( ) )
556
+ }
487
557
_ => bail ! ( "Cannot offer a fix when there are multiple __all__ definitions" ) ,
488
558
} ;
489
559
0 commit comments