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