1use std::path::Path;
4
5use crate::{
6 SarifDocumentInput, SarifFindingFields as SarifFields,
7 SarifSourceSnippetCache as SourceSnippetCache, append_sarif_findings as push_sarif_results,
8 build_sarif_document, build_sarif_result_with_snippet as sarif_result_with_snippet,
9 issue_output_contracts, normalize_uri,
10};
11use fallow_config::{RulesConfig, Severity};
12use fallow_types::{
13 issue_meta::issue_sarif_rule_description,
14 output_dead_code::*,
15 results::{
16 AnalysisResults, BoundaryCallViolation, BoundaryCoverageViolation, BoundaryViolation,
17 CircularDependency, DevDependencyInProduction, DuplicatePropShape,
18 DynamicSegmentNameConflict, InvalidClientExport, MisplacedDirective,
19 MixedClientServerBarrel, PolicyViolation, PolicyViolationSeverity, PrivateTypeLeak,
20 PropDrillingChain, RouteCollision, StaleSuppression, TestOnlyDependency, ThinWrapper,
21 TypeOnlyDependency, UnprovidedInject, UnrenderedComponent, UnresolvedImport,
22 UnusedComponentEmit, UnusedComponentInput, UnusedComponentOutput, UnusedComponentProp,
23 UnusedDependency, UnusedExport, UnusedFile, UnusedMember, UnusedServerAction,
24 UnusedSvelteEvent,
25 },
26};
27
28fn relative_uri(path: &Path, root: &Path) -> String {
29 normalize_uri(
30 &path
31 .strip_prefix(root)
32 .unwrap_or(path)
33 .display()
34 .to_string(),
35 )
36}
37
38#[derive(Clone, Copy)]
42struct SarifCtx<'a> {
43 results: &'a AnalysisResults,
44 root: &'a Path,
45 rules: &'a RulesConfig,
46}
47
48fn severity_to_sarif_level(s: Severity) -> &'static str {
49 match s {
50 Severity::Error => "error",
51 Severity::Warn => "warning",
52 Severity::Off => unreachable!(),
53 }
54}
55
56fn configured_sarif_level(s: Severity) -> &'static str {
57 match s {
58 Severity::Error | Severity::Warn => severity_to_sarif_level(s),
59 Severity::Off => "none",
60 }
61}
62
63fn sarif_export_fields(
65 export: &UnusedExport,
66 root: &Path,
67 rule_id: &'static str,
68 level: &'static str,
69 kind: &str,
70 re_kind: &str,
71) -> SarifFields {
72 let label = if export.is_re_export { re_kind } else { kind };
73 SarifFields {
74 rule_id,
75 level,
76 message: format!(
77 "{} '{}' is never imported by other modules",
78 label, export.export_name
79 ),
80 uri: relative_uri(&export.path, root),
81 region: Some((export.line, export.col + 1)),
82 source_path: Some(export.path.clone()),
83 properties: if export.is_re_export {
84 Some(serde_json::json!({ "is_re_export": true }))
85 } else {
86 None
87 },
88 }
89}
90
91fn sarif_private_type_leak_fields(
92 leak: &PrivateTypeLeak,
93 root: &Path,
94 level: &'static str,
95) -> SarifFields {
96 SarifFields {
97 rule_id: "fallow/private-type-leak",
98 level,
99 message: format!(
100 "Export '{}' references private type '{}'",
101 leak.export_name, leak.type_name
102 ),
103 uri: relative_uri(&leak.path, root),
104 region: Some((leak.line, leak.col + 1)),
105 source_path: Some(leak.path.clone()),
106 properties: None,
107 }
108}
109
110fn sarif_dep_fields(
112 dep: &UnusedDependency,
113 root: &Path,
114 rule_id: &'static str,
115 level: &'static str,
116 section: &str,
117) -> SarifFields {
118 let workspace_context = if dep.used_in_workspaces.is_empty() {
119 String::new()
120 } else {
121 let workspaces = dep
122 .used_in_workspaces
123 .iter()
124 .map(|path| relative_uri(path, root))
125 .collect::<Vec<_>>()
126 .join(", ");
127 format!("; imported in other workspaces: {workspaces}")
128 };
129 SarifFields {
130 rule_id,
131 level,
132 message: format!(
133 "Package '{}' is in {} but never imported{}",
134 dep.package_name, section, workspace_context
135 ),
136 uri: relative_uri(&dep.path, root),
137 region: if dep.line > 0 {
138 Some((dep.line, 1))
139 } else {
140 None
141 },
142 source_path: (dep.line > 0).then(|| dep.path.clone()),
143 properties: None,
144 }
145}
146
147fn sarif_member_fields(
149 member: &UnusedMember,
150 root: &Path,
151 rule_id: &'static str,
152 level: &'static str,
153 kind: &str,
154) -> SarifFields {
155 SarifFields {
156 rule_id,
157 level,
158 message: format!(
159 "{} member '{}.{}' is never referenced",
160 kind, member.parent_name, member.member_name
161 ),
162 uri: relative_uri(&member.path, root),
163 region: Some((member.line, member.col + 1)),
164 source_path: Some(member.path.clone()),
165 properties: None,
166 }
167}
168
169fn sarif_unused_file_fields(file: &UnusedFile, root: &Path, level: &'static str) -> SarifFields {
170 SarifFields {
171 rule_id: "fallow/unused-file",
172 level,
173 message: "File is not reachable from any entry point".to_string(),
174 uri: relative_uri(&file.path, root),
175 region: None,
176 source_path: None,
177 properties: None,
178 }
179}
180
181fn sarif_type_only_dep_fields(
182 dep: &TypeOnlyDependency,
183 root: &Path,
184 level: &'static str,
185) -> SarifFields {
186 SarifFields {
187 rule_id: "fallow/type-only-dependency",
188 level,
189 message: format!(
190 "Package '{}' is only imported via type-only imports (consider moving to devDependencies)",
191 dep.package_name
192 ),
193 uri: relative_uri(&dep.path, root),
194 region: if dep.line > 0 {
195 Some((dep.line, 1))
196 } else {
197 None
198 },
199 source_path: (dep.line > 0).then(|| dep.path.clone()),
200 properties: None,
201 }
202}
203
204fn sarif_test_only_dep_fields(
205 dep: &TestOnlyDependency,
206 root: &Path,
207 level: &'static str,
208) -> SarifFields {
209 SarifFields {
210 rule_id: "fallow/test-only-dependency",
211 level,
212 message: format!(
213 "Package '{}' is only imported by test files (consider moving to devDependencies)",
214 dep.package_name
215 ),
216 uri: relative_uri(&dep.path, root),
217 region: if dep.line > 0 {
218 Some((dep.line, 1))
219 } else {
220 None
221 },
222 source_path: (dep.line > 0).then(|| dep.path.clone()),
223 properties: None,
224 }
225}
226
227fn sarif_dev_dep_in_prod_fields(
228 dep: &DevDependencyInProduction,
229 root: &Path,
230 level: &'static str,
231) -> SarifFields {
232 SarifFields {
233 rule_id: "fallow/dev-dependency-in-production",
234 level,
235 message: format!(
236 "devDependency '{}' is imported by production code at runtime (consider moving to dependencies)",
237 dep.package_name
238 ),
239 uri: relative_uri(&dep.path, root),
240 region: if dep.line > 0 {
241 Some((dep.line, 1))
242 } else {
243 None
244 },
245 source_path: (dep.line > 0).then(|| dep.path.clone()),
246 properties: None,
247 }
248}
249
250fn sarif_unresolved_import_fields(
251 import: &UnresolvedImport,
252 root: &Path,
253 level: &'static str,
254) -> SarifFields {
255 SarifFields {
256 rule_id: "fallow/unresolved-import",
257 level,
258 message: format!("Import '{}' could not be resolved", import.specifier),
259 uri: relative_uri(&import.path, root),
260 region: Some((import.line, import.col + 1)),
261 source_path: Some(import.path.clone()),
262 properties: None,
263 }
264}
265
266fn sarif_circular_dep_fields(
267 cycle: &CircularDependency,
268 root: &Path,
269 level: &'static str,
270) -> SarifFields {
271 let chain: Vec<String> = cycle.files.iter().map(|p| relative_uri(p, root)).collect();
272 let mut display_chain = chain.clone();
273 if let Some(first) = chain.first() {
274 display_chain.push(first.clone());
275 }
276 let first_uri = chain.first().map_or_else(String::new, Clone::clone);
277 let first_path = cycle.files.first().cloned();
278 SarifFields {
279 rule_id: "fallow/circular-dependency",
280 level,
281 message: format!(
282 "Circular dependency{}: {}",
283 if cycle.is_cross_package {
284 " (cross-package)"
285 } else {
286 ""
287 },
288 display_chain.join(" \u{2192} ")
289 ),
290 uri: first_uri,
291 region: if cycle.line > 0 {
292 Some((cycle.line, cycle.col + 1))
293 } else {
294 None
295 },
296 source_path: (cycle.line > 0).then_some(first_path).flatten(),
297 properties: None,
298 }
299}
300
301fn sarif_re_export_cycle_fields(
302 cycle: &fallow_types::results::ReExportCycle,
303 root: &Path,
304 level: &'static str,
305) -> SarifFields {
306 let chain: Vec<String> = cycle.files.iter().map(|p| relative_uri(p, root)).collect();
307 let first_uri = chain.first().map_or_else(String::new, Clone::clone);
308 let first_path = cycle.files.first().cloned();
309 let kind_tag = match cycle.kind {
310 fallow_types::results::ReExportCycleKind::SelfLoop => " (self-loop)",
311 fallow_types::results::ReExportCycleKind::MultiNode => "",
312 };
313 SarifFields {
314 rule_id: "fallow/re-export-cycle",
315 level,
316 message: format!("Re-export cycle{}: {}", kind_tag, chain.join(" <-> ")),
317 uri: first_uri,
318 region: None,
319 source_path: first_path,
320 properties: None,
321 }
322}
323
324fn sarif_boundary_violation_fields(
325 violation: &BoundaryViolation,
326 root: &Path,
327 level: &'static str,
328) -> SarifFields {
329 let from_uri = relative_uri(&violation.from_path, root);
330 let to_uri = relative_uri(&violation.to_path, root);
331 SarifFields {
332 rule_id: "fallow/boundary-violation",
333 level,
334 message: format!(
335 "Import from zone '{}' to zone '{}' is not allowed ({})",
336 violation.from_zone, violation.to_zone, to_uri,
337 ),
338 uri: from_uri,
339 region: if violation.line > 0 {
340 Some((violation.line, violation.col + 1))
341 } else {
342 None
343 },
344 source_path: (violation.line > 0).then(|| violation.from_path.clone()),
345 properties: None,
346 }
347}
348
349fn sarif_boundary_coverage_fields(
350 violation: &BoundaryCoverageViolation,
351 root: &Path,
352 level: &'static str,
353) -> SarifFields {
354 SarifFields {
355 rule_id: "fallow/boundary-coverage",
356 level,
357 message: "File does not match any configured architecture boundary zone".to_string(),
358 uri: relative_uri(&violation.path, root),
359 region: Some((violation.line, violation.col + 1)),
360 source_path: Some(violation.path.clone()),
361 properties: None,
362 }
363}
364
365fn sarif_boundary_call_fields(
366 violation: &BoundaryCallViolation,
367 root: &Path,
368 level: &'static str,
369) -> SarifFields {
370 SarifFields {
371 rule_id: "fallow/boundary-call-violation",
372 level,
373 message: format!(
374 "Call to `{}` matches forbidden pattern `{}` in zone '{}'",
375 violation.callee, violation.pattern, violation.zone
376 ),
377 uri: relative_uri(&violation.path, root),
378 region: Some((violation.line, violation.col + 1)),
379 source_path: Some(violation.path.clone()),
380 properties: None,
381 }
382}
383
384fn sarif_policy_violation_fields(violation: &PolicyViolation, root: &Path) -> SarifFields {
385 let level = match violation.severity {
386 PolicyViolationSeverity::Error => "error",
387 PolicyViolationSeverity::Warn => "warning",
388 };
389 let message = match &violation.message {
390 Some(message) => format!(
391 "Policy violation `{}/{}`: `{}` is banned. {message}",
392 violation.pack, violation.rule_id, violation.matched
393 ),
394 None => format!(
395 "Policy violation `{}/{}`: `{}` is banned",
396 violation.pack, violation.rule_id, violation.matched
397 ),
398 };
399 SarifFields {
400 rule_id: "fallow/policy-violation",
401 level,
402 message,
403 uri: relative_uri(&violation.path, root),
404 region: Some((violation.line, violation.col + 1)),
405 source_path: Some(violation.path.clone()),
406 properties: Some(serde_json::json!({
412 "policyRule": format!("{}/{}", violation.pack, violation.rule_id),
413 })),
414 }
415}
416
417fn sarif_invalid_client_export_fields(
418 export: &InvalidClientExport,
419 root: &Path,
420 level: &'static str,
421) -> SarifFields {
422 SarifFields {
423 rule_id: "fallow/invalid-client-export",
424 level,
425 message: format!(
426 "Export '{}' is not allowed in a \"{}\" file (Next.js server-only / route-config name)",
427 export.export_name, export.directive
428 ),
429 uri: relative_uri(&export.path, root),
430 region: Some((export.line, export.col + 1)),
431 source_path: Some(export.path.clone()),
432 properties: None,
433 }
434}
435
436fn sarif_mixed_client_server_barrel_fields(
437 barrel: &MixedClientServerBarrel,
438 root: &Path,
439 level: &'static str,
440) -> SarifFields {
441 SarifFields {
442 rule_id: "fallow/mixed-client-server-barrel",
443 level,
444 message: format!(
445 "Barrel re-exports both a \"use client\" module ('{}') and a server-only module ('{}'); one import drags the other's directive across the boundary",
446 barrel.client_origin, barrel.server_origin
447 ),
448 uri: relative_uri(&barrel.path, root),
449 region: Some((barrel.line, barrel.col + 1)),
450 source_path: Some(barrel.path.clone()),
451 properties: None,
452 }
453}
454
455fn sarif_misplaced_directive_fields(
456 directive_site: &MisplacedDirective,
457 root: &Path,
458 level: &'static str,
459) -> SarifFields {
460 SarifFields {
461 rule_id: "fallow/misplaced-directive",
462 level,
463 message: format!(
464 "Directive \"{}\" is not in the leading position, so the RSC bundler ignores it; move it to the top of the file",
465 directive_site.directive
466 ),
467 uri: relative_uri(&directive_site.path, root),
468 region: Some((directive_site.line, directive_site.col + 1)),
469 source_path: Some(directive_site.path.clone()),
470 properties: None,
471 }
472}
473
474fn sarif_unprovided_inject_fields(
475 inject: &UnprovidedInject,
476 root: &Path,
477 level: &'static str,
478) -> SarifFields {
479 SarifFields {
480 rule_id: "fallow/unprovided-inject",
481 level,
482 message: format!(
483 "inject(\"{}\") has no matching provide(\"{}\") in this project; at runtime it returns undefined; provide the key or remove this inject",
484 inject.key_name, inject.key_name
485 ),
486 uri: relative_uri(&inject.path, root),
487 region: Some((inject.line, inject.col + 1)),
488 source_path: Some(inject.path.clone()),
489 properties: None,
490 }
491}
492
493fn sarif_unrendered_component_fields(
494 component: &UnrenderedComponent,
495 root: &Path,
496 level: &'static str,
497) -> SarifFields {
498 SarifFields {
499 rule_id: "fallow/unrendered-component",
500 level,
501 message: format!(
502 "component \"{}\" is reachable but rendered nowhere in this project; render it somewhere or remove it",
503 component.component_name
504 ),
505 uri: relative_uri(&component.path, root),
506 region: Some((component.line, component.col + 1)),
507 source_path: Some(component.path.clone()),
508 properties: None,
509 }
510}
511
512fn sarif_unused_component_prop_fields(
513 prop: &UnusedComponentProp,
514 root: &Path,
515 level: &'static str,
516) -> SarifFields {
517 SarifFields {
518 rule_id: "fallow/unused-component-prop",
519 level,
520 message: format!(
521 "prop \"{}\" is declared but referenced nowhere inside component \"{}\"; remove it or use it",
522 prop.prop_name, prop.component_name
523 ),
524 uri: relative_uri(&prop.path, root),
525 region: Some((prop.line, prop.col + 1)),
526 source_path: Some(prop.path.clone()),
527 properties: None,
528 }
529}
530
531fn sarif_unused_component_emit_fields(
532 emit: &UnusedComponentEmit,
533 root: &Path,
534 level: &'static str,
535) -> SarifFields {
536 SarifFields {
537 rule_id: "fallow/unused-component-emit",
538 level,
539 message: format!(
540 "emit \"{}\" is declared but emitted nowhere inside component \"{}\"; remove it or emit it",
541 emit.emit_name, emit.component_name
542 ),
543 uri: relative_uri(&emit.path, root),
544 region: Some((emit.line, emit.col + 1)),
545 source_path: Some(emit.path.clone()),
546 properties: None,
547 }
548}
549
550fn sarif_unused_svelte_event_fields(
551 event: &UnusedSvelteEvent,
552 root: &Path,
553 level: &'static str,
554) -> SarifFields {
555 SarifFields {
556 rule_id: "fallow/unused-svelte-event",
557 level,
558 message: format!(
559 "event \"{}\" is dispatched by component \"{}\" but listened to nowhere in the project; remove it or listen for it",
560 event.event_name, event.component_name
561 ),
562 uri: relative_uri(&event.path, root),
563 region: Some((event.line, event.col + 1)),
564 source_path: Some(event.path.clone()),
565 properties: None,
566 }
567}
568
569fn sarif_unused_component_input_fields(
570 input: &UnusedComponentInput,
571 root: &Path,
572 level: &'static str,
573) -> SarifFields {
574 SarifFields {
575 rule_id: "fallow/unused-component-input",
576 level,
577 message: format!(
578 "input \"{}\" is declared but read nowhere inside component \"{}\"; remove it or use it",
579 input.input_name, input.component_name
580 ),
581 uri: relative_uri(&input.path, root),
582 region: Some((input.line, input.col + 1)),
583 source_path: Some(input.path.clone()),
584 properties: None,
585 }
586}
587
588fn sarif_unused_component_output_fields(
589 output: &UnusedComponentOutput,
590 root: &Path,
591 level: &'static str,
592) -> SarifFields {
593 SarifFields {
594 rule_id: "fallow/unused-component-output",
595 level,
596 message: format!(
597 "output \"{}\" is declared but emitted nowhere inside component \"{}\"; remove it or emit it",
598 output.output_name, output.component_name
599 ),
600 uri: relative_uri(&output.path, root),
601 region: Some((output.line, output.col + 1)),
602 source_path: Some(output.path.clone()),
603 properties: None,
604 }
605}
606
607fn sarif_unused_server_action_fields(
608 action: &UnusedServerAction,
609 root: &Path,
610 level: &'static str,
611) -> SarifFields {
612 SarifFields {
613 rule_id: "fallow/unused-server-action",
614 level,
615 message: format!(
616 "server action \"{}\" is exported from a \"use server\" file but no code in this project references it; wire it to a consumer or remove it",
617 action.action_name
618 ),
619 uri: relative_uri(&action.path, root),
620 region: Some((action.line, action.col + 1)),
621 source_path: Some(action.path.clone()),
622 properties: None,
623 }
624}
625
626fn sarif_unused_load_data_key_fields(
627 key: &fallow_types::results::UnusedLoadDataKey,
628 root: &Path,
629 level: &'static str,
630) -> SarifFields {
631 SarifFields {
632 rule_id: "fallow/unused-load-data-key",
633 level,
634 message: format!(
635 "load() return key \"{}\" is read by no consumer (sibling +page.svelte data.<key> or project-wide page.data.<key>); delete the key or wire a consumer",
636 key.key_name
637 ),
638 uri: relative_uri(&key.path, root),
639 region: Some((key.line, key.col + 1)),
640 source_path: Some(key.path.clone()),
641 properties: None,
642 }
643}
644
645fn sarif_prop_drilling_fields(
646 chain: &PropDrillingChain,
647 root: &Path,
648 level: &'static str,
649) -> SarifFields {
650 let source = chain.hops.first();
653 let consumer = chain.hops.last();
654 let (path, line) = source.map_or((std::path::PathBuf::new(), 1), |h| (h.file.clone(), h.line));
655 let consumer_name = consumer.map_or("a distant component", |h| h.component.as_str());
656 SarifFields {
657 rule_id: "fallow/prop-drilling",
658 level,
659 message: format!(
660 "prop \"{}\" is forwarded unchanged through {} component(s) before \"{}\" consumes it; colocate, lift to context, or compose",
661 chain.prop, chain.depth, consumer_name
662 ),
663 uri: relative_uri(&path, root),
664 region: Some((line, 1)),
665 source_path: Some(path),
666 properties: None,
667 }
668}
669
670fn sarif_thin_wrapper_fields(
671 wrapper: &ThinWrapper,
672 root: &Path,
673 level: &'static str,
674) -> SarifFields {
675 SarifFields {
676 rule_id: "fallow/thin-wrapper",
677 level,
678 message: format!(
679 "\"{}\" is a thin wrapper: its whole body forwards props to \"{}\"; inline it at call sites or delete it",
680 wrapper.component, wrapper.child_component
681 ),
682 uri: relative_uri(&wrapper.file, root),
683 region: Some((wrapper.line, 1)),
684 source_path: Some(wrapper.file.clone()),
685 properties: None,
686 }
687}
688
689fn sarif_duplicate_prop_shape_fields(
690 shape: &DuplicatePropShape,
691 root: &Path,
692 level: &'static str,
693) -> SarifFields {
694 SarifFields {
695 rule_id: "fallow/duplicate-prop-shape",
696 level,
697 message: format!(
698 "\"{}\" shares an identical prop shape {{{}}} with {} other component(s); extract a shared Props type or base component",
699 shape.component,
700 shape.shape.join(", "),
701 shape.group_size.saturating_sub(1)
702 ),
703 uri: relative_uri(&shape.file, root),
704 region: Some((shape.line, 1)),
705 source_path: Some(shape.file.clone()),
706 properties: None,
707 }
708}
709
710fn sarif_route_collision_fields(
711 collision: &RouteCollision,
712 root: &Path,
713 level: &'static str,
714) -> SarifFields {
715 SarifFields {
716 rule_id: "fallow/route-collision",
717 level,
718 message: format!(
719 "Route file resolves to '{}', which is also owned by {} other file(s); Next.js fails the build because a URL can have only one owner",
720 collision.url,
721 collision.conflicting_paths.len()
722 ),
723 uri: relative_uri(&collision.path, root),
724 region: Some((collision.line, collision.col + 1)),
725 source_path: Some(collision.path.clone()),
726 properties: None,
727 }
728}
729
730fn sarif_dynamic_segment_name_conflict_fields(
731 conflict: &DynamicSegmentNameConflict,
732 root: &Path,
733 level: &'static str,
734) -> SarifFields {
735 SarifFields {
736 rule_id: "fallow/dynamic-segment-name-conflict",
737 level,
738 message: format!(
739 "Dynamic segments at '{}' use different slug names ({}); Next.js requires one consistent name per dynamic path",
740 conflict.position,
741 conflict.conflicting_segments.join(", ")
742 ),
743 uri: relative_uri(&conflict.path, root),
744 region: Some((conflict.line, conflict.col + 1)),
745 source_path: Some(conflict.path.clone()),
746 properties: None,
747 }
748}
749
750fn sarif_stale_suppression_fields(
751 suppression: &StaleSuppression,
752 root: &Path,
753 level: &'static str,
754) -> SarifFields {
755 SarifFields {
756 rule_id: if suppression.missing_reason {
757 "fallow/missing-suppression-reason"
758 } else {
759 "fallow/stale-suppression"
760 },
761 level,
762 message: suppression.display_message(),
763 uri: relative_uri(&suppression.path, root),
764 region: Some((suppression.line, suppression.col + 1)),
765 source_path: Some(suppression.path.clone()),
766 properties: None,
767 }
768}
769
770fn stale_suppression_severity(suppression: &StaleSuppression, rules: &RulesConfig) -> Severity {
771 if suppression.missing_reason {
772 rules.require_suppression_reason
773 } else {
774 rules.stale_suppressions
775 }
776}
777
778fn sarif_unused_catalog_entry_fields(
779 entry: &UnusedCatalogEntryFinding,
780 root: &Path,
781 level: &'static str,
782) -> SarifFields {
783 let entry = &entry.entry;
784 let message = if entry.catalog_name == "default" {
785 format!(
786 "Catalog entry '{}' is not referenced by any workspace package",
787 entry.entry_name
788 )
789 } else {
790 format!(
791 "Catalog entry '{}' (catalog '{}') is not referenced by any workspace package",
792 entry.entry_name, entry.catalog_name
793 )
794 };
795 SarifFields {
796 rule_id: "fallow/unused-catalog-entry",
797 level,
798 message,
799 uri: relative_uri(&entry.path, root),
800 region: Some((entry.line, 1)),
801 source_path: Some(entry.path.clone()),
802 properties: None,
803 }
804}
805
806fn sarif_unused_dependency_override_fields(
807 finding: &UnusedDependencyOverrideFinding,
808 root: &Path,
809 level: &'static str,
810) -> SarifFields {
811 let finding = &finding.entry;
812 let mut message = format!(
813 "Override `{}` forces version `{}` but `{}` is not declared by any workspace package or resolved in the lockfile",
814 finding.raw_key, finding.version_range, finding.target_package,
815 );
816 if let Some(hint) = &finding.hint {
817 use std::fmt::Write as _;
818 let _ = write!(message, " ({hint})");
819 }
820 SarifFields {
821 rule_id: "fallow/unused-dependency-override",
822 level,
823 message,
824 uri: relative_uri(&finding.path, root),
825 region: Some((finding.line, 1)),
826 source_path: Some(finding.path.clone()),
827 properties: None,
828 }
829}
830
831fn sarif_misconfigured_dependency_override_fields(
832 finding: &MisconfiguredDependencyOverrideFinding,
833 root: &Path,
834 level: &'static str,
835) -> SarifFields {
836 let finding = &finding.entry;
837 let message = format!(
838 "Override `{}` -> `{}` is malformed: {}",
839 finding.raw_key,
840 finding.raw_value,
841 finding.reason.describe(),
842 );
843 SarifFields {
844 rule_id: "fallow/misconfigured-dependency-override",
845 level,
846 message,
847 uri: relative_uri(&finding.path, root),
848 region: Some((finding.line, 1)),
849 source_path: Some(finding.path.clone()),
850 properties: None,
851 }
852}
853
854fn sarif_unresolved_catalog_reference_fields(
855 finding: &UnresolvedCatalogReferenceFinding,
856 root: &Path,
857 level: &'static str,
858) -> SarifFields {
859 let finding = &finding.reference;
860 let catalog_phrase = if finding.catalog_name == "default" {
861 "the default catalog".to_string()
862 } else {
863 format!("catalog '{}'", finding.catalog_name)
864 };
865 let mut message = format!(
866 "Package '{}' is referenced via `catalog:{}` but {} does not declare it",
867 finding.entry_name,
868 if finding.catalog_name == "default" {
869 ""
870 } else {
871 finding.catalog_name.as_str()
872 },
873 catalog_phrase,
874 );
875 if !finding.available_in_catalogs.is_empty() {
876 use std::fmt::Write as _;
877 let _ = write!(
878 message,
879 " (available in: {})",
880 finding.available_in_catalogs.join(", ")
881 );
882 }
883 SarifFields {
884 rule_id: "fallow/unresolved-catalog-reference",
885 level,
886 message,
887 uri: relative_uri(&finding.path, root),
888 region: Some((finding.line, 1)),
889 source_path: Some(finding.path.clone()),
890 properties: None,
891 }
892}
893
894fn sarif_empty_catalog_group_fields(
895 group: &EmptyCatalogGroupFinding,
896 root: &Path,
897 level: &'static str,
898) -> SarifFields {
899 let group = &group.group;
900 SarifFields {
901 rule_id: "fallow/empty-catalog-group",
902 level,
903 message: format!("Catalog group '{}' has no entries", group.catalog_name),
904 uri: relative_uri(&group.path, root),
905 region: Some((group.line, 1)),
906 source_path: Some(group.path.clone()),
907 properties: None,
908 }
909}
910
911fn push_sarif_unlisted_deps(
914 sarif_results: &mut Vec<serde_json::Value>,
915 deps: &[UnlistedDependencyFinding],
916 root: &Path,
917 level: &'static str,
918 snippets: &mut SourceSnippetCache,
919) {
920 for entry in deps {
921 let dep = &entry.dep;
922 for site in &dep.imported_from {
923 let uri = relative_uri(&site.path, root);
924 let source_snippet = snippets.line(&site.path, site.line);
925 sarif_results.push(sarif_result_with_snippet(
926 "fallow/unlisted-dependency",
927 level,
928 &format!(
929 "Package '{}' is imported but not listed in package.json",
930 dep.package_name
931 ),
932 &uri,
933 Some((site.line, site.col + 1)),
934 source_snippet.as_deref(),
935 ));
936 }
937 }
938}
939
940fn push_sarif_duplicate_exports(
943 sarif_results: &mut Vec<serde_json::Value>,
944 dups: &[DuplicateExportFinding],
945 root: &Path,
946 level: &'static str,
947 snippets: &mut SourceSnippetCache,
948) {
949 for dup in dups {
950 let dup = &dup.export;
951 for loc in &dup.locations {
952 let uri = relative_uri(&loc.path, root);
953 let source_snippet = snippets.line(&loc.path, loc.line);
954 sarif_results.push(sarif_result_with_snippet(
955 "fallow/duplicate-export",
956 level,
957 &format!("Export '{}' appears in multiple modules", dup.export_name),
958 &uri,
959 Some((loc.line, loc.col + 1)),
960 source_snippet.as_deref(),
961 ));
962 }
963 }
964}
965
966fn build_sarif_rules(
968 rules: &RulesConfig,
969 rule_builder: &dyn Fn(&str, &str, &str) -> serde_json::Value,
970) -> Vec<serde_json::Value> {
971 let mut sarif_rules = Vec::new();
972 for contract in issue_output_contracts() {
973 for rule_id in contract.sarif_rule_ids {
974 let severity = sarif_rule_severity(rules, contract.code, &rule_id);
975 let description = issue_sarif_rule_description(&rule_id).unwrap_or_else(|| {
976 panic!("dead-code SARIF rule {rule_id} is missing issue metadata")
977 });
978 sarif_rules.push(rule_builder(
979 &rule_id,
980 description,
981 configured_sarif_level(severity),
982 ));
983 }
984 }
985 sarif_rules
986}
987
988fn sarif_rule_severity(rules: &RulesConfig, issue_code: &str, rule_id: &str) -> Severity {
989 if rule_id == "fallow/missing-suppression-reason" {
990 return rules.require_suppression_reason;
991 }
992 dead_code_rule_severity(rules, issue_code)
993 .unwrap_or_else(|| panic!("dead-code SARIF rule {rule_id} has no severity mapping"))
994}
995
996fn dead_code_rule_severity(rules: &RulesConfig, issue_code: &str) -> Option<Severity> {
997 let severity = match issue_code {
998 "unused-file" => rules.unused_files,
999 "unused-export" => rules.unused_exports,
1000 "unused-type" => rules.unused_types,
1001 "private-type-leak" => rules.private_type_leaks,
1002 "unused-dependency" => rules.unused_dependencies,
1003 "unused-dev-dependency" => rules.unused_dev_dependencies,
1004 "unused-optional-dependency" => rules.unused_optional_dependencies,
1005 "type-only-dependency" => rules.type_only_dependencies,
1006 "test-only-dependency" => rules.test_only_dependencies,
1007 "dev-dependency-in-production" => rules.dev_dependencies_in_production,
1008 "unused-enum-member" => rules.unused_enum_members,
1009 "unused-class-member" => rules.unused_class_members,
1010 "unused-store-member" => rules.unused_store_members,
1011 "unresolved-import" => rules.unresolved_imports,
1012 "unlisted-dependency" => rules.unlisted_dependencies,
1013 "duplicate-export" => rules.duplicate_exports,
1014 "circular-dependency" => rules.circular_dependencies,
1015 "re-export-cycle" => rules.re_export_cycle,
1016 "boundary-violation" | "boundary-coverage" | "boundary-call-violation" => {
1017 rules.boundary_violation
1018 }
1019 "policy-violation" => rules.policy_violation,
1020 "invalid-client-export" => rules.invalid_client_export,
1021 "mixed-client-server-barrel" => rules.mixed_client_server_barrel,
1022 "misplaced-directive" => rules.misplaced_directive,
1023 "unprovided-inject" => rules.unprovided_injects,
1024 "unrendered-component" => rules.unrendered_components,
1025 "unused-component-prop" => rules.unused_component_props,
1026 "unused-component-emit" => rules.unused_component_emits,
1027 "unused-component-input" => rules.unused_component_inputs,
1028 "unused-component-output" => rules.unused_component_outputs,
1029 "unused-svelte-event" => rules.unused_svelte_events,
1030 "unused-server-action" => rules.unused_server_actions,
1031 "unused-load-data-key" => rules.unused_load_data_keys,
1032 "prop-drilling" => rules.prop_drilling,
1033 "thin-wrapper" => rules.thin_wrapper,
1034 "duplicate-prop-shape" => rules.duplicate_prop_shape,
1035 "route-collision" => rules.route_collision,
1036 "dynamic-segment-name-conflict" => rules.dynamic_segment_name_conflict,
1037 "stale-suppression" => rules.stale_suppressions,
1038 "unused-catalog-entry" => rules.unused_catalog_entries,
1039 "empty-catalog-group" => rules.empty_catalog_groups,
1040 "unresolved-catalog-reference" => rules.unresolved_catalog_references,
1041 "unused-dependency-override" => rules.unused_dependency_overrides,
1042 "misconfigured-dependency-override" => rules.misconfigured_dependency_overrides,
1043 _ => return None,
1044 };
1045 Some(severity)
1046}
1047
1048#[must_use]
1055pub fn build_dead_code_sarif(
1056 results: &AnalysisResults,
1057 root: &Path,
1058 rules: &RulesConfig,
1059 rule_builder: &dyn Fn(&str, &str, &str) -> serde_json::Value,
1060) -> serde_json::Value {
1061 let mut sarif_results = Vec::new();
1062 let mut snippets = SourceSnippetCache::with_root(root);
1063 let ctx = SarifCtx {
1064 results,
1065 root,
1066 rules,
1067 };
1068
1069 push_primary_dead_code_sarif_results(&mut sarif_results, &ctx, &mut snippets);
1070 push_dependency_sarif_results(&mut sarif_results, &ctx, &mut snippets);
1071 push_member_sarif_results(&mut sarif_results, &ctx, &mut snippets);
1072 push_sarif_results(
1073 &mut sarif_results,
1074 &results.unresolved_imports,
1075 &mut snippets,
1076 |i| {
1077 sarif_unresolved_import_fields(
1078 &i.import,
1079 root,
1080 severity_to_sarif_level(rules.unresolved_imports),
1081 )
1082 },
1083 );
1084 push_misc_sarif_results(&mut sarif_results, &ctx, &mut snippets);
1085 push_graph_sarif_results(&mut sarif_results, &ctx, &mut snippets);
1086 push_catalog_sarif_results(&mut sarif_results, &ctx, &mut snippets);
1087
1088 let sarif_rules = build_sarif_rules(rules, rule_builder);
1089 sarif_document(&sarif_results, &sarif_rules)
1090}
1091
1092fn push_primary_dead_code_sarif_results(
1093 sarif_results: &mut Vec<serde_json::Value>,
1094 ctx: &SarifCtx<'_>,
1095 snippets: &mut SourceSnippetCache,
1096) {
1097 let SarifCtx {
1098 results,
1099 root,
1100 rules,
1101 } = *ctx;
1102
1103 push_sarif_results(sarif_results, &results.unused_files, snippets, |finding| {
1104 sarif_unused_file_fields(
1105 &finding.file,
1106 root,
1107 severity_to_sarif_level(rules.unused_files),
1108 )
1109 });
1110 push_sarif_results(
1111 sarif_results,
1112 &results.unused_exports,
1113 snippets,
1114 |finding| {
1115 sarif_export_fields(
1116 &finding.export,
1117 root,
1118 "fallow/unused-export",
1119 severity_to_sarif_level(rules.unused_exports),
1120 "Export",
1121 "Re-export",
1122 )
1123 },
1124 );
1125 push_sarif_results(sarif_results, &results.unused_types, snippets, |finding| {
1126 sarif_export_fields(
1127 &finding.export,
1128 root,
1129 "fallow/unused-type",
1130 severity_to_sarif_level(rules.unused_types),
1131 "Type export",
1132 "Type re-export",
1133 )
1134 });
1135 push_sarif_results(
1136 sarif_results,
1137 &results.private_type_leaks,
1138 snippets,
1139 |finding| {
1140 sarif_private_type_leak_fields(
1141 &finding.leak,
1142 root,
1143 severity_to_sarif_level(rules.private_type_leaks),
1144 )
1145 },
1146 );
1147}
1148
1149fn sarif_document(
1150 sarif_results: &[serde_json::Value],
1151 sarif_rules: &[serde_json::Value],
1152) -> serde_json::Value {
1153 build_sarif_document(SarifDocumentInput {
1154 results: sarif_results,
1155 rules: sarif_rules,
1156 tool_version: env!("CARGO_PKG_VERSION"),
1157 })
1158}
1159
1160fn push_dependency_sarif_results(
1161 sarif_results: &mut Vec<serde_json::Value>,
1162 ctx: &SarifCtx<'_>,
1163 snippets: &mut SourceSnippetCache,
1164) {
1165 push_unused_dependency_sarif_results(sarif_results, ctx, snippets);
1166 push_classified_dependency_sarif_results(sarif_results, ctx, snippets);
1167}
1168
1169fn push_unused_dependency_sarif_results(
1171 sarif_results: &mut Vec<serde_json::Value>,
1172 ctx: &SarifCtx<'_>,
1173 snippets: &mut SourceSnippetCache,
1174) {
1175 let SarifCtx {
1176 results,
1177 root,
1178 rules,
1179 } = *ctx;
1180
1181 push_sarif_results(sarif_results, &results.unused_dependencies, snippets, |d| {
1182 sarif_dep_fields(
1183 &d.dep,
1184 root,
1185 "fallow/unused-dependency",
1186 severity_to_sarif_level(rules.unused_dependencies),
1187 "dependencies",
1188 )
1189 });
1190 push_sarif_results(
1191 sarif_results,
1192 &results.unused_dev_dependencies,
1193 snippets,
1194 |d| {
1195 sarif_dep_fields(
1196 &d.dep,
1197 root,
1198 "fallow/unused-dev-dependency",
1199 severity_to_sarif_level(rules.unused_dev_dependencies),
1200 "devDependencies",
1201 )
1202 },
1203 );
1204 push_sarif_results(
1205 sarif_results,
1206 &results.unused_optional_dependencies,
1207 snippets,
1208 |d| {
1209 sarif_dep_fields(
1210 &d.dep,
1211 root,
1212 "fallow/unused-optional-dependency",
1213 severity_to_sarif_level(rules.unused_optional_dependencies),
1214 "optionalDependencies",
1215 )
1216 },
1217 );
1218}
1219
1220fn push_classified_dependency_sarif_results(
1222 sarif_results: &mut Vec<serde_json::Value>,
1223 ctx: &SarifCtx<'_>,
1224 snippets: &mut SourceSnippetCache,
1225) {
1226 let SarifCtx {
1227 results,
1228 root,
1229 rules,
1230 } = *ctx;
1231
1232 push_sarif_results(
1233 sarif_results,
1234 &results.type_only_dependencies,
1235 snippets,
1236 |d| {
1237 sarif_type_only_dep_fields(
1238 &d.dep,
1239 root,
1240 severity_to_sarif_level(rules.type_only_dependencies),
1241 )
1242 },
1243 );
1244 push_sarif_results(
1245 sarif_results,
1246 &results.test_only_dependencies,
1247 snippets,
1248 |d| {
1249 sarif_test_only_dep_fields(
1250 &d.dep,
1251 root,
1252 severity_to_sarif_level(rules.test_only_dependencies),
1253 )
1254 },
1255 );
1256 push_sarif_results(
1257 sarif_results,
1258 &results.dev_dependencies_in_production,
1259 snippets,
1260 |d| {
1261 sarif_dev_dep_in_prod_fields(
1262 &d.dep,
1263 root,
1264 severity_to_sarif_level(rules.dev_dependencies_in_production),
1265 )
1266 },
1267 );
1268}
1269
1270fn push_member_sarif_results(
1271 sarif_results: &mut Vec<serde_json::Value>,
1272 ctx: &SarifCtx<'_>,
1273 snippets: &mut SourceSnippetCache,
1274) {
1275 let SarifCtx {
1276 results,
1277 root,
1278 rules,
1279 } = *ctx;
1280
1281 push_sarif_results(sarif_results, &results.unused_enum_members, snippets, |m| {
1282 sarif_member_fields(
1283 &m.member,
1284 root,
1285 "fallow/unused-enum-member",
1286 severity_to_sarif_level(rules.unused_enum_members),
1287 "Enum",
1288 )
1289 });
1290 push_sarif_results(
1291 sarif_results,
1292 &results.unused_class_members,
1293 snippets,
1294 |m| {
1295 sarif_member_fields(
1296 &m.member,
1297 root,
1298 "fallow/unused-class-member",
1299 severity_to_sarif_level(rules.unused_class_members),
1300 "Class",
1301 )
1302 },
1303 );
1304 push_sarif_results(
1305 sarif_results,
1306 &results.unused_store_members,
1307 snippets,
1308 |m| {
1309 sarif_member_fields(
1310 &m.member,
1311 root,
1312 "fallow/unused-store-member",
1313 severity_to_sarif_level(rules.unused_store_members),
1314 "Store",
1315 )
1316 },
1317 );
1318}
1319
1320fn push_misc_sarif_results(
1321 sarif_results: &mut Vec<serde_json::Value>,
1322 ctx: &SarifCtx<'_>,
1323 snippets: &mut SourceSnippetCache,
1324) {
1325 let SarifCtx {
1326 results,
1327 root,
1328 rules,
1329 } = *ctx;
1330
1331 if !results.unlisted_dependencies.is_empty() {
1332 push_sarif_unlisted_deps(
1333 sarif_results,
1334 &results.unlisted_dependencies,
1335 root,
1336 severity_to_sarif_level(rules.unlisted_dependencies),
1337 snippets,
1338 );
1339 }
1340 if !results.duplicate_exports.is_empty() {
1341 push_sarif_duplicate_exports(
1342 sarif_results,
1343 &results.duplicate_exports,
1344 root,
1345 severity_to_sarif_level(rules.duplicate_exports),
1346 snippets,
1347 );
1348 }
1349}
1350
1351fn push_component_contract_sarif_results(
1355 sarif_results: &mut Vec<serde_json::Value>,
1356 ctx: &SarifCtx<'_>,
1357 snippets: &mut SourceSnippetCache,
1358) {
1359 push_component_member_sarif_results(sarif_results, ctx, snippets);
1360 push_component_framework_sarif_results(sarif_results, ctx, snippets);
1361 push_component_shape_sarif_results(sarif_results, ctx, snippets);
1362}
1363
1364fn push_component_member_sarif_results(
1366 sarif_results: &mut Vec<serde_json::Value>,
1367 ctx: &SarifCtx<'_>,
1368 snippets: &mut SourceSnippetCache,
1369) {
1370 let SarifCtx {
1371 results,
1372 root,
1373 rules,
1374 } = *ctx;
1375
1376 push_sarif_results(
1377 sarif_results,
1378 &results.unused_component_props,
1379 snippets,
1380 |p| {
1381 sarif_unused_component_prop_fields(
1382 &p.prop,
1383 root,
1384 severity_to_sarif_level(rules.unused_component_props),
1385 )
1386 },
1387 );
1388 push_sarif_results(
1389 sarif_results,
1390 &results.unused_component_emits,
1391 snippets,
1392 |e| {
1393 sarif_unused_component_emit_fields(
1394 &e.emit,
1395 root,
1396 severity_to_sarif_level(rules.unused_component_emits),
1397 )
1398 },
1399 );
1400 push_sarif_results(
1401 sarif_results,
1402 &results.unused_component_inputs,
1403 snippets,
1404 |i| {
1405 sarif_unused_component_input_fields(
1406 &i.input,
1407 root,
1408 severity_to_sarif_level(rules.unused_component_inputs),
1409 )
1410 },
1411 );
1412 push_sarif_results(
1413 sarif_results,
1414 &results.unused_component_outputs,
1415 snippets,
1416 |o| {
1417 sarif_unused_component_output_fields(
1418 &o.output,
1419 root,
1420 severity_to_sarif_level(rules.unused_component_outputs),
1421 )
1422 },
1423 );
1424}
1425
1426fn push_component_framework_sarif_results(
1428 sarif_results: &mut Vec<serde_json::Value>,
1429 ctx: &SarifCtx<'_>,
1430 snippets: &mut SourceSnippetCache,
1431) {
1432 let SarifCtx {
1433 results,
1434 root,
1435 rules,
1436 } = *ctx;
1437
1438 push_sarif_results(
1439 sarif_results,
1440 &results.unused_svelte_events,
1441 snippets,
1442 |e| {
1443 sarif_unused_svelte_event_fields(
1444 &e.event,
1445 root,
1446 severity_to_sarif_level(rules.unused_svelte_events),
1447 )
1448 },
1449 );
1450 push_sarif_results(
1451 sarif_results,
1452 &results.unused_server_actions,
1453 snippets,
1454 |a| {
1455 sarif_unused_server_action_fields(
1456 &a.action,
1457 root,
1458 severity_to_sarif_level(rules.unused_server_actions),
1459 )
1460 },
1461 );
1462 push_sarif_results(
1463 sarif_results,
1464 &results.unused_load_data_keys,
1465 snippets,
1466 |k| {
1467 sarif_unused_load_data_key_fields(
1468 &k.key,
1469 root,
1470 severity_to_sarif_level(rules.unused_load_data_keys),
1471 )
1472 },
1473 );
1474}
1475
1476fn push_component_shape_sarif_results(
1478 sarif_results: &mut Vec<serde_json::Value>,
1479 ctx: &SarifCtx<'_>,
1480 snippets: &mut SourceSnippetCache,
1481) {
1482 let SarifCtx {
1483 results,
1484 root,
1485 rules,
1486 } = *ctx;
1487
1488 push_sarif_results(
1489 sarif_results,
1490 &results.prop_drilling_chains,
1491 snippets,
1492 |c| {
1493 sarif_prop_drilling_fields(&c.chain, root, severity_to_sarif_level(rules.prop_drilling))
1494 },
1495 );
1496 push_sarif_results(sarif_results, &results.thin_wrappers, snippets, |w| {
1497 sarif_thin_wrapper_fields(
1498 &w.wrapper,
1499 root,
1500 severity_to_sarif_level(rules.thin_wrapper),
1501 )
1502 });
1503 push_sarif_results(
1504 sarif_results,
1505 &results.duplicate_prop_shapes,
1506 snippets,
1507 |d| {
1508 sarif_duplicate_prop_shape_fields(
1509 &d.shape,
1510 root,
1511 severity_to_sarif_level(rules.duplicate_prop_shape),
1512 )
1513 },
1514 );
1515}
1516
1517fn push_graph_sarif_results(
1518 sarif_results: &mut Vec<serde_json::Value>,
1519 ctx: &SarifCtx<'_>,
1520 snippets: &mut SourceSnippetCache,
1521) {
1522 push_structure_sarif_results(sarif_results, ctx, snippets);
1523 push_framework_sarif_results(sarif_results, ctx, snippets);
1524 push_route_sarif_results(sarif_results, ctx, snippets);
1525 push_suppression_sarif_results(sarif_results, ctx, snippets);
1526}
1527
1528fn push_structure_sarif_results(
1529 sarif_results: &mut Vec<serde_json::Value>,
1530 ctx: &SarifCtx<'_>,
1531 snippets: &mut SourceSnippetCache,
1532) {
1533 push_cycle_sarif_results(sarif_results, ctx, snippets);
1534 push_boundary_sarif_results(sarif_results, ctx, snippets);
1535}
1536
1537fn push_cycle_sarif_results(
1539 sarif_results: &mut Vec<serde_json::Value>,
1540 ctx: &SarifCtx<'_>,
1541 snippets: &mut SourceSnippetCache,
1542) {
1543 let SarifCtx {
1544 results,
1545 root,
1546 rules,
1547 } = *ctx;
1548
1549 push_sarif_results(
1550 sarif_results,
1551 &results.circular_dependencies,
1552 snippets,
1553 |c| {
1554 sarif_circular_dep_fields(
1555 &c.cycle,
1556 root,
1557 severity_to_sarif_level(rules.circular_dependencies),
1558 )
1559 },
1560 );
1561 push_sarif_results(sarif_results, &results.re_export_cycles, snippets, |c| {
1562 sarif_re_export_cycle_fields(
1563 &c.cycle,
1564 root,
1565 severity_to_sarif_level(rules.re_export_cycle),
1566 )
1567 });
1568}
1569
1570fn push_boundary_sarif_results(
1572 sarif_results: &mut Vec<serde_json::Value>,
1573 ctx: &SarifCtx<'_>,
1574 snippets: &mut SourceSnippetCache,
1575) {
1576 let SarifCtx {
1577 results,
1578 root,
1579 rules,
1580 } = *ctx;
1581
1582 push_sarif_results(sarif_results, &results.boundary_violations, snippets, |v| {
1583 sarif_boundary_violation_fields(
1584 &v.violation,
1585 root,
1586 severity_to_sarif_level(rules.boundary_violation),
1587 )
1588 });
1589 push_sarif_results(
1590 sarif_results,
1591 &results.boundary_coverage_violations,
1592 snippets,
1593 |v| {
1594 sarif_boundary_coverage_fields(
1595 &v.violation,
1596 root,
1597 severity_to_sarif_level(rules.boundary_violation),
1598 )
1599 },
1600 );
1601 push_sarif_results(
1602 sarif_results,
1603 &results.boundary_call_violations,
1604 snippets,
1605 |v| {
1606 sarif_boundary_call_fields(
1607 &v.violation,
1608 root,
1609 severity_to_sarif_level(rules.boundary_violation),
1610 )
1611 },
1612 );
1613 push_sarif_results(sarif_results, &results.policy_violations, snippets, |v| {
1614 sarif_policy_violation_fields(&v.violation, root)
1615 });
1616}
1617
1618fn push_framework_sarif_results(
1619 sarif_results: &mut Vec<serde_json::Value>,
1620 ctx: &SarifCtx<'_>,
1621 snippets: &mut SourceSnippetCache,
1622) {
1623 push_framework_boundary_sarif_results(sarif_results, ctx, snippets);
1624 push_component_contract_sarif_results(sarif_results, ctx, snippets);
1625}
1626
1627fn push_framework_boundary_sarif_results(
1629 sarif_results: &mut Vec<serde_json::Value>,
1630 ctx: &SarifCtx<'_>,
1631 snippets: &mut SourceSnippetCache,
1632) {
1633 let SarifCtx {
1634 results,
1635 root,
1636 rules,
1637 } = *ctx;
1638
1639 push_sarif_results(
1640 sarif_results,
1641 &results.invalid_client_exports,
1642 snippets,
1643 |e| {
1644 sarif_invalid_client_export_fields(
1645 &e.export,
1646 root,
1647 severity_to_sarif_level(rules.invalid_client_export),
1648 )
1649 },
1650 );
1651 push_sarif_results(
1652 sarif_results,
1653 &results.mixed_client_server_barrels,
1654 snippets,
1655 |b| {
1656 sarif_mixed_client_server_barrel_fields(
1657 &b.barrel,
1658 root,
1659 severity_to_sarif_level(rules.mixed_client_server_barrel),
1660 )
1661 },
1662 );
1663 push_sarif_results(
1664 sarif_results,
1665 &results.misplaced_directives,
1666 snippets,
1667 |d| {
1668 sarif_misplaced_directive_fields(
1669 &d.directive_site,
1670 root,
1671 severity_to_sarif_level(rules.misplaced_directive),
1672 )
1673 },
1674 );
1675 push_framework_render_sarif_results(sarif_results, ctx, snippets);
1676}
1677
1678fn push_framework_render_sarif_results(
1679 sarif_results: &mut Vec<serde_json::Value>,
1680 ctx: &SarifCtx<'_>,
1681 snippets: &mut SourceSnippetCache,
1682) {
1683 let SarifCtx {
1684 results,
1685 root,
1686 rules,
1687 } = *ctx;
1688
1689 push_sarif_results(sarif_results, &results.unprovided_injects, snippets, |i| {
1690 sarif_unprovided_inject_fields(
1691 &i.inject,
1692 root,
1693 severity_to_sarif_level(rules.unprovided_injects),
1694 )
1695 });
1696 push_sarif_results(
1697 sarif_results,
1698 &results.unrendered_components,
1699 snippets,
1700 |c| {
1701 sarif_unrendered_component_fields(
1702 &c.component,
1703 root,
1704 severity_to_sarif_level(rules.unrendered_components),
1705 )
1706 },
1707 );
1708}
1709
1710fn push_route_sarif_results(
1711 sarif_results: &mut Vec<serde_json::Value>,
1712 ctx: &SarifCtx<'_>,
1713 snippets: &mut SourceSnippetCache,
1714) {
1715 let SarifCtx {
1716 results,
1717 root,
1718 rules,
1719 } = *ctx;
1720
1721 push_sarif_results(sarif_results, &results.route_collisions, snippets, |c| {
1722 sarif_route_collision_fields(
1723 &c.collision,
1724 root,
1725 severity_to_sarif_level(rules.route_collision),
1726 )
1727 });
1728 push_sarif_results(
1729 sarif_results,
1730 &results.dynamic_segment_name_conflicts,
1731 snippets,
1732 |c| {
1733 sarif_dynamic_segment_name_conflict_fields(
1734 &c.conflict,
1735 root,
1736 severity_to_sarif_level(rules.dynamic_segment_name_conflict),
1737 )
1738 },
1739 );
1740}
1741
1742fn push_suppression_sarif_results(
1743 sarif_results: &mut Vec<serde_json::Value>,
1744 ctx: &SarifCtx<'_>,
1745 snippets: &mut SourceSnippetCache,
1746) {
1747 let SarifCtx {
1748 results,
1749 root,
1750 rules,
1751 } = *ctx;
1752
1753 push_sarif_results(sarif_results, &results.stale_suppressions, snippets, |s| {
1754 sarif_stale_suppression_fields(
1755 s,
1756 root,
1757 severity_to_sarif_level(stale_suppression_severity(s, rules)),
1758 )
1759 });
1760}
1761
1762fn push_catalog_sarif_results(
1763 sarif_results: &mut Vec<serde_json::Value>,
1764 ctx: &SarifCtx<'_>,
1765 snippets: &mut SourceSnippetCache,
1766) {
1767 push_catalog_entry_sarif_results(sarif_results, ctx, snippets);
1768 push_dependency_override_sarif_results(sarif_results, ctx, snippets);
1769}
1770
1771fn push_catalog_entry_sarif_results(
1773 sarif_results: &mut Vec<serde_json::Value>,
1774 ctx: &SarifCtx<'_>,
1775 snippets: &mut SourceSnippetCache,
1776) {
1777 let SarifCtx {
1778 results,
1779 root,
1780 rules,
1781 } = *ctx;
1782
1783 push_sarif_results(
1784 sarif_results,
1785 &results.unused_catalog_entries,
1786 snippets,
1787 |e| {
1788 sarif_unused_catalog_entry_fields(
1789 e,
1790 root,
1791 severity_to_sarif_level(rules.unused_catalog_entries),
1792 )
1793 },
1794 );
1795 push_sarif_results(
1796 sarif_results,
1797 &results.empty_catalog_groups,
1798 snippets,
1799 |g| {
1800 sarif_empty_catalog_group_fields(
1801 g,
1802 root,
1803 severity_to_sarif_level(rules.empty_catalog_groups),
1804 )
1805 },
1806 );
1807 push_sarif_results(
1808 sarif_results,
1809 &results.unresolved_catalog_references,
1810 snippets,
1811 |f| {
1812 sarif_unresolved_catalog_reference_fields(
1813 f,
1814 root,
1815 severity_to_sarif_level(rules.unresolved_catalog_references),
1816 )
1817 },
1818 );
1819}
1820
1821fn push_dependency_override_sarif_results(
1823 sarif_results: &mut Vec<serde_json::Value>,
1824 ctx: &SarifCtx<'_>,
1825 snippets: &mut SourceSnippetCache,
1826) {
1827 let SarifCtx {
1828 results,
1829 root,
1830 rules,
1831 } = *ctx;
1832
1833 push_sarif_results(
1834 sarif_results,
1835 &results.unused_dependency_overrides,
1836 snippets,
1837 |f| {
1838 sarif_unused_dependency_override_fields(
1839 f,
1840 root,
1841 severity_to_sarif_level(rules.unused_dependency_overrides),
1842 )
1843 },
1844 );
1845 push_sarif_results(
1846 sarif_results,
1847 &results.misconfigured_dependency_overrides,
1848 snippets,
1849 |f| {
1850 sarif_misconfigured_dependency_override_fields(
1851 f,
1852 root,
1853 severity_to_sarif_level(rules.misconfigured_dependency_overrides),
1854 )
1855 },
1856 );
1857}
1858
1859#[cfg(test)]
1860mod tests {
1861 use std::collections::BTreeSet;
1862 use std::path::Path;
1863
1864 use fallow_config::RulesConfig;
1865 use fallow_types::results::AnalysisResults;
1866
1867 use super::*;
1868
1869 fn test_rule_builder(id: &str, description: &str, level: &str) -> serde_json::Value {
1870 serde_json::json!({
1871 "id": id,
1872 "shortDescription": { "text": description },
1873 "defaultConfiguration": { "level": level }
1874 })
1875 }
1876
1877 #[test]
1878 fn sarif_rule_list_is_backed_by_issue_contracts() {
1879 let sarif = build_dead_code_sarif(
1880 &AnalysisResults::default(),
1881 Path::new("."),
1882 &RulesConfig::default(),
1883 &test_rule_builder,
1884 );
1885 let Some(rules) = sarif
1886 .pointer("/runs/0/tool/driver/rules")
1887 .and_then(serde_json::Value::as_array)
1888 else {
1889 panic!("SARIF document should contain driver rules");
1890 };
1891
1892 let actual_ids = rules
1893 .iter()
1894 .filter_map(|rule| {
1895 rule.get("id")
1896 .and_then(serde_json::Value::as_str)
1897 .map(str::to_owned)
1898 })
1899 .collect::<BTreeSet<_>>();
1900 let expected_ids = issue_output_contracts()
1901 .flat_map(|contract| contract.sarif_rule_ids)
1902 .collect::<BTreeSet<_>>();
1903
1904 assert_eq!(actual_ids, expected_ids);
1905
1906 for rule in rules {
1907 let id = rule
1908 .get("id")
1909 .and_then(serde_json::Value::as_str)
1910 .expect("SARIF rule should have id");
1911 let description = rule
1912 .pointer("/shortDescription/text")
1913 .and_then(serde_json::Value::as_str)
1914 .expect("SARIF rule should have short description");
1915 assert_eq!(
1916 description,
1917 issue_sarif_rule_description(id).expect("SARIF rule description should resolve")
1918 );
1919 }
1920 }
1921}