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