@@ -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,86 @@ 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
+ #[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
190
+ enum DunderAllCount {
191
+ Zero ,
192
+ One ,
193
+ Many ,
194
+ }
195
+
196
+ impl From < usize > for DunderAllCount {
197
+ fn from ( value : usize ) -> Self {
198
+ match value {
199
+ 0 => Self :: Zero ,
200
+ 1 => Self :: One ,
201
+ _ => Self :: Many ,
165
202
}
166
203
}
167
204
}
168
205
206
+ #[ derive( Debug , Copy , Clone , Eq , PartialEq , is_macro:: Is ) ]
207
+ enum UnusedImportContext {
208
+ ExceptHandler ,
209
+ DunderInitFirstParty {
210
+ dunder_all_count : DunderAllCount ,
211
+ submodule_import : bool ,
212
+ } ,
213
+ Other ,
214
+ }
215
+
169
216
fn is_first_party ( qualified_name : & str , level : u32 , checker : & Checker ) -> bool {
170
217
let category = isort:: categorize (
171
218
qualified_name,
@@ -304,31 +351,20 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut
304
351
. into_iter ( )
305
352
. map ( |binding| {
306
353
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
- } )
354
+ UnusedImportContext :: ExceptHandler
355
+ } else if in_init
356
+ && is_first_party ( & binding. import . qualified_name ( ) . to_string ( ) , level, checker)
357
+ {
358
+ UnusedImportContext :: DunderInitFirstParty {
359
+ dunder_all_count : DunderAllCount :: from ( dunder_all_exprs. len ( ) ) ,
360
+ submodule_import : binding. import . is_submodule_import ( ) ,
361
+ }
318
362
} else {
319
- None
363
+ UnusedImportContext :: Other
320
364
} ;
321
365
( binding, context)
322
366
} )
323
- . partition ( |( _, context) | {
324
- matches ! (
325
- context,
326
- Some ( UnusedImportContext :: Init {
327
- first_party: true ,
328
- ..
329
- } )
330
- ) && preview_mode
331
- } ) ;
367
+ . partition ( |( _, context) | context. is_dunder_init_first_party ( ) && preview_mode) ;
332
368
333
369
// generate fixes that are shared across bindings in the statement
334
370
let ( fix_remove, fix_reexport) =
@@ -344,7 +380,7 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut
344
380
fix_by_reexporting (
345
381
checker,
346
382
import_statement,
347
- to_reexport. iter ( ) . map ( |( b, _) | b) . collect :: < Vec < _ > > ( ) ,
383
+ to_reexport. iter ( ) . map ( |( b, _) | b) ,
348
384
& dunder_all_exprs,
349
385
)
350
386
. ok ( ) ,
@@ -364,6 +400,7 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut
364
400
binding : binding. name . to_string ( ) ,
365
401
context,
366
402
multiple,
403
+ ignore_init_module_imports : !fix_init,
367
404
} ,
368
405
binding. range ,
369
406
) ;
@@ -387,8 +424,9 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut
387
424
name : binding. import . qualified_name ( ) . to_string ( ) ,
388
425
module : binding. import . member_name ( ) . to_string ( ) ,
389
426
binding : binding. name . to_string ( ) ,
390
- context : None ,
427
+ context : UnusedImportContext :: Other ,
391
428
multiple : false ,
429
+ ignore_init_module_imports : !fix_init,
392
430
} ,
393
431
binding. range ,
394
432
) ;
@@ -412,6 +450,31 @@ struct ImportBinding<'a> {
412
450
parent_range : Option < TextRange > ,
413
451
}
414
452
453
+ impl < ' a > ImportBinding < ' a > {
454
+ /// The symbol that is stored in the outer scope as a result of this import.
455
+ ///
456
+ /// For example:
457
+ /// - `import foo` => `foo` symbol stored in outer scope
458
+ /// - `import foo as bar` => `bar` symbol stored in outer scope
459
+ /// - `from foo import bar` => `bar` symbol stored in outer scope
460
+ /// - `from foo import bar as baz` => `baz` symbol stored in outer scope
461
+ /// - `import foo.bar` => `foo` symbol stored in outer scope
462
+ fn symbol_stored_in_outer_scope ( & self ) -> & str {
463
+ match & self . import {
464
+ AnyImport :: FromImport ( _) => self . name ,
465
+ AnyImport :: Import ( _) => self . name ,
466
+ AnyImport :: SubmoduleImport ( SubmoduleImport { qualified_name } ) => {
467
+ qualified_name. segments ( ) . first ( ) . unwrap_or_else ( || {
468
+ panic ! (
469
+ "Expected an import binding to have a non-empty qualified name;
470
+ got {qualified_name}"
471
+ )
472
+ } )
473
+ }
474
+ }
475
+ }
476
+ }
477
+
415
478
impl Ranged for ImportBinding < ' _ > {
416
479
fn range ( & self ) -> TextRange {
417
480
self . range
@@ -461,29 +524,31 @@ fn fix_by_removing_imports<'a>(
461
524
462
525
/// Generate a [`Fix`] to make bindings in a statement explicit, either by adding them to `__all__`
463
526
/// or changing them from `import a` to `import a as a`.
464
- fn fix_by_reexporting (
527
+ fn fix_by_reexporting < ' a > (
465
528
checker : & Checker ,
466
529
node_id : NodeId ,
467
- mut imports : Vec < & ImportBinding > ,
530
+ imports : impl IntoIterator < Item = & ' a ImportBinding < ' a > > ,
468
531
dunder_all_exprs : & [ & ast:: Expr ] ,
469
532
) -> Result < Fix > {
470
533
let statement = checker. semantic ( ) . statement ( node_id) ;
471
- if imports. is_empty ( ) {
472
- bail ! ( "Expected import bindings" ) ;
473
- }
474
534
475
- imports. sort_by_key ( |b| b. name ) ;
535
+ let imports = {
536
+ let mut imports: Vec < & str > = imports
537
+ . into_iter ( )
538
+ . map ( ImportBinding :: symbol_stored_in_outer_scope)
539
+ . collect ( ) ;
540
+ if imports. is_empty ( ) {
541
+ bail ! ( "Expected import bindings" ) ;
542
+ }
543
+ imports. sort_unstable ( ) ;
544
+ imports
545
+ } ;
476
546
477
547
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
- ) ,
548
+ [ ] => fix:: edits:: make_redundant_alias ( imports. into_iter ( ) , statement) ,
549
+ [ dunder_all] => {
550
+ fix:: edits:: add_to_dunder_all ( imports. into_iter ( ) , dunder_all, checker. stylist ( ) )
551
+ }
487
552
_ => bail ! ( "Cannot offer a fix when there are multiple __all__ definitions" ) ,
488
553
} ;
489
554
0 commit comments