Skip to main content

crap4rust/
app.rs

1// Copyright 2025 Umberto Gotti <umberto.gotti@umbertogotti.dev>
2// Licensed under the MIT License
3// SPDX-License-Identifier: MIT
4
5use std::process::ExitCode;
6
7use anyhow::{Context, Result, bail};
8
9use crate::cargo_package_resolver::CargoPackageResolver;
10use crate::cli::Args;
11use crate::config::Config;
12use crate::coverage_index::CoverageIndex;
13use crate::default_scorer::DefaultScorer;
14use crate::llvm_cov_provider::LlvmCovProvider;
15use crate::package_context::PackageContext;
16use crate::project_report::ProjectReport;
17use crate::source_function::SourceFunction;
18use crate::source_function_discovery::SourceFunctionDiscovery;
19use crate::stdout_reporter::StdoutReporter;
20use crate::traits::coverage_provider::CoverageProvider;
21use crate::traits::function_discovery::FunctionDiscovery;
22use crate::traits::package_resolver::PackageResolver;
23use crate::traits::reporter::Reporter;
24use crate::traits::scorer::Scorer;
25
26pub struct App {
27    resolver: Box<dyn PackageResolver>,
28    discovery: Box<dyn FunctionDiscovery>,
29    coverage_provider: Box<dyn CoverageProvider>,
30    scorer: Box<dyn Scorer>,
31    reporter: Box<dyn Reporter>,
32    config: Config,
33}
34
35impl App {
36    #[must_use]
37    pub fn new(config: Config) -> Self {
38        Self {
39            resolver: Box::new(CargoPackageResolver::new()),
40            discovery: Box::new(SourceFunctionDiscovery::new()),
41            coverage_provider: Box::new(LlvmCovProvider::new()),
42            scorer: Box::new(DefaultScorer::new()),
43            reporter: Box::new(StdoutReporter::new()),
44            config,
45        }
46    }
47
48    #[must_use]
49    pub fn with_deps(
50        resolver: Box<dyn PackageResolver>,
51        discovery: Box<dyn FunctionDiscovery>,
52        coverage_provider: Box<dyn CoverageProvider>,
53        scorer: Box<dyn Scorer>,
54        reporter: Box<dyn Reporter>,
55        config: Config,
56    ) -> Self {
57        Self {
58            resolver,
59            discovery,
60            coverage_provider,
61            scorer,
62            reporter,
63            config,
64        }
65    }
66
67    pub fn run(&self) -> Result<ExitCode> {
68        let packages = self.resolver.resolve(&self.config)?;
69        let coverage_records = self.coverage_provider.provide(&self.config, &packages)?;
70
71        let functions = self.discover_all_functions(&packages)?;
72        if functions.is_empty() {
73            bail!("no Rust functions were discovered in the selected packages");
74        }
75
76        if coverage_records.is_empty() {
77            bail!("coverage file did not contain any function records");
78        }
79        let coverage_index = CoverageIndex::from_records(coverage_records);
80
81        let matched_count = functions
82            .iter()
83            .filter(|function| coverage_index.match_function(function).is_some())
84            .count();
85        if matched_count == 0 {
86            bail!(
87                "coverage data could not be matched to any discovered function by file path and line"
88            );
89        }
90
91        let reports = self
92            .scorer
93            .score_functions(functions, &coverage_index, &self.config);
94        let metrics = self.scorer.project_metrics(&reports, &self.config);
95
96        let report_data = ProjectReport {
97            scope_name: packages
98                .iter()
99                .map(|package| package.name.clone())
100                .collect::<Vec<_>>()
101                .join(", "),
102            total_functions: metrics.total_functions,
103            crappy_functions: metrics.crappy_functions,
104            crappy_percent: metrics.crappy_percent,
105            verdict: metrics.verdict,
106            functions: reports,
107        };
108
109        self.reporter.render(&report_data, &self.config);
110
111        Ok(report_data.exit_code(&self.config))
112    }
113
114    fn discover_all_functions(&self, packages: &[PackageContext]) -> Result<Vec<SourceFunction>> {
115        let mut functions = Vec::new();
116        for package in packages {
117            let mut package_functions = self.discovery.discover(package).with_context(|| {
118                format!("failed to discover functions in package {}", package.name)
119            })?;
120            functions.append(&mut package_functions);
121        }
122        Ok(functions)
123    }
124}
125
126pub fn run(args: Args) -> Result<ExitCode> {
127    App::new(Config::from_args(args)).run()
128}