1use std::collections::hash_map::DefaultHasher;
2use std::hash::{Hash, Hasher};
3use std::path::Path;
4
5use rustc_hash::{FxHashMap, FxHashSet};
6
7use fallow_config::{ResolvedConfig, Severity};
8use fallow_types::envelope::AuditIntroduced;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct AuditFindingRecord {
13 pub collection: &'static str,
15 pub ordinal: usize,
17 pub stable_key: String,
19 pub introduced: bool,
21 pub effective_severity: Severity,
23}
24
25#[derive(Debug, Clone, Default)]
27pub struct DeadCodeAuditLedger {
28 records: Vec<AuditFindingRecord>,
29 keys: FxHashSet<String>,
30 introduced_keys: FxHashSet<String>,
31 inherited_keys: FxHashSet<String>,
32 #[cfg(test)]
33 classifications: usize,
34}
35
36impl DeadCodeAuditLedger {
37 #[must_use]
39 pub fn records(&self) -> &[AuditFindingRecord] {
40 &self.records
41 }
42
43 #[must_use]
45 pub const fn keys(&self) -> &FxHashSet<String> {
46 &self.keys
47 }
48
49 #[cfg(test)]
50 const fn classification_count(&self) -> usize {
51 self.classifications
52 }
53
54 #[must_use]
56 pub fn introduced_count(&self) -> usize {
57 self.introduced_keys.len()
58 }
59
60 #[must_use]
62 pub fn visible_count(&self) -> usize {
63 self.records
64 .iter()
65 .filter(|record| record.effective_severity != Severity::Off)
66 .count()
67 }
68
69 #[must_use]
71 pub fn inherited_count(&self) -> usize {
72 self.inherited_keys.len()
73 }
74
75 #[must_use]
77 pub fn has_introduced_errors(&self) -> bool {
78 self.records
79 .iter()
80 .any(|record| record.introduced && record.effective_severity == Severity::Error)
81 }
82
83 #[must_use]
85 pub fn has_introduced_warnings(&self) -> bool {
86 self.records
87 .iter()
88 .any(|record| record.introduced && record.effective_severity == Severity::Warn)
89 }
90
91 #[must_use]
93 pub fn has_errors(&self) -> bool {
94 self.records
95 .iter()
96 .any(|record| record.effective_severity == Severity::Error)
97 }
98
99 #[expect(
104 clippy::too_many_lines,
105 reason = "exhaustive result destructuring and field annotation must stay together so new finding collections require an explicit audit decision"
106 )]
107 pub fn annotate_results(&self, results: &mut fallow_types::results::AnalysisResults) {
108 let fallow_types::results::AnalysisResults {
109 unused_files,
110 unused_exports,
111 unused_types,
112 private_type_leaks,
113 unused_dependencies,
114 unused_dev_dependencies,
115 unused_optional_dependencies,
116 unused_enum_members,
117 unused_class_members,
118 unused_store_members,
119 unresolved_imports,
120 unlisted_dependencies,
121 duplicate_exports,
122 type_only_dependencies,
123 test_only_dependencies,
124 dev_dependencies_in_production,
125 circular_dependencies,
126 re_export_cycles,
127 boundary_violations,
128 boundary_coverage_violations,
129 boundary_call_violations,
130 policy_violations,
131 stale_suppressions: _stale_suppressions,
132 unused_catalog_entries,
133 empty_catalog_groups,
134 unresolved_catalog_references,
135 unused_dependency_overrides,
136 misconfigured_dependency_overrides,
137 invalid_client_exports,
138 mixed_client_server_barrels,
139 misplaced_directives,
140 unprovided_injects,
141 unrendered_components,
142 unused_component_props,
143 unused_component_emits,
144 unused_component_inputs,
145 unused_component_outputs,
146 unused_svelte_events,
147 unused_server_actions,
148 unused_load_data_keys,
149 unused_load_data_keys_global_abstain: _unused_load_data_keys_global_abstain,
150 route_collisions,
151 dynamic_segment_name_conflicts,
152 suppression_count: _suppression_count,
153 unused_component_props_exempted: _unused_component_props_exempted,
154 active_suppressions: _active_suppressions,
155 feature_flags: _feature_flags,
156 security_findings: _security_findings,
157 security_unresolved_edge_files: _security_unresolved_edge_files,
158 security_unresolved_callee_sites: _security_unresolved_callee_sites,
159 security_unresolved_callee_diagnostics: _security_unresolved_callee_diagnostics,
160 prop_drilling_chains: _prop_drilling_chains,
161 thin_wrappers: _thin_wrappers,
162 duplicate_prop_shapes: _duplicate_prop_shapes,
163 export_usages: _export_usages,
164 entry_point_summary: _entry_point_summary,
165 render_fan_in: _render_fan_in,
166 react_component_intel: _react_component_intel,
167 } = results;
168
169 macro_rules! annotate {
170 ($field:ident, $collection:literal) => {
171 for (item, introduced) in $field.iter_mut().zip(
172 self.records
173 .iter()
174 .filter(|record| record.collection == $collection)
175 .map(|record| record.introduced),
176 ) {
177 item.introduced = Some(AuditIntroduced(introduced));
178 }
179 };
180 }
181
182 annotate!(unused_files, "unused_files");
183 annotate!(unused_exports, "unused_exports");
184 annotate!(unused_types, "unused_types");
185 annotate!(private_type_leaks, "private_type_leaks");
186 annotate!(unused_dependencies, "unused_dependencies");
187 annotate!(unused_dev_dependencies, "unused_dev_dependencies");
188 annotate!(unused_optional_dependencies, "unused_optional_dependencies");
189 annotate!(unused_enum_members, "unused_enum_members");
190 annotate!(unused_class_members, "unused_class_members");
191 annotate!(unused_store_members, "unused_store_members");
192 annotate!(unresolved_imports, "unresolved_imports");
193 annotate!(unlisted_dependencies, "unlisted_dependencies");
194 annotate!(duplicate_exports, "duplicate_exports");
195 annotate!(type_only_dependencies, "type_only_dependencies");
196 annotate!(test_only_dependencies, "test_only_dependencies");
197 annotate!(
198 dev_dependencies_in_production,
199 "dev_dependencies_in_production"
200 );
201 annotate!(circular_dependencies, "circular_dependencies");
202 annotate!(re_export_cycles, "re_export_cycles");
203 annotate!(boundary_violations, "boundary_violations");
204 annotate!(boundary_coverage_violations, "boundary_coverage_violations");
205 annotate!(boundary_call_violations, "boundary_call_violations");
206 annotate!(policy_violations, "policy_violations");
207 annotate!(unused_catalog_entries, "unused_catalog_entries");
208 annotate!(empty_catalog_groups, "empty_catalog_groups");
209 annotate!(
210 unresolved_catalog_references,
211 "unresolved_catalog_references"
212 );
213 annotate!(unused_dependency_overrides, "unused_dependency_overrides");
214 annotate!(
215 misconfigured_dependency_overrides,
216 "misconfigured_dependency_overrides"
217 );
218 annotate!(invalid_client_exports, "invalid_client_exports");
219 annotate!(mixed_client_server_barrels, "mixed_client_server_barrels");
220 annotate!(misplaced_directives, "misplaced_directives");
221 annotate!(unprovided_injects, "unprovided_injects");
222 annotate!(unrendered_components, "unrendered_components");
223 annotate!(route_collisions, "route_collisions");
224 annotate!(
225 dynamic_segment_name_conflicts,
226 "dynamic_segment_name_conflicts"
227 );
228 annotate!(unused_component_props, "unused_component_props");
229 annotate!(unused_component_emits, "unused_component_emits");
230 annotate!(unused_component_inputs, "unused_component_inputs");
231 annotate!(unused_component_outputs, "unused_component_outputs");
232 annotate!(unused_svelte_events, "unused_svelte_events");
233 annotate!(unused_server_actions, "unused_server_actions");
234 annotate!(unused_load_data_keys, "unused_load_data_keys");
235 }
236}
237
238#[derive(Debug, Clone, Default)]
240pub struct AuditDomainLedger {
241 records: Vec<(String, bool)>,
242 keys: FxHashSet<String>,
243 introduced_keys: FxHashSet<String>,
244 inherited_keys: FxHashSet<String>,
245}
246
247impl AuditDomainLedger {
248 #[must_use]
250 pub fn compare(
251 keys: impl IntoIterator<Item = String>,
252 base: Option<&FxHashSet<String>>,
253 ) -> Self {
254 let mut records = Vec::new();
255 let mut unique_keys = FxHashSet::default();
256 let mut introduced_keys = FxHashSet::default();
257 let mut inherited_keys = FxHashSet::default();
258 for key in keys {
259 let introduced = base.is_some_and(|base| !base.contains(&key));
260 if base.is_some() {
261 if introduced {
262 introduced_keys.insert(key.clone());
263 } else {
264 inherited_keys.insert(key.clone());
265 }
266 }
267 unique_keys.insert(key.clone());
268 records.push((key, introduced));
269 }
270 Self {
271 records,
272 keys: unique_keys,
273 introduced_keys,
274 inherited_keys,
275 }
276 }
277
278 #[must_use]
280 pub const fn keys(&self) -> &FxHashSet<String> {
281 &self.keys
282 }
283
284 #[must_use]
286 pub fn introduced_count(&self) -> usize {
287 self.introduced_keys.len()
288 }
289
290 #[must_use]
292 pub fn inherited_count(&self) -> usize {
293 self.inherited_keys.len()
294 }
295
296 pub fn introduced(&self) -> impl ExactSizeIterator<Item = bool> + '_ {
298 self.records.iter().map(|(_, introduced)| *introduced)
299 }
300}
301
302#[derive(Debug, Clone, Default)]
304pub struct AuditComparison {
305 pub dead_code: DeadCodeAuditLedger,
307 pub health: AuditDomainLedger,
309 pub dupes: AuditDomainLedger,
311 pub styling: AuditDomainLedger,
313}
314
315pub struct AuditComparisonInput<'a> {
317 pub results: &'a fallow_types::results::AnalysisResults,
318 pub config: &'a ResolvedConfig,
319 pub root: &'a Path,
320 pub health: &'a fallow_output::HealthReport,
321 pub health_root: &'a Path,
322 pub dupe_keys: Vec<String>,
323 pub styling_keys: Vec<String>,
324 pub base_dead_code: Option<&'a FxHashSet<String>>,
325 pub base_health: Option<&'a FxHashSet<String>>,
326 pub base_dupes: Option<&'a FxHashSet<String>>,
327 pub base_styling: Option<&'a FxHashSet<String>>,
328}
329
330impl AuditComparison {
331 #[must_use]
333 pub fn build(input: AuditComparisonInput<'_>) -> Self {
334 Self {
335 dead_code: dead_code_audit_ledger(
336 input.results,
337 input.root,
338 input.config,
339 input.base_dead_code,
340 ),
341 health: AuditDomainLedger::compare(
342 input
343 .health
344 .findings
345 .iter()
346 .map(|finding| health_finding_key(finding, input.health_root)),
347 input.base_health,
348 ),
349 dupes: AuditDomainLedger::compare(input.dupe_keys, input.base_dupes),
350 styling: AuditDomainLedger::compare(input.styling_keys, input.base_styling),
351 }
352 }
353
354 pub fn annotate_typed_findings(
356 &self,
357 results: &mut fallow_types::results::AnalysisResults,
358 health: &mut fallow_output::HealthReport,
359 ) {
360 self.dead_code.annotate_results(results);
361 for (finding, introduced) in health.findings.iter_mut().zip(self.health.introduced()) {
362 finding.introduced = Some(introduced);
363 }
364 }
365}
366
367pub fn relative_key_path(path: &Path, root: &Path) -> String {
368 let simple_path = dunce::simplified(path);
369 let simple_root = dunce::simplified(root);
370 simple_path
371 .strip_prefix(simple_root)
372 .unwrap_or(simple_path)
373 .to_string_lossy()
374 .replace('\\', "/")
375}
376
377fn dependency_location_key(location: &fallow_types::results::DependencyLocation) -> &'static str {
378 match location {
379 fallow_types::results::DependencyLocation::Dependencies => "unused-dependency",
380 fallow_types::results::DependencyLocation::DevDependencies => "unused-dev-dependency",
381 fallow_types::results::DependencyLocation::OptionalDependencies => {
382 "unused-optional-dependency"
383 }
384 }
385}
386
387fn unused_dependency_key(item: &fallow_types::results::UnusedDependency, root: &Path) -> String {
388 format!(
389 "{}:{}:{}",
390 dependency_location_key(&item.location),
391 relative_key_path(&item.path, root),
392 item.package_name
393 )
394}
395
396fn invalid_client_export_key(
397 item: &fallow_types::results::InvalidClientExport,
398 root: &Path,
399) -> String {
400 format!(
401 "invalid-client-export:{}:{}",
402 relative_key_path(&item.path, root),
403 item.export_name
404 )
405}
406
407fn mixed_client_server_barrel_key(
408 item: &fallow_types::results::MixedClientServerBarrel,
409 root: &Path,
410) -> String {
411 format!(
412 "mixed-client-server-barrel:{}:{}:{}",
413 relative_key_path(&item.path, root),
414 item.client_origin,
415 item.server_origin
416 )
417}
418
419fn misplaced_directive_key(
420 item: &fallow_types::results::MisplacedDirective,
421 root: &Path,
422) -> String {
423 format!(
424 "misplaced-directive:{}:{}:{}",
425 relative_key_path(&item.path, root),
426 item.line,
427 item.directive
428 )
429}
430
431fn unprovided_inject_key(item: &fallow_types::results::UnprovidedInject, root: &Path) -> String {
432 format!(
433 "unprovided-inject:{}:{}",
434 relative_key_path(&item.path, root),
435 item.key_name
436 )
437}
438
439fn unrendered_component_key(
440 item: &fallow_types::results::UnrenderedComponent,
441 root: &Path,
442) -> String {
443 format!(
444 "unrendered-component:{}:{}",
445 relative_key_path(&item.path, root),
446 item.component_name
447 )
448}
449
450fn unused_component_prop_key(
451 item: &fallow_types::results::UnusedComponentProp,
452 root: &Path,
453) -> String {
454 format!(
455 "unused-component-prop:{}:{}",
456 relative_key_path(&item.path, root),
457 item.prop_name
458 )
459}
460
461fn unused_component_emit_key(
462 item: &fallow_types::results::UnusedComponentEmit,
463 root: &Path,
464) -> String {
465 format!(
466 "unused-component-emit:{}:{}",
467 relative_key_path(&item.path, root),
468 item.emit_name
469 )
470}
471
472fn unused_component_input_key(
473 item: &fallow_types::results::UnusedComponentInput,
474 root: &Path,
475) -> String {
476 format!(
477 "unused-component-input:{}:{}",
478 relative_key_path(&item.path, root),
479 item.input_name
480 )
481}
482
483fn unused_component_output_key(
484 item: &fallow_types::results::UnusedComponentOutput,
485 root: &Path,
486) -> String {
487 format!(
488 "unused-component-output:{}:{}",
489 relative_key_path(&item.path, root),
490 item.output_name
491 )
492}
493
494fn unused_svelte_event_key(item: &fallow_types::results::UnusedSvelteEvent, root: &Path) -> String {
495 format!(
496 "unused-svelte-event:{}:{}",
497 relative_key_path(&item.path, root),
498 item.event_name
499 )
500}
501
502fn unused_server_action_key(
503 item: &fallow_types::results::UnusedServerAction,
504 root: &Path,
505) -> String {
506 format!(
507 "unused-server-action:{}:{}",
508 relative_key_path(&item.path, root),
509 item.action_name
510 )
511}
512
513fn unused_load_data_key_key(
514 item: &fallow_types::results::UnusedLoadDataKey,
515 root: &Path,
516) -> String {
517 format!(
518 "unused-load-data-key:{}:{}",
519 relative_key_path(&item.path, root),
520 item.key_name
521 )
522}
523
524fn route_collision_key(item: &fallow_types::results::RouteCollision, root: &Path) -> String {
525 format!(
526 "route-collision:{}:{}",
527 relative_key_path(&item.path, root),
528 item.url
529 )
530}
531
532fn dynamic_segment_name_conflict_key(
533 item: &fallow_types::results::DynamicSegmentNameConflict,
534 root: &Path,
535) -> String {
536 format!(
537 "dynamic-segment-name-conflict:{}:{}",
538 relative_key_path(&item.path, root),
539 item.position
540 )
541}
542
543fn unlisted_dependency_key(
544 item: &fallow_types::results::UnlistedDependency,
545 root: &Path,
546) -> String {
547 let mut sites = item
548 .imported_from
549 .iter()
550 .map(|site| {
551 format!(
552 "{}:{}:{}",
553 relative_key_path(&site.path, root),
554 site.line,
555 site.col
556 )
557 })
558 .collect::<Vec<_>>();
559 sites.sort();
560 sites.dedup();
561 format!(
562 "unlisted-dependency:{}:{}",
563 item.package_name,
564 sites.join("|")
565 )
566}
567
568fn unused_member_key(
569 rule_id: &str,
570 item: &fallow_types::results::UnusedMember,
571 root: &Path,
572) -> String {
573 format!(
574 "{}:{}:{}:{}",
575 rule_id,
576 relative_key_path(&item.path, root),
577 item.parent_name,
578 item.member_name
579 )
580}
581
582fn unused_catalog_entry_key(
583 item: &fallow_types::results::UnusedCatalogEntry,
584 root: &Path,
585) -> String {
586 format!(
587 "unused-catalog-entry:{}:{}:{}:{}",
588 relative_key_path(&item.path, root),
589 item.line,
590 item.catalog_name,
591 item.entry_name
592 )
593}
594
595fn empty_catalog_group_key(item: &fallow_types::results::EmptyCatalogGroup, root: &Path) -> String {
596 format!(
597 "empty-catalog-group:{}:{}:{}",
598 relative_key_path(&item.path, root),
599 item.line,
600 item.catalog_name
601 )
602}
603
604fn sorted_relative_path_keys<'a>(
605 paths: impl Iterator<Item = &'a Path>,
606 root: &Path,
607) -> Vec<String> {
608 let mut keys = paths
609 .map(|path| relative_key_path(path, root))
610 .collect::<Vec<_>>();
611 keys.sort();
612 keys
613}
614
615fn duplicate_export_key(
616 item: &fallow_types::output_dead_code::DuplicateExportFinding,
617 root: &Path,
618) -> String {
619 let mut locations = sorted_relative_path_keys(
620 item.export.locations.iter().map(|loc| loc.path.as_path()),
621 root,
622 );
623 locations.dedup();
624 format!(
625 "duplicate-export:{}:{}",
626 item.export.export_name,
627 locations.join("|")
628 )
629}
630
631fn circular_dependency_key(
632 item: &fallow_types::output_dead_code::CircularDependencyFinding,
633 root: &Path,
634) -> String {
635 let files = sorted_relative_path_keys(
636 item.cycle.files.iter().map(std::path::PathBuf::as_path),
637 root,
638 );
639 format!("circular-dependency:{}", files.join("|"))
640}
641
642fn re_export_cycle_key(
643 item: &fallow_types::output_dead_code::ReExportCycleFinding,
644 root: &Path,
645) -> String {
646 let kind = match item.cycle.kind {
647 fallow_types::results::ReExportCycleKind::MultiNode => "multi-node",
648 fallow_types::results::ReExportCycleKind::SelfLoop => "self-loop",
649 };
650 let files = sorted_relative_path_keys(
651 item.cycle.files.iter().map(std::path::PathBuf::as_path),
652 root,
653 );
654 format!("re-export-cycle:{kind}:{}", files.join("|"))
655}
656
657fn boundary_violation_key(
658 item: &fallow_types::output_dead_code::BoundaryViolationFinding,
659 root: &Path,
660) -> String {
661 format!(
662 "boundary-violation:{}:{}:{}",
663 relative_key_path(&item.violation.from_path, root),
664 relative_key_path(&item.violation.to_path, root),
665 item.violation.import_specifier
666 )
667}
668
669fn boundary_coverage_key(
670 item: &fallow_types::output_dead_code::BoundaryCoverageViolationFinding,
671 root: &Path,
672) -> String {
673 format!(
674 "boundary-coverage:{}",
675 relative_key_path(&item.violation.path, root)
676 )
677}
678
679fn boundary_call_key(
680 item: &fallow_types::output_dead_code::BoundaryCallViolationFinding,
681 root: &Path,
682) -> String {
683 format!(
684 "boundary-call:{}:{}",
685 relative_key_path(&item.violation.path, root),
686 item.violation.callee
687 )
688}
689
690fn policy_violation_key(
691 item: &fallow_types::output_dead_code::PolicyViolationFinding,
692 root: &Path,
693) -> String {
694 format!(
695 "policy-violation:{}:{}/{}:{}",
696 relative_key_path(&item.violation.path, root),
697 item.violation.pack,
698 item.violation.rule_id,
699 item.violation.matched
700 )
701}
702
703fn stale_suppression_key(item: &fallow_types::results::StaleSuppression, root: &Path) -> String {
704 let rule_id = if item.missing_reason {
705 "missing-suppression-reason"
706 } else {
707 "stale-suppression"
708 };
709 format!(
710 "{rule_id}:{}:{}",
711 relative_key_path(&item.path, root),
712 item.description()
713 )
714}
715
716fn unresolved_catalog_reference_key(
717 item: &fallow_types::output_dead_code::UnresolvedCatalogReferenceFinding,
718 root: &Path,
719) -> String {
720 format!(
721 "unresolved-catalog-reference:{}:{}:{}:{}",
722 relative_key_path(&item.reference.path, root),
723 item.reference.line,
724 item.reference.catalog_name,
725 item.reference.entry_name
726 )
727}
728
729fn unused_dependency_override_key(
730 item: &fallow_types::output_dead_code::UnusedDependencyOverrideFinding,
731 root: &Path,
732) -> String {
733 format!(
734 "unused-dependency-override:{}:{}:{}",
735 relative_key_path(&item.entry.path, root),
736 item.entry.line,
737 item.entry.raw_key
738 )
739}
740
741fn misconfigured_dependency_override_key(
742 item: &fallow_types::output_dead_code::MisconfiguredDependencyOverrideFinding,
743 root: &Path,
744) -> String {
745 format!(
746 "misconfigured-dependency-override:{}:{}:{}",
747 relative_key_path(&item.entry.path, root),
748 item.entry.line,
749 item.entry.raw_key
750 )
751}
752
753#[derive(Clone, Copy)]
770#[allow(
771 clippy::struct_field_names,
772 reason = "field names mirror the AnalysisResults field names so the destructure stays shorthand"
773)]
774struct DependencyFindingSlices<'a> {
775 unused_dependencies: &'a [fallow_types::output_dead_code::UnusedDependencyFinding],
776 unused_dev_dependencies: &'a [fallow_types::output_dead_code::UnusedDevDependencyFinding],
777 unused_optional_dependencies:
778 &'a [fallow_types::output_dead_code::UnusedOptionalDependencyFinding],
779 unlisted_dependencies: &'a [fallow_types::output_dead_code::UnlistedDependencyFinding],
780 type_only_dependencies: &'a [fallow_types::output_dead_code::TypeOnlyDependencyFinding],
781 test_only_dependencies: &'a [fallow_types::output_dead_code::TestOnlyDependencyFinding],
782 dev_dependencies_in_production:
783 &'a [fallow_types::output_dead_code::DevDependencyInProductionFinding],
784}
785
786#[derive(Clone, Copy)]
789struct FrameworkFindingSlices<'a> {
790 unprovided_injects: &'a [fallow_types::output_dead_code::UnprovidedInjectFinding],
791 unrendered_components: &'a [fallow_types::output_dead_code::UnrenderedComponentFinding],
792 unused_server_actions: &'a [fallow_types::output_dead_code::UnusedServerActionFinding],
793 unused_load_data_keys: &'a [fallow_types::output_dead_code::UnusedLoadDataKeyFinding],
794 route_collisions: &'a [fallow_types::output_dead_code::RouteCollisionFinding],
795 dynamic_segment_name_conflicts:
796 &'a [fallow_types::output_dead_code::DynamicSegmentNameConflictFinding],
797}
798
799pub fn dead_code_keys(
807 results: &fallow_types::results::AnalysisResults,
808 root: &Path,
809) -> FxHashSet<String> {
810 let mut collector = DeadCodeKeyCollector::new(root);
811 collector.add_all_findings(results);
812 collector.into_keys()
813}
814
815#[must_use]
817#[expect(
818 clippy::implicit_hasher,
819 reason = "fallow standardizes on FxHashSet across audit attribution keys"
820)]
821pub fn dead_code_audit_ledger(
822 results: &fallow_types::results::AnalysisResults,
823 root: &Path,
824 config: &ResolvedConfig,
825 base: Option<&FxHashSet<String>>,
826) -> DeadCodeAuditLedger {
827 let mut collector = DeadCodeKeyCollector::for_comparison(root, config, base);
828 collector.add_all_findings(results);
829 collector.into_ledger()
830}
831
832impl DeadCodeKeyCollector<'_> {
833 #[expect(
834 clippy::too_many_lines,
835 reason = "flat field-by-field destructure of the large AnalysisResults struct (with per-field provenance comments) plus straight-line dispatch; length tracks the field count, not branching"
836 )]
837 fn add_all_findings(&mut self, results: &fallow_types::results::AnalysisResults) {
838 let fallow_types::results::AnalysisResults {
839 unused_files,
840 unused_exports,
841 unused_types,
842 private_type_leaks,
843 unused_dependencies,
844 unused_dev_dependencies,
845 unused_optional_dependencies,
846 unused_enum_members,
847 unused_class_members,
848 unused_store_members,
849 unresolved_imports,
850 unlisted_dependencies,
851 duplicate_exports,
852 type_only_dependencies,
853 test_only_dependencies,
854 dev_dependencies_in_production,
855 circular_dependencies,
856 re_export_cycles,
857 boundary_violations,
858 boundary_coverage_violations,
859 boundary_call_violations,
860 policy_violations,
861 stale_suppressions,
862 unused_catalog_entries,
863 empty_catalog_groups,
864 unresolved_catalog_references,
865 unused_dependency_overrides,
866 misconfigured_dependency_overrides,
867 invalid_client_exports,
868 mixed_client_server_barrels,
869 misplaced_directives,
870 unprovided_injects,
871 unrendered_components,
872 unused_component_props,
873 unused_component_emits,
874 unused_component_inputs,
875 unused_component_outputs,
876 unused_svelte_events,
877 unused_server_actions,
878 unused_load_data_keys,
879 unused_load_data_keys_global_abstain: _unused_load_data_keys_global_abstain,
880 route_collisions,
881 dynamic_segment_name_conflicts,
882 suppression_count: _suppression_count,
884 unused_component_props_exempted: _unused_component_props_exempted,
885 active_suppressions: _active_suppressions,
886 feature_flags: _feature_flags,
887 security_findings: _security_findings,
890 security_unresolved_edge_files: _security_unresolved_edge_files,
891 security_unresolved_callee_sites: _security_unresolved_callee_sites,
892 security_unresolved_callee_diagnostics: _security_unresolved_callee_diagnostics,
893 prop_drilling_chains: _prop_drilling_chains,
896 thin_wrappers: _thin_wrappers,
899 duplicate_prop_shapes: _duplicate_prop_shapes,
903 export_usages: _export_usages,
906 entry_point_summary: _entry_point_summary,
907 render_fan_in: _render_fan_in,
910 react_component_intel: _react_component_intel,
913 } = results;
914
915 self.add_core_findings(
916 unused_files,
917 unused_exports,
918 unused_types,
919 private_type_leaks,
920 );
921 self.add_client_directive_findings(
922 invalid_client_exports,
923 mixed_client_server_barrels,
924 misplaced_directives,
925 );
926 self.add_dependency_findings(&DependencyFindingSlices {
927 unused_dependencies,
928 unused_dev_dependencies,
929 unused_optional_dependencies,
930 unlisted_dependencies,
931 type_only_dependencies,
932 test_only_dependencies,
933 dev_dependencies_in_production,
934 });
935 self.add_dependency_override_findings(
936 unused_dependency_overrides,
937 misconfigured_dependency_overrides,
938 );
939 self.add_member_findings(
940 unused_enum_members,
941 unused_class_members,
942 unused_store_members,
943 );
944 self.add_component_contract_findings(
945 unused_component_props,
946 unused_component_emits,
947 unused_component_inputs,
948 unused_component_outputs,
949 unused_svelte_events,
950 );
951 self.add_graph_findings(
952 unresolved_imports,
953 duplicate_exports,
954 circular_dependencies,
955 re_export_cycles,
956 );
957 self.add_boundary_findings(
958 boundary_violations,
959 boundary_coverage_violations,
960 boundary_call_violations,
961 policy_violations,
962 stale_suppressions,
963 );
964 self.add_catalog_findings(
965 unresolved_catalog_references,
966 unused_catalog_entries,
967 empty_catalog_groups,
968 );
969 self.add_framework_findings(&FrameworkFindingSlices {
970 unprovided_injects,
971 unrendered_components,
972 unused_server_actions,
973 unused_load_data_keys,
974 route_collisions,
975 dynamic_segment_name_conflicts,
976 });
977 }
978}
979
980#[derive(Clone, Copy)]
981enum AuditCollection {
982 UnusedFiles,
983 UnusedExports,
984 UnusedTypes,
985 PrivateTypeLeaks,
986 UnusedDependencies,
987 UnusedDevDependencies,
988 UnusedOptionalDependencies,
989 UnusedEnumMembers,
990 UnusedClassMembers,
991 UnusedStoreMembers,
992 UnresolvedImports,
993 UnlistedDependencies,
994 DuplicateExports,
995 TypeOnlyDependencies,
996 TestOnlyDependencies,
997 DevDependenciesInProduction,
998 CircularDependencies,
999 ReExportCycles,
1000 BoundaryViolations,
1001 BoundaryCoverageViolations,
1002 BoundaryCallViolations,
1003 PolicyViolations,
1004 StaleSuppressions,
1005 UnusedCatalogEntries,
1006 EmptyCatalogGroups,
1007 UnresolvedCatalogReferences,
1008 UnusedDependencyOverrides,
1009 MisconfiguredDependencyOverrides,
1010 InvalidClientExports,
1011 MixedClientServerBarrels,
1012 MisplacedDirectives,
1013 UnprovidedInjects,
1014 UnrenderedComponents,
1015 RouteCollisions,
1016 DynamicSegmentNameConflicts,
1017 UnusedComponentProps,
1018 UnusedComponentEmits,
1019 UnusedComponentInputs,
1020 UnusedComponentOutputs,
1021 UnusedSvelteEvents,
1022 UnusedServerActions,
1023 UnusedLoadDataKeys,
1024}
1025
1026impl AuditCollection {
1027 const fn json_key(self) -> &'static str {
1028 match self {
1029 Self::UnusedFiles => "unused_files",
1030 Self::UnusedExports => "unused_exports",
1031 Self::UnusedTypes => "unused_types",
1032 Self::PrivateTypeLeaks => "private_type_leaks",
1033 Self::UnusedDependencies => "unused_dependencies",
1034 Self::UnusedDevDependencies => "unused_dev_dependencies",
1035 Self::UnusedOptionalDependencies => "unused_optional_dependencies",
1036 Self::UnusedEnumMembers => "unused_enum_members",
1037 Self::UnusedClassMembers => "unused_class_members",
1038 Self::UnusedStoreMembers => "unused_store_members",
1039 Self::UnresolvedImports => "unresolved_imports",
1040 Self::UnlistedDependencies => "unlisted_dependencies",
1041 Self::DuplicateExports => "duplicate_exports",
1042 Self::TypeOnlyDependencies => "type_only_dependencies",
1043 Self::TestOnlyDependencies => "test_only_dependencies",
1044 Self::DevDependenciesInProduction => "dev_dependencies_in_production",
1045 Self::CircularDependencies => "circular_dependencies",
1046 Self::ReExportCycles => "re_export_cycles",
1047 Self::BoundaryViolations => "boundary_violations",
1048 Self::BoundaryCoverageViolations => "boundary_coverage_violations",
1049 Self::BoundaryCallViolations => "boundary_call_violations",
1050 Self::PolicyViolations => "policy_violations",
1051 Self::StaleSuppressions => "stale_suppressions",
1052 Self::UnusedCatalogEntries => "unused_catalog_entries",
1053 Self::EmptyCatalogGroups => "empty_catalog_groups",
1054 Self::UnresolvedCatalogReferences => "unresolved_catalog_references",
1055 Self::UnusedDependencyOverrides => "unused_dependency_overrides",
1056 Self::MisconfiguredDependencyOverrides => "misconfigured_dependency_overrides",
1057 Self::InvalidClientExports => "invalid_client_exports",
1058 Self::MixedClientServerBarrels => "mixed_client_server_barrels",
1059 Self::MisplacedDirectives => "misplaced_directives",
1060 Self::UnprovidedInjects => "unprovided_injects",
1061 Self::UnrenderedComponents => "unrendered_components",
1062 Self::RouteCollisions => "route_collisions",
1063 Self::DynamicSegmentNameConflicts => "dynamic_segment_name_conflicts",
1064 Self::UnusedComponentProps => "unused_component_props",
1065 Self::UnusedComponentEmits => "unused_component_emits",
1066 Self::UnusedComponentInputs => "unused_component_inputs",
1067 Self::UnusedComponentOutputs => "unused_component_outputs",
1068 Self::UnusedSvelteEvents => "unused_svelte_events",
1069 Self::UnusedServerActions => "unused_server_actions",
1070 Self::UnusedLoadDataKeys => "unused_load_data_keys",
1071 }
1072 }
1073}
1074
1075struct DeadCodeKeyCollector<'a> {
1076 root: &'a Path,
1077 keys: FxHashSet<String>,
1078 introduced_keys: FxHashSet<String>,
1079 inherited_keys: FxHashSet<String>,
1080 records: Vec<AuditFindingRecord>,
1081 collection_counts: FxHashMap<&'static str, usize>,
1082 config: Option<&'a ResolvedConfig>,
1083 base: Option<&'a FxHashSet<String>>,
1084 #[cfg(test)]
1085 classifications: usize,
1086}
1087
1088impl<'a> DeadCodeKeyCollector<'a> {
1089 fn new(root: &'a Path) -> Self {
1090 Self {
1091 root,
1092 keys: FxHashSet::default(),
1093 introduced_keys: FxHashSet::default(),
1094 inherited_keys: FxHashSet::default(),
1095 records: Vec::new(),
1096 collection_counts: FxHashMap::default(),
1097 config: None,
1098 base: None,
1099 #[cfg(test)]
1100 classifications: 0,
1101 }
1102 }
1103
1104 fn for_comparison(
1105 root: &'a Path,
1106 config: &'a ResolvedConfig,
1107 base: Option<&'a FxHashSet<String>>,
1108 ) -> Self {
1109 Self {
1110 root,
1111 keys: FxHashSet::default(),
1112 introduced_keys: FxHashSet::default(),
1113 inherited_keys: FxHashSet::default(),
1114 records: Vec::new(),
1115 collection_counts: FxHashMap::default(),
1116 config: Some(config),
1117 base,
1118 #[cfg(test)]
1119 classifications: 0,
1120 }
1121 }
1122
1123 fn into_keys(self) -> FxHashSet<String> {
1124 self.keys
1125 }
1126
1127 fn into_ledger(self) -> DeadCodeAuditLedger {
1128 DeadCodeAuditLedger {
1129 records: self.records,
1130 keys: self.keys,
1131 introduced_keys: self.introduced_keys,
1132 inherited_keys: self.inherited_keys,
1133 #[cfg(test)]
1134 classifications: self.classifications,
1135 }
1136 }
1137
1138 fn insert(&mut self, collection: AuditCollection, key: String, effective_severity: Severity) {
1139 let collection = collection.json_key();
1140 let ordinal = self.collection_counts.entry(collection).or_default();
1141 if self.config.is_some() {
1142 #[cfg(test)]
1143 {
1144 self.classifications += 1;
1145 }
1146 let introduced = self.base.is_some_and(|base| !base.contains(&key));
1147 if effective_severity != Severity::Off && self.base.is_some() {
1148 if introduced {
1149 self.introduced_keys.insert(key.clone());
1150 } else {
1151 self.inherited_keys.insert(key.clone());
1152 }
1153 }
1154 self.records.push(AuditFindingRecord {
1155 collection,
1156 ordinal: *ordinal,
1157 introduced,
1158 effective_severity,
1159 stable_key: key.clone(),
1160 });
1161 }
1162 *ordinal += 1;
1163 self.keys.insert(key);
1164 }
1165
1166 fn insert_file(
1167 &mut self,
1168 collection: AuditCollection,
1169 key: String,
1170 path: &Path,
1171 severity: fn(&fallow_config::RulesConfig) -> Severity,
1172 ) {
1173 let effective = self.config.map_or(Severity::Off, |config| {
1174 severity(&config.resolve_rules_for_path(path))
1175 });
1176 self.insert(collection, key, effective);
1177 }
1178
1179 fn insert_project(
1180 &mut self,
1181 collection: AuditCollection,
1182 key: String,
1183 severity: fn(&fallow_config::RulesConfig) -> Severity,
1184 ) {
1185 let effective = self
1186 .config
1187 .map_or(Severity::Off, |config| severity(&config.rules));
1188 self.insert(collection, key, effective);
1189 }
1190
1191 fn add_core_findings(
1192 &mut self,
1193 unused_files: &[fallow_types::output_dead_code::UnusedFileFinding],
1194 unused_exports: &[fallow_types::output_dead_code::UnusedExportFinding],
1195 unused_types: &[fallow_types::output_dead_code::UnusedTypeFinding],
1196 private_type_leaks: &[fallow_types::output_dead_code::PrivateTypeLeakFinding],
1197 ) {
1198 self.add_unused_files(unused_files);
1199 self.add_unused_exports(unused_exports);
1200 self.add_unused_types(unused_types);
1201 self.add_private_type_leaks(private_type_leaks);
1202 }
1203
1204 fn add_client_directive_findings(
1205 &mut self,
1206 invalid_client_exports: &[fallow_types::output_dead_code::InvalidClientExportFinding],
1207 mixed_client_server_barrels: &[fallow_types::output_dead_code::MixedClientServerBarrelFinding],
1208 misplaced_directives: &[fallow_types::output_dead_code::MisplacedDirectiveFinding],
1209 ) {
1210 self.add_invalid_client_exports(invalid_client_exports);
1211 self.add_mixed_client_server_barrels(mixed_client_server_barrels);
1212 self.add_misplaced_directives(misplaced_directives);
1213 }
1214
1215 fn add_dependency_findings(&mut self, deps: &DependencyFindingSlices<'_>) {
1216 let DependencyFindingSlices {
1217 unused_dependencies,
1218 unused_dev_dependencies,
1219 unused_optional_dependencies,
1220 unlisted_dependencies,
1221 type_only_dependencies,
1222 test_only_dependencies,
1223 dev_dependencies_in_production,
1224 } = *deps;
1225 self.add_unused_dependencies(unused_dependencies);
1226 self.add_unused_dev_dependencies(unused_dev_dependencies);
1227 self.add_unused_optional_dependencies(unused_optional_dependencies);
1228 self.add_unlisted_dependencies(unlisted_dependencies);
1229 self.add_type_only_dependencies(type_only_dependencies);
1230 self.add_test_only_dependencies(test_only_dependencies);
1231 self.add_dev_dependencies_in_production(dev_dependencies_in_production);
1232 }
1233
1234 fn add_dependency_override_findings(
1235 &mut self,
1236 unused_dependency_overrides: &[fallow_types::output_dead_code::UnusedDependencyOverrideFinding],
1237 misconfigured_dependency_overrides: &[fallow_types::output_dead_code::MisconfiguredDependencyOverrideFinding],
1238 ) {
1239 self.add_unused_dependency_overrides(unused_dependency_overrides);
1240 self.add_misconfigured_dependency_overrides(misconfigured_dependency_overrides);
1241 }
1242
1243 fn add_member_findings(
1244 &mut self,
1245 unused_enum_members: &[fallow_types::output_dead_code::UnusedEnumMemberFinding],
1246 unused_class_members: &[fallow_types::output_dead_code::UnusedClassMemberFinding],
1247 unused_store_members: &[fallow_types::output_dead_code::UnusedStoreMemberFinding],
1248 ) {
1249 self.add_unused_enum_members(unused_enum_members);
1250 self.add_unused_class_members(unused_class_members);
1251 self.add_unused_store_members(unused_store_members);
1252 }
1253
1254 fn add_component_contract_findings(
1255 &mut self,
1256 unused_component_props: &[fallow_types::output_dead_code::UnusedComponentPropFinding],
1257 unused_component_emits: &[fallow_types::output_dead_code::UnusedComponentEmitFinding],
1258 unused_component_inputs: &[fallow_types::output_dead_code::UnusedComponentInputFinding],
1259 unused_component_outputs: &[fallow_types::output_dead_code::UnusedComponentOutputFinding],
1260 unused_svelte_events: &[fallow_types::output_dead_code::UnusedSvelteEventFinding],
1261 ) {
1262 self.add_unused_component_props(unused_component_props);
1263 self.add_unused_component_emits(unused_component_emits);
1264 self.add_unused_component_inputs(unused_component_inputs);
1265 self.add_unused_component_outputs(unused_component_outputs);
1266 self.add_unused_svelte_events(unused_svelte_events);
1267 }
1268
1269 fn add_graph_findings(
1270 &mut self,
1271 unresolved_imports: &[fallow_types::output_dead_code::UnresolvedImportFinding],
1272 duplicate_exports: &[fallow_types::output_dead_code::DuplicateExportFinding],
1273 circular_dependencies: &[fallow_types::output_dead_code::CircularDependencyFinding],
1274 re_export_cycles: &[fallow_types::output_dead_code::ReExportCycleFinding],
1275 ) {
1276 self.add_unresolved_imports(unresolved_imports);
1277 self.add_duplicate_exports(duplicate_exports);
1278 self.add_circular_dependencies(circular_dependencies);
1279 self.add_re_export_cycles(re_export_cycles);
1280 }
1281
1282 fn add_boundary_findings(
1283 &mut self,
1284 boundary_violations: &[fallow_types::output_dead_code::BoundaryViolationFinding],
1285 boundary_coverage_violations: &[fallow_types::output_dead_code::BoundaryCoverageViolationFinding],
1286 boundary_call_violations: &[fallow_types::output_dead_code::BoundaryCallViolationFinding],
1287 policy_violations: &[fallow_types::output_dead_code::PolicyViolationFinding],
1288 stale_suppressions: &[fallow_types::results::StaleSuppression],
1289 ) {
1290 self.add_boundary_violations(boundary_violations);
1291 self.add_boundary_coverage_violations(boundary_coverage_violations);
1292 self.add_boundary_call_violations(boundary_call_violations);
1293 self.add_policy_violations(policy_violations);
1294 self.add_stale_suppressions(stale_suppressions);
1295 }
1296
1297 fn add_catalog_findings(
1298 &mut self,
1299 unresolved_catalog_references: &[fallow_types::output_dead_code::UnresolvedCatalogReferenceFinding],
1300 unused_catalog_entries: &[fallow_types::output_dead_code::UnusedCatalogEntryFinding],
1301 empty_catalog_groups: &[fallow_types::output_dead_code::EmptyCatalogGroupFinding],
1302 ) {
1303 self.add_unresolved_catalog_references(unresolved_catalog_references);
1304 self.add_unused_catalog_entries(unused_catalog_entries);
1305 self.add_empty_catalog_groups(empty_catalog_groups);
1306 }
1307
1308 fn add_framework_findings(&mut self, framework: &FrameworkFindingSlices<'_>) {
1309 let FrameworkFindingSlices {
1310 unprovided_injects,
1311 unrendered_components,
1312 unused_server_actions,
1313 unused_load_data_keys,
1314 route_collisions,
1315 dynamic_segment_name_conflicts,
1316 } = *framework;
1317 self.add_unprovided_injects(unprovided_injects);
1318 self.add_unrendered_components(unrendered_components);
1319 self.add_unused_server_actions(unused_server_actions);
1320 self.add_unused_load_data_keys(unused_load_data_keys);
1321 self.add_route_collisions(route_collisions);
1322 self.add_dynamic_segment_name_conflicts(dynamic_segment_name_conflicts);
1323 }
1324
1325 fn add_unused_files(&mut self, items: &[fallow_types::output_dead_code::UnusedFileFinding]) {
1326 for item in items {
1327 self.insert_file(
1328 AuditCollection::UnusedFiles,
1329 format!(
1330 "unused-file:{}",
1331 relative_key_path(&item.file.path, self.root)
1332 ),
1333 &item.file.path,
1334 |rules| rules.unused_files,
1335 );
1336 }
1337 }
1338
1339 fn add_unused_exports(
1340 &mut self,
1341 items: &[fallow_types::output_dead_code::UnusedExportFinding],
1342 ) {
1343 for item in items {
1344 self.insert_file(
1345 AuditCollection::UnusedExports,
1346 format!(
1347 "unused-export:{}:{}",
1348 relative_key_path(&item.export.path, self.root),
1349 item.export.export_name
1350 ),
1351 &item.export.path,
1352 |rules| rules.unused_exports,
1353 );
1354 }
1355 }
1356
1357 fn add_unused_types(&mut self, items: &[fallow_types::output_dead_code::UnusedTypeFinding]) {
1358 for item in items {
1359 self.insert_file(
1360 AuditCollection::UnusedTypes,
1361 format!(
1362 "unused-type:{}:{}",
1363 relative_key_path(&item.export.path, self.root),
1364 item.export.export_name
1365 ),
1366 &item.export.path,
1367 |rules| rules.unused_types,
1368 );
1369 }
1370 }
1371
1372 fn add_private_type_leaks(
1373 &mut self,
1374 items: &[fallow_types::output_dead_code::PrivateTypeLeakFinding],
1375 ) {
1376 for item in items {
1377 self.insert_file(
1378 AuditCollection::PrivateTypeLeaks,
1379 format!(
1380 "private-type-leak:{}:{}:{}",
1381 relative_key_path(&item.leak.path, self.root),
1382 item.leak.export_name,
1383 item.leak.type_name
1384 ),
1385 &item.leak.path,
1386 |rules| rules.private_type_leaks,
1387 );
1388 }
1389 }
1390
1391 fn add_invalid_client_exports(
1392 &mut self,
1393 items: &[fallow_types::output_dead_code::InvalidClientExportFinding],
1394 ) {
1395 for item in items {
1396 self.insert_file(
1397 AuditCollection::InvalidClientExports,
1398 invalid_client_export_key(&item.export, self.root),
1399 &item.export.path,
1400 |rules| rules.invalid_client_export,
1401 );
1402 }
1403 }
1404
1405 fn add_mixed_client_server_barrels(
1406 &mut self,
1407 items: &[fallow_types::output_dead_code::MixedClientServerBarrelFinding],
1408 ) {
1409 for item in items {
1410 self.insert_file(
1411 AuditCollection::MixedClientServerBarrels,
1412 mixed_client_server_barrel_key(&item.barrel, self.root),
1413 &item.barrel.path,
1414 |rules| rules.mixed_client_server_barrel,
1415 );
1416 }
1417 }
1418
1419 fn add_misplaced_directives(
1420 &mut self,
1421 items: &[fallow_types::output_dead_code::MisplacedDirectiveFinding],
1422 ) {
1423 for item in items {
1424 self.insert_file(
1425 AuditCollection::MisplacedDirectives,
1426 misplaced_directive_key(&item.directive_site, self.root),
1427 &item.directive_site.path,
1428 |rules| rules.misplaced_directive,
1429 );
1430 }
1431 }
1432
1433 fn add_unprovided_injects(
1434 &mut self,
1435 items: &[fallow_types::output_dead_code::UnprovidedInjectFinding],
1436 ) {
1437 for item in items {
1438 self.insert_file(
1439 AuditCollection::UnprovidedInjects,
1440 unprovided_inject_key(&item.inject, self.root),
1441 &item.inject.path,
1442 |rules| rules.unprovided_injects,
1443 );
1444 }
1445 }
1446
1447 fn add_unrendered_components(
1448 &mut self,
1449 items: &[fallow_types::output_dead_code::UnrenderedComponentFinding],
1450 ) {
1451 for item in items {
1452 self.insert_file(
1453 AuditCollection::UnrenderedComponents,
1454 unrendered_component_key(&item.component, self.root),
1455 &item.component.path,
1456 |rules| rules.unrendered_components,
1457 );
1458 }
1459 }
1460
1461 fn add_unused_component_props(
1462 &mut self,
1463 items: &[fallow_types::output_dead_code::UnusedComponentPropFinding],
1464 ) {
1465 for item in items {
1466 self.insert_file(
1467 AuditCollection::UnusedComponentProps,
1468 unused_component_prop_key(&item.prop, self.root),
1469 &item.prop.path,
1470 |rules| rules.unused_component_props,
1471 );
1472 }
1473 }
1474
1475 fn add_unused_component_emits(
1476 &mut self,
1477 items: &[fallow_types::output_dead_code::UnusedComponentEmitFinding],
1478 ) {
1479 for item in items {
1480 self.insert_file(
1481 AuditCollection::UnusedComponentEmits,
1482 unused_component_emit_key(&item.emit, self.root),
1483 &item.emit.path,
1484 |rules| rules.unused_component_emits,
1485 );
1486 }
1487 }
1488
1489 fn add_unused_component_inputs(
1490 &mut self,
1491 items: &[fallow_types::output_dead_code::UnusedComponentInputFinding],
1492 ) {
1493 for item in items {
1494 self.insert_file(
1495 AuditCollection::UnusedComponentInputs,
1496 unused_component_input_key(&item.input, self.root),
1497 &item.input.path,
1498 |rules| rules.unused_component_inputs,
1499 );
1500 }
1501 }
1502
1503 fn add_unused_component_outputs(
1504 &mut self,
1505 items: &[fallow_types::output_dead_code::UnusedComponentOutputFinding],
1506 ) {
1507 for item in items {
1508 self.insert_file(
1509 AuditCollection::UnusedComponentOutputs,
1510 unused_component_output_key(&item.output, self.root),
1511 &item.output.path,
1512 |rules| rules.unused_component_outputs,
1513 );
1514 }
1515 }
1516
1517 fn add_unused_svelte_events(
1518 &mut self,
1519 items: &[fallow_types::output_dead_code::UnusedSvelteEventFinding],
1520 ) {
1521 for item in items {
1522 self.insert_file(
1523 AuditCollection::UnusedSvelteEvents,
1524 unused_svelte_event_key(&item.event, self.root),
1525 &item.event.path,
1526 |rules| rules.unused_svelte_events,
1527 );
1528 }
1529 }
1530
1531 fn add_unused_server_actions(
1532 &mut self,
1533 items: &[fallow_types::output_dead_code::UnusedServerActionFinding],
1534 ) {
1535 for item in items {
1536 self.insert_file(
1537 AuditCollection::UnusedServerActions,
1538 unused_server_action_key(&item.action, self.root),
1539 &item.action.path,
1540 |rules| rules.unused_server_actions,
1541 );
1542 }
1543 }
1544
1545 fn add_unused_load_data_keys(
1546 &mut self,
1547 items: &[fallow_types::output_dead_code::UnusedLoadDataKeyFinding],
1548 ) {
1549 for item in items {
1550 self.insert_file(
1551 AuditCollection::UnusedLoadDataKeys,
1552 unused_load_data_key_key(&item.key, self.root),
1553 &item.key.path,
1554 |rules| rules.unused_load_data_keys,
1555 );
1556 }
1557 }
1558
1559 fn add_route_collisions(
1560 &mut self,
1561 items: &[fallow_types::output_dead_code::RouteCollisionFinding],
1562 ) {
1563 for item in items {
1564 self.insert_file(
1565 AuditCollection::RouteCollisions,
1566 route_collision_key(&item.collision, self.root),
1567 &item.collision.path,
1568 |rules| rules.route_collision,
1569 );
1570 }
1571 }
1572
1573 fn add_dynamic_segment_name_conflicts(
1574 &mut self,
1575 items: &[fallow_types::output_dead_code::DynamicSegmentNameConflictFinding],
1576 ) {
1577 for item in items {
1578 self.insert_file(
1579 AuditCollection::DynamicSegmentNameConflicts,
1580 dynamic_segment_name_conflict_key(&item.conflict, self.root),
1581 &item.conflict.path,
1582 |rules| rules.dynamic_segment_name_conflict,
1583 );
1584 }
1585 }
1586
1587 fn add_unused_dependencies(
1588 &mut self,
1589 items: &[fallow_types::output_dead_code::UnusedDependencyFinding],
1590 ) {
1591 for item in items {
1592 self.insert_project(
1593 AuditCollection::UnusedDependencies,
1594 unused_dependency_key(&item.dep, self.root),
1595 |rules| rules.unused_dependencies,
1596 );
1597 }
1598 }
1599
1600 fn add_unused_dev_dependencies(
1601 &mut self,
1602 items: &[fallow_types::output_dead_code::UnusedDevDependencyFinding],
1603 ) {
1604 for item in items {
1605 self.insert_project(
1606 AuditCollection::UnusedDevDependencies,
1607 unused_dependency_key(&item.dep, self.root),
1608 |rules| rules.unused_dev_dependencies,
1609 );
1610 }
1611 }
1612
1613 fn add_unused_optional_dependencies(
1614 &mut self,
1615 items: &[fallow_types::output_dead_code::UnusedOptionalDependencyFinding],
1616 ) {
1617 for item in items {
1618 self.insert_project(
1619 AuditCollection::UnusedOptionalDependencies,
1620 unused_dependency_key(&item.dep, self.root),
1621 |rules| rules.unused_optional_dependencies,
1622 );
1623 }
1624 }
1625
1626 fn add_unused_enum_members(
1627 &mut self,
1628 items: &[fallow_types::output_dead_code::UnusedEnumMemberFinding],
1629 ) {
1630 for item in items {
1631 self.insert_file(
1632 AuditCollection::UnusedEnumMembers,
1633 unused_member_key("unused-enum-member", &item.member, self.root),
1634 &item.member.path,
1635 |rules| rules.unused_enum_members,
1636 );
1637 }
1638 }
1639
1640 fn add_unused_class_members(
1641 &mut self,
1642 items: &[fallow_types::output_dead_code::UnusedClassMemberFinding],
1643 ) {
1644 for item in items {
1645 self.insert_file(
1646 AuditCollection::UnusedClassMembers,
1647 unused_member_key("unused-class-member", &item.member, self.root),
1648 &item.member.path,
1649 |rules| rules.unused_class_members,
1650 );
1651 }
1652 }
1653
1654 fn add_unused_store_members(
1655 &mut self,
1656 items: &[fallow_types::output_dead_code::UnusedStoreMemberFinding],
1657 ) {
1658 for item in items {
1659 self.insert_file(
1660 AuditCollection::UnusedStoreMembers,
1661 unused_member_key("unused-store-member", &item.member, self.root),
1662 &item.member.path,
1663 |rules| rules.unused_store_members,
1664 );
1665 }
1666 }
1667
1668 fn add_unresolved_imports(
1669 &mut self,
1670 items: &[fallow_types::output_dead_code::UnresolvedImportFinding],
1671 ) {
1672 for item in items {
1673 self.insert_file(
1674 AuditCollection::UnresolvedImports,
1675 format!(
1676 "unresolved-import:{}:{}",
1677 relative_key_path(&item.import.path, self.root),
1678 item.import.specifier
1679 ),
1680 &item.import.path,
1681 |rules| rules.unresolved_imports,
1682 );
1683 }
1684 }
1685
1686 fn add_unlisted_dependencies(
1687 &mut self,
1688 items: &[fallow_types::output_dead_code::UnlistedDependencyFinding],
1689 ) {
1690 for item in items {
1691 self.insert_project(
1692 AuditCollection::UnlistedDependencies,
1693 unlisted_dependency_key(&item.dep, self.root),
1694 |rules| rules.unlisted_dependencies,
1695 );
1696 }
1697 }
1698
1699 fn add_duplicate_exports(
1700 &mut self,
1701 items: &[fallow_types::output_dead_code::DuplicateExportFinding],
1702 ) {
1703 for item in items {
1704 self.insert_project(
1705 AuditCollection::DuplicateExports,
1706 duplicate_export_key(item, self.root),
1707 |rules| rules.duplicate_exports,
1708 );
1709 }
1710 }
1711
1712 fn add_type_only_dependencies(
1713 &mut self,
1714 items: &[fallow_types::output_dead_code::TypeOnlyDependencyFinding],
1715 ) {
1716 for item in items {
1717 self.insert_project(
1718 AuditCollection::TypeOnlyDependencies,
1719 format!(
1720 "type-only-dependency:{}:{}",
1721 relative_key_path(&item.dep.path, self.root),
1722 item.dep.package_name
1723 ),
1724 |rules| rules.type_only_dependencies,
1725 );
1726 }
1727 }
1728
1729 fn add_test_only_dependencies(
1730 &mut self,
1731 items: &[fallow_types::output_dead_code::TestOnlyDependencyFinding],
1732 ) {
1733 for item in items {
1734 self.insert_project(
1735 AuditCollection::TestOnlyDependencies,
1736 format!(
1737 "test-only-dependency:{}:{}",
1738 relative_key_path(&item.dep.path, self.root),
1739 item.dep.package_name
1740 ),
1741 |rules| rules.test_only_dependencies,
1742 );
1743 }
1744 }
1745
1746 fn add_dev_dependencies_in_production(
1747 &mut self,
1748 items: &[fallow_types::output_dead_code::DevDependencyInProductionFinding],
1749 ) {
1750 for item in items {
1751 self.insert_project(
1752 AuditCollection::DevDependenciesInProduction,
1753 format!(
1754 "dev-dependency-in-production:{}:{}",
1755 relative_key_path(&item.dep.path, self.root),
1756 item.dep.package_name
1757 ),
1758 |rules| rules.dev_dependencies_in_production,
1759 );
1760 }
1761 }
1762
1763 fn add_circular_dependencies(
1764 &mut self,
1765 items: &[fallow_types::output_dead_code::CircularDependencyFinding],
1766 ) {
1767 for item in items {
1768 let severity = self.config.map_or(Severity::Off, |config| {
1769 item.cycle
1770 .files
1771 .iter()
1772 .fold(Severity::Off, |current, path| {
1773 merge_severity(
1774 current,
1775 config.resolve_rules_for_path(path).circular_dependencies,
1776 )
1777 })
1778 });
1779 self.insert(
1780 AuditCollection::CircularDependencies,
1781 circular_dependency_key(item, self.root),
1782 severity,
1783 );
1784 }
1785 }
1786
1787 fn add_re_export_cycles(
1788 &mut self,
1789 items: &[fallow_types::output_dead_code::ReExportCycleFinding],
1790 ) {
1791 for item in items {
1792 self.insert_project(
1793 AuditCollection::ReExportCycles,
1794 re_export_cycle_key(item, self.root),
1795 |rules| rules.re_export_cycle,
1796 );
1797 }
1798 }
1799
1800 fn add_boundary_violations(
1801 &mut self,
1802 items: &[fallow_types::output_dead_code::BoundaryViolationFinding],
1803 ) {
1804 for item in items {
1805 self.insert_file(
1806 AuditCollection::BoundaryViolations,
1807 boundary_violation_key(item, self.root),
1808 &item.violation.from_path,
1809 |rules| rules.boundary_violation,
1810 );
1811 }
1812 }
1813
1814 fn add_boundary_coverage_violations(
1815 &mut self,
1816 items: &[fallow_types::output_dead_code::BoundaryCoverageViolationFinding],
1817 ) {
1818 for item in items {
1819 self.insert_file(
1820 AuditCollection::BoundaryCoverageViolations,
1821 boundary_coverage_key(item, self.root),
1822 &item.violation.path,
1823 |rules| rules.boundary_violation,
1824 );
1825 }
1826 }
1827
1828 fn add_boundary_call_violations(
1829 &mut self,
1830 items: &[fallow_types::output_dead_code::BoundaryCallViolationFinding],
1831 ) {
1832 for item in items {
1833 self.insert_file(
1834 AuditCollection::BoundaryCallViolations,
1835 boundary_call_key(item, self.root),
1836 &item.violation.path,
1837 |rules| rules.boundary_violation,
1838 );
1839 }
1840 }
1841
1842 fn add_policy_violations(
1843 &mut self,
1844 items: &[fallow_types::output_dead_code::PolicyViolationFinding],
1845 ) {
1846 for item in items {
1847 let severity = match item.violation.severity {
1848 fallow_types::results::PolicyViolationSeverity::Error => Severity::Error,
1849 fallow_types::results::PolicyViolationSeverity::Warn => Severity::Warn,
1850 };
1851 self.insert(
1852 AuditCollection::PolicyViolations,
1853 policy_violation_key(item, self.root),
1854 severity,
1855 );
1856 }
1857 }
1858
1859 fn add_stale_suppressions(&mut self, items: &[fallow_types::results::StaleSuppression]) {
1860 for item in items {
1861 let effective = self.config.map_or(Severity::Off, |config| {
1862 let rules = config.resolve_rules_for_path(&item.path);
1863 if item.missing_reason {
1864 rules.require_suppression_reason
1865 } else {
1866 rules.stale_suppressions
1867 }
1868 });
1869 self.insert(
1870 AuditCollection::StaleSuppressions,
1871 stale_suppression_key(item, self.root),
1872 effective,
1873 );
1874 }
1875 }
1876
1877 fn add_unresolved_catalog_references(
1878 &mut self,
1879 items: &[fallow_types::output_dead_code::UnresolvedCatalogReferenceFinding],
1880 ) {
1881 for item in items {
1882 self.insert_file(
1883 AuditCollection::UnresolvedCatalogReferences,
1884 unresolved_catalog_reference_key(item, self.root),
1885 &item.reference.path,
1886 |rules| rules.unresolved_catalog_references,
1887 );
1888 }
1889 }
1890
1891 fn add_unused_catalog_entries(
1892 &mut self,
1893 items: &[fallow_types::output_dead_code::UnusedCatalogEntryFinding],
1894 ) {
1895 for item in items {
1896 self.insert_project(
1897 AuditCollection::UnusedCatalogEntries,
1898 unused_catalog_entry_key(&item.entry, self.root),
1899 |rules| rules.unused_catalog_entries,
1900 );
1901 }
1902 }
1903
1904 fn add_empty_catalog_groups(
1905 &mut self,
1906 items: &[fallow_types::output_dead_code::EmptyCatalogGroupFinding],
1907 ) {
1908 for item in items {
1909 self.insert_file(
1910 AuditCollection::EmptyCatalogGroups,
1911 empty_catalog_group_key(&item.group, self.root),
1912 &item.group.path,
1913 |rules| rules.empty_catalog_groups,
1914 );
1915 }
1916 }
1917
1918 fn add_unused_dependency_overrides(
1919 &mut self,
1920 items: &[fallow_types::output_dead_code::UnusedDependencyOverrideFinding],
1921 ) {
1922 for item in items {
1923 self.insert_file(
1924 AuditCollection::UnusedDependencyOverrides,
1925 unused_dependency_override_key(item, self.root),
1926 &item.entry.path,
1927 |rules| rules.unused_dependency_overrides,
1928 );
1929 }
1930 }
1931
1932 fn add_misconfigured_dependency_overrides(
1933 &mut self,
1934 items: &[fallow_types::output_dead_code::MisconfiguredDependencyOverrideFinding],
1935 ) {
1936 for item in items {
1937 self.insert_file(
1938 AuditCollection::MisconfiguredDependencyOverrides,
1939 misconfigured_dependency_override_key(item, self.root),
1940 &item.entry.path,
1941 |rules| rules.misconfigured_dependency_overrides,
1942 );
1943 }
1944 }
1945}
1946
1947const fn merge_severity(left: Severity, right: Severity) -> Severity {
1948 match (left, right) {
1949 (Severity::Error, _) | (_, Severity::Error) => Severity::Error,
1950 (Severity::Warn, _) | (_, Severity::Warn) => Severity::Warn,
1951 (Severity::Off, Severity::Off) => Severity::Off,
1952 }
1953}
1954
1955#[expect(
1975 clippy::implicit_hasher,
1976 reason = "fallow standardizes on FxHashSet across audit attribution keys"
1977)]
1978pub fn retain_introduced_dead_code(
1979 results: &mut fallow_types::results::AnalysisResults,
1980 root: &Path,
1981 base: Option<&FxHashSet<String>>,
1982) {
1983 let Some(base) = base else {
1984 return;
1985 };
1986
1987 let introduced = introduced_dead_code_keys(results, root, base);
1995 classify_introduced_dead_code_fields(results);
1996
1997 retain_introduced_fast_paths(
2002 &mut results.unused_files,
2003 &mut results.unused_exports,
2004 &mut results.unused_types,
2005 root,
2006 base,
2007 );
2008 retain_introduced_core_findings(results, root, &introduced);
2009 retain_introduced_dependency_and_graph_findings(results, root, &introduced);
2010 retain_introduced_workspace_findings(results, root, &introduced);
2011 retain_introduced_framework_findings(results, root, &introduced);
2012}
2013
2014fn introduced_dead_code_keys(
2015 results: &fallow_types::results::AnalysisResults,
2016 root: &Path,
2017 base: &FxHashSet<String>,
2018) -> FxHashSet<String> {
2019 dead_code_keys(results, root)
2020 .into_iter()
2021 .filter(|key| !base.contains(key))
2022 .collect()
2023}
2024
2025fn classify_introduced_dead_code_fields(results: &fallow_types::results::AnalysisResults) {
2026 let fallow_types::results::AnalysisResults {
2027 unused_files: _unused_files,
2028 unused_exports: _unused_exports,
2029 unused_types: _unused_types,
2030 private_type_leaks: _private_type_leaks,
2031 unused_dependencies: _unused_dependencies,
2032 unused_dev_dependencies: _unused_dev_dependencies,
2033 unused_optional_dependencies: _unused_optional_dependencies,
2034 unused_enum_members: _unused_enum_members,
2035 unused_class_members: _unused_class_members,
2036 unused_store_members: _unused_store_members,
2037 unresolved_imports: _unresolved_imports,
2038 unlisted_dependencies: _unlisted_dependencies,
2039 duplicate_exports: _duplicate_exports,
2040 type_only_dependencies: _type_only_dependencies,
2041 test_only_dependencies: _test_only_dependencies,
2042 dev_dependencies_in_production: _dev_dependencies_in_production,
2043 circular_dependencies: _circular_dependencies,
2044 re_export_cycles: _re_export_cycles,
2045 boundary_violations: _boundary_violations,
2046 boundary_coverage_violations: _boundary_coverage_violations,
2047 boundary_call_violations: _boundary_call_violations,
2048 policy_violations: _policy_violations,
2049 stale_suppressions: _stale_suppressions,
2050 unused_catalog_entries: _unused_catalog_entries,
2051 empty_catalog_groups: _empty_catalog_groups,
2052 unresolved_catalog_references: _unresolved_catalog_references,
2053 unused_dependency_overrides: _unused_dependency_overrides,
2054 misconfigured_dependency_overrides: _misconfigured_dependency_overrides,
2055 invalid_client_exports: _invalid_client_exports,
2056 mixed_client_server_barrels: _mixed_client_server_barrels,
2057 misplaced_directives: _misplaced_directives,
2058 unprovided_injects: _unprovided_injects,
2059 unrendered_components: _unrendered_components,
2060 unused_component_props: _unused_component_props,
2061 unused_component_emits: _unused_component_emits,
2062 unused_component_inputs: _unused_component_inputs,
2063 unused_component_outputs: _unused_component_outputs,
2064 unused_svelte_events: _unused_svelte_events,
2065 unused_server_actions: _unused_server_actions,
2066 unused_load_data_keys: _unused_load_data_keys,
2067 unused_load_data_keys_global_abstain: _unused_load_data_keys_global_abstain,
2068 route_collisions: _route_collisions,
2069 dynamic_segment_name_conflicts: _dynamic_segment_name_conflicts,
2070 suppression_count: _suppression_count,
2073 unused_component_props_exempted: _unused_component_props_exempted,
2074 active_suppressions: _active_suppressions,
2075 feature_flags: _feature_flags,
2076 security_findings: _security_findings,
2080 security_unresolved_edge_files: _security_unresolved_edge_files,
2081 security_unresolved_callee_sites: _security_unresolved_callee_sites,
2082 security_unresolved_callee_diagnostics: _security_unresolved_callee_diagnostics,
2083 prop_drilling_chains: _prop_drilling_chains,
2086 thin_wrappers: _thin_wrappers,
2089 duplicate_prop_shapes: _duplicate_prop_shapes,
2092 export_usages: _export_usages,
2095 entry_point_summary: _entry_point_summary,
2096 render_fan_in: _render_fan_in,
2099 react_component_intel: _react_component_intel,
2102 } = results;
2103}
2104
2105fn retain_introduced_fast_paths(
2106 unused_files: &mut Vec<fallow_types::output_dead_code::UnusedFileFinding>,
2107 unused_exports: &mut Vec<fallow_types::output_dead_code::UnusedExportFinding>,
2108 unused_types: &mut Vec<fallow_types::output_dead_code::UnusedTypeFinding>,
2109 root: &Path,
2110 base: &FxHashSet<String>,
2111) {
2112 unused_files.retain(|item| {
2113 !base.contains(&format!(
2114 "unused-file:{}",
2115 relative_key_path(&item.file.path, root)
2116 ))
2117 });
2118 unused_exports.retain(|item| {
2119 !base.contains(&format!(
2120 "unused-export:{}:{}",
2121 relative_key_path(&item.export.path, root),
2122 item.export.export_name
2123 ))
2124 });
2125 unused_types.retain(|item| {
2126 !base.contains(&format!(
2127 "unused-type:{}:{}",
2128 relative_key_path(&item.export.path, root),
2129 item.export.export_name
2130 ))
2131 });
2132}
2133
2134fn keep_introduced(introduced: &FxHashSet<String>, key: impl AsRef<str>) -> bool {
2135 introduced.contains(key.as_ref())
2136}
2137
2138fn retain_introduced_core_findings(
2139 results: &mut fallow_types::results::AnalysisResults,
2140 root: &Path,
2141 introduced: &FxHashSet<String>,
2142) {
2143 results.private_type_leaks.retain(|item| {
2144 keep_introduced(
2145 introduced,
2146 format!(
2147 "private-type-leak:{}:{}:{}",
2148 relative_key_path(&item.leak.path, root),
2149 item.leak.export_name,
2150 item.leak.type_name
2151 ),
2152 )
2153 });
2154 results.unused_enum_members.retain(|item| {
2155 keep_introduced(
2156 introduced,
2157 unused_member_key("unused-enum-member", &item.member, root),
2158 )
2159 });
2160 results.unused_class_members.retain(|item| {
2161 keep_introduced(
2162 introduced,
2163 unused_member_key("unused-class-member", &item.member, root),
2164 )
2165 });
2166 results.unused_store_members.retain(|item| {
2167 keep_introduced(
2168 introduced,
2169 unused_member_key("unused-store-member", &item.member, root),
2170 )
2171 });
2172 results.unresolved_imports.retain(|item| {
2173 keep_introduced(
2174 introduced,
2175 format!(
2176 "unresolved-import:{}:{}",
2177 relative_key_path(&item.import.path, root),
2178 item.import.specifier
2179 ),
2180 )
2181 });
2182}
2183
2184fn retain_introduced_dependency_and_graph_findings(
2185 results: &mut fallow_types::results::AnalysisResults,
2186 root: &Path,
2187 introduced: &FxHashSet<String>,
2188) {
2189 results
2190 .unused_dependencies
2191 .retain(|item| keep_introduced(introduced, unused_dependency_key(&item.dep, root)));
2192 results
2193 .unused_dev_dependencies
2194 .retain(|item| keep_introduced(introduced, unused_dependency_key(&item.dep, root)));
2195 results
2196 .unused_optional_dependencies
2197 .retain(|item| keep_introduced(introduced, unused_dependency_key(&item.dep, root)));
2198 results
2199 .unlisted_dependencies
2200 .retain(|item| keep_introduced(introduced, unlisted_dependency_key(&item.dep, root)));
2201 results
2202 .duplicate_exports
2203 .retain(|item| keep_introduced(introduced, duplicate_export_key(item, root)));
2204 results.type_only_dependencies.retain(|item| {
2205 keep_introduced(
2206 introduced,
2207 format!(
2208 "type-only-dependency:{}:{}",
2209 relative_key_path(&item.dep.path, root),
2210 item.dep.package_name
2211 ),
2212 )
2213 });
2214 results.test_only_dependencies.retain(|item| {
2215 keep_introduced(
2216 introduced,
2217 format!(
2218 "test-only-dependency:{}:{}",
2219 relative_key_path(&item.dep.path, root),
2220 item.dep.package_name
2221 ),
2222 )
2223 });
2224 results
2225 .circular_dependencies
2226 .retain(|item| keep_introduced(introduced, circular_dependency_key(item, root)));
2227 results
2228 .re_export_cycles
2229 .retain(|item| keep_introduced(introduced, re_export_cycle_key(item, root)));
2230 results
2231 .boundary_violations
2232 .retain(|item| keep_introduced(introduced, boundary_violation_key(item, root)));
2233 results
2234 .boundary_coverage_violations
2235 .retain(|item| keep_introduced(introduced, boundary_coverage_key(item, root)));
2236 results
2237 .boundary_call_violations
2238 .retain(|item| keep_introduced(introduced, boundary_call_key(item, root)));
2239 results
2240 .policy_violations
2241 .retain(|item| keep_introduced(introduced, policy_violation_key(item, root)));
2242 results
2243 .stale_suppressions
2244 .retain(|item| keep_introduced(introduced, stale_suppression_key(item, root)));
2245}
2246
2247fn retain_introduced_workspace_findings(
2248 results: &mut fallow_types::results::AnalysisResults,
2249 root: &Path,
2250 introduced: &FxHashSet<String>,
2251) {
2252 results
2253 .unresolved_catalog_references
2254 .retain(|item| keep_introduced(introduced, unresolved_catalog_reference_key(item, root)));
2255 results
2256 .unused_catalog_entries
2257 .retain(|item| keep_introduced(introduced, unused_catalog_entry_key(&item.entry, root)));
2258 results
2259 .empty_catalog_groups
2260 .retain(|item| keep_introduced(introduced, empty_catalog_group_key(&item.group, root)));
2261 results
2262 .unused_dependency_overrides
2263 .retain(|item| keep_introduced(introduced, unused_dependency_override_key(item, root)));
2264 results.misconfigured_dependency_overrides.retain(|item| {
2265 keep_introduced(
2266 introduced,
2267 misconfigured_dependency_override_key(item, root),
2268 )
2269 });
2270}
2271
2272fn retain_introduced_framework_findings(
2273 results: &mut fallow_types::results::AnalysisResults,
2274 root: &Path,
2275 introduced: &FxHashSet<String>,
2276) {
2277 results
2278 .invalid_client_exports
2279 .retain(|item| keep_introduced(introduced, invalid_client_export_key(&item.export, root)));
2280 results.mixed_client_server_barrels.retain(|item| {
2281 keep_introduced(
2282 introduced,
2283 mixed_client_server_barrel_key(&item.barrel, root),
2284 )
2285 });
2286 results.misplaced_directives.retain(|item| {
2287 keep_introduced(
2288 introduced,
2289 misplaced_directive_key(&item.directive_site, root),
2290 )
2291 });
2292 results
2293 .unprovided_injects
2294 .retain(|item| keep_introduced(introduced, unprovided_inject_key(&item.inject, root)));
2295 results.unrendered_components.retain(|item| {
2296 keep_introduced(introduced, unrendered_component_key(&item.component, root))
2297 });
2298 results
2299 .unused_component_props
2300 .retain(|item| keep_introduced(introduced, unused_component_prop_key(&item.prop, root)));
2301 results
2302 .unused_component_emits
2303 .retain(|item| keep_introduced(introduced, unused_component_emit_key(&item.emit, root)));
2304 results
2305 .unused_component_inputs
2306 .retain(|item| keep_introduced(introduced, unused_component_input_key(&item.input, root)));
2307 results.unused_component_outputs.retain(|item| {
2308 keep_introduced(introduced, unused_component_output_key(&item.output, root))
2309 });
2310 results
2311 .unused_svelte_events
2312 .retain(|item| keep_introduced(introduced, unused_svelte_event_key(&item.event, root)));
2313 results
2314 .unused_server_actions
2315 .retain(|item| keep_introduced(introduced, unused_server_action_key(&item.action, root)));
2316 results
2317 .unused_load_data_keys
2318 .retain(|item| keep_introduced(introduced, unused_load_data_key_key(&item.key, root)));
2319 results
2320 .route_collisions
2321 .retain(|item| keep_introduced(introduced, route_collision_key(&item.collision, root)));
2322 results.dynamic_segment_name_conflicts.retain(|item| {
2323 keep_introduced(
2324 introduced,
2325 dynamic_segment_name_conflict_key(&item.conflict, root),
2326 )
2327 });
2328}
2329
2330fn issue_was_introduced(key: &str, base: &FxHashSet<String>) -> bool {
2331 !base.contains(key)
2332}
2333
2334fn annotate_issue_array<I>(json: &mut serde_json::Value, key: &str, introduced: I)
2335where
2336 I: IntoIterator<Item = bool>,
2337{
2338 let Some(items) = json.get_mut(key).and_then(serde_json::Value::as_array_mut) else {
2339 return;
2340 };
2341 for (item, introduced) in items.iter_mut().zip(introduced) {
2342 if let serde_json::Value::Object(map) = item {
2343 map.insert("introduced".to_string(), serde_json::json!(introduced));
2344 }
2345 }
2346}
2347
2348#[expect(
2349 clippy::implicit_hasher,
2350 reason = "fallow standardizes on FxHashSet across audit attribution keys"
2351)]
2352pub fn annotate_dead_code_json(
2353 json: &mut serde_json::Value,
2354 results: &fallow_types::results::AnalysisResults,
2355 root: &Path,
2356 base: &FxHashSet<String>,
2357) {
2358 let mut annotator = DeadCodeJsonAnnotator {
2359 json,
2360 results,
2361 root,
2362 base,
2363 };
2364 annotator.annotate_file_symbols();
2365 annotator.annotate_dependencies();
2366 annotator.annotate_members();
2367 annotator.annotate_imports_and_exports();
2368 annotator.annotate_graph();
2369 annotator.annotate_catalog();
2370}
2371
2372#[expect(
2376 clippy::implicit_hasher,
2377 reason = "fallow standardizes on FxHashSet across audit attribution keys"
2378)]
2379pub fn annotate_stale_suppressions_json(
2380 json: &mut serde_json::Value,
2381 results: &fallow_types::results::AnalysisResults,
2382 root: &Path,
2383 base: &FxHashSet<String>,
2384) {
2385 annotate_issue_array(
2386 json,
2387 "stale_suppressions",
2388 results
2389 .stale_suppressions
2390 .iter()
2391 .map(|item| issue_was_introduced(&stale_suppression_key(item, root), base)),
2392 );
2393}
2394
2395struct DeadCodeJsonAnnotator<'a> {
2396 json: &'a mut serde_json::Value,
2397 results: &'a fallow_types::results::AnalysisResults,
2398 root: &'a Path,
2399 base: &'a FxHashSet<String>,
2400}
2401
2402impl DeadCodeJsonAnnotator<'_> {
2403 fn annotate_file_symbols(&mut self) {
2404 annotate_issue_array(
2405 self.json,
2406 "unused_files",
2407 self.results.unused_files.iter().map(|item| {
2408 issue_was_introduced(
2409 &format!(
2410 "unused-file:{}",
2411 relative_key_path(&item.file.path, self.root)
2412 ),
2413 self.base,
2414 )
2415 }),
2416 );
2417 annotate_issue_array(
2418 self.json,
2419 "unused_exports",
2420 self.results.unused_exports.iter().map(|item| {
2421 issue_was_introduced(
2422 &format!(
2423 "unused-export:{}:{}",
2424 relative_key_path(&item.export.path, self.root),
2425 item.export.export_name
2426 ),
2427 self.base,
2428 )
2429 }),
2430 );
2431 annotate_issue_array(
2432 self.json,
2433 "unused_types",
2434 self.results.unused_types.iter().map(|item| {
2435 issue_was_introduced(
2436 &format!(
2437 "unused-type:{}:{}",
2438 relative_key_path(&item.export.path, self.root),
2439 item.export.export_name
2440 ),
2441 self.base,
2442 )
2443 }),
2444 );
2445 annotate_issue_array(
2446 self.json,
2447 "private_type_leaks",
2448 self.results.private_type_leaks.iter().map(|item| {
2449 issue_was_introduced(
2450 &format!(
2451 "private-type-leak:{}:{}:{}",
2452 relative_key_path(&item.leak.path, self.root),
2453 item.leak.export_name,
2454 item.leak.type_name
2455 ),
2456 self.base,
2457 )
2458 }),
2459 );
2460 }
2461
2462 fn annotate_dependencies(&mut self) {
2463 annotate_dependency_json(self.json, self.results, self.root, self.base);
2464 annotate_issue_array(
2465 self.json,
2466 "type_only_dependencies",
2467 self.results.type_only_dependencies.iter().map(|item| {
2468 issue_was_introduced(
2469 &format!(
2470 "type-only-dependency:{}:{}",
2471 relative_key_path(&item.dep.path, self.root),
2472 item.dep.package_name
2473 ),
2474 self.base,
2475 )
2476 }),
2477 );
2478 annotate_issue_array(
2479 self.json,
2480 "test_only_dependencies",
2481 self.results.test_only_dependencies.iter().map(|item| {
2482 issue_was_introduced(
2483 &format!(
2484 "test-only-dependency:{}:{}",
2485 relative_key_path(&item.dep.path, self.root),
2486 item.dep.package_name
2487 ),
2488 self.base,
2489 )
2490 }),
2491 );
2492 }
2493
2494 fn annotate_members(&mut self) {
2495 annotate_member_json(self.json, self.results, self.root, self.base);
2496 }
2497
2498 fn annotate_imports_and_exports(&mut self) {
2499 self.annotate_import_dependency_keys();
2500 self.annotate_framework_keys();
2501 self.annotate_component_keys();
2502 self.annotate_route_keys();
2503 }
2504
2505 fn annotate_import_dependency_keys(&mut self) {
2506 annotate_issue_array(
2507 self.json,
2508 "unresolved_imports",
2509 self.results.unresolved_imports.iter().map(|item| {
2510 issue_was_introduced(
2511 &format!(
2512 "unresolved-import:{}:{}",
2513 relative_key_path(&item.import.path, self.root),
2514 item.import.specifier
2515 ),
2516 self.base,
2517 )
2518 }),
2519 );
2520 annotate_issue_array(
2521 self.json,
2522 "unlisted_dependencies",
2523 self.results.unlisted_dependencies.iter().map(|item| {
2524 issue_was_introduced(&unlisted_dependency_key(&item.dep, self.root), self.base)
2525 }),
2526 );
2527 annotate_issue_array(
2528 self.json,
2529 "duplicate_exports",
2530 self.results.duplicate_exports.iter().map(|item| {
2531 let mut locations: Vec<String> = item
2532 .export
2533 .locations
2534 .iter()
2535 .map(|loc| relative_key_path(&loc.path, self.root))
2536 .collect();
2537 locations.sort();
2538 locations.dedup();
2539 issue_was_introduced(
2540 &format!(
2541 "duplicate-export:{}:{}",
2542 item.export.export_name,
2543 locations.join("|")
2544 ),
2545 self.base,
2546 )
2547 }),
2548 );
2549 }
2550
2551 fn annotate_framework_keys(&mut self) {
2552 annotate_issue_array(
2553 self.json,
2554 "invalid_client_exports",
2555 self.results.invalid_client_exports.iter().map(|item| {
2556 issue_was_introduced(
2557 &invalid_client_export_key(&item.export, self.root),
2558 self.base,
2559 )
2560 }),
2561 );
2562 annotate_issue_array(
2563 self.json,
2564 "mixed_client_server_barrels",
2565 self.results.mixed_client_server_barrels.iter().map(|item| {
2566 issue_was_introduced(
2567 &mixed_client_server_barrel_key(&item.barrel, self.root),
2568 self.base,
2569 )
2570 }),
2571 );
2572 annotate_issue_array(
2573 self.json,
2574 "misplaced_directives",
2575 self.results.misplaced_directives.iter().map(|item| {
2576 issue_was_introduced(
2577 &misplaced_directive_key(&item.directive_site, self.root),
2578 self.base,
2579 )
2580 }),
2581 );
2582 annotate_issue_array(
2583 self.json,
2584 "unprovided_injects",
2585 self.results.unprovided_injects.iter().map(|item| {
2586 issue_was_introduced(&unprovided_inject_key(&item.inject, self.root), self.base)
2587 }),
2588 );
2589 }
2590
2591 fn annotate_component_keys(&mut self) {
2592 self.annotate_component_render_keys();
2593 self.annotate_component_io_keys();
2594 }
2595
2596 fn annotate_component_render_keys(&mut self) {
2598 annotate_issue_array(
2599 self.json,
2600 "unrendered_components",
2601 self.results.unrendered_components.iter().map(|item| {
2602 issue_was_introduced(
2603 &unrendered_component_key(&item.component, self.root),
2604 self.base,
2605 )
2606 }),
2607 );
2608 annotate_issue_array(
2609 self.json,
2610 "unused_component_props",
2611 self.results.unused_component_props.iter().map(|item| {
2612 issue_was_introduced(&unused_component_prop_key(&item.prop, self.root), self.base)
2613 }),
2614 );
2615 annotate_issue_array(
2616 self.json,
2617 "unused_component_emits",
2618 self.results.unused_component_emits.iter().map(|item| {
2619 issue_was_introduced(&unused_component_emit_key(&item.emit, self.root), self.base)
2620 }),
2621 );
2622 }
2623
2624 fn annotate_component_io_keys(&mut self) {
2626 annotate_issue_array(
2627 self.json,
2628 "unused_component_inputs",
2629 self.results.unused_component_inputs.iter().map(|item| {
2630 issue_was_introduced(
2631 &unused_component_input_key(&item.input, self.root),
2632 self.base,
2633 )
2634 }),
2635 );
2636 annotate_issue_array(
2637 self.json,
2638 "unused_component_outputs",
2639 self.results.unused_component_outputs.iter().map(|item| {
2640 issue_was_introduced(
2641 &unused_component_output_key(&item.output, self.root),
2642 self.base,
2643 )
2644 }),
2645 );
2646 annotate_issue_array(
2647 self.json,
2648 "unused_svelte_events",
2649 self.results.unused_svelte_events.iter().map(|item| {
2650 issue_was_introduced(&unused_svelte_event_key(&item.event, self.root), self.base)
2651 }),
2652 );
2653 annotate_issue_array(
2654 self.json,
2655 "unused_server_actions",
2656 self.results.unused_server_actions.iter().map(|item| {
2657 issue_was_introduced(
2658 &unused_server_action_key(&item.action, self.root),
2659 self.base,
2660 )
2661 }),
2662 );
2663 }
2664
2665 fn annotate_route_keys(&mut self) {
2666 annotate_issue_array(
2667 self.json,
2668 "route_collisions",
2669 self.results.route_collisions.iter().map(|item| {
2670 issue_was_introduced(&route_collision_key(&item.collision, self.root), self.base)
2671 }),
2672 );
2673 annotate_issue_array(
2674 self.json,
2675 "dynamic_segment_name_conflicts",
2676 self.results
2677 .dynamic_segment_name_conflicts
2678 .iter()
2679 .map(|item| {
2680 issue_was_introduced(
2681 &dynamic_segment_name_conflict_key(&item.conflict, self.root),
2682 self.base,
2683 )
2684 }),
2685 );
2686 }
2687
2688 fn annotate_graph(&mut self) {
2689 annotate_graph_json(self.json, self.results, self.root, self.base);
2690 }
2691
2692 fn annotate_catalog(&mut self) {
2693 annotate_catalog_json(self.json, self.results, self.root, self.base);
2694 }
2695}
2696
2697fn annotate_dependency_json(
2698 json: &mut serde_json::Value,
2699 results: &fallow_types::results::AnalysisResults,
2700 root: &Path,
2701 base: &FxHashSet<String>,
2702) {
2703 annotate_issue_array(
2704 json,
2705 "unused_dependencies",
2706 results
2707 .unused_dependencies
2708 .iter()
2709 .map(|item| issue_was_introduced(&unused_dependency_key(&item.dep, root), base)),
2710 );
2711 annotate_issue_array(
2712 json,
2713 "unused_dev_dependencies",
2714 results
2715 .unused_dev_dependencies
2716 .iter()
2717 .map(|item| issue_was_introduced(&unused_dependency_key(&item.dep, root), base)),
2718 );
2719 annotate_issue_array(
2720 json,
2721 "unused_optional_dependencies",
2722 results
2723 .unused_optional_dependencies
2724 .iter()
2725 .map(|item| issue_was_introduced(&unused_dependency_key(&item.dep, root), base)),
2726 );
2727}
2728
2729fn annotate_member_json(
2730 json: &mut serde_json::Value,
2731 results: &fallow_types::results::AnalysisResults,
2732 root: &Path,
2733 base: &FxHashSet<String>,
2734) {
2735 annotate_issue_array(
2736 json,
2737 "unused_enum_members",
2738 results.unused_enum_members.iter().map(|item| {
2739 issue_was_introduced(
2740 &unused_member_key("unused-enum-member", &item.member, root),
2741 base,
2742 )
2743 }),
2744 );
2745 annotate_issue_array(
2746 json,
2747 "unused_class_members",
2748 results.unused_class_members.iter().map(|item| {
2749 issue_was_introduced(
2750 &unused_member_key("unused-class-member", &item.member, root),
2751 base,
2752 )
2753 }),
2754 );
2755 annotate_issue_array(
2756 json,
2757 "unused_store_members",
2758 results.unused_store_members.iter().map(|item| {
2759 issue_was_introduced(
2760 &unused_member_key("unused-store-member", &item.member, root),
2761 base,
2762 )
2763 }),
2764 );
2765}
2766
2767fn annotate_graph_json(
2768 json: &mut serde_json::Value,
2769 results: &fallow_types::results::AnalysisResults,
2770 root: &Path,
2771 base: &FxHashSet<String>,
2772) {
2773 annotate_cycle_json(json, results, root, base);
2774 annotate_boundary_json(json, results, root, base);
2775 annotate_policy_json(json, results, root, base);
2776}
2777
2778fn annotate_cycle_json(
2779 json: &mut serde_json::Value,
2780 results: &fallow_types::results::AnalysisResults,
2781 root: &Path,
2782 base: &FxHashSet<String>,
2783) {
2784 annotate_issue_array(
2785 json,
2786 "circular_dependencies",
2787 results.circular_dependencies.iter().map(|item| {
2788 let mut files: Vec<String> = item
2789 .cycle
2790 .files
2791 .iter()
2792 .map(|path| relative_key_path(path, root))
2793 .collect();
2794 files.sort();
2795 issue_was_introduced(&format!("circular-dependency:{}", files.join("|")), base)
2796 }),
2797 );
2798 annotate_issue_array(
2799 json,
2800 "re_export_cycles",
2801 results.re_export_cycles.iter().map(|item| {
2802 let kind = match item.cycle.kind {
2803 fallow_types::results::ReExportCycleKind::MultiNode => "multi-node",
2804 fallow_types::results::ReExportCycleKind::SelfLoop => "self-loop",
2805 };
2806 let mut files: Vec<String> = item
2807 .cycle
2808 .files
2809 .iter()
2810 .map(|path| relative_key_path(path, root))
2811 .collect();
2812 files.sort();
2813 issue_was_introduced(&format!("re-export-cycle:{kind}:{}", files.join("|")), base)
2814 }),
2815 );
2816}
2817
2818fn annotate_boundary_json(
2819 json: &mut serde_json::Value,
2820 results: &fallow_types::results::AnalysisResults,
2821 root: &Path,
2822 base: &FxHashSet<String>,
2823) {
2824 annotate_issue_array(
2825 json,
2826 "boundary_violations",
2827 results.boundary_violations.iter().map(|item| {
2828 issue_was_introduced(
2829 &format!(
2830 "boundary-violation:{}:{}:{}",
2831 relative_key_path(&item.violation.from_path, root),
2832 relative_key_path(&item.violation.to_path, root),
2833 item.violation.import_specifier
2834 ),
2835 base,
2836 )
2837 }),
2838 );
2839 annotate_issue_array(
2840 json,
2841 "boundary_coverage_violations",
2842 results.boundary_coverage_violations.iter().map(|item| {
2843 issue_was_introduced(
2844 &format!(
2845 "boundary-coverage:{}",
2846 relative_key_path(&item.violation.path, root)
2847 ),
2848 base,
2849 )
2850 }),
2851 );
2852 annotate_issue_array(
2853 json,
2854 "boundary_call_violations",
2855 results.boundary_call_violations.iter().map(|item| {
2856 issue_was_introduced(
2857 &format!(
2858 "boundary-call:{}:{}",
2859 relative_key_path(&item.violation.path, root),
2860 item.violation.callee
2861 ),
2862 base,
2863 )
2864 }),
2865 );
2866}
2867
2868fn annotate_policy_json(
2869 json: &mut serde_json::Value,
2870 results: &fallow_types::results::AnalysisResults,
2871 root: &Path,
2872 base: &FxHashSet<String>,
2873) {
2874 annotate_issue_array(
2875 json,
2876 "policy_violations",
2877 results.policy_violations.iter().map(|item| {
2878 issue_was_introduced(
2879 &format!(
2880 "policy-violation:{}:{}/{}:{}",
2881 relative_key_path(&item.violation.path, root),
2882 item.violation.pack,
2883 item.violation.rule_id,
2884 item.violation.matched
2885 ),
2886 base,
2887 )
2888 }),
2889 );
2890 annotate_issue_array(
2891 json,
2892 "stale_suppressions",
2893 results
2894 .stale_suppressions
2895 .iter()
2896 .map(|item| issue_was_introduced(&stale_suppression_key(item, root), base)),
2897 );
2898}
2899
2900fn annotate_catalog_json(
2901 json: &mut serde_json::Value,
2902 results: &fallow_types::results::AnalysisResults,
2903 root: &Path,
2904 base: &FxHashSet<String>,
2905) {
2906 annotate_catalog_entry_json(json, results, root, base);
2907 annotate_dependency_override_json(json, results, root, base);
2908}
2909
2910fn annotate_catalog_entry_json(
2912 json: &mut serde_json::Value,
2913 results: &fallow_types::results::AnalysisResults,
2914 root: &Path,
2915 base: &FxHashSet<String>,
2916) {
2917 annotate_issue_array(
2918 json,
2919 "unresolved_catalog_references",
2920 results.unresolved_catalog_references.iter().map(|item| {
2921 issue_was_introduced(
2922 &format!(
2923 "unresolved-catalog-reference:{}:{}:{}:{}",
2924 relative_key_path(&item.reference.path, root),
2925 item.reference.line,
2926 item.reference.catalog_name,
2927 item.reference.entry_name
2928 ),
2929 base,
2930 )
2931 }),
2932 );
2933 annotate_issue_array(
2934 json,
2935 "unused_catalog_entries",
2936 results
2937 .unused_catalog_entries
2938 .iter()
2939 .map(|item| issue_was_introduced(&unused_catalog_entry_key(&item.entry, root), base)),
2940 );
2941 annotate_issue_array(
2942 json,
2943 "empty_catalog_groups",
2944 results
2945 .empty_catalog_groups
2946 .iter()
2947 .map(|item| issue_was_introduced(&empty_catalog_group_key(&item.group, root), base)),
2948 );
2949}
2950
2951fn annotate_dependency_override_json(
2953 json: &mut serde_json::Value,
2954 results: &fallow_types::results::AnalysisResults,
2955 root: &Path,
2956 base: &FxHashSet<String>,
2957) {
2958 annotate_issue_array(
2959 json,
2960 "unused_dependency_overrides",
2961 results.unused_dependency_overrides.iter().map(|item| {
2962 issue_was_introduced(
2963 &format!(
2964 "unused-dependency-override:{}:{}:{}",
2965 relative_key_path(&item.entry.path, root),
2966 item.entry.line,
2967 item.entry.raw_key
2968 ),
2969 base,
2970 )
2971 }),
2972 );
2973 annotate_issue_array(
2974 json,
2975 "misconfigured_dependency_overrides",
2976 results
2977 .misconfigured_dependency_overrides
2978 .iter()
2979 .map(|item| {
2980 issue_was_introduced(
2981 &format!(
2982 "misconfigured-dependency-override:{}:{}:{}",
2983 relative_key_path(&item.entry.path, root),
2984 item.entry.line,
2985 item.entry.raw_key
2986 ),
2987 base,
2988 )
2989 }),
2990 );
2991}
2992
2993#[expect(
2994 clippy::implicit_hasher,
2995 reason = "fallow standardizes on FxHashSet across audit attribution keys"
2996)]
2997pub fn annotate_health_json(
2998 json: &mut serde_json::Value,
2999 report: &fallow_output::HealthReport,
3000 root: &Path,
3001 base: &FxHashSet<String>,
3002) {
3003 if let Some(items) = json
3004 .get_mut("findings")
3005 .and_then(serde_json::Value::as_array_mut)
3006 {
3007 for (item, finding) in items.iter_mut().zip(&report.findings) {
3008 if let serde_json::Value::Object(map) = item {
3009 map.insert(
3010 "introduced".to_string(),
3011 serde_json::json!(issue_was_introduced(
3012 &health_finding_key(finding, root),
3013 base
3014 )),
3015 );
3016 }
3017 }
3018 }
3019 if let Some(items) = json
3020 .get_mut("styling_findings")
3021 .and_then(serde_json::Value::as_array_mut)
3022 {
3023 for (item, finding) in items.iter_mut().zip(&report.styling_findings) {
3024 if let serde_json::Value::Object(map) = item {
3025 map.insert(
3026 "introduced".to_string(),
3027 serde_json::json!(issue_was_introduced(
3028 &styling_finding_key(finding, root),
3029 base
3030 )),
3031 );
3032 }
3033 }
3034 }
3035}
3036
3037#[expect(
3038 clippy::implicit_hasher,
3039 reason = "fallow standardizes on FxHashSet across audit attribution keys"
3040)]
3041pub fn annotate_dupes_json(
3042 json: &mut serde_json::Value,
3043 report: &fallow_types::duplicates::DuplicationReport,
3044 root: &Path,
3045 base: &FxHashSet<String>,
3046) {
3047 let Some(items) = json
3048 .get_mut("clone_groups")
3049 .and_then(serde_json::Value::as_array_mut)
3050 else {
3051 return;
3052 };
3053 for (item, group) in items.iter_mut().zip(&report.clone_groups) {
3054 if let serde_json::Value::Object(map) = item {
3055 map.insert(
3056 "introduced".to_string(),
3057 serde_json::json!(issue_was_introduced(&dupe_group_key(group, root), base)),
3058 );
3059 }
3060 }
3061}
3062
3063pub fn annotate_domain_json(
3065 json: &mut serde_json::Value,
3066 collection: &str,
3067 introduced: impl IntoIterator<Item = bool>,
3068) {
3069 annotate_issue_array(json, collection, introduced);
3070}
3071
3072pub fn health_keys(report: &fallow_output::HealthReport, root: &Path) -> FxHashSet<String> {
3073 report
3074 .findings
3075 .iter()
3076 .map(|finding| health_finding_key(finding, root))
3077 .collect()
3078}
3079
3080pub fn health_finding_key(finding: &fallow_output::ComplexityViolation, root: &Path) -> String {
3081 format!(
3082 "complexity:{}:{}:{:?}",
3083 relative_key_path(Path::new(&finding.path), root),
3084 finding.name,
3085 finding.exceeded
3086 )
3087}
3088
3089pub fn styling_keys(report: &fallow_output::HealthReport, root: &Path) -> FxHashSet<String> {
3090 report
3091 .styling_findings
3092 .iter()
3093 .map(|finding| styling_finding_key(finding, root))
3094 .collect()
3095}
3096
3097pub fn styling_finding_key(finding: &fallow_output::StylingFinding, root: &Path) -> String {
3098 format!(
3099 "styling:{}:{}:{}:{}:{}",
3100 finding.code,
3101 finding.sub_kind,
3102 relative_key_path(Path::new(&finding.path), root),
3103 finding.line,
3104 finding.value
3105 )
3106}
3107
3108pub fn dupes_keys(
3109 report: &fallow_types::duplicates::DuplicationReport,
3110 root: &Path,
3111) -> FxHashSet<String> {
3112 report
3113 .clone_groups
3114 .iter()
3115 .map(|group| dupe_group_key(group, root))
3116 .collect()
3117}
3118
3119pub fn dupe_group_key(group: &fallow_types::duplicates::CloneGroup, root: &Path) -> String {
3120 let mut files: Vec<String> = group
3121 .instances
3122 .iter()
3123 .map(|instance| relative_key_path(&instance.file, root))
3124 .collect();
3125 files.sort();
3126 files.dedup();
3127 let mut hasher = DefaultHasher::new();
3128 for instance in &group.instances {
3129 instance.fragment.hash(&mut hasher);
3130 }
3131 format!(
3132 "dupe:{}:{}:{}:{:x}",
3133 files.join("|"),
3134 group.token_count,
3135 group.line_count,
3136 hasher.finish()
3137 )
3138}
3139
3140#[cfg(test)]
3141mod tests {
3142 use std::path::{Path, PathBuf};
3143
3144 use fallow_config::{FallowConfig, Severity};
3145 use fallow_types::duplicates::{CloneGroup, CloneInstance, DuplicationReport};
3146 use fallow_types::envelope::AuditIntroduced;
3147 use fallow_types::extract::MemberKind;
3148 use fallow_types::output_dead_code::*;
3149 use fallow_types::output_format::OutputFormat;
3150 use fallow_types::results::*;
3151 use rustc_hash::FxHashSet;
3152 use serde_json::json;
3153
3154 use fallow_output::{
3155 ComplexityViolation, ExceededThreshold, FindingSeverity, HealthFinding, HealthReport,
3156 };
3157
3158 use super::{
3159 AuditDomainLedger, annotate_dead_code_json, annotate_dupes_json, annotate_health_json,
3160 annotate_stale_suppressions_json, dead_code_audit_ledger, dead_code_keys, dupe_group_key,
3161 dupes_keys, health_finding_key, health_keys, relative_key_path,
3162 retain_introduced_dead_code,
3163 };
3164
3165 fn root() -> PathBuf {
3166 PathBuf::from("/repo")
3167 }
3168
3169 fn export(path: &Path, name: &str) -> UnusedExportFinding {
3170 UnusedExportFinding::with_actions(UnusedExport {
3171 path: path.to_path_buf(),
3172 export_name: name.to_string(),
3173 is_type_only: false,
3174 line: 1,
3175 col: 0,
3176 span_start: 0,
3177 is_re_export: false,
3178 })
3179 }
3180
3181 fn unused_file(path: &Path) -> UnusedFileFinding {
3182 UnusedFileFinding::with_actions(UnusedFile {
3183 path: path.to_path_buf(),
3184 })
3185 }
3186
3187 fn dependency(path: &Path, package_name: &str) -> UnusedDependencyFinding {
3188 UnusedDependencyFinding::with_actions(UnusedDependency {
3189 package_name: package_name.to_string(),
3190 location: DependencyLocation::Dependencies,
3191 path: path.to_path_buf(),
3192 line: 4,
3193 used_in_workspaces: Vec::new(),
3194 })
3195 }
3196
3197 fn unresolved(path: &Path, specifier: &str) -> UnresolvedImportFinding {
3198 UnresolvedImportFinding::with_actions(UnresolvedImport {
3199 path: path.to_path_buf(),
3200 specifier: specifier.to_string(),
3201 line: 2,
3202 col: 1,
3203 specifier_col: 8,
3204 })
3205 }
3206
3207 fn unlisted(path: &Path, package_name: &str) -> UnlistedDependencyFinding {
3208 UnlistedDependencyFinding::with_actions(UnlistedDependency {
3209 package_name: package_name.to_string(),
3210 imported_from: vec![
3211 ImportSite {
3212 path: path.to_path_buf(),
3213 line: 9,
3214 col: 2,
3215 },
3216 ImportSite {
3217 path: path.to_path_buf(),
3218 line: 9,
3219 col: 2,
3220 },
3221 ],
3222 })
3223 }
3224
3225 fn duplicate_export(root: &Path) -> DuplicateExportFinding {
3226 DuplicateExportFinding::with_actions(DuplicateExport {
3227 export_name: "Button".to_string(),
3228 locations: vec![
3229 DuplicateLocation {
3230 path: root.join("src/b.ts"),
3231 line: 1,
3232 col: 0,
3233 },
3234 DuplicateLocation {
3235 path: root.join("src/a.ts"),
3236 line: 1,
3237 col: 0,
3238 },
3239 DuplicateLocation {
3240 path: root.join("src/a.ts"),
3241 line: 2,
3242 col: 0,
3243 },
3244 ],
3245 })
3246 }
3247
3248 fn sample_results(root: &Path) -> AnalysisResults {
3249 let source = root.join("src/page.ts");
3250 let package_json = root.join("package.json");
3251 let mut results = AnalysisResults::default();
3252 results
3253 .unused_files
3254 .push(unused_file(&root.join("src/dead.ts")));
3255 results.unused_exports.push(export(&source, "loader"));
3256 results
3257 .unused_dependencies
3258 .push(dependency(&package_json, "left-pad"));
3259 results
3260 .unresolved_imports
3261 .push(unresolved(&source, "./missing"));
3262 results.unlisted_dependencies.push(unlisted(&source, "zod"));
3263 results.duplicate_exports.push(duplicate_export(root));
3264 results
3265 }
3266
3267 #[test]
3268 fn relative_key_path_strips_root_and_normalizes_separators() {
3269 let path = Path::new("/repo/src\\feature\\index.ts");
3270 assert_eq!(
3271 relative_key_path(path, Path::new("/repo")),
3272 "src/feature/index.ts"
3273 );
3274 }
3275
3276 #[test]
3277 fn dead_code_keys_are_stable_for_unsorted_and_duplicate_locations() {
3278 let root = root();
3279 let keys = dead_code_keys(&sample_results(&root), &root);
3280
3281 assert!(keys.contains("unused-file:src/dead.ts"));
3282 assert!(keys.contains("unused-export:src/page.ts:loader"));
3283 assert!(keys.contains("unused-dependency:package.json:left-pad"));
3284 assert!(keys.contains("unresolved-import:src/page.ts:./missing"));
3285 assert!(keys.contains("unlisted-dependency:zod:src/page.ts:9:2"));
3286 assert!(keys.contains("duplicate-export:Button:src/a.ts|src/b.ts"));
3287 }
3288
3289 #[test]
3290 fn dead_code_keys_cover_type_member_and_dependency_variants() {
3291 let root = root();
3292 let source = root.join("src/types.ts");
3293 let package_json = root.join("package.json");
3294 let mut results = AnalysisResults::default();
3295 results
3296 .unused_types
3297 .push(UnusedTypeFinding::with_actions(UnusedExport {
3298 path: source.clone(),
3299 export_name: "UnusedType".to_string(),
3300 is_type_only: true,
3301 line: 3,
3302 col: 0,
3303 span_start: 12,
3304 is_re_export: false,
3305 }));
3306 results
3307 .private_type_leaks
3308 .push(PrivateTypeLeakFinding::with_actions(PrivateTypeLeak {
3309 path: source.clone(),
3310 export_name: "makePublic".to_string(),
3311 type_name: "PrivateShape".to_string(),
3312 line: 7,
3313 col: 12,
3314 span_start: 64,
3315 }));
3316 results
3317 .unused_dev_dependencies
3318 .push(UnusedDevDependencyFinding::with_actions(UnusedDependency {
3319 package_name: "vite".to_string(),
3320 location: DependencyLocation::DevDependencies,
3321 path: package_json.clone(),
3322 line: 10,
3323 used_in_workspaces: Vec::new(),
3324 }));
3325 results
3326 .unused_optional_dependencies
3327 .push(UnusedOptionalDependencyFinding::with_actions(
3328 UnusedDependency {
3329 package_name: "fsevents".to_string(),
3330 location: DependencyLocation::OptionalDependencies,
3331 path: package_json.clone(),
3332 line: 11,
3333 used_in_workspaces: Vec::new(),
3334 },
3335 ));
3336 results
3337 .unused_enum_members
3338 .push(UnusedEnumMemberFinding::with_actions(UnusedMember {
3339 path: source.clone(),
3340 parent_name: "Status".to_string(),
3341 member_name: "Idle".to_string(),
3342 kind: MemberKind::EnumMember,
3343 line: 15,
3344 col: 2,
3345 }));
3346 results
3347 .unused_class_members
3348 .push(UnusedClassMemberFinding::with_actions(UnusedMember {
3349 path: source,
3350 parent_name: "Controller".to_string(),
3351 member_name: "legacy".to_string(),
3352 kind: MemberKind::ClassMethod,
3353 line: 21,
3354 col: 2,
3355 }));
3356 results
3357 .type_only_dependencies
3358 .push(TypeOnlyDependencyFinding::with_actions(
3359 TypeOnlyDependency {
3360 package_name: "zod".to_string(),
3361 path: package_json.clone(),
3362 line: 12,
3363 },
3364 ));
3365 results
3366 .test_only_dependencies
3367 .push(TestOnlyDependencyFinding::with_actions(
3368 TestOnlyDependency {
3369 package_name: "vitest".to_string(),
3370 path: package_json,
3371 line: 13,
3372 },
3373 ));
3374
3375 let keys = dead_code_keys(&results, &root);
3376
3377 assert!(keys.contains("unused-type:src/types.ts:UnusedType"));
3378 assert!(keys.contains("private-type-leak:src/types.ts:makePublic:PrivateShape"));
3379 assert!(keys.contains("unused-dev-dependency:package.json:vite"));
3380 assert!(keys.contains("unused-optional-dependency:package.json:fsevents"));
3381 assert!(keys.contains("unused-enum-member:src/types.ts:Status:Idle"));
3382 assert!(keys.contains("unused-class-member:src/types.ts:Controller:legacy"));
3383 assert!(keys.contains("type-only-dependency:package.json:zod"));
3384 assert!(keys.contains("test-only-dependency:package.json:vitest"));
3385 }
3386
3387 #[expect(
3388 clippy::too_many_lines,
3389 reason = "test fixture; linear setup/assert, length is not a maintainability concern"
3390 )]
3391 fn graph_boundary_catalog_override_results(root: &std::path::Path) -> AnalysisResults {
3392 let source = root.join("src/app.ts");
3393 let other = root.join("src/other.ts");
3394 let workspace = root.join("pnpm-workspace.yaml");
3395 let mut results = AnalysisResults::default();
3396 results
3397 .circular_dependencies
3398 .push(CircularDependencyFinding::with_actions(
3399 CircularDependency {
3400 files: vec![other.clone(), source.clone()],
3401 length: 2,
3402 line: 4,
3403 col: 0,
3404 edges: Vec::new(),
3405 is_cross_package: false,
3406 },
3407 ));
3408 results
3409 .re_export_cycles
3410 .push(ReExportCycleFinding::with_actions(ReExportCycle {
3411 files: vec![source.clone()],
3412 kind: ReExportCycleKind::SelfLoop,
3413 }));
3414 results
3415 .boundary_violations
3416 .push(BoundaryViolationFinding::with_actions(BoundaryViolation {
3417 from_path: source.clone(),
3418 to_path: other,
3419 from_zone: "ui".to_string(),
3420 to_zone: "server".to_string(),
3421 import_specifier: "../other".to_string(),
3422 line: 1,
3423 col: 0,
3424 }));
3425 results
3426 .boundary_coverage_violations
3427 .push(BoundaryCoverageViolationFinding::with_actions(
3428 BoundaryCoverageViolation {
3429 path: root.join("src/unmatched.ts"),
3430 line: 1,
3431 col: 0,
3432 },
3433 ));
3434 results
3435 .boundary_call_violations
3436 .push(BoundaryCallViolationFinding::with_actions(
3437 BoundaryCallViolation {
3438 path: source.clone(),
3439 line: 12,
3440 col: 4,
3441 zone: "ui".to_string(),
3442 callee: "child_process.exec".to_string(),
3443 pattern: "child_process.*".to_string(),
3444 },
3445 ));
3446 results.stale_suppressions.push(StaleSuppression {
3447 path: source,
3448 line: 2,
3449 col: 0,
3450 origin: SuppressionOrigin::Comment {
3451 issue_kind: Some("unused-export".to_string()),
3452 reason: None,
3453 is_file_level: false,
3454 kind_known: true,
3455 },
3456 missing_reason: false,
3457 actions: StaleSuppression::actions_for(false),
3458 });
3459 results.stale_suppressions.push(StaleSuppression {
3460 path: root.join("src/app.ts"),
3461 line: 2,
3462 col: 0,
3463 origin: SuppressionOrigin::Comment {
3464 issue_kind: Some("unused-export".to_string()),
3465 reason: None,
3466 is_file_level: false,
3467 kind_known: true,
3468 },
3469 missing_reason: true,
3470 actions: StaleSuppression::actions_for(true),
3471 });
3472 results.unresolved_catalog_references.push(
3473 UnresolvedCatalogReferenceFinding::with_actions(UnresolvedCatalogReference {
3474 entry_name: "react".to_string(),
3475 catalog_name: "default".to_string(),
3476 path: root.join("packages/app/package.json"),
3477 line: 9,
3478 available_in_catalogs: vec!["react18".to_string()],
3479 }),
3480 );
3481 results
3482 .unused_catalog_entries
3483 .push(UnusedCatalogEntryFinding::with_actions(
3484 UnusedCatalogEntry {
3485 entry_name: "lodash".to_string(),
3486 catalog_name: "default".to_string(),
3487 path: workspace.clone(),
3488 line: 3,
3489 hardcoded_consumers: Vec::new(),
3490 },
3491 ));
3492 results
3493 .empty_catalog_groups
3494 .push(EmptyCatalogGroupFinding::with_actions(EmptyCatalogGroup {
3495 catalog_name: "react17".to_string(),
3496 path: workspace.clone(),
3497 line: 7,
3498 }));
3499 results
3500 .unused_dependency_overrides
3501 .push(UnusedDependencyOverrideFinding::with_actions(
3502 UnusedDependencyOverride {
3503 raw_key: "left-pad".to_string(),
3504 target_package: "left-pad".to_string(),
3505 parent_package: None,
3506 version_constraint: None,
3507 version_range: "^1.3.0".to_string(),
3508 source: DependencyOverrideSource::PnpmWorkspaceYaml,
3509 path: workspace.clone(),
3510 line: 11,
3511 hint: None,
3512 },
3513 ));
3514 results.misconfigured_dependency_overrides.push(
3515 MisconfiguredDependencyOverrideFinding::with_actions(MisconfiguredDependencyOverride {
3516 raw_key: ">".to_string(),
3517 target_package: None,
3518 raw_value: String::new(),
3519 reason: DependencyOverrideMisconfigReason::UnparsableKey,
3520 source: DependencyOverrideSource::PnpmWorkspaceYaml,
3521 path: workspace,
3522 line: 12,
3523 }),
3524 );
3525 results
3526 }
3527
3528 #[test]
3529 fn dead_code_keys_cover_graph_boundary_catalog_and_override_variants() {
3530 let root = root();
3531 let results = graph_boundary_catalog_override_results(&root);
3532
3533 let keys = dead_code_keys(&results, &root);
3534
3535 assert!(keys.contains("circular-dependency:src/app.ts|src/other.ts"));
3536 assert!(keys.contains("re-export-cycle:self-loop:src/app.ts"));
3537 assert!(keys.contains("boundary-violation:src/app.ts:src/other.ts:../other"));
3538 assert!(keys.contains("boundary-coverage:src/unmatched.ts"));
3539 assert!(keys.contains("boundary-call:src/app.ts:child_process.exec"));
3540 assert!(
3541 keys.contains("stale-suppression:src/app.ts:// fallow-ignore-next-line unused-export")
3542 );
3543 assert!(keys.contains(
3544 "missing-suppression-reason:src/app.ts:// fallow-ignore-next-line unused-export"
3545 ));
3546 assert!(
3547 keys.contains("unresolved-catalog-reference:packages/app/package.json:9:default:react")
3548 );
3549 assert!(keys.contains("unused-catalog-entry:pnpm-workspace.yaml:3:default:lodash"));
3550 assert!(keys.contains("empty-catalog-group:pnpm-workspace.yaml:7:react17"));
3551 assert!(keys.contains("unused-dependency-override:pnpm-workspace.yaml:11:left-pad"));
3552 assert!(keys.contains("misconfigured-dependency-override:pnpm-workspace.yaml:12:>"));
3553 }
3554
3555 #[test]
3556 fn retain_introduced_dead_code_keeps_only_findings_absent_from_base() {
3557 let root = root();
3558 let mut results = sample_results(&root);
3559 let base = FxHashSet::from_iter([
3560 "unused-file:src/dead.ts".to_string(),
3561 "unused-dependency:package.json:left-pad".to_string(),
3562 "unresolved-import:src/page.ts:./missing".to_string(),
3563 ]);
3564
3565 retain_introduced_dead_code(&mut results, &root, Some(&base));
3566
3567 assert!(results.unused_files.is_empty());
3568 assert!(results.unused_dependencies.is_empty());
3569 assert!(results.unresolved_imports.is_empty());
3570 assert_eq!(results.unused_exports.len(), 1);
3571 assert_eq!(results.unlisted_dependencies.len(), 1);
3572 assert_eq!(results.duplicate_exports.len(), 1);
3573 }
3574
3575 #[test]
3576 fn annotate_dead_code_json_marks_introduced_status_by_matching_key_order() {
3577 let root = root();
3578 let results = sample_results(&root);
3579 let base = FxHashSet::from_iter([
3580 "unused-file:src/dead.ts".to_string(),
3581 "unlisted-dependency:zod:src/page.ts:9:2".to_string(),
3582 ]);
3583 let mut json = json!({
3584 "unused_files": [{}],
3585 "unused_exports": [{}],
3586 "unused_dependencies": [{}],
3587 "unresolved_imports": [{}],
3588 "unlisted_dependencies": [{}],
3589 "duplicate_exports": [{}],
3590 });
3591
3592 annotate_dead_code_json(&mut json, &results, &root, &base);
3593
3594 assert_eq!(json["unused_files"][0]["introduced"], false);
3595 assert_eq!(json["unused_exports"][0]["introduced"], true);
3596 assert_eq!(json["unused_dependencies"][0]["introduced"], true);
3597 assert_eq!(json["unresolved_imports"][0]["introduced"], true);
3598 assert_eq!(json["unlisted_dependencies"][0]["introduced"], false);
3599 assert_eq!(json["duplicate_exports"][0]["introduced"], true);
3600 }
3601
3602 #[test]
3605 fn dead_code_keys_cover_framework_inject_and_render_variants() {
3606 let root = root();
3607 let src = root.join("src/App.vue");
3608 let mut results = AnalysisResults::default();
3609 results
3610 .unprovided_injects
3611 .push(UnprovidedInjectFinding::with_actions(UnprovidedInject {
3612 path: src.clone(),
3613 key_name: "userStore".to_string(),
3614 framework: "vue".to_string(),
3615 line: 5,
3616 col: 0,
3617 }));
3618 results
3619 .unrendered_components
3620 .push(UnrenderedComponentFinding::with_actions(
3621 UnrenderedComponent {
3622 path: src.clone(),
3623 component_name: "MyModal".to_string(),
3624 framework: "vue".to_string(),
3625 reachable_via: None,
3626 line: 1,
3627 col: 0,
3628 },
3629 ));
3630 results
3631 .unused_component_props
3632 .push(UnusedComponentPropFinding::with_actions(
3633 UnusedComponentProp {
3634 path: src.clone(),
3635 component_name: "MyModal".to_string(),
3636 prop_name: "title".to_string(),
3637 line: 3,
3638 col: 2,
3639 },
3640 ));
3641 results
3642 .unused_component_emits
3643 .push(UnusedComponentEmitFinding::with_actions(
3644 UnusedComponentEmit {
3645 path: src,
3646 component_name: "MyModal".to_string(),
3647 emit_name: "close".to_string(),
3648 line: 4,
3649 col: 2,
3650 },
3651 ));
3652 results
3653 .unused_svelte_events
3654 .push(UnusedSvelteEventFinding::with_actions(UnusedSvelteEvent {
3655 path: root.join("src/Counter.svelte"),
3656 component_name: "Counter".to_string(),
3657 event_name: "increment".to_string(),
3658 line: 8,
3659 col: 0,
3660 }));
3661
3662 let keys = dead_code_keys(&results, &root);
3663
3664 assert!(keys.contains("unprovided-inject:src/App.vue:userStore"));
3665 assert!(keys.contains("unrendered-component:src/App.vue:MyModal"));
3666 assert!(keys.contains("unused-component-prop:src/App.vue:title"));
3667 assert!(keys.contains("unused-component-emit:src/App.vue:close"));
3668 assert!(keys.contains("unused-svelte-event:src/Counter.svelte:increment"));
3669 }
3670
3671 #[test]
3672 fn dead_code_keys_cover_server_action_load_data_and_route_variants() {
3673 let root = root();
3674 let actions_file = root.join("src/actions/submit.ts");
3675 let page_file = root.join("src/routes/blog/+page.server.ts");
3676 let route_file = root.join("app/(auth)/login/page.tsx");
3677 let route_file2 = root.join("app/login/page.tsx");
3678 let mut results = AnalysisResults::default();
3679 results
3680 .unused_server_actions
3681 .push(UnusedServerActionFinding::with_actions(
3682 UnusedServerAction {
3683 path: actions_file,
3684 action_name: "submitForm".to_string(),
3685 line: 2,
3686 col: 0,
3687 },
3688 ));
3689 results
3690 .unused_load_data_keys
3691 .push(UnusedLoadDataKeyFinding::with_actions(UnusedLoadDataKey {
3692 path: page_file,
3693 key_name: "posts".to_string(),
3694 line: 10,
3695 col: 4,
3696 route_dir: None,
3697 }));
3698 results
3699 .route_collisions
3700 .push(RouteCollisionFinding::with_actions(RouteCollision {
3701 path: route_file.clone(),
3702 url: "/login".to_string(),
3703 conflicting_paths: vec![route_file2.clone()],
3704 line: 1,
3705 col: 0,
3706 }));
3707 results.dynamic_segment_name_conflicts.push(
3708 DynamicSegmentNameConflictFinding::with_actions(DynamicSegmentNameConflict {
3709 path: route_file,
3710 position: "/shop".to_string(),
3711 conflicting_segments: vec!["[id]".to_string(), "[slug]".to_string()],
3712 conflicting_paths: vec![route_file2],
3713 line: 1,
3714 col: 0,
3715 }),
3716 );
3717
3718 let keys = dead_code_keys(&results, &root);
3719
3720 assert!(keys.contains("unused-server-action:src/actions/submit.ts:submitForm"));
3721 assert!(keys.contains("unused-load-data-key:src/routes/blog/+page.server.ts:posts"));
3722 assert!(keys.contains("route-collision:app/(auth)/login/page.tsx:/login"));
3723 assert!(keys.contains("dynamic-segment-name-conflict:app/(auth)/login/page.tsx:/shop"));
3724 }
3725
3726 #[test]
3727 fn dead_code_keys_cover_angular_input_output_and_policy_variants() {
3728 let root = root();
3729 let component = root.join("src/app/card.component.ts");
3730 let src = root.join("src/utils.ts");
3731 let mut results = AnalysisResults::default();
3732 results
3733 .unused_component_inputs
3734 .push(UnusedComponentInputFinding::with_actions(
3735 UnusedComponentInput {
3736 path: component.clone(),
3737 component_name: "CardComponent".to_string(),
3738 input_name: "label".to_string(),
3739 line: 12,
3740 col: 4,
3741 },
3742 ));
3743 results
3744 .unused_component_outputs
3745 .push(UnusedComponentOutputFinding::with_actions(
3746 UnusedComponentOutput {
3747 path: component,
3748 component_name: "CardComponent".to_string(),
3749 output_name: "clicked".to_string(),
3750 line: 13,
3751 col: 4,
3752 },
3753 ));
3754 results
3755 .policy_violations
3756 .push(PolicyViolationFinding::with_actions(PolicyViolation {
3757 path: src,
3758 line: 7,
3759 col: 0,
3760 pack: "security".to_string(),
3761 rule_id: "no-eval".to_string(),
3762 kind: PolicyRuleKind::BannedCall,
3763 matched: "eval".to_string(),
3764 severity: PolicyViolationSeverity::Error,
3765 message: None,
3766 }));
3767
3768 let keys = dead_code_keys(&results, &root);
3769
3770 assert!(keys.contains("unused-component-input:src/app/card.component.ts:label"));
3771 assert!(keys.contains("unused-component-output:src/app/card.component.ts:clicked"));
3772 assert!(keys.contains("policy-violation:src/utils.ts:security/no-eval:eval"));
3773 }
3774
3775 #[test]
3776 fn dead_code_keys_cover_re_export_cycle_multi_node_variant() {
3777 let root = root();
3778 let a = root.join("src/a.ts");
3779 let b = root.join("src/b.ts");
3780 let mut results = AnalysisResults::default();
3781 results
3782 .re_export_cycles
3783 .push(ReExportCycleFinding::with_actions(ReExportCycle {
3784 files: vec![b, a],
3785 kind: ReExportCycleKind::MultiNode,
3786 }));
3787
3788 let keys = dead_code_keys(&results, &root);
3789
3790 assert!(keys.contains("re-export-cycle:multi-node:src/a.ts|src/b.ts"));
3792 }
3793
3794 #[test]
3795 fn dead_code_keys_cover_unused_store_member() {
3796 let root = root();
3797 let src = root.join("src/store.ts");
3798 let mut results = AnalysisResults::default();
3799 results
3800 .unused_store_members
3801 .push(UnusedStoreMemberFinding::with_actions(UnusedMember {
3802 path: src,
3803 parent_name: "useAuthStore".to_string(),
3804 member_name: "resetPassword".to_string(),
3805 kind: MemberKind::ClassMethod,
3806 line: 42,
3807 col: 2,
3808 }));
3809
3810 let keys = dead_code_keys(&results, &root);
3811
3812 assert!(keys.contains("unused-store-member:src/store.ts:useAuthStore:resetPassword"));
3813 }
3814
3815 #[test]
3818 fn annotate_dead_code_json_marks_framework_keys_correctly() {
3819 let root = root();
3820 let src = root.join("src/App.vue");
3821 let mut results = AnalysisResults::default();
3822 results
3823 .unprovided_injects
3824 .push(UnprovidedInjectFinding::with_actions(UnprovidedInject {
3825 path: src.clone(),
3826 key_name: "theme".to_string(),
3827 framework: "vue".to_string(),
3828 line: 3,
3829 col: 0,
3830 }));
3831 results
3832 .unrendered_components
3833 .push(UnrenderedComponentFinding::with_actions(
3834 UnrenderedComponent {
3835 path: src.clone(),
3836 component_name: "Dialog".to_string(),
3837 framework: "vue".to_string(),
3838 reachable_via: None,
3839 line: 1,
3840 col: 0,
3841 },
3842 ));
3843 results
3844 .unused_component_props
3845 .push(UnusedComponentPropFinding::with_actions(
3846 UnusedComponentProp {
3847 path: src.clone(),
3848 component_name: "Dialog".to_string(),
3849 prop_name: "open".to_string(),
3850 line: 5,
3851 col: 2,
3852 },
3853 ));
3854 results
3855 .unused_component_emits
3856 .push(UnusedComponentEmitFinding::with_actions(
3857 UnusedComponentEmit {
3858 path: src,
3859 component_name: "Dialog".to_string(),
3860 emit_name: "dismiss".to_string(),
3861 line: 6,
3862 col: 2,
3863 },
3864 ));
3865
3866 let base = FxHashSet::from_iter(["unprovided-inject:src/App.vue:theme".to_string()]);
3868 let mut json_val = json!({
3869 "unprovided_injects": [{}],
3870 "unrendered_components": [{}],
3871 "unused_component_props": [{}],
3872 "unused_component_emits": [{}],
3873 });
3874
3875 annotate_dead_code_json(&mut json_val, &results, &root, &base);
3876
3877 assert_eq!(json_val["unprovided_injects"][0]["introduced"], false);
3878 assert_eq!(json_val["unrendered_components"][0]["introduced"], true);
3879 assert_eq!(json_val["unused_component_props"][0]["introduced"], true);
3880 assert_eq!(json_val["unused_component_emits"][0]["introduced"], true);
3881 }
3882
3883 #[test]
3884 fn annotate_dead_code_json_marks_component_io_and_route_keys_correctly() {
3885 let root = root();
3886 let component = root.join("src/card.component.ts");
3887 let svelte_file = root.join("src/Counter.svelte");
3888 let page_file = root.join("src/routes/+page.server.ts");
3889 let route_file = root.join("app/about/page.tsx");
3890 let route_file2 = root.join("app/(info)/about/page.tsx");
3891 let mut results = AnalysisResults::default();
3892 results
3893 .unused_component_inputs
3894 .push(UnusedComponentInputFinding::with_actions(
3895 UnusedComponentInput {
3896 path: component.clone(),
3897 component_name: "CardComponent".to_string(),
3898 input_name: "size".to_string(),
3899 line: 8,
3900 col: 2,
3901 },
3902 ));
3903 results
3904 .unused_component_outputs
3905 .push(UnusedComponentOutputFinding::with_actions(
3906 UnusedComponentOutput {
3907 path: component,
3908 component_name: "CardComponent".to_string(),
3909 output_name: "hovered".to_string(),
3910 line: 9,
3911 col: 2,
3912 },
3913 ));
3914 results
3915 .unused_svelte_events
3916 .push(UnusedSvelteEventFinding::with_actions(UnusedSvelteEvent {
3917 path: svelte_file,
3918 component_name: "Counter".to_string(),
3919 event_name: "reset".to_string(),
3920 line: 12,
3921 col: 0,
3922 }));
3923 results
3924 .unused_server_actions
3925 .push(UnusedServerActionFinding::with_actions(
3926 UnusedServerAction {
3927 path: page_file,
3928 action_name: "deletePost".to_string(),
3929 line: 3,
3930 col: 0,
3931 },
3932 ));
3933 results
3934 .route_collisions
3935 .push(RouteCollisionFinding::with_actions(RouteCollision {
3936 path: route_file.clone(),
3937 url: "/about".to_string(),
3938 conflicting_paths: vec![route_file2.clone()],
3939 line: 1,
3940 col: 0,
3941 }));
3942 results.dynamic_segment_name_conflicts.push(
3943 DynamicSegmentNameConflictFinding::with_actions(DynamicSegmentNameConflict {
3944 path: route_file,
3945 position: "/".to_string(),
3946 conflicting_segments: vec!["[id]".to_string()],
3947 conflicting_paths: vec![route_file2],
3948 line: 1,
3949 col: 0,
3950 }),
3951 );
3952
3953 let base = FxHashSet::default();
3955 let mut json_val = json!({
3956 "unused_component_inputs": [{}],
3957 "unused_component_outputs": [{}],
3958 "unused_svelte_events": [{}],
3959 "unused_server_actions": [{}],
3960 "route_collisions": [{}],
3961 "dynamic_segment_name_conflicts": [{}],
3962 });
3963
3964 annotate_dead_code_json(&mut json_val, &results, &root, &base);
3965
3966 assert_eq!(json_val["unused_component_inputs"][0]["introduced"], true);
3967 assert_eq!(json_val["unused_component_outputs"][0]["introduced"], true);
3968 assert_eq!(json_val["unused_svelte_events"][0]["introduced"], true);
3969 assert_eq!(json_val["unused_server_actions"][0]["introduced"], true);
3970 assert_eq!(json_val["route_collisions"][0]["introduced"], true);
3971 assert_eq!(
3972 json_val["dynamic_segment_name_conflicts"][0]["introduced"],
3973 true
3974 );
3975 }
3976
3977 #[test]
3978 fn annotate_dead_code_json_marks_members_and_dependencies_correctly() {
3979 let root = root();
3980 let src = root.join("src/types.ts");
3981 let pkg = root.join("package.json");
3982 let mut results = AnalysisResults::default();
3983 results
3984 .unused_enum_members
3985 .push(UnusedEnumMemberFinding::with_actions(UnusedMember {
3986 path: src.clone(),
3987 parent_name: "Color".to_string(),
3988 member_name: "Blue".to_string(),
3989 kind: MemberKind::EnumMember,
3990 line: 5,
3991 col: 2,
3992 }));
3993 results
3994 .unused_class_members
3995 .push(UnusedClassMemberFinding::with_actions(UnusedMember {
3996 path: src.clone(),
3997 parent_name: "Service".to_string(),
3998 member_name: "reset".to_string(),
3999 kind: MemberKind::ClassMethod,
4000 line: 20,
4001 col: 2,
4002 }));
4003 results
4004 .unused_store_members
4005 .push(UnusedStoreMemberFinding::with_actions(UnusedMember {
4006 path: src,
4007 parent_name: "useStore".to_string(),
4008 member_name: "logout".to_string(),
4009 kind: MemberKind::ClassMethod,
4010 line: 30,
4011 col: 2,
4012 }));
4013 results
4014 .unused_dev_dependencies
4015 .push(UnusedDevDependencyFinding::with_actions(UnusedDependency {
4016 package_name: "typescript".to_string(),
4017 location: DependencyLocation::DevDependencies,
4018 path: pkg.clone(),
4019 line: 8,
4020 used_in_workspaces: Vec::new(),
4021 }));
4022 results
4023 .type_only_dependencies
4024 .push(TypeOnlyDependencyFinding::with_actions(
4025 TypeOnlyDependency {
4026 package_name: "zod".to_string(),
4027 path: pkg.clone(),
4028 line: 9,
4029 },
4030 ));
4031 results
4032 .test_only_dependencies
4033 .push(TestOnlyDependencyFinding::with_actions(
4034 TestOnlyDependency {
4035 package_name: "vitest".to_string(),
4036 path: pkg,
4037 line: 10,
4038 },
4039 ));
4040
4041 let base = FxHashSet::from_iter([
4044 "unused-enum-member:src/types.ts:Color:Blue".to_string(),
4045 "unused-dev-dependency:package.json:typescript".to_string(),
4046 ]);
4047 let mut json_val = json!({
4048 "unused_enum_members": [{}],
4049 "unused_class_members": [{}],
4050 "unused_store_members": [{}],
4051 "unused_dev_dependencies": [{}],
4052 "type_only_dependencies": [{}],
4053 "test_only_dependencies": [{}],
4054 });
4055
4056 annotate_dead_code_json(&mut json_val, &results, &root, &base);
4057
4058 assert_eq!(json_val["unused_enum_members"][0]["introduced"], false);
4059 assert_eq!(json_val["unused_class_members"][0]["introduced"], true);
4060 assert_eq!(json_val["unused_store_members"][0]["introduced"], true);
4061 assert_eq!(json_val["unused_dev_dependencies"][0]["introduced"], false);
4062 assert_eq!(json_val["type_only_dependencies"][0]["introduced"], true);
4063 assert_eq!(json_val["test_only_dependencies"][0]["introduced"], true);
4064 }
4065
4066 #[test]
4067 fn annotate_dead_code_json_handles_missing_json_key_gracefully() {
4068 let root = root();
4071 let results = sample_results(&root);
4072 let base = FxHashSet::default();
4073 let mut json_val = json!({"other_key": []});
4074
4075 annotate_dead_code_json(&mut json_val, &results, &root, &base);
4077 }
4078
4079 fn make_violation(path: &Path, name: &str) -> ComplexityViolation {
4082 ComplexityViolation {
4083 path: path.to_path_buf(),
4084 name: name.to_string(),
4085 line: 1,
4086 col: 0,
4087 cyclomatic: 20,
4088 cognitive: 5,
4089 line_count: 30,
4090 param_count: 2,
4091 react_hook_count: 0,
4092 react_jsx_max_depth: 0,
4093 react_prop_count: 0,
4094 react_hook_profile: None,
4095 exceeded: ExceededThreshold::Cyclomatic,
4096 severity: FindingSeverity::High,
4097 crap: None,
4098 coverage_pct: None,
4099 coverage_tier: None,
4100 coverage_source: None,
4101 inherited_from: None,
4102 component_rollup: None,
4103 contributions: Vec::new(),
4104 effective_thresholds: None,
4105 threshold_source: None,
4106 }
4107 }
4108
4109 fn make_health_report(paths_and_names: &[(&Path, &str)]) -> HealthReport {
4110 let findings = paths_and_names
4111 .iter()
4112 .map(|(path, name)| HealthFinding::from(make_violation(path, name)))
4113 .collect();
4114 HealthReport {
4115 findings,
4116 ..HealthReport::default()
4117 }
4118 }
4119
4120 #[test]
4121 fn health_keys_produces_stable_key_per_finding() {
4122 let root = root();
4123 let path = root.join("src/heavy.ts");
4124 let report = make_health_report(&[(&path, "processAll")]);
4125 let keys = health_keys(&report, &root);
4126 assert!(keys.contains("complexity:src/heavy.ts:processAll:Cyclomatic"));
4127 }
4128
4129 #[test]
4130 fn health_finding_key_uses_path_name_and_exceeded() {
4131 let root = root();
4132 let path = root.join("src/heavy.ts");
4133 let violation = make_violation(&path, "render");
4134 let key = health_finding_key(&violation, &root);
4135 assert_eq!(key, "complexity:src/heavy.ts:render:Cyclomatic");
4136 }
4137
4138 #[test]
4139 fn annotate_health_json_marks_introduced_and_inherited_flags() {
4140 let root = root();
4141 let path_a = root.join("src/heavy.ts");
4142 let path_b = root.join("src/other.ts");
4143 let report = make_health_report(&[(&path_a, "doWork"), (&path_b, "render")]);
4144
4145 let base = FxHashSet::from_iter(["complexity:src/other.ts:render:Cyclomatic".to_string()]);
4147 let mut json_val = json!({
4148 "findings": [{}, {}],
4149 });
4150
4151 annotate_health_json(&mut json_val, &report, &root, &base);
4152
4153 assert_eq!(json_val["findings"][0]["introduced"], true);
4154 assert_eq!(json_val["findings"][1]["introduced"], false);
4155 }
4156
4157 #[test]
4158 fn annotate_health_json_is_noop_when_findings_key_absent() {
4159 let root = root();
4160 let report = make_health_report(&[]);
4161 let base = FxHashSet::default();
4162 let mut json_val = json!({"summary": {}});
4163 annotate_health_json(&mut json_val, &report, &root, &base);
4165 }
4166
4167 fn make_clone_group(files: &[PathBuf], fragment: &str) -> CloneGroup {
4170 CloneGroup {
4171 instances: files
4172 .iter()
4173 .map(|f| CloneInstance {
4174 file: f.clone(),
4175 start_line: 1,
4176 end_line: 5,
4177 start_col: 0,
4178 end_col: 80,
4179 fragment: fragment.to_string(),
4180 })
4181 .collect(),
4182 token_count: 10,
4183 line_count: 5,
4184 }
4185 }
4186
4187 fn make_duplication_report(groups: Vec<CloneGroup>) -> DuplicationReport {
4188 DuplicationReport {
4189 clone_groups: groups,
4190 clone_families: Vec::new(),
4191 mirrored_directories: Vec::new(),
4192 stats: fallow_types::duplicates::DuplicationStats::default(),
4193 }
4194 }
4195
4196 #[test]
4197 fn dupe_group_key_is_stable_for_sorted_deduplicated_files() {
4198 let root = root();
4199 let a = root.join("src/a.ts");
4200 let b = root.join("src/b.ts");
4201 let group_ab = make_clone_group(&[a.clone(), b.clone()], "const x = 1;");
4203 let group_ba = make_clone_group(&[b, a], "const x = 1;");
4204 let key_ab = dupe_group_key(&group_ab, &root);
4205 let key_ba = dupe_group_key(&group_ba, &root);
4206 assert!(key_ab.starts_with("dupe:src/a.ts|src/b.ts:"));
4208 assert!(key_ba.starts_with("dupe:src/a.ts|src/b.ts:"));
4209 assert_eq!(key_ab, key_ba);
4211 }
4212
4213 #[test]
4214 fn dupes_keys_produces_one_key_per_clone_group() {
4215 let root = root();
4216 let a = root.join("src/a.ts");
4217 let b = root.join("src/b.ts");
4218 let groups = vec![
4219 make_clone_group(&[a.clone(), b.clone()], "block one"),
4220 make_clone_group(&[a, b], "block two"),
4221 ];
4222 let report = make_duplication_report(groups);
4223 let keys = dupes_keys(&report, &root);
4224 assert_eq!(keys.len(), 2);
4225 }
4226
4227 #[test]
4228 fn annotate_dupes_json_marks_introduced_and_inherited_flags() {
4229 let root = root();
4230 let a = root.join("src/a.ts");
4231 let b = root.join("src/b.ts");
4232 let group_new = make_clone_group(&[a.clone(), b.clone()], "new block");
4233 let group_old = make_clone_group(&[a, b], "old block");
4234 let old_key = dupe_group_key(&group_old, &root);
4235 let base = FxHashSet::from_iter([old_key]);
4236 let report = make_duplication_report(vec![group_new, group_old]);
4237 let mut json_val = json!({
4238 "clone_groups": [{}, {}],
4239 });
4240
4241 annotate_dupes_json(&mut json_val, &report, &root, &base);
4242
4243 assert_eq!(json_val["clone_groups"][0]["introduced"], true);
4244 assert_eq!(json_val["clone_groups"][1]["introduced"], false);
4245 }
4246
4247 #[test]
4248 fn annotate_dupes_json_is_noop_when_clone_groups_key_absent() {
4249 let root = root();
4250 let report = make_duplication_report(Vec::new());
4251 let base = FxHashSet::default();
4252 let mut json_val = json!({"stats": {}});
4253 annotate_dupes_json(&mut json_val, &report, &root, &base);
4255 }
4256
4257 #[test]
4260 fn retain_introduced_dead_code_is_noop_when_base_is_none() {
4261 let root = root();
4262 let mut results = sample_results(&root);
4263 let original_file_count = results.unused_files.len();
4264 let original_export_count = results.unused_exports.len();
4265
4266 retain_introduced_dead_code(&mut results, &root, None);
4267
4268 assert_eq!(results.unused_files.len(), original_file_count);
4270 assert_eq!(results.unused_exports.len(), original_export_count);
4271 }
4272
4273 #[test]
4276 fn retain_introduced_dead_code_filters_framework_findings() {
4277 let root = root();
4278 let src = root.join("src/App.vue");
4279 let mut results = AnalysisResults::default();
4280 results
4281 .unprovided_injects
4282 .push(UnprovidedInjectFinding::with_actions(UnprovidedInject {
4283 path: src.clone(),
4284 key_name: "existing".to_string(),
4285 framework: "vue".to_string(),
4286 line: 1,
4287 col: 0,
4288 }));
4289 results
4290 .unprovided_injects
4291 .push(UnprovidedInjectFinding::with_actions(UnprovidedInject {
4292 path: src.clone(),
4293 key_name: "new".to_string(),
4294 framework: "vue".to_string(),
4295 line: 2,
4296 col: 0,
4297 }));
4298 results
4299 .unrendered_components
4300 .push(UnrenderedComponentFinding::with_actions(
4301 UnrenderedComponent {
4302 path: src,
4303 component_name: "OldWidget".to_string(),
4304 framework: "vue".to_string(),
4305 reachable_via: None,
4306 line: 1,
4307 col: 0,
4308 },
4309 ));
4310
4311 let base = FxHashSet::from_iter([
4312 "unprovided-inject:src/App.vue:existing".to_string(),
4313 "unrendered-component:src/App.vue:OldWidget".to_string(),
4314 ]);
4315
4316 retain_introduced_dead_code(&mut results, &root, Some(&base));
4317
4318 assert_eq!(results.unprovided_injects.len(), 1);
4320 assert_eq!(results.unprovided_injects[0].inject.key_name, "new");
4321 assert!(results.unrendered_components.is_empty());
4322 }
4323
4324 #[test]
4325 fn retain_introduced_dead_code_filters_graph_findings() {
4326 let root = root();
4327 let a = root.join("src/a.ts");
4328 let b = root.join("src/b.ts");
4329 let mut results = AnalysisResults::default();
4330 results
4331 .circular_dependencies
4332 .push(CircularDependencyFinding::with_actions(
4333 CircularDependency {
4334 files: vec![a.clone(), b],
4335 length: 2,
4336 line: 1,
4337 col: 0,
4338 edges: Vec::new(),
4339 is_cross_package: false,
4340 },
4341 ));
4342 results
4343 .re_export_cycles
4344 .push(ReExportCycleFinding::with_actions(ReExportCycle {
4345 files: vec![a],
4346 kind: ReExportCycleKind::SelfLoop,
4347 }));
4348
4349 let base = FxHashSet::from_iter(["circular-dependency:src/a.ts|src/b.ts".to_string()]);
4351
4352 retain_introduced_dead_code(&mut results, &root, Some(&base));
4353
4354 assert!(results.circular_dependencies.is_empty());
4355 assert_eq!(results.re_export_cycles.len(), 1);
4356 }
4357
4358 #[test]
4359 fn audit_ledger_routes_override_severity_and_persists_introduced_flags() {
4360 let root = root();
4361 let config: FallowConfig = serde_json::from_value(json!({
4362 "rules": { "unused-exports": "warn" },
4363 "overrides": [{
4364 "files": ["src/generated/**"],
4365 "rules": { "unused-exports": "error" }
4366 }]
4367 }))
4368 .expect("config");
4369 let config = config.resolve(root.clone(), OutputFormat::Json, 1, false, true, None);
4370 let mut results = AnalysisResults::default();
4371 results
4372 .unused_exports
4373 .push(export(&root.join("src/base.ts"), "baseExport"));
4374 results
4375 .unused_exports
4376 .push(export(&root.join("src/generated/new.ts"), "newExport"));
4377 let base = FxHashSet::from_iter(["unused-export:src/base.ts:baseExport".to_string()]);
4378
4379 let ledger = dead_code_audit_ledger(&results, &root, &config, Some(&base));
4380
4381 assert_eq!(ledger.classification_count(), 2);
4382 assert_eq!(ledger.introduced_count(), 1);
4383 assert_eq!(ledger.inherited_count(), 1);
4384 assert!(ledger.has_introduced_errors());
4385 assert!(!ledger.has_introduced_warnings());
4386 assert_eq!(ledger.records()[0].effective_severity, Severity::Warn);
4387 assert_eq!(ledger.records()[1].effective_severity, Severity::Error);
4388
4389 ledger.annotate_results(&mut results);
4390 assert_eq!(ledger.classification_count(), 2);
4391 assert_eq!(
4392 results.unused_exports[0].introduced,
4393 Some(AuditIntroduced(false))
4394 );
4395 assert_eq!(
4396 results.unused_exports[1].introduced,
4397 Some(AuditIntroduced(true))
4398 );
4399 }
4400
4401 #[test]
4402 fn audit_ledger_counts_colliding_dead_code_keys_once_but_annotates_each_record() {
4403 let root = root();
4404 let config: FallowConfig = serde_json::from_value(json!({
4405 "rules": { "unused-exports": "error" }
4406 }))
4407 .expect("config");
4408 let config = config.resolve(root.clone(), OutputFormat::Json, 1, false, true, None);
4409 let mut results = AnalysisResults::default();
4410 results
4411 .unused_exports
4412 .push(export(&root.join("src/collision.ts"), "sameExport"));
4413 results
4414 .unused_exports
4415 .push(export(&root.join("src/collision.ts"), "sameExport"));
4416
4417 let ledger = dead_code_audit_ledger(&results, &root, &config, Some(&FxHashSet::default()));
4418
4419 assert_eq!(ledger.classification_count(), 2);
4420 assert_eq!(ledger.records().len(), 2);
4421 assert_eq!(ledger.introduced_count(), 1);
4422 assert_eq!(ledger.inherited_count(), 0);
4423
4424 ledger.annotate_results(&mut results);
4425 assert!(
4426 results
4427 .unused_exports
4428 .iter()
4429 .all(|finding| { finding.introduced == Some(AuditIntroduced(true)) })
4430 );
4431 }
4432
4433 #[test]
4434 fn audit_domain_ledger_counts_colliding_keys_once_but_preserves_record_membership() {
4435 let introduced = AuditDomainLedger::compare(
4436 ["same-key".to_string(), "same-key".to_string()],
4437 Some(&FxHashSet::default()),
4438 );
4439 assert_eq!(introduced.introduced_count(), 1);
4440 assert_eq!(introduced.inherited_count(), 0);
4441 assert_eq!(introduced.introduced().collect::<Vec<_>>(), [true, true]);
4442
4443 let base = FxHashSet::from_iter(["same-key".to_string()]);
4444 let inherited = AuditDomainLedger::compare(
4445 ["same-key".to_string(), "same-key".to_string()],
4446 Some(&base),
4447 );
4448 assert_eq!(inherited.introduced_count(), 0);
4449 assert_eq!(inherited.inherited_count(), 1);
4450 assert_eq!(inherited.introduced().collect::<Vec<_>>(), [false, false]);
4451 }
4452
4453 #[test]
4454 fn stale_suppression_fallback_preserves_introduced_annotation() {
4455 let root = root();
4456 let mut results = AnalysisResults::default();
4457 results.stale_suppressions.push(StaleSuppression {
4458 path: root.join("src/new.ts"),
4459 line: 2,
4460 col: 0,
4461 origin: SuppressionOrigin::Comment {
4462 issue_kind: Some("unused-export".to_string()),
4463 reason: None,
4464 is_file_level: false,
4465 kind_known: true,
4466 },
4467 missing_reason: false,
4468 actions: StaleSuppression::actions_for(false),
4469 });
4470 let mut json = serde_json::to_value(&results).expect("json");
4471
4472 annotate_stale_suppressions_json(&mut json, &results, &root, &FxHashSet::default());
4473
4474 assert_eq!(json["stale_suppressions"][0]["introduced"], true);
4475 }
4476}