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