mod analyzer;
mod extractor;
mod report;
use std::collections::{HashMap, HashSet};
use std::error::Error;
use std::path::{Path, PathBuf};
use crate::walk::{self, WalkConfig};
use analyzer::{DepEntry, DepResult, build_graph, resolve_import};
use extractor::extract_imports;
fn detect_go_module(root: &Path) -> Option<String> {
let content = std::fs::read_to_string(root.join("go.mod")).ok()?;
for line in content.lines() {
if let Some(rest) = line.strip_prefix("module ") {
return Some(rest.trim().to_string());
}
}
None
}
pub fn run(
cfg: &WalkConfig<'_>,
json: bool,
cycles_only: bool,
sort_by: &str,
top: usize,
) -> Result<(), Box<dyn Error>> {
let go_module = detect_go_module(cfg.path);
let all_files: Vec<(PathBuf, String)> =
walk::source_files(cfg.path, cfg.exclude_tests(), cfg.filter)
.into_iter()
.map(|(p, spec)| {
let rel = p.strip_prefix(cfg.path).unwrap_or(&p).to_path_buf();
(rel, spec.name.to_string())
})
.collect();
let file_set: HashSet<PathBuf> = all_files.iter().map(|(p, _)| p.clone()).collect();
let mut edges: HashMap<PathBuf, Vec<PathBuf>> = HashMap::new();
for (rel_path, language) in &all_files {
let abs_path = cfg.path.join(rel_path);
let source = match std::fs::read_to_string(&abs_path) {
Ok(s) => s,
Err(_) => {
edges.entry(rel_path.clone()).or_default();
continue;
}
};
let raw_imports = extract_imports(rel_path, language, &source);
let resolved: Vec<PathBuf> = raw_imports
.iter()
.filter_map(|imp| {
resolve_import(rel_path, imp, language, &file_set, go_module.as_deref())
})
.collect();
let mut deduped = resolved;
deduped.sort();
deduped.dedup();
edges.insert(rel_path.clone(), deduped);
}
for (path, _) in &all_files {
edges.entry(path.clone()).or_default();
}
let mut result = build_graph(&all_files, &edges);
match sort_by {
"fan-in" => result.entries.sort_by(|a, b| b.fan_in.cmp(&a.fan_in)),
"fan-out" => result.entries.sort_by(|a, b| b.fan_out.cmp(&a.fan_out)),
_ => {
result.entries.sort_by(|a, b| {
b.fan_out
.cmp(&a.fan_out)
.then_with(|| b.fan_in.cmp(&a.fan_in))
});
}
}
let entries: Vec<&DepEntry> = if cycles_only {
result.entries.iter().filter(|e| e.in_cycle).collect()
} else {
result.entries.iter().take(top).collect()
};
if json {
let filtered = DepResult {
entries: entries
.iter()
.map(|e| analyzer::DepEntry {
path: e.path.clone(),
language: e.language.clone(),
fan_in: e.fan_in,
fan_out: e.fan_out,
in_cycle: e.in_cycle,
})
.collect(),
cycles: result.cycles.clone(),
};
report::print_json(&filtered)
} else {
let entries_vec: Vec<DepEntry> = entries
.into_iter()
.map(|e| analyzer::DepEntry {
path: e.path.clone(),
language: e.language.clone(),
fan_in: e.fan_in,
fan_out: e.fan_out,
in_cycle: e.in_cycle,
})
.collect();
report::print_report(&entries_vec, &result);
Ok(())
}
}
#[cfg(test)]
#[path = "mod_test.rs"]
mod tests;