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