Skip to content

Commit 670c0b5

Browse files
committed
do the better fix
1 parent f041004 commit 670c0b5

14 files changed

+181
-105
lines changed

crates/ruff_linter/src/fix/edits.rs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
//! Interface for generating fix edits from higher-level actions (e.g., "remove an argument").
22
3-
use std::borrow::Cow;
4-
53
use anyhow::{Context, Result};
64

75
use ruff_diagnostics::Edit;
@@ -126,7 +124,7 @@ pub(crate) fn remove_unused_imports<'a>(
126124

127125
/// Edits to make the specified imports explicit, e.g. change `import x` to `import x as x`.
128126
pub(crate) fn make_redundant_alias<'a>(
129-
member_names: impl Iterator<Item = Cow<'a, str>>,
127+
member_names: impl Iterator<Item = &'a str>,
130128
stmt: &Stmt,
131129
) -> Vec<Edit> {
132130
let aliases = match stmt {
@@ -527,7 +525,6 @@ fn all_lines_fit(
527525
#[cfg(test)]
528526
mod tests {
529527
use anyhow::{anyhow, Result};
530-
use std::borrow::Cow;
531528
use test_case::test_case;
532529

533530
use ruff_diagnostics::{Diagnostic, Edit, Fix};
@@ -619,23 +616,23 @@ x = 1 \
619616
let contents = "import x, y as y, z as bees";
620617
let stmt = parse_first_stmt(contents)?;
621618
assert_eq!(
622-
make_redundant_alias(["x"].into_iter().map(Cow::from), &stmt),
619+
make_redundant_alias(["x"].into_iter(), &stmt),
623620
vec![Edit::range_replacement(
624621
String::from("x as x"),
625622
TextRange::new(TextSize::new(7), TextSize::new(8)),
626623
)],
627624
"make just one item redundant"
628625
);
629626
assert_eq!(
630-
make_redundant_alias(vec!["x", "y"].into_iter().map(Cow::from), &stmt),
627+
make_redundant_alias(vec!["x", "y"].into_iter(), &stmt),
631628
vec![Edit::range_replacement(
632629
String::from("x as x"),
633630
TextRange::new(TextSize::new(7), TextSize::new(8)),
634631
)],
635632
"the second item is already a redundant alias"
636633
);
637634
assert_eq!(
638-
make_redundant_alias(vec!["x", "z"].into_iter().map(Cow::from), &stmt),
635+
make_redundant_alias(vec!["x", "z"].into_iter(), &stmt),
639636
vec![Edit::range_replacement(
640637
String::from("x as x"),
641638
TextRange::new(TextSize::new(7), TextSize::new(8)),

crates/ruff_linter/src/rules/pyflakes/mod.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,12 +234,16 @@ mod tests {
234234
Ok(())
235235
}
236236

237+
#[test_case(
238+
r"import submodule.a",
239+
"f401_preview_first_party_submodule_no_dunder_all"
240+
)]
237241
#[test_case(
238242
r"
239243
import submodule.a
240244
__all__ = ['FOO']
241245
FOO = 42",
242-
"f401_preview_first_party_submodule"
246+
"f401_preview_first_party_submodule_dunder_all"
243247
)]
244248
fn f401_preview_first_party_submodule(contents: &str, snapshot: &str) {
245249
let diagnostics = test_contents(

crates/ruff_linter/src/rules/pyflakes/rules/unused_import.rs

Lines changed: 137 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use ruff_macros::{derive_message_formats, violation};
99
use ruff_python_ast as ast;
1010
use ruff_python_ast::{Stmt, StmtImportFrom};
1111
use ruff_python_semantic::{
12-
AnyImport, BindingKind, Exceptions, Imported, NodeId, Scope, SemanticModel,
12+
AnyImport, BindingKind, Exceptions, Imported, NodeId, Scope, SemanticModel, SubmoduleImport,
1313
};
1414
use ruff_text_size::{Ranged, TextRange};
1515

@@ -18,16 +18,6 @@ use crate::fix;
1818
use crate::registry::Rule;
1919
use crate::rules::{isort, isort::ImportSection, isort::ImportType};
2020

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-
3121
/// ## What it does
3222
/// Checks for unused imports.
3323
///
@@ -111,8 +101,9 @@ pub struct UnusedImport {
111101
module: String,
112102
/// Name of the import binding
113103
binding: String,
114-
context: Option<UnusedImportContext>,
104+
context: UnusedImportContext,
115105
multiple: bool,
106+
ignore_init_module_imports: bool,
116107
}
117108

118109
impl Violation for UnusedImport {
@@ -122,17 +113,17 @@ impl Violation for UnusedImport {
122113
fn message(&self) -> String {
123114
let UnusedImport { name, context, .. } = self;
124115
match context {
125-
Some(UnusedImportContext::ExceptHandler) => {
116+
UnusedImportContext::ExceptHandler => {
126117
format!(
127118
"`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability"
128119
)
129120
}
130-
Some(UnusedImportContext::Init { .. }) => {
121+
UnusedImportContext::DunderInitFirstParty { .. } => {
131122
format!(
132123
"`{name}` imported but unused; consider removing, adding to `__all__`, or using a redundant alias"
133124
)
134125
}
135-
None => format!("`{name}` imported but unused"),
126+
UnusedImportContext::Other => format!("`{name}` imported but unused"),
136127
}
137128
}
138129

@@ -142,30 +133,86 @@ impl Violation for UnusedImport {
142133
module,
143134
binding,
144135
multiple,
145-
..
136+
ignore_init_module_imports,
137+
context,
146138
} = 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,
165202
}
166203
}
167204
}
168205

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+
169216
fn is_first_party(qualified_name: &str, level: u32, checker: &Checker) -> bool {
170217
let category = isort::categorize(
171218
qualified_name,
@@ -304,31 +351,20 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut
304351
.into_iter()
305352
.map(|binding| {
306353
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+
}
318362
} else {
319-
None
363+
UnusedImportContext::Other
320364
};
321365
(binding, context)
322366
})
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);
332368

333369
// generate fixes that are shared across bindings in the statement
334370
let (fix_remove, fix_reexport) =
@@ -344,7 +380,7 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut
344380
fix_by_reexporting(
345381
checker,
346382
import_statement,
347-
to_reexport.iter().map(|(b, _)| b).collect::<Vec<_>>(),
383+
to_reexport.iter().map(|(b, _)| b),
348384
&dunder_all_exprs,
349385
)
350386
.ok(),
@@ -364,6 +400,7 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut
364400
binding: binding.name.to_string(),
365401
context,
366402
multiple,
403+
ignore_init_module_imports: !fix_init,
367404
},
368405
binding.range,
369406
);
@@ -387,8 +424,9 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut
387424
name: binding.import.qualified_name().to_string(),
388425
module: binding.import.member_name().to_string(),
389426
binding: binding.name.to_string(),
390-
context: None,
427+
context: UnusedImportContext::Other,
391428
multiple: false,
429+
ignore_init_module_imports: !fix_init,
392430
},
393431
binding.range,
394432
);
@@ -412,6 +450,31 @@ struct ImportBinding<'a> {
412450
parent_range: Option<TextRange>,
413451
}
414452

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+
415478
impl Ranged for ImportBinding<'_> {
416479
fn range(&self) -> TextRange {
417480
self.range
@@ -461,29 +524,31 @@ fn fix_by_removing_imports<'a>(
461524

462525
/// Generate a [`Fix`] to make bindings in a statement explicit, either by adding them to `__all__`
463526
/// or changing them from `import a` to `import a as a`.
464-
fn fix_by_reexporting(
527+
fn fix_by_reexporting<'a>(
465528
checker: &Checker,
466529
node_id: NodeId,
467-
mut imports: Vec<&ImportBinding>,
530+
imports: impl IntoIterator<Item = &'a ImportBinding<'a>>,
468531
dunder_all_exprs: &[&ast::Expr],
469532
) -> Result<Fix> {
470533
let statement = checker.semantic().statement(node_id);
471-
if imports.is_empty() {
472-
bail!("Expected import bindings");
473-
}
474534

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+
};
476546

477547
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+
}
487552
_ => bail!("Cannot offer a fix when there are multiple __all__ definitions"),
488553
};
489554

crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F401_deprecated_option_F401_24____init__.py.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
33
---
4-
__init__.py:19:8: F401 [*] `sys` imported but unused; consider removing, adding to `__all__`, or using a redundant alias
4+
__init__.py:19:8: F401 [*] `sys` imported but unused
55
|
66
19 | import sys # F401: remove unused
77
| ^^^ F401

0 commit comments

Comments
 (0)