use crate::StringImports;
use ruff_python_ast::visitor::source_order::{
SourceOrderVisitor, walk_expr, walk_module, walk_stmt,
};
use ruff_python_ast::{self as ast, Expr, Mod, Stmt};
use ty_module_resolver::ModuleName;
#[derive(Default, Debug)]
pub(crate) struct Collector<'a> {
module_path: Option<&'a [String]>,
string_imports: StringImports,
imports: Vec<CollectedImport>,
type_checking_imports: bool,
}
impl<'a> Collector<'a> {
pub(crate) fn new(
module_path: Option<&'a [String]>,
string_imports: StringImports,
type_checking_imports: bool,
) -> Self {
Self {
module_path,
string_imports,
imports: Vec::new(),
type_checking_imports,
}
}
#[must_use]
pub(crate) fn collect(mut self, module: &Mod) -> Vec<CollectedImport> {
walk_module(&mut self, module);
self.imports
}
}
impl<'ast> SourceOrderVisitor<'ast> for Collector<'_> {
fn visit_stmt(&mut self, stmt: &'ast Stmt) {
match stmt {
Stmt::ImportFrom(ast::StmtImportFrom {
names,
module,
level,
is_lazy: _,
range: _,
node_index: _,
}) => {
let module = module.as_deref();
let level = *level;
for alias in names {
let mut components = vec![];
if level > 0 {
let Some(module_path) = self.module_path else {
return;
};
components.extend(module_path.iter().map(String::as_str));
for _ in 0..level {
if components.is_empty() {
return;
}
components.pop();
}
}
if let Some(module) = module {
components.extend(module.split('.'));
}
if alias.name.as_str() != "*" {
components.push(alias.name.as_str());
}
if let Some(module_name) = ModuleName::from_components(components) {
self.imports.push(CollectedImport::ImportFrom(module_name));
}
}
}
Stmt::Import(ast::StmtImport {
names,
is_lazy: _,
range: _,
node_index: _,
}) => {
for alias in names {
if let Some(module_name) = ModuleName::new(alias.name.as_str()) {
self.imports.push(CollectedImport::Import(module_name));
}
}
}
Stmt::If(ast::StmtIf {
test,
body,
elif_else_clauses,
range: _,
node_index: _,
}) => {
if self.type_checking_imports || !is_type_checking_condition(test) {
self.visit_body(body);
}
for clause in elif_else_clauses {
self.visit_elif_else_clause(clause);
}
}
Stmt::FunctionDef(_)
| Stmt::ClassDef(_)
| Stmt::While(_)
| Stmt::With(_)
| Stmt::Match(_)
| Stmt::Try(_)
| Stmt::For(_) => {
walk_stmt(self, stmt);
}
Stmt::Return(_)
| Stmt::Delete(_)
| Stmt::Assign(_)
| Stmt::AugAssign(_)
| Stmt::AnnAssign(_)
| Stmt::TypeAlias(_)
| Stmt::Raise(_)
| Stmt::Assert(_)
| Stmt::Global(_)
| Stmt::Nonlocal(_)
| Stmt::Expr(_)
| Stmt::Pass(_)
| Stmt::Break(_)
| Stmt::Continue(_)
| Stmt::IpyEscapeCommand(_) => {
if self.string_imports.enabled {
walk_stmt(self, stmt);
}
}
}
}
fn visit_expr(&mut self, expr: &'ast Expr) {
if self.string_imports.enabled {
if let Expr::StringLiteral(ast::ExprStringLiteral {
value,
range: _,
node_index: _,
}) = expr
{
let value = value.to_str();
if self.string_imports.min_dots == 0
|| memchr::memchr_iter(b'.', value.as_bytes()).count()
>= self.string_imports.min_dots
{
if let Some(module_name) = ModuleName::new(value) {
self.imports.push(CollectedImport::StringImport(
module_name,
self.string_imports.min_dots,
));
}
}
}
walk_expr(self, expr);
}
}
}
fn is_type_checking_condition(expr: &Expr) -> bool {
match expr {
Expr::Name(ast::ExprName { id, .. }) => id.as_str() == "TYPE_CHECKING",
Expr::Attribute(ast::ExprAttribute { value, attr, .. }) => {
attr.as_str() == "TYPE_CHECKING"
&& matches!(
value.as_ref(),
Expr::Name(ast::ExprName { id, .. }) if id.as_str() == "typing"
)
}
_ => false,
}
}
#[derive(Debug)]
pub(crate) enum CollectedImport {
Import(ModuleName),
ImportFrom(ModuleName),
StringImport(ModuleName, usize),
}