1use std::path::Path;
4
5use fallow_output::{
6 CodeClimateIssue, CodeClimateIssueInput, CodeClimateSeverity, ComplexityViolation,
7 CoverageIntelligenceFinding, CoverageIntelligenceRecommendation, CoverageIntelligenceVerdict,
8 ExceededThreshold, FindingSeverity, HealthReport, HealthSummary, RuntimeCoverageFinding,
9 RuntimeCoverageVerdict, StylingFinding, StylingFindingSeverity, UntestedExportFinding,
10 UntestedFileFinding, build_codeclimate_issue, codeclimate_fingerprint_hash, normalize_uri,
11};
12use fallow_types::output_dead_code::EffectiveSeverity;
13
14struct HealthCodeClimateContext<'a> {
15 root: &'a Path,
16 summary: &'a HealthSummary,
20}
21
22impl HealthCodeClimateContext<'_> {
23 fn complexity_issue(&self, finding: &ComplexityViolation) -> CodeClimateIssue {
24 let path = codeclimate_path(&finding.path, self.root);
25 let check_name = complexity_check_name(finding);
26 let line_str = finding.line.to_string();
27 let fp = codeclimate_fingerprint_hash(&[check_name, &path, &line_str, &finding.name]);
28 build_codeclimate_issue(CodeClimateIssueInput {
29 check_name,
30 description: &self.complexity_description(finding),
31 severity: complexity_codeclimate_severity(finding),
32 category: "Complexity",
33 path: &path,
34 begin_line: Some(finding.line),
35 fingerprint: &fp,
36 })
37 }
38
39 fn styling_issue(&self, finding: &StylingFinding) -> CodeClimateIssue {
40 let path = codeclimate_path(Path::new(&finding.path), self.root);
41 let check_name = format!("fallow/{}", finding.code);
42 let description = format!("[{}] {}: {}", finding.code, finding.sub_kind, finding.value);
43 let line_str = finding.line.to_string();
44 let fp = codeclimate_fingerprint_hash(&[
45 &check_name,
46 &path,
47 &line_str,
48 &finding.sub_kind,
49 &finding.value,
50 ]);
51 build_codeclimate_issue(CodeClimateIssueInput {
52 check_name: &check_name,
53 description: &description,
54 severity: styling_finding_severity(finding.effective_severity),
55 category: "Style",
56 path: &path,
57 begin_line: Some(finding.line),
58 fingerprint: &fp,
59 })
60 }
61
62 fn complexity_description(&self, finding: &ComplexityViolation) -> String {
63 let thresholds = finding.resolved_thresholds(self.summary);
64 match finding.exceeded {
65 ExceededThreshold::Both => format!(
66 "'{}' has cyclomatic complexity {} (threshold: {}) and cognitive complexity {} (threshold: {})",
67 finding.name,
68 finding.cyclomatic,
69 thresholds.max_cyclomatic,
70 finding.cognitive,
71 thresholds.max_cognitive
72 ),
73 ExceededThreshold::Cyclomatic => format!(
74 "'{}' has cyclomatic complexity {} (threshold: {})",
75 finding.name, finding.cyclomatic, thresholds.max_cyclomatic
76 ),
77 ExceededThreshold::Cognitive => format!(
78 "'{}' has cognitive complexity {} (threshold: {})",
79 finding.name, finding.cognitive, thresholds.max_cognitive
80 ),
81 ExceededThreshold::Crap
82 | ExceededThreshold::CyclomaticCrap
83 | ExceededThreshold::CognitiveCrap
84 | ExceededThreshold::All => {
85 let crap = finding.crap.unwrap_or(0.0);
86 let coverage = finding
87 .coverage_pct
88 .map(|pct| format!(", coverage {pct:.0}%"))
89 .unwrap_or_default();
90 format!(
91 "'{}' has CRAP score {crap:.1} (threshold: {:.1}, cyclomatic {}{coverage})",
92 finding.name, thresholds.max_crap, finding.cyclomatic,
93 )
94 }
95 }
96 }
97
98 fn runtime_coverage_issue(&self, finding: &RuntimeCoverageFinding) -> CodeClimateIssue {
99 let path = codeclimate_path(&finding.path, self.root);
100 let check_name = runtime_coverage_check_name(finding.verdict);
101 let invocations_hint = finding.invocations.map_or_else(
102 || "untracked".to_owned(),
103 |hits| format!("{hits} invocations"),
104 );
105 let description = format!(
106 "'{}' runtime coverage verdict: {} ({})",
107 finding.function,
108 finding.verdict.human_label(),
109 invocations_hint,
110 );
111 let fp = codeclimate_fingerprint_hash(&[
112 check_name,
113 &path,
114 &finding.line.to_string(),
115 &finding.function,
116 ]);
117 build_codeclimate_issue(CodeClimateIssueInput {
118 check_name,
119 description: &description,
120 severity: runtime_coverage_severity(finding.verdict),
121 category: "Bug Risk",
122 path: &path,
123 begin_line: Some(finding.line),
124 fingerprint: &fp,
125 })
126 }
127
128 fn coverage_intelligence_issue(
129 &self,
130 finding: &CoverageIntelligenceFinding,
131 ) -> Option<CodeClimateIssue> {
132 let severity = coverage_intelligence_severity(finding.verdict)?;
133 let path = codeclimate_path(&finding.path, self.root);
134 let check_name = coverage_intelligence_check_name(finding.recommendation);
135 let identity = finding.identity.as_deref().unwrap_or("code");
136 let description = format!(
137 "'{}' coverage intelligence verdict: {} ({})",
138 identity, finding.verdict, finding.recommendation,
139 );
140 let fp = codeclimate_fingerprint_hash(&[
141 check_name,
142 &path,
143 &finding.line.to_string(),
144 identity,
145 &finding.id,
146 ]);
147 Some(build_codeclimate_issue(CodeClimateIssueInput {
148 check_name,
149 description: &description,
150 severity,
151 category: "Bug Risk",
152 path: &path,
153 begin_line: Some(finding.line),
154 fingerprint: &fp,
155 }))
156 }
157
158 fn untested_file_issue(&self, item: &UntestedFileFinding) -> CodeClimateIssue {
159 let path = codeclimate_path(&item.file.path, self.root);
160 let description = format!(
161 "File is runtime-reachable but has no test dependency path ({} value export{})",
162 item.file.value_export_count,
163 if item.file.value_export_count == 1 {
164 ""
165 } else {
166 "s"
167 },
168 );
169 let fp = codeclimate_fingerprint_hash(&["fallow/untested-file", &path]);
170 build_codeclimate_issue(CodeClimateIssueInput {
171 check_name: "fallow/untested-file",
172 description: &description,
173 severity: CodeClimateSeverity::Minor,
174 category: "Coverage",
175 path: &path,
176 begin_line: None,
177 fingerprint: &fp,
178 })
179 }
180
181 fn untested_export_issue(&self, item: &UntestedExportFinding) -> CodeClimateIssue {
182 let path = codeclimate_path(&item.export.path, self.root);
183 let description = format!(
184 "Export '{}' is runtime-reachable but never referenced by test-reachable modules",
185 item.export.export_name
186 );
187 let line_str = item.export.line.to_string();
188 let fp = codeclimate_fingerprint_hash(&[
189 "fallow/untested-export",
190 &path,
191 &line_str,
192 &item.export.export_name,
193 ]);
194 build_codeclimate_issue(CodeClimateIssueInput {
195 check_name: "fallow/untested-export",
196 description: &description,
197 severity: CodeClimateSeverity::Minor,
198 category: "Coverage",
199 path: &path,
200 begin_line: Some(item.export.line),
201 fingerprint: &fp,
202 })
203 }
204}
205
206#[must_use]
208pub fn build_health_codeclimate(report: &HealthReport, root: &Path) -> Vec<CodeClimateIssue> {
209 let mut issues = Vec::new();
210 let ctx = HealthCodeClimateContext {
211 root,
212 summary: &report.summary,
213 };
214
215 for finding in &report.findings {
216 issues.push(ctx.complexity_issue(finding));
217 }
218 for finding in &report.styling_findings {
219 issues.push(ctx.styling_issue(finding));
220 }
221
222 if let Some(ref production) = report.runtime_coverage {
223 for finding in &production.findings {
224 issues.push(ctx.runtime_coverage_issue(finding));
225 }
226 }
227
228 if let Some(ref intelligence) = report.coverage_intelligence {
229 for finding in &intelligence.findings {
230 if let Some(issue) = ctx.coverage_intelligence_issue(finding) {
231 issues.push(issue);
232 }
233 }
234 }
235
236 if let Some(ref gaps) = report.coverage_gaps {
237 for item in &gaps.files {
238 issues.push(ctx.untested_file_issue(item));
239 }
240
241 for item in &gaps.exports {
242 issues.push(ctx.untested_export_issue(item));
243 }
244 }
245
246 issues
247}
248
249fn codeclimate_path(path: &Path, root: &Path) -> String {
250 normalize_uri(
251 &path
252 .strip_prefix(root)
253 .unwrap_or(path)
254 .display()
255 .to_string(),
256 )
257}
258
259const fn coverage_intelligence_check_name(
260 recommendation: CoverageIntelligenceRecommendation,
261) -> &'static str {
262 match recommendation {
263 CoverageIntelligenceRecommendation::AddTestOrSplitBeforeMerge => {
264 "fallow/coverage-intelligence-risky-change"
265 }
266 CoverageIntelligenceRecommendation::DeleteAfterConfirmingOwner => {
267 "fallow/coverage-intelligence-delete"
268 }
269 CoverageIntelligenceRecommendation::ReviewBeforeChanging => {
270 "fallow/coverage-intelligence-review"
271 }
272 CoverageIntelligenceRecommendation::RefactorCarefullyKeepBehavior => {
273 "fallow/coverage-intelligence-refactor"
274 }
275 }
276}
277
278const fn complexity_check_name(finding: &ComplexityViolation) -> &'static str {
279 match finding.exceeded {
280 ExceededThreshold::Both => "fallow/high-complexity",
281 ExceededThreshold::Cyclomatic => "fallow/high-cyclomatic-complexity",
282 ExceededThreshold::Cognitive => "fallow/high-cognitive-complexity",
283 ExceededThreshold::Crap
284 | ExceededThreshold::CyclomaticCrap
285 | ExceededThreshold::CognitiveCrap
286 | ExceededThreshold::All => "fallow/high-crap-score",
287 }
288}
289
290const fn complexity_codeclimate_severity(finding: &ComplexityViolation) -> CodeClimateSeverity {
297 match finding.effective_severity {
298 Some(EffectiveSeverity::Error) => CodeClimateSeverity::Major,
299 Some(EffectiveSeverity::Warn) => CodeClimateSeverity::Minor,
300 None => health_finding_severity(finding.severity),
301 }
302}
303
304const fn health_finding_severity(severity: FindingSeverity) -> CodeClimateSeverity {
305 match severity {
306 FindingSeverity::Critical => CodeClimateSeverity::Critical,
307 FindingSeverity::High => CodeClimateSeverity::Major,
308 FindingSeverity::Moderate => CodeClimateSeverity::Minor,
309 }
310}
311
312const fn styling_finding_severity(severity: StylingFindingSeverity) -> CodeClimateSeverity {
313 match severity {
314 StylingFindingSeverity::Error => CodeClimateSeverity::Major,
315 StylingFindingSeverity::Warn => CodeClimateSeverity::Minor,
316 }
317}
318
319const fn runtime_coverage_check_name(verdict: RuntimeCoverageVerdict) -> &'static str {
320 match verdict {
321 RuntimeCoverageVerdict::SafeToDelete => "fallow/runtime-safe-to-delete",
322 RuntimeCoverageVerdict::ReviewRequired => "fallow/runtime-review-required",
323 RuntimeCoverageVerdict::LowTraffic => "fallow/runtime-low-traffic",
324 RuntimeCoverageVerdict::CoverageUnavailable => "fallow/runtime-coverage-unavailable",
325 RuntimeCoverageVerdict::Active | RuntimeCoverageVerdict::Unknown => {
326 "fallow/runtime-coverage"
327 }
328 }
329}
330
331const fn runtime_coverage_severity(verdict: RuntimeCoverageVerdict) -> CodeClimateSeverity {
332 match verdict {
333 RuntimeCoverageVerdict::SafeToDelete => CodeClimateSeverity::Critical,
334 RuntimeCoverageVerdict::ReviewRequired => CodeClimateSeverity::Major,
335 _ => CodeClimateSeverity::Minor,
336 }
337}
338
339const fn coverage_intelligence_severity(
340 verdict: CoverageIntelligenceVerdict,
341) -> Option<CodeClimateSeverity> {
342 match verdict {
343 CoverageIntelligenceVerdict::RiskyChangeDetected
344 | CoverageIntelligenceVerdict::HighConfidenceDelete => Some(CodeClimateSeverity::Major),
345 CoverageIntelligenceVerdict::ReviewRequired
346 | CoverageIntelligenceVerdict::RefactorCarefully => Some(CodeClimateSeverity::Minor),
347 CoverageIntelligenceVerdict::Clean | CoverageIntelligenceVerdict::Unknown => None,
348 }
349}
350
351#[cfg(test)]
352mod tests {
353 use std::path::{Path, PathBuf};
354
355 use fallow_output::{
356 ComplexityViolation, ExceededThreshold, FindingSeverity, HealthReport, HealthSummary,
357 StylingFinding, StylingFindingSeverity,
358 };
359
360 use super::*;
361
362 #[test]
363 fn health_codeclimate_uses_relative_normalized_paths() {
364 let report = HealthReport {
365 summary: HealthSummary {
366 max_cyclomatic_threshold: 10,
367 max_cognitive_threshold: 8,
368 max_crap_threshold: 30.0,
369 ..HealthSummary::default()
370 },
371 findings: vec![
372 ComplexityViolation {
373 path: PathBuf::from("/root/app/[id]/page.tsx"),
374 name: "render".to_string(),
375 line: 7,
376 col: 0,
377 cyclomatic: 12,
378 cognitive: 9,
379 line_count: 20,
380 param_count: 1,
381 react_hook_count: 0,
382 react_jsx_max_depth: 0,
383 react_prop_count: 0,
384 react_hook_profile: None,
385 exceeded: ExceededThreshold::Both,
386 effective_severity: None,
387 severity: FindingSeverity::High,
388 coverage_pct: None,
389 crap: None,
390 coverage_tier: None,
391 coverage_source: None,
392 inherited_from: None,
393 component_rollup: None,
394 contributions: Vec::new(),
395 effective_thresholds: None,
396 threshold_source: None,
397 }
398 .into(),
399 ],
400 ..HealthReport::default()
401 };
402
403 let issues = build_health_codeclimate(&report, Path::new("/root"));
404
405 assert_eq!(issues.len(), 1);
406 let issue = &issues[0];
407 assert_eq!(issue.check_name, "fallow/high-complexity");
408 assert_eq!(issue.location.path, "app/%5Bid%5D/page.tsx");
409 assert_eq!(issue.location.lines.begin, 7);
410 assert_eq!(issue.severity, CodeClimateSeverity::Major);
411 }
412
413 #[test]
414 fn health_codeclimate_includes_styling_findings() {
415 let report = HealthReport {
416 styling_findings: vec![StylingFinding {
417 code: "css-selector-complexity".to_string(),
418 sub_kind: "high-specificity".to_string(),
419 path: "src/styles.css".to_string(),
420 line: 4,
421 value: "#app .card .title".to_string(),
422 effective_severity: StylingFindingSeverity::Error,
423 blast_radius: None,
424 confidence: None,
425 agent_disposition: None,
426 nearest_token: None,
427 fix_hint: None,
428 actions: Vec::new(),
429 }],
430 ..HealthReport::default()
431 };
432
433 let issues = build_health_codeclimate(&report, Path::new("/root"));
434
435 assert_eq!(issues.len(), 1);
436 let issue = &issues[0];
437 assert_eq!(issue.check_name, "fallow/css-selector-complexity");
438 assert_eq!(issue.location.path, "src/styles.css");
439 assert_eq!(issue.location.lines.begin, 4);
440 assert_eq!(issue.severity, CodeClimateSeverity::Major);
441 }
442
443 fn crap_violation(
444 effective_thresholds: Option<fallow_output::HealthEffectiveThresholds>,
445 ) -> ComplexityViolation {
446 ComplexityViolation {
447 path: PathBuf::from("/root/src/Board.astro"),
448 name: "<template>".to_string(),
449 line: 6,
450 col: 3,
451 cyclomatic: 11,
452 cognitive: 4,
453 line_count: 20,
454 param_count: 0,
455 react_hook_count: 0,
456 react_jsx_max_depth: 0,
457 react_prop_count: 0,
458 react_hook_profile: None,
459 exceeded: ExceededThreshold::Crap,
460 effective_severity: None,
461 severity: FindingSeverity::Critical,
462 coverage_pct: None,
463 crap: Some(132.0),
464 coverage_tier: None,
465 coverage_source: None,
466 inherited_from: None,
467 component_rollup: None,
468 contributions: Vec::new(),
469 threshold_source: effective_thresholds
470 .map(|_| fallow_output::ThresholdSource::Override),
471 effective_thresholds,
472 }
473 }
474
475 fn crap_report(
476 effective_thresholds: Option<fallow_output::HealthEffectiveThresholds>,
477 ) -> HealthReport {
478 HealthReport {
479 summary: HealthSummary {
480 max_crap_threshold: 30.0,
481 ..HealthSummary::default()
482 },
483 findings: vec![crap_violation(effective_thresholds).into()],
484 ..HealthReport::default()
485 }
486 }
487
488 #[test]
492 fn codeclimate_description_uses_the_override_ceiling_not_the_global_one() {
493 let report = crap_report(Some(fallow_output::HealthEffectiveThresholds {
494 max_cyclomatic: 20,
495 max_cognitive: 15,
496 max_crap: 100.0,
497 max_unit_size: 60,
498 }));
499
500 let issues = build_health_codeclimate(&report, Path::new("/root"));
501
502 assert_eq!(issues.len(), 1);
503 assert!(
504 issues[0].description.contains("threshold: 100.0"),
505 "{}",
506 issues[0].description
507 );
508 assert!(
509 !issues[0].description.contains("threshold: 30.0"),
510 "{}",
511 issues[0].description
512 );
513 }
514
515 #[test]
516 fn codeclimate_description_falls_back_to_the_global_ceiling() {
517 let issues = build_health_codeclimate(&crap_report(None), Path::new("/root"));
518
519 assert_eq!(issues.len(), 1);
520 assert!(
521 issues[0].description.contains("threshold: 30.0"),
522 "{}",
523 issues[0].description
524 );
525 }
526
527 #[test]
528 fn health_severities_reach_serialized_codeclimate_issues() {
529 for (severity, expected) in [
530 (FindingSeverity::Critical, "critical"),
531 (FindingSeverity::High, "major"),
532 (FindingSeverity::Moderate, "minor"),
533 ] {
534 let mut violation = crap_violation(None);
535 violation.severity = severity;
536 let report = HealthReport {
537 findings: vec![violation.into()],
538 ..HealthReport::default()
539 };
540 let issues = fallow_output::codeclimate_issues_to_value(&build_health_codeclimate(
541 &report,
542 Path::new("/root"),
543 ));
544 let issues = issues
545 .as_array()
546 .expect("CodeClimate output must be an array");
547 assert_eq!(issues.len(), 1);
548 assert_eq!(issues[0]["severity"], expected);
549 assert_eq!(issues[0]["check_name"], "fallow/high-crap-score");
550 }
551 }
552
553 #[test]
554 fn runtime_verdicts_reach_serialized_codeclimate_issues() {
555 use fallow_output::{
556 RuntimeCoverageConfidence, RuntimeCoverageEvidence, RuntimeCoverageReport,
557 };
558
559 for (verdict, check_name, severity) in [
560 (
561 RuntimeCoverageVerdict::SafeToDelete,
562 "fallow/runtime-safe-to-delete",
563 "critical",
564 ),
565 (
566 RuntimeCoverageVerdict::ReviewRequired,
567 "fallow/runtime-review-required",
568 "major",
569 ),
570 (
571 RuntimeCoverageVerdict::LowTraffic,
572 "fallow/runtime-low-traffic",
573 "minor",
574 ),
575 (
576 RuntimeCoverageVerdict::CoverageUnavailable,
577 "fallow/runtime-coverage-unavailable",
578 "minor",
579 ),
580 (
581 RuntimeCoverageVerdict::Active,
582 "fallow/runtime-coverage",
583 "minor",
584 ),
585 (
586 RuntimeCoverageVerdict::Unknown,
587 "fallow/runtime-coverage",
588 "minor",
589 ),
590 ] {
591 let report = HealthReport {
592 runtime_coverage: Some(RuntimeCoverageReport {
593 findings: vec![RuntimeCoverageFinding {
594 id: "runtime-fixture".to_string(),
595 stable_id: None,
596 source_hash: None,
597 path: PathBuf::from("/root/src/legacy.ts"),
598 function: "legacyHelper".to_string(),
599 line: 12,
600 verdict,
601 invocations: Some(0),
602 confidence: RuntimeCoverageConfidence::High,
603 evidence: RuntimeCoverageEvidence {
604 static_status: "unused".to_string(),
605 test_coverage: "not_covered".to_string(),
606 test_only_reference: None,
607 v8_tracking: "tracked".to_string(),
608 untracked_reason: None,
609 observation_days: 30,
610 deployments_observed: 3,
611 },
612 actions: Vec::new(),
613 discriminators: None,
614 }],
615 ..RuntimeCoverageReport::default()
616 }),
617 ..HealthReport::default()
618 };
619 let issues = fallow_output::codeclimate_issues_to_value(&build_health_codeclimate(
620 &report,
621 Path::new("/root"),
622 ));
623 let issues = issues
624 .as_array()
625 .expect("CodeClimate output must be an array");
626 assert_eq!(issues.len(), 1, "{verdict:?}");
627 assert_eq!(issues[0]["check_name"], check_name);
628 assert_eq!(issues[0]["severity"], severity);
629 assert_eq!(issues[0]["location"]["path"], "src/legacy.ts");
630 assert_eq!(issues[0]["location"]["lines"]["begin"], 12);
631 }
632 }
633
634 #[test]
635 fn coverage_intelligence_verdicts_and_recommendations_reach_serialized_issues() {
636 use fallow_output::{
637 CoverageIntelligenceConfidence, CoverageIntelligenceEvidence,
638 CoverageIntelligenceReport,
639 };
640
641 let recommendations = [
642 (
643 CoverageIntelligenceRecommendation::AddTestOrSplitBeforeMerge,
644 "fallow/coverage-intelligence-risky-change",
645 ),
646 (
647 CoverageIntelligenceRecommendation::DeleteAfterConfirmingOwner,
648 "fallow/coverage-intelligence-delete",
649 ),
650 (
651 CoverageIntelligenceRecommendation::ReviewBeforeChanging,
652 "fallow/coverage-intelligence-review",
653 ),
654 (
655 CoverageIntelligenceRecommendation::RefactorCarefullyKeepBehavior,
656 "fallow/coverage-intelligence-refactor",
657 ),
658 ];
659 for (verdict, severity) in [
660 (
661 CoverageIntelligenceVerdict::RiskyChangeDetected,
662 Some("major"),
663 ),
664 (
665 CoverageIntelligenceVerdict::HighConfidenceDelete,
666 Some("major"),
667 ),
668 (CoverageIntelligenceVerdict::ReviewRequired, Some("minor")),
669 (
670 CoverageIntelligenceVerdict::RefactorCarefully,
671 Some("minor"),
672 ),
673 (CoverageIntelligenceVerdict::Clean, None),
674 (CoverageIntelligenceVerdict::Unknown, None),
675 ] {
676 for (recommendation, check_name) in recommendations {
677 let report = HealthReport {
678 coverage_intelligence: Some(CoverageIntelligenceReport {
679 schema_version: fallow_output::CoverageIntelligenceSchemaVersion::default(),
680 verdict,
681 summary: fallow_output::CoverageIntelligenceSummary::default(),
682 findings: vec![CoverageIntelligenceFinding {
683 id: "coverage-fixture".to_string(),
684 path: PathBuf::from("/root/src/legacy.ts"),
685 identity: Some("legacyHelper".to_string()),
686 line: 12,
687 verdict,
688 signals: Vec::new(),
689 recommendation,
690 confidence: CoverageIntelligenceConfidence::High,
691 related_ids: Vec::new(),
692 evidence: CoverageIntelligenceEvidence::default(),
693 actions: Vec::new(),
694 }],
695 }),
696 ..HealthReport::default()
697 };
698 let issues = fallow_output::codeclimate_issues_to_value(&build_health_codeclimate(
699 &report,
700 Path::new("/root"),
701 ));
702 let issues = issues
703 .as_array()
704 .expect("CodeClimate output must be an array");
705 if let Some(severity) = severity {
706 assert_eq!(issues.len(), 1, "{verdict:?}");
707 assert_eq!(issues[0]["check_name"], check_name);
708 assert_eq!(issues[0]["severity"], severity);
709 assert_eq!(issues[0]["location"]["path"], "src/legacy.ts");
710 } else {
711 assert!(issues.is_empty(), "{verdict:?}");
712 }
713 }
714 }
715 }
716}