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