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