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