mod analyzer;
mod elixir;
mod elixir_functions;
mod elixir_literals;
mod extractor;
pub mod graph;
pub mod heex;
mod kaikai;
mod phoenix;
mod report;
pub mod routes;
use std::cmp::Reverse;
use std::collections::{HashMap, HashSet};
use std::error::Error;
use std::path::{Path, PathBuf};
use crate::walk::{self, WalkConfig};
use analyzer::{DepEntry, DepResult, UnsupportedLanguage, build_graph};
pub use extractor::is_supported;
use graph::{FileGraph, Source};
fn count_unsupported(skipped: &[(PathBuf, String)]) -> Vec<UnsupportedLanguage> {
let mut counts: HashMap<&str, usize> = HashMap::new();
for (_, language) in skipped {
*counts.entry(language).or_default() += 1;
}
let mut unsupported: Vec<UnsupportedLanguage> = counts
.into_iter()
.map(|(language, files)| UnsupportedLanguage {
language: language.to_string(),
files,
})
.collect();
unsupported.sort_by(|a, b| {
b.files
.cmp(&a.files)
.then_with(|| a.language.cmp(&b.language))
});
unsupported
}
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
}
fn analyze(cfg: &WalkConfig<'_>) -> DepResult {
let go_module = detect_go_module(cfg.path);
let (all_files, skipped): (Vec<_>, Vec<_>) =
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())
})
.partition(|(_, language)| is_supported(language));
let unsupported = count_unsupported(&skipped);
let file_set: HashSet<PathBuf> = all_files
.iter()
.chain(&skipped)
.map(|(p, _)| p.clone())
.collect();
let sources: Vec<Source> = all_files
.iter()
.map(|(rel_path, language)| Source {
path: rel_path.clone(),
language: language.clone(),
text: std::fs::read_to_string(cfg.path.join(rel_path)).unwrap_or_default(),
templates: Vec::new(),
})
.collect();
let edges = FileGraph::build(&sources, &file_set, go_module.as_deref(), &|_, _| false).edges();
let mut result = build_graph(&all_files, &edges);
result.unsupported = unsupported;
result
}
fn sort_entries(entries: &mut [DepEntry], sort_by: &str) {
match sort_by {
"fan-in" => entries.sort_by_key(|e| Reverse(e.fan_in)),
"fan-out" => entries.sort_by_key(|e| Reverse(e.fan_out)),
_ => entries.sort_by(|a, b| {
b.fan_out
.cmp(&a.fan_out)
.then_with(|| b.fan_in.cmp(&a.fan_in))
}),
}
}
fn visible_entries(entries: &[DepEntry], cycles_only: bool, top: usize) -> Vec<&DepEntry> {
if cycles_only {
entries.iter().filter(|e| e.in_cycle).collect()
} else {
entries.iter().take(top).collect()
}
}
pub fn run(
cfg: &WalkConfig<'_>,
output: crate::cli::OutputMode,
cycles_only: bool,
sort_by: &str,
top: usize,
) -> Result<(), Box<dyn Error>> {
let mut result = analyze(cfg);
sort_entries(&mut result.entries, sort_by);
let entries = visible_entries(&result.entries, cycles_only, top);
match output {
crate::cli::OutputMode::Json => {
let filtered = DepResult {
entries: entries.iter().map(|e| (*e).clone()).collect(),
cycles: result.cycles.clone(),
unsupported: result.unsupported.clone(),
};
report::print_json(&filtered)
}
crate::cli::OutputMode::Short => {
report::print_short(&result);
Ok(())
}
crate::cli::OutputMode::Terse => {
report::print_terse(&result);
Ok(())
}
crate::cli::OutputMode::Github | crate::cli::OutputMode::Codeclimate => {
Err(crate::cli::ERR_CI_FORMAT_ONLY.into())
}
crate::cli::OutputMode::Table => {
let entries_vec: Vec<DepEntry> = entries.into_iter().cloned().collect();
report::print_report(&entries_vec, &result);
Ok(())
}
}
}
#[cfg(test)]
#[path = "mod_test.rs"]
mod tests;