diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 5506e01bc0..4e779154a6 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -3,14 +3,13 @@ use std::path::Path; use itertools::Itertools; use log::error; use ruff_text_size::{TextRange, TextSize}; -use rustc_hash::FxHashMap; use rustpython_format::cformat::{CFormatError, CFormatErrorType}; use rustpython_parser::ast::{ self, Arg, Arguments, Comprehension, Constant, Excepthandler, Expr, ExprContext, Keyword, Operator, Pattern, Ranged, Stmt, Suite, Unaryop, }; -use ruff_diagnostics::{Diagnostic, Fix, IsolationLevel}; +use ruff_diagnostics::{Diagnostic, IsolationLevel}; use ruff_python_ast::all::{extract_all_names, AllNamesFlags}; use ruff_python_ast::helpers::{extract_handled_exceptions, to_module_path}; use ruff_python_ast::source_code::{Generator, Indexer, Locator, Quote, Stylist}; @@ -31,7 +30,6 @@ use ruff_python_semantic::context::ExecutionContext; use ruff_python_semantic::definition::{ContextualizedDefinition, Module, ModuleKind}; use ruff_python_semantic::globals::Globals; use ruff_python_semantic::model::{ResolvedReference, SemanticModel, SemanticModelFlags}; -use ruff_python_semantic::node::NodeId; use ruff_python_semantic::scope::{Scope, ScopeId, ScopeKind}; use ruff_python_stdlib::builtins::{BUILTINS, MAGIC_GLOBALS}; use ruff_python_stdlib::path::is_python_stub_file; @@ -56,7 +54,7 @@ use crate::rules::{ }; use crate::settings::types::PythonVersion; use crate::settings::{flags, Settings}; -use crate::{autofix, docstrings, noqa, warn_user}; +use crate::{docstrings, noqa, warn_user}; mod deferred; @@ -190,6 +188,10 @@ impl<'a> Checker<'a> { self.package } + pub(crate) const fn path(&self) -> &'a Path { + self.path + } + /// Returns whether the given rule should be checked. #[inline] pub(crate) const fn enabled(&self, rule: Rule) -> bool { @@ -1586,7 +1588,7 @@ where if self.enabled(Rule::IterationOverSet) { pylint::rules::iteration_over_set(self, iter); } - if matches!(stmt, Stmt::For(_)) { + if stmt.is_for_stmt() { if self.enabled(Rule::ReimplementedBuiltin) { flake8_simplify::rules::convert_for_loop_to_any_all( self, @@ -5133,7 +5135,7 @@ impl<'a> Checker<'a> { if binding.kind.is_global() { if let Some(source) = binding.source { let stmt = &self.semantic_model.stmts[source]; - if matches!(stmt, Stmt::Global(_)) { + if stmt.is_global_stmt() { diagnostics.push(Diagnostic::new( pylint::rules::GlobalVariableNotAssigned { name: (*name).to_string(), @@ -5232,146 +5234,7 @@ impl<'a> Checker<'a> { } if self.enabled(Rule::UnusedImport) { - // Collect all unused imports by location. (Multiple unused imports at the same - // location indicates an `import from`.) - type UnusedImport<'a> = (&'a str, &'a TextRange); - type BindingContext<'a> = (NodeId, Option, Exceptions); - - let mut unused: FxHashMap> = FxHashMap::default(); - let mut ignored: FxHashMap> = - FxHashMap::default(); - - for binding_id in scope.binding_ids() { - let binding = &self.semantic_model.bindings[binding_id]; - - if binding.is_used() || binding.is_explicit_export() { - continue; - } - - let full_name = match &binding.kind { - BindingKind::Importation(Importation { full_name, .. }) => full_name, - BindingKind::FromImportation(FromImportation { full_name, .. }) => { - full_name.as_str() - } - BindingKind::SubmoduleImportation(SubmoduleImportation { - full_name, - .. - }) => full_name, - _ => continue, - }; - - let stmt_id = binding.source.unwrap(); - let parent_id = self.semantic_model.stmts.parent_id(stmt_id); - - let exceptions = binding.exceptions; - let diagnostic_offset = binding.range.start(); - let stmt = &self.semantic_model.stmts[stmt_id]; - let parent_offset = if matches!(stmt, Stmt::ImportFrom(_)) { - Some(stmt.start()) - } else { - None - }; - - if self.rule_is_ignored(Rule::UnusedImport, diagnostic_offset) - || parent_offset.map_or(false, |parent_offset| { - self.rule_is_ignored(Rule::UnusedImport, parent_offset) - }) - { - ignored - .entry((stmt_id, parent_id, exceptions)) - .or_default() - .push((full_name, &binding.range)); - } else { - unused - .entry((stmt_id, parent_id, exceptions)) - .or_default() - .push((full_name, &binding.range)); - } - } - - let in_init = - self.settings.ignore_init_module_imports && self.path.ends_with("__init__.py"); - for ((stmt_id, parent_id, exceptions), unused_imports) in unused - .into_iter() - .sorted_by_key(|((defined_by, ..), ..)| *defined_by) - { - let stmt = self.semantic_model.stmts[stmt_id]; - let parent = parent_id.map(|parent_id| self.semantic_model.stmts[parent_id]); - let multiple = unused_imports.len() > 1; - let in_except_handler = exceptions - .intersects(Exceptions::MODULE_NOT_FOUND_ERROR | Exceptions::IMPORT_ERROR); - - let fix = if !in_init && !in_except_handler && self.patch(Rule::UnusedImport) { - autofix::edits::remove_unused_imports( - unused_imports.iter().map(|(full_name, _)| *full_name), - stmt, - parent, - self.locator, - self.indexer, - self.stylist, - ) - .ok() - } else { - None - }; - - for (full_name, range) in unused_imports { - let mut diagnostic = Diagnostic::new( - pyflakes::rules::UnusedImport { - name: full_name.to_string(), - context: if in_except_handler { - Some(pyflakes::rules::UnusedImportContext::ExceptHandler) - } else if in_init { - Some(pyflakes::rules::UnusedImportContext::Init) - } else { - None - }, - multiple, - }, - *range, - ); - if matches!(stmt, Stmt::ImportFrom(_)) { - diagnostic.set_parent(stmt.start()); - } - if let Some(edit) = fix.as_ref() { - diagnostic.set_fix(Fix::automatic(edit.clone()).isolate( - parent_id.map_or(IsolationLevel::default(), |node_id| { - IsolationLevel::Group(node_id.into()) - }), - )); - } - diagnostics.push(diagnostic); - } - } - for ((stmt_id, .., exceptions), unused_imports) in ignored - .into_iter() - .sorted_by_key(|((stmt_id, ..), ..)| *stmt_id) - { - let stmt = self.semantic_model.stmts[stmt_id]; - let multiple = unused_imports.len() > 1; - let in_except_handler = exceptions - .intersects(Exceptions::MODULE_NOT_FOUND_ERROR | Exceptions::IMPORT_ERROR); - for (full_name, range) in unused_imports { - let mut diagnostic = Diagnostic::new( - pyflakes::rules::UnusedImport { - name: full_name.to_string(), - context: if in_except_handler { - Some(pyflakes::rules::UnusedImportContext::ExceptHandler) - } else if in_init { - Some(pyflakes::rules::UnusedImportContext::Init) - } else { - None - }, - multiple, - }, - *range, - ); - if matches!(stmt, Stmt::ImportFrom(_)) { - diagnostic.set_parent(stmt.start()); - } - diagnostics.push(diagnostic); - } - } + pyflakes::rules::unused_import(self, scope, &mut diagnostics); } } self.diagnostics.extend(diagnostics); diff --git a/crates/ruff/src/message/mod.rs b/crates/ruff/src/message/mod.rs index 2765347e16..072bf79fae 100644 --- a/crates/ruff/src/message/mod.rs +++ b/crates/ruff/src/message/mod.rs @@ -6,7 +6,6 @@ use std::ops::Deref; use ruff_text_size::{TextRange, TextSize}; use rustc_hash::FxHashMap; -use crate::jupyter::JupyterIndex; pub use azure::AzureEmitter; pub use github::GithubEmitter; pub use gitlab::GitlabEmitter; @@ -18,6 +17,8 @@ use ruff_diagnostics::{Diagnostic, DiagnosticKind, Fix}; use ruff_python_ast::source_code::{SourceFile, SourceLocation}; pub use text::TextEmitter; +use crate::jupyter::JupyterIndex; + mod azure; mod diff; mod github; @@ -150,11 +151,10 @@ mod tests { use ruff_text_size::{TextRange, TextSize}; use rustc_hash::FxHashMap; - use ruff_diagnostics::{Diagnostic, Edit, Fix}; + use ruff_diagnostics::{Diagnostic, DiagnosticKind, Edit, Fix}; use ruff_python_ast::source_code::SourceFileBuilder; use crate::message::{Emitter, EmitterContext, Message}; - use crate::rules::pyflakes::rules::{UndefinedName, UnusedImport, UnusedVariable}; pub(super) fn create_messages() -> Vec { let fib = r#"import os @@ -172,10 +172,10 @@ def fibonacci(n): "#; let unused_import = Diagnostic::new( - UnusedImport { - name: "os".to_string(), - context: None, - multiple: false, + DiagnosticKind { + name: "UnusedImport".to_string(), + body: "`os` imported but unused".to_string(), + suggestion: Some("Remove unused import: `os`".to_string()), }, TextRange::new(TextSize::from(7), TextSize::from(9)), ) @@ -187,8 +187,10 @@ def fibonacci(n): let fib_source = SourceFileBuilder::new("fib.py", fib).finish(); let unused_variable = Diagnostic::new( - UnusedVariable { - name: "x".to_string(), + DiagnosticKind { + name: "UnusedVariable".to_string(), + body: "Local variable `x` is assigned to but never used".to_string(), + suggestion: Some("Remove assignment to unused variable `x`".to_string()), }, TextRange::new(TextSize::from(94), TextSize::from(95)), ) @@ -200,8 +202,10 @@ def fibonacci(n): let file_2 = r#"if a == 1: pass"#; let undefined_name = Diagnostic::new( - UndefinedName { - name: "a".to_string(), + DiagnosticKind { + name: "UndefinedName".to_string(), + body: "Undefined name `a`".to_string(), + suggestion: None, }, TextRange::new(TextSize::from(3), TextSize::from(4)), ); diff --git a/crates/ruff/src/rules/pyflakes/rules/future_feature_not_defined.rs b/crates/ruff/src/rules/pyflakes/rules/future_feature_not_defined.rs new file mode 100644 index 0000000000..9e234ed1a8 --- /dev/null +++ b/crates/ruff/src/rules/pyflakes/rules/future_feature_not_defined.rs @@ -0,0 +1,41 @@ +use rustpython_parser::ast::{Alias, Ranged}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_stdlib::future::ALL_FEATURE_NAMES; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for `__future__` imports that are not defined in the current Python +/// version. +/// +/// ## Why is this bad? +/// Importing undefined or unsupported members from the `__future__` module is +/// a `SyntaxError`. +/// +/// ## References +/// - [Python documentation](https://docs.python.org/3/library/__future__.html) +#[violation] +pub struct FutureFeatureNotDefined { + name: String, +} + +impl Violation for FutureFeatureNotDefined { + #[derive_message_formats] + fn message(&self) -> String { + let FutureFeatureNotDefined { name } = self; + format!("Future feature `{name}` is not defined") + } +} + +pub(crate) fn future_feature_not_defined(checker: &mut Checker, alias: &Alias) { + if !ALL_FEATURE_NAMES.contains(&alias.name.as_str()) { + checker.diagnostics.push(Diagnostic::new( + FutureFeatureNotDefined { + name: alias.name.to_string(), + }, + alias.range(), + )); + } +} diff --git a/crates/ruff/src/rules/pyflakes/rules/imports.rs b/crates/ruff/src/rules/pyflakes/rules/imports.rs index a61ae7d4da..220925b709 100644 --- a/crates/ruff/src/rules/pyflakes/rules/imports.rs +++ b/crates/ruff/src/rules/pyflakes/rules/imports.rs @@ -1,101 +1,8 @@ use itertools::Itertools; -use rustpython_parser::ast::{Alias, Ranged}; -use ruff_diagnostics::Diagnostic; -use ruff_diagnostics::{AutofixKind, Violation}; +use ruff_diagnostics::Violation; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::source_code::OneIndexed; -use ruff_python_stdlib::future::ALL_FEATURE_NAMES; - -use crate::checkers::ast::Checker; - -#[derive(Debug, PartialEq, Eq, Copy, Clone)] -pub(crate) enum UnusedImportContext { - ExceptHandler, - Init, -} - -/// ## What it does -/// Checks for unused imports. -/// -/// ## Why is this bad? -/// Unused imports add a performance overhead at runtime, and risk creating -/// import cycles. They also increase the cognitive load of reading the code. -/// -/// If an import statement is used to check for the availability or existence -/// of a module, consider using `importlib.util.find_spec` instead. -/// -/// ## Options -/// -/// - `pyflakes.extend-generics` -/// -/// ## Example -/// ```python -/// import numpy as np # unused import -/// -/// -/// def area(radius): -/// return 3.14 * radius**2 -/// ``` -/// -/// Use instead: -/// ```python -/// def area(radius): -/// return 3.14 * radius**2 -/// ``` -/// -/// To check the availability of a module, use `importlib.util.find_spec`: -/// ```python -/// from importlib.util import find_spec -/// -/// if find_spec("numpy") is not None: -/// print("numpy is installed") -/// else: -/// print("numpy is not installed") -/// ``` -/// -/// ## References -/// - [Python documentation](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement) -/// - [Python documentation](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec) -#[violation] -pub struct UnusedImport { - pub(crate) name: String, - pub(crate) context: Option, - pub(crate) multiple: bool, -} - -impl Violation for UnusedImport { - const AUTOFIX: AutofixKind = AutofixKind::Sometimes; - - #[derive_message_formats] - fn message(&self) -> String { - let UnusedImport { name, context, .. } = self; - match context { - Some(UnusedImportContext::ExceptHandler) => { - format!( - "`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability" - ) - } - Some(UnusedImportContext::Init) => { - format!( - "`{name}` imported but unused; consider adding to `__all__` or using a redundant \ - alias" - ) - } - None => format!("`{name}` imported but unused"), - } - } - - fn autofix_title(&self) -> Option { - let UnusedImport { name, multiple, .. } = self; - - Some(if *multiple { - "Remove unused import".to_string() - } else { - format!("Remove unused import: `{name}`") - }) - } -} /// ## What it does /// Checks for import bindings that are shadowed by loop variables. @@ -308,37 +215,3 @@ impl Violation for UndefinedLocalWithNestedImportStarUsage { format!("`from {name} import *` only allowed at module level") } } - -/// ## What it does -/// Checks for `__future__` imports that are not defined in the current Python -/// version. -/// -/// ## Why is this bad? -/// Importing undefined or unsupported members from the `__future__` module is -/// a `SyntaxError`. -/// -/// ## References -/// - [Python documentation](https://docs.python.org/3/library/__future__.html) -#[violation] -pub struct FutureFeatureNotDefined { - name: String, -} - -impl Violation for FutureFeatureNotDefined { - #[derive_message_formats] - fn message(&self) -> String { - let FutureFeatureNotDefined { name } = self; - format!("Future feature `{name}` is not defined") - } -} - -pub(crate) fn future_feature_not_defined(checker: &mut Checker, alias: &Alias) { - if !ALL_FEATURE_NAMES.contains(&alias.name.as_str()) { - checker.diagnostics.push(Diagnostic::new( - FutureFeatureNotDefined { - name: alias.name.to_string(), - }, - alias.range(), - )); - } -} diff --git a/crates/ruff/src/rules/pyflakes/rules/mod.rs b/crates/ruff/src/rules/pyflakes/rules/mod.rs index 530a920a56..3a66bb0e8f 100644 --- a/crates/ruff/src/rules/pyflakes/rules/mod.rs +++ b/crates/ruff/src/rules/pyflakes/rules/mod.rs @@ -6,11 +6,11 @@ pub(crate) use f_string_missing_placeholders::{ f_string_missing_placeholders, FStringMissingPlaceholders, }; pub(crate) use forward_annotation_syntax_error::ForwardAnnotationSyntaxError; +pub(crate) use future_feature_not_defined::{future_feature_not_defined, FutureFeatureNotDefined}; pub(crate) use if_tuple::{if_tuple, IfTuple}; pub(crate) use imports::{ - future_feature_not_defined, FutureFeatureNotDefined, ImportShadowedByLoopVar, LateFutureImport, - UndefinedLocalWithImportStar, UndefinedLocalWithImportStarUsage, - UndefinedLocalWithNestedImportStarUsage, UnusedImport, UnusedImportContext, + ImportShadowedByLoopVar, LateFutureImport, UndefinedLocalWithImportStar, + UndefinedLocalWithImportStarUsage, UndefinedLocalWithNestedImportStarUsage, }; pub(crate) use invalid_literal_comparisons::{invalid_literal_comparison, IsLiteral}; pub(crate) use invalid_print_syntax::{invalid_print_syntax, InvalidPrintSyntax}; @@ -41,6 +41,7 @@ pub(crate) use undefined_export::{undefined_export, UndefinedExport}; pub(crate) use undefined_local::{undefined_local, UndefinedLocal}; pub(crate) use undefined_name::UndefinedName; pub(crate) use unused_annotation::{unused_annotation, UnusedAnnotation}; +pub(crate) use unused_import::{unused_import, UnusedImport}; pub(crate) use unused_variable::{unused_variable, UnusedVariable}; pub(crate) use yield_outside_function::{yield_outside_function, YieldOutsideFunction}; @@ -50,6 +51,7 @@ mod continue_outside_loop; mod default_except_not_last; mod f_string_missing_placeholders; mod forward_annotation_syntax_error; +mod future_feature_not_defined; mod if_tuple; mod imports; mod invalid_literal_comparisons; @@ -64,5 +66,6 @@ mod undefined_export; mod undefined_local; mod undefined_name; mod unused_annotation; +mod unused_import; mod unused_variable; mod yield_outside_function; diff --git a/crates/ruff/src/rules/pyflakes/rules/undefined_name.rs b/crates/ruff/src/rules/pyflakes/rules/undefined_name.rs index 05b6109a9b..a4619cd618 100644 --- a/crates/ruff/src/rules/pyflakes/rules/undefined_name.rs +++ b/crates/ruff/src/rules/pyflakes/rules/undefined_name.rs @@ -23,7 +23,7 @@ use ruff_macros::{derive_message_formats, violation}; /// - [Python documentation](https://docs.python.org/3/reference/executionmodel.html#naming-and-binding) #[violation] pub struct UndefinedName { - pub name: String, + pub(crate) name: String, } impl Violation for UndefinedName { diff --git a/crates/ruff/src/rules/pyflakes/rules/unused_import.rs b/crates/ruff/src/rules/pyflakes/rules/unused_import.rs new file mode 100644 index 0000000000..0e1fd4a4e4 --- /dev/null +++ b/crates/ruff/src/rules/pyflakes/rules/unused_import.rs @@ -0,0 +1,246 @@ +use itertools::Itertools; +use ruff_text_size::TextRange; +use rustc_hash::FxHashMap; +use rustpython_parser::ast::Ranged; + +use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, IsolationLevel, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_semantic::binding::{ + BindingKind, Exceptions, FromImportation, Importation, SubmoduleImportation, +}; +use ruff_python_semantic::node::NodeId; +use ruff_python_semantic::scope::Scope; + +use crate::autofix; +use crate::checkers::ast::Checker; +use crate::registry::Rule; + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +enum UnusedImportContext { + ExceptHandler, + Init, +} + +/// ## What it does +/// Checks for unused imports. +/// +/// ## Why is this bad? +/// Unused imports add a performance overhead at runtime, and risk creating +/// import cycles. They also increase the cognitive load of reading the code. +/// +/// If an import statement is used to check for the availability or existence +/// of a module, consider using `importlib.util.find_spec` instead. +/// +/// ## Options +/// +/// - `pyflakes.extend-generics` +/// +/// ## Example +/// ```python +/// import numpy as np # unused import +/// +/// +/// def area(radius): +/// return 3.14 * radius**2 +/// ``` +/// +/// Use instead: +/// ```python +/// def area(radius): +/// return 3.14 * radius**2 +/// ``` +/// +/// To check the availability of a module, use `importlib.util.find_spec`: +/// ```python +/// from importlib.util import find_spec +/// +/// if find_spec("numpy") is not None: +/// print("numpy is installed") +/// else: +/// print("numpy is not installed") +/// ``` +/// +/// ## References +/// - [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement) +/// - [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec) +#[violation] +pub struct UnusedImport { + name: String, + context: Option, + multiple: bool, +} + +impl Violation for UnusedImport { + const AUTOFIX: AutofixKind = AutofixKind::Sometimes; + + #[derive_message_formats] + fn message(&self) -> String { + let UnusedImport { name, context, .. } = self; + match context { + Some(UnusedImportContext::ExceptHandler) => { + format!( + "`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability" + ) + } + Some(UnusedImportContext::Init) => { + format!( + "`{name}` imported but unused; consider adding to `__all__` or using a redundant \ + alias" + ) + } + None => format!("`{name}` imported but unused"), + } + } + + fn autofix_title(&self) -> Option { + let UnusedImport { name, multiple, .. } = self; + Some(if *multiple { + "Remove unused import".to_string() + } else { + format!("Remove unused import: `{name}`") + }) + } +} + +type SpannedName<'a> = (&'a str, &'a TextRange); +type BindingContext<'a> = (NodeId, Option, Exceptions); + +pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut Vec) { + // Collect all unused imports by statement. + let mut unused: FxHashMap> = FxHashMap::default(); + let mut ignored: FxHashMap> = FxHashMap::default(); + + for binding_id in scope.binding_ids() { + let binding = &checker.semantic_model().bindings[binding_id]; + + if binding.is_used() || binding.is_explicit_export() { + continue; + } + + let full_name = match &binding.kind { + BindingKind::Importation(Importation { full_name, .. }) => full_name, + BindingKind::FromImportation(FromImportation { full_name, .. }) => full_name.as_str(), + BindingKind::SubmoduleImportation(SubmoduleImportation { full_name, .. }) => full_name, + _ => continue, + }; + + let stmt_id = binding.source.unwrap(); + let parent_id = checker.semantic_model().stmts.parent_id(stmt_id); + + let exceptions = binding.exceptions; + let diagnostic_offset = binding.range.start(); + let stmt = &checker.semantic_model().stmts[stmt_id]; + let parent_offset = if stmt.is_import_from_stmt() { + Some(stmt.start()) + } else { + None + }; + + if checker.rule_is_ignored(Rule::UnusedImport, diagnostic_offset) + || parent_offset.map_or(false, |parent_offset| { + checker.rule_is_ignored(Rule::UnusedImport, parent_offset) + }) + { + ignored + .entry((stmt_id, parent_id, exceptions)) + .or_default() + .push((full_name, &binding.range)); + } else { + unused + .entry((stmt_id, parent_id, exceptions)) + .or_default() + .push((full_name, &binding.range)); + } + } + + let in_init = + checker.settings.ignore_init_module_imports && checker.path().ends_with("__init__.py"); + + // Generate a diagnostic for every unused import, but share a fix across all unused imports + // within the same statement (excluding those that are ignored). + for ((stmt_id, parent_id, exceptions), unused_imports) in unused + .into_iter() + .sorted_by_key(|((defined_by, ..), ..)| *defined_by) + { + let stmt = checker.semantic_model().stmts[stmt_id]; + let parent = parent_id.map(|parent_id| checker.semantic_model().stmts[parent_id]); + let multiple = unused_imports.len() > 1; + let in_except_handler = + exceptions.intersects(Exceptions::MODULE_NOT_FOUND_ERROR | Exceptions::IMPORT_ERROR); + + let fix = if !in_init && !in_except_handler && checker.patch(Rule::UnusedImport) { + autofix::edits::remove_unused_imports( + unused_imports.iter().map(|(full_name, _)| *full_name), + stmt, + parent, + checker.locator, + checker.indexer, + checker.stylist, + ) + .ok() + } else { + None + }; + + for (full_name, range) in unused_imports { + let mut diagnostic = Diagnostic::new( + UnusedImport { + name: full_name.to_string(), + context: if in_except_handler { + Some(UnusedImportContext::ExceptHandler) + } else if in_init { + Some(UnusedImportContext::Init) + } else { + None + }, + multiple, + }, + *range, + ); + if stmt.is_import_from_stmt() { + diagnostic.set_parent(stmt.start()); + } + if let Some(edit) = fix.as_ref() { + diagnostic.set_fix(Fix::automatic(edit.clone()).isolate( + parent_id.map_or(IsolationLevel::default(), |node_id| { + IsolationLevel::Group(node_id.into()) + }), + )); + } + diagnostics.push(diagnostic); + } + } + + // Separately, generate a diagnostic for every _ignored_ unused import, but don't bother + // creating a fix. We have to generate these diagnostics, even though they'll be ignored later + // on, so that the suppression comments themselves aren't marked as unnecessary. + for ((stmt_id, .., exceptions), unused_imports) in ignored + .into_iter() + .sorted_by_key(|((stmt_id, ..), ..)| *stmt_id) + { + let stmt = checker.semantic_model().stmts[stmt_id]; + let multiple = unused_imports.len() > 1; + let in_except_handler = + exceptions.intersects(Exceptions::MODULE_NOT_FOUND_ERROR | Exceptions::IMPORT_ERROR); + for (full_name, range) in unused_imports { + let mut diagnostic = Diagnostic::new( + UnusedImport { + name: full_name.to_string(), + context: if in_except_handler { + Some(UnusedImportContext::ExceptHandler) + } else if in_init { + Some(UnusedImportContext::Init) + } else { + None + }, + multiple, + }, + *range, + ); + if stmt.is_import_from_stmt() { + diagnostic.set_parent(stmt.start()); + } + diagnostics.push(diagnostic); + } + } +}