mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 05:15:12 +00:00
Move unused imports rule into its own module (#4795)
This commit is contained in:
parent
10ba79489a
commit
b030c70dda
7 changed files with 319 additions and 289 deletions
|
@ -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<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);
|
||||
}
|
||||
}
|
||||
pyflakes::rules::unused_import(self, scope, &mut diagnostics);
|
||||
}
|
||||
}
|
||||
self.diagnostics.extend(diagnostics);
|
||||
|
|
|
@ -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<Message> {
|
||||
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)),
|
||||
);
|
||||
|
|
|
@ -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(),
|
||||
));
|
||||
}
|
||||
}
|
|
@ -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<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
|
||||
/// 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(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
246
crates/ruff/src/rules/pyflakes/rules/unused_import.rs
Normal file
246
crates/ruff/src/rules/pyflakes/rules/unused_import.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue