mirror of https://github.com/astral-sh/ruff
Ignore unused imports in `ModuleNotFoundError` blocks (#3288)
This commit is contained in:
parent
310f13c7db
commit
4a70a4c323
|
|
@ -0,0 +1,10 @@
|
||||||
|
"""Test: imports within `ModuleNotFoundError` handlers."""
|
||||||
|
|
||||||
|
|
||||||
|
def check_orjson():
|
||||||
|
try:
|
||||||
|
import orjson
|
||||||
|
|
||||||
|
return True
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
return False
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
use bitflags::bitflags;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use log::error;
|
use log::error;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
|
@ -575,33 +576,32 @@ pub fn has_non_none_keyword(keywords: &[Keyword], keyword: &str) -> bool {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bitflags! {
|
||||||
|
pub struct Exceptions: u32 {
|
||||||
|
const NAME_ERROR = 0b0000_0001;
|
||||||
|
const MODULE_NOT_FOUND_ERROR = 0b0000_0010;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Extract the names of all handled exceptions.
|
/// Extract the names of all handled exceptions.
|
||||||
pub fn extract_handler_names(handlers: &[Excepthandler]) -> Vec<CallPath> {
|
pub fn extract_handled_exceptions(handlers: &[Excepthandler]) -> Vec<&Expr> {
|
||||||
// TODO(charlie): Use `resolve_call_path` to avoid false positives for
|
let mut handled_exceptions = Vec::new();
|
||||||
// overridden builtins.
|
|
||||||
let mut handler_names = vec![];
|
|
||||||
for handler in handlers {
|
for handler in handlers {
|
||||||
match &handler.node {
|
match &handler.node {
|
||||||
ExcepthandlerKind::ExceptHandler { type_, .. } => {
|
ExcepthandlerKind::ExceptHandler { type_, .. } => {
|
||||||
if let Some(type_) = type_ {
|
if let Some(type_) = type_ {
|
||||||
if let ExprKind::Tuple { elts, .. } = &type_.node {
|
if let ExprKind::Tuple { elts, .. } = &type_.node {
|
||||||
for type_ in elts {
|
for type_ in elts {
|
||||||
let call_path = collect_call_path(type_);
|
handled_exceptions.push(type_);
|
||||||
if !call_path.is_empty() {
|
|
||||||
handler_names.push(call_path);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let call_path = collect_call_path(type_);
|
handled_exceptions.push(type_);
|
||||||
if !call_path.is_empty() {
|
|
||||||
handler_names.push(call_path);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
handler_names
|
handled_exceptions
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the set of all bound argument names.
|
/// Return the set of all bound argument names.
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,8 @@ use rustpython_parser::ast::{
|
||||||
use smallvec::smallvec;
|
use smallvec::smallvec;
|
||||||
|
|
||||||
use crate::ast::helpers::{
|
use crate::ast::helpers::{
|
||||||
binding_range, collect_call_path, extract_handler_names, from_relative_import, to_module_path,
|
binding_range, collect_call_path, extract_handled_exceptions, from_relative_import,
|
||||||
|
to_module_path, Exceptions,
|
||||||
};
|
};
|
||||||
use crate::ast::operations::{extract_all_names, AllNamesFlags};
|
use crate::ast::operations::{extract_all_names, AllNamesFlags};
|
||||||
use crate::ast::relocate::relocate_expr;
|
use crate::ast::relocate::relocate_expr;
|
||||||
|
|
@ -109,7 +110,7 @@ pub struct Checker<'a> {
|
||||||
pub(crate) seen_import_boundary: bool,
|
pub(crate) seen_import_boundary: bool,
|
||||||
futures_allowed: bool,
|
futures_allowed: bool,
|
||||||
annotations_future_enabled: bool,
|
annotations_future_enabled: bool,
|
||||||
except_handlers: Vec<Vec<Vec<&'a str>>>,
|
handled_exceptions: Vec<Exceptions>,
|
||||||
// Check-specific state.
|
// Check-specific state.
|
||||||
pub(crate) flake8_bugbear_seen: Vec<&'a Expr>,
|
pub(crate) flake8_bugbear_seen: Vec<&'a Expr>,
|
||||||
}
|
}
|
||||||
|
|
@ -178,7 +179,7 @@ impl<'a> Checker<'a> {
|
||||||
seen_import_boundary: false,
|
seen_import_boundary: false,
|
||||||
futures_allowed: true,
|
futures_allowed: true,
|
||||||
annotations_future_enabled: is_interface_definition,
|
annotations_future_enabled: is_interface_definition,
|
||||||
except_handlers: vec![],
|
handled_exceptions: vec![],
|
||||||
// Check-specific state.
|
// Check-specific state.
|
||||||
flake8_bugbear_seen: vec![],
|
flake8_bugbear_seen: vec![],
|
||||||
}
|
}
|
||||||
|
|
@ -970,6 +971,13 @@ where
|
||||||
pyupgrade::rules::rewrite_mock_import(self, stmt);
|
pyupgrade::rules::rewrite_mock_import(self, stmt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If a module is imported within a `ModuleNotFoundError` body, treat that as a
|
||||||
|
// synthetic usage.
|
||||||
|
let is_handled = self
|
||||||
|
.handled_exceptions
|
||||||
|
.iter()
|
||||||
|
.any(|exceptions| exceptions.contains(Exceptions::MODULE_NOT_FOUND_ERROR));
|
||||||
|
|
||||||
for alias in names {
|
for alias in names {
|
||||||
if alias.node.name == "__future__" {
|
if alias.node.name == "__future__" {
|
||||||
let name = alias.node.asname.as_ref().unwrap_or(&alias.node.name);
|
let name = alias.node.asname.as_ref().unwrap_or(&alias.node.name);
|
||||||
|
|
@ -1012,7 +1020,18 @@ where
|
||||||
Binding {
|
Binding {
|
||||||
kind: BindingKind::SubmoduleImportation(name, full_name),
|
kind: BindingKind::SubmoduleImportation(name, full_name),
|
||||||
runtime_usage: None,
|
runtime_usage: None,
|
||||||
synthetic_usage: None,
|
synthetic_usage: if is_handled {
|
||||||
|
Some((
|
||||||
|
self.scopes[*(self
|
||||||
|
.scope_stack
|
||||||
|
.last()
|
||||||
|
.expect("No current scope found"))]
|
||||||
|
.id,
|
||||||
|
Range::from_located(alias),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
typing_usage: None,
|
typing_usage: None,
|
||||||
range: Range::from_located(alias),
|
range: Range::from_located(alias),
|
||||||
source: Some(self.current_stmt().clone()),
|
source: Some(self.current_stmt().clone()),
|
||||||
|
|
@ -1020,6 +1039,14 @@ where
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
// Treat explicit re-export as usage (e.g., `from .applications
|
||||||
|
// import FastAPI as FastAPI`).
|
||||||
|
let is_explicit_reexport = alias
|
||||||
|
.node
|
||||||
|
.asname
|
||||||
|
.as_ref()
|
||||||
|
.map_or(false, |asname| asname == &alias.node.name);
|
||||||
|
|
||||||
// Given `import foo`, `name` and `full_name` would both be `foo`.
|
// Given `import foo`, `name` and `full_name` would both be `foo`.
|
||||||
// Given `import foo as bar`, `name` would be `bar` and `full_name` would
|
// Given `import foo as bar`, `name` would be `bar` and `full_name` would
|
||||||
// be `foo`.
|
// be `foo`.
|
||||||
|
|
@ -1030,14 +1057,7 @@ where
|
||||||
Binding {
|
Binding {
|
||||||
kind: BindingKind::Importation(name, full_name),
|
kind: BindingKind::Importation(name, full_name),
|
||||||
runtime_usage: None,
|
runtime_usage: None,
|
||||||
// Treat explicit re-export as usage (e.g., `import applications
|
synthetic_usage: if is_handled || is_explicit_reexport {
|
||||||
// as applications`).
|
|
||||||
synthetic_usage: if alias
|
|
||||||
.node
|
|
||||||
.asname
|
|
||||||
.as_ref()
|
|
||||||
.map_or(false, |asname| asname == &alias.node.name)
|
|
||||||
{
|
|
||||||
Some((
|
Some((
|
||||||
self.scopes[*(self
|
self.scopes[*(self
|
||||||
.scope_stack
|
.scope_stack
|
||||||
|
|
@ -1293,6 +1313,13 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If a module is imported within a `ModuleNotFoundError` body, treat that as a
|
||||||
|
// synthetic usage.
|
||||||
|
let is_handled = self
|
||||||
|
.handled_exceptions
|
||||||
|
.iter()
|
||||||
|
.any(|exceptions| exceptions.contains(Exceptions::MODULE_NOT_FOUND_ERROR));
|
||||||
|
|
||||||
for alias in names {
|
for alias in names {
|
||||||
if let Some("__future__") = module.as_deref() {
|
if let Some("__future__") = module.as_deref() {
|
||||||
let name = alias.node.asname.as_ref().unwrap_or(&alias.node.name);
|
let name = alias.node.asname.as_ref().unwrap_or(&alias.node.name);
|
||||||
|
|
@ -1339,7 +1366,18 @@ where
|
||||||
Binding {
|
Binding {
|
||||||
kind: BindingKind::StarImportation(*level, module.clone()),
|
kind: BindingKind::StarImportation(*level, module.clone()),
|
||||||
runtime_usage: None,
|
runtime_usage: None,
|
||||||
synthetic_usage: None,
|
synthetic_usage: if is_handled {
|
||||||
|
Some((
|
||||||
|
self.scopes[*(self
|
||||||
|
.scope_stack
|
||||||
|
.last()
|
||||||
|
.expect("No current scope found"))]
|
||||||
|
.id,
|
||||||
|
Range::from_located(alias),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
typing_usage: None,
|
typing_usage: None,
|
||||||
range: Range::from_located(stmt),
|
range: Range::from_located(stmt),
|
||||||
source: Some(self.current_stmt().clone()),
|
source: Some(self.current_stmt().clone()),
|
||||||
|
|
@ -1383,6 +1421,14 @@ where
|
||||||
self.check_builtin_shadowing(asname, stmt, false);
|
self.check_builtin_shadowing(asname, stmt, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Treat explicit re-export as usage (e.g., `from .applications
|
||||||
|
// import FastAPI as FastAPI`).
|
||||||
|
let is_explicit_reexport = alias
|
||||||
|
.node
|
||||||
|
.asname
|
||||||
|
.as_ref()
|
||||||
|
.map_or(false, |asname| asname == &alias.node.name);
|
||||||
|
|
||||||
// Given `from foo import bar`, `name` would be "bar" and `full_name` would
|
// Given `from foo import bar`, `name` would be "bar" and `full_name` would
|
||||||
// be "foo.bar". Given `from foo import bar as baz`, `name` would be "baz"
|
// be "foo.bar". Given `from foo import bar as baz`, `name` would be "baz"
|
||||||
// and `full_name` would be "foo.bar".
|
// and `full_name` would be "foo.bar".
|
||||||
|
|
@ -1392,33 +1438,25 @@ where
|
||||||
module.as_deref(),
|
module.as_deref(),
|
||||||
&alias.node.name,
|
&alias.node.name,
|
||||||
);
|
);
|
||||||
let range = Range::from_located(alias);
|
|
||||||
self.add_binding(
|
self.add_binding(
|
||||||
name,
|
name,
|
||||||
Binding {
|
Binding {
|
||||||
kind: BindingKind::FromImportation(name, full_name),
|
kind: BindingKind::FromImportation(name, full_name),
|
||||||
runtime_usage: None,
|
runtime_usage: None,
|
||||||
// Treat explicit re-export as usage (e.g., `from .applications
|
synthetic_usage: if is_handled || is_explicit_reexport {
|
||||||
// import FastAPI as FastAPI`).
|
|
||||||
synthetic_usage: if alias
|
|
||||||
.node
|
|
||||||
.asname
|
|
||||||
.as_ref()
|
|
||||||
.map_or(false, |asname| asname == &alias.node.name)
|
|
||||||
{
|
|
||||||
Some((
|
Some((
|
||||||
self.scopes[*(self
|
self.scopes[*(self
|
||||||
.scope_stack
|
.scope_stack
|
||||||
.last()
|
.last()
|
||||||
.expect("No current scope found"))]
|
.expect("No current scope found"))]
|
||||||
.id,
|
.id,
|
||||||
range,
|
Range::from_located(alias),
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
typing_usage: None,
|
typing_usage: None,
|
||||||
range,
|
range: Range::from_located(alias),
|
||||||
source: Some(self.current_stmt().clone()),
|
source: Some(self.current_stmt().clone()),
|
||||||
context: self.execution_context(),
|
context: self.execution_context(),
|
||||||
},
|
},
|
||||||
|
|
@ -2128,17 +2166,23 @@ where
|
||||||
orelse,
|
orelse,
|
||||||
finalbody,
|
finalbody,
|
||||||
} => {
|
} => {
|
||||||
// TODO(charlie): The use of `smallvec` here leads to a lifetime issue.
|
let mut handled_exceptions = Exceptions::empty();
|
||||||
let handler_names = extract_handler_names(handlers)
|
for type_ in extract_handled_exceptions(handlers) {
|
||||||
.into_iter()
|
if let Some(call_path) = self.resolve_call_path(type_) {
|
||||||
.map(|call_path| call_path.to_vec())
|
if call_path.as_slice() == ["", "NameError"] {
|
||||||
.collect();
|
handled_exceptions |= Exceptions::NAME_ERROR;
|
||||||
self.except_handlers.push(handler_names);
|
} else if call_path.as_slice() == ["", "ModuleNotFoundError"] {
|
||||||
|
handled_exceptions |= Exceptions::MODULE_NOT_FOUND_ERROR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.handled_exceptions.push(handled_exceptions);
|
||||||
if self.settings.rules.enabled(&Rule::JumpStatementInFinally) {
|
if self.settings.rules.enabled(&Rule::JumpStatementInFinally) {
|
||||||
flake8_bugbear::rules::jump_statement_in_finally(self, finalbody);
|
flake8_bugbear::rules::jump_statement_in_finally(self, finalbody);
|
||||||
}
|
}
|
||||||
self.visit_body(body);
|
self.visit_body(body);
|
||||||
self.except_handlers.pop();
|
self.handled_exceptions.pop();
|
||||||
|
|
||||||
self.in_exception_handler = true;
|
self.in_exception_handler = true;
|
||||||
for excepthandler in handlers {
|
for excepthandler in handlers {
|
||||||
|
|
@ -4493,13 +4537,12 @@ impl<'a> Checker<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Avoid flagging if NameError is handled.
|
// Avoid flagging if NameError is handled.
|
||||||
if let Some(handler_names) = self.except_handlers.last() {
|
if self
|
||||||
if handler_names
|
.handled_exceptions
|
||||||
.iter()
|
.iter()
|
||||||
.any(|call_path| call_path.as_slice() == ["NameError"])
|
.any(|handler_names| handler_names.contains(Exceptions::NAME_ERROR))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.diagnostics.push(Diagnostic::new(
|
self.diagnostics.push(Diagnostic::new(
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
use ruff_macros::{define_violation, derive_message_formats};
|
|
||||||
use rustpython_parser::ast::{Excepthandler, ExcepthandlerKind, Located, Stmt, StmtKind};
|
use rustpython_parser::ast::{Excepthandler, ExcepthandlerKind, Located, Stmt, StmtKind};
|
||||||
|
|
||||||
|
use ruff_macros::{define_violation, derive_message_formats};
|
||||||
|
|
||||||
use crate::ast::helpers;
|
use crate::ast::helpers;
|
||||||
|
use crate::ast::helpers::compose_call_path;
|
||||||
use crate::ast::types::Range;
|
use crate::ast::types::Range;
|
||||||
use crate::checkers::ast::Checker;
|
use crate::checkers::ast::Checker;
|
||||||
use crate::registry::Diagnostic;
|
use crate::registry::Diagnostic;
|
||||||
|
|
@ -52,9 +54,9 @@ pub fn use_contextlib_suppress(
|
||||||
let ExcepthandlerKind::ExceptHandler { body, .. } = &handler.node;
|
let ExcepthandlerKind::ExceptHandler { body, .. } = &handler.node;
|
||||||
if body.len() == 1 {
|
if body.len() == 1 {
|
||||||
if matches!(body[0].node, StmtKind::Pass) {
|
if matches!(body[0].node, StmtKind::Pass) {
|
||||||
let handler_names: Vec<_> = helpers::extract_handler_names(handlers)
|
let handler_names: Vec<String> = helpers::extract_handled_exceptions(handlers)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|call_path| helpers::format_call_path(&call_path))
|
.filter_map(compose_call_path)
|
||||||
.collect();
|
.collect();
|
||||||
let exception = if handler_names.is_empty() {
|
let exception = if handler_names.is_empty() {
|
||||||
"Exception".to_string()
|
"Exception".to_string()
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ mod tests {
|
||||||
#[test_case(Rule::UnusedImport, Path::new("F401_7.py"); "F401_7")]
|
#[test_case(Rule::UnusedImport, Path::new("F401_7.py"); "F401_7")]
|
||||||
#[test_case(Rule::UnusedImport, Path::new("F401_8.py"); "F401_8")]
|
#[test_case(Rule::UnusedImport, Path::new("F401_8.py"); "F401_8")]
|
||||||
#[test_case(Rule::UnusedImport, Path::new("F401_9.py"); "F401_9")]
|
#[test_case(Rule::UnusedImport, Path::new("F401_9.py"); "F401_9")]
|
||||||
|
#[test_case(Rule::UnusedImport, Path::new("F401_10.py"); "F401_10")]
|
||||||
#[test_case(Rule::ImportShadowedByLoopVar, Path::new("F402.py"); "F402")]
|
#[test_case(Rule::ImportShadowedByLoopVar, Path::new("F402.py"); "F402")]
|
||||||
#[test_case(Rule::ImportStar, Path::new("F403.py"); "F403")]
|
#[test_case(Rule::ImportStar, Path::new("F403.py"); "F403")]
|
||||||
#[test_case(Rule::LateFutureImport, Path::new("F404.py"); "F404")]
|
#[test_case(Rule::LateFutureImport, Path::new("F404.py"); "F404")]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff/src/rules/pyflakes/mod.rs
|
||||||
|
expression: diagnostics
|
||||||
|
---
|
||||||
|
[]
|
||||||
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
source: src/rules/pyflakes/mod.rs
|
source: crates/ruff/src/rules/pyflakes/mod.rs
|
||||||
expression: diagnostics
|
expression: diagnostics
|
||||||
---
|
---
|
||||||
- kind:
|
- kind:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue