Implement runtime-import-in-type-checking-block (TYP004) (#2146)

This commit is contained in:
Charlie Marsh 2023-01-24 23:33:26 -05:00 committed by GitHub
parent deff503932
commit 0758049e49
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 364 additions and 40 deletions

View file

@ -1202,6 +1202,7 @@ For more, see [flake8-type-checking](https://pypi.org/project/flake8-type-checki
| Code | Name | Message | Fix | | Code | Name | Message | Fix |
| ---- | ---- | ------- | --- | | ---- | ---- | ------- | --- |
| TYP004 | runtime-import-in-type-checking-block | Move import `{}` out of type-checking block. Import is used for more than type hinting. | |
| TYP005 | empty-type-checking-block | Found empty type-checking block | | | TYP005 | empty-type-checking-block | Found empty type-checking block | |
### tryceratops (TRY) ### tryceratops (TRY)

View file

@ -0,0 +1,5 @@
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from datetime import datetime
x = datetime

View file

@ -0,0 +1,8 @@
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from datetime import date
def example():
return date()

View file

@ -0,0 +1,6 @@
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Any
CustomType = Any

View file

@ -0,0 +1,11 @@
from typing import TYPE_CHECKING, Type
if TYPE_CHECKING:
from typing import Any
def example(*args: Any, **kwargs: Any):
return
my_type: Type[Any] | Any

View file

@ -0,0 +1,8 @@
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import List, Sequence, Set
def example(a: List[int], /, b: Sequence[int], *, c: Set[int]):
return

View file

@ -0,0 +1,10 @@
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from pandas import DataFrame
def example():
from pandas import DataFrame
x = DataFrame

View file

@ -0,0 +1,10 @@
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import AsyncIterator, List
class Example:
async def example(self) -> AsyncIterator[List[str]]:
yield 0

View file

@ -0,0 +1,7 @@
from typing import TYPE_CHECKING
from weakref import WeakKeyDictionary
if TYPE_CHECKING:
from typing import Any
d = WeakKeyDictionary["Any", "Any"]()

View file

@ -1784,6 +1784,7 @@
"TYP", "TYP",
"TYP0", "TYP0",
"TYP00", "TYP00",
"TYP004",
"TYP005", "TYP005",
"UP", "UP",
"UP0", "UP0",

View file

@ -33,6 +33,10 @@ impl Range {
pub fn from_located<T>(located: &Located<T>) -> Self { pub fn from_located<T>(located: &Located<T>) -> Self {
Range::new(located.location, located.end_location.unwrap()) Range::new(located.location, located.end_location.unwrap())
} }
pub fn contains(&self, other: &Range) -> bool {
self.location <= other.location && self.end_location >= other.end_location
}
} }
#[derive(Debug)] #[derive(Debug)]
@ -130,8 +134,22 @@ pub struct Binding<'a> {
/// The statement in which the [`Binding`] was defined. /// The statement in which the [`Binding`] was defined.
pub source: Option<RefEquality<'a, Stmt>>, pub source: Option<RefEquality<'a, Stmt>>,
/// Tuple of (scope index, range) indicating the scope and range at which /// Tuple of (scope index, range) indicating the scope and range at which
/// the binding was last used. /// the binding was last used in a runtime context.
pub used: Option<(usize, Range)>, pub runtime_usage: Option<(usize, Range)>,
/// Tuple of (scope index, range) indicating the scope and range at which
/// the binding was last used in a typing-time context.
pub typing_usage: Option<(usize, Range)>,
/// Tuple of (scope index, range) indicating the scope and range at which
/// the binding was last used in a synthetic context. This is used for
/// (e.g.) `__future__` imports, explicit re-exports, and other bindings
/// that should be considered used even if they're never referenced.
pub synthetic_usage: Option<(usize, Range)>,
}
#[derive(Copy, Clone)]
pub enum UsageContext {
Runtime,
Typing,
} }
// Pyflakes defines the following binding hierarchy (via inheritance): // Pyflakes defines the following binding hierarchy (via inheritance):
@ -152,6 +170,19 @@ pub struct Binding<'a> {
// FutureImportation // FutureImportation
impl<'a> Binding<'a> { impl<'a> Binding<'a> {
pub fn mark_used(&mut self, scope: usize, range: Range, context: UsageContext) {
match context {
UsageContext::Runtime => self.runtime_usage = Some((scope, range)),
UsageContext::Typing => self.typing_usage = Some((scope, range)),
}
}
pub fn used(&self) -> bool {
self.runtime_usage.is_some()
|| self.synthetic_usage.is_some()
|| self.typing_usage.is_some()
}
pub fn is_definition(&self) -> bool { pub fn is_definition(&self) -> bool {
matches!( matches!(
self.kind, self.kind,

View file

@ -20,7 +20,7 @@ use crate::ast::operations::extract_all_names;
use crate::ast::relocate::relocate_expr; use crate::ast::relocate::relocate_expr;
use crate::ast::types::{ use crate::ast::types::{
Binding, BindingKind, CallPath, ClassDef, FunctionDef, Lambda, Node, Range, RefEquality, Scope, Binding, BindingKind, CallPath, ClassDef, FunctionDef, Lambda, Node, Range, RefEquality, Scope,
ScopeKind, ScopeKind, UsageContext,
}; };
use crate::ast::visitor::{walk_excepthandler, Visitor}; use crate::ast::visitor::{walk_excepthandler, Visitor};
use crate::ast::{branch_detection, cast, helpers, operations, visitor}; use crate::ast::{branch_detection, cast, helpers, operations, visitor};
@ -92,12 +92,14 @@ pub struct Checker<'a> {
in_deferred_type_definition: bool, in_deferred_type_definition: bool,
in_literal: bool, in_literal: bool,
in_subscript: bool, in_subscript: bool,
in_type_checking_block: bool,
seen_import_boundary: bool, 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>>>, except_handlers: Vec<Vec<Vec<&'a str>>>,
// Check-specific state. // Check-specific state.
pub(crate) flake8_bugbear_seen: Vec<&'a Expr>, pub(crate) flake8_bugbear_seen: Vec<&'a Expr>,
pub(crate) type_checking_blocks: Vec<&'a Stmt>,
} }
impl<'a> Checker<'a> { impl<'a> Checker<'a> {
@ -149,12 +151,14 @@ impl<'a> Checker<'a> {
in_deferred_type_definition: false, in_deferred_type_definition: false,
in_literal: false, in_literal: false,
in_subscript: false, in_subscript: false,
in_type_checking_block: false,
seen_import_boundary: false, seen_import_boundary: false,
futures_allowed: true, futures_allowed: true,
annotations_future_enabled: path.extension().map_or(false, |ext| ext == "pyi"), annotations_future_enabled: path.extension().map_or(false, |ext| ext == "pyi"),
except_handlers: vec![], except_handlers: vec![],
// Check-specific state. // Check-specific state.
flake8_bugbear_seen: vec![], flake8_bugbear_seen: vec![],
type_checking_blocks: vec![],
} }
} }
@ -316,6 +320,8 @@ where
} }
} }
let prev_in_type_checking_block = self.in_type_checking_block;
// Pre-visit. // Pre-visit.
match &stmt.node { match &stmt.node {
StmtKind::Global { names } => { StmtKind::Global { names } => {
@ -329,7 +335,9 @@ where
let index = self.bindings.len(); let index = self.bindings.len();
self.bindings.push(Binding { self.bindings.push(Binding {
kind: BindingKind::Global, kind: BindingKind::Global,
used: usage, runtime_usage: None,
synthetic_usage: usage,
typing_usage: None,
range: *range, range: *range,
source: Some(RefEquality(stmt)), source: Some(RefEquality(stmt)),
}); });
@ -355,7 +363,9 @@ where
let index = self.bindings.len(); let index = self.bindings.len();
self.bindings.push(Binding { self.bindings.push(Binding {
kind: BindingKind::Nonlocal, kind: BindingKind::Nonlocal,
used: usage, runtime_usage: None,
synthetic_usage: usage,
typing_usage: None,
range: *range, range: *range,
source: Some(RefEquality(stmt)), source: Some(RefEquality(stmt)),
}); });
@ -369,7 +379,7 @@ where
for index in self.scope_stack.iter().skip(1).rev().skip(1) { for index in self.scope_stack.iter().skip(1).rev().skip(1) {
if let Some(index) = self.scopes[*index].values.get(&name.as_str()) { if let Some(index) = self.scopes[*index].values.get(&name.as_str()) {
exists = true; exists = true;
self.bindings[*index].used = usage; self.bindings[*index].runtime_usage = usage;
} }
} }
@ -667,7 +677,9 @@ where
name, name,
Binding { Binding {
kind: BindingKind::FunctionDefinition, kind: BindingKind::FunctionDefinition,
used: None, runtime_usage: None,
synthetic_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()),
}, },
@ -819,7 +831,9 @@ where
name, name,
Binding { Binding {
kind: BindingKind::SubmoduleImportation(name, full_name), kind: BindingKind::SubmoduleImportation(name, full_name),
used: None, runtime_usage: None,
synthetic_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()),
}, },
@ -838,9 +852,10 @@ where
name, name,
Binding { Binding {
kind: BindingKind::Importation(name, full_name), kind: BindingKind::Importation(name, full_name),
runtime_usage: None,
// Treat explicit re-export as usage (e.g., `import applications // Treat explicit re-export as usage (e.g., `import applications
// as applications`). // as applications`).
used: if alias synthetic_usage: if alias
.node .node
.asname .asname
.as_ref() .as_ref()
@ -857,6 +872,7 @@ where
} else { } else {
None 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()),
}, },
@ -1086,8 +1102,9 @@ where
name, name,
Binding { Binding {
kind: BindingKind::FutureImportation, kind: BindingKind::FutureImportation,
runtime_usage: None,
// Always mark `__future__` imports as used. // Always mark `__future__` imports as used.
used: Some(( synthetic_usage: Some((
self.scopes[*(self self.scopes[*(self
.scope_stack .scope_stack
.last() .last()
@ -1095,6 +1112,7 @@ where
.id, .id,
Range::from_located(alias), Range::from_located(alias),
)), )),
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()),
}, },
@ -1128,7 +1146,9 @@ where
"*", "*",
Binding { Binding {
kind: BindingKind::StarImportation(*level, module.clone()), kind: BindingKind::StarImportation(*level, module.clone()),
used: None, runtime_usage: None,
synthetic_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()),
}, },
@ -1182,9 +1202,10 @@ where
name, name,
Binding { Binding {
kind: BindingKind::FromImportation(name, full_name), kind: BindingKind::FromImportation(name, full_name),
runtime_usage: None,
// Treat explicit re-export as usage (e.g., `from .applications // Treat explicit re-export as usage (e.g., `from .applications
// import FastAPI as FastAPI`). // import FastAPI as FastAPI`).
used: if alias synthetic_usage: if alias
.node .node
.asname .asname
.as_ref() .as_ref()
@ -1201,6 +1222,7 @@ where
} else { } else {
None None
}, },
typing_usage: None,
range, range,
source: Some(self.current_stmt().clone()), source: Some(self.current_stmt().clone()),
}, },
@ -1374,12 +1396,16 @@ where
self.handle_node_load(target); self.handle_node_load(target);
} }
StmtKind::If { test, body, orelse } => { StmtKind::If { test, body, orelse } => {
if self.settings.rules.enabled(&Rule::IfTuple) { if flake8_type_checking::helpers::is_type_checking_block(self, test) {
pyflakes::rules::if_tuple(self, stmt, test);
}
if self.settings.rules.enabled(&Rule::EmptyTypeCheckingBlock) { if self.settings.rules.enabled(&Rule::EmptyTypeCheckingBlock) {
flake8_type_checking::rules::empty_type_checking_block(self, test, body); flake8_type_checking::rules::empty_type_checking_block(self, test, body);
} }
self.in_type_checking_block = true;
self.type_checking_blocks.push(stmt);
}
if self.settings.rules.enabled(&Rule::IfTuple) {
pyflakes::rules::if_tuple(self, stmt, test);
}
if self.settings.rules.enabled(&Rule::NestedIfStatements) { if self.settings.rules.enabled(&Rule::NestedIfStatements) {
flake8_simplify::rules::nested_if_statements( flake8_simplify::rules::nested_if_statements(
self, self,
@ -1709,7 +1735,9 @@ where
let index = self.bindings.len(); let index = self.bindings.len();
self.bindings.push(Binding { self.bindings.push(Binding {
kind: BindingKind::Assignment, kind: BindingKind::Assignment,
used: None, runtime_usage: None,
synthetic_usage: None,
typing_usage: None,
range: Range::from_located(stmt), range: Range::from_located(stmt),
source: Some(RefEquality(stmt)), source: Some(RefEquality(stmt)),
}); });
@ -1770,7 +1798,9 @@ where
let index = self.bindings.len(); let index = self.bindings.len();
self.bindings.push(Binding { self.bindings.push(Binding {
kind: BindingKind::Assignment, kind: BindingKind::Assignment,
used: None, runtime_usage: None,
synthetic_usage: None,
typing_usage: None,
range: Range::from_located(stmt), range: Range::from_located(stmt),
source: Some(RefEquality(stmt)), source: Some(RefEquality(stmt)),
}); });
@ -1831,6 +1861,7 @@ where
} }
_ => visitor::walk_stmt(self, stmt), _ => visitor::walk_stmt(self, stmt),
}; };
self.in_type_checking_block = prev_in_type_checking_block;
self.visible_scope = prev_visible_scope; self.visible_scope = prev_visible_scope;
// Post-visit. // Post-visit.
@ -1844,7 +1875,9 @@ where
name, name,
Binding { Binding {
kind: BindingKind::ClassDefinition, kind: BindingKind::ClassDefinition,
used: None, runtime_usage: None,
synthetic_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()),
}, },
@ -3414,7 +3447,7 @@ where
[*(self.scope_stack.last().expect("No current scope found"))]; [*(self.scope_stack.last().expect("No current scope found"))];
&scope.values.remove(&name.as_str()) &scope.values.remove(&name.as_str())
} { } {
if self.bindings[*index].used.is_none() { if !self.bindings[*index].used() {
if self.settings.rules.enabled(&Rule::UnusedVariable) { if self.settings.rules.enabled(&Rule::UnusedVariable) {
let mut diagnostic = Diagnostic::new( let mut diagnostic = Diagnostic::new(
violations::UnusedVariable(name.to_string()), violations::UnusedVariable(name.to_string()),
@ -3532,7 +3565,9 @@ where
&arg.node.arg, &arg.node.arg,
Binding { Binding {
kind: BindingKind::Argument, kind: BindingKind::Argument,
used: None, runtime_usage: None,
synthetic_usage: None,
typing_usage: None,
range: Range::from_located(arg), range: Range::from_located(arg),
source: Some(self.current_stmt().clone()), source: Some(self.current_stmt().clone()),
}, },
@ -3631,7 +3666,9 @@ impl<'a> Checker<'a> {
self.bindings.push(Binding { self.bindings.push(Binding {
kind: BindingKind::Builtin, kind: BindingKind::Builtin,
range: Range::default(), range: Range::default(),
used: None, runtime_usage: None,
synthetic_usage: None,
typing_usage: None,
source: None, source: None,
}); });
scope.values.insert(builtin, index); scope.values.insert(builtin, index);
@ -3716,7 +3753,7 @@ impl<'a> Checker<'a> {
)); ));
} }
} else if in_current_scope { } else if in_current_scope {
if existing.used.is_none() if !existing.used()
&& binding.redefines(existing) && binding.redefines(existing)
&& (!self.settings.dummy_variable_rgx.is_match(name) || existing_is_import) && (!self.settings.dummy_variable_rgx.is_match(name) || existing_is_import)
&& !(matches!(existing.kind, BindingKind::FunctionDefinition) && !(matches!(existing.kind, BindingKind::FunctionDefinition)
@ -3749,7 +3786,9 @@ impl<'a> Checker<'a> {
let binding = match scope.values.get(&name) { let binding = match scope.values.get(&name) {
None => binding, None => binding,
Some(index) => Binding { Some(index) => Binding {
used: self.bindings[*index].used, runtime_usage: self.bindings[*index].runtime_usage,
synthetic_usage: self.bindings[*index].synthetic_usage,
typing_usage: self.bindings[*index].typing_usage,
..binding ..binding
}, },
}; };
@ -3784,8 +3823,14 @@ impl<'a> Checker<'a> {
} }
if let Some(index) = scope.values.get(&id.as_str()) { if let Some(index) = scope.values.get(&id.as_str()) {
let context = if self.in_type_definition || self.in_type_checking_block {
UsageContext::Typing
} else {
UsageContext::Runtime
};
// Mark the binding as used. // Mark the binding as used.
self.bindings[*index].used = Some((scope_id, Range::from_located(expr))); self.bindings[*index].mark_used(scope_id, Range::from_located(expr), context);
if matches!(self.bindings[*index].kind, BindingKind::Annotation) if matches!(self.bindings[*index].kind, BindingKind::Annotation)
&& !self.in_deferred_string_type_definition && !self.in_deferred_string_type_definition
@ -3813,8 +3858,11 @@ impl<'a> Checker<'a> {
if has_alias { if has_alias {
// Mark the sub-importation as used. // Mark the sub-importation as used.
if let Some(index) = scope.values.get(full_name) { if let Some(index) = scope.values.get(full_name) {
self.bindings[*index].used = self.bindings[*index].mark_used(
Some((scope_id, Range::from_located(expr))); scope_id,
Range::from_located(expr),
context,
);
} }
} }
} }
@ -3827,8 +3875,11 @@ impl<'a> Checker<'a> {
if has_alias { if has_alias {
// Mark the sub-importation as used. // Mark the sub-importation as used.
if let Some(index) = scope.values.get(full_name.as_str()) { if let Some(index) = scope.values.get(full_name.as_str()) {
self.bindings[*index].used = self.bindings[*index].mark_used(
Some((scope_id, Range::from_located(expr))); scope_id,
Range::from_located(expr),
context,
);
} }
} }
} }
@ -3956,7 +4007,9 @@ impl<'a> Checker<'a> {
id, id,
Binding { Binding {
kind: BindingKind::Annotation, kind: BindingKind::Annotation,
used: None, runtime_usage: None,
synthetic_usage: None,
typing_usage: None,
range: Range::from_located(expr), range: Range::from_located(expr),
source: Some(self.current_stmt().clone()), source: Some(self.current_stmt().clone()),
}, },
@ -3973,7 +4026,9 @@ impl<'a> Checker<'a> {
id, id,
Binding { Binding {
kind: BindingKind::LoopVar, kind: BindingKind::LoopVar,
used: None, runtime_usage: None,
synthetic_usage: None,
typing_usage: None,
range: Range::from_located(expr), range: Range::from_located(expr),
source: Some(self.current_stmt().clone()), source: Some(self.current_stmt().clone()),
}, },
@ -3986,7 +4041,9 @@ impl<'a> Checker<'a> {
id, id,
Binding { Binding {
kind: BindingKind::Binding, kind: BindingKind::Binding,
used: None, runtime_usage: None,
synthetic_usage: None,
typing_usage: None,
range: Range::from_located(expr), range: Range::from_located(expr),
source: Some(self.current_stmt().clone()), source: Some(self.current_stmt().clone()),
}, },
@ -4036,7 +4093,9 @@ impl<'a> Checker<'a> {
current, current,
&self.bindings, &self.bindings,
)), )),
used: None, runtime_usage: None,
synthetic_usage: None,
typing_usage: None,
range: Range::from_located(expr), range: Range::from_located(expr),
source: Some(self.current_stmt().clone()), source: Some(self.current_stmt().clone()),
}, },
@ -4049,7 +4108,9 @@ impl<'a> Checker<'a> {
id, id,
Binding { Binding {
kind: BindingKind::Assignment, kind: BindingKind::Assignment,
used: None, runtime_usage: None,
synthetic_usage: None,
typing_usage: None,
range: Range::from_located(expr), range: Range::from_located(expr),
source: Some(self.current_stmt().clone()), source: Some(self.current_stmt().clone()),
}, },
@ -4237,6 +4298,10 @@ impl<'a> Checker<'a> {
.settings .settings
.rules .rules
.enabled(&Rule::GlobalVariableNotAssigned) .enabled(&Rule::GlobalVariableNotAssigned)
&& !self
.settings
.rules
.enabled(&Rule::RuntimeImportInTypeCheckingBlock)
{ {
return; return;
} }
@ -4313,7 +4378,7 @@ impl<'a> Checker<'a> {
| BindingKind::FutureImportation | BindingKind::FutureImportation
) { ) {
// Skip used exports from `__all__` // Skip used exports from `__all__`
if binding.used.is_some() if binding.used()
|| all_names || all_names
.as_ref() .as_ref()
.map(|names| names.contains(name)) .map(|names| names.contains(name))
@ -4369,6 +4434,24 @@ impl<'a> Checker<'a> {
} }
} }
// TYP001
for (_name, index) in scope
.values
.iter()
.chain(scope.overridden.iter().map(|(a, b)| (a, b)))
{
let binding = &self.bindings[*index];
if let Some(diagnostic) =
flake8_type_checking::rules::runtime_import_in_type_checking_block(
binding,
&self.type_checking_blocks,
)
{
diagnostics.push(diagnostic);
}
}
if self.settings.rules.enabled(&Rule::UnusedImport) { if self.settings.rules.enabled(&Rule::UnusedImport) {
// Collect all unused imports by location. (Multiple unused imports at the same // Collect all unused imports by location. (Multiple unused imports at the same
// location indicates an `import from`.) // location indicates an `import from`.)
@ -4395,7 +4478,7 @@ impl<'a> Checker<'a> {
}; };
// Skip used exports from `__all__` // Skip used exports from `__all__`
if binding.used.is_some() if binding.used()
|| all_names || all_names
.as_ref() .as_ref()
.map(|names| names.contains(name)) .map(|names| names.contains(name))

View file

@ -429,6 +429,7 @@ ruff_macros::define_rule_mapping!(
EXE004 => rules::flake8_executable::rules::ShebangWhitespace, EXE004 => rules::flake8_executable::rules::ShebangWhitespace,
EXE005 => rules::flake8_executable::rules::ShebangNewline, EXE005 => rules::flake8_executable::rules::ShebangNewline,
// flake8-type-checking // flake8-type-checking
TYP004 => rules::flake8_type_checking::rules::RuntimeImportInTypeCheckingBlock,
TYP005 => rules::flake8_type_checking::rules::EmptyTypeCheckingBlock, TYP005 => rules::flake8_type_checking::rules::EmptyTypeCheckingBlock,
// tryceratops // tryceratops
TRY004 => rules::tryceratops::rules::PreferTypeError, TRY004 => rules::tryceratops::rules::PreferTypeError,

View file

@ -0,0 +1,9 @@
use rustpython_ast::Expr;
use crate::checkers::ast::Checker;
pub fn is_type_checking_block(checker: &Checker, test: &Expr) -> bool {
checker.resolve_call_path(test).map_or(false, |call_path| {
call_path.as_slice() == ["typing", "TYPE_CHECKING"]
})
}

View file

@ -1,4 +1,5 @@
//! Rules from [flake8-type-checking](https://pypi.org/project/flake8-type-checking/). //! Rules from [flake8-type-checking](https://pypi.org/project/flake8-type-checking/).
pub(crate) mod helpers;
pub(crate) mod rules; pub(crate) mod rules;
#[cfg(test)] #[cfg(test)]
@ -13,6 +14,14 @@ mod tests {
use crate::registry::Rule; use crate::registry::Rule;
use crate::settings; use crate::settings;
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TYP004_1.py"); "TYP004_1")]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TYP004_2.py"); "TYP004_2")]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TYP004_3.py"); "TYP004_3")]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TYP004_4.py"); "TYP004_4")]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TYP004_5.py"); "TYP004_5")]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TYP004_6.py"); "TYP004_6")]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TYP004_7.py"); "TYP004_7")]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TYP004_8.py"); "TYP004_8")]
#[test_case(Rule::EmptyTypeCheckingBlock, Path::new("TYP005.py"); "TYP005")] #[test_case(Rule::EmptyTypeCheckingBlock, Path::new("TYP005.py"); "TYP005")]
fn rules(rule_code: Rule, path: &Path) -> Result<()> { fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy()); let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());

View file

@ -1,3 +1,7 @@
pub use empty_type_checking_block::{empty_type_checking_block, EmptyTypeCheckingBlock}; pub use empty_type_checking_block::{empty_type_checking_block, EmptyTypeCheckingBlock};
pub use runtime_import_in_type_checking_block::{
runtime_import_in_type_checking_block, RuntimeImportInTypeCheckingBlock,
};
mod empty_type_checking_block; mod empty_type_checking_block;
mod runtime_import_in_type_checking_block;

View file

@ -0,0 +1,52 @@
use ruff_macros::derive_message_formats;
use rustpython_ast::Stmt;
use crate::ast::types::{Binding, BindingKind, Range};
use crate::define_violation;
use crate::registry::Diagnostic;
use crate::violation::Violation;
define_violation!(
pub struct RuntimeImportInTypeCheckingBlock {
pub full_name: String,
}
);
impl Violation for RuntimeImportInTypeCheckingBlock {
#[derive_message_formats]
fn message(&self) -> String {
format!(
"Move import `{}` out of type-checking block. Import is used for more than type \
hinting.",
self.full_name
)
}
}
/// TYP004
pub fn runtime_import_in_type_checking_block(
binding: &Binding,
blocks: &[&Stmt],
) -> Option<Diagnostic> {
let full_name = match &binding.kind {
BindingKind::Importation(.., full_name) => full_name,
BindingKind::FromImportation(.., full_name) => full_name.as_str(),
BindingKind::SubmoduleImportation(.., full_name) => full_name,
_ => return None,
};
let defined_in_type_checking = blocks
.iter()
.any(|block| Range::from_located(block).contains(&binding.range));
if defined_in_type_checking {
if binding.runtime_usage.is_some() {
return Some(Diagnostic::new(
RuntimeImportInTypeCheckingBlock {
full_name: full_name.to_string(),
},
binding.range,
));
}
}
None
}

