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