Skip to main content

grip/
app.rs

1// Copyright 2026 Umberto Gotti <umberto.gotti@umbertogotti.dev>
2// Licensed under the MIT License
3// SPDX-License-Identifier: MIT
4
5use std::ffi::OsString;
6use std::path::Path;
7use std::process::ExitCode;
8
9use anyhow::Result;
10
11use crate::args::Args;
12use crate::cache::Cache;
13use crate::collector::Collector;
14use crate::config::Config;
15use crate::function_info::FunctionInfo;
16use crate::grip_report::GripReport;
17use crate::item_counts::ItemCounts;
18use crate::overall_stats::OverallStats;
19use crate::traits::reporter::Reporter;
20use crate::traits::scorer::Scorer;
21use crate::traits::walk::Walk;
22
23type CollectedFiles = (Vec<(String, ItemCounts)>, Vec<FunctionInfo>);
24
25#[derive(Debug)]
26pub struct App<W: Walk, S: Scorer, R: Reporter> {
27    walker: W,
28    scorer: S,
29    reporter: R,
30    config: Config,
31}
32
33impl
34    App<
35        crate::fs_walk::FsWalk,
36        crate::default_scorer::DefaultScorer,
37        crate::stdout_reporter::StdoutReporter,
38    >
39{
40    #[must_use]
41    pub fn new(config: Config) -> Self {
42        Self {
43            walker: crate::fs_walk::FsWalk::new(&config.path),
44            scorer: crate::default_scorer::DefaultScorer::new(),
45            reporter: crate::stdout_reporter::StdoutReporter::new(config.json, config.verbose),
46            config,
47        }
48    }
49}
50
51impl<W: Walk, S: Scorer, R: Reporter> App<W, S, R> {
52    #[must_use]
53    pub fn with_deps(walker: W, scorer: S, reporter: R, config: Config) -> Self {
54        Self {
55            walker,
56            scorer,
57            reporter,
58            config,
59        }
60    }
61
62    #[must_use]
63    pub fn reporter(&self) -> &R {
64        &self.reporter
65    }
66
67    pub fn run(&self) -> Result<ExitCode> {
68        let mut cache = Cache::new(&self.config.path);
69        let (indexed, functions) = self.collect_files(&mut cache)?;
70        cache.flush();
71
72        if indexed.is_empty() {
73            return Err(anyhow::anyhow!(
74                "no Rust source files found in {}",
75                self.config.path.display()
76            ));
77        }
78
79        let report = self.compute_report(indexed, functions);
80        self.handle_output(&report)
81    }
82
83    fn collect_files(&self, cache: &mut Cache) -> Result<CollectedFiles> {
84        let files = self.walker.rust_files()?;
85        let mut indexed = Vec::with_capacity(files.len());
86        let mut all_functions = Vec::new();
87        for (path, source) in files {
88            let module = self.module_from_path(&path);
89            let (counts, functions) = Collector::collect(&source, &path);
90            all_functions.extend(functions);
91            if cache.get(&path).is_none() {
92                cache.set(&path, &source, &counts);
93            }
94            indexed.push((module, counts));
95        }
96        Ok((indexed, all_functions))
97    }
98
99    fn compute_report(
100        &self,
101        indexed: Vec<(String, ItemCounts)>,
102        functions: Vec<FunctionInfo>,
103    ) -> GripReport {
104        let (overall_counts, modules) = self.scorer.agg_modules(indexed);
105        let (grip_score, pure_ratio, public_ratio, trait_ratio, avg_contribution, clean_fn_ratio) =
106            self.scorer.score_counts(&overall_counts);
107        let overall = OverallStats {
108            grip_score,
109            public_items: overall_counts.public_items,
110            total_functions: overall_counts.total_functions,
111            pure_functions: overall_counts.pure_functions,
112            pure_ratio,
113            public_ratio,
114            inherent_methods: overall_counts.inherent_methods,
115            local_trait_methods: overall_counts.local_trait_methods,
116            trait_ratio,
117            avg_contribution,
118            clean_fn_ratio,
119        };
120        let target = self
121            .config
122            .path
123            .file_name()
124            .and_then(|n| n.to_str())
125            .unwrap_or(".")
126            .to_string();
127        let module_stats = self.scorer.module_stats(modules);
128        let offender_threshold = self.config.threshold.unwrap_or(50);
129        let offenders = module_stats
130            .iter()
131            .filter(|m| m.grip_score < offender_threshold)
132            .map(|m| crate::offender::Offender {
133                path: m.path.clone(),
134                grip_score: m.grip_score,
135            })
136            .collect();
137        GripReport {
138            version: env!("CARGO_PKG_VERSION").to_string(),
139            target,
140            overall,
141            modules: module_stats,
142            offenders,
143            offender_threshold,
144            functions,
145        }
146    }
147
148    fn handle_output(&self, report: &GripReport) -> Result<ExitCode> {
149        if let Some(min) = self.config.threshold {
150            return Ok(if report.overall.grip_score >= min {
151                ExitCode::SUCCESS
152            } else {
153                ExitCode::FAILURE
154            });
155        }
156        self.reporter.write(report)?;
157        Ok(ExitCode::SUCCESS)
158    }
159
160    fn module_from_path(&self, path: &Path) -> String {
161        let relative = path.strip_prefix(&self.config.path).unwrap_or(path);
162        let s = relative.to_string_lossy().replace('\\', "/");
163        let without_src = s.strip_prefix("src/").map(|s| s.to_string()).unwrap_or(s);
164        if let Some(pos) = without_src.rfind('/') {
165            without_src[..pos].to_string()
166        } else {
167            ".".to_string()
168        }
169    }
170}
171
172pub fn run() -> Result<ExitCode> {
173    let args = Args::parse_cargo();
174    let config = Config::from_args(args);
175    App::new(config).run()
176}
177
178pub fn run_from_args<I, T>(args: I) -> Result<ExitCode>
179where
180    I: IntoIterator<Item = T>,
181    T: Into<OsString> + Clone,
182{
183    let args = Args::parse_from_args(args);
184    let config = Config::from_args(args);
185    App::new(config).run()
186}