View file

@ -0,0 +1,16 @@
---
source: src/rules/flake8_type_checking/mod.rs
expression: diagnostics
---
- kind:
RuntimeImportInTypeCheckingBlock:
full_name: datetime.datetime
location:
row: 4
column: 25
end_location:
row: 4
column: 33
fix: ~
parent: ~

View file

@ -0,0 +1,16 @@
---
source: src/rules/flake8_type_checking/mod.rs
expression: diagnostics
---
- kind:
RuntimeImportInTypeCheckingBlock:
full_name: datetime.date
location:
row: 4
column: 25
end_location:
row: 4
column: 29
fix: ~
parent: ~

View file

@ -0,0 +1,6 @@
---
source: src/rules/flake8_type_checking/mod.rs
expression: diagnostics
---
[]

View file

@ -0,0 +1,6 @@
---
source: src/rules/flake8_type_checking/mod.rs
expression: diagnostics
---
[]

View file

@ -0,0 +1,6 @@
---
source: src/rules/flake8_type_checking/mod.rs
expression: diagnostics
---
[]

View file

@ -0,0 +1,6 @@
---
source: src/rules/flake8_type_checking/mod.rs
expression: diagnostics
---
[]

View file

@ -0,0 +1,6 @@
---
source: src/rules/flake8_type_checking/mod.rs
expression: diagnostics
---
[]

