mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-02 22:55:08 +00:00
Automatically remove unused imports (#298)
This commit is contained in:
parent
bc335f839e
commit
b049cced04
11 changed files with 467 additions and 65 deletions
14
resources/test/fixtures/F401.py
vendored
14
resources/test/fixtures/F401.py
vendored
|
@ -30,6 +30,17 @@ if TYPE_CHECKING:
|
||||||
from models import Fruit, Nut, Vegetable
|
from models import Fruit, Nut, Vegetable
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
import shelve
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
"""Hello, world!"""
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
z = 1
|
||||||
|
|
||||||
|
|
||||||
class X:
|
class X:
|
||||||
datetime: datetime
|
datetime: datetime
|
||||||
foo: Type["NamedTuple"]
|
foo: Type["NamedTuple"]
|
||||||
|
@ -39,6 +50,9 @@ class X:
|
||||||
y = Counter()
|
y = Counter()
|
||||||
z = multiprocessing.pool.ThreadPool()
|
z = multiprocessing.pool.ThreadPool()
|
||||||
|
|
||||||
|
def b(self) -> None:
|
||||||
|
import pickle
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["ClassA"] + ["ClassB"]
|
__all__ = ["ClassA"] + ["ClassB"]
|
||||||
__all__ += ["ClassC"]
|
__all__ += ["ClassC"]
|
||||||
|
|
|
@ -2,7 +2,10 @@ from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from models import Fruit, Nut
|
from models import (
|
||||||
|
Fruit,
|
||||||
|
Nut,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
|
@ -99,7 +99,7 @@ pub fn check_unused_variables(
|
||||||
{
|
{
|
||||||
checks.push(Check::new(
|
checks.push(Check::new(
|
||||||
CheckKind::UnusedVariable(name.to_string()),
|
CheckKind::UnusedVariable(name.to_string()),
|
||||||
locator.locate_check(binding.location),
|
locator.locate_check(binding.range),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use rustpython_parser::ast::{Constant, Expr, ExprKind, Location, Stmt, StmtKind};
|
use rustpython_parser::ast::{Constant, Expr, ExprKind, Location, Stmt, StmtKind};
|
||||||
|
|
||||||
use crate::ast::types::{BindingKind, Scope};
|
use crate::ast::types::{BindingKind, Range, Scope};
|
||||||
|
|
||||||
/// Extract the names bound to a given __all__ assignment.
|
/// Extract the names bound to a given __all__ assignment.
|
||||||
pub fn extract_all_names(stmt: &Stmt, scope: &Scope) -> Vec<String> {
|
pub fn extract_all_names(stmt: &Stmt, scope: &Scope) -> Vec<String> {
|
||||||
|
@ -148,7 +148,7 @@ impl<'a> SourceCodeLocator<'a> {
|
||||||
&self.content[offset..]
|
&self.content[offset..]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn slice_source_code_range(&mut self, start: &Location, end: &Location) -> &'a str {
|
pub fn slice_source_code_range(&mut self, range: &Range) -> &'a str {
|
||||||
if !self.initialized {
|
if !self.initialized {
|
||||||
let mut offset = 0;
|
let mut offset = 0;
|
||||||
for i in self.content.lines() {
|
for i in self.content.lines() {
|
||||||
|
@ -158,8 +158,8 @@ impl<'a> SourceCodeLocator<'a> {
|
||||||
}
|
}
|
||||||
self.initialized = true;
|
self.initialized = true;
|
||||||
}
|
}
|
||||||
let start = self.offsets[start.row() - 1] + start.column() - 1;
|
let start = self.offsets[range.location.row() - 1] + range.location.column() - 1;
|
||||||
let end = self.offsets[end.row() - 1] + end.column() - 1;
|
let end = self.offsets[range.end_location.row() - 1] + range.end_location.column() - 1;
|
||||||
&self.content[start..end]
|
&self.content[start..end]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ fn id() -> usize {
|
||||||
COUNTER.fetch_add(1, Ordering::Relaxed)
|
COUNTER.fetch_add(1, Ordering::Relaxed)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default)]
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub struct Range {
|
pub struct Range {
|
||||||
pub location: Location,
|
pub location: Location,
|
||||||
pub end_location: Location,
|
pub end_location: Location,
|
||||||
|
@ -55,6 +55,12 @@ impl Scope {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct BindingContext {
|
||||||
|
pub defined_by: usize,
|
||||||
|
pub defined_in: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum BindingKind {
|
pub enum BindingKind {
|
||||||
Annotation,
|
Annotation,
|
||||||
|
@ -67,15 +73,16 @@ pub enum BindingKind {
|
||||||
Definition,
|
Definition,
|
||||||
Export(Vec<String>),
|
Export(Vec<String>),
|
||||||
FutureImportation,
|
FutureImportation,
|
||||||
Importation(String),
|
|
||||||
StarImportation,
|
StarImportation,
|
||||||
SubmoduleImportation(String),
|
Importation(String, BindingContext),
|
||||||
|
FromImportation(String, BindingContext),
|
||||||
|
SubmoduleImportation(String, BindingContext),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Binding {
|
pub struct Binding {
|
||||||
pub kind: BindingKind,
|
pub kind: BindingKind,
|
||||||
pub location: Range,
|
pub range: Range,
|
||||||
/// Tuple of (scope index, range) indicating the scope and range at which the binding was
|
/// Tuple of (scope index, range) indicating the scope and range at which the binding was
|
||||||
/// last used.
|
/// last used.
|
||||||
pub used: Option<(usize, Range)>,
|
pub used: Option<(usize, Range)>,
|
||||||
|
@ -84,3 +91,9 @@ pub struct Binding {
|
||||||
pub trait CheckLocator {
|
pub trait CheckLocator {
|
||||||
fn locate_check(&self, default: Range) -> Range;
|
fn locate_check(&self, default: Range) -> Range;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub enum ImportKind {
|
||||||
|
Import,
|
||||||
|
ImportFrom,
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use itertools::Itertools;
|
||||||
use rustpython_parser::ast::Location;
|
use rustpython_parser::ast::Location;
|
||||||
|
|
||||||
use crate::checks::{Check, Fix};
|
use crate::checks::{Check, Fix};
|
||||||
|
@ -43,7 +44,7 @@ fn apply_fixes<'a>(fixes: impl Iterator<Item = &'a mut Fix>, contents: &str) ->
|
||||||
let mut output = "".to_string();
|
let mut output = "".to_string();
|
||||||
let mut last_pos: Location = Location::new(0, 0);
|
let mut last_pos: Location = Location::new(0, 0);
|
||||||
|
|
||||||
for fix in fixes {
|
for fix in fixes.sorted_by_key(|fix| fix.location) {
|
||||||
// Best-effort approach: if this fix overlaps with a fix we've already applied, skip it.
|
// Best-effort approach: if this fix overlaps with a fix we've already applied, skip it.
|
||||||
if last_pos > fix.location {
|
if last_pos > fix.location {
|
||||||
continue;
|
continue;
|
||||||
|
@ -71,14 +72,19 @@ fn apply_fixes<'a>(fixes: impl Iterator<Item = &'a mut Fix>, contents: &str) ->
|
||||||
fix.applied = true;
|
fix.applied = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if last_pos.row() > 0 || last_pos.column() > 0 {
|
if last_pos.row() > 0
|
||||||
|
&& (last_pos.row() - 1) < lines.len()
|
||||||
|
&& (last_pos.row() > 0 || last_pos.column() > 0)
|
||||||
|
{
|
||||||
output.push_str(&lines[last_pos.row() - 1][last_pos.column() - 1..]);
|
output.push_str(&lines[last_pos.row() - 1][last_pos.column() - 1..]);
|
||||||
output.push('\n');
|
output.push('\n');
|
||||||
}
|
}
|
||||||
|
if last_pos.row() < lines.len() {
|
||||||
for line in &lines[last_pos.row()..] {
|
for line in &lines[last_pos.row()..] {
|
||||||
output.push_str(line);
|
output.push_str(line);
|
||||||
output.push('\n');
|
output.push('\n');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
output
|
output
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use libcst_native::ImportNames::Aliases;
|
||||||
|
use libcst_native::NameOrAttribute::N;
|
||||||
use libcst_native::{Codegen, Expression, SmallStatement, Statement};
|
use libcst_native::{Codegen, Expression, SmallStatement, Statement};
|
||||||
use rustpython_parser::ast::{Expr, Keyword, Location};
|
use rustpython_parser::ast::{ExcepthandlerKind, Expr, Keyword, Location, Stmt, StmtKind};
|
||||||
use rustpython_parser::lexer;
|
use rustpython_parser::lexer;
|
||||||
use rustpython_parser::token::Tok;
|
use rustpython_parser::token::Tok;
|
||||||
|
|
||||||
use crate::ast::operations::SourceCodeLocator;
|
use crate::ast::operations::SourceCodeLocator;
|
||||||
|
use crate::ast::types::Range;
|
||||||
use crate::checks::Fix;
|
use crate::checks::Fix;
|
||||||
|
|
||||||
/// Convert a location within a file (relative to `base`) to an absolute position.
|
/// Convert a location within a file (relative to `base`) to an absolute position.
|
||||||
|
@ -127,7 +132,8 @@ pub fn remove_class_def_base(
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_super_arguments(locator: &mut SourceCodeLocator, expr: &Expr) -> Option<Fix> {
|
pub fn remove_super_arguments(locator: &mut SourceCodeLocator, expr: &Expr) -> Option<Fix> {
|
||||||
let contents = locator.slice_source_code_range(&expr.location, &expr.end_location);
|
let range = Range::from_located(expr);
|
||||||
|
let contents = locator.slice_source_code_range(&range);
|
||||||
|
|
||||||
let mut tree = match libcst_native::parse_module(contents, None) {
|
let mut tree = match libcst_native::parse_module(contents, None) {
|
||||||
Ok(m) => m,
|
Ok(m) => m,
|
||||||
|
@ -146,8 +152,8 @@ pub fn remove_super_arguments(locator: &mut SourceCodeLocator, expr: &Expr) -> O
|
||||||
|
|
||||||
return Some(Fix {
|
return Some(Fix {
|
||||||
content: state.to_string(),
|
content: state.to_string(),
|
||||||
location: expr.location,
|
location: range.location,
|
||||||
end_location: expr.end_location,
|
end_location: range.end_location,
|
||||||
applied: false,
|
applied: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -156,3 +162,170 @@ pub fn remove_super_arguments(locator: &mut SourceCodeLocator, expr: &Expr) -> O
|
||||||
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Determine if a body contains only a single statement, taking into account deleted.
|
||||||
|
fn has_single_child(body: &[Stmt], deleted: &[&Stmt]) -> bool {
|
||||||
|
body.iter().filter(|child| !deleted.contains(child)).count() == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determine if a child is the only statement in its body.
|
||||||
|
fn is_lone_child(child: &Stmt, parent: &Stmt, deleted: &[&Stmt]) -> Result<bool> {
|
||||||
|
match &parent.node {
|
||||||
|
StmtKind::FunctionDef { body, .. }
|
||||||
|
| StmtKind::AsyncFunctionDef { body, .. }
|
||||||
|
| StmtKind::ClassDef { body, .. }
|
||||||
|
| StmtKind::With { body, .. }
|
||||||
|
| StmtKind::AsyncWith { body, .. } => {
|
||||||
|
if body.iter().contains(child) {
|
||||||
|
Ok(has_single_child(body, deleted))
|
||||||
|
} else {
|
||||||
|
Err(anyhow::anyhow!("Unable to find child in parent body."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StmtKind::For { body, orelse, .. }
|
||||||
|
| StmtKind::AsyncFor { body, orelse, .. }
|
||||||
|
| StmtKind::While { body, orelse, .. }
|
||||||
|
| StmtKind::If { body, orelse, .. } => {
|
||||||
|
if body.iter().contains(child) {
|
||||||
|
Ok(has_single_child(body, deleted))
|
||||||
|
} else if orelse.iter().contains(child) {
|
||||||
|
Ok(has_single_child(orelse, deleted))
|
||||||
|
} else {
|
||||||
|
Err(anyhow::anyhow!("Unable to find child in parent body."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StmtKind::Try {
|
||||||
|
body,
|
||||||
|
handlers,
|
||||||
|
orelse,
|
||||||
|
finalbody,
|
||||||
|
} => {
|
||||||
|
if body.iter().contains(child) {
|
||||||
|
Ok(has_single_child(body, deleted))
|
||||||
|
} else if orelse.iter().contains(child) {
|
||||||
|
Ok(has_single_child(orelse, deleted))
|
||||||
|
} else if finalbody.iter().contains(child) {
|
||||||
|
Ok(has_single_child(finalbody, deleted))
|
||||||
|
} else if let Some(body) = handlers.iter().find_map(|handler| match &handler.node {
|
||||||
|
ExcepthandlerKind::ExceptHandler { body, .. } => {
|
||||||
|
if body.iter().contains(child) {
|
||||||
|
Some(body)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Ok(has_single_child(body, deleted))
|
||||||
|
} else {
|
||||||
|
Err(anyhow::anyhow!("Unable to find child in parent body."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Err(anyhow::anyhow!("Unable to find child in parent body.")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_stmt(stmt: &Stmt, parent: Option<&Stmt>, deleted: &[&Stmt]) -> Result<Fix> {
|
||||||
|
if parent
|
||||||
|
.map(|parent| is_lone_child(stmt, parent, deleted))
|
||||||
|
.map_or(Ok(None), |v| v.map(Some))?
|
||||||
|
.unwrap_or_default()
|
||||||
|
{
|
||||||
|
// If removing this node would lead to an invalid syntax tree, replace
|
||||||
|
// it with a `pass`.
|
||||||
|
Ok(Fix {
|
||||||
|
location: stmt.location,
|
||||||
|
end_location: stmt.end_location,
|
||||||
|
content: "pass".to_string(),
|
||||||
|
applied: false,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Otherwise, nuke the entire line.
|
||||||
|
// TODO(charlie): This logic assumes that there are no multi-statement physical lines.
|
||||||
|
Ok(Fix {
|
||||||
|
location: Location::new(stmt.location.row(), 1),
|
||||||
|
end_location: Location::new(stmt.end_location.row() + 1, 1),
|
||||||
|
content: "".to_string(),
|
||||||
|
applied: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a Fix to remove any unused imports from an `import` statement.
|
||||||
|
pub fn remove_unused_imports(stmt: &Stmt, parent: Option<&Stmt>, deleted: &[&Stmt]) -> Result<Fix> {
|
||||||
|
remove_stmt(stmt, parent, deleted)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a Fix to remove any unused imports from an `import from` statement.
|
||||||
|
pub fn remove_unused_import_froms(
|
||||||
|
locator: &mut SourceCodeLocator,
|
||||||
|
full_names: &[String],
|
||||||
|
stmt: &Stmt,
|
||||||
|
parent: Option<&Stmt>,
|
||||||
|
deleted: &[&Stmt],
|
||||||
|
) -> Result<Fix> {
|
||||||
|
let mut tree = match libcst_native::parse_module(
|
||||||
|
locator.slice_source_code_range(&Range::from_located(stmt)),
|
||||||
|
None,
|
||||||
|
) {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(_) => return Err(anyhow::anyhow!("Failed to extract CST from source.")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let body = if let Some(Statement::Simple(body)) = tree.body.first_mut() {
|
||||||
|
body
|
||||||
|
} else {
|
||||||
|
return Err(anyhow::anyhow!("Expected node to be: Statement::Simple."));
|
||||||
|
};
|
||||||
|
let body = if let Some(SmallStatement::ImportFrom(body)) = body.body.first_mut() {
|
||||||
|
body
|
||||||
|
} else {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Expected node to be: SmallStatement::ImportFrom."
|
||||||
|
));
|
||||||
|
};
|
||||||
|
let aliases = if let Aliases(aliases) = &mut body.names {
|
||||||
|
aliases
|
||||||
|
} else {
|
||||||
|
return Err(anyhow::anyhow!("Expected node to be: Aliases."));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Preserve the trailing comma (or not) from the last entry.
|
||||||
|
let trailing_comma = aliases.last().and_then(|alias| alias.comma.clone());
|
||||||
|
|
||||||
|
// Identify unused imports from within the `import from`.
|
||||||
|
let mut removable = vec![];
|
||||||
|
for (index, alias) in aliases.iter().enumerate() {
|
||||||
|
if let N(name) = &alias.name {
|
||||||
|
let import_name = if let Some(N(module_name)) = &body.module {
|
||||||
|
format!("{}.{}", module_name.value, name.value)
|
||||||
|
} else {
|
||||||
|
name.value.to_string()
|
||||||
|
};
|
||||||
|
if full_names.contains(&import_name) {
|
||||||
|
removable.push(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO(charlie): This is quadratic.
|
||||||
|
for index in removable.iter().rev() {
|
||||||
|
aliases.remove(*index);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(alias) = aliases.last_mut() {
|
||||||
|
alias.comma = trailing_comma;
|
||||||
|
}
|
||||||
|
|
||||||
|
if aliases.is_empty() {
|
||||||
|
remove_stmt(stmt, parent, deleted)
|
||||||
|
} else {
|
||||||
|
let mut state = Default::default();
|
||||||
|
tree.codegen(&mut state);
|
||||||
|
|
||||||
|
Ok(Fix {
|
||||||
|
content: state.to_string(),
|
||||||
|
location: stmt.location,
|
||||||
|
end_location: stmt.end_location,
|
||||||
|
applied: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
164
src/check_ast.rs
164
src/check_ast.rs
|
@ -1,6 +1,8 @@
|
||||||
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
use log::error;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use rustpython_parser::ast::{
|
use rustpython_parser::ast::{
|
||||||
|
@ -12,11 +14,12 @@ use rustpython_parser::parser;
|
||||||
use crate::ast::operations::{extract_all_names, SourceCodeLocator};
|
use crate::ast::operations::{extract_all_names, SourceCodeLocator};
|
||||||
use crate::ast::relocate::relocate_expr;
|
use crate::ast::relocate::relocate_expr;
|
||||||
use crate::ast::types::{
|
use crate::ast::types::{
|
||||||
Binding, BindingKind, CheckLocator, FunctionScope, Range, Scope, ScopeKind,
|
Binding, BindingContext, BindingKind, CheckLocator, FunctionScope, ImportKind, Range, Scope,
|
||||||
|
ScopeKind,
|
||||||
};
|
};
|
||||||
use crate::ast::visitor::{walk_excepthandler, Visitor};
|
use crate::ast::visitor::{walk_excepthandler, Visitor};
|
||||||
use crate::ast::{checks, operations, visitor};
|
use crate::ast::{checks, operations, visitor};
|
||||||
use crate::autofix::fixer;
|
use crate::autofix::{fixer, fixes};
|
||||||
use crate::checks::{Check, CheckCode, CheckKind};
|
use crate::checks::{Check, CheckCode, CheckKind};
|
||||||
use crate::python::builtins::{BUILTINS, MAGIC_GLOBALS};
|
use crate::python::builtins::{BUILTINS, MAGIC_GLOBALS};
|
||||||
use crate::python::future::ALL_FEATURE_NAMES;
|
use crate::python::future::ALL_FEATURE_NAMES;
|
||||||
|
@ -55,6 +58,8 @@ struct Checker<'a> {
|
||||||
seen_docstring: bool,
|
seen_docstring: bool,
|
||||||
futures_allowed: bool,
|
futures_allowed: bool,
|
||||||
annotations_future_enabled: bool,
|
annotations_future_enabled: bool,
|
||||||
|
// Edit tracking.
|
||||||
|
deletions: BTreeSet<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Checker<'a> {
|
impl<'a> Checker<'a> {
|
||||||
|
@ -87,6 +92,7 @@ impl<'a> Checker<'a> {
|
||||||
seen_docstring: false,
|
seen_docstring: false,
|
||||||
futures_allowed: true,
|
futures_allowed: true,
|
||||||
annotations_future_enabled: false,
|
annotations_future_enabled: false,
|
||||||
|
deletions: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -221,7 +227,7 @@ where
|
||||||
Binding {
|
Binding {
|
||||||
kind: BindingKind::Assignment,
|
kind: BindingKind::Assignment,
|
||||||
used: Some((global_scope_id, Range::from_located(stmt))),
|
used: Some((global_scope_id, Range::from_located(stmt))),
|
||||||
location: Range::from_located(stmt),
|
range: Range::from_located(stmt),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -329,7 +335,7 @@ where
|
||||||
Binding {
|
Binding {
|
||||||
kind: BindingKind::Definition,
|
kind: BindingKind::Definition,
|
||||||
used: None,
|
used: None,
|
||||||
location: Range::from_located(stmt),
|
range: Range::from_located(stmt),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -424,9 +430,10 @@ where
|
||||||
Binding {
|
Binding {
|
||||||
kind: BindingKind::SubmoduleImportation(
|
kind: BindingKind::SubmoduleImportation(
|
||||||
alias.node.name.to_string(),
|
alias.node.name.to_string(),
|
||||||
|
self.binding_context(),
|
||||||
),
|
),
|
||||||
used: None,
|
used: None,
|
||||||
location: Range::from_located(stmt),
|
range: Range::from_located(stmt),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
@ -447,9 +454,10 @@ where
|
||||||
.asname
|
.asname
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| alias.node.name.clone()),
|
.unwrap_or_else(|| alias.node.name.clone()),
|
||||||
|
self.binding_context(),
|
||||||
),
|
),
|
||||||
used: None,
|
used: None,
|
||||||
location: Range::from_located(stmt),
|
range: Range::from_located(stmt),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -492,7 +500,7 @@ where
|
||||||
.id,
|
.id,
|
||||||
Range::from_located(stmt),
|
Range::from_located(stmt),
|
||||||
)),
|
)),
|
||||||
location: Range::from_located(stmt),
|
range: Range::from_located(stmt),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -528,7 +536,7 @@ where
|
||||||
Binding {
|
Binding {
|
||||||
kind: BindingKind::StarImportation,
|
kind: BindingKind::StarImportation,
|
||||||
used: None,
|
used: None,
|
||||||
location: Range::from_located(stmt),
|
range: Range::from_located(stmt),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -561,12 +569,15 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
let binding = Binding {
|
let binding = Binding {
|
||||||
kind: BindingKind::Importation(match module {
|
kind: BindingKind::FromImportation(
|
||||||
|
match module {
|
||||||
None => name.clone(),
|
None => name.clone(),
|
||||||
Some(parent) => format!("{}.{}", parent, name),
|
Some(parent) => format!("{}.{}", parent, name),
|
||||||
}),
|
},
|
||||||
|
self.binding_context(),
|
||||||
|
),
|
||||||
used: None,
|
used: None,
|
||||||
location: Range::from_located(stmt),
|
range: Range::from_located(stmt),
|
||||||
};
|
};
|
||||||
self.add_binding(name, binding)
|
self.add_binding(name, binding)
|
||||||
}
|
}
|
||||||
|
@ -661,7 +672,7 @@ where
|
||||||
Binding {
|
Binding {
|
||||||
kind: BindingKind::ClassDefinition,
|
kind: BindingKind::ClassDefinition,
|
||||||
used: None,
|
used: None,
|
||||||
location: Range::from_located(stmt),
|
range: Range::from_located(stmt),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1106,7 +1117,6 @@ where
|
||||||
),
|
),
|
||||||
parent,
|
parent,
|
||||||
);
|
);
|
||||||
self.parents.push(parent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let parent =
|
let parent =
|
||||||
|
@ -1125,7 +1135,6 @@ where
|
||||||
),
|
),
|
||||||
parent,
|
parent,
|
||||||
);
|
);
|
||||||
self.parents.push(parent);
|
|
||||||
|
|
||||||
walk_excepthandler(self, excepthandler);
|
walk_excepthandler(self, excepthandler);
|
||||||
|
|
||||||
|
@ -1183,7 +1192,7 @@ where
|
||||||
Binding {
|
Binding {
|
||||||
kind: BindingKind::Argument,
|
kind: BindingKind::Argument,
|
||||||
used: None,
|
used: None,
|
||||||
location: Range::from_located(arg),
|
range: Range::from_located(arg),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1239,7 +1248,7 @@ impl<'a> Checker<'a> {
|
||||||
(*builtin).to_string(),
|
(*builtin).to_string(),
|
||||||
Binding {
|
Binding {
|
||||||
kind: BindingKind::Builtin,
|
kind: BindingKind::Builtin,
|
||||||
location: Default::default(),
|
range: Default::default(),
|
||||||
used: None,
|
used: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -1249,13 +1258,23 @@ impl<'a> Checker<'a> {
|
||||||
(*builtin).to_string(),
|
(*builtin).to_string(),
|
||||||
Binding {
|
Binding {
|
||||||
kind: BindingKind::Builtin,
|
kind: BindingKind::Builtin,
|
||||||
location: Default::default(),
|
range: Default::default(),
|
||||||
used: None,
|
used: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn binding_context(&self) -> BindingContext {
|
||||||
|
let mut rev = self.parent_stack.iter().rev().fuse();
|
||||||
|
let defined_by = *rev.next().expect("Expected to bind within a statement.");
|
||||||
|
let defined_in = rev.next().cloned();
|
||||||
|
BindingContext {
|
||||||
|
defined_by,
|
||||||
|
defined_in,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn add_binding(&mut self, name: String, binding: Binding) {
|
fn add_binding(&mut self, name: String, binding: Binding) {
|
||||||
let scope = &mut self.scopes[*(self.scope_stack.last().expect("No current scope found."))];
|
let scope = &mut self.scopes[*(self.scope_stack.last().expect("No current scope found."))];
|
||||||
|
|
||||||
|
@ -1264,20 +1283,23 @@ impl<'a> Checker<'a> {
|
||||||
None => binding,
|
None => binding,
|
||||||
Some(existing) => {
|
Some(existing) => {
|
||||||
if self.settings.select.contains(&CheckCode::F402)
|
if self.settings.select.contains(&CheckCode::F402)
|
||||||
&& matches!(existing.kind, BindingKind::Importation(_))
|
|
||||||
&& matches!(binding.kind, BindingKind::LoopVar)
|
&& matches!(binding.kind, BindingKind::LoopVar)
|
||||||
|
&& matches!(
|
||||||
|
existing.kind,
|
||||||
|
BindingKind::Importation(_, _) | BindingKind::FromImportation(_, _)
|
||||||
|
)
|
||||||
{
|
{
|
||||||
self.checks.push(Check::new(
|
self.checks.push(Check::new(
|
||||||
CheckKind::ImportShadowedByLoopVar(
|
CheckKind::ImportShadowedByLoopVar(
|
||||||
name.clone(),
|
name.clone(),
|
||||||
existing.location.location.row(),
|
existing.range.location.row(),
|
||||||
),
|
),
|
||||||
binding.location,
|
binding.range,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
Binding {
|
Binding {
|
||||||
kind: binding.kind,
|
kind: binding.kind,
|
||||||
location: binding.location,
|
range: binding.range,
|
||||||
used: existing.used,
|
used: existing.used,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1378,7 +1400,7 @@ impl<'a> Checker<'a> {
|
||||||
Binding {
|
Binding {
|
||||||
kind: BindingKind::Annotation,
|
kind: BindingKind::Annotation,
|
||||||
used: None,
|
used: None,
|
||||||
location: Range::from_located(expr),
|
range: Range::from_located(expr),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
@ -1394,7 +1416,7 @@ impl<'a> Checker<'a> {
|
||||||
Binding {
|
Binding {
|
||||||
kind: BindingKind::LoopVar,
|
kind: BindingKind::LoopVar,
|
||||||
used: None,
|
used: None,
|
||||||
location: Range::from_located(expr),
|
range: Range::from_located(expr),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
@ -1406,7 +1428,7 @@ impl<'a> Checker<'a> {
|
||||||
Binding {
|
Binding {
|
||||||
kind: BindingKind::Binding,
|
kind: BindingKind::Binding,
|
||||||
used: None,
|
used: None,
|
||||||
location: Range::from_located(expr),
|
range: Range::from_located(expr),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
@ -1426,7 +1448,7 @@ impl<'a> Checker<'a> {
|
||||||
Binding {
|
Binding {
|
||||||
kind: BindingKind::Export(extract_all_names(parent, current)),
|
kind: BindingKind::Export(extract_all_names(parent, current)),
|
||||||
used: None,
|
used: None,
|
||||||
location: Range::from_located(expr),
|
range: Range::from_located(expr),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
@ -1437,7 +1459,7 @@ impl<'a> Checker<'a> {
|
||||||
Binding {
|
Binding {
|
||||||
kind: BindingKind::Assignment,
|
kind: BindingKind::Assignment,
|
||||||
used: None,
|
used: None,
|
||||||
location: Range::from_located(expr),
|
range: Range::from_located(expr),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1571,7 +1593,7 @@ impl<'a> Checker<'a> {
|
||||||
if !scope.values.contains_key(name) {
|
if !scope.values.contains_key(name) {
|
||||||
self.checks.push(Check::new(
|
self.checks.push(Check::new(
|
||||||
CheckKind::UndefinedExport(name.to_string()),
|
CheckKind::UndefinedExport(name.to_string()),
|
||||||
self.locate_check(all_binding.location),
|
self.locate_check(all_binding.range),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1594,7 +1616,7 @@ impl<'a> Checker<'a> {
|
||||||
if !scope.values.contains_key(name) {
|
if !scope.values.contains_key(name) {
|
||||||
self.checks.push(Check::new(
|
self.checks.push(Check::new(
|
||||||
CheckKind::ImportStarUsage(name.clone(), from_list.join(", ")),
|
CheckKind::ImportStarUsage(name.clone(), from_list.join(", ")),
|
||||||
self.locate_check(all_binding.location),
|
self.locate_check(all_binding.range),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1603,6 +1625,11 @@ impl<'a> Checker<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.settings.select.contains(&CheckCode::F401) {
|
if self.settings.select.contains(&CheckCode::F401) {
|
||||||
|
// Collect all unused imports by location. (Multiple unused imports at the same
|
||||||
|
// location indicates an `import from`.)
|
||||||
|
let mut unused: BTreeMap<(ImportKind, usize, Option<usize>), Vec<String>> =
|
||||||
|
BTreeMap::new();
|
||||||
|
|
||||||
for (name, binding) in scope.values.iter().rev() {
|
for (name, binding) in scope.values.iter().rev() {
|
||||||
let used = binding.used.is_some()
|
let used = binding.used.is_some()
|
||||||
|| all_names
|
|| all_names
|
||||||
|
@ -1611,17 +1638,82 @@ impl<'a> Checker<'a> {
|
||||||
|
|
||||||
if !used {
|
if !used {
|
||||||
match &binding.kind {
|
match &binding.kind {
|
||||||
BindingKind::Importation(full_name)
|
BindingKind::FromImportation(full_name, context) => {
|
||||||
| BindingKind::SubmoduleImportation(full_name) => {
|
let full_names = unused
|
||||||
self.checks.push(Check::new(
|
.entry((
|
||||||
CheckKind::UnusedImport(full_name.to_string()),
|
ImportKind::ImportFrom,
|
||||||
self.locate_check(binding.location),
|
context.defined_by,
|
||||||
));
|
context.defined_in,
|
||||||
|
))
|
||||||
|
.or_insert(vec![]);
|
||||||
|
full_names.push(full_name.to_string());
|
||||||
|
}
|
||||||
|
BindingKind::Importation(full_name, context)
|
||||||
|
| BindingKind::SubmoduleImportation(full_name, context) => {
|
||||||
|
let full_names = unused
|
||||||
|
.entry((
|
||||||
|
ImportKind::Import,
|
||||||
|
context.defined_by,
|
||||||
|
context.defined_in,
|
||||||
|
))
|
||||||
|
.or_insert(vec![]);
|
||||||
|
full_names.push(full_name.to_string());
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for ((kind, defined_by, defined_in), full_names) in unused {
|
||||||
|
let child = self.parents[defined_by];
|
||||||
|
let parent = defined_in.map(|defined_in| self.parents[defined_in]);
|
||||||
|
|
||||||
|
let mut check = Check::new(
|
||||||
|
CheckKind::UnusedImport(full_names.join(", ")),
|
||||||
|
self.locate_check(Range::from_located(child)),
|
||||||
|
);
|
||||||
|
|
||||||
|
if matches!(self.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||||
|
let deleted: Vec<&Stmt> = self
|
||||||
|
.deletions
|
||||||
|
.iter()
|
||||||
|
.map(|index| self.parents[*index])
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
match kind {
|
||||||
|
ImportKind::Import => {
|
||||||
|
match fixes::remove_unused_imports(child, parent, &deleted) {
|
||||||
|
Ok(fix) => {
|
||||||
|
if fix.content.is_empty() || fix.content == "pass" {
|
||||||
|
self.deletions.insert(defined_by);
|
||||||
|
}
|
||||||
|
check.amend(fix)
|
||||||
|
}
|
||||||
|
Err(e) => error!("Failed to fix unused imports: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImportKind::ImportFrom => {
|
||||||
|
match fixes::remove_unused_import_froms(
|
||||||
|
&mut self.locator,
|
||||||
|
&full_names,
|
||||||
|
child,
|
||||||
|
parent,
|
||||||
|
&deleted,
|
||||||
|
) {
|
||||||
|
Ok(fix) => {
|
||||||
|
if fix.content.is_empty() || fix.content == "pass" {
|
||||||
|
self.deletions.insert(defined_by);
|
||||||
|
}
|
||||||
|
check.amend(fix)
|
||||||
|
}
|
||||||
|
Err(e) => error!("Failed to fix unused imports: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.checks.push(check);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1666,12 +1758,12 @@ impl<'a> Checker<'a> {
|
||||||
|
|
||||||
pub fn check_ast(
|
pub fn check_ast(
|
||||||
python_ast: &Suite,
|
python_ast: &Suite,
|
||||||
content: &str,
|
contents: &str,
|
||||||
settings: &Settings,
|
settings: &Settings,
|
||||||
autofix: &fixer::Mode,
|
autofix: &fixer::Mode,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
) -> Vec<Check> {
|
) -> Vec<Check> {
|
||||||
let mut checker = Checker::new(settings, autofix, path, content);
|
let mut checker = Checker::new(settings, autofix, path, contents);
|
||||||
checker.push_scope(Scope::new(ScopeKind::Module));
|
checker.push_scope(Scope::new(ScopeKind::Module));
|
||||||
checker.bind_builtins();
|
checker.bind_builtins();
|
||||||
|
|
||||||
|
|
|
@ -713,6 +713,7 @@ impl CheckKind {
|
||||||
| CheckKind::UselessObjectInheritance(_)
|
| CheckKind::UselessObjectInheritance(_)
|
||||||
| CheckKind::UnusedNOQA(_)
|
| CheckKind::UnusedNOQA(_)
|
||||||
| CheckKind::SuperCallWithParameters
|
| CheckKind::SuperCallWithParameters
|
||||||
|
| CheckKind::UnusedImport(_)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,15 @@ expression: checks
|
||||||
end_location:
|
end_location:
|
||||||
row: 3
|
row: 3
|
||||||
column: 17
|
column: 17
|
||||||
fix: ~
|
fix:
|
||||||
|
content: ""
|
||||||
|
location:
|
||||||
|
row: 3
|
||||||
|
column: 1
|
||||||
|
end_location:
|
||||||
|
row: 4
|
||||||
|
column: 1
|
||||||
|
applied: false
|
||||||
- kind:
|
- kind:
|
||||||
UnusedImport: collections.OrderedDict
|
UnusedImport: collections.OrderedDict
|
||||||
location:
|
location:
|
||||||
|
@ -19,7 +27,15 @@ expression: checks
|
||||||
end_location:
|
end_location:
|
||||||
row: 9
|
row: 9
|
||||||
column: 2
|
column: 2
|
||||||
fix: ~
|
fix:
|
||||||
|
content: "from collections import (\n Counter,\n namedtuple,\n)"
|
||||||
|
location:
|
||||||
|
row: 5
|
||||||
|
column: 1
|
||||||
|
end_location:
|
||||||
|
row: 9
|
||||||
|
column: 2
|
||||||
|
applied: false
|
||||||
- kind:
|
- kind:
|
||||||
UnusedImport: logging.handlers
|
UnusedImport: logging.handlers
|
||||||
location:
|
location:
|
||||||
|
@ -28,5 +44,81 @@ expression: checks
|
||||||
end_location:
|
end_location:
|
||||||
row: 13
|
row: 13
|
||||||
column: 24
|
column: 24
|
||||||
fix: ~
|
fix:
|
||||||
|
content: ""
|
||||||
|
location:
|
||||||
|
row: 13
|
||||||
|
column: 1
|
||||||
|
end_location:
|
||||||
|
row: 14
|
||||||
|
column: 1
|
||||||
|
applied: false
|
||||||
|
- kind:
|
||||||
|
UnusedImport: shelve
|
||||||
|
location:
|
||||||
|
row: 34
|
||||||
|
column: 5
|
||||||
|
end_location:
|
||||||
|
row: 34
|
||||||
|
column: 18
|
||||||
|
fix:
|
||||||
|
content: ""
|
||||||
|
location:
|
||||||
|
row: 34
|
||||||
|
column: 1
|
||||||
|
end_location:
|
||||||
|
row: 35
|
||||||
|
column: 1
|
||||||
|
applied: false
|
||||||
|
- kind:
|
||||||
|
UnusedImport: importlib
|
||||||
|
location:
|
||||||
|
row: 35
|
||||||
|
column: 5
|
||||||
|
end_location:
|
||||||
|
row: 35
|
||||||
|
column: 21
|
||||||
|
fix:
|
||||||
|
content: pass
|
||||||
|
location:
|
||||||
|
row: 35
|
||||||
|
column: 5
|
||||||
|
end_location:
|
||||||
|
row: 35
|
||||||
|
column: 21
|
||||||
|
applied: false
|
||||||
|
- kind:
|
||||||
|
UnusedImport: pathlib
|
||||||
|
location:
|
||||||
|
row: 39
|
||||||
|
column: 5
|
||||||
|
end_location:
|
||||||
|
row: 39
|
||||||
|
column: 19
|
||||||
|
fix:
|
||||||
|
content: ""
|
||||||
|
location:
|
||||||
|
row: 39
|
||||||
|
column: 1
|
||||||
|
end_location:
|
||||||
|
row: 40
|
||||||
|
column: 1
|
||||||
|
applied: false
|
||||||
|
- kind:
|
||||||
|
UnusedImport: pickle
|
||||||
|
location:
|
||||||
|
row: 54
|
||||||
|
column: 9
|
||||||
|
end_location:
|
||||||
|
row: 54
|
||||||
|
column: 22
|
||||||
|
fix:
|
||||||
|
content: pass
|
||||||
|
location:
|
||||||
|
row: 54
|
||||||
|
column: 9
|
||||||
|
end_location:
|
||||||
|
row: 54
|
||||||
|
column: 22
|
||||||
|
applied: false
|
||||||
|
|
||||||
|
|
|
@ -8,16 +8,24 @@ expression: checks
|
||||||
row: 5
|
row: 5
|
||||||
column: 1
|
column: 1
|
||||||
end_location:
|
end_location:
|
||||||
|
row: 8
|
||||||
|
column: 2
|
||||||
|
fix:
|
||||||
|
content: "from models import (\n Fruit,\n)"
|
||||||
|
location:
|
||||||
row: 5
|
row: 5
|
||||||
column: 30
|
column: 1
|
||||||
fix: ~
|
end_location:
|
||||||
|
row: 8
|
||||||
|
column: 2
|
||||||
|
applied: false
|
||||||
- kind:
|
- kind:
|
||||||
UndefinedName: Bar
|
UndefinedName: Bar
|
||||||
location:
|
location:
|
||||||
row: 22
|
row: 25
|
||||||
column: 19
|
column: 19
|
||||||
end_location:
|
end_location:
|
||||||
row: 22
|
row: 25
|
||||||
column: 22
|
column: 22
|
||||||
fix: ~
|
fix: ~
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue