Move unused imports rule into its own module (#4795)

This commit is contained in:
Charlie Marsh 2023-06-02 00:27:23 -04:00 committed by GitHub
parent 10ba79489a
commit b030c70dda
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 319 additions and 289 deletions

View file

@ -3,14 +3,13 @@ use std::path::Path;
use itertools::Itertools; use itertools::Itertools;
use log::error; use log::error;
use ruff_text_size::{TextRange, TextSize}; use ruff_text_size::{TextRange, TextSize};
use rustc_hash::FxHashMap;
use rustpython_format::cformat::{CFormatError, CFormatErrorType}; use rustpython_format::cformat::{CFormatError, CFormatErrorType};
use rustpython_parser::ast::{ use rustpython_parser::ast::{
self, Arg, Arguments, Comprehension, Constant, Excepthandler, Expr, ExprContext, Keyword, self, Arg, Arguments, Comprehension, Constant, Excepthandler, Expr, ExprContext, Keyword,
Operator, Pattern, Ranged, Stmt, Suite, Unaryop, 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::all::{extract_all_names, AllNamesFlags};
use ruff_python_ast::helpers::{extract_handled_exceptions, to_module_path}; use ruff_python_ast::helpers::{extract_handled_exceptions, to_module_path};
use ruff_python_ast::source_code::{Generator, Indexer, Locator, Quote, Stylist}; 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::definition::{ContextualizedDefinition, Module, ModuleKind};
use ruff_python_semantic::globals::Globals; use ruff_python_semantic::globals::Globals;
use ruff_python_semantic::model::{ResolvedReference, SemanticModel, SemanticModelFlags}; 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_semantic::scope::{Scope, ScopeId, ScopeKind};
use ruff_python_stdlib::builtins::{BUILTINS, MAGIC_GLOBALS}; use ruff_python_stdlib::builtins::{BUILTINS, MAGIC_GLOBALS};
use ruff_python_stdlib::path::is_python_stub_file; use ruff_python_stdlib::path::is_python_stub_file;
@ -56,7 +54,7 @@ use crate::rules::{
}; };
use crate::settings::types::PythonVersion; use crate::settings::types::PythonVersion;
use crate::settings::{flags, Settings}; use crate::settings::{flags, Settings};
use crate::{autofix, docstrings, noqa, warn_user}; use crate::{docstrings, noqa, warn_user};
mod deferred; mod deferred;
@ -190,6 +188,10 @@ impl<'a> Checker<'a> {
self.package self.package
} }
pub(crate) const fn path(&self) -> &'a Path {
self.path
}
/// Returns whether the given rule should be checked. /// Returns whether the given rule should be checked.
#[inline] #[inline]
pub(crate) const fn enabled(&self, rule: Rule) -> bool { pub(crate) const fn enabled(&self, rule: Rule) -> bool {
@ -1586,7 +1588,7 @@ where
if self.enabled(Rule::IterationOverSet) { if self.enabled(Rule::IterationOverSet) {
pylint::rules::iteration_over_set(self, iter); pylint::rules::iteration_over_set(self, iter);
} }
if matches!(stmt, Stmt::For(_)) { if stmt.is_for_stmt() {
if self.enabled(Rule::ReimplementedBuiltin) { if self.enabled(Rule::ReimplementedBuiltin) {
flake8_simplify::rules::convert_for_loop_to_any_all( flake8_simplify::rules::convert_for_loop_to_any_all(
self, self,
@ -5133,7 +5135,7 @@ impl<'a> Checker<'a> {
if binding.kind.is_global() { if binding.kind.is_global() {
if let Some(source) = binding.source { if let Some(source) = binding.source {
let stmt = &self.semantic_model.stmts[source]; let stmt = &self.semantic_model.stmts[source];
if matches!(stmt, Stmt::Global(_)) { if stmt.is_global_stmt() {
diagnostics.push(Diagnostic::new( diagnostics.push(Diagnostic::new(
pylint::rules::GlobalVariableNotAssigned { pylint::rules::GlobalVariableNotAssigned {
name: (*name).to_string(), name: (*name).to_string(),
@ -5232,146 +5234,7 @@ impl<'a> Checker<'a> {
} }
if self.enabled(Rule::UnusedImport) { if self.enabled(Rule::UnusedImport) {
// Collect all unused imports by location. (Multiple unused imports at the same pyflakes::rules::unused_import(self, scope, &mut diagnostics);
// location indicates an `import from`.)
type UnusedImport<'a> = (&'a str, &'a TextRange);
type BindingContext<'a> = (NodeId, Option<NodeId>, Exceptions);
let mut unused: FxHashMap<BindingContext, Vec<UnusedImport>> = FxHashMap::default();
let mut ignored: FxHashMap<BindingContext, Vec<UnusedImport>> =
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);
}
}
} }
} }
self.diagnostics.extend(diagnostics); self.diagnostics.extend(diagnostics);

View file

@ -6,7 +6,6 @@ use std::ops::Deref;
use ruff_text_size::{TextRange, TextSize}; use ruff_text_size::{TextRange, TextSize};
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use crate::jupyter::JupyterIndex;
pub use azure::AzureEmitter; pub use azure::AzureEmitter;
pub use github::GithubEmitter; pub use github::GithubEmitter;
pub use gitlab::GitlabEmitter; pub use gitlab::GitlabEmitter;
@ -18,6 +17,8 @@ use ruff_diagnostics::{Diagnostic, DiagnosticKind, Fix};
use ruff_python_ast::source_code::{SourceFile, SourceLocation}; use ruff_python_ast::source_code::{SourceFile, SourceLocation};
pub use text::TextEmitter; pub use text::TextEmitter;
use crate::jupyter::JupyterIndex;
mod azure; mod azure;
mod diff; mod diff;
mod github; mod github;
@ -150,11 +151,10 @@ mod tests {
use ruff_text_size::{TextRange, TextSize}; use ruff_text_size::{TextRange, TextSize};
use rustc_hash::FxHashMap; 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 ruff_python_ast::source_code::SourceFileBuilder;
use crate::message::{Emitter, EmitterContext, Message}; use crate::message::{Emitter, EmitterContext, Message};
use crate::rules::pyflakes::rules::{UndefinedName, UnusedImport, UnusedVariable};
pub(super) fn create_messages() -> Vec<Message> { pub(super) fn create_messages() -> Vec<Message> {
let fib = r#"import os let fib = r#"import os
@ -172,10 +172,10 @@ def fibonacci(n):
"#; "#;
let unused_import = Diagnostic::new( let unused_import = Diagnostic::new(
UnusedImport { DiagnosticKind {
name: "os".to_string(), name: "UnusedImport".to_string(),
context: None, body: "`os` imported but unused".to_string(),
multiple: false, suggestion: Some("Remove unused import: `os`".to_string()),
}, },
TextRange::new(TextSize::from(7), TextSize::from(9)), 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 fib_source = SourceFileBuilder::new("fib.py", fib).finish();
let unused_variable = Diagnostic::new( let unused_variable = Diagnostic::new(
UnusedVariable { DiagnosticKind {
name: "x".to_string(), 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)), TextRange::new(TextSize::from(94), TextSize::from(95)),
) )
@ -200,8 +202,10 @@ def fibonacci(n):
let file_2 = r#"if a == 1: pass"#; let file_2 = r#"if a == 1: pass"#;
let undefined_name = Diagnostic::new( let undefined_name = Diagnostic::new(
UndefinedName { DiagnosticKind {
name: "a".to_string(), name: "UndefinedName".to_string(),
body: "Undefined name `a`".to_string(),
suggestion: None,
}, },
TextRange::new(TextSize::from(3), TextSize::from(4)), TextRange::new(TextSize::from(3), TextSize::from(4)),
); );

View file

@ -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(),
));
}
}

View file

@ -1,101 +1,8 @@
use itertools::Itertools; use itertools::Itertools;
use rustpython_parser::ast::{Alias, Ranged};
use ruff_diagnostics::Diagnostic; use ruff_diagnostics::Violation;
use ruff_diagnostics::{AutofixKind, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::source_code::OneIndexed; 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<UnusedImportContext>,
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<String> {
let UnusedImport { name, multiple, .. } = self;
Some(if *multiple {
"Remove unused import".to_string()
} else {
format!("Remove unused import: `{name}`")
})
}
}
/// ## What it does /// ## What it does
/// Checks for import bindings that are shadowed by loop variables. /// 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") 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(),
));
}
}

View file

@ -6,11 +6,11 @@ pub(crate) use f_string_missing_placeholders::{
f_string_missing_placeholders, FStringMissingPlaceholders, f_string_missing_placeholders, FStringMissingPlaceholders,
}; };
pub(crate) use forward_annotation_syntax_error::ForwardAnnotationSyntaxError; 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 if_tuple::{if_tuple, IfTuple};
pub(crate) use imports::{ pub(crate) use imports::{
future_feature_not_defined, FutureFeatureNotDefined, ImportShadowedByLoopVar, LateFutureImport, ImportShadowedByLoopVar, LateFutureImport, UndefinedLocalWithImportStar,
UndefinedLocalWithImportStar, UndefinedLocalWithImportStarUsage, UndefinedLocalWithImportStarUsage, UndefinedLocalWithNestedImportStarUsage,
UndefinedLocalWithNestedImportStarUsage, UnusedImport, UnusedImportContext,
}; };
pub(crate) use invalid_literal_comparisons::{invalid_literal_comparison, IsLiteral}; pub(crate) use invalid_literal_comparisons::{invalid_literal_comparison, IsLiteral};
pub(crate) use invalid_print_syntax::{invalid_print_syntax, InvalidPrintSyntax}; 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_local::{undefined_local, UndefinedLocal};
pub(crate) use undefined_name::UndefinedName; pub(crate) use undefined_name::UndefinedName;
pub(crate) use unused_annotation::{unused_annotation, UnusedAnnotation}; 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 unused_variable::{unused_variable, UnusedVariable};
pub(crate) use yield_outside_function::{yield_outside_function, YieldOutsideFunction}; pub(crate) use yield_outside_function::{yield_outside_function, YieldOutsideFunction};
@ -50,6 +51,7 @@ mod continue_outside_loop;
mod default_except_not_last; mod default_except_not_last;
mod f_string_missing_placeholders; mod f_string_missing_placeholders;
mod forward_annotation_syntax_error; mod forward_annotation_syntax_error;
mod future_feature_not_defined;
mod if_tuple; mod if_tuple;
mod imports; mod imports;
mod invalid_literal_comparisons; mod invalid_literal_comparisons;
@ -64,5 +66,6 @@ mod undefined_export;
mod undefined_local; mod undefined_local;
mod undefined_name; mod undefined_name;
mod unused_annotation; mod unused_annotation;
mod unused_import;
mod unused_variable; mod unused_variable;
mod yield_outside_function; mod yield_outside_function;

View file

@ -23,7 +23,7 @@ use ruff_macros::{derive_message_formats, violation};
/// - [Python documentation](https://docs.python.org/3/reference/executionmodel.html#naming-and-binding) /// - [Python documentation](https://docs.python.org/3/reference/executionmodel.html#naming-and-binding)
#[violation] #[violation]
pub struct UndefinedName { pub struct UndefinedName {
pub name: String, pub(crate) name: String,
} }
impl Violation for UndefinedName { impl Violation for UndefinedName {

View file

@ -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<UnusedImportContext>,
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<String> {
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<NodeId>, Exceptions);
pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut Vec<Diagnostic>) {
// Collect all unused imports by statement.
let mut unused: FxHashMap<BindingContext, Vec<SpannedName>> = FxHashMap::default();
let mut ignored: FxHashMap<BindingContext, Vec<SpannedName>> = 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);
}
}
}