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