From 59afff0e6a3aabc1bdbda5e6d333529911a8a101 Mon Sep 17 00:00:00 2001 From: plredmond <51248199+plredmond@users.noreply.github.com> Date: Thu, 2 May 2024 16:10:32 -0700 Subject: [PATCH] F401 - Distinguish between imports we wish to remove and those we wish to make explicit-exports (#11168) Resolves #10390 and starts to address #10391 # Changes to behavior * In `__init__.py` we now offer some fixes for unused imports. * If the import binding is first-party this PR suggests a fix to turn it into a redundant alias. * If the import binding is not first-party, this PR suggests a fix to remove it from the `__init__.py`. * The fix-titles are specific to these new suggested fixes. * `checker.settings.ignore_init_module_imports` setting is deprecated/ignored. There is probably a documentation change to make that complete which I haven't done. ---
Old description of implementation changes # Changes to the implementation * In the body of the loop over import statements that contain unused bindings, the bindings are partitioned into `to_reexport` and `to_remove` (according to how we want to resolve the fact they're unused) with the following predicate: ```rust in_init && is_first_party(checker, &import.qualified_name().to_string()) // true means make it a reexport ``` * Instead of generating a single fix per import statement, we now generate up to two fixes per import statement: ```rust (fix_by_removing_imports(checker, node_id, &to_remove, in_init).ok(), fix_by_reexporting(checker, node_id, &to_reexport, dunder_all).ok()) ``` * The `to_remove` fixes are unsafe when `in_init`. * The `to_explicit` fixes are safe. Currently, until a future PR, we make them redundant aliases (e.g. `import a` would become `import a as a`). ## Other changes * `checker.settings.ignore_init_module_imports` is deprecated/ignored. Instead, all fixes are gated on `checker.settings.preview.is_enabled()`. * Got rid of the pattern match on the import-binding bound by the inner loop because it seemed less readable than referencing fields on the binding. * [x] `// FIXME: rename "imports" to "bindings"` if reviewer agrees (see code) * [x] `// FIXME: rename "node_id" to "import_statement"` if reviewer agrees (see code)

Scope cut until a future PR

* (Not implemented) The `to_explicit` fixes will be added to `__all__` unless it doesn't exist. When `__all__` doesn't exist they're resolved by converting to redundant aliases (e.g. `import a` would become `import a as a`). ---
# Test plan * [x] `crates/ruff_linter/resources/test/fixtures/pyflakes/F401_24` contains an `__init__.py` with*out* `__all__` that exercises the features in this PR, but it doesn't pass. * [x] `crates/ruff_linter/resources/test/fixtures/pyflakes/F401_25_dunder_all` contains an `__init__.py` *with* `__all__` that exercises the features in this PR, but it doesn't pass. * [x] Write unit tests for the new edit functions in `fix::edits::make_redundant_alias`.
--------- Co-authored-by: Micha Reiser --- .../fixtures/pyflakes/F401_24/__init__.py | 36 ++++ .../test/fixtures/pyflakes/F401_24/aliased.py | 1 + .../test/fixtures/pyflakes/F401_24/renamed.py | 1 + .../test/fixtures/pyflakes/F401_24/unused.py | 1 + .../test/fixtures/pyflakes/F401_24/used.py | 1 + .../pyflakes/F401_25__all/__init__.py | 42 +++++ .../fixtures/pyflakes/F401_25__all/aliased.py | 1 + .../pyflakes/F401_25__all/exported.py | 1 + .../fixtures/pyflakes/F401_25__all/renamed.py | 1 + .../fixtures/pyflakes/F401_25__all/unused.py | 1 + .../fixtures/pyflakes/F401_25__all/used.py | 1 + crates/ruff_linter/src/fix/edits.rs | 58 +++++- crates/ruff_linter/src/rules/pyflakes/mod.rs | 16 +- .../src/rules/pyflakes/rules/unused_import.rs | 168 ++++++++++++++---- ...s__preview__F401_F401_24____init__.py.snap | 42 +++++ ...eview__F401_F401_25__all____init__.py.snap | 18 ++ ...es__tests__preview__F401___init__.py.snap} | 0 17 files changed, 343 insertions(+), 46 deletions(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/pyflakes/F401_24/__init__.py create mode 100644 crates/ruff_linter/resources/test/fixtures/pyflakes/F401_24/aliased.py create mode 100644 crates/ruff_linter/resources/test/fixtures/pyflakes/F401_24/renamed.py create mode 100644 crates/ruff_linter/resources/test/fixtures/pyflakes/F401_24/unused.py create mode 100644 crates/ruff_linter/resources/test/fixtures/pyflakes/F401_24/used.py create mode 100644 crates/ruff_linter/resources/test/fixtures/pyflakes/F401_25__all/__init__.py create mode 100644 crates/ruff_linter/resources/test/fixtures/pyflakes/F401_25__all/aliased.py create mode 100644 crates/ruff_linter/resources/test/fixtures/pyflakes/F401_25__all/exported.py create mode 100644 crates/ruff_linter/resources/test/fixtures/pyflakes/F401_25__all/renamed.py create mode 100644 crates/ruff_linter/resources/test/fixtures/pyflakes/F401_25__all/unused.py create mode 100644 crates/ruff_linter/resources/test/fixtures/pyflakes/F401_25__all/used.py create mode 100644 crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F401_F401_24____init__.py.snap create mode 100644 crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F401_F401_25__all____init__.py.snap rename crates/ruff_linter/src/rules/pyflakes/snapshots/{ruff_linter__rules__pyflakes__tests__init_unused_import_opt_in_to_fix.snap => ruff_linter__rules__pyflakes__tests__preview__F401___init__.py.snap} (100%) diff --git a/crates/ruff_linter/resources/test/fixtures/pyflakes/F401_24/__init__.py b/crates/ruff_linter/resources/test/fixtures/pyflakes/F401_24/__init__.py new file mode 100644 index 0000000000000..9768b40f1cce6 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyflakes/F401_24/__init__.py @@ -0,0 +1,36 @@ +"""__init__.py without __all__ + +Unused stdlib and third party imports are unsafe removals + +Unused first party imports get changed to redundant aliases +""" + + +# stdlib + +import os # Ok: is used + +_ = os + + +import argparse as argparse # Ok: is redundant alias + + +import sys # F401: remove unused + + +# first-party + + +from . import used # Ok: is used + +_ = used + + +from . import aliased as aliased # Ok: is redundant alias + + +from . import unused # F401: change to redundant alias + + +from . import renamed as bees # F401: no fix diff --git a/crates/ruff_linter/resources/test/fixtures/pyflakes/F401_24/aliased.py b/crates/ruff_linter/resources/test/fixtures/pyflakes/F401_24/aliased.py new file mode 100644 index 0000000000000..7391604add03e --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyflakes/F401_24/aliased.py @@ -0,0 +1 @@ +# empty module imported by __init__.py for test fixture diff --git a/crates/ruff_linter/resources/test/fixtures/pyflakes/F401_24/renamed.py b/crates/ruff_linter/resources/test/fixtures/pyflakes/F401_24/renamed.py new file mode 100644 index 0000000000000..7391604add03e --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyflakes/F401_24/renamed.py @@ -0,0 +1 @@ +# empty module imported by __init__.py for test fixture diff --git a/crates/ruff_linter/resources/test/fixtures/pyflakes/F401_24/unused.py b/crates/ruff_linter/resources/test/fixtures/pyflakes/F401_24/unused.py new file mode 100644 index 0000000000000..7391604add03e --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyflakes/F401_24/unused.py @@ -0,0 +1 @@ +# empty module imported by __init__.py for test fixture diff --git a/crates/ruff_linter/resources/test/fixtures/pyflakes/F401_24/used.py b/crates/ruff_linter/resources/test/fixtures/pyflakes/F401_24/used.py new file mode 100644 index 0000000000000..7391604add03e --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyflakes/F401_24/used.py @@ -0,0 +1 @@ +# empty module imported by __init__.py for test fixture diff --git a/crates/ruff_linter/resources/test/fixtures/pyflakes/F401_25__all/__init__.py b/crates/ruff_linter/resources/test/fixtures/pyflakes/F401_25__all/__init__.py new file mode 100644 index 0000000000000..3d6f86c663ca4 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyflakes/F401_25__all/__init__.py @@ -0,0 +1,42 @@ +"""__init__.py with __all__ + +Unused stdlib and third party imports are unsafe removals + +Unused first party imports get added to __all__ +""" + + +# stdlib + +import os # Ok: is used + +_ = os + + +import argparse # Ok: is exported in __all__ + + +import sys # F401: remove unused + + +# first-party + + +from . import used # Ok: is used + +_ = used + + +from . import aliased as aliased # Ok: is redundant alias + + +from . import exported # Ok: is exported in __all__ + + +# from . import unused # F401: add to __all__ + + +# from . import renamed as bees # F401: add to __all__ + + +__all__ = ["argparse", "exported"] diff --git a/crates/ruff_linter/resources/test/fixtures/pyflakes/F401_25__all/aliased.py b/crates/ruff_linter/resources/test/fixtures/pyflakes/F401_25__all/aliased.py new file mode 100644 index 0000000000000..7391604add03e --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyflakes/F401_25__all/aliased.py @@ -0,0 +1 @@ +# empty module imported by __init__.py for test fixture diff --git a/crates/ruff_linter/resources/test/fixtures/pyflakes/F401_25__all/exported.py b/crates/ruff_linter/resources/test/fixtures/pyflakes/F401_25__all/exported.py new file mode 100644 index 0000000000000..7391604add03e --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyflakes/F401_25__all/exported.py @@ -0,0 +1 @@ +# empty module imported by __init__.py for test fixture diff --git a/crates/ruff_linter/resources/test/fixtures/pyflakes/F401_25__all/renamed.py b/crates/ruff_linter/resources/test/fixtures/pyflakes/F401_25__all/renamed.py new file mode 100644 index 0000000000000..7391604add03e --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyflakes/F401_25__all/renamed.py @@ -0,0 +1 @@ +# empty module imported by __init__.py for test fixture diff --git a/crates/ruff_linter/resources/test/fixtures/pyflakes/F401_25__all/unused.py b/crates/ruff_linter/resources/test/fixtures/pyflakes/F401_25__all/unused.py new file mode 100644 index 0000000000000..7391604add03e --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyflakes/F401_25__all/unused.py @@ -0,0 +1 @@ +# empty module imported by __init__.py for test fixture diff --git a/crates/ruff_linter/resources/test/fixtures/pyflakes/F401_25__all/used.py b/crates/ruff_linter/resources/test/fixtures/pyflakes/F401_25__all/used.py new file mode 100644 index 0000000000000..7391604add03e --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyflakes/F401_25__all/used.py @@ -0,0 +1 @@ +# empty module imported by __init__.py for test fixture diff --git a/crates/ruff_linter/src/fix/edits.rs b/crates/ruff_linter/src/fix/edits.rs index 0a70cc4e2327c..3ce660b70ce28 100644 --- a/crates/ruff_linter/src/fix/edits.rs +++ b/crates/ruff_linter/src/fix/edits.rs @@ -122,6 +122,28 @@ pub(crate) fn remove_unused_imports<'a>( } } +/// Edits to make the specified imports explicit, e.g. change `import x` to `import x as x`. +pub(crate) fn make_redundant_alias<'a>( + member_names: impl Iterator, + stmt: &Stmt, +) -> Vec { + let aliases = match stmt { + Stmt::Import(ast::StmtImport { names, .. }) => names, + Stmt::ImportFrom(ast::StmtImportFrom { names, .. }) => names, + _ => { + return Vec::new(); + } + }; + member_names + .filter_map(|name| { + aliases + .iter() + .find(|alias| alias.asname.is_none() && name == alias.name.id) + .map(|alias| Edit::range_replacement(format!("{name} as {name}"), alias.range)) + }) + .collect() +} + #[derive(Debug, Copy, Clone)] pub(crate) enum Parentheses { /// Remove parentheses, if the removed argument is the only argument left. @@ -457,11 +479,12 @@ fn all_lines_fit( mod tests { use anyhow::Result; + use ruff_diagnostics::Edit; use ruff_python_parser::parse_suite; use ruff_source_file::Locator; - use ruff_text_size::{Ranged, TextSize}; + use ruff_text_size::{Ranged, TextRange, TextSize}; - use crate::fix::edits::{next_stmt_break, trailing_semicolon}; + use crate::fix::edits::{make_redundant_alias, next_stmt_break, trailing_semicolon}; #[test] fn find_semicolon() -> Result<()> { @@ -532,4 +555,35 @@ x = 1 \ TextSize::from(12) ); } + + #[test] + fn redundant_alias() { + let contents = "import x, y as y, z as bees"; + let program = parse_suite(contents).unwrap(); + let stmt = program.first().unwrap(); + assert_eq!( + make_redundant_alias(["x"].into_iter(), stmt), + vec![Edit::range_replacement( + String::from("x as x"), + TextRange::new(TextSize::new(7), TextSize::new(8)), + )], + "make just one item redundant" + ); + assert_eq!( + make_redundant_alias(vec!["x", "y"].into_iter(), stmt), + vec![Edit::range_replacement( + String::from("x as x"), + TextRange::new(TextSize::new(7), TextSize::new(8)), + )], + "the second item is already a redundant alias" + ); + assert_eq!( + make_redundant_alias(vec!["x", "z"].into_iter(), stmt), + vec![Edit::range_replacement( + String::from("x as x"), + TextRange::new(TextSize::new(7), TextSize::new(8)), + )], + "the third item is already aliased to something else" + ); + } } diff --git a/crates/ruff_linter/src/rules/pyflakes/mod.rs b/crates/ruff_linter/src/rules/pyflakes/mod.rs index a7736614012f5..6212c18c585ee 100644 --- a/crates/ruff_linter/src/rules/pyflakes/mod.rs +++ b/crates/ruff_linter/src/rules/pyflakes/mod.rs @@ -205,6 +205,9 @@ mod tests { } #[test_case(Rule::UnusedVariable, Path::new("F841_4.py"))] + #[test_case(Rule::UnusedImport, Path::new("__init__.py"))] + #[test_case(Rule::UnusedImport, Path::new("F401_24/__init__.py"))] + #[test_case(Rule::UnusedImport, Path::new("F401_25__all/__init__.py"))] fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!( "preview__{}_{}", @@ -249,19 +252,6 @@ mod tests { Ok(()) } - #[test] - fn init_unused_import_opt_in_to_fix() -> Result<()> { - let diagnostics = test_path( - Path::new("pyflakes/__init__.py"), - &LinterSettings { - ignore_init_module_imports: false, - ..LinterSettings::for_rules(vec![Rule::UnusedImport]) - }, - )?; - assert_messages!(diagnostics); - Ok(()) - } - #[test] fn default_builtins() -> Result<()> { let diagnostics = test_path( diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/unused_import.rs b/crates/ruff_linter/src/rules/pyflakes/rules/unused_import.rs index df446ff608e0c..963bc8dd2e19b 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/unused_import.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/unused_import.rs @@ -1,21 +1,24 @@ use std::borrow::Cow; +use std::iter; -use anyhow::Result; +use anyhow::{anyhow, bail, Result}; use rustc_hash::FxHashMap; use ruff_diagnostics::{Applicability, Diagnostic, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::{Stmt, StmtImportFrom}; use ruff_python_semantic::{AnyImport, Exceptions, Imported, NodeId, Scope}; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; use crate::fix; use crate::registry::Rule; +use crate::rules::{isort, isort::ImportSection, isort::ImportType}; #[derive(Debug, Copy, Clone, Eq, PartialEq)] enum UnusedImportContext { ExceptHandler, - Init, + Init { first_party: bool }, } /// ## What it does @@ -93,7 +96,7 @@ impl Violation for UnusedImport { "`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability" ) } - Some(UnusedImportContext::Init) => { + Some(UnusedImportContext::Init { .. }) => { format!( "`{name}` imported but unused; consider removing, adding to `__all__`, or using a redundant alias" ) @@ -104,14 +107,47 @@ impl Violation for UnusedImport { fn fix_title(&self) -> Option { let UnusedImport { name, multiple, .. } = self; + let resolution = match self.context { + Some(UnusedImportContext::Init { first_party: true }) => "Use a redundant alias", + _ => "Remove unused import", + }; Some(if *multiple { - "Remove unused import".to_string() + resolution.to_string() } else { - format!("Remove unused import: `{name}`") + format!("{resolution}: `{name}`") }) } } +fn is_first_party(qualified_name: &str, level: u32, checker: &Checker) -> bool { + let category = isort::categorize( + qualified_name, + level, + &checker.settings.src, + checker.package(), + checker.settings.isort.detect_same_package, + &checker.settings.isort.known_modules, + checker.settings.target_version, + checker.settings.isort.no_sections, + &checker.settings.isort.section_order, + &checker.settings.isort.default_section, + ); + matches! { + category, + ImportSection::Known(ImportType::FirstParty | ImportType::LocalFolder) + } +} + +/// For some unused binding in an import statement... +/// +/// __init__.py ∧ 1stpty → safe, convert to redundant-alias +/// __init__.py ∧ stdlib → unsafe, remove +/// __init__.py ∧ 3rdpty → unsafe, remove +/// +/// ¬__init__.py ∧ 1stpty → safe, remove +/// ¬__init__.py ∧ stdlib → safe, remove +/// ¬__init__.py ∧ 3rdpty → safe, remove +/// pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut Vec) { // Collect all unused imports by statement. let mut unused: FxHashMap<(NodeId, Exceptions), Vec> = FxHashMap::default(); @@ -160,42 +196,82 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut } let in_init = checker.path().ends_with("__init__.py"); - let fix_init = !checker.settings.ignore_init_module_imports; + let fix_init = checker.settings.preview.is_enabled(); - // Generate a diagnostic for every import, but share a fix across all imports within the same + // Generate a diagnostic for every import, but share fixes across all imports within the same // statement (excluding those that are ignored). - for ((node_id, exceptions), imports) in unused { + for ((import_statement, exceptions), bindings) in unused { let in_except_handler = exceptions.intersects(Exceptions::MODULE_NOT_FOUND_ERROR | Exceptions::IMPORT_ERROR); - let multiple = imports.len() > 1; + let multiple = bindings.len() > 1; + let level = match checker.semantic().statement(import_statement) { + Stmt::Import(_) => 0, + Stmt::ImportFrom(StmtImportFrom { level, .. }) => *level, + _ => { + continue; + } + }; + + // pair each binding with context; divide them by how we want to fix them + let (to_reexport, to_remove): (Vec<_>, Vec<_>) = bindings + .into_iter() + .map(|binding| { + let context = if in_except_handler { + Some(UnusedImportContext::ExceptHandler) + } else if in_init { + Some(UnusedImportContext::Init { + first_party: is_first_party( + &binding.import.qualified_name().to_string(), + level, + checker, + ), + }) + } else { + None + }; + (binding, context) + }) + .partition(|(_, context)| { + matches!( + context, + Some(UnusedImportContext::Init { first_party: true }) + ) + }); - let fix = if (!in_init || fix_init) && !in_except_handler { - fix_imports(checker, node_id, &imports, in_init).ok() + // generate fixes that are shared across bindings in the statement + let (fix_remove, fix_reexport) = if (!in_init || fix_init) && !in_except_handler { + ( + fix_by_removing_imports( + checker, + import_statement, + to_remove.iter().map(|(binding, _)| binding), + in_init, + ) + .ok(), + fix_by_reexporting( + checker, + import_statement, + to_reexport.iter().map(|(binding, _)| binding), + ) + .ok(), + ) } else { - None + (None, None) }; - for ImportBinding { - import, - range, - parent_range, - } in imports - { + for ((binding, context), fix) in iter::Iterator::chain( + iter::zip(to_remove, iter::repeat(fix_remove)), + iter::zip(to_reexport, iter::repeat(fix_reexport)), + ) { let mut diagnostic = Diagnostic::new( UnusedImport { - name: import.qualified_name().to_string(), - context: if in_except_handler { - Some(UnusedImportContext::ExceptHandler) - } else if in_init { - Some(UnusedImportContext::Init) - } else { - None - }, + name: binding.import.qualified_name().to_string(), + context, multiple, }, - range, + binding.range, ); - if let Some(range) = parent_range { + if let Some(range) = binding.parent_range { diagnostic.set_parent(range.start()); } if !in_except_handler { @@ -248,20 +324,22 @@ impl Ranged for ImportBinding<'_> { } /// Generate a [`Fix`] to remove unused imports from a statement. -fn fix_imports( +fn fix_by_removing_imports<'a>( checker: &Checker, node_id: NodeId, - imports: &[ImportBinding], + imports: impl Iterator>, in_init: bool, ) -> Result { let statement = checker.semantic().statement(node_id); let parent = checker.semantic().parent_statement(node_id); let member_names: Vec> = imports - .iter() .map(|ImportBinding { import, .. }| import) .map(Imported::member_name) .collect(); + if member_names.is_empty() { + bail!("Expected import bindings"); + } let edit = fix::edits::remove_unused_imports( member_names.iter().map(AsRef::as_ref), @@ -271,15 +349,43 @@ fn fix_imports( checker.stylist(), checker.indexer(), )?; + // It's unsafe to remove things from `__init__.py` because it can break public interfaces let applicability = if in_init { Applicability::Unsafe } else { Applicability::Safe }; + Ok( Fix::applicable_edit(edit, applicability).isolate(Checker::isolation( checker.semantic().parent_statement_id(node_id), )), ) } + +/// Generate a [`Fix`] to make bindings in a statement explicit, by changing from `import a` to +/// `import a as a`. +fn fix_by_reexporting<'a>( + checker: &Checker, + node_id: NodeId, + imports: impl Iterator>, +) -> Result { + let statement = checker.semantic().statement(node_id); + + let member_names = imports + .map(|binding| binding.import.member_name()) + .collect::>(); + if member_names.is_empty() { + bail!("Expected import bindings"); + } + + let edits = fix::edits::make_redundant_alias(member_names.iter().map(AsRef::as_ref), statement); + + // Only emit a fix if there are edits + let mut tail = edits.into_iter(); + let head = tail.next().ok_or(anyhow!("No edits to make"))?; + + let isolation = Checker::isolation(checker.semantic().parent_statement_id(node_id)); + Ok(Fix::safe_edits(head, tail).isolate(isolation)) +} diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F401_F401_24____init__.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F401_F401_24____init__.py.snap new file mode 100644 index 0000000000000..2a5f8071045ea --- /dev/null +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F401_F401_24____init__.py.snap @@ -0,0 +1,42 @@ +--- +source: crates/ruff_linter/src/rules/pyflakes/mod.rs +--- +__init__.py:19:8: F401 [*] `sys` imported but unused; consider removing, adding to `__all__`, or using a redundant alias + | +19 | import sys # F401: remove unused + | ^^^ F401 + | + = help: Remove unused import: `sys` + +ℹ Unsafe fix +16 16 | import argparse as argparse # Ok: is redundant alias +17 17 | +18 18 | +19 |-import sys # F401: remove unused +20 19 | +21 20 | +22 21 | # first-party + +__init__.py:33:15: F401 [*] `.unused` imported but unused; consider removing, adding to `__all__`, or using a redundant alias + | +33 | from . import unused # F401: change to redundant alias + | ^^^^^^ F401 + | + = help: Use a redundant alias: `.unused` + +ℹ Safe fix +30 30 | from . import aliased as aliased # Ok: is redundant alias +31 31 | +32 32 | +33 |-from . import unused # F401: change to redundant alias + 33 |+from . import unused as unused # F401: change to redundant alias +34 34 | +35 35 | +36 36 | from . import renamed as bees # F401: no fix + +__init__.py:36:26: F401 `.renamed` imported but unused; consider removing, adding to `__all__`, or using a redundant alias + | +36 | from . import renamed as bees # F401: no fix + | ^^^^ F401 + | + = help: Use a redundant alias: `.renamed` diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F401_F401_25__all____init__.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F401_F401_25__all____init__.py.snap new file mode 100644 index 0000000000000..43c550ba55f7d --- /dev/null +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F401_F401_25__all____init__.py.snap @@ -0,0 +1,18 @@ +--- +source: crates/ruff_linter/src/rules/pyflakes/mod.rs +--- +__init__.py:19:8: F401 [*] `sys` imported but unused; consider removing, adding to `__all__`, or using a redundant alias + | +19 | import sys # F401: remove unused + | ^^^ F401 + | + = help: Remove unused import: `sys` + +ℹ Unsafe fix +16 16 | import argparse # Ok: is exported in __all__ +17 17 | +18 18 | +19 |-import sys # F401: remove unused +20 19 | +21 20 | +22 21 | # first-party diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__init_unused_import_opt_in_to_fix.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F401___init__.py.snap similarity index 100% rename from crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__init_unused_import_opt_in_to_fix.snap rename to crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__preview__F401___init__.py.snap