Skip to main content

crap4rust/invocation/
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::process::ExitCode;
7
8use anyhow::{Context, Result, bail};
9use clap::Parser;
10
11use crate::analysis::source_function::SourceFunction;
12use crate::analysis::source_function_discovery::SourceFunctionDiscovery;
13use crate::coverage_io::coverage_index::CoverageIndex;
14use crate::coverage_io::llvm_cov_provider::LlvmCovProvider;
15use crate::invocation::cargo_package_resolver::CargoPackageResolver;
16use crate::invocation::cli::Args;
17use crate::invocation::config::Config;
18use crate::invocation::package_context::PackageContext;
19use crate::reporting::default_scorer::DefaultScorer;
20use crate::reporting::project_report::ProjectReport;
21use crate::reporting::stdout_reporter::StdoutReporter;
22use crate::traits::coverage_provider::CoverageProvider;
23use crate::traits::function_discovery::FunctionDiscovery;
24use crate::traits::package_resolver::PackageResolver;
25use crate::traits::reporter::Reporter;
26use crate::traits::scorer::Scorer;
27
28pub struct App {
29    resolver: Box<dyn PackageResolver>,
30    discovery: Box<dyn FunctionDiscovery>,
31    coverage_provider: Box<dyn CoverageProvider>,
32    scorer: Box<dyn Scorer>,
33    reporter: Box<dyn Reporter>,
34    config: Config,
35}
36
37impl App {
38    #[must_use]
39    pub fn new(config: Config) -> Self {
40        Self {
41            resolver: Box::new(CargoPackageResolver::new()),
42            discovery: Box::new(SourceFunctionDiscovery::new()),
43            coverage_provider: Box::new(LlvmCovProvider::new()),
44            scorer: Box::new(DefaultScorer::new()),
45            reporter: Box::new(StdoutReporter::new()),
46            config,
47        }
48    }
49
50    #[must_use]
51    pub fn with_deps(
52        resolver: Box<dyn PackageResolver>,
53        discovery: Box<dyn FunctionDiscovery>,
54        coverage_provider: Box<dyn CoverageProvider>,
55        scorer: Box<dyn Scorer>,
56        reporter: Box<dyn Reporter>,
57        config: Config,
58    ) -> Self {
59        Self {
60            resolver,
61            discovery,
62            coverage_provider,
63            scorer,
64            reporter,
65            config,
66        }
67    }
68
69    pub fn run(&self) -> Result<ExitCode> {
70        let packages = self.resolver.resolve(&self.config)?;
71        let coverage_records = self.coverage_provider.provide(&self.config, &packages)?;
72
73        let functions = self.discover_all_functions(&packages)?;
74        if functions.is_empty() {
75            bail!("no Rust functions were discovered in the selected packages");
76        }
77
78        if coverage_records.is_empty() {
79            bail!("coverage file did not contain any function records");
80        }
81        let coverage_index = CoverageIndex::from_records(coverage_records);
82
83        let matched_count = functions
84            .iter()
85            .filter(|function| coverage_index.match_function(function).is_some())
86            .count();
87        if matched_count == 0 {
88            bail!(
89                "coverage data could not be matched to any discovered function by file path and line"
90            );
91        }
92
93        let reports = self
94            .scorer
95            .score_functions(functions, &coverage_index, &self.config);
96        let metrics = self.scorer.project_metrics(&reports, &self.config);
97
98        let report_data = ProjectReport {
99            scope_name: packages
100                .iter()
101                .map(|package| package.name.clone())
102                .collect::<Vec<_>>()
103                .join(", "),
104            total_functions: metrics.total_functions,
105            crappy_functions: metrics.crappy_functions,
106            crappy_percent: metrics.crappy_percent,
107            verdict: metrics.verdict,
108            functions: reports,
109        };
110
111        self.reporter.render(&report_data, &self.config);
112
113        Ok(report_data.exit_code(&self.config))
114    }
115
116    fn discover_all_functions(&self, packages: &[PackageContext]) -> Result<Vec<SourceFunction>> {
117        let mut functions = Vec::new();
118        for package in packages {
119            let mut package_functions = self.discovery.discover(package).with_context(|| {
120                format!("failed to discover functions in package {}", package.name)
121            })?;
122            functions.append(&mut package_functions);
123        }
124        Ok(functions)
125    }
126}
127
128// The crate's entry points, beside the App they build rather than in the crate
129// root. lib.rs is an index of the modules beneath it; a reader looking for what
130// happens when this tool runs should find it here, next to the type that does
131// it.
132pub fn run() -> Result<ExitCode> {
133    App::new(Config::from_args(Args::parse_args())).run()
134}
135
136pub fn run_from_args<I, T>(args: I) -> Result<ExitCode>
137where
138    I: IntoIterator<Item = T>,
139    T: Into<OsString> + Clone,
140{
141    let args = <Args as Parser>::parse_from(args);
142    App::new(Config::from_args(args)).run()
143}