mod analyzer;
mod elixir;
mod elixir_functions;
mod elixir_literals;
mod extractor;
mod golang;
pub mod graph;
pub mod heex;
mod kaikai;
pub mod layout;
mod phoenix;
mod python;
mod python_names;
mod python_uses;
mod report;
pub mod routes;
mod rust;
mod rust_crates;
pub(crate) mod rust_literals;
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};
use layout::{Layout, is_manifest};
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 analyze(cfg: &WalkConfig<'_>) -> DepResult {
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 mut manifests: Vec<(PathBuf, String)> = skipped
.iter()
.filter(|(path, _)| is_manifest(path))
.map(|(path, _)| {
let text = std::fs::read_to_string(cfg.path.join(path)).unwrap_or_default();
(path.clone(), text)
})
.collect();
if all_files.iter().any(|(_, language)| language == "Go") {
let go_mod = |path: &Path| path.file_name().is_some_and(|name| name == "go.mod");
manifests.extend(crate::projects::files_on_disk(cfg.path, go_mod));
}
let layout = Layout {
known: file_set,
..Layout::default()
}
.with_manifests(manifests.iter().map(|(p, t)| (p.as_path(), t.as_str())));
let edges = FileGraph::build(&sources, &layout, &|_, _| 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;