1use std::process::ExitCode;
6
7use anyhow::{Context, Result, bail};
8
9use crate::cli::Args;
10use crate::coverage;
11use crate::manifest;
12use crate::model::{Config, FunctionReport, ProjectReport, Verdict};
13use crate::report;
14use crate::source;
15
16use crate::coverage_index::CoverageIndex;
17pub use crate::coverage_index::match_function_coverage;
18
19pub fn run(args: Args) -> Result<ExitCode> {
20 let config = Config {
21 coverage_path: args.coverage,
22 manifest_path: args.manifest_path,
23 packages: args.package,
24 features: args.features,
25 all_features: args.all_features,
26 no_default_features: args.no_default_features,
27 include_test_targets: args.include_test_targets,
28 exclude_paths: args.exclude_path,
29 threshold: args.threshold,
30 warn_threshold: args.warn_threshold,
31 project_threshold: args.project_threshold,
32 strict: args.strict,
33 warn_only: args.warn_only,
34 output_format: args.output_format,
35 };
36
37 let packages = manifest::resolve_packages(&config)?;
38 let coverage_path = coverage::ensure_coverage_path(&config, &packages)?;
39 let mut functions = Vec::new();
40 for package in &packages {
41 let mut package_functions = source::discover_functions(package)
42 .with_context(|| format!("failed to discover functions in package {}", package.name))?;
43 functions.append(&mut package_functions);
44 }
45 if functions.is_empty() {
46 bail!("no Rust functions were discovered in the selected packages");
47 }
48
49 let coverage_records = coverage::load_coverage_records(&coverage_path)?;
50 if coverage_records.is_empty() {
51 bail!("coverage file did not contain any function records");
52 }
53
54 let coverage_index = CoverageIndex::from_records(coverage_records);
55
56 let matched_count = functions
57 .iter()
58 .filter(|function| coverage_index.match_function(function).is_some())
59 .count();
60 if matched_count == 0 {
61 bail!(
62 "coverage data could not be matched to any discovered function by file path and line"
63 );
64 }
65
66 let mut reports = functions
67 .into_iter()
68 .map(|function| {
69 let coverage = coverage_index.match_function(&function).unwrap_or(0.0);
70 let crap_score = compute_crap_score(function.complexity, coverage);
71 let verdict = classify(crap_score, config.threshold, config.warn_threshold);
72
73 FunctionReport {
74 package_name: function.package_name,
75 name: function.name,
76 relative_file: function.relative_file,
77 line: function.line,
78 complexity: function.complexity,
79 coverage,
80 crap_score,
81 verdict,
82 }
83 })
84 .collect::<Vec<_>>();
85
86 reports.sort_by(|left, right| {
87 right
88 .crap_score
89 .partial_cmp(&left.crap_score)
90 .unwrap_or(std::cmp::Ordering::Equal)
91 .then_with(|| left.name.cmp(&right.name))
92 });
93
94 let (project_verdict, crappy_functions, total_functions, crappy_percent) =
95 compute_project_metrics(&reports, &config);
96
97 let report_data = ProjectReport {
98 scope_name: packages
99 .iter()
100 .map(|package| package.name.clone())
101 .collect::<Vec<_>>()
102 .join(", "),
103 total_functions,
104 crappy_functions,
105 crappy_percent,
106 verdict: project_verdict,
107 functions: reports,
108 };
109
110 report::print_report(&report_data, &config);
111
112 Ok(determine_exit_code(&report_data, &config))
113}
114
115pub fn classify(score: f64, threshold: f64, warn_threshold: f64) -> Verdict {
116 if score > threshold {
117 Verdict::Crappy
118 } else if score >= warn_threshold {
119 Verdict::Warn
120 } else {
121 Verdict::Clean
122 }
123}
124
125pub fn compute_crap_score(complexity: u32, coverage: f64) -> f64 {
126 let complexity = f64::from(complexity);
127 complexity.powi(2) * (1.0 - coverage).powi(3) + complexity
128}
129
130fn determine_exit_code(report: &ProjectReport, config: &Config) -> ExitCode {
131 if config.warn_only {
132 return ExitCode::SUCCESS;
133 }
134
135 if project_fails(report.crappy_functions, report.crappy_percent, config) {
136 ExitCode::from(1)
137 } else {
138 ExitCode::SUCCESS
139 }
140}
141
142pub fn project_fails(crappy_functions: usize, crappy_percent: f64, config: &Config) -> bool {
143 if config.strict {
144 crappy_functions > 0
145 } else {
146 crappy_percent > config.project_threshold
147 }
148}
149
150fn compute_project_metrics(
151 reports: &[FunctionReport],
152 config: &Config,
153) -> (Verdict, usize, usize, f64) {
154 let crappy_functions = reports
155 .iter()
156 .filter(|function| function.verdict == Verdict::Crappy)
157 .count();
158 let total_functions = reports.len();
159 let crappy_percent = if total_functions == 0 {
160 0.0
161 } else {
162 (crappy_functions as f64 / total_functions as f64) * 100.0
163 };
164 let verdict = if project_fails(crappy_functions, crappy_percent, config) {
165 Verdict::Crappy
166 } else if crappy_functions > 0
167 || reports
168 .iter()
169 .any(|function| function.verdict == Verdict::Warn)
170 {
171 Verdict::Warn
172 } else {
173 Verdict::Clean
174 };
175 (verdict, crappy_functions, total_functions, crappy_percent)
176}