1use std::collections::hash_map::DefaultHasher;
2use std::hash::{Hash, Hasher};
3use std::path::Path;
4
5use rustc_hash::FxHashSet;
6
7pub fn relative_key_path(path: &Path, root: &Path) -> String {
8 let simple_path = dunce::simplified(path);
9 let simple_root = dunce::simplified(root);
10 simple_path
11 .strip_prefix(simple_root)
12 .unwrap_or(simple_path)
13 .to_string_lossy()
14 .replace('\\', "/")
15}
16
17fn dependency_location_key(location: &fallow_types::results::DependencyLocation) -> &'static str {
18 match location {
19 fallow_types::results::DependencyLocation::Dependencies => "unused-dependency",
20 fallow_types::results::DependencyLocation::DevDependencies => "unused-dev-dependency",
21 fallow_types::results::DependencyLocation::OptionalDependencies => {
22 "unused-optional-dependency"
23 }
24 }
25}
26
27fn unused_dependency_key(item: &fallow_types::results::UnusedDependency, root: &Path) -> String {
28 format!(
29 "{}:{}:{}",
30 dependency_location_key(&item.location),
31 relative_key_path(&item.path, root),
32 item.package_name
33 )
34}
35
36fn invalid_client_export_key(
37 item: &fallow_types::results::InvalidClientExport,
38 root: &Path,
39) -> String {
40 format!(
41 "invalid-client-export:{}:{}",
42 relative_key_path(&item.path, root),
43 item.export_name
44 )
45}
46
47fn mixed_client_server_barrel_key(
48 item: &fallow_types::results::MixedClientServerBarrel,
49 root: &Path,
50) -> String {
51 format!(
52 "mixed-client-server-barrel:{}:{}:{}",
53 relative_key_path(&item.path, root),
54 item.client_origin,
55 item.server_origin
56 )
57}
58
59fn misplaced_directive_key(
60 item: &fallow_types::results::MisplacedDirective,
61 root: &Path,
62) -> String {
63 format!(
64 "misplaced-directive:{}:{}:{}",
65 relative_key_path(&item.path, root),
66 item.line,
67 item.directive
68 )
69}
70
71fn unprovided_inject_key(item: &fallow_types::results::UnprovidedInject, root: &Path) -> String {
72 format!(
73 "unprovided-inject:{}:{}",
74 relative_key_path(&item.path, root),
75 item.key_name
76 )
77}
78
79fn unrendered_component_key(
80 item: &fallow_types::results::UnrenderedComponent,
81 root: &Path,
82) -> String {
83 format!(
84 "unrendered-component:{}:{}",
85 relative_key_path(&item.path, root),
86 item.component_name
87 )
88}
89
90fn unused_component_prop_key(
91 item: &fallow_types::results::UnusedComponentProp,
92 root: &Path,
93) -> String {
94 format!(
95 "unused-component-prop:{}:{}",
96 relative_key_path(&item.path, root),
97 item.prop_name
98 )
99}
100
101fn unused_component_emit_key(
102 item: &fallow_types::results::UnusedComponentEmit,
103 root: &Path,
104) -> String {
105 format!(
106 "unused-component-emit:{}:{}",
107 relative_key_path(&item.path, root),
108 item.emit_name
109 )
110}
111
112fn unused_component_input_key(
113 item: &fallow_types::results::UnusedComponentInput,
114 root: &Path,
115) -> String {
116 format!(
117 "unused-component-input:{}:{}",
118 relative_key_path(&item.path, root),
119 item.input_name
120 )
121}
122
123fn unused_component_output_key(
124 item: &fallow_types::results::UnusedComponentOutput,
125 root: &Path,
126) -> String {
127 format!(
128 "unused-component-output:{}:{}",
129 relative_key_path(&item.path, root),
130 item.output_name
131 )
132}
133
134fn unused_svelte_event_key(item: &fallow_types::results::UnusedSvelteEvent, root: &Path) -> String {
135 format!(
136 "unused-svelte-event:{}:{}",
137 relative_key_path(&item.path, root),
138 item.event_name
139 )
140}
141
142fn unused_server_action_key(
143 item: &fallow_types::results::UnusedServerAction,
144 root: &Path,
145) -> String {
146 format!(
147 "unused-server-action:{}:{}",
148 relative_key_path(&item.path, root),
149 item.action_name
150 )
151}
152
153fn unused_load_data_key_key(
154 item: &fallow_types::results::UnusedLoadDataKey,
155 root: &Path,
156) -> String {
157 format!(
158 "unused-load-data-key:{}:{}",
159 relative_key_path(&item.path, root),
160 item.key_name
161 )
162}
163
164fn route_collision_key(item: &fallow_types::results::RouteCollision, root: &Path) -> String {
165 format!(
166 "route-collision:{}:{}",
167 relative_key_path(&item.path, root),
168 item.url
169 )
170}
171
172fn dynamic_segment_name_conflict_key(
173 item: &fallow_types::results::DynamicSegmentNameConflict,
174 root: &Path,
175) -> String {
176 format!(
177 "dynamic-segment-name-conflict:{}:{}",
178 relative_key_path(&item.path, root),
179 item.position
180 )
181}
182
183fn unlisted_dependency_key(
184 item: &fallow_types::results::UnlistedDependency,
185 root: &Path,
186) -> String {
187 let mut sites = item
188 .imported_from
189 .iter()
190 .map(|site| {
191 format!(
192 "{}:{}:{}",
193 relative_key_path(&site.path, root),
194 site.line,
195 site.col
196 )
197 })
198 .collect::<Vec<_>>();
199 sites.sort();
200 sites.dedup();
201 format!(
202 "unlisted-dependency:{}:{}",
203 item.package_name,
204 sites.join("|")
205 )
206}
207
208fn unused_member_key(
209 rule_id: &str,
210 item: &fallow_types::results::UnusedMember,
211 root: &Path,
212) -> String {
213 format!(
214 "{}:{}:{}:{}",
215 rule_id,
216 relative_key_path(&item.path, root),
217 item.parent_name,
218 item.member_name
219 )
220}
221
222fn unused_catalog_entry_key(
223 item: &fallow_types::results::UnusedCatalogEntry,
224 root: &Path,
225) -> String {
226 format!(
227 "unused-catalog-entry:{}:{}:{}:{}",
228 relative_key_path(&item.path, root),
229 item.line,
230 item.catalog_name,
231 item.entry_name
232 )
233}
234
235fn empty_catalog_group_key(item: &fallow_types::results::EmptyCatalogGroup, root: &Path) -> String {
236 format!(
237 "empty-catalog-group:{}:{}:{}",
238 relative_key_path(&item.path, root),
239 item.line,
240 item.catalog_name
241 )
242}
243
244fn sorted_relative_path_keys<'a>(
245 paths: impl Iterator<Item = &'a Path>,
246 root: &Path,
247) -> Vec<String> {
248 let mut keys = paths
249 .map(|path| relative_key_path(path, root))
250 .collect::<Vec<_>>();
251 keys.sort();
252 keys
253}
254
255fn duplicate_export_key(
256 item: &fallow_types::output_dead_code::DuplicateExportFinding,
257 root: &Path,
258) -> String {
259 let mut locations = sorted_relative_path_keys(
260 item.export.locations.iter().map(|loc| loc.path.as_path()),
261 root,
262 );
263 locations.dedup();
264 format!(
265 "duplicate-export:{}:{}",
266 item.export.export_name,
267 locations.join("|")
268 )
269}
270
271fn circular_dependency_key(
272 item: &fallow_types::output_dead_code::CircularDependencyFinding,
273 root: &Path,
274) -> String {
275 let files = sorted_relative_path_keys(
276 item.cycle.files.iter().map(std::path::PathBuf::as_path),
277 root,
278 );
279 format!("circular-dependency:{}", files.join("|"))
280}
281
282fn re_export_cycle_key(
283 item: &fallow_types::output_dead_code::ReExportCycleFinding,
284 root: &Path,
285) -> String {
286 let kind = match item.cycle.kind {
287 fallow_types::results::ReExportCycleKind::MultiNode => "multi-node",
288 fallow_types::results::ReExportCycleKind::SelfLoop => "self-loop",
289 };
290 let files = sorted_relative_path_keys(
291 item.cycle.files.iter().map(std::path::PathBuf::as_path),
292 root,
293 );
294 format!("re-export-cycle:{kind}:{}", files.join("|"))
295}
296
297fn boundary_violation_key(
298 item: &fallow_types::output_dead_code::BoundaryViolationFinding,
299 root: &Path,
300) -> String {
301 format!(
302 "boundary-violation:{}:{}:{}",
303 relative_key_path(&item.violation.from_path, root),
304 relative_key_path(&item.violation.to_path, root),
305 item.violation.import_specifier
306 )
307}
308
309fn boundary_coverage_key(
310 item: &fallow_types::output_dead_code::BoundaryCoverageViolationFinding,
311 root: &Path,
312) -> String {
313 format!(
314 "boundary-coverage:{}",
315 relative_key_path(&item.violation.path, root)
316 )
317}
318
319fn boundary_call_key(
320 item: &fallow_types::output_dead_code::BoundaryCallViolationFinding,
321 root: &Path,
322) -> String {
323 format!(
324 "boundary-call:{}:{}",
325 relative_key_path(&item.violation.path, root),
326 item.violation.callee
327 )
328}
329
330fn policy_violation_key(
331 item: &fallow_types::output_dead_code::PolicyViolationFinding,
332 root: &Path,
333) -> String {
334 format!(
335 "policy-violation:{}:{}/{}:{}",
336 relative_key_path(&item.violation.path, root),
337 item.violation.pack,
338 item.violation.rule_id,
339 item.violation.matched
340 )
341}
342
343fn stale_suppression_key(item: &fallow_types::results::StaleSuppression, root: &Path) -> String {
344 let rule_id = if item.missing_reason {
345 "missing-suppression-reason"
346 } else {
347 "stale-suppression"
348 };
349 format!(
350 "{rule_id}:{}:{}",
351 relative_key_path(&item.path, root),
352 item.description()
353 )
354}
355
356fn unresolved_catalog_reference_key(
357 item: &fallow_types::output_dead_code::UnresolvedCatalogReferenceFinding,
358 root: &Path,
359) -> String {
360 format!(
361 "unresolved-catalog-reference:{}:{}:{}:{}",
362 relative_key_path(&item.reference.path, root),
363 item.reference.line,
364 item.reference.catalog_name,
365 item.reference.entry_name
366 )
367}
368
369fn unused_dependency_override_key(
370 item: &fallow_types::output_dead_code::UnusedDependencyOverrideFinding,
371 root: &Path,
372) -> String {
373 format!(
374 "unused-dependency-override:{}:{}:{}",
375 relative_key_path(&item.entry.path, root),
376 item.entry.line,
377 item.entry.raw_key
378 )
379}
380
381fn misconfigured_dependency_override_key(
382 item: &fallow_types::output_dead_code::MisconfiguredDependencyOverrideFinding,
383 root: &Path,
384) -> String {
385 format!(
386 "misconfigured-dependency-override:{}:{}:{}",
387 relative_key_path(&item.entry.path, root),
388 item.entry.line,
389 item.entry.raw_key
390 )
391}
392
393#[derive(Clone, Copy)]
410#[allow(
411 clippy::struct_field_names,
412 reason = "field names mirror the AnalysisResults field names so the destructure stays shorthand"
413)]
414struct DependencyFindingSlices<'a> {
415 unused_dependencies: &'a [fallow_types::output_dead_code::UnusedDependencyFinding],
416 unused_dev_dependencies: &'a [fallow_types::output_dead_code::UnusedDevDependencyFinding],
417 unused_optional_dependencies:
418 &'a [fallow_types::output_dead_code::UnusedOptionalDependencyFinding],
419 unlisted_dependencies: &'a [fallow_types::output_dead_code::UnlistedDependencyFinding],
420 type_only_dependencies: &'a [fallow_types::output_dead_code::TypeOnlyDependencyFinding],
421 test_only_dependencies: &'a [fallow_types::output_dead_code::TestOnlyDependencyFinding],
422 dev_dependencies_in_production:
423 &'a [fallow_types::output_dead_code::DevDependencyInProductionFinding],
424}
425
426#[derive(Clone, Copy)]
429struct FrameworkFindingSlices<'a> {
430 unprovided_injects: &'a [fallow_types::output_dead_code::UnprovidedInjectFinding],
431 unrendered_components: &'a [fallow_types::output_dead_code::UnrenderedComponentFinding],
432 unused_server_actions: &'a [fallow_types::output_dead_code::UnusedServerActionFinding],
433 unused_load_data_keys: &'a [fallow_types::output_dead_code::UnusedLoadDataKeyFinding],
434 route_collisions: &'a [fallow_types::output_dead_code::RouteCollisionFinding],
435 dynamic_segment_name_conflicts:
436 &'a [fallow_types::output_dead_code::DynamicSegmentNameConflictFinding],
437}
438
439pub fn dead_code_keys(
447 results: &fallow_types::results::AnalysisResults,
448 root: &Path,
449) -> FxHashSet<String> {
450 let mut collector = DeadCodeKeyCollector::new(root);
451 collector.add_all_findings(results);
452 collector.into_keys()
453}
454
455impl DeadCodeKeyCollector<'_> {
456 #[expect(
457 clippy::too_many_lines,
458 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"
459 )]
460 fn add_all_findings(&mut self, results: &fallow_types::results::AnalysisResults) {
461 let fallow_types::results::AnalysisResults {
462 unused_files,
463 unused_exports,
464 unused_types,
465 private_type_leaks,
466 unused_dependencies,
467 unused_dev_dependencies,
468 unused_optional_dependencies,
469 unused_enum_members,
470 unused_class_members,
471 unused_store_members,
472 unresolved_imports,
473 unlisted_dependencies,
474 duplicate_exports,
475 type_only_dependencies,
476 test_only_dependencies,
477 dev_dependencies_in_production,
478 circular_dependencies,
479 re_export_cycles,
480 boundary_violations,
481 boundary_coverage_violations,
482 boundary_call_violations,
483 policy_violations,
484 stale_suppressions,
485 unused_catalog_entries,
486 empty_catalog_groups,
487 unresolved_catalog_references,
488 unused_dependency_overrides,
489 misconfigured_dependency_overrides,
490 invalid_client_exports,
491 mixed_client_server_barrels,
492 misplaced_directives,
493 unprovided_injects,
494 unrendered_components,
495 unused_component_props,
496 unused_component_emits,
497 unused_component_inputs,
498 unused_component_outputs,
499 unused_svelte_events,
500 unused_server_actions,
501 unused_load_data_keys,
502 unused_load_data_keys_global_abstain: _unused_load_data_keys_global_abstain,
503 route_collisions,
504 dynamic_segment_name_conflicts,
505 suppression_count: _suppression_count,
507 unused_component_props_exempted: _unused_component_props_exempted,
508 active_suppressions: _active_suppressions,
509 feature_flags: _feature_flags,
510 security_findings: _security_findings,
513 security_unresolved_edge_files: _security_unresolved_edge_files,
514 security_unresolved_callee_sites: _security_unresolved_callee_sites,
515 security_unresolved_callee_diagnostics: _security_unresolved_callee_diagnostics,
516 prop_drilling_chains: _prop_drilling_chains,
519 thin_wrappers: _thin_wrappers,
522 duplicate_prop_shapes: _duplicate_prop_shapes,
526 export_usages: _export_usages,
529 entry_point_summary: _entry_point_summary,
530 render_fan_in: _render_fan_in,
533 react_component_intel: _react_component_intel,
536 } = results;
537
538 self.add_core_findings(
539 unused_files,
540 unused_exports,
541 unused_types,
542 private_type_leaks,
543 );
544 self.add_client_directive_findings(
545 invalid_client_exports,
546 mixed_client_server_barrels,
547 misplaced_directives,
548 );
549 self.add_dependency_findings(&DependencyFindingSlices {
550 unused_dependencies,
551 unused_dev_dependencies,
552 unused_optional_dependencies,
553 unlisted_dependencies,
554 type_only_dependencies,
555 test_only_dependencies,
556 dev_dependencies_in_production,
557 });
558 self.add_dependency_override_findings(
559 unused_dependency_overrides,
560 misconfigured_dependency_overrides,
561 );
562 self.add_member_findings(
563 unused_enum_members,
564 unused_class_members,
565 unused_store_members,
566 );
567 self.add_component_contract_findings(
568 unused_component_props,
569 unused_component_emits,
570 unused_component_inputs,
571 unused_component_outputs,
572 unused_svelte_events,
573 );
574 self.add_graph_findings(
575 unresolved_imports,
576 duplicate_exports,
577 circular_dependencies,
578 re_export_cycles,
579 );
580 self.add_boundary_findings(
581 boundary_violations,
582 boundary_coverage_violations,
583 boundary_call_violations,
584 policy_violations,
585 stale_suppressions,
586 );
587 self.add_catalog_findings(
588 unresolved_catalog_references,
589 unused_catalog_entries,
590 empty_catalog_groups,
591 );
592 self.add_framework_findings(&FrameworkFindingSlices {
593 unprovided_injects,
594 unrendered_components,
595 unused_server_actions,
596 unused_load_data_keys,
597 route_collisions,
598 dynamic_segment_name_conflicts,
599 });
600 }
601}
602
603struct DeadCodeKeyCollector<'a> {
604 root: &'a Path,
605 keys: FxHashSet<String>,
606}
607
608impl<'a> DeadCodeKeyCollector<'a> {
609 fn new(root: &'a Path) -> Self {
610 Self {
611 root,
612 keys: FxHashSet::default(),
613 }
614 }
615
616 fn into_keys(self) -> FxHashSet<String> {
617 self.keys
618 }
619
620 fn insert(&mut self, key: String) {
621 self.keys.insert(key);
622 }
623
624 fn add_core_findings(
625 &mut self,
626 unused_files: &[fallow_types::output_dead_code::UnusedFileFinding],
627 unused_exports: &[fallow_types::output_dead_code::UnusedExportFinding],
628 unused_types: &[fallow_types::output_dead_code::UnusedTypeFinding],
629 private_type_leaks: &[fallow_types::output_dead_code::PrivateTypeLeakFinding],
630 ) {
631 self.add_unused_files(unused_files);
632 self.add_unused_exports(unused_exports);
633 self.add_unused_types(unused_types);
634 self.add_private_type_leaks(private_type_leaks);
635 }
636
637 fn add_client_directive_findings(
638 &mut self,
639 invalid_client_exports: &[fallow_types::output_dead_code::InvalidClientExportFinding],
640 mixed_client_server_barrels: &[fallow_types::output_dead_code::MixedClientServerBarrelFinding],
641 misplaced_directives: &[fallow_types::output_dead_code::MisplacedDirectiveFinding],
642 ) {
643 self.add_invalid_client_exports(invalid_client_exports);
644 self.add_mixed_client_server_barrels(mixed_client_server_barrels);
645 self.add_misplaced_directives(misplaced_directives);
646 }
647
648 fn add_dependency_findings(&mut self, deps: &DependencyFindingSlices<'_>) {
649 let DependencyFindingSlices {
650 unused_dependencies,
651 unused_dev_dependencies,
652 unused_optional_dependencies,
653 unlisted_dependencies,
654 type_only_dependencies,
655 test_only_dependencies,
656 dev_dependencies_in_production,
657 } = *deps;
658 self.add_unused_dependencies(unused_dependencies);
659 self.add_unused_dev_dependencies(unused_dev_dependencies);
660 self.add_unused_optional_dependencies(unused_optional_dependencies);
661 self.add_unlisted_dependencies(unlisted_dependencies);
662 self.add_type_only_dependencies(type_only_dependencies);
663 self.add_test_only_dependencies(test_only_dependencies);
664 self.add_dev_dependencies_in_production(dev_dependencies_in_production);
665 }
666
667 fn add_dependency_override_findings(
668 &mut self,
669 unused_dependency_overrides: &[fallow_types::output_dead_code::UnusedDependencyOverrideFinding],
670 misconfigured_dependency_overrides: &[fallow_types::output_dead_code::MisconfiguredDependencyOverrideFinding],
671 ) {
672 self.add_unused_dependency_overrides(unused_dependency_overrides);
673 self.add_misconfigured_dependency_overrides(misconfigured_dependency_overrides);
674 }
675
676 fn add_member_findings(
677 &mut self,
678 unused_enum_members: &[fallow_types::output_dead_code::UnusedEnumMemberFinding],
679 unused_class_members: &[fallow_types::output_dead_code::UnusedClassMemberFinding],
680 unused_store_members: &[fallow_types::output_dead_code::UnusedStoreMemberFinding],
681 ) {
682 self.add_unused_enum_members(unused_enum_members);
683 self.add_unused_class_members(unused_class_members);
684 self.add_unused_store_members(unused_store_members);
685 }
686
687 fn add_component_contract_findings(
688 &mut self,
689 unused_component_props: &[fallow_types::output_dead_code::UnusedComponentPropFinding],
690 unused_component_emits: &[fallow_types::output_dead_code::UnusedComponentEmitFinding],
691 unused_component_inputs: &[fallow_types::output_dead_code::UnusedComponentInputFinding],
692 unused_component_outputs: &[fallow_types::output_dead_code::UnusedComponentOutputFinding],
693 unused_svelte_events: &[fallow_types::output_dead_code::UnusedSvelteEventFinding],
694 ) {
695 self.add_unused_component_props(unused_component_props);
696 self.add_unused_component_emits(unused_component_emits);
697 self.add_unused_component_inputs(unused_component_inputs);
698 self.add_unused_component_outputs(unused_component_outputs);
699 self.add_unused_svelte_events(unused_svelte_events);
700 }
701
702 fn add_graph_findings(
703 &mut self,
704 unresolved_imports: &[fallow_types::output_dead_code::UnresolvedImportFinding],
705 duplicate_exports: &[fallow_types::output_dead_code::DuplicateExportFinding],
706 circular_dependencies: &[fallow_types::output_dead_code::CircularDependencyFinding],
707 re_export_cycles: &[fallow_types::output_dead_code::ReExportCycleFinding],
708 ) {
709 self.add_unresolved_imports(unresolved_imports);
710 self.add_duplicate_exports(duplicate_exports);
711 self.add_circular_dependencies(circular_dependencies);
712 self.add_re_export_cycles(re_export_cycles);
713 }
714
715 fn add_boundary_findings(
716 &mut self,
717 boundary_violations: &[fallow_types::output_dead_code::BoundaryViolationFinding],
718 boundary_coverage_violations: &[fallow_types::output_dead_code::BoundaryCoverageViolationFinding],
719 boundary_call_violations: &[fallow_types::output_dead_code::BoundaryCallViolationFinding],
720 policy_violations: &[fallow_types::output_dead_code::PolicyViolationFinding],
721 stale_suppressions: &[fallow_types::results::StaleSuppression],
722 ) {
723 self.add_boundary_violations(boundary_violations);
724 self.add_boundary_coverage_violations(boundary_coverage_violations);
725 self.add_boundary_call_violations(boundary_call_violations);
726 self.add_policy_violations(policy_violations);
727 self.add_stale_suppressions(stale_suppressions);
728 }
729
730 fn add_catalog_findings(
731 &mut self,
732 unresolved_catalog_references: &[fallow_types::output_dead_code::UnresolvedCatalogReferenceFinding],
733 unused_catalog_entries: &[fallow_types::output_dead_code::UnusedCatalogEntryFinding],
734 empty_catalog_groups: &[fallow_types::output_dead_code::EmptyCatalogGroupFinding],
735 ) {
736 self.add_unresolved_catalog_references(unresolved_catalog_references);
737 self.add_unused_catalog_entries(unused_catalog_entries);
738 self.add_empty_catalog_groups(empty_catalog_groups);
739 }
740
741 fn add_framework_findings(&mut self, framework: &FrameworkFindingSlices<'_>) {
742 let FrameworkFindingSlices {
743 unprovided_injects,
744 unrendered_components,
745 unused_server_actions,
746 unused_load_data_keys,
747 route_collisions,
748 dynamic_segment_name_conflicts,
749 } = *framework;
750 self.add_unprovided_injects(unprovided_injects);
751 self.add_unrendered_components(unrendered_components);
752 self.add_unused_server_actions(unused_server_actions);
753 self.add_unused_load_data_keys(unused_load_data_keys);
754 self.add_route_collisions(route_collisions);
755 self.add_dynamic_segment_name_conflicts(dynamic_segment_name_conflicts);
756 }
757
758 fn add_unused_files(&mut self, items: &[fallow_types::output_dead_code::UnusedFileFinding]) {
759 for item in items {
760 self.insert(format!(
761 "unused-file:{}",
762 relative_key_path(&item.file.path, self.root)
763 ));
764 }
765 }
766
767 fn add_unused_exports(
768 &mut self,
769 items: &[fallow_types::output_dead_code::UnusedExportFinding],
770 ) {
771 for item in items {
772 self.insert(format!(
773 "unused-export:{}:{}",
774 relative_key_path(&item.export.path, self.root),
775 item.export.export_name
776 ));
777 }
778 }
779
780 fn add_unused_types(&mut self, items: &[fallow_types::output_dead_code::UnusedTypeFinding]) {
781 for item in items {
782 self.insert(format!(
783 "unused-type:{}:{}",
784 relative_key_path(&item.export.path, self.root),
785 item.export.export_name
786 ));
787 }
788 }
789
790 fn add_private_type_leaks(
791 &mut self,
792 items: &[fallow_types::output_dead_code::PrivateTypeLeakFinding],
793 ) {
794 for item in items {
795 self.insert(format!(
796 "private-type-leak:{}:{}:{}",
797 relative_key_path(&item.leak.path, self.root),
798 item.leak.export_name,
799 item.leak.type_name
800 ));
801 }
802 }
803
804 fn add_invalid_client_exports(
805 &mut self,
806 items: &[fallow_types::output_dead_code::InvalidClientExportFinding],
807 ) {
808 for item in items {
809 self.insert(invalid_client_export_key(&item.export, self.root));
810 }
811 }
812
813 fn add_mixed_client_server_barrels(
814 &mut self,
815 items: &[fallow_types::output_dead_code::MixedClientServerBarrelFinding],
816 ) {
817 for item in items {
818 self.insert(mixed_client_server_barrel_key(&item.barrel, self.root));
819 }
820 }
821
822 fn add_misplaced_directives(
823 &mut self,
824 items: &[fallow_types::output_dead_code::MisplacedDirectiveFinding],
825 ) {
826 for item in items {
827 self.insert(misplaced_directive_key(&item.directive_site, self.root));
828 }
829 }
830
831 fn add_unprovided_injects(
832 &mut self,
833 items: &[fallow_types::output_dead_code::UnprovidedInjectFinding],
834 ) {
835 for item in items {
836 self.insert(unprovided_inject_key(&item.inject, self.root));
837 }
838 }
839
840 fn add_unrendered_components(
841 &mut self,
842 items: &[fallow_types::output_dead_code::UnrenderedComponentFinding],
843 ) {
844 for item in items {
845 self.insert(unrendered_component_key(&item.component, self.root));
846 }
847 }
848
849 fn add_unused_component_props(
850 &mut self,
851 items: &[fallow_types::output_dead_code::UnusedComponentPropFinding],
852 ) {
853 for item in items {
854 self.insert(unused_component_prop_key(&item.prop, self.root));
855 }
856 }
857
858 fn add_unused_component_emits(
859 &mut self,
860 items: &[fallow_types::output_dead_code::UnusedComponentEmitFinding],
861 ) {
862 for item in items {
863 self.insert(unused_component_emit_key(&item.emit, self.root));
864 }
865 }
866
867 fn add_unused_component_inputs(
868 &mut self,
869 items: &[fallow_types::output_dead_code::UnusedComponentInputFinding],
870 ) {
871 for item in items {
872 self.insert(unused_component_input_key(&item.input, self.root));
873 }
874 }
875
876 fn add_unused_component_outputs(
877 &mut self,
878 items: &[fallow_types::output_dead_code::UnusedComponentOutputFinding],
879 ) {
880 for item in items {
881 self.insert(unused_component_output_key(&item.output, self.root));
882 }
883 }
884
885 fn add_unused_svelte_events(
886 &mut self,
887 items: &[fallow_types::output_dead_code::UnusedSvelteEventFinding],
888 ) {
889 for item in items {
890 self.insert(unused_svelte_event_key(&item.event, self.root));
891 }
892 }
893
894 fn add_unused_server_actions(
895 &mut self,
896 items: &[fallow_types::output_dead_code::UnusedServerActionFinding],
897 ) {
898 for item in items {
899 self.insert(unused_server_action_key(&item.action, self.root));
900 }
901 }
902
903 fn add_unused_load_data_keys(
904 &mut self,
905 items: &[fallow_types::output_dead_code::UnusedLoadDataKeyFinding],
906 ) {
907 for item in items {
908 self.insert(unused_load_data_key_key(&item.key, self.root));
909 }
910 }
911
912 fn add_route_collisions(
913 &mut self,
914 items: &[fallow_types::output_dead_code::RouteCollisionFinding],
915 ) {
916 for item in items {
917 self.insert(route_collision_key(&item.collision, self.root));
918 }
919 }
920
921 fn add_dynamic_segment_name_conflicts(
922 &mut self,
923 items: &[fallow_types::output_dead_code::DynamicSegmentNameConflictFinding],
924 ) {
925 for item in items {
926 self.insert(dynamic_segment_name_conflict_key(&item.conflict, self.root));
927 }
928 }
929
930 fn add_unused_dependencies(
931 &mut self,
932 items: &[fallow_types::output_dead_code::UnusedDependencyFinding],
933 ) {
934 for item in items {
935 self.insert(unused_dependency_key(&item.dep, self.root));
936 }
937 }
938
939 fn add_unused_dev_dependencies(
940 &mut self,
941 items: &[fallow_types::output_dead_code::UnusedDevDependencyFinding],
942 ) {
943 for item in items {
944 self.insert(unused_dependency_key(&item.dep, self.root));
945 }
946 }
947
948 fn add_unused_optional_dependencies(
949 &mut self,
950 items: &[fallow_types::output_dead_code::UnusedOptionalDependencyFinding],
951 ) {
952 for item in items {
953 self.insert(unused_dependency_key(&item.dep, self.root));
954 }
955 }
956
957 fn add_unused_enum_members(
958 &mut self,
959 items: &[fallow_types::output_dead_code::UnusedEnumMemberFinding],
960 ) {
961 for item in items {
962 self.insert(unused_member_key(
963 "unused-enum-member",
964 &item.member,
965 self.root,
966 ));
967 }
968 }
969
970 fn add_unused_class_members(
971 &mut self,
972 items: &[fallow_types::output_dead_code::UnusedClassMemberFinding],
973 ) {
974 for item in items {
975 self.insert(unused_member_key(
976 "unused-class-member",
977 &item.member,
978 self.root,
979 ));
980 }
981 }
982
983 fn add_unused_store_members(
984 &mut self,
985 items: &[fallow_types::output_dead_code::UnusedStoreMemberFinding],
986 ) {
987 for item in items {
988 self.insert(unused_member_key(
989 "unused-store-member",
990 &item.member,
991 self.root,
992 ));
993 }
994 }
995
996 fn add_unresolved_imports(
997 &mut self,
998 items: &[fallow_types::output_dead_code::UnresolvedImportFinding],
999 ) {
1000 for item in items {
1001 self.insert(format!(
1002 "unresolved-import:{}:{}",
1003 relative_key_path(&item.import.path, self.root),
1004 item.import.specifier
1005 ));
1006 }
1007 }
1008
1009 fn add_unlisted_dependencies(
1010 &mut self,
1011 items: &[fallow_types::output_dead_code::UnlistedDependencyFinding],
1012 ) {
1013 for item in items {
1014 self.insert(unlisted_dependency_key(&item.dep, self.root));
1015 }
1016 }
1017
1018 fn add_duplicate_exports(
1019 &mut self,
1020 items: &[fallow_types::output_dead_code::DuplicateExportFinding],
1021 ) {
1022 for item in items {
1023 self.insert(duplicate_export_key(item, self.root));
1024 }
1025 }
1026
1027 fn add_type_only_dependencies(
1028 &mut self,
1029 items: &[fallow_types::output_dead_code::TypeOnlyDependencyFinding],
1030 ) {
1031 for item in items {
1032 self.insert(format!(
1033 "type-only-dependency:{}:{}",
1034 relative_key_path(&item.dep.path, self.root),
1035 item.dep.package_name
1036 ));
1037 }
1038 }
1039
1040 fn add_test_only_dependencies(
1041 &mut self,
1042 items: &[fallow_types::output_dead_code::TestOnlyDependencyFinding],
1043 ) {
1044 for item in items {
1045 self.insert(format!(
1046 "test-only-dependency:{}:{}",
1047 relative_key_path(&item.dep.path, self.root),
1048 item.dep.package_name
1049 ));
1050 }
1051 }
1052
1053 fn add_dev_dependencies_in_production(
1054 &mut self,
1055 items: &[fallow_types::output_dead_code::DevDependencyInProductionFinding],
1056 ) {
1057 for item in items {
1058 self.insert(format!(
1059 "dev-dependency-in-production:{}:{}",
1060 relative_key_path(&item.dep.path, self.root),
1061 item.dep.package_name
1062 ));
1063 }
1064 }
1065
1066 fn add_circular_dependencies(
1067 &mut self,
1068 items: &[fallow_types::output_dead_code::CircularDependencyFinding],
1069 ) {
1070 for item in items {
1071 self.insert(circular_dependency_key(item, self.root));
1072 }
1073 }
1074
1075 fn add_re_export_cycles(
1076 &mut self,
1077 items: &[fallow_types::output_dead_code::ReExportCycleFinding],
1078 ) {
1079 for item in items {
1080 self.insert(re_export_cycle_key(item, self.root));
1081 }
1082 }
1083
1084 fn add_boundary_violations(
1085 &mut self,
1086 items: &[fallow_types::output_dead_code::BoundaryViolationFinding],
1087 ) {
1088 for item in items {
1089 self.insert(boundary_violation_key(item, self.root));
1090 }
1091 }
1092
1093 fn add_boundary_coverage_violations(
1094 &mut self,
1095 items: &[fallow_types::output_dead_code::BoundaryCoverageViolationFinding],
1096 ) {
1097 for item in items {
1098 self.insert(boundary_coverage_key(item, self.root));
1099 }
1100 }
1101
1102 fn add_boundary_call_violations(
1103 &mut self,
1104 items: &[fallow_types::output_dead_code::BoundaryCallViolationFinding],
1105 ) {
1106 for item in items {
1107 self.insert(boundary_call_key(item, self.root));
1108 }
1109 }
1110
1111 fn add_policy_violations(
1112 &mut self,
1113 items: &[fallow_types::output_dead_code::PolicyViolationFinding],
1114 ) {
1115 for item in items {
1116 self.insert(policy_violation_key(item, self.root));
1117 }
1118 }
1119
1120 fn add_stale_suppressions(&mut self, items: &[fallow_types::results::StaleSuppression]) {
1121 for item in items {
1122 self.insert(stale_suppression_key(item, self.root));
1123 }
1124 }
1125
1126 fn add_unresolved_catalog_references(
1127 &mut self,
1128 items: &[fallow_types::output_dead_code::UnresolvedCatalogReferenceFinding],
1129 ) {
1130 for item in items {
1131 self.insert(unresolved_catalog_reference_key(item, self.root));
1132 }
1133 }
1134
1135 fn add_unused_catalog_entries(
1136 &mut self,
1137 items: &[fallow_types::output_dead_code::UnusedCatalogEntryFinding],
1138 ) {
1139 for item in items {
1140 self.insert(unused_catalog_entry_key(&item.entry, self.root));
1141 }
1142 }
1143
1144 fn add_empty_catalog_groups(
1145 &mut self,
1146 items: &[fallow_types::output_dead_code::EmptyCatalogGroupFinding],
1147 ) {
1148 for item in items {
1149 self.insert(empty_catalog_group_key(&item.group, self.root));
1150 }
1151 }
1152
1153 fn add_unused_dependency_overrides(
1154 &mut self,
1155 items: &[fallow_types::output_dead_code::UnusedDependencyOverrideFinding],
1156 ) {
1157 for item in items {
1158 self.insert(unused_dependency_override_key(item, self.root));
1159 }
1160 }
1161
1162 fn add_misconfigured_dependency_overrides(
1163 &mut self,
1164 items: &[fallow_types::output_dead_code::MisconfiguredDependencyOverrideFinding],
1165 ) {
1166 for item in items {
1167 self.insert(misconfigured_dependency_override_key(item, self.root));
1168 }
1169 }
1170}
1171
1172#[expect(
1192 clippy::implicit_hasher,
1193 reason = "fallow standardizes on FxHashSet across audit attribution keys"
1194)]
1195pub fn retain_introduced_dead_code(
1196 results: &mut fallow_types::results::AnalysisResults,
1197 root: &Path,
1198 base: Option<&FxHashSet<String>>,
1199) {
1200 let Some(base) = base else {
1201 return;
1202 };
1203
1204 let introduced = introduced_dead_code_keys(results, root, base);
1212 classify_introduced_dead_code_fields(results);
1213
1214 retain_introduced_fast_paths(
1219 &mut results.unused_files,
1220 &mut results.unused_exports,
1221 &mut results.unused_types,
1222 root,
1223 base,
1224 );
1225 retain_introduced_core_findings(results, root, &introduced);
1226 retain_introduced_dependency_and_graph_findings(results, root, &introduced);
1227 retain_introduced_workspace_findings(results, root, &introduced);
1228 retain_introduced_framework_findings(results, root, &introduced);
1229}
1230
1231fn introduced_dead_code_keys(
1232 results: &fallow_types::results::AnalysisResults,
1233 root: &Path,
1234 base: &FxHashSet<String>,
1235) -> FxHashSet<String> {
1236 dead_code_keys(results, root)
1237 .into_iter()
1238 .filter(|key| !base.contains(key))
1239 .collect()
1240}
1241
1242fn classify_introduced_dead_code_fields(results: &fallow_types::results::AnalysisResults) {
1243 let fallow_types::results::AnalysisResults {
1244 unused_files: _unused_files,
1245 unused_exports: _unused_exports,
1246 unused_types: _unused_types,
1247 private_type_leaks: _private_type_leaks,
1248 unused_dependencies: _unused_dependencies,
1249 unused_dev_dependencies: _unused_dev_dependencies,
1250 unused_optional_dependencies: _unused_optional_dependencies,
1251 unused_enum_members: _unused_enum_members,
1252 unused_class_members: _unused_class_members,
1253 unused_store_members: _unused_store_members,
1254 unresolved_imports: _unresolved_imports,
1255 unlisted_dependencies: _unlisted_dependencies,
1256 duplicate_exports: _duplicate_exports,
1257 type_only_dependencies: _type_only_dependencies,
1258 test_only_dependencies: _test_only_dependencies,
1259 dev_dependencies_in_production: _dev_dependencies_in_production,
1260 circular_dependencies: _circular_dependencies,
1261 re_export_cycles: _re_export_cycles,
1262 boundary_violations: _boundary_violations,
1263 boundary_coverage_violations: _boundary_coverage_violations,
1264 boundary_call_violations: _boundary_call_violations,
1265 policy_violations: _policy_violations,
1266 stale_suppressions: _stale_suppressions,
1267 unused_catalog_entries: _unused_catalog_entries,
1268 empty_catalog_groups: _empty_catalog_groups,
1269 unresolved_catalog_references: _unresolved_catalog_references,
1270 unused_dependency_overrides: _unused_dependency_overrides,
1271 misconfigured_dependency_overrides: _misconfigured_dependency_overrides,
1272 invalid_client_exports: _invalid_client_exports,
1273 mixed_client_server_barrels: _mixed_client_server_barrels,
1274 misplaced_directives: _misplaced_directives,
1275 unprovided_injects: _unprovided_injects,
1276 unrendered_components: _unrendered_components,
1277 unused_component_props: _unused_component_props,
1278 unused_component_emits: _unused_component_emits,
1279 unused_component_inputs: _unused_component_inputs,
1280 unused_component_outputs: _unused_component_outputs,
1281 unused_svelte_events: _unused_svelte_events,
1282 unused_server_actions: _unused_server_actions,
1283 unused_load_data_keys: _unused_load_data_keys,
1284 unused_load_data_keys_global_abstain: _unused_load_data_keys_global_abstain,
1285 route_collisions: _route_collisions,
1286 dynamic_segment_name_conflicts: _dynamic_segment_name_conflicts,
1287 suppression_count: _suppression_count,
1290 unused_component_props_exempted: _unused_component_props_exempted,
1291 active_suppressions: _active_suppressions,
1292 feature_flags: _feature_flags,
1293 security_findings: _security_findings,
1297 security_unresolved_edge_files: _security_unresolved_edge_files,
1298 security_unresolved_callee_sites: _security_unresolved_callee_sites,
1299 security_unresolved_callee_diagnostics: _security_unresolved_callee_diagnostics,
1300 prop_drilling_chains: _prop_drilling_chains,
1303 thin_wrappers: _thin_wrappers,
1306 duplicate_prop_shapes: _duplicate_prop_shapes,
1309 export_usages: _export_usages,
1312 entry_point_summary: _entry_point_summary,
1313 render_fan_in: _render_fan_in,
1316 react_component_intel: _react_component_intel,
1319 } = results;
1320}
1321
1322fn retain_introduced_fast_paths(
1323 unused_files: &mut Vec<fallow_types::output_dead_code::UnusedFileFinding>,
1324 unused_exports: &mut Vec<fallow_types::output_dead_code::UnusedExportFinding>,
1325 unused_types: &mut Vec<fallow_types::output_dead_code::UnusedTypeFinding>,
1326 root: &Path,
1327 base: &FxHashSet<String>,
1328) {
1329 unused_files.retain(|item| {
1330 !base.contains(&format!(
1331 "unused-file:{}",
1332 relative_key_path(&item.file.path, root)
1333 ))
1334 });
1335 unused_exports.retain(|item| {
1336 !base.contains(&format!(
1337 "unused-export:{}:{}",
1338 relative_key_path(&item.export.path, root),
1339 item.export.export_name
1340 ))
1341 });
1342 unused_types.retain(|item| {
1343 !base.contains(&format!(
1344 "unused-type:{}:{}",
1345 relative_key_path(&item.export.path, root),
1346 item.export.export_name
1347 ))
1348 });
1349}
1350
1351fn keep_introduced(introduced: &FxHashSet<String>, key: impl AsRef<str>) -> bool {
1352 introduced.contains(key.as_ref())
1353}
1354
1355fn retain_introduced_core_findings(
1356 results: &mut fallow_types::results::AnalysisResults,
1357 root: &Path,
1358 introduced: &FxHashSet<String>,
1359) {
1360 results.private_type_leaks.retain(|item| {
1361 keep_introduced(
1362 introduced,
1363 format!(
1364 "private-type-leak:{}:{}:{}",
1365 relative_key_path(&item.leak.path, root),
1366 item.leak.export_name,
1367 item.leak.type_name
1368 ),
1369 )
1370 });
1371 results.unused_enum_members.retain(|item| {
1372 keep_introduced(
1373 introduced,
1374 unused_member_key("unused-enum-member", &item.member, root),
1375 )
1376 });
1377 results.unused_class_members.retain(|item| {
1378 keep_introduced(
1379 introduced,
1380 unused_member_key("unused-class-member", &item.member, root),
1381 )
1382 });
1383 results.unused_store_members.retain(|item| {
1384 keep_introduced(
1385 introduced,
1386 unused_member_key("unused-store-member", &item.member, root),
1387 )
1388 });
1389 results.unresolved_imports.retain(|item| {
1390 keep_introduced(
1391 introduced,
1392 format!(
1393 "unresolved-import:{}:{}",
1394 relative_key_path(&item.import.path, root),
1395 item.import.specifier
1396 ),
1397 )
1398 });
1399}
1400
1401fn retain_introduced_dependency_and_graph_findings(
1402 results: &mut fallow_types::results::AnalysisResults,
1403 root: &Path,
1404 introduced: &FxHashSet<String>,
1405) {
1406 results
1407 .unused_dependencies
1408 .retain(|item| keep_introduced(introduced, unused_dependency_key(&item.dep, root)));
1409 results
1410 .unused_dev_dependencies
1411 .retain(|item| keep_introduced(introduced, unused_dependency_key(&item.dep, root)));
1412 results
1413 .unused_optional_dependencies
1414 .retain(|item| keep_introduced(introduced, unused_dependency_key(&item.dep, root)));
1415 results
1416 .unlisted_dependencies
1417 .retain(|item| keep_introduced(introduced, unlisted_dependency_key(&item.dep, root)));
1418 results
1419 .duplicate_exports
1420 .retain(|item| keep_introduced(introduced, duplicate_export_key(item, root)));
1421 results.type_only_dependencies.retain(|item| {
1422 keep_introduced(
1423 introduced,
1424 format!(
1425 "type-only-dependency:{}:{}",
1426 relative_key_path(&item.dep.path, root),
1427 item.dep.package_name
1428 ),
1429 )
1430 });
1431 results.test_only_dependencies.retain(|item| {
1432 keep_introduced(
1433 introduced,
1434 format!(
1435 "test-only-dependency:{}:{}",
1436 relative_key_path(&item.dep.path, root),
1437 item.dep.package_name
1438 ),
1439 )
1440 });
1441 results
1442 .circular_dependencies
1443 .retain(|item| keep_introduced(introduced, circular_dependency_key(item, root)));
1444 results
1445 .re_export_cycles
1446 .retain(|item| keep_introduced(introduced, re_export_cycle_key(item, root)));
1447 results
1448 .boundary_violations
1449 .retain(|item| keep_introduced(introduced, boundary_violation_key(item, root)));
1450 results
1451 .boundary_coverage_violations
1452 .retain(|item| keep_introduced(introduced, boundary_coverage_key(item, root)));
1453 results
1454 .boundary_call_violations
1455 .retain(|item| keep_introduced(introduced, boundary_call_key(item, root)));
1456 results
1457 .policy_violations
1458 .retain(|item| keep_introduced(introduced, policy_violation_key(item, root)));
1459 results
1460 .stale_suppressions
1461 .retain(|item| keep_introduced(introduced, stale_suppression_key(item, root)));
1462}
1463
1464fn retain_introduced_workspace_findings(
1465 results: &mut fallow_types::results::AnalysisResults,
1466 root: &Path,
1467 introduced: &FxHashSet<String>,
1468) {
1469 results
1470 .unresolved_catalog_references
1471 .retain(|item| keep_introduced(introduced, unresolved_catalog_reference_key(item, root)));
1472 results
1473 .unused_catalog_entries
1474 .retain(|item| keep_introduced(introduced, unused_catalog_entry_key(&item.entry, root)));
1475 results
1476 .empty_catalog_groups
1477 .retain(|item| keep_introduced(introduced, empty_catalog_group_key(&item.group, root)));
1478 results
1479 .unused_dependency_overrides
1480 .retain(|item| keep_introduced(introduced, unused_dependency_override_key(item, root)));
1481 results.misconfigured_dependency_overrides.retain(|item| {
1482 keep_introduced(
1483 introduced,
1484 misconfigured_dependency_override_key(item, root),
1485 )
1486 });
1487}
1488
1489fn retain_introduced_framework_findings(
1490 results: &mut fallow_types::results::AnalysisResults,
1491 root: &Path,
1492 introduced: &FxHashSet<String>,
1493) {
1494 results
1495 .invalid_client_exports
1496 .retain(|item| keep_introduced(introduced, invalid_client_export_key(&item.export, root)));
1497 results.mixed_client_server_barrels.retain(|item| {
1498 keep_introduced(
1499 introduced,
1500 mixed_client_server_barrel_key(&item.barrel, root),
1501 )
1502 });
1503 results.misplaced_directives.retain(|item| {
1504 keep_introduced(
1505 introduced,
1506 misplaced_directive_key(&item.directive_site, root),
1507 )
1508 });
1509 results
1510 .unprovided_injects
1511 .retain(|item| keep_introduced(introduced, unprovided_inject_key(&item.inject, root)));
1512 results.unrendered_components.retain(|item| {
1513 keep_introduced(introduced, unrendered_component_key(&item.component, root))
1514 });
1515 results
1516 .unused_component_props
1517 .retain(|item| keep_introduced(introduced, unused_component_prop_key(&item.prop, root)));
1518 results
1519 .unused_component_emits
1520 .retain(|item| keep_introduced(introduced, unused_component_emit_key(&item.emit, root)));
1521 results
1522 .unused_component_inputs
1523 .retain(|item| keep_introduced(introduced, unused_component_input_key(&item.input, root)));
1524 results.unused_component_outputs.retain(|item| {
1525 keep_introduced(introduced, unused_component_output_key(&item.output, root))
1526 });
1527 results
1528 .unused_svelte_events
1529 .retain(|item| keep_introduced(introduced, unused_svelte_event_key(&item.event, root)));
1530 results
1531 .unused_server_actions
1532 .retain(|item| keep_introduced(introduced, unused_server_action_key(&item.action, root)));
1533 results
1534 .unused_load_data_keys
1535 .retain(|item| keep_introduced(introduced, unused_load_data_key_key(&item.key, root)));
1536 results
1537 .route_collisions
1538 .retain(|item| keep_introduced(introduced, route_collision_key(&item.collision, root)));
1539 results.dynamic_segment_name_conflicts.retain(|item| {
1540 keep_introduced(
1541 introduced,
1542 dynamic_segment_name_conflict_key(&item.conflict, root),
1543 )
1544 });
1545}
1546
1547fn issue_was_introduced(key: &str, base: &FxHashSet<String>) -> bool {
1548 !base.contains(key)
1549}
1550
1551fn annotate_issue_array<I>(json: &mut serde_json::Value, key: &str, introduced: I)
1552where
1553 I: IntoIterator<Item = bool>,
1554{
1555 let Some(items) = json.get_mut(key).and_then(serde_json::Value::as_array_mut) else {
1556 return;
1557 };
1558 for (item, introduced) in items.iter_mut().zip(introduced) {
1559 if let serde_json::Value::Object(map) = item {
1560 map.insert("introduced".to_string(), serde_json::json!(introduced));
1561 }
1562 }
1563}
1564
1565#[expect(
1566 clippy::implicit_hasher,
1567 reason = "fallow standardizes on FxHashSet across audit attribution keys"
1568)]
1569pub fn annotate_dead_code_json(
1570 json: &mut serde_json::Value,
1571 results: &fallow_types::results::AnalysisResults,
1572 root: &Path,
1573 base: &FxHashSet<String>,
1574) {
1575 let mut annotator = DeadCodeJsonAnnotator {
1576 json,
1577 results,
1578 root,
1579 base,
1580 };
1581 annotator.annotate_file_symbols();
1582 annotator.annotate_dependencies();
1583 annotator.annotate_members();
1584 annotator.annotate_imports_and_exports();
1585 annotator.annotate_graph();
1586 annotator.annotate_catalog();
1587}
1588
1589struct DeadCodeJsonAnnotator<'a> {
1590 json: &'a mut serde_json::Value,
1591 results: &'a fallow_types::results::AnalysisResults,
1592 root: &'a Path,
1593 base: &'a FxHashSet<String>,
1594}
1595
1596impl DeadCodeJsonAnnotator<'_> {
1597 fn annotate_file_symbols(&mut self) {
1598 annotate_issue_array(
1599 self.json,
1600 "unused_files",
1601 self.results.unused_files.iter().map(|item| {
1602 issue_was_introduced(
1603 &format!(
1604 "unused-file:{}",
1605 relative_key_path(&item.file.path, self.root)
1606 ),
1607 self.base,
1608 )
1609 }),
1610 );
1611 annotate_issue_array(
1612 self.json,
1613 "unused_exports",
1614 self.results.unused_exports.iter().map(|item| {
1615 issue_was_introduced(
1616 &format!(
1617 "unused-export:{}:{}",
1618 relative_key_path(&item.export.path, self.root),
1619 item.export.export_name
1620 ),
1621 self.base,
1622 )
1623 }),
1624 );
1625 annotate_issue_array(
1626 self.json,
1627 "unused_types",
1628 self.results.unused_types.iter().map(|item| {
1629 issue_was_introduced(
1630 &format!(
1631 "unused-type:{}:{}",
1632 relative_key_path(&item.export.path, self.root),
1633 item.export.export_name
1634 ),
1635 self.base,
1636 )
1637 }),
1638 );
1639 annotate_issue_array(
1640 self.json,
1641 "private_type_leaks",
1642 self.results.private_type_leaks.iter().map(|item| {
1643 issue_was_introduced(
1644 &format!(
1645 "private-type-leak:{}:{}:{}",
1646 relative_key_path(&item.leak.path, self.root),
1647 item.leak.export_name,
1648 item.leak.type_name
1649 ),
1650 self.base,
1651 )
1652 }),
1653 );
1654 }
1655
1656 fn annotate_dependencies(&mut self) {
1657 annotate_dependency_json(self.json, self.results, self.root, self.base);
1658 annotate_issue_array(
1659 self.json,
1660 "type_only_dependencies",
1661 self.results.type_only_dependencies.iter().map(|item| {
1662 issue_was_introduced(
1663 &format!(
1664 "type-only-dependency:{}:{}",
1665 relative_key_path(&item.dep.path, self.root),
1666 item.dep.package_name
1667 ),
1668 self.base,
1669 )
1670 }),
1671 );
1672 annotate_issue_array(
1673 self.json,
1674 "test_only_dependencies",
1675 self.results.test_only_dependencies.iter().map(|item| {
1676 issue_was_introduced(
1677 &format!(
1678 "test-only-dependency:{}:{}",
1679 relative_key_path(&item.dep.path, self.root),
1680 item.dep.package_name
1681 ),
1682 self.base,
1683 )
1684 }),
1685 );
1686 }
1687
1688 fn annotate_members(&mut self) {
1689 annotate_member_json(self.json, self.results, self.root, self.base);
1690 }
1691
1692 fn annotate_imports_and_exports(&mut self) {
1693 self.annotate_import_dependency_keys();
1694 self.annotate_framework_keys();
1695 self.annotate_component_keys();
1696 self.annotate_route_keys();
1697 }
1698
1699 fn annotate_import_dependency_keys(&mut self) {
1700 annotate_issue_array(
1701 self.json,
1702 "unresolved_imports",
1703 self.results.unresolved_imports.iter().map(|item| {
1704 issue_was_introduced(
1705 &format!(
1706 "unresolved-import:{}:{}",
1707 relative_key_path(&item.import.path, self.root),
1708 item.import.specifier
1709 ),
1710 self.base,
1711 )
1712 }),
1713 );
1714 annotate_issue_array(
1715 self.json,
1716 "unlisted_dependencies",
1717 self.results.unlisted_dependencies.iter().map(|item| {
1718 issue_was_introduced(&unlisted_dependency_key(&item.dep, self.root), self.base)
1719 }),
1720 );
1721 annotate_issue_array(
1722 self.json,
1723 "duplicate_exports",
1724 self.results.duplicate_exports.iter().map(|item| {
1725 let mut locations: Vec<String> = item
1726 .export
1727 .locations
1728 .iter()
1729 .map(|loc| relative_key_path(&loc.path, self.root))
1730 .collect();
1731 locations.sort();
1732 locations.dedup();
1733 issue_was_introduced(
1734 &format!(
1735 "duplicate-export:{}:{}",
1736 item.export.export_name,
1737 locations.join("|")
1738 ),
1739 self.base,
1740 )
1741 }),
1742 );
1743 }
1744
1745 fn annotate_framework_keys(&mut self) {
1746 annotate_issue_array(
1747 self.json,
1748 "invalid_client_exports",
1749 self.results.invalid_client_exports.iter().map(|item| {
1750 issue_was_introduced(
1751 &invalid_client_export_key(&item.export, self.root),
1752 self.base,
1753 )
1754 }),
1755 );
1756 annotate_issue_array(
1757 self.json,
1758 "mixed_client_server_barrels",
1759 self.results.mixed_client_server_barrels.iter().map(|item| {
1760 issue_was_introduced(
1761 &mixed_client_server_barrel_key(&item.barrel, self.root),
1762 self.base,
1763 )
1764 }),
1765 );
1766 annotate_issue_array(
1767 self.json,
1768 "misplaced_directives",
1769 self.results.misplaced_directives.iter().map(|item| {
1770 issue_was_introduced(
1771 &misplaced_directive_key(&item.directive_site, self.root),
1772 self.base,
1773 )
1774 }),
1775 );
1776 annotate_issue_array(
1777 self.json,
1778 "unprovided_injects",
1779 self.results.unprovided_injects.iter().map(|item| {
1780 issue_was_introduced(&unprovided_inject_key(&item.inject, self.root), self.base)
1781 }),
1782 );
1783 }
1784
1785 fn annotate_component_keys(&mut self) {
1786 self.annotate_component_render_keys();
1787 self.annotate_component_io_keys();
1788 }
1789
1790 fn annotate_component_render_keys(&mut self) {
1792 annotate_issue_array(
1793 self.json,
1794 "unrendered_components",
1795 self.results.unrendered_components.iter().map(|item| {
1796 issue_was_introduced(
1797 &unrendered_component_key(&item.component, self.root),
1798 self.base,
1799 )
1800 }),
1801 );
1802 annotate_issue_array(
1803 self.json,
1804 "unused_component_props",
1805 self.results.unused_component_props.iter().map(|item| {
1806 issue_was_introduced(&unused_component_prop_key(&item.prop, self.root), self.base)
1807 }),
1808 );
1809 annotate_issue_array(
1810 self.json,
1811 "unused_component_emits",
1812 self.results.unused_component_emits.iter().map(|item| {
1813 issue_was_introduced(&unused_component_emit_key(&item.emit, self.root), self.base)
1814 }),
1815 );
1816 }
1817
1818 fn annotate_component_io_keys(&mut self) {
1820 annotate_issue_array(
1821 self.json,
1822 "unused_component_inputs",
1823 self.results.unused_component_inputs.iter().map(|item| {
1824 issue_was_introduced(
1825 &unused_component_input_key(&item.input, self.root),
1826 self.base,
1827 )
1828 }),
1829 );
1830 annotate_issue_array(
1831 self.json,
1832 "unused_component_outputs",
1833 self.results.unused_component_outputs.iter().map(|item| {
1834 issue_was_introduced(
1835 &unused_component_output_key(&item.output, self.root),
1836 self.base,
1837 )
1838 }),
1839 );
1840 annotate_issue_array(
1841 self.json,
1842 "unused_svelte_events",
1843 self.results.unused_svelte_events.iter().map(|item| {
1844 issue_was_introduced(&unused_svelte_event_key(&item.event, self.root), self.base)
1845 }),
1846 );
1847 annotate_issue_array(
1848 self.json,
1849 "unused_server_actions",
1850 self.results.unused_server_actions.iter().map(|item| {
1851 issue_was_introduced(
1852 &unused_server_action_key(&item.action, self.root),
1853 self.base,
1854 )
1855 }),
1856 );
1857 }
1858
1859 fn annotate_route_keys(&mut self) {
1860 annotate_issue_array(
1861 self.json,
1862 "route_collisions",
1863 self.results.route_collisions.iter().map(|item| {
1864 issue_was_introduced(&route_collision_key(&item.collision, self.root), self.base)
1865 }),
1866 );
1867 annotate_issue_array(
1868 self.json,
1869 "dynamic_segment_name_conflicts",
1870 self.results
1871 .dynamic_segment_name_conflicts
1872 .iter()
1873 .map(|item| {
1874 issue_was_introduced(
1875 &dynamic_segment_name_conflict_key(&item.conflict, self.root),
1876 self.base,
1877 )
1878 }),
1879 );
1880 }
1881
1882 fn annotate_graph(&mut self) {
1883 annotate_graph_json(self.json, self.results, self.root, self.base);
1884 }
1885
1886 fn annotate_catalog(&mut self) {
1887 annotate_catalog_json(self.json, self.results, self.root, self.base);
1888 }
1889}
1890
1891fn annotate_dependency_json(
1892 json: &mut serde_json::Value,
1893 results: &fallow_types::results::AnalysisResults,
1894 root: &Path,
1895 base: &FxHashSet<String>,
1896) {
1897 annotate_issue_array(
1898 json,
1899 "unused_dependencies",
1900 results
1901 .unused_dependencies
1902 .iter()
1903 .map(|item| issue_was_introduced(&unused_dependency_key(&item.dep, root), base)),
1904 );
1905 annotate_issue_array(
1906 json,
1907 "unused_dev_dependencies",
1908 results
1909 .unused_dev_dependencies
1910 .iter()
1911 .map(|item| issue_was_introduced(&unused_dependency_key(&item.dep, root), base)),
1912 );
1913 annotate_issue_array(
1914 json,
1915 "unused_optional_dependencies",
1916 results
1917 .unused_optional_dependencies
1918 .iter()
1919 .map(|item| issue_was_introduced(&unused_dependency_key(&item.dep, root), base)),
1920 );
1921}
1922
1923fn annotate_member_json(
1924 json: &mut serde_json::Value,
1925 results: &fallow_types::results::AnalysisResults,
1926 root: &Path,
1927 base: &FxHashSet<String>,
1928) {
1929 annotate_issue_array(
1930 json,
1931 "unused_enum_members",
1932 results.unused_enum_members.iter().map(|item| {
1933 issue_was_introduced(
1934 &unused_member_key("unused-enum-member", &item.member, root),
1935 base,
1936 )
1937 }),
1938 );
1939 annotate_issue_array(
1940 json,
1941 "unused_class_members",
1942 results.unused_class_members.iter().map(|item| {
1943 issue_was_introduced(
1944 &unused_member_key("unused-class-member", &item.member, root),
1945 base,
1946 )
1947 }),
1948 );
1949 annotate_issue_array(
1950 json,
1951 "unused_store_members",
1952 results.unused_store_members.iter().map(|item| {
1953 issue_was_introduced(
1954 &unused_member_key("unused-store-member", &item.member, root),
1955 base,
1956 )
1957 }),
1958 );
1959}
1960
1961fn annotate_graph_json(
1962 json: &mut serde_json::Value,
1963 results: &fallow_types::results::AnalysisResults,
1964 root: &Path,
1965 base: &FxHashSet<String>,
1966) {
1967 annotate_cycle_json(json, results, root, base);
1968 annotate_boundary_json(json, results, root, base);
1969 annotate_policy_json(json, results, root, base);
1970}
1971
1972fn annotate_cycle_json(
1973 json: &mut serde_json::Value,
1974 results: &fallow_types::results::AnalysisResults,
1975 root: &Path,
1976 base: &FxHashSet<String>,
1977) {
1978 annotate_issue_array(
1979 json,
1980 "circular_dependencies",
1981 results.circular_dependencies.iter().map(|item| {
1982 let mut files: Vec<String> = item
1983 .cycle
1984 .files
1985 .iter()
1986 .map(|path| relative_key_path(path, root))
1987 .collect();
1988 files.sort();
1989 issue_was_introduced(&format!("circular-dependency:{}", files.join("|")), base)
1990 }),
1991 );
1992 annotate_issue_array(
1993 json,
1994 "re_export_cycles",
1995 results.re_export_cycles.iter().map(|item| {
1996 let kind = match item.cycle.kind {
1997 fallow_types::results::ReExportCycleKind::MultiNode => "multi-node",
1998 fallow_types::results::ReExportCycleKind::SelfLoop => "self-loop",
1999 };
2000 let mut files: Vec<String> = item
2001 .cycle
2002 .files
2003 .iter()
2004 .map(|path| relative_key_path(path, root))
2005 .collect();
2006 files.sort();
2007 issue_was_introduced(&format!("re-export-cycle:{kind}:{}", files.join("|")), base)
2008 }),
2009 );
2010}
2011
2012fn annotate_boundary_json(
2013 json: &mut serde_json::Value,
2014 results: &fallow_types::results::AnalysisResults,
2015 root: &Path,
2016 base: &FxHashSet<String>,
2017) {
2018 annotate_issue_array(
2019 json,
2020 "boundary_violations",
2021 results.boundary_violations.iter().map(|item| {
2022 issue_was_introduced(
2023 &format!(
2024 "boundary-violation:{}:{}:{}",
2025 relative_key_path(&item.violation.from_path, root),
2026 relative_key_path(&item.violation.to_path, root),
2027 item.violation.import_specifier
2028 ),
2029 base,
2030 )
2031 }),
2032 );
2033 annotate_issue_array(
2034 json,
2035 "boundary_coverage_violations",
2036 results.boundary_coverage_violations.iter().map(|item| {
2037 issue_was_introduced(
2038 &format!(
2039 "boundary-coverage:{}",
2040 relative_key_path(&item.violation.path, root)
2041 ),
2042 base,
2043 )
2044 }),
2045 );
2046 annotate_issue_array(
2047 json,
2048 "boundary_call_violations",
2049 results.boundary_call_violations.iter().map(|item| {
2050 issue_was_introduced(
2051 &format!(
2052 "boundary-call:{}:{}",
2053 relative_key_path(&item.violation.path, root),
2054 item.violation.callee
2055 ),
2056 base,
2057 )
2058 }),
2059 );
2060}
2061
2062fn annotate_policy_json(
2063 json: &mut serde_json::Value,
2064 results: &fallow_types::results::AnalysisResults,
2065 root: &Path,
2066 base: &FxHashSet<String>,
2067) {
2068 annotate_issue_array(
2069 json,
2070 "policy_violations",
2071 results.policy_violations.iter().map(|item| {
2072 issue_was_introduced(
2073 &format!(
2074 "policy-violation:{}:{}/{}:{}",
2075 relative_key_path(&item.violation.path, root),
2076 item.violation.pack,
2077 item.violation.rule_id,
2078 item.violation.matched
2079 ),
2080 base,
2081 )
2082 }),
2083 );
2084 annotate_issue_array(
2085 json,
2086 "stale_suppressions",
2087 results
2088 .stale_suppressions
2089 .iter()
2090 .map(|item| issue_was_introduced(&stale_suppression_key(item, root), base)),
2091 );
2092}
2093
2094fn annotate_catalog_json(
2095 json: &mut serde_json::Value,
2096 results: &fallow_types::results::AnalysisResults,
2097 root: &Path,
2098 base: &FxHashSet<String>,
2099) {
2100 annotate_catalog_entry_json(json, results, root, base);
2101 annotate_dependency_override_json(json, results, root, base);
2102}
2103
2104fn annotate_catalog_entry_json(
2106 json: &mut serde_json::Value,
2107 results: &fallow_types::results::AnalysisResults,
2108 root: &Path,
2109 base: &FxHashSet<String>,
2110) {
2111 annotate_issue_array(
2112 json,
2113 "unresolved_catalog_references",
2114 results.unresolved_catalog_references.iter().map(|item| {
2115 issue_was_introduced(
2116 &format!(
2117 "unresolved-catalog-reference:{}:{}:{}:{}",
2118 relative_key_path(&item.reference.path, root),
2119 item.reference.line,
2120 item.reference.catalog_name,
2121 item.reference.entry_name
2122 ),
2123 base,
2124 )
2125 }),
2126 );
2127 annotate_issue_array(
2128 json,
2129 "unused_catalog_entries",
2130 results
2131 .unused_catalog_entries
2132 .iter()
2133 .map(|item| issue_was_introduced(&unused_catalog_entry_key(&item.entry, root), base)),
2134 );
2135 annotate_issue_array(
2136 json,
2137 "empty_catalog_groups",
2138 results
2139 .empty_catalog_groups
2140 .iter()
2141 .map(|item| issue_was_introduced(&empty_catalog_group_key(&item.group, root), base)),
2142 );
2143}
2144
2145fn annotate_dependency_override_json(
2147 json: &mut serde_json::Value,
2148 results: &fallow_types::results::AnalysisResults,
2149 root: &Path,
2150 base: &FxHashSet<String>,
2151) {
2152 annotate_issue_array(
2153 json,
2154 "unused_dependency_overrides",
2155 results.unused_dependency_overrides.iter().map(|item| {
2156 issue_was_introduced(
2157 &format!(
2158 "unused-dependency-override:{}:{}:{}",
2159 relative_key_path(&item.entry.path, root),
2160 item.entry.line,
2161 item.entry.raw_key
2162 ),
2163 base,
2164 )
2165 }),
2166 );
2167 annotate_issue_array(
2168 json,
2169 "misconfigured_dependency_overrides",
2170 results
2171 .misconfigured_dependency_overrides
2172 .iter()
2173 .map(|item| {
2174 issue_was_introduced(
2175 &format!(
2176 "misconfigured-dependency-override:{}:{}:{}",
2177 relative_key_path(&item.entry.path, root),
2178 item.entry.line,
2179 item.entry.raw_key
2180 ),
2181 base,
2182 )
2183 }),
2184 );
2185}
2186
2187#[expect(
2188 clippy::implicit_hasher,
2189 reason = "fallow standardizes on FxHashSet across audit attribution keys"
2190)]
2191pub fn annotate_health_json(
2192 json: &mut serde_json::Value,
2193 report: &fallow_output::HealthReport,
2194 root: &Path,
2195 base: &FxHashSet<String>,
2196) {
2197 let Some(items) = json
2198 .get_mut("findings")
2199 .and_then(serde_json::Value::as_array_mut)
2200 else {
2201 return;
2202 };
2203 for (item, finding) in items.iter_mut().zip(&report.findings) {
2204 if let serde_json::Value::Object(map) = item {
2205 map.insert(
2206 "introduced".to_string(),
2207 serde_json::json!(issue_was_introduced(
2208 &health_finding_key(finding, root),
2209 base
2210 )),
2211 );
2212 }
2213 }
2214}
2215
2216#[expect(
2217 clippy::implicit_hasher,
2218 reason = "fallow standardizes on FxHashSet across audit attribution keys"
2219)]
2220pub fn annotate_dupes_json(
2221 json: &mut serde_json::Value,
2222 report: &fallow_types::duplicates::DuplicationReport,
2223 root: &Path,
2224 base: &FxHashSet<String>,
2225) {
2226 let Some(items) = json
2227 .get_mut("clone_groups")
2228 .and_then(serde_json::Value::as_array_mut)
2229 else {
2230 return;
2231 };
2232 for (item, group) in items.iter_mut().zip(&report.clone_groups) {
2233 if let serde_json::Value::Object(map) = item {
2234 map.insert(
2235 "introduced".to_string(),
2236 serde_json::json!(issue_was_introduced(&dupe_group_key(group, root), base)),
2237 );
2238 }
2239 }
2240}
2241
2242pub fn health_keys(report: &fallow_output::HealthReport, root: &Path) -> FxHashSet<String> {
2243 report
2244 .findings
2245 .iter()
2246 .map(|finding| health_finding_key(finding, root))
2247 .collect()
2248}
2249
2250pub fn health_finding_key(finding: &fallow_output::ComplexityViolation, root: &Path) -> String {
2251 format!(
2252 "complexity:{}:{}:{:?}",
2253 relative_key_path(Path::new(&finding.path), root),
2254 finding.name,
2255 finding.exceeded
2256 )
2257}
2258
2259pub fn styling_keys(report: &fallow_output::HealthReport, root: &Path) -> FxHashSet<String> {
2260 report
2261 .styling_findings
2262 .iter()
2263 .map(|finding| styling_finding_key(finding, root))
2264 .collect()
2265}
2266
2267pub fn styling_finding_key(finding: &fallow_output::StylingFinding, root: &Path) -> String {
2268 format!(
2269 "styling:{}:{}:{}:{}:{}",
2270 finding.code,
2271 finding.sub_kind,
2272 relative_key_path(Path::new(&finding.path), root),
2273 finding.line,
2274 finding.value
2275 )
2276}
2277
2278pub fn dupes_keys(
2279 report: &fallow_types::duplicates::DuplicationReport,
2280 root: &Path,
2281) -> FxHashSet<String> {
2282 report
2283 .clone_groups
2284 .iter()
2285 .map(|group| dupe_group_key(group, root))
2286 .collect()
2287}
2288
2289pub fn dupe_group_key(group: &fallow_types::duplicates::CloneGroup, root: &Path) -> String {
2290 let mut files: Vec<String> = group
2291 .instances
2292 .iter()
2293 .map(|instance| relative_key_path(&instance.file, root))
2294 .collect();
2295 files.sort();
2296 files.dedup();
2297 let mut hasher = DefaultHasher::new();
2298 for instance in &group.instances {
2299 instance.fragment.hash(&mut hasher);
2300 }
2301 format!(
2302 "dupe:{}:{}:{}:{:x}",
2303 files.join("|"),
2304 group.token_count,
2305 group.line_count,
2306 hasher.finish()
2307 )
2308}
2309
2310#[cfg(test)]
2311mod tests {
2312 use std::path::{Path, PathBuf};
2313
2314 use fallow_types::duplicates::{CloneGroup, CloneInstance, DuplicationReport};
2315 use fallow_types::extract::MemberKind;
2316 use fallow_types::output_dead_code::*;
2317 use fallow_types::results::*;
2318 use rustc_hash::FxHashSet;
2319 use serde_json::json;
2320
2321 use fallow_output::{
2322 ComplexityViolation, ExceededThreshold, FindingSeverity, HealthFinding, HealthReport,
2323 };
2324
2325 use super::{
2326 annotate_dead_code_json, annotate_dupes_json, annotate_health_json, dead_code_keys,
2327 dupe_group_key, dupes_keys, health_finding_key, health_keys, relative_key_path,
2328 retain_introduced_dead_code,
2329 };
2330
2331 fn root() -> PathBuf {
2332 PathBuf::from("/repo")
2333 }
2334
2335 fn export(path: &Path, name: &str) -> UnusedExportFinding {
2336 UnusedExportFinding::with_actions(UnusedExport {
2337 path: path.to_path_buf(),
2338 export_name: name.to_string(),
2339 is_type_only: false,
2340 line: 1,
2341 col: 0,
2342 span_start: 0,
2343 is_re_export: false,
2344 })
2345 }
2346
2347 fn unused_file(path: &Path) -> UnusedFileFinding {
2348 UnusedFileFinding::with_actions(UnusedFile {
2349 path: path.to_path_buf(),
2350 })
2351 }
2352
2353 fn dependency(path: &Path, package_name: &str) -> UnusedDependencyFinding {
2354 UnusedDependencyFinding::with_actions(UnusedDependency {
2355 package_name: package_name.to_string(),
2356 location: DependencyLocation::Dependencies,
2357 path: path.to_path_buf(),
2358 line: 4,
2359 used_in_workspaces: Vec::new(),
2360 })
2361 }
2362
2363 fn unresolved(path: &Path, specifier: &str) -> UnresolvedImportFinding {
2364 UnresolvedImportFinding::with_actions(UnresolvedImport {
2365 path: path.to_path_buf(),
2366 specifier: specifier.to_string(),
2367 line: 2,
2368 col: 1,
2369 specifier_col: 8,
2370 })
2371 }
2372
2373 fn unlisted(path: &Path, package_name: &str) -> UnlistedDependencyFinding {
2374 UnlistedDependencyFinding::with_actions(UnlistedDependency {
2375 package_name: package_name.to_string(),
2376 imported_from: vec![
2377 ImportSite {
2378 path: path.to_path_buf(),
2379 line: 9,
2380 col: 2,
2381 },
2382 ImportSite {
2383 path: path.to_path_buf(),
2384 line: 9,
2385 col: 2,
2386 },
2387 ],
2388 })
2389 }
2390
2391 fn duplicate_export(root: &Path) -> DuplicateExportFinding {
2392 DuplicateExportFinding::with_actions(DuplicateExport {
2393 export_name: "Button".to_string(),
2394 locations: vec![
2395 DuplicateLocation {
2396 path: root.join("src/b.ts"),
2397 line: 1,
2398 col: 0,
2399 },
2400 DuplicateLocation {
2401 path: root.join("src/a.ts"),
2402 line: 1,
2403 col: 0,
2404 },
2405 DuplicateLocation {
2406 path: root.join("src/a.ts"),
2407 line: 2,
2408 col: 0,
2409 },
2410 ],
2411 })
2412 }
2413
2414 fn sample_results(root: &Path) -> AnalysisResults {
2415 let source = root.join("src/page.ts");
2416 let package_json = root.join("package.json");
2417 let mut results = AnalysisResults::default();
2418 results
2419 .unused_files
2420 .push(unused_file(&root.join("src/dead.ts")));
2421 results.unused_exports.push(export(&source, "loader"));
2422 results
2423 .unused_dependencies
2424 .push(dependency(&package_json, "left-pad"));
2425 results
2426 .unresolved_imports
2427 .push(unresolved(&source, "./missing"));
2428 results.unlisted_dependencies.push(unlisted(&source, "zod"));
2429 results.duplicate_exports.push(duplicate_export(root));
2430 results
2431 }
2432
2433 #[test]
2434 fn relative_key_path_strips_root_and_normalizes_separators() {
2435 let path = Path::new("/repo/src\\feature\\index.ts");
2436 assert_eq!(
2437 relative_key_path(path, Path::new("/repo")),
2438 "src/feature/index.ts"
2439 );
2440 }
2441
2442 #[test]
2443 fn dead_code_keys_are_stable_for_unsorted_and_duplicate_locations() {
2444 let root = root();
2445 let keys = dead_code_keys(&sample_results(&root), &root);
2446
2447 assert!(keys.contains("unused-file:src/dead.ts"));
2448 assert!(keys.contains("unused-export:src/page.ts:loader"));
2449 assert!(keys.contains("unused-dependency:package.json:left-pad"));
2450 assert!(keys.contains("unresolved-import:src/page.ts:./missing"));
2451 assert!(keys.contains("unlisted-dependency:zod:src/page.ts:9:2"));
2452 assert!(keys.contains("duplicate-export:Button:src/a.ts|src/b.ts"));
2453 }
2454
2455 #[test]
2456 fn dead_code_keys_cover_type_member_and_dependency_variants() {
2457 let root = root();
2458 let source = root.join("src/types.ts");
2459 let package_json = root.join("package.json");
2460 let mut results = AnalysisResults::default();
2461 results
2462 .unused_types
2463 .push(UnusedTypeFinding::with_actions(UnusedExport {
2464 path: source.clone(),
2465 export_name: "UnusedType".to_string(),
2466 is_type_only: true,
2467 line: 3,
2468 col: 0,
2469 span_start: 12,
2470 is_re_export: false,
2471 }));
2472 results
2473 .private_type_leaks
2474 .push(PrivateTypeLeakFinding::with_actions(PrivateTypeLeak {
2475 path: source.clone(),
2476 export_name: "makePublic".to_string(),
2477 type_name: "PrivateShape".to_string(),
2478 line: 7,
2479 col: 12,
2480 span_start: 64,
2481 }));
2482 results
2483 .unused_dev_dependencies
2484 .push(UnusedDevDependencyFinding::with_actions(UnusedDependency {
2485 package_name: "vite".to_string(),
2486 location: DependencyLocation::DevDependencies,
2487 path: package_json.clone(),
2488 line: 10,
2489 used_in_workspaces: Vec::new(),
2490 }));
2491 results
2492 .unused_optional_dependencies
2493 .push(UnusedOptionalDependencyFinding::with_actions(
2494 UnusedDependency {
2495 package_name: "fsevents".to_string(),
2496 location: DependencyLocation::OptionalDependencies,
2497 path: package_json.clone(),
2498 line: 11,
2499 used_in_workspaces: Vec::new(),
2500 },
2501 ));
2502 results
2503 .unused_enum_members
2504 .push(UnusedEnumMemberFinding::with_actions(UnusedMember {
2505 path: source.clone(),
2506 parent_name: "Status".to_string(),
2507 member_name: "Idle".to_string(),
2508 kind: MemberKind::EnumMember,
2509 line: 15,
2510 col: 2,
2511 }));
2512 results
2513 .unused_class_members
2514 .push(UnusedClassMemberFinding::with_actions(UnusedMember {
2515 path: source,
2516 parent_name: "Controller".to_string(),
2517 member_name: "legacy".to_string(),
2518 kind: MemberKind::ClassMethod,
2519 line: 21,
2520 col: 2,
2521 }));
2522 results
2523 .type_only_dependencies
2524 .push(TypeOnlyDependencyFinding::with_actions(
2525 TypeOnlyDependency {
2526 package_name: "zod".to_string(),
2527 path: package_json.clone(),
2528 line: 12,
2529 },
2530 ));
2531 results
2532 .test_only_dependencies
2533 .push(TestOnlyDependencyFinding::with_actions(
2534 TestOnlyDependency {
2535 package_name: "vitest".to_string(),
2536 path: package_json,
2537 line: 13,
2538 },
2539 ));
2540
2541 let keys = dead_code_keys(&results, &root);
2542
2543 assert!(keys.contains("unused-type:src/types.ts:UnusedType"));
2544 assert!(keys.contains("private-type-leak:src/types.ts:makePublic:PrivateShape"));
2545 assert!(keys.contains("unused-dev-dependency:package.json:vite"));
2546 assert!(keys.contains("unused-optional-dependency:package.json:fsevents"));
2547 assert!(keys.contains("unused-enum-member:src/types.ts:Status:Idle"));
2548 assert!(keys.contains("unused-class-member:src/types.ts:Controller:legacy"));
2549 assert!(keys.contains("type-only-dependency:package.json:zod"));
2550 assert!(keys.contains("test-only-dependency:package.json:vitest"));
2551 }
2552
2553 #[expect(
2554 clippy::too_many_lines,
2555 reason = "test fixture; linear setup/assert, length is not a maintainability concern"
2556 )]
2557 fn graph_boundary_catalog_override_results(root: &std::path::Path) -> AnalysisResults {
2558 let source = root.join("src/app.ts");
2559 let other = root.join("src/other.ts");
2560 let workspace = root.join("pnpm-workspace.yaml");
2561 let mut results = AnalysisResults::default();
2562 results
2563 .circular_dependencies
2564 .push(CircularDependencyFinding::with_actions(
2565 CircularDependency {
2566 files: vec![other.clone(), source.clone()],
2567 length: 2,
2568 line: 4,
2569 col: 0,
2570 edges: Vec::new(),
2571 is_cross_package: false,
2572 },
2573 ));
2574 results
2575 .re_export_cycles
2576 .push(ReExportCycleFinding::with_actions(ReExportCycle {
2577 files: vec![source.clone()],
2578 kind: ReExportCycleKind::SelfLoop,
2579 }));
2580 results
2581 .boundary_violations
2582 .push(BoundaryViolationFinding::with_actions(BoundaryViolation {
2583 from_path: source.clone(),
2584 to_path: other,
2585 from_zone: "ui".to_string(),
2586 to_zone: "server".to_string(),
2587 import_specifier: "../other".to_string(),
2588 line: 1,
2589 col: 0,
2590 }));
2591 results
2592 .boundary_coverage_violations
2593 .push(BoundaryCoverageViolationFinding::with_actions(
2594 BoundaryCoverageViolation {
2595 path: root.join("src/unmatched.ts"),
2596 line: 1,
2597 col: 0,
2598 },
2599 ));
2600 results
2601 .boundary_call_violations
2602 .push(BoundaryCallViolationFinding::with_actions(
2603 BoundaryCallViolation {
2604 path: source.clone(),
2605 line: 12,
2606 col: 4,
2607 zone: "ui".to_string(),
2608 callee: "child_process.exec".to_string(),
2609 pattern: "child_process.*".to_string(),
2610 },
2611 ));
2612 results.stale_suppressions.push(StaleSuppression {
2613 path: source,
2614 line: 2,
2615 col: 0,
2616 origin: SuppressionOrigin::Comment {
2617 issue_kind: Some("unused-export".to_string()),
2618 reason: None,
2619 is_file_level: false,
2620 kind_known: true,
2621 },
2622 missing_reason: false,
2623 actions: StaleSuppression::actions_for(false),
2624 });
2625 results.stale_suppressions.push(StaleSuppression {
2626 path: root.join("src/app.ts"),
2627 line: 2,
2628 col: 0,
2629 origin: SuppressionOrigin::Comment {
2630 issue_kind: Some("unused-export".to_string()),
2631 reason: None,
2632 is_file_level: false,
2633 kind_known: true,
2634 },
2635 missing_reason: true,
2636 actions: StaleSuppression::actions_for(true),
2637 });
2638 results.unresolved_catalog_references.push(
2639 UnresolvedCatalogReferenceFinding::with_actions(UnresolvedCatalogReference {
2640 entry_name: "react".to_string(),
2641 catalog_name: "default".to_string(),
2642 path: root.join("packages/app/package.json"),
2643 line: 9,
2644 available_in_catalogs: vec!["react18".to_string()],
2645 }),
2646 );
2647 results
2648 .unused_catalog_entries
2649 .push(UnusedCatalogEntryFinding::with_actions(
2650 UnusedCatalogEntry {
2651 entry_name: "lodash".to_string(),
2652 catalog_name: "default".to_string(),
2653 path: workspace.clone(),
2654 line: 3,
2655 hardcoded_consumers: Vec::new(),
2656 },
2657 ));
2658 results
2659 .empty_catalog_groups
2660 .push(EmptyCatalogGroupFinding::with_actions(EmptyCatalogGroup {
2661 catalog_name: "react17".to_string(),
2662 path: workspace.clone(),
2663 line: 7,
2664 }));
2665 results
2666 .unused_dependency_overrides
2667 .push(UnusedDependencyOverrideFinding::with_actions(
2668 UnusedDependencyOverride {
2669 raw_key: "left-pad".to_string(),
2670 target_package: "left-pad".to_string(),
2671 parent_package: None,
2672 version_constraint: None,
2673 version_range: "^1.3.0".to_string(),
2674 source: DependencyOverrideSource::PnpmWorkspaceYaml,
2675 path: workspace.clone(),
2676 line: 11,
2677 hint: None,
2678 },
2679 ));
2680 results.misconfigured_dependency_overrides.push(
2681 MisconfiguredDependencyOverrideFinding::with_actions(MisconfiguredDependencyOverride {
2682 raw_key: ">".to_string(),
2683 target_package: None,
2684 raw_value: String::new(),
2685 reason: DependencyOverrideMisconfigReason::UnparsableKey,
2686 source: DependencyOverrideSource::PnpmWorkspaceYaml,
2687 path: workspace,
2688 line: 12,
2689 }),
2690 );
2691 results
2692 }
2693
2694 #[test]
2695 fn dead_code_keys_cover_graph_boundary_catalog_and_override_variants() {
2696 let root = root();
2697 let results = graph_boundary_catalog_override_results(&root);
2698
2699 let keys = dead_code_keys(&results, &root);
2700
2701 assert!(keys.contains("circular-dependency:src/app.ts|src/other.ts"));
2702 assert!(keys.contains("re-export-cycle:self-loop:src/app.ts"));
2703 assert!(keys.contains("boundary-violation:src/app.ts:src/other.ts:../other"));
2704 assert!(keys.contains("boundary-coverage:src/unmatched.ts"));
2705 assert!(keys.contains("boundary-call:src/app.ts:child_process.exec"));
2706 assert!(
2707 keys.contains("stale-suppression:src/app.ts:// fallow-ignore-next-line unused-export")
2708 );
2709 assert!(keys.contains(
2710 "missing-suppression-reason:src/app.ts:// fallow-ignore-next-line unused-export"
2711 ));
2712 assert!(
2713 keys.contains("unresolved-catalog-reference:packages/app/package.json:9:default:react")
2714 );
2715 assert!(keys.contains("unused-catalog-entry:pnpm-workspace.yaml:3:default:lodash"));
2716 assert!(keys.contains("empty-catalog-group:pnpm-workspace.yaml:7:react17"));
2717 assert!(keys.contains("unused-dependency-override:pnpm-workspace.yaml:11:left-pad"));
2718 assert!(keys.contains("misconfigured-dependency-override:pnpm-workspace.yaml:12:>"));
2719 }
2720
2721 #[test]
2722 fn retain_introduced_dead_code_keeps_only_findings_absent_from_base() {
2723 let root = root();
2724 let mut results = sample_results(&root);
2725 let base = FxHashSet::from_iter([
2726 "unused-file:src/dead.ts".to_string(),
2727 "unused-dependency:package.json:left-pad".to_string(),
2728 "unresolved-import:src/page.ts:./missing".to_string(),
2729 ]);
2730
2731 retain_introduced_dead_code(&mut results, &root, Some(&base));
2732
2733 assert!(results.unused_files.is_empty());
2734 assert!(results.unused_dependencies.is_empty());
2735 assert!(results.unresolved_imports.is_empty());
2736 assert_eq!(results.unused_exports.len(), 1);
2737 assert_eq!(results.unlisted_dependencies.len(), 1);
2738 assert_eq!(results.duplicate_exports.len(), 1);
2739 }
2740
2741 #[test]
2742 fn annotate_dead_code_json_marks_introduced_status_by_matching_key_order() {
2743 let root = root();
2744 let results = sample_results(&root);
2745 let base = FxHashSet::from_iter([
2746 "unused-file:src/dead.ts".to_string(),
2747 "unlisted-dependency:zod:src/page.ts:9:2".to_string(),
2748 ]);
2749 let mut json = json!({
2750 "unused_files": [{}],
2751 "unused_exports": [{}],
2752 "unused_dependencies": [{}],
2753 "unresolved_imports": [{}],
2754 "unlisted_dependencies": [{}],
2755 "duplicate_exports": [{}],
2756 });
2757
2758 annotate_dead_code_json(&mut json, &results, &root, &base);
2759
2760 assert_eq!(json["unused_files"][0]["introduced"], false);
2761 assert_eq!(json["unused_exports"][0]["introduced"], true);
2762 assert_eq!(json["unused_dependencies"][0]["introduced"], true);
2763 assert_eq!(json["unresolved_imports"][0]["introduced"], true);
2764 assert_eq!(json["unlisted_dependencies"][0]["introduced"], false);
2765 assert_eq!(json["duplicate_exports"][0]["introduced"], true);
2766 }
2767
2768 #[test]
2771 fn dead_code_keys_cover_framework_inject_and_render_variants() {
2772 let root = root();
2773 let src = root.join("src/App.vue");
2774 let mut results = AnalysisResults::default();
2775 results
2776 .unprovided_injects
2777 .push(UnprovidedInjectFinding::with_actions(UnprovidedInject {
2778 path: src.clone(),
2779 key_name: "userStore".to_string(),
2780 framework: "vue".to_string(),
2781 line: 5,
2782 col: 0,
2783 }));
2784 results
2785 .unrendered_components
2786 .push(UnrenderedComponentFinding::with_actions(
2787 UnrenderedComponent {
2788 path: src.clone(),
2789 component_name: "MyModal".to_string(),
2790 framework: "vue".to_string(),
2791 reachable_via: None,
2792 line: 1,
2793 col: 0,
2794 },
2795 ));
2796 results
2797 .unused_component_props
2798 .push(UnusedComponentPropFinding::with_actions(
2799 UnusedComponentProp {
2800 path: src.clone(),
2801 component_name: "MyModal".to_string(),
2802 prop_name: "title".to_string(),
2803 line: 3,
2804 col: 2,
2805 },
2806 ));
2807 results
2808 .unused_component_emits
2809 .push(UnusedComponentEmitFinding::with_actions(
2810 UnusedComponentEmit {
2811 path: src,
2812 component_name: "MyModal".to_string(),
2813 emit_name: "close".to_string(),
2814 line: 4,
2815 col: 2,
2816 },
2817 ));
2818 results
2819 .unused_svelte_events
2820 .push(UnusedSvelteEventFinding::with_actions(UnusedSvelteEvent {
2821 path: root.join("src/Counter.svelte"),
2822 component_name: "Counter".to_string(),
2823 event_name: "increment".to_string(),
2824 line: 8,
2825 col: 0,
2826 }));
2827
2828 let keys = dead_code_keys(&results, &root);
2829
2830 assert!(keys.contains("unprovided-inject:src/App.vue:userStore"));
2831 assert!(keys.contains("unrendered-component:src/App.vue:MyModal"));
2832 assert!(keys.contains("unused-component-prop:src/App.vue:title"));
2833 assert!(keys.contains("unused-component-emit:src/App.vue:close"));
2834 assert!(keys.contains("unused-svelte-event:src/Counter.svelte:increment"));
2835 }
2836
2837 #[test]
2838 fn dead_code_keys_cover_server_action_load_data_and_route_variants() {
2839 let root = root();
2840 let actions_file = root.join("src/actions/submit.ts");
2841 let page_file = root.join("src/routes/blog/+page.server.ts");
2842 let route_file = root.join("app/(auth)/login/page.tsx");
2843 let route_file2 = root.join("app/login/page.tsx");
2844 let mut results = AnalysisResults::default();
2845 results
2846 .unused_server_actions
2847 .push(UnusedServerActionFinding::with_actions(
2848 UnusedServerAction {
2849 path: actions_file,
2850 action_name: "submitForm".to_string(),
2851 line: 2,
2852 col: 0,
2853 },
2854 ));
2855 results
2856 .unused_load_data_keys
2857 .push(UnusedLoadDataKeyFinding::with_actions(UnusedLoadDataKey {
2858 path: page_file,
2859 key_name: "posts".to_string(),
2860 line: 10,
2861 col: 4,
2862 route_dir: None,
2863 }));
2864 results
2865 .route_collisions
2866 .push(RouteCollisionFinding::with_actions(RouteCollision {
2867 path: route_file.clone(),
2868 url: "/login".to_string(),
2869 conflicting_paths: vec![route_file2.clone()],
2870 line: 1,
2871 col: 0,
2872 }));
2873 results.dynamic_segment_name_conflicts.push(
2874 DynamicSegmentNameConflictFinding::with_actions(DynamicSegmentNameConflict {
2875 path: route_file,
2876 position: "/shop".to_string(),
2877 conflicting_segments: vec!["[id]".to_string(), "[slug]".to_string()],
2878 conflicting_paths: vec![route_file2],
2879 line: 1,
2880 col: 0,
2881 }),
2882 );
2883
2884 let keys = dead_code_keys(&results, &root);
2885
2886 assert!(keys.contains("unused-server-action:src/actions/submit.ts:submitForm"));
2887 assert!(keys.contains("unused-load-data-key:src/routes/blog/+page.server.ts:posts"));
2888 assert!(keys.contains("route-collision:app/(auth)/login/page.tsx:/login"));
2889 assert!(keys.contains("dynamic-segment-name-conflict:app/(auth)/login/page.tsx:/shop"));
2890 }
2891
2892 #[test]
2893 fn dead_code_keys_cover_angular_input_output_and_policy_variants() {
2894 let root = root();
2895 let component = root.join("src/app/card.component.ts");
2896 let src = root.join("src/utils.ts");
2897 let mut results = AnalysisResults::default();
2898 results
2899 .unused_component_inputs
2900 .push(UnusedComponentInputFinding::with_actions(
2901 UnusedComponentInput {
2902 path: component.clone(),
2903 component_name: "CardComponent".to_string(),
2904 input_name: "label".to_string(),
2905 line: 12,
2906 col: 4,
2907 },
2908 ));
2909 results
2910 .unused_component_outputs
2911 .push(UnusedComponentOutputFinding::with_actions(
2912 UnusedComponentOutput {
2913 path: component,
2914 component_name: "CardComponent".to_string(),
2915 output_name: "clicked".to_string(),
2916 line: 13,
2917 col: 4,
2918 },
2919 ));
2920 results
2921 .policy_violations
2922 .push(PolicyViolationFinding::with_actions(PolicyViolation {
2923 path: src,
2924 line: 7,
2925 col: 0,
2926 pack: "security".to_string(),
2927 rule_id: "no-eval".to_string(),
2928 kind: PolicyRuleKind::BannedCall,
2929 matched: "eval".to_string(),
2930 severity: PolicyViolationSeverity::Error,
2931 message: None,
2932 }));
2933
2934 let keys = dead_code_keys(&results, &root);
2935
2936 assert!(keys.contains("unused-component-input:src/app/card.component.ts:label"));
2937 assert!(keys.contains("unused-component-output:src/app/card.component.ts:clicked"));
2938 assert!(keys.contains("policy-violation:src/utils.ts:security/no-eval:eval"));
2939 }
2940
2941 #[test]
2942 fn dead_code_keys_cover_re_export_cycle_multi_node_variant() {
2943 let root = root();
2944 let a = root.join("src/a.ts");
2945 let b = root.join("src/b.ts");
2946 let mut results = AnalysisResults::default();
2947 results
2948 .re_export_cycles
2949 .push(ReExportCycleFinding::with_actions(ReExportCycle {
2950 files: vec![b, a],
2951 kind: ReExportCycleKind::MultiNode,
2952 }));
2953
2954 let keys = dead_code_keys(&results, &root);
2955
2956 assert!(keys.contains("re-export-cycle:multi-node:src/a.ts|src/b.ts"));
2958 }
2959
2960 #[test]
2961 fn dead_code_keys_cover_unused_store_member() {
2962 let root = root();
2963 let src = root.join("src/store.ts");
2964 let mut results = AnalysisResults::default();
2965 results
2966 .unused_store_members
2967 .push(UnusedStoreMemberFinding::with_actions(UnusedMember {
2968 path: src,
2969 parent_name: "useAuthStore".to_string(),
2970 member_name: "resetPassword".to_string(),
2971 kind: MemberKind::ClassMethod,
2972 line: 42,
2973 col: 2,
2974 }));
2975
2976 let keys = dead_code_keys(&results, &root);
2977
2978 assert!(keys.contains("unused-store-member:src/store.ts:useAuthStore:resetPassword"));
2979 }
2980
2981 #[test]
2984 fn annotate_dead_code_json_marks_framework_keys_correctly() {
2985 let root = root();
2986 let src = root.join("src/App.vue");
2987 let mut results = AnalysisResults::default();
2988 results
2989 .unprovided_injects
2990 .push(UnprovidedInjectFinding::with_actions(UnprovidedInject {
2991 path: src.clone(),
2992 key_name: "theme".to_string(),
2993 framework: "vue".to_string(),
2994 line: 3,
2995 col: 0,
2996 }));
2997 results
2998 .unrendered_components
2999 .push(UnrenderedComponentFinding::with_actions(
3000 UnrenderedComponent {
3001 path: src.clone(),
3002 component_name: "Dialog".to_string(),
3003 framework: "vue".to_string(),
3004 reachable_via: None,
3005 line: 1,
3006 col: 0,
3007 },
3008 ));
3009 results
3010 .unused_component_props
3011 .push(UnusedComponentPropFinding::with_actions(
3012 UnusedComponentProp {
3013 path: src.clone(),
3014 component_name: "Dialog".to_string(),
3015 prop_name: "open".to_string(),
3016 line: 5,
3017 col: 2,
3018 },
3019 ));
3020 results
3021 .unused_component_emits
3022 .push(UnusedComponentEmitFinding::with_actions(
3023 UnusedComponentEmit {
3024 path: src,
3025 component_name: "Dialog".to_string(),
3026 emit_name: "dismiss".to_string(),
3027 line: 6,
3028 col: 2,
3029 },
3030 ));
3031
3032 let base = FxHashSet::from_iter(["unprovided-inject:src/App.vue:theme".to_string()]);
3034 let mut json_val = json!({
3035 "unprovided_injects": [{}],
3036 "unrendered_components": [{}],
3037 "unused_component_props": [{}],
3038 "unused_component_emits": [{}],
3039 });
3040
3041 annotate_dead_code_json(&mut json_val, &results, &root, &base);
3042
3043 assert_eq!(json_val["unprovided_injects"][0]["introduced"], false);
3044 assert_eq!(json_val["unrendered_components"][0]["introduced"], true);
3045 assert_eq!(json_val["unused_component_props"][0]["introduced"], true);
3046 assert_eq!(json_val["unused_component_emits"][0]["introduced"], true);
3047 }
3048
3049 #[test]
3050 fn annotate_dead_code_json_marks_component_io_and_route_keys_correctly() {
3051 let root = root();
3052 let component = root.join("src/card.component.ts");
3053 let svelte_file = root.join("src/Counter.svelte");
3054 let page_file = root.join("src/routes/+page.server.ts");
3055 let route_file = root.join("app/about/page.tsx");
3056 let route_file2 = root.join("app/(info)/about/page.tsx");
3057 let mut results = AnalysisResults::default();
3058 results
3059 .unused_component_inputs
3060 .push(UnusedComponentInputFinding::with_actions(
3061 UnusedComponentInput {
3062 path: component.clone(),
3063 component_name: "CardComponent".to_string(),
3064 input_name: "size".to_string(),
3065 line: 8,
3066 col: 2,
3067 },
3068 ));
3069 results
3070 .unused_component_outputs
3071 .push(UnusedComponentOutputFinding::with_actions(
3072 UnusedComponentOutput {
3073 path: component,
3074 component_name: "CardComponent".to_string(),
3075 output_name: "hovered".to_string(),
3076 line: 9,
3077 col: 2,
3078 },
3079 ));
3080 results
3081 .unused_svelte_events
3082 .push(UnusedSvelteEventFinding::with_actions(UnusedSvelteEvent {
3083 path: svelte_file,
3084 component_name: "Counter".to_string(),
3085 event_name: "reset".to_string(),
3086 line: 12,
3087 col: 0,
3088 }));
3089 results
3090 .unused_server_actions
3091 .push(UnusedServerActionFinding::with_actions(
3092 UnusedServerAction {
3093 path: page_file,
3094 action_name: "deletePost".to_string(),
3095 line: 3,
3096 col: 0,
3097 },
3098 ));
3099 results
3100 .route_collisions
3101 .push(RouteCollisionFinding::with_actions(RouteCollision {
3102 path: route_file.clone(),
3103 url: "/about".to_string(),
3104 conflicting_paths: vec![route_file2.clone()],
3105 line: 1,
3106 col: 0,
3107 }));
3108 results.dynamic_segment_name_conflicts.push(
3109 DynamicSegmentNameConflictFinding::with_actions(DynamicSegmentNameConflict {
3110 path: route_file,
3111 position: "/".to_string(),
3112 conflicting_segments: vec!["[id]".to_string()],
3113 conflicting_paths: vec![route_file2],
3114 line: 1,
3115 col: 0,
3116 }),
3117 );
3118
3119 let base = FxHashSet::default();
3121 let mut json_val = json!({
3122 "unused_component_inputs": [{}],
3123 "unused_component_outputs": [{}],
3124 "unused_svelte_events": [{}],
3125 "unused_server_actions": [{}],
3126 "route_collisions": [{}],
3127 "dynamic_segment_name_conflicts": [{}],
3128 });
3129
3130 annotate_dead_code_json(&mut json_val, &results, &root, &base);
3131
3132 assert_eq!(json_val["unused_component_inputs"][0]["introduced"], true);
3133 assert_eq!(json_val["unused_component_outputs"][0]["introduced"], true);
3134 assert_eq!(json_val["unused_svelte_events"][0]["introduced"], true);
3135 assert_eq!(json_val["unused_server_actions"][0]["introduced"], true);
3136 assert_eq!(json_val["route_collisions"][0]["introduced"], true);
3137 assert_eq!(
3138 json_val["dynamic_segment_name_conflicts"][0]["introduced"],
3139 true
3140 );
3141 }
3142
3143 #[test]
3144 fn annotate_dead_code_json_marks_members_and_dependencies_correctly() {
3145 let root = root();
3146 let src = root.join("src/types.ts");
3147 let pkg = root.join("package.json");
3148 let mut results = AnalysisResults::default();
3149 results
3150 .unused_enum_members
3151 .push(UnusedEnumMemberFinding::with_actions(UnusedMember {
3152 path: src.clone(),
3153 parent_name: "Color".to_string(),
3154 member_name: "Blue".to_string(),
3155 kind: MemberKind::EnumMember,
3156 line: 5,
3157 col: 2,
3158 }));
3159 results
3160 .unused_class_members
3161 .push(UnusedClassMemberFinding::with_actions(UnusedMember {
3162 path: src.clone(),
3163 parent_name: "Service".to_string(),
3164 member_name: "reset".to_string(),
3165 kind: MemberKind::ClassMethod,
3166 line: 20,
3167 col: 2,
3168 }));
3169 results
3170 .unused_store_members
3171 .push(UnusedStoreMemberFinding::with_actions(UnusedMember {
3172 path: src,
3173 parent_name: "useStore".to_string(),
3174 member_name: "logout".to_string(),
3175 kind: MemberKind::ClassMethod,
3176 line: 30,
3177 col: 2,
3178 }));
3179 results
3180 .unused_dev_dependencies
3181 .push(UnusedDevDependencyFinding::with_actions(UnusedDependency {
3182 package_name: "typescript".to_string(),
3183 location: DependencyLocation::DevDependencies,
3184 path: pkg.clone(),
3185 line: 8,
3186 used_in_workspaces: Vec::new(),
3187 }));
3188 results
3189 .type_only_dependencies
3190 .push(TypeOnlyDependencyFinding::with_actions(
3191 TypeOnlyDependency {
3192 package_name: "zod".to_string(),
3193 path: pkg.clone(),
3194 line: 9,
3195 },
3196 ));
3197 results
3198 .test_only_dependencies
3199 .push(TestOnlyDependencyFinding::with_actions(
3200 TestOnlyDependency {
3201 package_name: "vitest".to_string(),
3202 path: pkg,
3203 line: 10,
3204 },
3205 ));
3206
3207 let base = FxHashSet::from_iter([
3210 "unused-enum-member:src/types.ts:Color:Blue".to_string(),
3211 "unused-dev-dependency:package.json:typescript".to_string(),
3212 ]);
3213 let mut json_val = json!({
3214 "unused_enum_members": [{}],
3215 "unused_class_members": [{}],
3216 "unused_store_members": [{}],
3217 "unused_dev_dependencies": [{}],
3218 "type_only_dependencies": [{}],
3219 "test_only_dependencies": [{}],
3220 });
3221
3222 annotate_dead_code_json(&mut json_val, &results, &root, &base);
3223
3224 assert_eq!(json_val["unused_enum_members"][0]["introduced"], false);
3225 assert_eq!(json_val["unused_class_members"][0]["introduced"], true);
3226 assert_eq!(json_val["unused_store_members"][0]["introduced"], true);
3227 assert_eq!(json_val["unused_dev_dependencies"][0]["introduced"], false);
3228 assert_eq!(json_val["type_only_dependencies"][0]["introduced"], true);
3229 assert_eq!(json_val["test_only_dependencies"][0]["introduced"], true);
3230 }
3231
3232 #[test]
3233 fn annotate_dead_code_json_handles_missing_json_key_gracefully() {
3234 let root = root();
3237 let results = sample_results(&root);
3238 let base = FxHashSet::default();
3239 let mut json_val = json!({"other_key": []});
3240
3241 annotate_dead_code_json(&mut json_val, &results, &root, &base);
3243 }
3244
3245 fn make_violation(path: &Path, name: &str) -> ComplexityViolation {
3248 ComplexityViolation {
3249 path: path.to_path_buf(),
3250 name: name.to_string(),
3251 line: 1,
3252 col: 0,
3253 cyclomatic: 20,
3254 cognitive: 5,
3255 line_count: 30,
3256 param_count: 2,
3257 react_hook_count: 0,
3258 react_jsx_max_depth: 0,
3259 react_prop_count: 0,
3260 react_hook_profile: None,
3261 exceeded: ExceededThreshold::Cyclomatic,
3262 severity: FindingSeverity::High,
3263 crap: None,
3264 coverage_pct: None,
3265 coverage_tier: None,
3266 coverage_source: None,
3267 inherited_from: None,
3268 component_rollup: None,
3269 contributions: Vec::new(),
3270 effective_thresholds: None,
3271 threshold_source: None,
3272 }
3273 }
3274
3275 fn make_health_report(paths_and_names: &[(&Path, &str)]) -> HealthReport {
3276 let findings = paths_and_names
3277 .iter()
3278 .map(|(path, name)| HealthFinding::from(make_violation(path, name)))
3279 .collect();
3280 HealthReport {
3281 findings,
3282 ..HealthReport::default()
3283 }
3284 }
3285
3286 #[test]
3287 fn health_keys_produces_stable_key_per_finding() {
3288 let root = root();
3289 let path = root.join("src/heavy.ts");
3290 let report = make_health_report(&[(&path, "processAll")]);
3291 let keys = health_keys(&report, &root);
3292 assert!(keys.contains("complexity:src/heavy.ts:processAll:Cyclomatic"));
3293 }
3294
3295 #[test]
3296 fn health_finding_key_uses_path_name_and_exceeded() {
3297 let root = root();
3298 let path = root.join("src/heavy.ts");
3299 let violation = make_violation(&path, "render");
3300 let key = health_finding_key(&violation, &root);
3301 assert_eq!(key, "complexity:src/heavy.ts:render:Cyclomatic");
3302 }
3303
3304 #[test]
3305 fn annotate_health_json_marks_introduced_and_inherited_flags() {
3306 let root = root();
3307 let path_a = root.join("src/heavy.ts");
3308 let path_b = root.join("src/other.ts");
3309 let report = make_health_report(&[(&path_a, "doWork"), (&path_b, "render")]);
3310
3311 let base = FxHashSet::from_iter(["complexity:src/other.ts:render:Cyclomatic".to_string()]);
3313 let mut json_val = json!({
3314 "findings": [{}, {}],
3315 });
3316
3317 annotate_health_json(&mut json_val, &report, &root, &base);
3318
3319 assert_eq!(json_val["findings"][0]["introduced"], true);
3320 assert_eq!(json_val["findings"][1]["introduced"], false);
3321 }
3322
3323 #[test]
3324 fn annotate_health_json_is_noop_when_findings_key_absent() {
3325 let root = root();
3326 let report = make_health_report(&[]);
3327 let base = FxHashSet::default();
3328 let mut json_val = json!({"summary": {}});
3329 annotate_health_json(&mut json_val, &report, &root, &base);
3331 }
3332
3333 fn make_clone_group(files: &[PathBuf], fragment: &str) -> CloneGroup {
3336 CloneGroup {
3337 instances: files
3338 .iter()
3339 .map(|f| CloneInstance {
3340 file: f.clone(),
3341 start_line: 1,
3342 end_line: 5,
3343 start_col: 0,
3344 end_col: 80,
3345 fragment: fragment.to_string(),
3346 })
3347 .collect(),
3348 token_count: 10,
3349 line_count: 5,
3350 }
3351 }
3352
3353 fn make_duplication_report(groups: Vec<CloneGroup>) -> DuplicationReport {
3354 DuplicationReport {
3355 clone_groups: groups,
3356 clone_families: Vec::new(),
3357 mirrored_directories: Vec::new(),
3358 stats: fallow_types::duplicates::DuplicationStats::default(),
3359 }
3360 }
3361
3362 #[test]
3363 fn dupe_group_key_is_stable_for_sorted_deduplicated_files() {
3364 let root = root();
3365 let a = root.join("src/a.ts");
3366 let b = root.join("src/b.ts");
3367 let group_ab = make_clone_group(&[a.clone(), b.clone()], "const x = 1;");
3369 let group_ba = make_clone_group(&[b, a], "const x = 1;");
3370 let key_ab = dupe_group_key(&group_ab, &root);
3371 let key_ba = dupe_group_key(&group_ba, &root);
3372 assert!(key_ab.starts_with("dupe:src/a.ts|src/b.ts:"));
3374 assert!(key_ba.starts_with("dupe:src/a.ts|src/b.ts:"));
3375 assert_eq!(key_ab, key_ba);
3377 }
3378
3379 #[test]
3380 fn dupes_keys_produces_one_key_per_clone_group() {
3381 let root = root();
3382 let a = root.join("src/a.ts");
3383 let b = root.join("src/b.ts");
3384 let groups = vec![
3385 make_clone_group(&[a.clone(), b.clone()], "block one"),
3386 make_clone_group(&[a, b], "block two"),
3387 ];
3388 let report = make_duplication_report(groups);
3389 let keys = dupes_keys(&report, &root);
3390 assert_eq!(keys.len(), 2);
3391 }
3392
3393 #[test]
3394 fn annotate_dupes_json_marks_introduced_and_inherited_flags() {
3395 let root = root();
3396 let a = root.join("src/a.ts");
3397 let b = root.join("src/b.ts");
3398 let group_new = make_clone_group(&[a.clone(), b.clone()], "new block");
3399 let group_old = make_clone_group(&[a, b], "old block");
3400 let old_key = dupe_group_key(&group_old, &root);
3401 let base = FxHashSet::from_iter([old_key]);
3402 let report = make_duplication_report(vec![group_new, group_old]);
3403 let mut json_val = json!({
3404 "clone_groups": [{}, {}],
3405 });
3406
3407 annotate_dupes_json(&mut json_val, &report, &root, &base);
3408
3409 assert_eq!(json_val["clone_groups"][0]["introduced"], true);
3410 assert_eq!(json_val["clone_groups"][1]["introduced"], false);
3411 }
3412
3413 #[test]
3414 fn annotate_dupes_json_is_noop_when_clone_groups_key_absent() {
3415 let root = root();
3416 let report = make_duplication_report(Vec::new());
3417 let base = FxHashSet::default();
3418 let mut json_val = json!({"stats": {}});
3419 annotate_dupes_json(&mut json_val, &report, &root, &base);
3421 }
3422
3423 #[test]
3426 fn retain_introduced_dead_code_is_noop_when_base_is_none() {
3427 let root = root();
3428 let mut results = sample_results(&root);
3429 let original_file_count = results.unused_files.len();
3430 let original_export_count = results.unused_exports.len();
3431
3432 retain_introduced_dead_code(&mut results, &root, None);
3433
3434 assert_eq!(results.unused_files.len(), original_file_count);
3436 assert_eq!(results.unused_exports.len(), original_export_count);
3437 }
3438
3439 #[test]
3442 fn retain_introduced_dead_code_filters_framework_findings() {
3443 let root = root();
3444 let src = root.join("src/App.vue");
3445 let mut results = AnalysisResults::default();
3446 results
3447 .unprovided_injects
3448 .push(UnprovidedInjectFinding::with_actions(UnprovidedInject {
3449 path: src.clone(),
3450 key_name: "existing".to_string(),
3451 framework: "vue".to_string(),
3452 line: 1,
3453 col: 0,
3454 }));
3455 results
3456 .unprovided_injects
3457 .push(UnprovidedInjectFinding::with_actions(UnprovidedInject {
3458 path: src.clone(),
3459 key_name: "new".to_string(),
3460 framework: "vue".to_string(),
3461 line: 2,
3462 col: 0,
3463 }));
3464 results
3465 .unrendered_components
3466 .push(UnrenderedComponentFinding::with_actions(
3467 UnrenderedComponent {
3468 path: src,
3469 component_name: "OldWidget".to_string(),
3470 framework: "vue".to_string(),
3471 reachable_via: None,
3472 line: 1,
3473 col: 0,
3474 },
3475 ));
3476
3477 let base = FxHashSet::from_iter([
3478 "unprovided-inject:src/App.vue:existing".to_string(),
3479 "unrendered-component:src/App.vue:OldWidget".to_string(),
3480 ]);
3481
3482 retain_introduced_dead_code(&mut results, &root, Some(&base));
3483
3484 assert_eq!(results.unprovided_injects.len(), 1);
3486 assert_eq!(results.unprovided_injects[0].inject.key_name, "new");
3487 assert!(results.unrendered_components.is_empty());
3488 }
3489
3490 #[test]
3491 fn retain_introduced_dead_code_filters_graph_findings() {
3492 let root = root();
3493 let a = root.join("src/a.ts");
3494 let b = root.join("src/b.ts");
3495 let mut results = AnalysisResults::default();
3496 results
3497 .circular_dependencies
3498 .push(CircularDependencyFinding::with_actions(
3499 CircularDependency {
3500 files: vec![a.clone(), b],
3501 length: 2,
3502 line: 1,
3503 col: 0,
3504 edges: Vec::new(),
3505 is_cross_package: false,
3506 },
3507 ));
3508 results
3509 .re_export_cycles
3510 .push(ReExportCycleFinding::with_actions(ReExportCycle {
3511 files: vec![a],
3512 kind: ReExportCycleKind::SelfLoop,
3513 }));
3514
3515 let base = FxHashSet::from_iter(["circular-dependency:src/a.ts|src/b.ts".to_string()]);
3517
3518 retain_introduced_dead_code(&mut results, &root, Some(&base));
3519
3520 assert!(results.circular_dependencies.is_empty());
3521 assert_eq!(results.re_export_cycles.len(), 1);
3522 }
3523}