use std::path::Path;
use anyhow::Result;
use rustpython_parser::{Parse, ast as py_ast};
use super::{
ExportFact, ExportKind, ImportFact, ImportKind, ImportTarget, ModuleFacts, ReexportFact,
ReexportTarget,
};
pub(crate) fn parse_python_module(path: &Path, source: &str) -> Result<ModuleFacts> {
let suite = py_ast::Suite::parse(source, &path.display().to_string()).map_err(|error| {
anyhow::anyhow!("failed to parse python module {}: {error}", path.display())
})?;
let mut facts = ModuleFacts::empty(path);
for stmt in &suite {
collect_python_stmt(stmt, &mut facts);
}
facts
.used_locals
.extend(facts.imports.iter().map(|import| import.local.clone()));
Ok(facts)
}
fn collect_python_stmt(stmt: &py_ast::Stmt, facts: &mut ModuleFacts) {
match stmt {
py_ast::Stmt::FunctionDef(function) => {
add_python_export(facts, function.name.to_string());
collect_python_nested_imports(&function.body, facts);
}
py_ast::Stmt::AsyncFunctionDef(function) => {
add_python_export(facts, function.name.to_string());
collect_python_nested_imports(&function.body, facts);
}
py_ast::Stmt::ClassDef(class) => {
add_python_export(facts, class.name.to_string());
collect_python_nested_imports(&class.body, facts);
}
py_ast::Stmt::Assign(assign) => {
for target in &assign.targets {
collect_python_assignment_target(target, facts);
}
}
py_ast::Stmt::AnnAssign(assign) => collect_python_assignment_target(&assign.target, facts),
py_ast::Stmt::Import(import) => collect_python_import(import, facts),
py_ast::Stmt::ImportFrom(import) => collect_python_import_from(import, facts),
other => {
for suite in python_nested_suites(other) {
for stmt in suite {
collect_python_stmt(stmt, facts);
}
}
}
}
}
fn collect_python_nested_imports(suite: &[py_ast::Stmt], facts: &mut ModuleFacts) {
for stmt in suite {
match stmt {
py_ast::Stmt::Import(import) => collect_python_import(import, facts),
py_ast::Stmt::ImportFrom(import) => collect_python_import_from(import, facts),
py_ast::Stmt::FunctionDef(function) => {
collect_python_nested_imports(&function.body, facts);
}
py_ast::Stmt::AsyncFunctionDef(function) => {
collect_python_nested_imports(&function.body, facts);
}
py_ast::Stmt::ClassDef(class) => collect_python_nested_imports(&class.body, facts),
other => {
for inner in python_nested_suites(other) {
collect_python_nested_imports(inner, facts);
}
}
}
}
}
fn python_nested_suites(stmt: &py_ast::Stmt) -> Vec<&[py_ast::Stmt]> {
match stmt {
py_ast::Stmt::If(stmt) => vec![&stmt.body, &stmt.orelse],
py_ast::Stmt::While(stmt) => vec![&stmt.body, &stmt.orelse],
py_ast::Stmt::For(stmt) => vec![&stmt.body, &stmt.orelse],
py_ast::Stmt::AsyncFor(stmt) => vec![&stmt.body, &stmt.orelse],
py_ast::Stmt::With(stmt) => vec![&stmt.body],
py_ast::Stmt::AsyncWith(stmt) => vec![&stmt.body],
py_ast::Stmt::Try(stmt) => {
python_try_suites(&stmt.body, &stmt.handlers, &stmt.orelse, &stmt.finalbody)
}
py_ast::Stmt::TryStar(stmt) => {
python_try_suites(&stmt.body, &stmt.handlers, &stmt.orelse, &stmt.finalbody)
}
py_ast::Stmt::Match(stmt) => stmt.cases.iter().map(|case| case.body.as_slice()).collect(),
_ => Vec::new(),
}
}
fn python_try_suites<'a>(
body: &'a [py_ast::Stmt],
handlers: &'a [py_ast::ExceptHandler],
orelse: &'a [py_ast::Stmt],
finalbody: &'a [py_ast::Stmt],
) -> Vec<&'a [py_ast::Stmt]> {
let mut suites = vec![body, orelse, finalbody];
suites.extend(handlers.iter().map(|handler| {
let py_ast::ExceptHandler::ExceptHandler(handler) = handler;
handler.body.as_slice()
}));
suites
}
fn collect_python_import(import: &py_ast::StmtImport, facts: &mut ModuleFacts) {
for alias in &import.names {
let source = alias.name.to_string();
let local = alias
.asname
.as_ref()
.map(ToString::to_string)
.unwrap_or_else(|| source.split('.').next().unwrap_or(&source).to_string());
facts.imports.push(ImportFact {
source,
local,
imported: ImportTarget::Namespace,
kind: ImportKind::Esm,
type_only: false,
});
}
}
fn collect_python_import_from(import: &py_ast::StmtImportFrom, facts: &mut ModuleFacts) {
let level = import
.level
.as_ref()
.map(|level| level.to_usize())
.unwrap_or(0);
let module = import.module.as_ref().map(ToString::to_string);
let base = python_import_source(level, module.as_deref());
for alias in &import.names {
let imported_name = alias.name.to_string();
if imported_name == "*" {
facts.reexports.push(ReexportFact {
source: base.clone(),
imported: ReexportTarget::All,
exported: "*".to_string(),
is_ambiguous: true,
});
continue;
}
let local = alias
.asname
.as_ref()
.map(ToString::to_string)
.unwrap_or_else(|| imported_name.clone());
let source = if module.is_none() {
format!("{base}{imported_name}")
} else {
base.clone()
};
let imported = if module.is_none() {
ImportTarget::Namespace
} else {
ImportTarget::Name(imported_name.clone())
};
facts.imports.push(ImportFact {
source,
local: local.clone(),
imported,
kind: ImportKind::Esm,
type_only: false,
});
if module.is_some() {
facts.imports.push(ImportFact {
source: format!("{base}.{imported_name}"),
local: local.clone(),
imported: ImportTarget::Namespace,
kind: ImportKind::Esm,
type_only: true,
});
}
facts.exports.push(ExportFact {
exported: local,
local: Some(imported_name),
kind: ExportKind::Reexport,
});
}
}
fn python_import_source(level: usize, module: Option<&str>) -> String {
let mut source = ".".repeat(level);
if let Some(module) = module {
source.push_str(module);
}
source
}
fn collect_python_assignment_target(expr: &py_ast::Expr, facts: &mut ModuleFacts) {
match expr {
py_ast::Expr::Name(name) => add_python_export(facts, name.id.to_string()),
py_ast::Expr::Tuple(tuple) => {
for element in &tuple.elts {
collect_python_assignment_target(element, facts);
}
}
py_ast::Expr::List(list) => {
for element in &list.elts {
collect_python_assignment_target(element, facts);
}
}
_ => {}
}
}
fn add_python_export(facts: &mut ModuleFacts, name: String) {
if name.starts_with('_') {
return;
}
facts.exports.push(ExportFact {
exported: name.clone(),
local: Some(name),
kind: ExportKind::Local,
});
}