View file

@ -0,0 +1,6 @@
---
source: src/rules/flake8_type_checking/mod.rs
expression: diagnostics
---
[]

View file

@ -43,7 +43,7 @@ fn function(
.get(&arg.node.arg.as_str()) .get(&arg.node.arg.as_str())
.map(|index| &bindings[*index]) .map(|index| &bindings[*index])
{ {
if binding.used.is_none() if !binding.used()
&& matches!(binding.kind, BindingKind::Argument) && matches!(binding.kind, BindingKind::Argument)
&& !dummy_variable_rgx.is_match(arg.node.arg.as_str()) && !dummy_variable_rgx.is_match(arg.node.arg.as_str())
{ {
@ -88,7 +88,7 @@ fn method(
.get(&arg.node.arg.as_str()) .get(&arg.node.arg.as_str())
.map(|index| &bindings[*index]) .map(|index| &bindings[*index])
{ {
if binding.used.is_none() if !binding.used()
&& matches!(binding.kind, BindingKind::Argument) && matches!(binding.kind, BindingKind::Argument)
&& !dummy_variable_rgx.is_match(arg.node.arg.as_str()) && !dummy_variable_rgx.is_match(arg.node.arg.as_str())
{ {

View file

@ -44,7 +44,7 @@ pub fn undefined_local(name: &str, scopes: &[&Scope], bindings: &[Binding]) -> O
for scope in scopes.iter().rev().skip(1) { for scope in scopes.iter().rev().skip(1) {
if matches!(scope.kind, ScopeKind::Function(_) | ScopeKind::Module) { if matches!(scope.kind, ScopeKind::Function(_) | ScopeKind::Module) {
if let Some(binding) = scope.values.get(name).map(|index| &bindings[*index]) { if let Some(binding) = scope.values.get(name).map(|index| &bindings[*index]) {
if let Some((scope_id, location)) = binding.used { if let Some((scope_id, location)) = binding.runtime_usage {
if scope_id == current.id { if scope_id == current.id {
return Some(Diagnostic::new( return Some(Diagnostic::new(
violations::UndefinedLocal(name.to_string()), violations::UndefinedLocal(name.to_string()),

View file

@ -11,7 +11,7 @@ pub fn unused_annotation(checker: &mut Checker, scope: usize) {
.iter() .iter()
.map(|(name, index)| (name, &checker.bindings[*index])) .map(|(name, index)| (name, &checker.bindings[*index]))
{ {
if binding.used.is_none() if !binding.used()
&& matches!(binding.kind, BindingKind::Annotation) && matches!(binding.kind, BindingKind::Annotation)
&& !checker.settings.dummy_variable_rgx.is_match(name) && !checker.settings.dummy_variable_rgx.is_match(name)
{ {

View file

@ -156,7 +156,7 @@ pub fn unused_variable(checker: &mut Checker, scope: usize) {
.iter() .iter()
.map(|(name, index)| (name, &checker.bindings[*index])) .map(|(name, index)| (name, &checker.bindings[*index]))
{ {
if binding.used.is_none() if !binding.used()
&& matches!(binding.kind, BindingKind::Assignment) && matches!(binding.kind, BindingKind::Assignment)
&& !checker.settings.dummy_variable_rgx.is_match(name) && !checker.settings.dummy_variable_rgx.is_match(name)
&& name != &"__tracebackhide__" && name != &"__tracebackhide__"