use std::iter::once;
use ruff_python_ast::{Alias, ExceptHandler, Stmt};
use ruff_python_parser::parse_module;
use ruff_text_size::{Ranged, TextRange};
#[derive(Debug, Default, PartialEq, Eq)]
pub struct SnippetImports {
pub prelude: String,
pub modules: Vec<ImportedModule>,
}
#[derive(Debug, PartialEq, Eq)]
pub struct ImportedModule {
pub name: String,
pub range: TextRange,
}
#[must_use]
pub fn scan_imports(source: &str) -> SnippetImports {
let Ok(parsed) = parse_module(source) else {
return SnippetImports::default();
};
let mut imports = SnippetImports::default();
collect(&parsed.syntax().body, &mut imports);
imports
}
impl SnippetImports {
fn add(&mut self, line: &str, modules: impl Iterator<Item = (String, TextRange)>) {
self.prelude.push_str(line);
self.prelude.push('\n');
for (name, range) in modules {
if !self.modules.iter().any(|module| module.name == name) {
self.modules.push(ImportedModule { name, range });
}
}
}
}
fn collect(body: &[Stmt], imports: &mut SnippetImports) {
for statement in body {
match statement {
Stmt::Import(import) => {
let range = import.range();
imports.add(
&format!("import {}", aliases(&import.names)),
import.names.iter().map(|alias| (top_level(alias.name.as_str()), range)),
);
}
Stmt::ImportFrom(import) if import.level == 0 => {
if let Some(module) = &import.module
&& module.as_str() != "__future__"
{
imports.add(
&format!("from {} import {}", module.as_str(), aliases(&import.names)),
once((top_level(module.as_str()), import.range())),
);
}
}
Stmt::If(node) => {
collect(&node.body, imports);
for clause in &node.elif_else_clauses {
collect(&clause.body, imports);
}
}
Stmt::For(node) => {
collect(&node.body, imports);
collect(&node.orelse, imports);
}
Stmt::While(node) => {
collect(&node.body, imports);
collect(&node.orelse, imports);
}
Stmt::With(node) => collect(&node.body, imports),
Stmt::Try(node) => {
collect(&node.body, imports);
for ExceptHandler::ExceptHandler(handler) in &node.handlers {
collect(&handler.body, imports);
}
collect(&node.orelse, imports);
collect(&node.finalbody, imports);
}
Stmt::Match(node) => {
for case in &node.cases {
collect(&case.body, imports);
}
}
_ => {}
}
}
}
fn top_level(dotted: &str) -> String {
dotted.split('.').next().unwrap_or_default().to_owned()
}
fn aliases(names: &[Alias]) -> String {
names
.iter()
.map(|alias| match &alias.asname {
Some(asname) => format!("{} as {}", alias.name.as_str(), asname.as_str()),
None => alias.name.as_str().to_owned(),
})
.collect::<Vec<_>>()
.join(", ")
}