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 crap: None,
3239 mutation: None,
3240 coverage_pct: None,
3241 coverage_tier: None,
3242 coverage_source: None,
3243 inherited_from: None,
3244 component_rollup: None,
3245 contributions: Vec::new(),
3246 effective_thresholds: None,
3247 threshold_source: None,
3248 }
3249 }
3250
3251 fn make_health_report(paths_and_names: &[(&Path, &str)]) -> HealthReport {
3252 let findings = paths_and_names
3253 .iter()
3254 .map(|(path, name)| HealthFinding::from(make_violation(path, name)))
3255 .collect();
3256 HealthReport {
3257 findings,
3258 ..HealthReport::default()
3259 }
3260 }
3261
3262 #[test]
3263 fn health_keys_produces_stable_key_per_finding() {
3264 let root = root();
3265 let path = root.join("src/heavy.ts");
3266 let report = make_health_report(&[(&path, "processAll")]);
3267 let keys = health_keys(&report, &root);
3268 assert!(keys.contains("complexity:src/heavy.ts:processAll"));
3269 }
3270
3271 #[test]
3272 fn health_finding_key_uses_path_and_name() {
3273 let root = root();
3274 let path = root.join("src/heavy.ts");
3275 let violation = make_violation(&path, "render");
3276 let key = health_finding_key(&violation, &root);
3277 assert_eq!(key, "complexity:src/heavy.ts:render");
3278 }
3279
3280 #[test]
3281 fn annotate_health_json_marks_introduced_and_inherited_flags() {
3282 let root = root();
3283 let path_a = root.join("src/heavy.ts");
3284 let path_b = root.join("src/other.ts");
3285 let report = make_health_report(&[(&path_a, "doWork"), (&path_b, "render")]);
3286
3287 let base = FxHashSet::from_iter(["complexity:src/other.ts:render".to_string()]);
3289 let mut json_val = json!({
3290 "findings": [{}, {}],
3291 });
3292
3293 annotate_health_json(&mut json_val, &report, &root, &base);
3294
3295 assert_eq!(json_val["findings"][0]["introduced"], true);
3296 assert_eq!(json_val["findings"][1]["introduced"], false);
3297 }
3298
3299 #[test]
3300 fn annotate_health_json_keeps_the_typed_complexity_flag() {
3301 let root = root();
3302 let path = root.join("src/other.ts");
3303 let mut report = make_health_report(&[(&path, "render")]);
3304 report.findings[0].introduced = Some(true);
3306 let base = FxHashSet::from_iter(["complexity:src/other.ts:render".to_string()]);
3307 let mut json_val = json!({ "findings": [{}] });
3308
3309 annotate_health_json(&mut json_val, &report, &root, &base);
3310
3311 assert_eq!(json_val["findings"][0]["introduced"], true);
3312 }
3313
3314 #[test]
3315 fn annotate_health_json_is_noop_when_findings_key_absent() {
3316 let root = root();
3317 let report = make_health_report(&[]);
3318 let base = FxHashSet::default();
3319 let mut json_val = json!({"summary": {}});
3320 let original = json_val.clone();
3321
3322 annotate_health_json(&mut json_val, &report, &root, &base);
3323
3324 assert_eq!(json_val, original);
3325 }
3326
3327 fn make_clone_group(files: &[PathBuf], fragment: &str) -> CloneGroup {
3330 CloneGroup {
3331 instances: files
3332 .iter()
3333 .map(|f| CloneInstance {
3334 is_symlink: false,
3335 file: f.clone(),
3336 start_line: 1,
3337 end_line: 5,
3338 start_col: 0,
3339 end_col: 80,
3340 fragment: fragment.to_string(),
3341 })
3342 .collect(),
3343 token_count: 10,
3344 line_count: 5,
3345 similarity: None,
3346 }
3347 }
3348
3349 fn make_duplication_report(groups: Vec<CloneGroup>) -> DuplicationReport {
3350 DuplicationReport {
3351 clone_groups: groups,
3352 clone_families: Vec::new(),
3353 mirrored_directories: Vec::new(),
3354 stats: fallow_types::duplicates::DuplicationStats::default(),
3355 }
3356 }
3357
3358 #[test]
3359 fn dupe_group_key_is_stable_for_sorted_deduplicated_files() {
3360 let root = root();
3361 let a = root.join("src/a.ts");
3362 let b = root.join("src/b.ts");
3363 let group_ab = make_clone_group(&[a.clone(), b.clone()], "const x = 1;");
3365 let group_ba = make_clone_group(&[b, a], "const x = 1;");
3366 let key_ab = dupe_group_key(&group_ab, &root);
3367 let key_ba = dupe_group_key(&group_ba, &root);
3368 assert!(key_ab.starts_with("dupe:src/a.ts|src/b.ts:"));
3370 assert!(key_ba.starts_with("dupe:src/a.ts|src/b.ts:"));
3371 assert_eq!(key_ab, key_ba);
3373 }
3374
3375 #[test]
3376 fn preexisting_dupe_group_keys_collects_only_groups_outside_added_lines() {
3377 let root = root();
3378 let a = root.join("src/a.ts");
3379 let b = root.join("src/b.ts");
3380 let c = root.join("src/c.ts");
3381 let untouched = make_clone_group(&[a.clone(), b], "shared scaffolding");
3383 let touched = make_clone_group(&[a, c], "pasted block");
3384 let diff = fallow_output::DiffIndex::from_unified_diff(
3385 "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",
3386 );
3387
3388 let demote = super::preexisting_dupe_group_keys([&untouched, &touched], &root, &diff);
3389
3390 assert!(demote.contains(&dupe_group_key(&untouched, &root)));
3391 assert!(!demote.contains(&dupe_group_key(&touched, &root)));
3392 assert_eq!(demote.len(), 1);
3393 }
3394
3395 #[test]
3396 fn demote_introductions_moves_introduced_keys_to_inherited() {
3397 let base: FxHashSet<String> = std::iter::once("dupe:old".to_string()).collect();
3398 let mut ledger = AuditDomainLedger::compare(
3399 ["dupe:kept".to_string(), "dupe:demoted".to_string()],
3400 Some(&base),
3401 );
3402 assert_eq!(ledger.introduced_count(), 2);
3403
3404 let demote: FxHashSet<String> = std::iter::once("dupe:demoted".to_string()).collect();
3405 ledger.demote_introductions(&demote);
3406
3407 assert_eq!(ledger.introduced_count(), 1);
3408 assert_eq!(ledger.inherited_count(), 1);
3409 let introduced: Vec<bool> = ledger.introduced().collect();
3410 assert_eq!(introduced, vec![true, false]);
3411 assert_eq!(ledger.demoted_count(), 1);
3412 assert!(ledger.demoted_count() <= ledger.inherited_count());
3413 assert!(ledger.demoted_keys().contains("dupe:demoted"));
3414 let demoted: Vec<bool> = ledger.demoted().collect();
3415 assert_eq!(demoted, vec![false, true]);
3416 }
3417
3418 #[test]
3419 fn demote_introductions_ignores_keys_that_were_not_introduced() {
3420 let base: FxHashSet<String> = std::iter::once("dupe:old".to_string()).collect();
3421 let mut ledger = AuditDomainLedger::compare(
3422 ["dupe:old".to_string(), "dupe:new".to_string()],
3423 Some(&base),
3424 );
3425
3426 let demote: FxHashSet<String> = std::iter::once("dupe:old".to_string()).collect();
3427 ledger.demote_introductions(&demote);
3428
3429 assert_eq!(ledger.demoted_count(), 0);
3430 let demoted: Vec<bool> = ledger.demoted().collect();
3431 assert_eq!(demoted, vec![false, false]);
3432 }
3433
3434 #[test]
3435 fn annotate_domain_demotions_json_marks_only_demoted_entries() {
3436 let mut json_val = json!({
3437 "clone_groups": [{}, {}],
3438 });
3439
3440 super::annotate_domain_demotions_json(
3441 &mut json_val,
3442 "clone_groups",
3443 [false, true],
3444 crate::CloneDemotionReason::NoAddedLines,
3445 );
3446
3447 assert!(json_val["clone_groups"][0].get("demotion_reason").is_none());
3448 assert_eq!(
3449 json_val["clone_groups"][1]["demotion_reason"],
3450 "no-added-lines"
3451 );
3452 }
3453
3454 #[test]
3455 fn dupes_keys_produces_one_key_per_clone_group() {
3456 let root = root();
3457 let a = root.join("src/a.ts");
3458 let b = root.join("src/b.ts");
3459 let groups = vec![
3460 make_clone_group(&[a.clone(), b.clone()], "block one"),
3461 make_clone_group(&[a, b], "block two"),
3462 ];
3463 let report = make_duplication_report(groups);
3464 let keys = dupes_keys(&report, &root);
3465 assert_eq!(keys.len(), 2);
3466 }
3467
3468 #[test]
3469 fn annotate_dupes_json_marks_introduced_and_inherited_flags() {
3470 let root = root();
3471 let a = root.join("src/a.ts");
3472 let b = root.join("src/b.ts");
3473 let group_new = make_clone_group(&[a.clone(), b.clone()], "new block");
3474 let group_old = make_clone_group(&[a, b], "old block");
3475 let old_key = dupe_group_key(&group_old, &root);
3476 let base = FxHashSet::from_iter([old_key]);
3477 let report = make_duplication_report(vec![group_new, group_old]);
3478 let mut json_val = json!({
3479 "clone_groups": [{}, {}],
3480 });
3481
3482 annotate_dupes_json(&mut json_val, &report, &root, &base);
3483
3484 assert_eq!(json_val["clone_groups"][0]["introduced"], true);
3485 assert_eq!(json_val["clone_groups"][1]["introduced"], false);
3486 }
3487
3488 #[test]
3489 fn annotate_dupes_json_is_noop_when_clone_groups_key_absent() {
3490 let root = root();
3491 let report = make_duplication_report(Vec::new());
3492 let base = FxHashSet::default();
3493 let mut json_val = json!({"stats": {}});
3494 let original = json_val.clone();
3495
3496 annotate_dupes_json(&mut json_val, &report, &root, &base);
3497
3498 assert_eq!(json_val, original);
3499 }
3500
3501 #[test]
3504 fn retain_introduced_dead_code_is_noop_when_base_is_none() {
3505 let root = root();
3506 let mut results = sample_results(&root);
3507 let original_file_count = results.unused_files.len();
3508 let original_export_count = results.unused_exports.len();
3509
3510 retain_introduced_dead_code(&mut results, &root, None);
3511
3512 assert_eq!(results.unused_files.len(), original_file_count);
3514 assert_eq!(results.unused_exports.len(), original_export_count);
3515 }
3516
3517 #[test]
3520 fn retain_introduced_dead_code_filters_framework_findings() {
3521 let root = root();
3522 let src = root.join("src/App.vue");
3523 let mut results = AnalysisResults::default();
3524 results
3525 .unprovided_injects
3526 .push(UnprovidedInjectFinding::with_actions(UnprovidedInject {
3527 path: src.clone(),
3528 key_name: "existing".to_string(),
3529 framework: "vue".to_string(),
3530 line: 1,
3531 col: 0,
3532 }));
3533 results
3534 .unprovided_injects
3535 .push(UnprovidedInjectFinding::with_actions(UnprovidedInject {
3536 path: src.clone(),
3537 key_name: "new".to_string(),
3538 framework: "vue".to_string(),
3539 line: 2,
3540 col: 0,
3541 }));
3542 results
3543 .unrendered_components
3544 .push(UnrenderedComponentFinding::with_actions(
3545 UnrenderedComponent {
3546 path: src,
3547 component_name: "OldWidget".to_string(),
3548 framework: "vue".to_string(),
3549 reachable_via: None,
3550 line: 1,
3551 col: 0,
3552 },
3553 ));
3554
3555 let base = FxHashSet::from_iter([
3556 "unprovided-inject:src/App.vue:existing".to_string(),
3557 "unrendered-component:src/App.vue:OldWidget".to_string(),
3558 ]);
3559
3560 retain_introduced_dead_code(&mut results, &root, Some(&base));
3561
3562 assert_eq!(results.unprovided_injects.len(), 1);
3564 assert_eq!(results.unprovided_injects[0].inject.key_name, "new");
3565 assert!(results.unrendered_components.is_empty());
3566 }
3567
3568 #[test]
3569 fn retain_introduced_dead_code_filters_graph_findings() {
3570 let root = root();
3571 let a = root.join("src/a.ts");
3572 let b = root.join("src/b.ts");
3573 let mut results = AnalysisResults::default();
3574 results
3575 .circular_dependencies
3576 .push(CircularDependencyFinding::with_actions(
3577 CircularDependency {
3578 files: vec![a.clone(), b],
3579 length: 2,
3580 line: 1,
3581 col: 0,
3582 edges: Vec::new(),
3583 is_cross_package: false,
3584 },
3585 ));
3586 results
3587 .re_export_cycles
3588 .push(ReExportCycleFinding::with_actions(ReExportCycle {
3589 files: vec![a],
3590 kind: ReExportCycleKind::SelfLoop,
3591 }));
3592
3593 let base = FxHashSet::from_iter(["circular-dependency:src/a.ts|src/b.ts".to_string()]);
3595
3596 retain_introduced_dead_code(&mut results, &root, Some(&base));
3597
3598 assert!(results.circular_dependencies.is_empty());
3599 assert_eq!(results.re_export_cycles.len(), 1);
3600 }
3601
3602 #[test]
3603 fn audit_ledger_routes_override_severity_and_persists_introduced_flags() {
3604 let root = root();
3605 let config: FallowConfig = serde_json::from_value(json!({
3606 "rules": { "unused-exports": "warn" },
3607 "overrides": [{
3608 "files": ["src/generated/**"],
3609 "rules": { "unused-exports": "error" }
3610 }]
3611 }))
3612 .expect("config");
3613 let config = config.resolve(root.clone(), OutputFormat::Json, 1, false, true, None);
3614 let mut results = AnalysisResults::default();
3615 results
3616 .unused_exports
3617 .push(export(&root.join("src/base.ts"), "baseExport"));
3618 results
3619 .unused_exports
3620 .push(export(&root.join("src/generated/new.ts"), "newExport"));
3621 let base = FxHashSet::from_iter(["unused-export:src/base.ts:baseExport".to_string()]);
3622
3623 let ledger = dead_code_audit_ledger(&results, &root, &config, Some(&base));
3624
3625 assert_eq!(ledger.classification_count(), 2);
3626 assert_eq!(ledger.introduced_count(), 1);
3627 assert_eq!(ledger.inherited_count(), 1);
3628 assert!(ledger.has_introduced_errors());
3629 assert!(!ledger.has_introduced_warnings());
3630 assert_eq!(ledger.records()[0].effective_severity, Severity::Warn);
3631 assert_eq!(ledger.records()[1].effective_severity, Severity::Error);
3632
3633 ledger.annotate_results(&mut results);
3634 assert_eq!(ledger.classification_count(), 2);
3635 assert_eq!(
3636 results.unused_exports[0].introduced,
3637 Some(AuditIntroduced(false))
3638 );
3639 assert_eq!(
3640 results.unused_exports[1].introduced,
3641 Some(AuditIntroduced(true))
3642 );
3643 }
3644
3645 #[test]
3646 fn audit_ledger_demotes_semantic_only_introductions_but_keeps_syntactic_new_findings() {
3647 let root = root();
3648 let config: FallowConfig = serde_json::from_value(json!({
3649 "rules": { "unused-exports": "error" }
3650 }))
3651 .expect("config");
3652 let config = config.resolve(root.clone(), OutputFormat::Json, 1, false, true, None);
3653 let mut results = AnalysisResults::default();
3654 results
3657 .unused_exports
3658 .push(export(&root.join("src/base.ts"), "baseExport"));
3659 results
3660 .unused_exports
3661 .push(export(&root.join("src/new.ts"), "newExport"));
3662 results
3663 .unused_exports
3664 .push(export(&root.join("src/semantic.ts"), "semanticOnly"));
3665 let base = FxHashSet::from_iter(["unused-export:src/base.ts:baseExport".to_string()]);
3666 let head_syntactic = FxHashSet::from_iter([
3667 "unused-export:src/base.ts:baseExport".to_string(),
3668 "unused-export:src/new.ts:newExport".to_string(),
3669 ]);
3670
3671 let mut ledger = dead_code_audit_ledger(&results, &root, &config, Some(&base));
3672 assert_eq!(ledger.introduced_count(), 2);
3673
3674 ledger.demote_unattributable_introductions(&head_syntactic);
3675
3676 assert_eq!(ledger.introduced_count(), 1);
3678 assert!(ledger.has_introduced_errors());
3679 assert_eq!(ledger.inherited_count(), 2);
3680 ledger.annotate_results(&mut results);
3681 assert_eq!(
3682 results.unused_exports[0].introduced,
3683 Some(AuditIntroduced(false))
3684 );
3685 assert_eq!(
3686 results.unused_exports[1].introduced,
3687 Some(AuditIntroduced(true))
3688 );
3689 assert_eq!(
3690 results.unused_exports[2].introduced,
3691 Some(AuditIntroduced(false))
3692 );
3693 }
3694
3695 #[test]
3696 fn audit_ledger_counts_each_occurrence_of_a_repeated_key() {
3697 let root = root();
3698 let config: FallowConfig = serde_json::from_value(json!({
3699 "rules": { "unused-exports": "error" }
3700 }))
3701 .expect("config");
3702 let config = config.resolve(root.clone(), OutputFormat::Json, 1, false, true, None);
3703 let mut results = AnalysisResults::default();
3704 results
3705 .unused_exports
3706 .push(export(&root.join("src/collision.ts"), "sameExport"));
3707 results
3708 .unused_exports
3709 .push(export(&root.join("src/collision.ts"), "sameExport"));
3710
3711 let ledger = dead_code_audit_ledger(&results, &root, &config, Some(&FxHashSet::default()));
3712
3713 assert_eq!(ledger.classification_count(), 2);
3714 assert_eq!(ledger.records().len(), 2);
3715 assert_eq!(ledger.introduced_count(), 2);
3716 assert_eq!(ledger.inherited_count(), 0);
3717
3718 ledger.annotate_results(&mut results);
3719 assert!(
3720 results
3721 .unused_exports
3722 .iter()
3723 .all(|finding| { finding.introduced == Some(AuditIntroduced(true)) })
3724 );
3725 }
3726
3727 #[test]
3728 fn audit_domain_ledger_counts_colliding_keys_once_but_preserves_record_membership() {
3729 let introduced = AuditDomainLedger::compare(
3730 ["same-key".to_string(), "same-key".to_string()],
3731 Some(&FxHashSet::default()),
3732 );
3733 assert_eq!(introduced.introduced_count(), 1);
3734 assert_eq!(introduced.inherited_count(), 0);
3735 assert_eq!(introduced.introduced().collect::<Vec<_>>(), [true, true]);
3736
3737 let base = FxHashSet::from_iter(["same-key".to_string()]);
3738 let inherited = AuditDomainLedger::compare(
3739 ["same-key".to_string(), "same-key".to_string()],
3740 Some(&base),
3741 );
3742 assert_eq!(inherited.introduced_count(), 0);
3743 assert_eq!(inherited.inherited_count(), 1);
3744 assert_eq!(inherited.introduced().collect::<Vec<_>>(), [false, false]);
3745 }
3746
3747 #[test]
3748 fn stale_suppression_fallback_preserves_introduced_annotation() {
3749 let root = root();
3750 let mut results = AnalysisResults::default();
3751 results.stale_suppressions.push(StaleSuppression {
3752 finding_id: None,
3753 path: root.join("src/new.ts"),
3754 line: 2,
3755 col: 0,
3756 origin: SuppressionOrigin::Comment {
3757 issue_kind: Some("unused-export".to_string()),
3758 reason: None,
3759 is_file_level: false,
3760 kind_known: true,
3761 },
3762 missing_reason: false,
3763 actions: StaleSuppression::actions_for(false),
3764 effective_severity: None,
3765 });
3766 let mut json = serde_json::to_value(&results).expect("json");
3767
3768 annotate_stale_suppressions_json(&mut json, &results, &root, &FxHashSet::default());
3769
3770 assert_eq!(json["stale_suppressions"][0]["introduced"], true);
3771 }
3772
3773 #[test]
3774 fn remap_keys_for_renames_relocates_path_segments() {
3775 use rustc_hash::FxHashMap;
3776
3777 let mut renames = FxHashMap::default();
3778 renames.insert(
3779 "src/old/impl.ts".to_string(),
3780 "src/renamed/impl.ts".to_string(),
3781 );
3782
3783 let keys: FxHashSet<String> = [
3784 "complexity:src/old/impl.ts:complexFn:[Cyclomatic]".to_string(),
3785 "unused-export:src/old/impl.ts:unusedHelper".to_string(),
3786 "unused-file:src/old/impl.ts".to_string(),
3787 "unused-export:src/other.ts:src/old/impl.ts-unrelated".to_string(),
3788 ]
3789 .into_iter()
3790 .collect();
3791
3792 let remapped = remap_keys_for_renames(&keys, &renames);
3793
3794 assert!(remapped.contains("complexity:src/renamed/impl.ts:complexFn:[Cyclomatic]"));
3795 assert!(remapped.contains("unused-export:src/renamed/impl.ts:unusedHelper"));
3796 assert!(remapped.contains("unused-file:src/renamed/impl.ts"));
3797 assert!(
3798 remapped.contains("unused-export:src/other.ts:src/old/impl.ts-unrelated"),
3799 "non-exact segment matches must pass through untouched"
3800 );
3801 }
3802
3803 #[test]
3804 fn remap_keys_for_renames_recanonicalizes_sorted_path_lists() {
3805 use rustc_hash::FxHashMap;
3806
3807 let mut renames = FxHashMap::default();
3808 renames.insert("src/a.ts".to_string(), "src/z.ts".to_string());
3809
3810 let keys: FxHashSet<String> = [
3811 "dupe:src/a.ts|src/m.ts:120:12:deadbeef".to_string(),
3814 "circular-dependency:src/a.ts|src/m.ts".to_string(),
3815 ]
3816 .into_iter()
3817 .collect();
3818
3819 let remapped = remap_keys_for_renames(&keys, &renames);
3820
3821 assert!(
3822 remapped.contains("dupe:src/m.ts|src/z.ts:120:12:deadbeef"),
3823 "duplicate-group file lists must be re-sorted: {remapped:#?}"
3824 );
3825 assert!(
3826 remapped.contains("circular-dependency:src/m.ts|src/z.ts"),
3827 "cycle file lists must be re-sorted: {remapped:#?}"
3828 );
3829 }
3830
3831 #[test]
3832 fn remap_keys_for_renames_without_matches_is_identity() {
3833 use rustc_hash::FxHashMap;
3834
3835 let mut renames = FxHashMap::default();
3836 renames.insert("src/elsewhere.ts".to_string(), "src/moved.ts".to_string());
3837
3838 let keys: FxHashSet<String> =
3839 std::iter::once("unused-export:src/utils.ts:helper".to_string()).collect();
3840
3841 assert_eq!(remap_keys_for_renames(&keys, &renames), keys);
3842 }
3843
3844 fn production_and_directive_results(root: &Path) -> AnalysisResults {
3846 let page = root.join("src/app.ts");
3847 let mut results = AnalysisResults::default();
3848 results.dev_dependencies_in_production.push(
3849 DevDependencyInProductionFinding::with_actions(DevDependencyInProduction {
3850 package_name: "vite".to_string(),
3851 path: root.join("package.json"),
3852 line: 14,
3853 }),
3854 );
3855 results
3856 .invalid_client_exports
3857 .push(InvalidClientExportFinding::with_actions(
3858 InvalidClientExport {
3859 path: page.clone(),
3860 export_name: "config".to_string(),
3861 directive: "use client".to_string(),
3862 line: 3,
3863 col: 0,
3864 },
3865 ));
3866 results
3867 .mixed_client_server_barrels
3868 .push(MixedClientServerBarrelFinding::with_actions(
3869 MixedClientServerBarrel {
3870 path: page.clone(),
3871 client_origin: "./client".to_string(),
3872 server_origin: "./server".to_string(),
3873 line: 1,
3874 col: 0,
3875 },
3876 ));
3877 results
3878 .misplaced_directives
3879 .push(MisplacedDirectiveFinding::with_actions(
3880 MisplacedDirective {
3881 path: page,
3882 directive: "use server".to_string(),
3883 line: 5,
3884 col: 2,
3885 },
3886 ));
3887 results
3888 }
3889
3890 fn isolated_findings(fixtures: &[AnalysisResults]) -> Vec<(String, AnalysisResults)> {
3893 let mut isolated = Vec::new();
3894 for fixture in fixtures {
3895 let value = serde_json::to_value(fixture).expect("results serialize");
3896 let object = value.as_object().expect("results serialize as an object");
3897 for (collection, items) in object {
3898 let Some(items) = items.as_array() else {
3899 continue;
3900 };
3901 for item in items {
3902 let mut single = serde_json::to_value(AnalysisResults::default())
3903 .expect("empty results serialize");
3904 single[collection.as_str()] = serde_json::Value::Array(vec![item.clone()]);
3905 let results = serde_json::from_value(single)
3906 .unwrap_or_else(|error| panic!("{collection} round-trips: {error}"));
3907 isolated.push((collection.clone(), results));
3908 }
3909 }
3910 }
3911 isolated
3912 }
3913
3914 fn absent_component_prop_results(root: &Path) -> AnalysisResults {
3915 let mut results = AnalysisResults::default();
3916 results
3917 .absent_component_props
3918 .push(AbsentComponentPropFinding::with_actions(
3919 AbsentComponentProp {
3920 path: root.join("src/App.vue"),
3921 component_name: "MyModal".into(),
3922 framework: "vue".into(),
3923 prop_name: "highlight".into(),
3924 line: 3,
3925 col: 2,
3926 has_default: true,
3927 inspected_call_sites: vec![ComponentPropCallSite {
3928 path: root.join("src/page.ts"),
3929 line: 8,
3930 col: 0,
3931 }],
3932 explanation: "Review known callers and API intent.".into(),
3933 },
3934 ));
3935 results
3936 }
3937
3938 fn every_rule_config(
3943 base: [&str; 2],
3944 scoped: Option<[&str; 2]>,
3945 ) -> fallow_config::ResolvedConfig {
3946 let every_rule = |severities: [&str; 2]| {
3947 let rules = serde_json::to_value(fallow_config::RulesConfig::default())
3948 .expect("rules serialize");
3949 let names = rules
3950 .as_object()
3951 .expect("rules serialize as an object")
3952 .keys();
3953 serde_json::Value::Object(
3954 names
3955 .enumerate()
3956 .map(|(index, name)| (name.clone(), json!(severities[index % 2])))
3957 .collect(),
3958 )
3959 };
3960 let mut config = json!({ "rules": every_rule(base) });
3961 if let Some(scoped) = scoped {
3962 config["overrides"] = json!([{
3963 "files": [
3964 "package.json",
3965 "pnpm-workspace.yaml",
3966 "src/app.ts",
3967 "src/page.ts",
3968 "src/types.ts",
3969 "src/App.vue",
3970 "src/actions/**",
3971 "app/**",
3972 "src/app/**",
3973 "src/store.ts"
3974 ],
3975 "rules": every_rule(scoped)
3976 }]);
3977 }
3978 let config: FallowConfig = serde_json::from_value(config).expect("config");
3979 config.resolve(root(), OutputFormat::Json, 1, false, true, None)
3980 }
3981
3982 #[test]
3987 fn audit_ledger_errors_match_the_exit_code_rule_for_every_collection() {
3988 let root = root();
3989 let fixtures: Vec<AnalysisResults> = [
3991 sample_results(&root),
3992 graph_boundary_catalog_override_results(&root),
3993 type_member_and_dependency_results(&root),
3994 framework_inject_and_render_results(&root),
3995 absent_component_prop_results(&root),
3996 ]
3997 .into_iter()
3998 .chain([
3999 server_action_load_data_and_route_results(&root),
4000 angular_input_output_and_policy_results(&root),
4001 unused_store_member_results(&root),
4002 production_and_directive_results(&root),
4003 AnalysisResults {
4004 prop_drilling_chains: vec![PropDrillingChainFinding::with_actions(
4005 PropDrillingChain {
4006 prop: "user".to_owned(),
4007 depth: 3,
4008 hops: vec![PropDrillHop {
4009 file: root.join("src/page.ts"),
4010 line: 4,
4011 component: "Page".to_owned(),
4012 }],
4013 },
4014 )],
4015 ..effect_results(&root)
4016 },
4017 ])
4018 .collect();
4019 let findings = isolated_findings(&fixtures);
4020 let empty_base = FxHashSet::default();
4021 let mut covered = std::collections::BTreeSet::new();
4022 let mut mismatches = Vec::new();
4023 for (base, scoped) in [
4024 (["error", "error"], None),
4025 (["warn", "warn"], None),
4026 (["error", "warn"], None),
4027 (["error", "error"], Some(["warn", "warn"])),
4028 (["warn", "warn"], Some(["error", "error"])),
4029 (["off", "off"], Some(["error", "error"])),
4030 (["error", "warn"], Some(["warn", "error"])),
4031 (["warn", "error"], Some(["error", "warn"])),
4032 ] {
4033 let config = every_rule_config(base, scoped);
4034 for (collection, results) in &findings {
4035 let ledger = dead_code_audit_ledger(results, &root, &config, Some(&empty_base));
4036 covered.extend(ledger.records().iter().map(|record| record.collection));
4037 let exit_rule = fallow_engine::error_severity::has_error_severity_issues(
4038 results,
4039 &config.rules,
4040 Some(&config),
4041 false,
4042 );
4043 if ledger.has_introduced_errors() != exit_rule {
4044 mismatches.push(format!(
4045 "{collection} (rules {base:?}, overrides {scoped:?}): ledger {}, exit rule {exit_rule}",
4046 ledger.has_introduced_errors()
4047 ));
4048 }
4049 }
4050 }
4051 assert!(mismatches.is_empty(), "{}", mismatches.join("\n"));
4052 let missing: Vec<&str> = super::AuditCollection::ALL
4053 .iter()
4054 .map(|collection| collection.json_key())
4055 .filter(|key| !covered.contains(key))
4056 .collect();
4057 assert!(
4058 missing.is_empty(),
4059 "the fixtures hold no finding in {missing:?}"
4060 );
4061 }
4062
4063 fn audit_suppression(root: &Path, line: u32, reason: Option<&str>) -> StaleSuppression {
4066 StaleSuppression {
4067 finding_id: None,
4068 path: root.join("src/flags.ts"),
4069 line,
4070 col: 0,
4071 origin: SuppressionOrigin::Comment {
4072 issue_kind: Some("unused-export".to_string()),
4073 reason: reason.map(str::to_owned),
4074 is_file_level: false,
4075 kind_known: true,
4076 },
4077 missing_reason: false,
4078 actions: StaleSuppression::actions_for(false),
4079 effective_severity: None,
4080 }
4081 }
4082
4083 fn shift_lines(results: &mut AnalysisResults, by: u32) {
4085 for item in &mut results.unlisted_dependencies {
4086 for site in &mut item.dep.imported_from {
4087 site.line += by;
4088 }
4089 }
4090 for item in &mut results.stale_suppressions {
4091 item.line += by;
4092 }
4093 for item in &mut results.unused_catalog_entries {
4094 item.entry.line += by;
4095 }
4096 for item in &mut results.empty_catalog_groups {
4097 item.group.line += by;
4098 }
4099 for item in &mut results.unresolved_catalog_references {
4100 item.reference.line += by;
4101 }
4102 for item in &mut results.unused_dependency_overrides {
4103 item.entry.line += by;
4104 }
4105 for item in &mut results.misconfigured_dependency_overrides {
4106 item.entry.line += by;
4107 }
4108 for item in &mut results.misplaced_directives {
4109 item.directive_site.line += by;
4110 }
4111 for item in &mut results.unused_exports {
4112 item.export.line += by;
4113 }
4114 }
4115
4116 fn line_sensitive_results(root: &Path) -> AnalysisResults {
4117 let mut results = graph_boundary_catalog_override_results(root);
4118 let sample = sample_results(root);
4119 results.unlisted_dependencies = sample.unlisted_dependencies;
4120 results.unused_exports = sample.unused_exports;
4121 results
4122 .misplaced_directives
4123 .push(MisplacedDirectiveFinding::with_actions(
4124 MisplacedDirective {
4125 path: root.join("src/action.ts"),
4126 directive: "use server".to_string(),
4127 line: 4,
4128 col: 0,
4129 },
4130 ));
4131 results
4132 }
4133
4134 #[test]
4135 fn dead_code_keys_do_not_change_when_lines_shift() {
4136 let root = root();
4137 let before = line_sensitive_results(&root);
4138 let mut after = before.clone();
4139 shift_lines(&mut after, 20);
4140
4141 let before_keys = dead_code_keys(&before, &root);
4142 let after_keys = dead_code_keys(&after, &root);
4143
4144 let mut changed: Vec<&String> = after_keys.difference(&before_keys).collect();
4145 changed.sort();
4146 assert!(changed.is_empty(), "keys that hold a line: {changed:?}");
4147 }
4148
4149 #[test]
4150 fn stale_suppression_keys_ignore_the_reason_text() {
4151 let root = root();
4152 let with_reason = |reason: &str| AnalysisResults {
4153 stale_suppressions: vec![audit_suppression(&root, 3, Some(reason))],
4154 ..AnalysisResults::default()
4155 };
4156
4157 assert_eq!(
4158 dead_code_keys(&with_reason("kept for the plugin API"), &root),
4159 dead_code_keys(&with_reason("removed in v3"), &root)
4160 );
4161 }
4162
4163 #[test]
4164 fn unlisted_dependency_keys_ignore_the_import_sites() {
4165 let root = root();
4166 let base = sample_results(&root);
4167 let mut head = base.clone();
4168 let item = &mut head.unlisted_dependencies[0].dep;
4169 item.imported_from[0].line = 40;
4170 item.imported_from.push(ImportSite {
4171 path: root.join("src/other.ts"),
4172 line: 1,
4173 col: 0,
4174 });
4175
4176 assert_eq!(dead_code_keys(&base, &root), dead_code_keys(&head, &root));
4177 }
4178
4179 #[test]
4180 fn a_second_occurrence_of_an_inherited_key_is_introduced() {
4181 let root = root();
4182 let config: FallowConfig = serde_json::from_value(json!({
4183 "rules": { "stale-suppressions": "error" }
4184 }))
4185 .expect("config");
4186 let config = config.resolve(root.clone(), OutputFormat::Json, 1, false, true, None);
4187 let base_results = AnalysisResults {
4188 stale_suppressions: vec![audit_suppression(&root, 3, None)],
4189 ..AnalysisResults::default()
4190 };
4191 let base = dead_code_keys(&base_results, &root);
4192 let mut head = AnalysisResults {
4193 stale_suppressions: vec![
4194 audit_suppression(&root, 13, None),
4195 audit_suppression(&root, 17, None),
4196 ],
4197 ..AnalysisResults::default()
4198 };
4199
4200 let ledger = dead_code_audit_ledger(&head, &root, &config, Some(&base));
4201 assert_eq!(ledger.introduced_count(), 1);
4202 assert_eq!(ledger.inherited_count(), 1);
4203 assert!(!ledger.records()[0].introduced);
4204 assert!(ledger.records()[1].introduced);
4205
4206 retain_introduced_dead_code(&mut head, &root, Some(&base));
4207 assert_eq!(head.stale_suppressions.len(), 1);
4208 assert_eq!(head.stale_suppressions[0].line, 17);
4209 }
4210
4211 #[test]
4212 fn a_renamed_file_keeps_its_findings_and_occurrences_inherited() {
4213 let root = root();
4214 let finding = |path: &str, line: u32| {
4215 let mut suppression = audit_suppression(&root, line, None);
4216 suppression.path = root.join(path);
4217 suppression
4218 };
4219 let base_results = AnalysisResults {
4220 stale_suppressions: vec![finding("src/old.ts", 3), finding("src/old.ts", 9)],
4221 ..AnalysisResults::default()
4222 };
4223 let head = AnalysisResults {
4224 stale_suppressions: vec![finding("src/new.ts", 4), finding("src/new.ts", 10)],
4225 ..AnalysisResults::default()
4226 };
4227 let renames = rustc_hash::FxHashMap::from_iter([(
4228 "src/old.ts".to_string(),
4229 "src/new.ts".to_string(),
4230 )]);
4231
4232 let base = remap_keys_for_renames(&dead_code_keys(&base_results, &root), &renames);
4233
4234 assert_eq!(base, dead_code_keys(&head, &root));
4235 }
4236}