cargo-crap4rust 0.8.2

Cargo subcommand for computing CRAP scores across Rust crates
Documentation
// Copyright 2026 Umberto Gotti <umberto.gotti@umbertogotti.dev>
// Licensed under the MIT License
// SPDX-License-Identifier: MIT

use std::ffi::OsString;
use std::process::ExitCode;

use anyhow::{Context, Result, bail};
use clap::Parser;

use crate::analysis::source_function::SourceFunction;
use crate::analysis::source_function_discovery::SourceFunctionDiscovery;
use crate::coverage_io::coverage_index::CoverageIndex;
use crate::coverage_io::llvm_cov_provider::LlvmCovProvider;
use crate::invocation::cargo_package_resolver::CargoPackageResolver;
use crate::invocation::cli::Args;
use crate::invocation::config::Config;
use crate::invocation::package_context::PackageContext;
use crate::reporting::default_scorer::DefaultScorer;
use crate::reporting::project_report::ProjectReport;
use crate::reporting::stdout_reporter::StdoutReporter;
use crate::traits::coverage_provider::CoverageProvider;
use crate::traits::function_discovery::FunctionDiscovery;
use crate::traits::package_resolver::PackageResolver;
use crate::traits::reporter::Reporter;
use crate::traits::scorer::Scorer;

pub struct App {
    resolver: Box<dyn PackageResolver>,
    discovery: Box<dyn FunctionDiscovery>,
    coverage_provider: Box<dyn CoverageProvider>,
    scorer: Box<dyn Scorer>,
    reporter: Box<dyn Reporter>,
    config: Config,
}

impl App {
    #[must_use]
    pub fn new(config: Config) -> Self {
        Self {
            resolver: Box::new(CargoPackageResolver::new()),
            discovery: Box::new(SourceFunctionDiscovery::new()),
            coverage_provider: Box::new(LlvmCovProvider::new()),
            scorer: Box::new(DefaultScorer::new()),
            reporter: Box::new(StdoutReporter::new()),
            config,
        }
    }

    #[must_use]
    pub fn with_deps(
        resolver: Box<dyn PackageResolver>,
        discovery: Box<dyn FunctionDiscovery>,
        coverage_provider: Box<dyn CoverageProvider>,
        scorer: Box<dyn Scorer>,
        reporter: Box<dyn Reporter>,
        config: Config,
    ) -> Self {
        Self {
            resolver,
            discovery,
            coverage_provider,
            scorer,
            reporter,
            config,
        }
    }

    pub fn run(&self) -> Result<ExitCode> {
        let packages = self.resolver.resolve(&self.config)?;
        let coverage_records = self.coverage_provider.provide(&self.config, &packages)?;

        let functions = self.discover_all_functions(&packages)?;
        if functions.is_empty() {
            bail!("no Rust functions were discovered in the selected packages");
        }

        if coverage_records.is_empty() {
            bail!("coverage file did not contain any function records");
        }
        let coverage_index = CoverageIndex::from_records(coverage_records);

        let matched_count = functions
            .iter()
            .filter(|function| coverage_index.match_function(function).is_some())
            .count();
        if matched_count == 0 {
            bail!(
                "coverage data could not be matched to any discovered function by file path and line"
            );
        }

        let reports = self
            .scorer
            .score_functions(functions, &coverage_index, &self.config);
        let metrics = self.scorer.project_metrics(&reports, &self.config);

        let report_data = ProjectReport {
            scope_name: packages
                .iter()
                .map(|package| package.name.clone())
                .collect::<Vec<_>>()
                .join(", "),
            total_functions: metrics.total_functions,
            crappy_functions: metrics.crappy_functions,
            crappy_percent: metrics.crappy_percent,
            verdict: metrics.verdict,
            functions: reports,
        };

        self.reporter.render(&report_data, &self.config);

        Ok(report_data.exit_code(&self.config))
    }

    fn discover_all_functions(&self, packages: &[PackageContext]) -> Result<Vec<SourceFunction>> {
        let mut functions = Vec::new();
        for package in packages {
            let mut package_functions = self.discovery.discover(package).with_context(|| {
                format!("failed to discover functions in package {}", package.name)
            })?;
            functions.append(&mut package_functions);
        }
        Ok(functions)
    }
}

// The crate's entry points, beside the App they build rather than in the crate
// root. lib.rs is an index of the modules beneath it; a reader looking for what
// happens when this tool runs should find it here, next to the type that does
// it.
pub fn run() -> Result<ExitCode> {
    App::new(Config::from_args(Args::parse_args())).run()
}

pub fn run_from_args<I, T>(args: I) -> Result<ExitCode>
where
    I: IntoIterator<Item = T>,
    T: Into<OsString> + Clone,
{
    let args = <Args as Parser>::parse_from(args);
    App::new(Config::from_args(args)).run()
}