1pub mod audit;
4pub mod clippy;
5pub mod complexity;
6pub mod coverage;
7pub mod deny;
8pub mod duplicates;
9pub mod fmt;
10pub mod hack;
11pub mod loc;
12pub mod mutants;
13pub mod size;
14pub mod tests;
15
16use crate::context::Context;
17use crate::schema::{
18 AuditResult, ClippyLint, ClippyResult, CollectorStatus, ComplexityResult, CoverageResult,
19 DenyResult, DuplicatesResult, FmtResult, HackResult, LocResult, MetricsSummary, MutantsResult,
20 ProjectInfo, SizeResult, TestResult,
21};
22
23pub trait Collector: Send + Sync {
24 fn name(&self) -> &'static str;
25 fn is_available(&self) -> bool;
26 fn collect(&self, ctx: &Context) -> Result<CollectorOutput, CollectorError>;
27}
28
29#[derive(Debug, Clone)]
30pub struct CollectorOutput {
31 pub status: CollectorStatus,
32 pub duration_ms: u64,
33 pub stdout: String,
34 pub stderr: String,
35}
36
37#[derive(Debug, thiserror::Error)]
38pub enum CollectorError {
39 #[error("collector not available: {0}")]
40 NotAvailable(String),
41 #[error("execution failed: {0}")]
42 ExecutionFailed(String),
43 #[error("parse error: {0}")]
44 ParseError(String),
45 #[error("I/O error: {0}")]
46 IoError(String),
47}
48
49pub struct MockCollector {
50 pub name_val: &'static str,
51 pub available: bool,
52 pub output: CollectorOutput,
53}
54
55impl Collector for MockCollector {
56 fn name(&self) -> &'static str {
57 self.name_val
58 }
59
60 fn is_available(&self) -> bool {
61 self.available
62 }
63
64 fn collect(&self, _ctx: &Context) -> Result<CollectorOutput, CollectorError> {
65 Ok(self.output.clone())
66 }
67}
68
69pub fn execute_collectors<'a>(
74 collectors: &'a [Box<dyn Collector>],
75 ctx: &Context,
76 parallel: bool,
77) -> Vec<(&'a str, CollectorOutput)> {
78 if parallel {
79 use rayon::prelude::*;
80 collectors
81 .par_iter()
82 .filter(|col| {
83 let name_lower = col.name().to_lowercase();
84 !ctx.disabled_collectors
85 .iter()
86 .any(|c| c.to_string() == name_lower)
87 })
88 .filter(|col| col.is_available())
89 .flat_map(|col| match col.collect(ctx) {
90 Ok(o) => vec![(col.name(), o)],
91 Err(e) => {
92 let output = CollectorOutput {
93 status: CollectorStatus::Error,
94 duration_ms: 0,
95 stdout: String::new(),
96 stderr: format!("{:?}", e),
97 };
98 vec![(col.name(), output)]
99 }
100 })
101 .collect()
102 } else {
103 let mut results = Vec::new();
104 for col in collectors {
105 let name_lower = col.name().to_lowercase();
106 if ctx
107 .disabled_collectors
108 .iter()
109 .any(|c| c.to_string() == name_lower)
110 {
111 continue;
112 }
113 if !col.is_available() {
114 continue;
115 }
116 match col.collect(ctx) {
117 Ok(o) => results.push((col.name(), o)),
118 Err(e) => {
119 let output = CollectorOutput {
120 status: CollectorStatus::Error,
121 duration_ms: 0,
122 stdout: String::new(),
123 stderr: format!("{:?}", e),
124 };
125 results.push((col.name(), output));
126 }
127 }
128 }
129 results
130 }
131}
132
133pub fn assemble_results(
137 results: &[(&str, CollectorOutput)],
138 project_name: &str,
139 rust_edition: &str,
140 workspace_root: &str,
141) -> MetricsSummary {
142 let mut fmt_result = FmtResult {
143 status: CollectorStatus::Skipped,
144 details: Default::default(),
145 };
146 let mut clippy_result = ClippyResult {
147 status: CollectorStatus::Skipped,
148 warning_count: 0,
149 details: vec![],
150 };
151 let mut test_result = TestResult {
152 status: CollectorStatus::Skipped,
153 passed: 0,
154 failed: 0,
155 ignored: 0,
156 runner: None,
157 };
158 let mut coverage_result = CoverageResult {
159 status: CollectorStatus::Skipped,
160 line_percent: 0.0,
161 };
162 let mut deny_result = DenyResult {
163 status: CollectorStatus::Skipped,
164 banned_count: 0,
165 license_violations: 0,
166 };
167 let mut audit_result = AuditResult {
168 status: CollectorStatus::Skipped,
169 vulnerability_count: 0,
170 critical_count: 0,
171 };
172 let mut hack_result = HackResult {
173 status: CollectorStatus::Skipped,
174 feature_combinations_tested: 0,
175 };
176 let mut mutants_result = MutantsResult {
177 status: CollectorStatus::Skipped,
178 mutation_score: 0.0,
179 caught: 0,
180 missed: 0,
181 };
182 let mut duplicates_result = DuplicatesResult {
183 status: CollectorStatus::Skipped,
184 total_lines: 0,
185 duplicate_lines: 0,
186 files_with_duplicates: 0,
187 duplicate_files: vec![],
188 };
189 let mut loc_result = LocResult {
190 status: CollectorStatus::Skipped,
191 total_lines: 0,
192 code_lines: 0,
193 comment_lines: 0,
194 blank_lines: 0,
195 long_lines: 0,
196 max_line_length_found: 0,
197 max_line_length_allowed: 0,
198 files: 0,
199 files_with_long_lines: 0,
200 long_line_files: vec![],
201 };
202 let mut size_result = SizeResult {
203 status: CollectorStatus::Skipped,
204 files: 0,
205 max_lines_per_file: 0,
206 max_code_lines_per_file: 0,
207 max_lines_per_function: 0,
208 max_parameters_per_function: 0,
209 violations: vec![],
210 };
211 let mut complexity_result = ComplexityResult {
212 status: CollectorStatus::Skipped,
213 functions: 0,
214 max_cyclomatic_complexity: 0,
215 max_nesting_depth: 0,
216 complex_functions: 0,
217 violations: vec![],
218 };
219
220 for (name, output) in results {
221 let details = serde_json::from_str::<serde_json::Value>(&output.stdout).ok();
222 match *name {
223 "fmt" => fmt_result.status.clone_from(&output.status),
224 "clippy" => {
225 if let Some(ref d) = details {
226 clippy_result.warning_count = d["warningCount"].as_u64().unwrap_or(0) as u32;
227 if let Some(arr) = d["details"].as_array() {
228 clippy_result.details = arr
229 .iter()
230 .map(|v| ClippyLint {
231 code: v["code"].as_str().unwrap_or("").to_string(),
232 message: v["message"].as_str().unwrap_or("").to_string(),
233 file: v["file"].as_str().map(String::from),
234 line: v["line"].as_u64().map(|v| v as u32),
235 })
236 .collect();
237 }
238 }
239 clippy_result.status.clone_from(&output.status);
240 }
241 "tests" => {
242 if let Some(ref d) = details {
243 test_result.passed = d["passed"].as_u64().unwrap_or(0) as u32;
244 test_result.failed = d["failed"].as_u64().unwrap_or(0) as u32;
245 test_result.ignored = d["ignored"].as_u64().unwrap_or(0) as u32;
246 test_result.runner = d["runner"].as_str().map(String::from);
247 }
248 test_result.status.clone_from(&output.status);
249 }
250 "coverage" => {
251 if let Some(ref d) = details {
252 coverage_result.line_percent = d["linePercent"].as_f64().unwrap_or(0.0);
253 }
254 coverage_result.status.clone_from(&output.status);
255 }
256 "deny" => {
257 if let Some(ref d) = details {
258 deny_result.banned_count = d["bannedCount"].as_u64().unwrap_or(0) as u32;
259 deny_result.license_violations =
260 d["licenseViolations"].as_u64().unwrap_or(0) as u32;
261 }
262 deny_result.status.clone_from(&output.status);
263 }
264 "audit" => {
265 if let Some(ref d) = details {
266 audit_result.vulnerability_count =
267 d["vulnerabilityCount"].as_u64().unwrap_or(0) as u32;
268 audit_result.critical_count =
269 d["criticalCount"].as_u64().unwrap_or(0) as u32;
270 }
271 audit_result.status.clone_from(&output.status);
272 }
273 "hack" => {
274 if let Some(ref d) = details {
275 hack_result.feature_combinations_tested =
276 d["featureCombinationsTested"].as_u64().unwrap_or(0) as u32;
277 }
278 hack_result.status.clone_from(&output.status);
279 }
280 "mutants" => mutants_result.status.clone_from(&output.status),
281 "duplicates" => {
282 if let Some(ref d) = details {
283 duplicates_result.total_lines = d["totalLines"].as_u64().unwrap_or(0) as u32;
284 duplicates_result.duplicate_lines =
285 d["duplicateLines"].as_u64().unwrap_or(0) as u32;
286 duplicates_result.files_with_duplicates =
287 d["filesWithDuplicates"].as_u64().unwrap_or(0) as u32;
288 }
289 duplicates_result.status.clone_from(&output.status);
290 }
291 "loc" => {
292 if let Some(ref d) = details {
293 loc_result.total_lines = d["totalLines"].as_u64().unwrap_or(0) as u32;
294 loc_result.code_lines = d["codeLines"].as_u64().unwrap_or(0) as u32;
295 loc_result.comment_lines = d["commentLines"].as_u64().unwrap_or(0) as u32;
296 loc_result.blank_lines = d["blankLines"].as_u64().unwrap_or(0) as u32;
297 loc_result.long_lines = d["longLines"].as_u64().unwrap_or(0) as u32;
298 loc_result.max_line_length_found =
299 d["maxLineLengthFound"].as_u64().unwrap_or(0) as usize;
300 loc_result.files = d["files"].as_u64().unwrap_or(0) as u32;
301 }
302 loc_result.status.clone_from(&output.status);
303 }
304 "size" => {
305 if let Some(ref d) = details {
306 size_result.files = d["files"].as_u64().unwrap_or(0) as u32;
307 size_result.max_lines_per_file =
308 d["maxLinesPerFile"].as_u64().unwrap_or(0) as u32;
309 size_result.max_code_lines_per_file =
310 d["maxCodeLinesPerFile"].as_u64().unwrap_or(0) as u32;
311 size_result.max_lines_per_function =
312 d["maxLinesPerFunction"].as_u64().unwrap_or(0) as u32;
313 size_result.max_parameters_per_function =
314 d["maxParametersPerFunction"].as_u64().unwrap_or(0) as u32;
315 if let Some(arr) = d["violations"].as_array() {
316 size_result.violations = arr
317 .iter()
318 .map(|v| crate::schema::SizeViolation {
319 rule_id: v["ruleId"].as_str().unwrap_or("").to_string(),
320 file: v["file"].as_str().unwrap_or("").to_string(),
321 line: v["line"].as_u64().unwrap_or(0) as u32,
322 function: v["function"].as_str().map(String::from),
323 message: v["message"].as_str().unwrap_or("").to_string(),
324 actual: v["actual"].as_u64().unwrap_or(0) as u32,
325 threshold: v["threshold"].as_u64().unwrap_or(0) as u32,
326 severity: v["severity"].as_str().unwrap_or("").to_string(),
327 })
328 .collect();
329 }
330 }
331 size_result.status.clone_from(&output.status);
332 }
333 "complexity" => {
334 if let Some(ref d) = details {
335 complexity_result.functions = d["functions"].as_u64().unwrap_or(0) as u32;
336 complexity_result.max_cyclomatic_complexity =
337 d["maxCyclomaticComplexity"].as_u64().unwrap_or(0) as u32;
338 complexity_result.max_nesting_depth =
339 d["maxNestingDepth"].as_u64().unwrap_or(0) as u32;
340 complexity_result.complex_functions =
341 d["complexFunctions"].as_u64().unwrap_or(0) as u32;
342 if let Some(arr) = d["violations"].as_array() {
343 complexity_result.violations = arr
344 .iter()
345 .map(|v| crate::schema::ComplexityViolation {
346 rule_id: v["ruleId"].as_str().unwrap_or("").to_string(),
347 file: v["file"].as_str().unwrap_or("").to_string(),
348 line: v["line"].as_u64().unwrap_or(0) as u32,
349 function: v["function"].as_str().map(String::from),
350 message: v["message"].as_str().unwrap_or("").to_string(),
351 actual: v["actual"].as_u64().unwrap_or(0) as u32,
352 threshold: v["threshold"].as_u64().unwrap_or(0) as u32,
353 severity: v["severity"].as_str().unwrap_or("").to_string(),
354 })
355 .collect();
356 }
357 }
358 complexity_result.status.clone_from(&output.status);
359 }
360 _ => {}
361 }
362 }
363
364 MetricsSummary {
365 schema_version: "1".to_string(),
366 generated_at: crate::util::chrono_now(),
367 rustquty_version: env!("CARGO_PKG_VERSION").to_string(),
368 project: ProjectInfo {
369 name: project_name.to_string(),
370 rust_edition: rust_edition.to_string(),
371 workspace_root: workspace_root.to_string(),
372 },
373 collectors: crate::schema::CollectorsSummary {
374 fmt: fmt_result,
375 clippy: clippy_result,
376 tests: test_result,
377 coverage: coverage_result,
378 deny: deny_result,
379 audit: audit_result,
380 hack: hack_result,
381 mutants: mutants_result,
382 duplicates: duplicates_result,
383 loc: loc_result,
384 size: size_result,
385 complexity: complexity_result,
386 },
387 }
388}
389
390pub fn run_collectors(
394 collectors: &[Box<dyn Collector>],
395 ctx: &Context,
396 parallel: bool,
397) -> MetricsSummary {
398 let results = execute_collectors(collectors, ctx, parallel);
399 let project_name = ctx
400 .workspace_root
401 .file_name()
402 .map(|s| s.to_string_lossy().to_string())
403 .unwrap_or_else(|| "unknown".to_string());
404 assemble_results(
405 &results,
406 &project_name,
407 "2021",
408 &ctx.workspace_root.to_string_lossy(),
409 )
410}
411
412#[cfg(test)]
413mod collector_tests {
414 use super::*;
415
416 #[test]
417 fn test_mock_collector() {
418 let mock = MockCollector {
419 name_val: "test",
420 available: true,
421 output: CollectorOutput {
422 status: CollectorStatus::Pass,
423 duration_ms: 10,
424 stdout: String::new(),
425 stderr: String::new(),
426 },
427 };
428 assert_eq!(mock.name(), "test");
429 assert!(mock.is_available());
430 }
431}