1use std::path::Path;
4
5use crate::{
6 CoverageIntelligenceRecommendation, CoverageIntelligenceReport, CoverageIntelligenceVerdict,
7 ExceededThreshold, FindingSeverity, HealthReport, RuntimeCoverageReport,
8 RuntimeCoverageVerdict, SarifDocumentInput, SarifSourceSnippetCache as SourceSnippetCache,
9 StylingFindingSeverity, build_sarif_document,
10 build_sarif_result_with_snippet as sarif_result_with_snippet, normalize_uri,
11};
12use fallow_types::duplicates::{CloneGroup, DuplicationReport};
13use fallow_types::output_dead_code::EffectiveSeverity;
14
15type SarifRuleBuilder<'a> = dyn Fn(&str, &str, &str) -> serde_json::Value + 'a;
16
17#[must_use]
19pub fn build_duplication_sarif(
20 report: &DuplicationReport,
21 root: &Path,
22 rule_builder: &SarifRuleBuilder<'_>,
23) -> serde_json::Value {
24 build_duplication_sarif_with_group(report, root, rule_builder, |_| None)
25}
26
27#[must_use]
29pub fn build_grouped_duplication_sarif(
30 report: &DuplicationReport,
31 root: &Path,
32 rule_builder: &SarifRuleBuilder<'_>,
33 group_for_clone: impl Fn(&CloneGroup) -> String,
34) -> serde_json::Value {
35 build_duplication_sarif_with_group(report, root, rule_builder, |group| {
36 Some(group_for_clone(group))
37 })
38}
39
40#[expect(
41 clippy::cast_possible_truncation,
42 reason = "line and column values are bounded by source size"
43)]
44fn build_duplication_sarif_with_group(
45 report: &DuplicationReport,
46 root: &Path,
47 rule_builder: &SarifRuleBuilder<'_>,
48 group_for_clone: impl Fn(&CloneGroup) -> Option<String>,
49) -> serde_json::Value {
50 let mut sarif_results = Vec::new();
51 let mut snippets = SourceSnippetCache::with_root(root);
52
53 for (i, group) in report.clone_groups.iter().enumerate() {
54 let group_value = group_for_clone(group);
55 for instance in &group.instances {
56 let uri = relative_uri(&instance.file, root);
57 let source_snippet = snippets.line(&instance.file, instance.start_line as u32);
58 let mut result = sarif_result_with_snippet(
59 "fallow/code-duplication",
60 "warning",
61 &format!(
62 "Code clone group {} ({} lines, {} instances)",
63 i + 1,
64 group.line_count,
65 group.instances.len()
66 ),
67 &uri,
68 Some((instance.start_line as u32, (instance.start_col + 1) as u32)),
69 source_snippet.as_deref(),
70 );
71 if let Some(group) = &group_value {
72 set_sarif_result_property(&mut result, "group", group.clone());
73 }
74 sarif_results.push(result);
75 }
76 }
77
78 let rules = vec![rule_builder(
79 "fallow/code-duplication",
80 "Duplicated code block",
81 "warning",
82 )];
83 sarif_document(&sarif_results, &rules)
84}
85
86#[must_use]
88pub fn build_health_sarif(
89 report: &HealthReport,
90 root: &Path,
91 rule_builder: &SarifRuleBuilder<'_>,
92) -> serde_json::Value {
93 let mut sarif_results = Vec::new();
94 let mut snippets = SourceSnippetCache::with_root(root);
95
96 append_health_sarif_results(report, root, &mut sarif_results, &mut snippets);
97 let health_rules = health_sarif_rules(rule_builder, report);
98 sarif_document(&sarif_results, &health_rules)
99}
100
101pub fn annotate_sarif_results(
103 sarif: &mut serde_json::Value,
104 property: &str,
105 mut value_for_uri: impl FnMut(&str) -> String,
106) {
107 if let Some(runs) = sarif
108 .get_mut("runs")
109 .and_then(serde_json::Value::as_array_mut)
110 {
111 for run in runs {
112 if let Some(results) = run
113 .get_mut("results")
114 .and_then(serde_json::Value::as_array_mut)
115 {
116 for result in results {
117 let uri = result
118 .pointer("/locations/0/physicalLocation/artifactLocation/uri")
119 .and_then(serde_json::Value::as_str)
120 .unwrap_or("");
121 let value = value_for_uri(uri);
122 set_sarif_result_property(result, property, value);
123 }
124 }
125 }
126 }
127}
128
129fn set_sarif_result_property(result: &mut serde_json::Value, key: &str, value: String) {
130 let Some(result) = result.as_object_mut() else {
131 return;
132 };
133 let props = result
134 .entry("properties")
135 .or_insert_with(|| serde_json::json!({}));
136 let Some(props) = props.as_object_mut() else {
137 return;
138 };
139 props.insert(key.to_string(), serde_json::Value::String(value));
140}
141
142fn append_health_sarif_results(
143 report: &HealthReport,
144 root: &Path,
145 sarif_results: &mut Vec<serde_json::Value>,
146 snippets: &mut SourceSnippetCache,
147) {
148 append_complexity_sarif_results(sarif_results, report, root, snippets);
149
150 if let Some(ref production) = report.runtime_coverage {
151 append_runtime_coverage_sarif_results(sarif_results, production, root, snippets);
152 }
153 if let Some(ref intelligence) = report.coverage_intelligence {
154 append_coverage_intelligence_sarif_results(sarif_results, intelligence, root, snippets);
155 }
156
157 append_refactoring_target_sarif_results(sarif_results, report, root);
158 append_coverage_gap_sarif_results(sarif_results, report, root, snippets);
159 append_styling_sarif_results(sarif_results, report, root);
160}
161
162fn append_styling_sarif_results(
166 sarif_results: &mut Vec<serde_json::Value>,
167 report: &HealthReport,
168 root: &Path,
169) {
170 for finding in &report.styling_findings {
171 let uri = relative_uri(std::path::Path::new(&finding.path), root);
172 let message = format!(
173 "[{}] {}: `{}`",
174 finding.code, finding.sub_kind, finding.value
175 );
176 sarif_results.push(sarif_result(
177 &format!("fallow/{}", finding.code),
178 styling_sarif_level(finding.effective_severity),
179 &message,
180 &uri,
181 Some((finding.line, 1)),
182 ));
183 }
184}
185
186fn health_styling_sarif_rules(
187 rule_builder: &SarifRuleBuilder<'_>,
188 report: &HealthReport,
189) -> Vec<serde_json::Value> {
190 vec![
191 rule_builder(
192 "fallow/css-token-drift",
193 "CSS / CSS-in-JS design-token drift (a hardcoded value where a token exists)",
194 styling_rule_default_level(report, "css-token-drift"),
195 ),
196 rule_builder(
197 "fallow/css-duplicate-block",
198 "CSS / CSS-in-JS duplicate declaration block",
199 styling_rule_default_level(report, "css-duplicate-block"),
200 ),
201 rule_builder(
202 "fallow/css-selector-complexity",
203 "CSS selector complexity, deep nesting, or important density",
204 styling_rule_default_level(report, "css-selector-complexity"),
205 ),
206 rule_builder(
207 "fallow/css-dead-surface",
208 "CSS / CSS-in-JS dead styling surface",
209 styling_rule_default_level(report, "css-dead-surface"),
210 ),
211 rule_builder(
212 "fallow/css-broken-reference",
213 "CSS / CSS-in-JS reference resolves to no known styling definition",
214 styling_rule_default_level(report, "css-broken-reference"),
215 ),
216 ]
217}
218
219fn health_sarif_rules(
220 rule_builder: &SarifRuleBuilder<'_>,
221 report: &HealthReport,
222) -> Vec<serde_json::Value> {
223 let mut rules = health_complexity_sarif_rules(rule_builder);
224 rules.extend(health_runtime_sarif_rules(rule_builder));
225 rules.extend(health_coverage_intelligence_sarif_rules(rule_builder));
226 rules.extend(health_styling_sarif_rules(rule_builder, report));
227 rules
228}
229
230fn styling_rule_default_level(report: &HealthReport, code: &str) -> &'static str {
231 if report.styling_findings.iter().any(|finding| {
232 finding.code == code && finding.effective_severity == StylingFindingSeverity::Error
233 }) {
234 "error"
235 } else {
236 "warning"
237 }
238}
239
240const fn styling_sarif_level(severity: StylingFindingSeverity) -> &'static str {
241 match severity {
242 StylingFindingSeverity::Error => "error",
243 StylingFindingSeverity::Warn => "warning",
244 }
245}
246
247fn health_complexity_sarif_rules(rule_builder: &SarifRuleBuilder<'_>) -> Vec<serde_json::Value> {
248 vec![
249 rule_builder(
250 "fallow/high-cyclomatic-complexity",
251 "Function has high cyclomatic complexity",
252 "note",
253 ),
254 rule_builder(
255 "fallow/high-cognitive-complexity",
256 "Function has high cognitive complexity",
257 "note",
258 ),
259 rule_builder(
260 "fallow/high-complexity",
261 "Function exceeds both complexity thresholds",
262 "note",
263 ),
264 rule_builder(
265 "fallow/high-crap-score",
266 "Function has a high CRAP score (high complexity combined with low coverage)",
267 "warning",
268 ),
269 rule_builder(
270 "fallow/refactoring-target",
271 "File identified as a high-priority refactoring candidate",
272 "warning",
273 ),
274 ]
275}
276
277fn health_runtime_sarif_rules(rule_builder: &SarifRuleBuilder<'_>) -> Vec<serde_json::Value> {
278 vec![
279 rule_builder(
280 "fallow/untested-file",
281 "Runtime-reachable file has no test dependency path",
282 "warning",
283 ),
284 rule_builder(
285 "fallow/untested-export",
286 "Runtime-reachable export has no test dependency path",
287 "warning",
288 ),
289 rule_builder(
290 "fallow/runtime-safe-to-delete",
291 "Function is statically unused and was never invoked in production",
292 "warning",
293 ),
294 rule_builder(
295 "fallow/runtime-review-required",
296 "Function is statically used but was never invoked in production",
297 "warning",
298 ),
299 rule_builder(
300 "fallow/runtime-low-traffic",
301 "Function was invoked below the low-traffic threshold relative to total trace count",
302 "note",
303 ),
304 rule_builder(
305 "fallow/runtime-coverage-unavailable",
306 "Runtime coverage could not be resolved for this function",
307 "note",
308 ),
309 rule_builder(
310 "fallow/runtime-coverage",
311 "Runtime coverage finding",
312 "note",
313 ),
314 ]
315}
316
317fn health_coverage_intelligence_sarif_rules(
318 rule_builder: &SarifRuleBuilder<'_>,
319) -> Vec<serde_json::Value> {
320 vec![
321 rule_builder(
322 "fallow/coverage-intelligence-risky-change",
323 "Changed hot path combines high CRAP and low test coverage",
324 "warning",
325 ),
326 rule_builder(
327 "fallow/coverage-intelligence-delete",
328 "Static and runtime evidence indicate code can be deleted",
329 "warning",
330 ),
331 rule_builder(
332 "fallow/coverage-intelligence-review",
333 "Cold reachable uncovered code needs owner review",
334 "warning",
335 ),
336 rule_builder(
337 "fallow/coverage-intelligence-refactor",
338 "Hot covered code has high CRAP and should be refactored carefully",
339 "warning",
340 ),
341 ]
342}
343
344fn append_complexity_sarif_results(
345 sarif_results: &mut Vec<serde_json::Value>,
346 report: &HealthReport,
347 root: &Path,
348 snippets: &mut SourceSnippetCache,
349) {
350 for finding in &report.findings {
351 let uri = relative_uri(&finding.path, root);
352 let (rule_id, message) = health_complexity_sarif_message(finding, report);
353 let level = complexity_sarif_level(finding);
354 let source_snippet = snippets.line(&finding.path, finding.line);
355 let mut result = sarif_result_with_snippet(
356 rule_id,
357 level,
358 &message,
359 &uri,
360 Some((finding.line, finding.col + 1)),
361 source_snippet.as_deref(),
362 );
363 set_audit_attribution_properties(&mut result, finding);
364 sarif_results.push(result);
365 }
366}
367
368fn set_audit_attribution_properties(
372 result: &mut serde_json::Value,
373 finding: &crate::HealthFinding,
374) {
375 if finding.attribution.is_none() && finding.gate_exemption.is_none() {
376 return;
377 }
378 let Some(object) = result.as_object_mut() else {
379 return;
380 };
381 let props = object
382 .entry("properties")
383 .or_insert_with(|| serde_json::json!({}));
384 let Some(props) = props.as_object_mut() else {
385 return;
386 };
387 if let Some(attribution) = finding.attribution {
388 props.insert(
389 "attribution".to_string(),
390 serde_json::to_value(attribution).unwrap_or(serde_json::Value::Null),
391 );
392 }
393 if let Some(exemption) = finding.gate_exemption {
394 props.insert(
395 "gate_exemption".to_string(),
396 serde_json::Value::String(exemption.token().to_string()),
397 );
398 }
399 if !finding.worsened_metrics.is_empty() {
400 props.insert(
401 "worsened_metrics".to_string(),
402 serde_json::to_value(&finding.worsened_metrics).unwrap_or(serde_json::Value::Null),
403 );
404 }
405}
406
407const fn complexity_sarif_level(finding: &crate::ComplexityViolation) -> &'static str {
413 match finding.effective_severity {
414 Some(EffectiveSeverity::Error) => "error",
415 Some(EffectiveSeverity::Warn) => "warning",
416 None => match finding.severity {
417 FindingSeverity::Critical => "error",
418 FindingSeverity::High => "warning",
419 FindingSeverity::Moderate => "note",
420 },
421 }
422}
423
424fn health_complexity_sarif_message(
430 finding: &crate::ComplexityViolation,
431 report: &HealthReport,
432) -> (&'static str, String) {
433 let thresholds = finding.resolved_thresholds(&report.summary);
434 match finding.exceeded {
435 ExceededThreshold::Cyclomatic => (
436 "fallow/high-cyclomatic-complexity",
437 format!(
438 "'{}' has cyclomatic complexity {} (threshold: {})",
439 finding.name, finding.cyclomatic, thresholds.max_cyclomatic,
440 ),
441 ),
442 ExceededThreshold::Cognitive => (
443 "fallow/high-cognitive-complexity",
444 format!(
445 "'{}' has cognitive complexity {} (threshold: {})",
446 finding.name, finding.cognitive, thresholds.max_cognitive,
447 ),
448 ),
449 ExceededThreshold::Both => (
450 "fallow/high-complexity",
451 format!(
452 "'{}' has cyclomatic complexity {} (threshold: {}) and cognitive complexity {} (threshold: {})",
453 finding.name,
454 finding.cyclomatic,
455 thresholds.max_cyclomatic,
456 finding.cognitive,
457 thresholds.max_cognitive,
458 ),
459 ),
460 ExceededThreshold::Crap
461 | ExceededThreshold::CyclomaticCrap
462 | ExceededThreshold::CognitiveCrap
463 | ExceededThreshold::All => {
464 let crap = finding.crap.unwrap_or(0.0);
465 let coverage = finding
466 .coverage_pct
467 .map(|pct| format!(", coverage {pct:.0}%"))
468 .unwrap_or_default();
469 (
470 "fallow/high-crap-score",
471 format!(
472 "'{}' has CRAP score {:.1} (threshold: {:.1}, cyclomatic {}{})",
473 finding.name, crap, thresholds.max_crap, finding.cyclomatic, coverage,
474 ),
475 )
476 }
477 }
478}
479
480fn append_refactoring_target_sarif_results(
481 sarif_results: &mut Vec<serde_json::Value>,
482 report: &HealthReport,
483 root: &Path,
484) {
485 for target in &report.targets {
486 let uri = relative_uri(&target.path, root);
487 let message = format!(
488 "[{}] {} (priority: {:.1}, efficiency: {:.1}, effort: {}, confidence: {})",
489 target.category.label(),
490 target.recommendation,
491 target.priority,
492 target.efficiency,
493 target.effort.label(),
494 target.confidence.label(),
495 );
496 sarif_results.push(sarif_result(
497 "fallow/refactoring-target",
498 "warning",
499 &message,
500 &uri,
501 None,
502 ));
503 }
504}
505
506fn append_coverage_gap_sarif_results(
507 sarif_results: &mut Vec<serde_json::Value>,
508 report: &HealthReport,
509 root: &Path,
510 snippets: &mut SourceSnippetCache,
511) {
512 let Some(ref gaps) = report.coverage_gaps else {
513 return;
514 };
515 for item in &gaps.files {
516 let uri = relative_uri(&item.file.path, root);
517 let message = format!(
518 "File is runtime-reachable but has no test dependency path ({} value export{})",
519 item.file.value_export_count,
520 if item.file.value_export_count == 1 {
521 ""
522 } else {
523 "s"
524 },
525 );
526 sarif_results.push(sarif_result(
527 "fallow/untested-file",
528 "warning",
529 &message,
530 &uri,
531 None,
532 ));
533 }
534
535 for item in &gaps.exports {
536 let uri = relative_uri(&item.export.path, root);
537 let message = format!(
538 "Export '{}' is runtime-reachable but never referenced by test-reachable modules",
539 item.export.export_name
540 );
541 let source_snippet = snippets.line(&item.export.path, item.export.line);
542 sarif_results.push(sarif_result_with_snippet(
543 "fallow/untested-export",
544 "warning",
545 &message,
546 &uri,
547 Some((item.export.line, item.export.col + 1)),
548 source_snippet.as_deref(),
549 ));
550 }
551}
552
553fn append_runtime_coverage_sarif_results(
554 sarif_results: &mut Vec<serde_json::Value>,
555 production: &RuntimeCoverageReport,
556 root: &Path,
557 snippets: &mut SourceSnippetCache,
558) {
559 for finding in &production.findings {
560 let uri = relative_uri(&finding.path, root);
561 let rule_id = match finding.verdict {
562 RuntimeCoverageVerdict::SafeToDelete => "fallow/runtime-safe-to-delete",
563 RuntimeCoverageVerdict::ReviewRequired => "fallow/runtime-review-required",
564 RuntimeCoverageVerdict::LowTraffic => "fallow/runtime-low-traffic",
565 RuntimeCoverageVerdict::CoverageUnavailable => "fallow/runtime-coverage-unavailable",
566 RuntimeCoverageVerdict::Active | RuntimeCoverageVerdict::Unknown => {
567 "fallow/runtime-coverage"
568 }
569 };
570 let level = match finding.verdict {
571 RuntimeCoverageVerdict::SafeToDelete | RuntimeCoverageVerdict::ReviewRequired => {
572 "warning"
573 }
574 _ => "note",
575 };
576 let invocations_hint = finding.invocations.map_or_else(
577 || "untracked".to_owned(),
578 |hits| format!("{hits} invocations"),
579 );
580 let message = format!(
581 "'{}' runtime coverage verdict: {} ({})",
582 finding.function,
583 finding.verdict.human_label(),
584 invocations_hint,
585 );
586 let source_snippet = snippets.line(&finding.path, finding.line);
587 sarif_results.push(sarif_result_with_snippet(
588 rule_id,
589 level,
590 &message,
591 &uri,
592 Some((finding.line, 1)),
593 source_snippet.as_deref(),
594 ));
595 }
596}
597
598fn append_coverage_intelligence_sarif_results(
599 sarif_results: &mut Vec<serde_json::Value>,
600 intelligence: &CoverageIntelligenceReport,
601 root: &Path,
602 snippets: &mut SourceSnippetCache,
603) {
604 for finding in &intelligence.findings {
605 let rule_id = coverage_intelligence_rule_id(finding.recommendation);
606 let level = match finding.verdict {
607 CoverageIntelligenceVerdict::Clean | CoverageIntelligenceVerdict::Unknown => continue,
608 _ => "warning",
609 };
610 let uri = relative_uri(&finding.path, root);
611 let identity = finding.identity.as_deref().unwrap_or("code");
612 let signals = finding
613 .signals
614 .iter()
615 .map(ToString::to_string)
616 .collect::<Vec<_>>()
617 .join(", ");
618 let message = format!(
619 "'{}' coverage intelligence verdict: {} ({}, signals: {})",
620 identity, finding.verdict, finding.recommendation, signals,
621 );
622 let source_snippet = snippets.line(&finding.path, finding.line);
623 let mut result = sarif_result_with_snippet(
624 rule_id,
625 level,
626 &message,
627 &uri,
628 Some((finding.line, 1)),
629 source_snippet.as_deref(),
630 );
631 result["properties"] = serde_json::json!({
632 "coverage_intelligence_id": &finding.id,
633 "verdict": finding.verdict,
634 "recommendation": finding.recommendation,
635 "confidence": finding.confidence,
636 "signals": &finding.signals,
637 "related_ids": &finding.related_ids,
638 });
639 sarif_results.push(result);
640 }
641}
642
643fn coverage_intelligence_rule_id(
644 recommendation: CoverageIntelligenceRecommendation,
645) -> &'static str {
646 match recommendation {
647 CoverageIntelligenceRecommendation::AddTestOrSplitBeforeMerge => {
648 "fallow/coverage-intelligence-risky-change"
649 }
650 CoverageIntelligenceRecommendation::DeleteAfterConfirmingOwner => {
651 "fallow/coverage-intelligence-delete"
652 }
653 CoverageIntelligenceRecommendation::ReviewBeforeChanging => {
654 "fallow/coverage-intelligence-review"
655 }
656 CoverageIntelligenceRecommendation::RefactorCarefullyKeepBehavior => {
657 "fallow/coverage-intelligence-refactor"
658 }
659 }
660}
661
662fn sarif_document(
663 sarif_results: &[serde_json::Value],
664 sarif_rules: &[serde_json::Value],
665) -> serde_json::Value {
666 build_sarif_document(SarifDocumentInput {
667 results: sarif_results,
668 rules: sarif_rules,
669 tool_version: env!("CARGO_PKG_VERSION"),
670 })
671}
672
673fn sarif_result(
674 rule_id: &str,
675 level: &str,
676 message: &str,
677 uri: &str,
678 region: Option<(u32, u32)>,
679) -> serde_json::Value {
680 sarif_result_with_snippet(rule_id, level, message, uri, region, None)
681}
682
683fn relative_uri(path: &Path, root: &Path) -> String {
684 normalize_uri(
685 &path
686 .strip_prefix(root)
687 .unwrap_or(path)
688 .display()
689 .to_string(),
690 )
691}
692
693#[cfg(test)]
694mod tests {
695 use std::path::PathBuf;
696
697 use crate::{SarifRuleInput, build_sarif_rule};
698 use fallow_types::duplicates::{CloneGroup, CloneInstance, DuplicationStats};
699
700 use super::*;
701
702 fn rule(id: &str, short_description: &str, level: &str) -> serde_json::Value {
703 build_sarif_rule(SarifRuleInput {
704 id,
705 short_description,
706 level,
707 full_description: None,
708 help_uri: None,
709 })
710 }
711
712 #[test]
713 fn grouped_duplication_sarif_attaches_group_property() {
714 let root = PathBuf::from("/repo");
715 let report = DuplicationReport {
716 clone_groups: vec![CloneGroup {
717 instances: vec![CloneInstance {
718 is_symlink: false,
719 file: root.join("src/a.ts"),
720 start_line: 2,
721 end_line: 5,
722 start_col: 0,
723 end_col: 1,
724 fragment: "copy();".to_string(),
725 }],
726 token_count: 10,
727 line_count: 4,
728 similarity: None,
729 }],
730 clone_families: Vec::new(),
731 mirrored_directories: Vec::new(),
732 stats: DuplicationStats::default(),
733 };
734
735 let sarif = build_grouped_duplication_sarif(&report, &root, &rule, |_| "src".to_string());
736
737 assert_eq!(sarif["runs"][0]["results"][0]["properties"]["group"], "src");
738 assert_eq!(
739 sarif["runs"][0]["results"][0]["locations"][0]["physicalLocation"]["artifactLocation"]
740 ["uri"],
741 "src/a.ts"
742 );
743 }
744
745 fn sarif_message_violation(
746 exceeded: ExceededThreshold,
747 effective: Option<crate::HealthEffectiveThresholds>,
748 ) -> crate::ComplexityViolation {
749 crate::ComplexityViolation {
750 path: PathBuf::from("src/Widget.svelte"),
751 name: "<template>".to_string(),
752 line: 5,
753 col: 0,
754 cyclomatic: 25,
755 standard_cyclomatic: None,
756 cognitive: 21,
757 line_count: 13,
758 param_count: 0,
759 react_hook_count: 0,
760 react_jsx_max_depth: 0,
761 react_prop_count: 0,
762 react_hook_profile: None,
763 exceeded,
764 severity: crate::FindingSeverity::High,
765 effective_severity: None,
766 crap: Some(90.0),
767 mutation: None,
768 coverage_pct: None,
769 coverage_tier: None,
770 coverage_source: None,
771 inherited_from: None,
772 component_rollup: None,
773 contributions: Vec::new(),
774 effective_thresholds: effective,
775 threshold_source: effective.map(|_| crate::ThresholdSource::Override),
776 }
777 }
778
779 #[test]
780 fn sarif_message_uses_the_override_ceiling_not_the_global_one() {
781 let report = HealthReport::default();
782 let overridden = crate::HealthEffectiveThresholds {
783 max_cyclomatic: 500,
784 max_cognitive: 500,
785 max_crap: 500.0,
786 max_unit_size: 60,
787 };
788
789 let (global_rule, global_message) = health_complexity_sarif_message(
790 &sarif_message_violation(ExceededThreshold::Cyclomatic, None),
791 &report,
792 );
793 assert_eq!(global_rule, "fallow/high-cyclomatic-complexity");
794 assert!(
795 global_message.contains(&format!(
796 "threshold: {}",
797 report.summary.max_cyclomatic_threshold
798 )),
799 "{global_message}"
800 );
801
802 let (_, override_message) = health_complexity_sarif_message(
803 &sarif_message_violation(ExceededThreshold::Cyclomatic, Some(overridden)),
804 &report,
805 );
806 assert!(
807 override_message.contains("threshold: 500"),
808 "an override-affected finding must not be described against the global ceiling: {override_message}"
809 );
810
811 let (crap_rule, crap_message) = health_complexity_sarif_message(
812 &sarif_message_violation(ExceededThreshold::Crap, Some(overridden)),
813 &report,
814 );
815 assert_eq!(crap_rule, "fallow/high-crap-score");
816 assert!(crap_message.contains("threshold: 500.0"), "{crap_message}");
817 }
818}