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