1use std::path::Path;
6use std::process::ExitCode;
7
8use anyhow::Result;
9
10use crate::cache::Cache;
11use crate::collector::Collector;
12use crate::config::Config;
13use crate::grip_report::GripReport;
14use crate::item_counts::ItemCounts;
15use crate::overall_stats::OverallStats;
16use crate::traits::reporter::Reporter;
17use crate::traits::scorer::Scorer;
18use crate::traits::walk::Walk;
19
20#[derive(Debug)]
21pub struct App<W: Walk, S: Scorer, R: Reporter> {
22 walker: W,
23 scorer: S,
24 reporter: R,
25 config: Config,
26}
27
28impl
29 App<
30 crate::fs_walk::FsWalk,
31 crate::default_scorer::DefaultScorer,
32 crate::stdout_reporter::StdoutReporter,
33 >
34{
35 #[must_use]
36 pub fn new(config: Config) -> Self {
37 Self {
38 walker: crate::fs_walk::FsWalk::new(&config.path),
39 scorer: crate::default_scorer::DefaultScorer::new(),
40 reporter: crate::stdout_reporter::StdoutReporter::new(config.json),
41 config,
42 }
43 }
44}
45
46impl<W: Walk, S: Scorer, R: Reporter> App<W, S, R> {
47 #[must_use]
48 pub fn with_deps(walker: W, scorer: S, reporter: R, config: Config) -> Self {
49 Self {
50 walker,
51 scorer,
52 reporter,
53 config,
54 }
55 }
56
57 #[must_use]
58 pub fn reporter(&self) -> &R {
59 &self.reporter
60 }
61
62 pub fn run(&self) -> Result<ExitCode> {
63 let mut cache = Cache::new(&self.config.path);
64 let indexed = self.collect_files(&mut cache)?;
65 cache.flush();
66
67 if indexed.is_empty() {
68 return Err(anyhow::anyhow!(
69 "no Rust source files found in {}",
70 self.config.path.display()
71 ));
72 }
73
74 let report = self.compute_report(indexed);
75 self.handle_output(&report)
76 }
77
78 fn collect_files(&self, cache: &mut Cache) -> Result<Vec<(String, ItemCounts)>> {
79 let files = self.walker.rust_files()?;
80 let mut indexed = Vec::with_capacity(files.len());
81 for (path, source) in files {
82 let module = self.module_from_path(&path);
83 let counts = if let Some(cached) = cache.get(&path) {
84 cached
85 } else {
86 let counts = Collector::collect(&source);
87 cache.set(&path, &source, &counts);
88 counts
89 };
90 indexed.push((module, counts));
91 }
92 Ok(indexed)
93 }
94
95 fn compute_report(&self, indexed: Vec<(String, ItemCounts)>) -> GripReport {
96 let (overall_counts, modules) = self.scorer.agg_modules(indexed);
97 let (grip_score, pure_ratio, public_ratio) = self.scorer.score_counts(&overall_counts);
98 let overall = OverallStats {
99 grip_score,
100 public_items: overall_counts.public_items,
101 total_functions: overall_counts.total_functions,
102 pure_functions: overall_counts.pure_functions,
103 pure_ratio,
104 public_ratio,
105 };
106 let target = self
107 .config
108 .path
109 .file_name()
110 .and_then(|n| n.to_str())
111 .unwrap_or(".")
112 .to_string();
113 let module_stats = self.scorer.module_stats(modules);
114 let offender_threshold = self.config.threshold.unwrap_or(50);
115 let offenders = module_stats
116 .iter()
117 .filter(|m| m.grip_score < offender_threshold)
118 .map(|m| crate::offender::Offender {
119 path: m.path.clone(),
120 grip_score: m.grip_score,
121 })
122 .collect();
123 GripReport {
124 version: env!("CARGO_PKG_VERSION").to_string(),
125 target,
126 overall,
127 modules: module_stats,
128 offenders,
129 offender_threshold,
130 }
131 }
132
133 fn handle_output(&self, report: &GripReport) -> Result<ExitCode> {
134 if let Some(min) = self.config.threshold {
135 return Ok(if report.overall.grip_score >= min {
136 ExitCode::SUCCESS
137 } else {
138 ExitCode::FAILURE
139 });
140 }
141 self.reporter.write(report)?;
142 Ok(ExitCode::SUCCESS)
143 }
144
145 fn module_from_path(&self, path: &Path) -> String {
146 let relative = path.strip_prefix(&self.config.path).unwrap_or(path);
147 let s = relative.to_string_lossy().replace('\\', "/");
148 let without_src = s.strip_prefix("src/").map(|s| s.to_string()).unwrap_or(s);
149 if let Some(pos) = without_src.rfind('/') {
150 without_src[..pos].to_string()
151 } else {
152 ".".to_string()
153 }
154 }
155}