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 pnpm-lock.yaml",
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]
1049pub fn build_dead_code_sarif(
1050 results: &AnalysisResults,
1051 root: &Path,
1052 rules: &RulesConfig,
1053 rule_builder: &dyn Fn(&str, &str, &str) -> serde_json::Value,
1054) -> serde_json::Value {
1055 let mut sarif_results = Vec::new();
1056 let mut snippets = SourceSnippetCache::default();
1057 let ctx = SarifCtx {
1058 results,
1059 root,
1060 rules,
1061 };
1062
1063 push_primary_dead_code_sarif_results(&mut sarif_results, &ctx, &mut snippets);
1064 push_dependency_sarif_results(&mut sarif_results, &ctx, &mut snippets);
1065 push_member_sarif_results(&mut sarif_results, &ctx, &mut snippets);
1066 push_sarif_results(
1067 &mut sarif_results,
1068 &results.unresolved_imports,
1069 &mut snippets,
1070 |i| {
1071 sarif_unresolved_import_fields(
1072 &i.import,
1073 root,
1074 severity_to_sarif_level(rules.unresolved_imports),
1075 )
1076 },
1077 );
1078 push_misc_sarif_results(&mut sarif_results, &ctx, &mut snippets);
1079 push_graph_sarif_results(&mut sarif_results, &ctx, &mut snippets);
1080 push_catalog_sarif_results(&mut sarif_results, &ctx, &mut snippets);
1081
1082 let sarif_rules = build_sarif_rules(rules, rule_builder);
1083 sarif_document(&sarif_results, &sarif_rules)
1084}
1085
1086fn push_primary_dead_code_sarif_results(
1087 sarif_results: &mut Vec<serde_json::Value>,
1088 ctx: &SarifCtx<'_>,
1089 snippets: &mut SourceSnippetCache,
1090) {
1091 let SarifCtx {
1092 results,
1093 root,
1094 rules,
1095 } = *ctx;
1096
1097 push_sarif_results(sarif_results, &results.unused_files, snippets, |finding| {
1098 sarif_unused_file_fields(
1099 &finding.file,
1100 root,
1101 severity_to_sarif_level(rules.unused_files),
1102 )
1103 });
1104 push_sarif_results(
1105 sarif_results,
1106 &results.unused_exports,
1107 snippets,
1108 |finding| {
1109 sarif_export_fields(
1110 &finding.export,
1111 root,
1112 "fallow/unused-export",
1113 severity_to_sarif_level(rules.unused_exports),
1114 "Export",
1115 "Re-export",
1116 )
1117 },
1118 );
1119 push_sarif_results(sarif_results, &results.unused_types, snippets, |finding| {
1120 sarif_export_fields(
1121 &finding.export,
1122 root,
1123 "fallow/unused-type",
1124 severity_to_sarif_level(rules.unused_types),
1125 "Type export",
1126 "Type re-export",
1127 )
1128 });
1129 push_sarif_results(
1130 sarif_results,
1131 &results.private_type_leaks,
1132 snippets,
1133 |finding| {
1134 sarif_private_type_leak_fields(
1135 &finding.leak,
1136 root,
1137 severity_to_sarif_level(rules.private_type_leaks),
1138 )
1139 },
1140 );
1141}
1142
1143fn sarif_document(
1144 sarif_results: &[serde_json::Value],
1145 sarif_rules: &[serde_json::Value],
1146) -> serde_json::Value {
1147 build_sarif_document(SarifDocumentInput {
1148 results: sarif_results,
1149 rules: sarif_rules,
1150 tool_version: env!("CARGO_PKG_VERSION"),
1151 })
1152}
1153
1154fn push_dependency_sarif_results(
1155 sarif_results: &mut Vec<serde_json::Value>,
1156 ctx: &SarifCtx<'_>,
1157 snippets: &mut SourceSnippetCache,
1158) {
1159 push_unused_dependency_sarif_results(sarif_results, ctx, snippets);
1160 push_classified_dependency_sarif_results(sarif_results, ctx, snippets);
1161}
1162
1163fn push_unused_dependency_sarif_results(
1165 sarif_results: &mut Vec<serde_json::Value>,
1166 ctx: &SarifCtx<'_>,
1167 snippets: &mut SourceSnippetCache,
1168) {
1169 let SarifCtx {
1170 results,
1171 root,
1172 rules,
1173 } = *ctx;
1174
1175 push_sarif_results(sarif_results, &results.unused_dependencies, snippets, |d| {
1176 sarif_dep_fields(
1177 &d.dep,
1178 root,
1179 "fallow/unused-dependency",
1180 severity_to_sarif_level(rules.unused_dependencies),
1181 "dependencies",
1182 )
1183 });
1184 push_sarif_results(
1185 sarif_results,
1186 &results.unused_dev_dependencies,
1187 snippets,
1188 |d| {
1189 sarif_dep_fields(
1190 &d.dep,
1191 root,
1192 "fallow/unused-dev-dependency",
1193 severity_to_sarif_level(rules.unused_dev_dependencies),
1194 "devDependencies",
1195 )
1196 },
1197 );
1198 push_sarif_results(
1199 sarif_results,
1200 &results.unused_optional_dependencies,
1201 snippets,
1202 |d| {
1203 sarif_dep_fields(
1204 &d.dep,
1205 root,
1206 "fallow/unused-optional-dependency",
1207 severity_to_sarif_level(rules.unused_optional_dependencies),
1208 "optionalDependencies",
1209 )
1210 },
1211 );
1212}
1213
1214fn push_classified_dependency_sarif_results(
1216 sarif_results: &mut Vec<serde_json::Value>,
1217 ctx: &SarifCtx<'_>,
1218 snippets: &mut SourceSnippetCache,
1219) {
1220 let SarifCtx {
1221 results,
1222 root,
1223 rules,
1224 } = *ctx;
1225
1226 push_sarif_results(
1227 sarif_results,
1228 &results.type_only_dependencies,
1229 snippets,
1230 |d| {
1231 sarif_type_only_dep_fields(
1232 &d.dep,
1233 root,
1234 severity_to_sarif_level(rules.type_only_dependencies),
1235 )
1236 },
1237 );
1238 push_sarif_results(
1239 sarif_results,
1240 &results.test_only_dependencies,
1241 snippets,
1242 |d| {
1243 sarif_test_only_dep_fields(
1244 &d.dep,
1245 root,
1246 severity_to_sarif_level(rules.test_only_dependencies),
1247 )
1248 },
1249 );
1250 push_sarif_results(
1251 sarif_results,
1252 &results.dev_dependencies_in_production,
1253 snippets,
1254 |d| {
1255 sarif_dev_dep_in_prod_fields(
1256 &d.dep,
1257 root,
1258 severity_to_sarif_level(rules.dev_dependencies_in_production),
1259 )
1260 },
1261 );
1262}
1263
1264fn push_member_sarif_results(
1265 sarif_results: &mut Vec<serde_json::Value>,
1266 ctx: &SarifCtx<'_>,
1267 snippets: &mut SourceSnippetCache,
1268) {
1269 let SarifCtx {
1270 results,
1271 root,
1272 rules,
1273 } = *ctx;
1274
1275 push_sarif_results(sarif_results, &results.unused_enum_members, snippets, |m| {
1276 sarif_member_fields(
1277 &m.member,
1278 root,
1279 "fallow/unused-enum-member",
1280 severity_to_sarif_level(rules.unused_enum_members),
1281 "Enum",
1282 )
1283 });
1284 push_sarif_results(
1285 sarif_results,
1286 &results.unused_class_members,
1287 snippets,
1288 |m| {
1289 sarif_member_fields(
1290 &m.member,
1291 root,
1292 "fallow/unused-class-member",
1293 severity_to_sarif_level(rules.unused_class_members),
1294 "Class",
1295 )
1296 },
1297 );
1298 push_sarif_results(
1299 sarif_results,
1300 &results.unused_store_members,
1301 snippets,
1302 |m| {
1303 sarif_member_fields(
1304 &m.member,
1305 root,
1306 "fallow/unused-store-member",
1307 severity_to_sarif_level(rules.unused_store_members),
1308 "Store",
1309 )
1310 },
1311 );
1312}
1313
1314fn push_misc_sarif_results(
1315 sarif_results: &mut Vec<serde_json::Value>,
1316 ctx: &SarifCtx<'_>,
1317 snippets: &mut SourceSnippetCache,
1318) {
1319 let SarifCtx {
1320 results,
1321 root,
1322 rules,
1323 } = *ctx;
1324
1325 if !results.unlisted_dependencies.is_empty() {
1326 push_sarif_unlisted_deps(
1327 sarif_results,
1328 &results.unlisted_dependencies,
1329 root,
1330 severity_to_sarif_level(rules.unlisted_dependencies),
1331 snippets,
1332 );
1333 }
1334 if !results.duplicate_exports.is_empty() {
1335 push_sarif_duplicate_exports(
1336 sarif_results,
1337 &results.duplicate_exports,
1338 root,
1339 severity_to_sarif_level(rules.duplicate_exports),
1340 snippets,
1341 );
1342 }
1343}
1344
1345fn push_component_contract_sarif_results(
1349 sarif_results: &mut Vec<serde_json::Value>,
1350 ctx: &SarifCtx<'_>,
1351 snippets: &mut SourceSnippetCache,
1352) {
1353 push_component_member_sarif_results(sarif_results, ctx, snippets);
1354 push_component_framework_sarif_results(sarif_results, ctx, snippets);
1355 push_component_shape_sarif_results(sarif_results, ctx, snippets);
1356}
1357
1358fn push_component_member_sarif_results(
1360 sarif_results: &mut Vec<serde_json::Value>,
1361 ctx: &SarifCtx<'_>,
1362 snippets: &mut SourceSnippetCache,
1363) {
1364 let SarifCtx {
1365 results,
1366 root,
1367 rules,
1368 } = *ctx;
1369
1370 push_sarif_results(
1371 sarif_results,
1372 &results.unused_component_props,
1373 snippets,
1374 |p| {
1375 sarif_unused_component_prop_fields(
1376 &p.prop,
1377 root,
1378 severity_to_sarif_level(rules.unused_component_props),
1379 )
1380 },
1381 );
1382 push_sarif_results(
1383 sarif_results,
1384 &results.unused_component_emits,
1385 snippets,
1386 |e| {
1387 sarif_unused_component_emit_fields(
1388 &e.emit,
1389 root,
1390 severity_to_sarif_level(rules.unused_component_emits),
1391 )
1392 },
1393 );
1394 push_sarif_results(
1395 sarif_results,
1396 &results.unused_component_inputs,
1397 snippets,
1398 |i| {
1399 sarif_unused_component_input_fields(
1400 &i.input,
1401 root,
1402 severity_to_sarif_level(rules.unused_component_inputs),
1403 )
1404 },
1405 );
1406 push_sarif_results(
1407 sarif_results,
1408 &results.unused_component_outputs,
1409 snippets,
1410 |o| {
1411 sarif_unused_component_output_fields(
1412 &o.output,
1413 root,
1414 severity_to_sarif_level(rules.unused_component_outputs),
1415 )
1416 },
1417 );
1418}
1419
1420fn push_component_framework_sarif_results(
1422 sarif_results: &mut Vec<serde_json::Value>,
1423 ctx: &SarifCtx<'_>,
1424 snippets: &mut SourceSnippetCache,
1425) {
1426 let SarifCtx {
1427 results,
1428 root,
1429 rules,
1430 } = *ctx;
1431
1432 push_sarif_results(
1433 sarif_results,
1434 &results.unused_svelte_events,
1435 snippets,
1436 |e| {
1437 sarif_unused_svelte_event_fields(
1438 &e.event,
1439 root,
1440 severity_to_sarif_level(rules.unused_svelte_events),
1441 )
1442 },
1443 );
1444 push_sarif_results(
1445 sarif_results,
1446 &results.unused_server_actions,
1447 snippets,
1448 |a| {
1449 sarif_unused_server_action_fields(
1450 &a.action,
1451 root,
1452 severity_to_sarif_level(rules.unused_server_actions),
1453 )
1454 },
1455 );
1456 push_sarif_results(
1457 sarif_results,
1458 &results.unused_load_data_keys,
1459 snippets,
1460 |k| {
1461 sarif_unused_load_data_key_fields(
1462 &k.key,
1463 root,
1464 severity_to_sarif_level(rules.unused_load_data_keys),
1465 )
1466 },
1467 );
1468}
1469
1470fn push_component_shape_sarif_results(
1472 sarif_results: &mut Vec<serde_json::Value>,
1473 ctx: &SarifCtx<'_>,
1474 snippets: &mut SourceSnippetCache,
1475) {
1476 let SarifCtx {
1477 results,
1478 root,
1479 rules,
1480 } = *ctx;
1481
1482 push_sarif_results(
1483 sarif_results,
1484 &results.prop_drilling_chains,
1485 snippets,
1486 |c| {
1487 sarif_prop_drilling_fields(&c.chain, root, severity_to_sarif_level(rules.prop_drilling))
1488 },
1489 );
1490 push_sarif_results(sarif_results, &results.thin_wrappers, snippets, |w| {
1491 sarif_thin_wrapper_fields(
1492 &w.wrapper,
1493 root,
1494 severity_to_sarif_level(rules.thin_wrapper),
1495 )
1496 });
1497 push_sarif_results(
1498 sarif_results,
1499 &results.duplicate_prop_shapes,
1500 snippets,
1501 |d| {
1502 sarif_duplicate_prop_shape_fields(
1503 &d.shape,
1504 root,
1505 severity_to_sarif_level(rules.duplicate_prop_shape),
1506 )
1507 },
1508 );
1509}
1510
1511fn push_graph_sarif_results(
1512 sarif_results: &mut Vec<serde_json::Value>,
1513 ctx: &SarifCtx<'_>,
1514 snippets: &mut SourceSnippetCache,
1515) {
1516 push_structure_sarif_results(sarif_results, ctx, snippets);
1517 push_framework_sarif_results(sarif_results, ctx, snippets);
1518 push_route_sarif_results(sarif_results, ctx, snippets);
1519 push_suppression_sarif_results(sarif_results, ctx, snippets);
1520}
1521
1522fn push_structure_sarif_results(
1523 sarif_results: &mut Vec<serde_json::Value>,
1524 ctx: &SarifCtx<'_>,
1525 snippets: &mut SourceSnippetCache,
1526) {
1527 push_cycle_sarif_results(sarif_results, ctx, snippets);
1528 push_boundary_sarif_results(sarif_results, ctx, snippets);
1529}
1530
1531fn push_cycle_sarif_results(
1533 sarif_results: &mut Vec<serde_json::Value>,
1534 ctx: &SarifCtx<'_>,
1535 snippets: &mut SourceSnippetCache,
1536) {
1537 let SarifCtx {
1538 results,
1539 root,
1540 rules,
1541 } = *ctx;
1542
1543 push_sarif_results(
1544 sarif_results,
1545 &results.circular_dependencies,
1546 snippets,
1547 |c| {
1548 sarif_circular_dep_fields(
1549 &c.cycle,
1550 root,
1551 severity_to_sarif_level(rules.circular_dependencies),
1552 )
1553 },
1554 );
1555 push_sarif_results(sarif_results, &results.re_export_cycles, snippets, |c| {
1556 sarif_re_export_cycle_fields(
1557 &c.cycle,
1558 root,
1559 severity_to_sarif_level(rules.re_export_cycle),
1560 )
1561 });
1562}
1563
1564fn push_boundary_sarif_results(
1566 sarif_results: &mut Vec<serde_json::Value>,
1567 ctx: &SarifCtx<'_>,
1568 snippets: &mut SourceSnippetCache,
1569) {
1570 let SarifCtx {
1571 results,
1572 root,
1573 rules,
1574 } = *ctx;
1575
1576 push_sarif_results(sarif_results, &results.boundary_violations, snippets, |v| {
1577 sarif_boundary_violation_fields(
1578 &v.violation,
1579 root,
1580 severity_to_sarif_level(rules.boundary_violation),
1581 )
1582 });
1583 push_sarif_results(
1584 sarif_results,
1585 &results.boundary_coverage_violations,
1586 snippets,
1587 |v| {
1588 sarif_boundary_coverage_fields(
1589 &v.violation,
1590 root,
1591 severity_to_sarif_level(rules.boundary_violation),
1592 )
1593 },
1594 );
1595 push_sarif_results(
1596 sarif_results,
1597 &results.boundary_call_violations,
1598 snippets,
1599 |v| {
1600 sarif_boundary_call_fields(
1601 &v.violation,
1602 root,
1603 severity_to_sarif_level(rules.boundary_violation),
1604 )
1605 },
1606 );
1607 push_sarif_results(sarif_results, &results.policy_violations, snippets, |v| {
1608 sarif_policy_violation_fields(&v.violation, root)
1609 });
1610}
1611
1612fn push_framework_sarif_results(
1613 sarif_results: &mut Vec<serde_json::Value>,
1614 ctx: &SarifCtx<'_>,
1615 snippets: &mut SourceSnippetCache,
1616) {
1617 push_framework_boundary_sarif_results(sarif_results, ctx, snippets);
1618 push_component_contract_sarif_results(sarif_results, ctx, snippets);
1619}
1620
1621fn push_framework_boundary_sarif_results(
1623 sarif_results: &mut Vec<serde_json::Value>,
1624 ctx: &SarifCtx<'_>,
1625 snippets: &mut SourceSnippetCache,
1626) {
1627 let SarifCtx {
1628 results,
1629 root,
1630 rules,
1631 } = *ctx;
1632
1633 push_sarif_results(
1634 sarif_results,
1635 &results.invalid_client_exports,
1636 snippets,
1637 |e| {
1638 sarif_invalid_client_export_fields(
1639 &e.export,
1640 root,
1641 severity_to_sarif_level(rules.invalid_client_export),
1642 )
1643 },
1644 );
1645 push_sarif_results(
1646 sarif_results,
1647 &results.mixed_client_server_barrels,
1648 snippets,
1649 |b| {
1650 sarif_mixed_client_server_barrel_fields(
1651 &b.barrel,
1652 root,
1653 severity_to_sarif_level(rules.mixed_client_server_barrel),
1654 )
1655 },
1656 );
1657 push_sarif_results(
1658 sarif_results,
1659 &results.misplaced_directives,
1660 snippets,
1661 |d| {
1662 sarif_misplaced_directive_fields(
1663 &d.directive_site,
1664 root,
1665 severity_to_sarif_level(rules.misplaced_directive),
1666 )
1667 },
1668 );
1669 push_framework_render_sarif_results(sarif_results, ctx, snippets);
1670}
1671
1672fn push_framework_render_sarif_results(
1673 sarif_results: &mut Vec<serde_json::Value>,
1674 ctx: &SarifCtx<'_>,
1675 snippets: &mut SourceSnippetCache,
1676) {
1677 let SarifCtx {
1678 results,
1679 root,
1680 rules,
1681 } = *ctx;
1682
1683 push_sarif_results(sarif_results, &results.unprovided_injects, snippets, |i| {
1684 sarif_unprovided_inject_fields(
1685 &i.inject,
1686 root,
1687 severity_to_sarif_level(rules.unprovided_injects),
1688 )
1689 });
1690 push_sarif_results(
1691 sarif_results,
1692 &results.unrendered_components,
1693 snippets,
1694 |c| {
1695 sarif_unrendered_component_fields(
1696 &c.component,
1697 root,
1698 severity_to_sarif_level(rules.unrendered_components),
1699 )
1700 },
1701 );
1702}
1703
1704fn push_route_sarif_results(
1705 sarif_results: &mut Vec<serde_json::Value>,
1706 ctx: &SarifCtx<'_>,
1707 snippets: &mut SourceSnippetCache,
1708) {
1709 let SarifCtx {
1710 results,
1711 root,
1712 rules,
1713 } = *ctx;
1714
1715 push_sarif_results(sarif_results, &results.route_collisions, snippets, |c| {
1716 sarif_route_collision_fields(
1717 &c.collision,
1718 root,
1719 severity_to_sarif_level(rules.route_collision),
1720 )
1721 });
1722 push_sarif_results(
1723 sarif_results,
1724 &results.dynamic_segment_name_conflicts,
1725 snippets,
1726 |c| {
1727 sarif_dynamic_segment_name_conflict_fields(
1728 &c.conflict,
1729 root,
1730 severity_to_sarif_level(rules.dynamic_segment_name_conflict),
1731 )
1732 },
1733 );
1734}
1735
1736fn push_suppression_sarif_results(
1737 sarif_results: &mut Vec<serde_json::Value>,
1738 ctx: &SarifCtx<'_>,
1739 snippets: &mut SourceSnippetCache,
1740) {
1741 let SarifCtx {
1742 results,
1743 root,
1744 rules,
1745 } = *ctx;
1746
1747 push_sarif_results(sarif_results, &results.stale_suppressions, snippets, |s| {
1748 sarif_stale_suppression_fields(
1749 s,
1750 root,
1751 severity_to_sarif_level(stale_suppression_severity(s, rules)),
1752 )
1753 });
1754}
1755
1756fn push_catalog_sarif_results(
1757 sarif_results: &mut Vec<serde_json::Value>,
1758 ctx: &SarifCtx<'_>,
1759 snippets: &mut SourceSnippetCache,
1760) {
1761 push_catalog_entry_sarif_results(sarif_results, ctx, snippets);
1762 push_dependency_override_sarif_results(sarif_results, ctx, snippets);
1763}
1764
1765fn push_catalog_entry_sarif_results(
1767 sarif_results: &mut Vec<serde_json::Value>,
1768 ctx: &SarifCtx<'_>,
1769 snippets: &mut SourceSnippetCache,
1770) {
1771 let SarifCtx {
1772 results,
1773 root,
1774 rules,
1775 } = *ctx;
1776
1777 push_sarif_results(
1778 sarif_results,
1779 &results.unused_catalog_entries,
1780 snippets,
1781 |e| {
1782 sarif_unused_catalog_entry_fields(
1783 e,
1784 root,
1785 severity_to_sarif_level(rules.unused_catalog_entries),
1786 )
1787 },
1788 );
1789 push_sarif_results(
1790 sarif_results,
1791 &results.empty_catalog_groups,
1792 snippets,
1793 |g| {
1794 sarif_empty_catalog_group_fields(
1795 g,
1796 root,
1797 severity_to_sarif_level(rules.empty_catalog_groups),
1798 )
1799 },
1800 );
1801 push_sarif_results(
1802 sarif_results,
1803 &results.unresolved_catalog_references,
1804 snippets,
1805 |f| {
1806 sarif_unresolved_catalog_reference_fields(
1807 f,
1808 root,
1809 severity_to_sarif_level(rules.unresolved_catalog_references),
1810 )
1811 },
1812 );
1813}
1814
1815fn push_dependency_override_sarif_results(
1817 sarif_results: &mut Vec<serde_json::Value>,
1818 ctx: &SarifCtx<'_>,
1819 snippets: &mut SourceSnippetCache,
1820) {
1821 let SarifCtx {
1822 results,
1823 root,
1824 rules,
1825 } = *ctx;
1826
1827 push_sarif_results(
1828 sarif_results,
1829 &results.unused_dependency_overrides,
1830 snippets,
1831 |f| {
1832 sarif_unused_dependency_override_fields(
1833 f,
1834 root,
1835 severity_to_sarif_level(rules.unused_dependency_overrides),
1836 )
1837 },
1838 );
1839 push_sarif_results(
1840 sarif_results,
1841 &results.misconfigured_dependency_overrides,
1842 snippets,
1843 |f| {
1844 sarif_misconfigured_dependency_override_fields(
1845 f,
1846 root,
1847 severity_to_sarif_level(rules.misconfigured_dependency_overrides),
1848 )
1849 },
1850 );
1851}
1852
1853#[cfg(test)]
1854mod tests {
1855 use std::collections::BTreeSet;
1856 use std::path::Path;
1857
1858 use fallow_config::RulesConfig;
1859 use fallow_types::results::AnalysisResults;
1860
1861 use super::*;
1862
1863 fn test_rule_builder(id: &str, description: &str, level: &str) -> serde_json::Value {
1864 serde_json::json!({
1865 "id": id,
1866 "shortDescription": { "text": description },
1867 "defaultConfiguration": { "level": level }
1868 })
1869 }
1870
1871 #[test]
1872 fn sarif_rule_list_is_backed_by_issue_contracts() {
1873 let sarif = build_dead_code_sarif(
1874 &AnalysisResults::default(),
1875 Path::new("."),
1876 &RulesConfig::default(),
1877 &test_rule_builder,
1878 );
1879 let Some(rules) = sarif
1880 .pointer("/runs/0/tool/driver/rules")
1881 .and_then(serde_json::Value::as_array)
1882 else {
1883 panic!("SARIF document should contain driver rules");
1884 };
1885
1886 let actual_ids = rules
1887 .iter()
1888 .filter_map(|rule| {
1889 rule.get("id")
1890 .and_then(serde_json::Value::as_str)
1891 .map(str::to_owned)
1892 })
1893 .collect::<BTreeSet<_>>();
1894 let expected_ids = issue_output_contracts()
1895 .flat_map(|contract| contract.sarif_rule_ids)
1896 .collect::<BTreeSet<_>>();
1897
1898 assert_eq!(actual_ids, expected_ids);
1899
1900 for rule in rules {
1901 let id = rule
1902 .get("id")
1903 .and_then(serde_json::Value::as_str)
1904 .expect("SARIF rule should have id");
1905 let description = rule
1906 .pointer("/shortDescription/text")
1907 .and_then(serde_json::Value::as_str)
1908 .expect("SARIF rule should have short description");
1909 assert_eq!(
1910 description,
1911 issue_sarif_rule_description(id).expect("SARIF rule description should resolve")
1912 );
1913 }
1914 }
1915}