use mollify_graph::ModuleGraph;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Trace {
pub target: String,
pub imports: Vec<String>,
pub imported_by: Vec<String>,
}
pub fn module(graph: &ModuleGraph, query: &str) -> Option<Trace> {
let target = resolve(graph, query)?;
let mut imports = Vec::new();
let mut imported_by = Vec::new();
for (importer, imported) in graph.import_edges() {
if importer == target {
imports.push(imported.to_string());
}
if imported == target {
imported_by.push(importer.to_string());
}
}
imports.sort();
imports.dedup();
imported_by.sort();
imported_by.dedup();
Some(Trace {
target,
imports,
imported_by,
})
}
fn resolve(graph: &ModuleGraph, query: &str) -> Option<String> {
let mut names: Vec<&str> = graph.modules.iter().map(|m| m.dotted.as_str()).collect();
names.sort();
if let Some(exact) = names.iter().find(|n| **n == query) {
return Some((*exact).to_string());
}
names
.iter()
.find(|n| n.ends_with(&format!(".{query}")) || **n == query)
.map(|n| (*n).to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use camino::Utf8PathBuf;
use mollify_graph::discover_python_files;
fn temp(tag: &str) -> Utf8PathBuf {
let base =
std::env::temp_dir().join(format!("mollify-core-trace-{}-{tag}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
std::fs::create_dir_all(&base).unwrap();
Utf8PathBuf::from_path_buf(base).unwrap()
}
#[test]
fn traces_both_directions() {
let d = temp("t");
std::fs::write(d.join("a.py"), "import b\n").unwrap();
std::fs::write(d.join("b.py"), "import c\n").unwrap();
std::fs::write(d.join("c.py"), "").unwrap();
let files = discover_python_files(&d);
let g = ModuleGraph::build(&d, &files);
let t = module(&g, "b").unwrap();
assert_eq!(t.target, "b");
assert!(t.imports.contains(&"c".to_string()), "{t:?}");
assert!(t.imported_by.contains(&"a".to_string()), "{t:?}");
assert!(module(&g, "nonexistent").is_none());
std::fs::remove_dir_all(&d).ok();
}
}