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