1use std::path::{Path, PathBuf};
4
5use serde::{Deserialize, Serialize};
6
7use crate::extract::{
8 MemberKind, SecurityControlKind, SecurityUrlShape, SkippedSecurityCalleeExpressionKind,
9 SkippedSecurityCalleeReason,
10};
11use crate::output::{
12 FixAction, FixActionType, IssueAction, SuppressLineAction, SuppressLineKind, SuppressLineScope,
13};
14use crate::output_dead_code::{
15 BoundaryCallViolationFinding, BoundaryCoverageViolationFinding, BoundaryViolationFinding,
16 CircularDependencyFinding, DevDependencyInProductionFinding, DuplicateExportFinding,
17 DuplicatePropShapeFinding, DynamicSegmentNameConflictFinding, EmptyCatalogGroupFinding,
18 InvalidClientExportFinding, MisconfiguredDependencyOverrideFinding, MisplacedDirectiveFinding,
19 MixedClientServerBarrelFinding, PolicyViolationFinding, PrivateTypeLeakFinding,
20 PropDrillingChainFinding, ReExportCycleFinding, RouteCollisionFinding,
21 TestOnlyDependencyFinding, ThinWrapperFinding, TypeOnlyDependencyFinding,
22 UnlistedDependencyFinding, UnprovidedInjectFinding, UnrenderedComponentFinding,
23 UnresolvedCatalogReferenceFinding, UnresolvedImportFinding, UnusedCatalogEntryFinding,
24 UnusedClassMemberFinding, UnusedComponentEmitFinding, UnusedComponentInputFinding,
25 UnusedComponentOutputFinding, UnusedComponentPropFinding, UnusedDependencyFinding,
26 UnusedDependencyOverrideFinding, UnusedDevDependencyFinding, UnusedEnumMemberFinding,
27 UnusedExportFinding, UnusedFileFinding, UnusedLoadDataKeyFinding,
28 UnusedOptionalDependencyFinding, UnusedServerActionFinding, UnusedStoreMemberFinding,
29 UnusedSvelteEventFinding, UnusedTypeFinding,
30};
31use crate::serde_path;
32use crate::suppress::{IssueKind, closest_known_kind_name};
33
34#[derive(Debug, Clone, Default)]
39#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
40pub struct EntryPointSummary {
41 pub total: usize,
43 pub by_source: Vec<(String, usize)>,
46}
47
48#[derive(Debug, Clone, Default)]
70pub struct RenderFanInMetric {
71 pub per_component: Vec<RenderFanInComponent>,
76 pub p95_distinct_parents: Option<u32>,
80 pub high_pct: Option<f64>,
84 pub max_distinct_parents: Option<u32>,
90}
91
92#[derive(Debug, Clone)]
95pub struct RenderFanInComponent {
96 pub file: PathBuf,
98 pub component: String,
100 pub render_sites: u32,
105 pub distinct_parents: u32,
110}
111
112#[derive(Debug, Clone, Default, PartialEq, Eq)]
117pub struct ReactHookSummary {
118 pub state: u16,
120 pub effect: u16,
122 pub memo: u16,
124 pub callback: u16,
126 pub custom: u16,
128}
129
130#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct ReactPropDrill {
140 pub depth: u32,
143 pub hops: Vec<String>,
146}
147
148#[derive(Debug, Clone, PartialEq, Eq)]
156pub struct ReactPropIntel {
157 pub name: String,
159 pub anchor_line: u32,
161 pub anchor_col: u32,
164 pub used_in_body: bool,
167 pub passed_from_sites: u32,
170 pub drill: Option<ReactPropDrill>,
175}
176
177#[derive(Debug, Clone, PartialEq, Eq)]
190pub struct ReactComponentIntel {
191 pub path: PathBuf,
193 pub component_name: String,
195 pub anchor_line: u32,
197 pub anchor_col: u32,
199 pub render_sites: u32,
203 pub distinct_parents: u32,
206 pub prop_count: u16,
208 pub hooks: ReactHookSummary,
210 pub props: Vec<ReactPropIntel>,
212}
213
214#[derive(Debug, Default, Clone, Serialize, Deserialize)]
236#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
237pub struct AnalysisResults {
238 pub unused_files: Vec<UnusedFileFinding>,
242 pub unused_exports: Vec<UnusedExportFinding>,
246 pub unused_types: Vec<UnusedTypeFinding>,
251 pub private_type_leaks: Vec<PrivateTypeLeakFinding>,
255 pub unused_dependencies: Vec<UnusedDependencyFinding>,
260 pub unused_dev_dependencies: Vec<UnusedDevDependencyFinding>,
265 pub unused_optional_dependencies: Vec<UnusedOptionalDependencyFinding>,
269 pub unused_enum_members: Vec<UnusedEnumMemberFinding>,
273 pub unused_class_members: Vec<UnusedClassMemberFinding>,
279 #[serde(default, skip_serializing_if = "Vec::is_empty")]
286 pub unused_store_members: Vec<UnusedStoreMemberFinding>,
287 pub unresolved_imports: Vec<UnresolvedImportFinding>,
291 pub unlisted_dependencies: Vec<UnlistedDependencyFinding>,
294 pub duplicate_exports: Vec<DuplicateExportFinding>,
299 pub type_only_dependencies: Vec<TypeOnlyDependencyFinding>,
303 #[serde(default)]
306 pub test_only_dependencies: Vec<TestOnlyDependencyFinding>,
307 #[serde(default)]
312 pub dev_dependencies_in_production: Vec<DevDependencyInProductionFinding>,
313 pub circular_dependencies: Vec<CircularDependencyFinding>,
317 #[serde(default)]
324 pub re_export_cycles: Vec<ReExportCycleFinding>,
325 #[serde(default)]
329 pub boundary_violations: Vec<BoundaryViolationFinding>,
330 #[serde(default)]
333 pub boundary_coverage_violations: Vec<BoundaryCoverageViolationFinding>,
334 #[serde(default)]
339 pub boundary_call_violations: Vec<BoundaryCallViolationFinding>,
340 #[serde(default)]
346 pub policy_violations: Vec<PolicyViolationFinding>,
347 #[serde(default)]
349 pub stale_suppressions: Vec<StaleSuppression>,
350 #[serde(default)]
357 pub unused_catalog_entries: Vec<UnusedCatalogEntryFinding>,
358 #[serde(default)]
362 pub empty_catalog_groups: Vec<EmptyCatalogGroupFinding>,
363 #[serde(default)]
370 pub unresolved_catalog_references: Vec<UnresolvedCatalogReferenceFinding>,
371 #[serde(default)]
379 pub unused_dependency_overrides: Vec<UnusedDependencyOverrideFinding>,
380 #[serde(default)]
386 pub misconfigured_dependency_overrides: Vec<MisconfiguredDependencyOverrideFinding>,
387 #[serde(default)]
392 pub invalid_client_exports: Vec<InvalidClientExportFinding>,
393 #[serde(default)]
398 pub mixed_client_server_barrels: Vec<MixedClientServerBarrelFinding>,
399 #[serde(default)]
405 pub misplaced_directives: Vec<MisplacedDirectiveFinding>,
406 #[serde(default, skip_serializing_if = "Vec::is_empty")]
411 pub unprovided_injects: Vec<UnprovidedInjectFinding>,
412 #[serde(default, skip_serializing_if = "Vec::is_empty")]
417 pub unrendered_components: Vec<UnrenderedComponentFinding>,
418 #[serde(default)]
423 pub route_collisions: Vec<RouteCollisionFinding>,
424 #[serde(default)]
429 pub dynamic_segment_name_conflicts: Vec<DynamicSegmentNameConflictFinding>,
430 #[serde(default, skip_serializing_if = "Vec::is_empty")]
435 pub unused_component_props: Vec<UnusedComponentPropFinding>,
436 #[serde(default, skip_serializing_if = "Vec::is_empty")]
441 pub unused_component_emits: Vec<UnusedComponentEmitFinding>,
442 #[serde(default, skip_serializing_if = "Vec::is_empty")]
447 pub unused_component_inputs: Vec<UnusedComponentInputFinding>,
448 #[serde(default, skip_serializing_if = "Vec::is_empty")]
453 pub unused_component_outputs: Vec<UnusedComponentOutputFinding>,
454 #[serde(default, skip_serializing_if = "Vec::is_empty")]
460 pub unused_svelte_events: Vec<UnusedSvelteEventFinding>,
461 #[serde(default, skip_serializing_if = "Vec::is_empty")]
467 pub unused_server_actions: Vec<UnusedServerActionFinding>,
468 #[serde(default, skip_serializing_if = "Vec::is_empty")]
472 pub unused_load_data_keys: Vec<UnusedLoadDataKeyFinding>,
473 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
479 pub unused_load_data_keys_global_abstain: bool,
480 #[serde(default, skip_serializing_if = "Vec::is_empty")]
486 pub prop_drilling_chains: Vec<PropDrillingChainFinding>,
487 #[serde(default, skip_serializing_if = "Vec::is_empty")]
494 pub thin_wrappers: Vec<ThinWrapperFinding>,
495 #[serde(default, skip_serializing_if = "Vec::is_empty")]
504 pub duplicate_prop_shapes: Vec<DuplicatePropShapeFinding>,
505 #[serde(skip)]
509 pub suppression_count: usize,
510 #[serde(skip)]
516 pub unused_component_props_exempted: usize,
517 #[serde(skip)]
524 pub active_suppressions: Vec<ActiveSuppression>,
525 #[serde(skip)]
528 pub feature_flags: Vec<FeatureFlag>,
529 #[serde(skip)]
537 pub security_findings: Vec<SecurityFinding>,
538 #[serde(skip)]
544 pub security_unresolved_edge_files: usize,
545 #[serde(skip)]
551 pub security_unresolved_callee_sites: usize,
552 #[serde(skip)]
556 pub security_unresolved_callee_diagnostics: Vec<SecurityUnresolvedCalleeDiagnostic>,
557 #[serde(skip)]
561 pub export_usages: Vec<ExportUsage>,
562 #[serde(skip)]
566 pub entry_point_summary: Option<EntryPointSummary>,
567 #[serde(skip)]
576 pub render_fan_in: Option<RenderFanInMetric>,
577 #[serde(skip)]
585 pub react_component_intel: Vec<ReactComponentIntel>,
586 #[serde(skip)]
589 #[cfg_attr(feature = "schema", schemars(skip))]
590 pub semantic_framework_contracts: Vec<crate::semantic::SemanticFrameworkContract>,
591}
592
593struct AnalysisResultsCoreMergeParts {
594 unused_files: Vec<UnusedFileFinding>,
595 unused_exports: Vec<UnusedExportFinding>,
596 unused_types: Vec<UnusedTypeFinding>,
597 private_type_leaks: Vec<PrivateTypeLeakFinding>,
598 unused_enum_members: Vec<UnusedEnumMemberFinding>,
599 unused_class_members: Vec<UnusedClassMemberFinding>,
600 unused_store_members: Vec<UnusedStoreMemberFinding>,
601 unresolved_imports: Vec<UnresolvedImportFinding>,
602 boundary_violations: Vec<BoundaryViolationFinding>,
603 boundary_coverage_violations: Vec<BoundaryCoverageViolationFinding>,
604 boundary_call_violations: Vec<BoundaryCallViolationFinding>,
605 policy_violations: Vec<PolicyViolationFinding>,
606 stale_suppressions: Vec<StaleSuppression>,
607}
608
609struct AnalysisResultsGraphMergeParts {
610 unused_dependencies: Vec<UnusedDependencyFinding>,
611 unused_dev_dependencies: Vec<UnusedDevDependencyFinding>,
612 unused_optional_dependencies: Vec<UnusedOptionalDependencyFinding>,
613 unlisted_dependencies: Vec<UnlistedDependencyFinding>,
614 duplicate_exports: Vec<DuplicateExportFinding>,
615 type_only_dependencies: Vec<TypeOnlyDependencyFinding>,
616 test_only_dependencies: Vec<TestOnlyDependencyFinding>,
617 dev_dependencies_in_production: Vec<DevDependencyInProductionFinding>,
618 circular_dependencies: Vec<CircularDependencyFinding>,
619 re_export_cycles: Vec<ReExportCycleFinding>,
620}
621
622struct AnalysisResultsWorkspaceMergeParts {
623 unused_catalog_entries: Vec<UnusedCatalogEntryFinding>,
624 empty_catalog_groups: Vec<EmptyCatalogGroupFinding>,
625 unresolved_catalog_references: Vec<UnresolvedCatalogReferenceFinding>,
626 unused_dependency_overrides: Vec<UnusedDependencyOverrideFinding>,
627 misconfigured_dependency_overrides: Vec<MisconfiguredDependencyOverrideFinding>,
628}
629
630struct AnalysisResultsFrameworkMergeParts {
631 invalid_client_exports: Vec<InvalidClientExportFinding>,
632 mixed_client_server_barrels: Vec<MixedClientServerBarrelFinding>,
633 misplaced_directives: Vec<MisplacedDirectiveFinding>,
634 unprovided_injects: Vec<UnprovidedInjectFinding>,
635 unrendered_components: Vec<UnrenderedComponentFinding>,
636 route_collisions: Vec<RouteCollisionFinding>,
637 dynamic_segment_name_conflicts: Vec<DynamicSegmentNameConflictFinding>,
638 unused_component_props: Vec<UnusedComponentPropFinding>,
639 unused_component_emits: Vec<UnusedComponentEmitFinding>,
640 unused_component_inputs: Vec<UnusedComponentInputFinding>,
641 unused_component_outputs: Vec<UnusedComponentOutputFinding>,
642 unused_svelte_events: Vec<UnusedSvelteEventFinding>,
643 unused_server_actions: Vec<UnusedServerActionFinding>,
644 unused_load_data_keys: Vec<UnusedLoadDataKeyFinding>,
645 unused_load_data_keys_global_abstain: bool,
646 prop_drilling_chains: Vec<PropDrillingChainFinding>,
647 thin_wrappers: Vec<ThinWrapperFinding>,
648 duplicate_prop_shapes: Vec<DuplicatePropShapeFinding>,
649}
650
651struct AnalysisResultsMetadataMergeParts {
652 suppression_count: usize,
653 unused_component_props_exempted: usize,
654 active_suppressions: Vec<ActiveSuppression>,
655 feature_flags: Vec<FeatureFlag>,
656 security_findings: Vec<SecurityFinding>,
657 security_unresolved_edge_files: usize,
658 security_unresolved_callee_sites: usize,
659 security_unresolved_callee_diagnostics: Vec<SecurityUnresolvedCalleeDiagnostic>,
660 export_usages: Vec<ExportUsage>,
661 entry_point_summary: Option<EntryPointSummary>,
662 render_fan_in: Option<RenderFanInMetric>,
663 react_component_intel: Vec<ReactComponentIntel>,
664 semantic_framework_contracts: Vec<crate::semantic::SemanticFrameworkContract>,
665}
666
667#[expect(
674 clippy::too_many_lines,
675 reason = "irreducible single exhaustive field-routing: the one `let Self { .. }` destructure must name every field so a newly added field is a compile error (issue #444); splitting it would defeat that exhaustiveness guarantee"
676)]
677fn split_merge_parts(
678 other: AnalysisResults,
679) -> (
680 AnalysisResultsCoreMergeParts,
681 AnalysisResultsGraphMergeParts,
682 AnalysisResultsWorkspaceMergeParts,
683 AnalysisResultsFrameworkMergeParts,
684 AnalysisResultsMetadataMergeParts,
685) {
686 let AnalysisResults {
687 unused_files,
688 unused_exports,
689 unused_types,
690 private_type_leaks,
691 unused_dependencies,
692 unused_dev_dependencies,
693 unused_optional_dependencies,
694 unused_enum_members,
695 unused_class_members,
696 unused_store_members,
697 unresolved_imports,
698 unlisted_dependencies,
699 duplicate_exports,
700 type_only_dependencies,
701 test_only_dependencies,
702 dev_dependencies_in_production,
703 circular_dependencies,
704 re_export_cycles,
705 boundary_violations,
706 boundary_coverage_violations,
707 boundary_call_violations,
708 policy_violations,
709 stale_suppressions,
710 unused_catalog_entries,
711 empty_catalog_groups,
712 unresolved_catalog_references,
713 unused_dependency_overrides,
714 misconfigured_dependency_overrides,
715 invalid_client_exports,
716 mixed_client_server_barrels,
717 misplaced_directives,
718 unprovided_injects,
719 unrendered_components,
720 route_collisions,
721 dynamic_segment_name_conflicts,
722 unused_component_props,
723 unused_component_emits,
724 unused_component_inputs,
725 unused_component_outputs,
726 unused_svelte_events,
727 unused_server_actions,
728 unused_load_data_keys,
729 unused_load_data_keys_global_abstain,
730 prop_drilling_chains,
731 thin_wrappers,
732 duplicate_prop_shapes,
733 suppression_count,
734 unused_component_props_exempted,
735 active_suppressions,
736 feature_flags,
737 security_findings,
738 security_unresolved_edge_files,
739 security_unresolved_callee_sites,
740 security_unresolved_callee_diagnostics,
741 export_usages,
742 entry_point_summary,
743 render_fan_in,
744 react_component_intel,
745 semantic_framework_contracts,
746 } = other;
747
748 (
749 AnalysisResultsCoreMergeParts {
750 unused_files,
751 unused_exports,
752 unused_types,
753 private_type_leaks,
754 unused_enum_members,
755 unused_class_members,
756 unused_store_members,
757 unresolved_imports,
758 boundary_violations,
759 boundary_coverage_violations,
760 boundary_call_violations,
761 policy_violations,
762 stale_suppressions,
763 },
764 AnalysisResultsGraphMergeParts {
765 unused_dependencies,
766 unused_dev_dependencies,
767 unused_optional_dependencies,
768 unlisted_dependencies,
769 duplicate_exports,
770 type_only_dependencies,
771 test_only_dependencies,
772 dev_dependencies_in_production,
773 circular_dependencies,
774 re_export_cycles,
775 },
776 AnalysisResultsWorkspaceMergeParts {
777 unused_catalog_entries,
778 empty_catalog_groups,
779 unresolved_catalog_references,
780 unused_dependency_overrides,
781 misconfigured_dependency_overrides,
782 },
783 AnalysisResultsFrameworkMergeParts {
784 invalid_client_exports,
785 mixed_client_server_barrels,
786 misplaced_directives,
787 unprovided_injects,
788 unrendered_components,
789 route_collisions,
790 dynamic_segment_name_conflicts,
791 unused_component_props,
792 unused_component_emits,
793 unused_component_inputs,
794 unused_component_outputs,
795 unused_svelte_events,
796 unused_server_actions,
797 unused_load_data_keys,
798 unused_load_data_keys_global_abstain,
799 prop_drilling_chains,
800 thin_wrappers,
801 duplicate_prop_shapes,
802 },
803 AnalysisResultsMetadataMergeParts {
804 suppression_count,
805 unused_component_props_exempted,
806 active_suppressions,
807 feature_flags,
808 security_findings,
809 security_unresolved_edge_files,
810 security_unresolved_callee_sites,
811 security_unresolved_callee_diagnostics,
812 export_usages,
813 entry_point_summary,
814 render_fan_in,
815 react_component_intel,
816 semantic_framework_contracts,
817 },
818 )
819}
820
821macro_rules! counted_analysis_result_fields {
822 ($callback:ident $(, $arg:expr)? ) => {
823 $callback! {
824 $($arg,)?
825 unused_files => "unused_files",
826 unused_exports => "unused_exports",
827 unused_types => "unused_types",
828 private_type_leaks => "private_type_leaks",
829 unused_dependencies => "unused_dependencies",
830 unused_dev_dependencies => "unused_dev_dependencies",
831 unused_optional_dependencies => "unused_optional_dependencies",
832 unused_enum_members => "unused_enum_members",
833 unused_class_members => "unused_class_members",
834 unused_store_members => "unused_store_members",
835 unresolved_imports => "unresolved_imports",
836 unlisted_dependencies => "unlisted_dependencies",
837 duplicate_exports => "duplicate_exports",
838 type_only_dependencies => "type_only_dependencies",
839 test_only_dependencies => "test_only_dependencies",
840 dev_dependencies_in_production => "dev_dependencies_in_production",
841 circular_dependencies => "circular_dependencies",
842 re_export_cycles => "re_export_cycles",
843 boundary_violations => "boundary_violations",
844 boundary_coverage_violations => "boundary_coverage_violations",
845 boundary_call_violations => "boundary_call_violations",
846 policy_violations => "policy_violations",
847 stale_suppressions => "stale_suppressions",
848 unused_catalog_entries => "unused_catalog_entries",
849 empty_catalog_groups => "empty_catalog_groups",
850 unresolved_catalog_references => "unresolved_catalog_references",
851 unused_dependency_overrides => "unused_dependency_overrides",
852 misconfigured_dependency_overrides => "misconfigured_dependency_overrides",
853 invalid_client_exports => "invalid_client_exports",
854 mixed_client_server_barrels => "mixed_client_server_barrels",
855 misplaced_directives => "misplaced_directives",
856 unprovided_injects => "unprovided_injects",
857 unrendered_components => "unrendered_components",
858 route_collisions => "route_collisions",
859 dynamic_segment_name_conflicts => "dynamic_segment_name_conflicts",
860 unused_component_props => "unused_component_props",
861 unused_component_emits => "unused_component_emits",
862 unused_component_inputs => "unused_component_inputs",
863 unused_component_outputs => "unused_component_outputs",
864 unused_svelte_events => "unused_svelte_events",
865 unused_server_actions => "unused_server_actions",
866 unused_load_data_keys => "unused_load_data_keys",
867 }
868 };
869}
870
871trait FindingIgnorePolicy {
872 fn should_ignore(&self, predicate: &mut impl FnMut(&Path) -> bool) -> bool;
873}
874
875macro_rules! impl_single_source_dead_code {
876 ($($finding:ty => $($field:ident).+),+ $(,)?) => {
877 $(
878 impl FindingIgnorePolicy for $finding {
879 fn should_ignore(
880 &self,
881 predicate: &mut impl FnMut(&Path) -> bool,
882 ) -> bool {
883 predicate(&self.$($field).+)
884 }
885 }
886 )+
887 };
888}
889
890impl_single_source_dead_code! {
891 UnusedFileFinding => file.path,
892 UnusedExportFinding => export.path,
893 UnusedTypeFinding => export.path,
894 PrivateTypeLeakFinding => leak.path,
895 UnusedEnumMemberFinding => member.path,
896 UnusedClassMemberFinding => member.path,
897 UnusedStoreMemberFinding => member.path,
898 UnresolvedImportFinding => import.path,
899 UnprovidedInjectFinding => inject.path,
900 UnrenderedComponentFinding => component.path,
901 UnusedComponentPropFinding => prop.path,
902 UnusedComponentEmitFinding => emit.path,
903 UnusedComponentInputFinding => input.path,
904 UnusedComponentOutputFinding => output.path,
905 UnusedSvelteEventFinding => event.path,
906 UnusedServerActionFinding => action.path,
907 UnusedLoadDataKeyFinding => key.path,
908 ThinWrapperFinding => wrapper.file,
909 DuplicatePropShapeFinding => shape.file,
910}
911
912macro_rules! impl_never_ignored_finding {
913 ($($finding:ty),+ $(,)?) => {
914 $(
915 impl FindingIgnorePolicy for $finding {
916 fn should_ignore(
917 &self,
918 _predicate: &mut impl FnMut(&Path) -> bool,
919 ) -> bool {
920 false
921 }
922 }
923 )+
924 };
925}
926
927impl_never_ignored_finding! {
928 UnusedDependencyFinding,
929 UnusedDevDependencyFinding,
930 UnusedOptionalDependencyFinding,
931 TypeOnlyDependencyFinding,
932 TestOnlyDependencyFinding,
933 DevDependencyInProductionFinding,
934 UnusedCatalogEntryFinding,
935 EmptyCatalogGroupFinding,
936 UnresolvedCatalogReferenceFinding,
937 UnusedDependencyOverrideFinding,
938 MisconfiguredDependencyOverrideFinding,
939 BoundaryViolationFinding,
940 BoundaryCoverageViolationFinding,
941 BoundaryCallViolationFinding,
942 PolicyViolationFinding,
943 StaleSuppression,
944 InvalidClientExportFinding,
945 MixedClientServerBarrelFinding,
946 MisplacedDirectiveFinding,
947 RouteCollisionFinding,
948 DynamicSegmentNameConflictFinding,
949}
950
951fn all_nonempty_paths_match<'a>(
952 mut paths: impl Iterator<Item = &'a PathBuf>,
953 predicate: &mut impl FnMut(&Path) -> bool,
954) -> bool {
955 let Some(first) = paths.next() else {
956 return false;
957 };
958 predicate(first) && paths.all(|path| predicate(path))
959}
960
961impl FindingIgnorePolicy for UnlistedDependencyFinding {
962 fn should_ignore(&self, predicate: &mut impl FnMut(&Path) -> bool) -> bool {
963 all_nonempty_paths_match(
964 self.dep.imported_from.iter().map(|site| &site.path),
965 predicate,
966 )
967 }
968}
969
970impl FindingIgnorePolicy for DuplicateExportFinding {
971 fn should_ignore(&self, predicate: &mut impl FnMut(&Path) -> bool) -> bool {
972 all_nonempty_paths_match(
973 self.export.locations.iter().map(|location| &location.path),
974 predicate,
975 )
976 }
977}
978
979impl FindingIgnorePolicy for CircularDependencyFinding {
980 fn should_ignore(&self, predicate: &mut impl FnMut(&Path) -> bool) -> bool {
981 all_nonempty_paths_match(self.cycle.files.iter(), predicate)
982 }
983}
984
985impl FindingIgnorePolicy for ReExportCycleFinding {
986 fn should_ignore(&self, predicate: &mut impl FnMut(&Path) -> bool) -> bool {
987 all_nonempty_paths_match(self.cycle.files.iter(), predicate)
988 }
989}
990
991impl FindingIgnorePolicy for PropDrillingChainFinding {
992 fn should_ignore(&self, predicate: &mut impl FnMut(&Path) -> bool) -> bool {
993 all_nonempty_paths_match(self.chain.hops.iter().map(|hop| &hop.file), predicate)
994 }
995}
996
997macro_rules! uncounted_source_owned_result_fields {
1004 ($callback:ident $(, $arg:expr)? ) => {
1005 $callback! {
1006 $($arg,)?
1007 prop_drilling_chains => "prop_drilling_chains",
1008 thin_wrappers => "thin_wrappers",
1009 duplicate_prop_shapes => "duplicate_prop_shapes",
1010 }
1011 };
1012}
1013
1014macro_rules! remove_configured_ignored_findings {
1015 ($state:expr, $($field:ident => $key:literal,)+) => {{
1016 let (results, predicate) = $state;
1017 $(
1018 results.$field.retain(|issue| {
1019 !issue.should_ignore(&mut *predicate)
1020 });
1021 )+
1022 }};
1023}
1024
1025macro_rules! counted_result_key_slice {
1026 ($($field:ident => $key:literal,)+) => {
1027 &[$($key),+]
1028 };
1029}
1030
1031macro_rules! counted_result_field_sum {
1032 ($results:expr, $($field:ident => $key:literal,)+) => {
1033 0 $(+ ($results).$field.len())+
1034 };
1035}
1036
1037pub const TOTAL_ISSUE_RESULT_KEYS: &[&str] =
1039 counted_analysis_result_fields!(counted_result_key_slice);
1040
1041fn classify_ignore_findings_fields(results: &AnalysisResults) {
1049 let AnalysisResults {
1050 unused_files: _unused_files,
1052 unused_exports: _unused_exports,
1053 unused_types: _unused_types,
1054 private_type_leaks: _private_type_leaks,
1055 unused_enum_members: _unused_enum_members,
1056 unused_class_members: _unused_class_members,
1057 unused_store_members: _unused_store_members,
1058 unresolved_imports: _unresolved_imports,
1059 unlisted_dependencies: _unlisted_dependencies,
1060 duplicate_exports: _duplicate_exports,
1061 circular_dependencies: _circular_dependencies,
1062 re_export_cycles: _re_export_cycles,
1063 unprovided_injects: _unprovided_injects,
1064 unrendered_components: _unrendered_components,
1065 unused_component_props: _unused_component_props,
1066 unused_component_emits: _unused_component_emits,
1067 unused_component_inputs: _unused_component_inputs,
1068 unused_component_outputs: _unused_component_outputs,
1069 unused_svelte_events: _unused_svelte_events,
1070 unused_server_actions: _unused_server_actions,
1071 unused_load_data_keys: _unused_load_data_keys,
1072 prop_drilling_chains: _prop_drilling_chains,
1074 thin_wrappers: _thin_wrappers,
1075 duplicate_prop_shapes: _duplicate_prop_shapes,
1076 unused_dependencies: _unused_dependencies,
1078 unused_dev_dependencies: _unused_dev_dependencies,
1079 unused_optional_dependencies: _unused_optional_dependencies,
1080 type_only_dependencies: _type_only_dependencies,
1081 test_only_dependencies: _test_only_dependencies,
1082 dev_dependencies_in_production: _dev_dependencies_in_production,
1083 unused_catalog_entries: _unused_catalog_entries,
1084 empty_catalog_groups: _empty_catalog_groups,
1085 unresolved_catalog_references: _unresolved_catalog_references,
1086 unused_dependency_overrides: _unused_dependency_overrides,
1087 misconfigured_dependency_overrides: _misconfigured_dependency_overrides,
1088 boundary_violations: _boundary_violations,
1091 boundary_coverage_violations: _boundary_coverage_violations,
1092 boundary_call_violations: _boundary_call_violations,
1093 policy_violations: _policy_violations,
1094 stale_suppressions: _stale_suppressions,
1095 invalid_client_exports: _invalid_client_exports,
1096 mixed_client_server_barrels: _mixed_client_server_barrels,
1097 misplaced_directives: _misplaced_directives,
1098 route_collisions: _route_collisions,
1099 dynamic_segment_name_conflicts: _dynamic_segment_name_conflicts,
1100 security_findings: _security_findings,
1104 security_unresolved_edge_files: _security_unresolved_edge_files,
1105 security_unresolved_callee_sites: _security_unresolved_callee_sites,
1106 security_unresolved_callee_diagnostics: _security_unresolved_callee_diagnostics,
1107 unused_load_data_keys_global_abstain: _unused_load_data_keys_global_abstain,
1109 suppression_count: _suppression_count,
1110 unused_component_props_exempted: _unused_component_props_exempted,
1111 active_suppressions: _active_suppressions,
1112 feature_flags: _feature_flags,
1113 export_usages: _export_usages,
1114 entry_point_summary: _entry_point_summary,
1115 render_fan_in: _render_fan_in,
1116 react_component_intel: _react_component_intel,
1117 semantic_framework_contracts: _semantic_framework_contracts,
1118 } = results;
1119}
1120
1121impl AnalysisResults {
1122 #[doc(hidden)]
1129 pub fn remove_ignored_dead_code_findings(&mut self, mut is_ignored: impl FnMut(&Path) -> bool) {
1130 classify_ignore_findings_fields(self);
1131 counted_analysis_result_fields!(
1132 remove_configured_ignored_findings,
1133 (&mut *self, &mut is_ignored)
1134 );
1135 uncounted_source_owned_result_fields!(
1136 remove_configured_ignored_findings,
1137 (&mut *self, &mut is_ignored)
1138 );
1139 }
1140
1141 #[must_use]
1172 pub const fn total_issues(&self) -> usize {
1173 counted_analysis_result_fields!(counted_result_field_sum, self)
1174 }
1175
1176 #[must_use]
1178 pub const fn has_issues(&self) -> bool {
1179 self.total_issues() > 0
1180 }
1181
1182 pub fn merge_into(&mut self, other: Self) {
1195 let (core, graph, workspace, framework, metadata) = split_merge_parts(other);
1196 self.merge_core_findings(core);
1197 self.merge_dependency_and_graph_findings(graph);
1198 self.merge_workspace_findings(workspace);
1199 self.merge_framework_findings(framework);
1200 self.merge_metadata_and_security(metadata);
1201 }
1202
1203 fn merge_core_findings(&mut self, parts: AnalysisResultsCoreMergeParts) {
1204 self.unused_files.extend(parts.unused_files);
1205 self.unused_exports.extend(parts.unused_exports);
1206 self.unused_types.extend(parts.unused_types);
1207 self.private_type_leaks.extend(parts.private_type_leaks);
1208 self.unused_enum_members.extend(parts.unused_enum_members);
1209 self.unused_class_members.extend(parts.unused_class_members);
1210 self.unused_store_members.extend(parts.unused_store_members);
1211 self.unresolved_imports.extend(parts.unresolved_imports);
1212 self.boundary_violations.extend(parts.boundary_violations);
1213 self.boundary_coverage_violations
1214 .extend(parts.boundary_coverage_violations);
1215 self.boundary_call_violations
1216 .extend(parts.boundary_call_violations);
1217 self.policy_violations.extend(parts.policy_violations);
1218 self.stale_suppressions.extend(parts.stale_suppressions);
1219 }
1220
1221 fn merge_dependency_and_graph_findings(&mut self, parts: AnalysisResultsGraphMergeParts) {
1222 self.unused_dependencies.extend(parts.unused_dependencies);
1223 self.unused_dev_dependencies
1224 .extend(parts.unused_dev_dependencies);
1225 self.unused_optional_dependencies
1226 .extend(parts.unused_optional_dependencies);
1227 self.unlisted_dependencies
1228 .extend(parts.unlisted_dependencies);
1229 self.duplicate_exports.extend(parts.duplicate_exports);
1230 self.type_only_dependencies
1231 .extend(parts.type_only_dependencies);
1232 self.test_only_dependencies
1233 .extend(parts.test_only_dependencies);
1234 self.dev_dependencies_in_production
1235 .extend(parts.dev_dependencies_in_production);
1236 self.circular_dependencies
1237 .extend(parts.circular_dependencies);
1238 self.re_export_cycles.extend(parts.re_export_cycles);
1239 }
1240
1241 fn merge_workspace_findings(&mut self, parts: AnalysisResultsWorkspaceMergeParts) {
1242 self.unused_catalog_entries
1243 .extend(parts.unused_catalog_entries);
1244 self.empty_catalog_groups.extend(parts.empty_catalog_groups);
1245 self.unresolved_catalog_references
1246 .extend(parts.unresolved_catalog_references);
1247 self.unused_dependency_overrides
1248 .extend(parts.unused_dependency_overrides);
1249 self.misconfigured_dependency_overrides
1250 .extend(parts.misconfigured_dependency_overrides);
1251 }
1252
1253 fn merge_framework_findings(&mut self, parts: AnalysisResultsFrameworkMergeParts) {
1254 self.invalid_client_exports
1255 .extend(parts.invalid_client_exports);
1256 self.mixed_client_server_barrels
1257 .extend(parts.mixed_client_server_barrels);
1258 self.misplaced_directives.extend(parts.misplaced_directives);
1259 self.unprovided_injects.extend(parts.unprovided_injects);
1260 self.unrendered_components
1261 .extend(parts.unrendered_components);
1262 self.route_collisions.extend(parts.route_collisions);
1263 self.dynamic_segment_name_conflicts
1264 .extend(parts.dynamic_segment_name_conflicts);
1265 self.unused_component_props
1266 .extend(parts.unused_component_props);
1267 self.unused_component_emits
1268 .extend(parts.unused_component_emits);
1269 self.unused_component_inputs
1270 .extend(parts.unused_component_inputs);
1271 self.unused_component_outputs
1272 .extend(parts.unused_component_outputs);
1273 self.unused_svelte_events.extend(parts.unused_svelte_events);
1274 self.unused_server_actions
1275 .extend(parts.unused_server_actions);
1276 self.unused_load_data_keys
1277 .extend(parts.unused_load_data_keys);
1278 self.unused_load_data_keys_global_abstain |= parts.unused_load_data_keys_global_abstain;
1279 self.prop_drilling_chains.extend(parts.prop_drilling_chains);
1280 self.thin_wrappers.extend(parts.thin_wrappers);
1281 self.duplicate_prop_shapes
1282 .extend(parts.duplicate_prop_shapes);
1283 }
1284
1285 fn merge_metadata_and_security(&mut self, parts: AnalysisResultsMetadataMergeParts) {
1286 self.feature_flags.extend(parts.feature_flags);
1287 self.security_findings.extend(parts.security_findings);
1288 self.security_unresolved_edge_files += parts.security_unresolved_edge_files;
1289 self.security_unresolved_callee_sites += parts.security_unresolved_callee_sites;
1290 self.security_unresolved_callee_diagnostics
1291 .extend(parts.security_unresolved_callee_diagnostics);
1292 self.export_usages.extend(parts.export_usages);
1293 self.active_suppressions.extend(parts.active_suppressions);
1294 self.suppression_count += parts.suppression_count;
1295 self.unused_component_props_exempted += parts.unused_component_props_exempted;
1296 if self.entry_point_summary.is_none() {
1297 self.entry_point_summary = parts.entry_point_summary;
1298 }
1299 if self.render_fan_in.is_none() {
1300 self.render_fan_in = parts.render_fan_in;
1301 }
1302 self.react_component_intel
1303 .extend(parts.react_component_intel);
1304 for contract in parts.semantic_framework_contracts {
1305 if !self.semantic_framework_contracts.contains(&contract) {
1306 self.semantic_framework_contracts.push(contract);
1307 }
1308 }
1309 }
1310
1311 pub fn sort(&mut self) {
1318 self.semantic_framework_contracts.sort();
1319 self.sort_core_findings();
1320 self.sort_dependency_findings();
1321 self.sort_graph_findings();
1322 self.sort_catalog_findings();
1323 self.sort_metadata_findings();
1324 self.sort_export_usages();
1325 }
1326
1327 fn sort_core_findings(&mut self) {
1328 self.sort_core_declaration_findings();
1329 self.sort_core_member_findings();
1330 self.sort_core_framework_findings();
1331 self.sort_core_route_and_load_findings();
1332 }
1333
1334 fn sort_core_declaration_findings(&mut self) {
1335 self.unused_files
1336 .sort_by(|a, b| a.file.path.cmp(&b.file.path));
1337
1338 self.unused_exports.sort_by(|a, b| {
1339 a.export
1340 .path
1341 .cmp(&b.export.path)
1342 .then(a.export.line.cmp(&b.export.line))
1343 .then(a.export.export_name.cmp(&b.export.export_name))
1344 });
1345
1346 self.unused_types.sort_by(|a, b| {
1347 a.export
1348 .path
1349 .cmp(&b.export.path)
1350 .then(a.export.line.cmp(&b.export.line))
1351 .then(a.export.export_name.cmp(&b.export.export_name))
1352 });
1353
1354 self.private_type_leaks.sort_by(|a, b| {
1355 a.leak
1356 .path
1357 .cmp(&b.leak.path)
1358 .then(a.leak.line.cmp(&b.leak.line))
1359 .then(a.leak.export_name.cmp(&b.leak.export_name))
1360 .then(a.leak.type_name.cmp(&b.leak.type_name))
1361 });
1362
1363 self.unused_dependencies.sort_by(|a, b| {
1364 a.dep
1365 .path
1366 .cmp(&b.dep.path)
1367 .then(a.dep.line.cmp(&b.dep.line))
1368 .then(a.dep.package_name.cmp(&b.dep.package_name))
1369 });
1370
1371 self.unused_dev_dependencies.sort_by(|a, b| {
1372 a.dep
1373 .path
1374 .cmp(&b.dep.path)
1375 .then(a.dep.line.cmp(&b.dep.line))
1376 .then(a.dep.package_name.cmp(&b.dep.package_name))
1377 });
1378
1379 self.unused_optional_dependencies.sort_by(|a, b| {
1380 a.dep
1381 .path
1382 .cmp(&b.dep.path)
1383 .then(a.dep.line.cmp(&b.dep.line))
1384 .then(a.dep.package_name.cmp(&b.dep.package_name))
1385 });
1386 }
1387
1388 fn sort_core_member_findings(&mut self) {
1389 self.unused_enum_members.sort_by(|a, b| {
1390 a.member
1391 .path
1392 .cmp(&b.member.path)
1393 .then(a.member.line.cmp(&b.member.line))
1394 .then(a.member.parent_name.cmp(&b.member.parent_name))
1395 .then(a.member.member_name.cmp(&b.member.member_name))
1396 });
1397
1398 self.unused_class_members.sort_by(|a, b| {
1399 a.member
1400 .path
1401 .cmp(&b.member.path)
1402 .then(a.member.line.cmp(&b.member.line))
1403 .then(a.member.parent_name.cmp(&b.member.parent_name))
1404 .then(a.member.member_name.cmp(&b.member.member_name))
1405 });
1406
1407 self.unused_store_members.sort_by(|a, b| {
1408 a.member
1409 .path
1410 .cmp(&b.member.path)
1411 .then(a.member.line.cmp(&b.member.line))
1412 .then(a.member.parent_name.cmp(&b.member.parent_name))
1413 .then(a.member.member_name.cmp(&b.member.member_name))
1414 });
1415
1416 self.unresolved_imports.sort_by(|a, b| {
1417 a.import
1418 .path
1419 .cmp(&b.import.path)
1420 .then(a.import.line.cmp(&b.import.line))
1421 .then(a.import.col.cmp(&b.import.col))
1422 .then(a.import.specifier.cmp(&b.import.specifier))
1423 });
1424 }
1425
1426 fn sort_core_framework_findings(&mut self) {
1427 self.invalid_client_exports.sort_by(|a, b| {
1428 a.export
1429 .path
1430 .cmp(&b.export.path)
1431 .then(a.export.line.cmp(&b.export.line))
1432 .then(a.export.export_name.cmp(&b.export.export_name))
1433 });
1434
1435 self.mixed_client_server_barrels.sort_by(|a, b| {
1436 a.barrel
1437 .path
1438 .cmp(&b.barrel.path)
1439 .then(a.barrel.line.cmp(&b.barrel.line))
1440 .then(a.barrel.client_origin.cmp(&b.barrel.client_origin))
1441 .then(a.barrel.server_origin.cmp(&b.barrel.server_origin))
1442 });
1443
1444 self.misplaced_directives.sort_by(|a, b| {
1445 a.directive_site
1446 .path
1447 .cmp(&b.directive_site.path)
1448 .then(a.directive_site.line.cmp(&b.directive_site.line))
1449 .then(a.directive_site.col.cmp(&b.directive_site.col))
1450 .then(a.directive_site.directive.cmp(&b.directive_site.directive))
1451 });
1452
1453 self.unprovided_injects.sort_by(|a, b| {
1454 a.inject
1455 .path
1456 .cmp(&b.inject.path)
1457 .then(a.inject.line.cmp(&b.inject.line))
1458 .then(a.inject.col.cmp(&b.inject.col))
1459 .then(a.inject.key_name.cmp(&b.inject.key_name))
1460 });
1461
1462 self.unrendered_components.sort_by(|a, b| {
1463 a.component
1464 .path
1465 .cmp(&b.component.path)
1466 .then(a.component.line.cmp(&b.component.line))
1467 .then(a.component.col.cmp(&b.component.col))
1468 .then(a.component.component_name.cmp(&b.component.component_name))
1469 });
1470 }
1471
1472 fn sort_core_route_and_load_findings(&mut self) {
1473 self.sort_core_route_findings();
1474 self.sort_core_component_prop_and_emit_findings();
1475 self.sort_core_component_io_findings();
1476 self.sort_core_server_load_findings();
1477 }
1478
1479 fn sort_core_route_findings(&mut self) {
1480 self.route_collisions.sort_by(|a, b| {
1481 a.collision
1482 .path
1483 .cmp(&b.collision.path)
1484 .then(a.collision.url.cmp(&b.collision.url))
1485 });
1486
1487 self.dynamic_segment_name_conflicts.sort_by(|a, b| {
1488 a.conflict
1489 .path
1490 .cmp(&b.conflict.path)
1491 .then(a.conflict.position.cmp(&b.conflict.position))
1492 });
1493 }
1494
1495 fn sort_core_component_prop_and_emit_findings(&mut self) {
1496 self.unused_component_props.sort_by(|a, b| {
1497 a.prop
1498 .path
1499 .cmp(&b.prop.path)
1500 .then(a.prop.line.cmp(&b.prop.line))
1501 .then(a.prop.prop_name.cmp(&b.prop.prop_name))
1502 });
1503
1504 self.unused_component_emits.sort_by(|a, b| {
1505 a.emit
1506 .path
1507 .cmp(&b.emit.path)
1508 .then(a.emit.line.cmp(&b.emit.line))
1509 .then(a.emit.emit_name.cmp(&b.emit.emit_name))
1510 });
1511
1512 self.unused_svelte_events.sort_by(|a, b| {
1513 a.event
1514 .path
1515 .cmp(&b.event.path)
1516 .then(a.event.line.cmp(&b.event.line))
1517 .then(a.event.event_name.cmp(&b.event.event_name))
1518 });
1519 }
1520
1521 fn sort_core_component_io_findings(&mut self) {
1522 self.unused_component_inputs.sort_by(|a, b| {
1523 a.input
1524 .path
1525 .cmp(&b.input.path)
1526 .then(a.input.line.cmp(&b.input.line))
1527 .then(a.input.input_name.cmp(&b.input.input_name))
1528 });
1529
1530 self.unused_component_outputs.sort_by(|a, b| {
1531 a.output
1532 .path
1533 .cmp(&b.output.path)
1534 .then(a.output.line.cmp(&b.output.line))
1535 .then(a.output.output_name.cmp(&b.output.output_name))
1536 });
1537 }
1538
1539 fn sort_core_server_load_findings(&mut self) {
1540 self.unused_server_actions.sort_by(|a, b| {
1541 a.action
1542 .path
1543 .cmp(&b.action.path)
1544 .then(a.action.line.cmp(&b.action.line))
1545 .then(a.action.col.cmp(&b.action.col))
1546 .then(a.action.action_name.cmp(&b.action.action_name))
1547 });
1548
1549 self.unused_load_data_keys.sort_by(|a, b| {
1550 a.key
1551 .path
1552 .cmp(&b.key.path)
1553 .then(a.key.line.cmp(&b.key.line))
1554 .then(a.key.col.cmp(&b.key.col))
1555 .then(a.key.key_name.cmp(&b.key.key_name))
1556 });
1557 }
1558
1559 fn sort_prop_drilling_chains(&mut self) {
1563 self.prop_drilling_chains.sort_by(|a, b| {
1564 let a_src = a.chain.hops.first();
1565 let b_src = b.chain.hops.first();
1566 let a_file = a_src.map(|h| &h.file);
1567 let b_file = b_src.map(|h| &h.file);
1568 a_file
1569 .cmp(&b_file)
1570 .then_with(|| a_src.map(|h| h.line).cmp(&b_src.map(|h| h.line)))
1571 .then(a.chain.prop.cmp(&b.chain.prop))
1572 .then(a.chain.depth.cmp(&b.chain.depth))
1573 });
1574 }
1575
1576 fn sort_thin_wrappers(&mut self) {
1579 self.thin_wrappers.sort_by(|a, b| {
1580 a.wrapper
1581 .file
1582 .cmp(&b.wrapper.file)
1583 .then(a.wrapper.line.cmp(&b.wrapper.line))
1584 .then(a.wrapper.component.cmp(&b.wrapper.component))
1585 });
1586 }
1587
1588 fn sort_duplicate_prop_shapes(&mut self) {
1592 self.duplicate_prop_shapes.sort_by(|a, b| {
1593 a.shape
1594 .shape
1595 .cmp(&b.shape.shape)
1596 .then(a.shape.file.cmp(&b.shape.file))
1597 .then(a.shape.line.cmp(&b.shape.line))
1598 .then(a.shape.component.cmp(&b.shape.component))
1599 });
1600 }
1601
1602 fn sort_dependency_findings(&mut self) {
1603 self.unlisted_dependencies
1604 .sort_by(|a, b| a.dep.package_name.cmp(&b.dep.package_name));
1605 for dep in &mut self.unlisted_dependencies {
1606 dep.dep
1607 .imported_from
1608 .sort_by(|a, b| a.path.cmp(&b.path).then(a.line.cmp(&b.line)));
1609 }
1610
1611 self.duplicate_exports
1612 .sort_by(|a, b| a.export.export_name.cmp(&b.export.export_name));
1613 for dup in &mut self.duplicate_exports {
1614 dup.export
1615 .locations
1616 .sort_by(|a, b| a.path.cmp(&b.path).then(a.line.cmp(&b.line)));
1617 }
1618
1619 self.type_only_dependencies.sort_by(|a, b| {
1620 a.dep
1621 .path
1622 .cmp(&b.dep.path)
1623 .then(a.dep.line.cmp(&b.dep.line))
1624 .then(a.dep.package_name.cmp(&b.dep.package_name))
1625 });
1626
1627 self.test_only_dependencies.sort_by(|a, b| {
1628 a.dep
1629 .path
1630 .cmp(&b.dep.path)
1631 .then(a.dep.line.cmp(&b.dep.line))
1632 .then(a.dep.package_name.cmp(&b.dep.package_name))
1633 });
1634
1635 self.dev_dependencies_in_production.sort_by(|a, b| {
1636 a.dep
1637 .path
1638 .cmp(&b.dep.path)
1639 .then(a.dep.line.cmp(&b.dep.line))
1640 .then(a.dep.package_name.cmp(&b.dep.package_name))
1641 });
1642 }
1643
1644 fn sort_graph_findings(&mut self) {
1645 self.circular_dependencies.sort_by(|a, b| {
1646 a.cycle
1647 .files
1648 .cmp(&b.cycle.files)
1649 .then(a.cycle.length.cmp(&b.cycle.length))
1650 });
1651
1652 self.re_export_cycles
1653 .sort_by(|a, b| a.cycle.files.cmp(&b.cycle.files));
1654
1655 self.boundary_violations.sort_by(|a, b| {
1656 a.violation
1657 .from_path
1658 .cmp(&b.violation.from_path)
1659 .then(a.violation.line.cmp(&b.violation.line))
1660 .then(a.violation.col.cmp(&b.violation.col))
1661 .then(a.violation.to_path.cmp(&b.violation.to_path))
1662 });
1663
1664 self.boundary_coverage_violations.sort_by(|a, b| {
1665 a.violation
1666 .path
1667 .cmp(&b.violation.path)
1668 .then(a.violation.line.cmp(&b.violation.line))
1669 .then(a.violation.col.cmp(&b.violation.col))
1670 });
1671
1672 self.boundary_call_violations.sort_by(|a, b| {
1673 a.violation
1674 .path
1675 .cmp(&b.violation.path)
1676 .then(a.violation.line.cmp(&b.violation.line))
1677 .then(a.violation.col.cmp(&b.violation.col))
1678 .then(a.violation.callee.cmp(&b.violation.callee))
1679 });
1680
1681 self.policy_violations.sort_by(|a, b| {
1682 a.violation
1683 .path
1684 .cmp(&b.violation.path)
1685 .then(a.violation.line.cmp(&b.violation.line))
1686 .then(a.violation.col.cmp(&b.violation.col))
1687 .then(a.violation.rule_id.cmp(&b.violation.rule_id))
1688 });
1689 }
1690
1691 fn sort_catalog_findings(&mut self) {
1692 self.sort_stale_suppressions();
1693 self.sort_unused_catalog_entries();
1694 self.sort_empty_catalog_groups();
1695 self.sort_unresolved_catalog_references();
1696 self.sort_unused_dependency_overrides();
1697 }
1698
1699 fn sort_stale_suppressions(&mut self) {
1700 self.stale_suppressions.sort_by(|a, b| {
1701 a.path
1702 .cmp(&b.path)
1703 .then(a.line.cmp(&b.line))
1704 .then(a.col.cmp(&b.col))
1705 });
1706 }
1707
1708 fn sort_unused_catalog_entries(&mut self) {
1709 self.unused_catalog_entries.sort_by(|a, b| {
1710 a.entry
1711 .path
1712 .cmp(&b.entry.path)
1713 .then_with(|| {
1714 catalog_sort_key(&a.entry.catalog_name)
1715 .cmp(&catalog_sort_key(&b.entry.catalog_name))
1716 })
1717 .then(a.entry.catalog_name.cmp(&b.entry.catalog_name))
1718 .then(a.entry.entry_name.cmp(&b.entry.entry_name))
1719 });
1720 for finding in &mut self.unused_catalog_entries {
1721 finding.entry.hardcoded_consumers.sort();
1722 finding.entry.hardcoded_consumers.dedup();
1723 }
1724 }
1725
1726 fn sort_empty_catalog_groups(&mut self) {
1727 self.empty_catalog_groups.sort_by(|a, b| {
1728 a.group
1729 .path
1730 .cmp(&b.group.path)
1731 .then_with(|| {
1732 catalog_sort_key(&a.group.catalog_name)
1733 .cmp(&catalog_sort_key(&b.group.catalog_name))
1734 })
1735 .then(a.group.catalog_name.cmp(&b.group.catalog_name))
1736 .then(a.group.line.cmp(&b.group.line))
1737 });
1738 }
1739
1740 fn sort_unresolved_catalog_references(&mut self) {
1741 self.unresolved_catalog_references.sort_by(|a, b| {
1742 a.reference
1743 .path
1744 .cmp(&b.reference.path)
1745 .then(a.reference.line.cmp(&b.reference.line))
1746 .then_with(|| {
1747 catalog_sort_key(&a.reference.catalog_name)
1748 .cmp(&catalog_sort_key(&b.reference.catalog_name))
1749 })
1750 .then(a.reference.catalog_name.cmp(&b.reference.catalog_name))
1751 .then(a.reference.entry_name.cmp(&b.reference.entry_name))
1752 });
1753 for finding in &mut self.unresolved_catalog_references {
1754 finding.reference.available_in_catalogs.sort();
1755 finding.reference.available_in_catalogs.dedup();
1756 }
1757 }
1758
1759 fn sort_unused_dependency_overrides(&mut self) {
1760 self.unused_dependency_overrides.sort_by(|a, b| {
1761 a.entry
1762 .path
1763 .cmp(&b.entry.path)
1764 .then(a.entry.line.cmp(&b.entry.line))
1765 .then(a.entry.raw_key.cmp(&b.entry.raw_key))
1766 });
1767 }
1768
1769 fn sort_metadata_findings(&mut self) {
1770 self.sort_prop_drilling_chains();
1771 self.sort_thin_wrappers();
1772 self.sort_duplicate_prop_shapes();
1773
1774 self.misconfigured_dependency_overrides.sort_by(|a, b| {
1775 a.entry
1776 .path
1777 .cmp(&b.entry.path)
1778 .then(a.entry.line.cmp(&b.entry.line))
1779 .then(a.entry.raw_key.cmp(&b.entry.raw_key))
1780 });
1781
1782 self.feature_flags.sort_by(|a, b| {
1783 a.path
1784 .cmp(&b.path)
1785 .then(a.line.cmp(&b.line))
1786 .then(a.flag_name.cmp(&b.flag_name))
1787 });
1788
1789 self.security_unresolved_callee_diagnostics.sort_by(|a, b| {
1790 a.path
1791 .cmp(&b.path)
1792 .then(a.line.cmp(&b.line))
1793 .then(a.col.cmp(&b.col))
1794 .then(a.reason.cmp(&b.reason))
1795 .then(a.expression_kind.cmp(&b.expression_kind))
1796 });
1797 }
1798
1799 fn sort_export_usages(&mut self) {
1800 for usage in &mut self.export_usages {
1801 usage.reference_locations.sort_by(|a, b| {
1802 a.path
1803 .cmp(&b.path)
1804 .then(a.line.cmp(&b.line))
1805 .then(a.col.cmp(&b.col))
1806 });
1807 }
1808 self.export_usages.sort_by(|a, b| {
1809 a.path
1810 .cmp(&b.path)
1811 .then(a.line.cmp(&b.line))
1812 .then(a.export_name.cmp(&b.export_name))
1813 });
1814 }
1815}
1816
1817fn catalog_sort_key(name: &str) -> (u8, &str) {
1819 if name == "default" {
1820 (0, name)
1821 } else {
1822 (1, name)
1823 }
1824}
1825
1826#[derive(Debug, Clone, Serialize, Deserialize)]
1828#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1829pub struct UnusedFile {
1830 #[serde(serialize_with = "serde_path::serialize")]
1832 pub path: PathBuf,
1833}
1834
1835#[derive(Debug, Clone, Serialize, Deserialize)]
1837#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1838pub struct UnusedExport {
1839 #[serde(serialize_with = "serde_path::serialize")]
1841 pub path: PathBuf,
1842 pub export_name: String,
1844 pub is_type_only: bool,
1846 pub line: u32,
1848 pub col: u32,
1850 pub span_start: u32,
1852 pub is_re_export: bool,
1854}
1855
1856#[derive(Debug, Clone, Serialize, Deserialize)]
1858#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1859pub struct PrivateTypeLeak {
1860 #[serde(serialize_with = "serde_path::serialize")]
1862 pub path: PathBuf,
1863 pub export_name: String,
1865 pub type_name: String,
1867 pub line: u32,
1869 pub col: u32,
1871 pub span_start: u32,
1873 #[serde(default, skip_serializing_if = "Option::is_none")]
1876 pub semantic: Option<crate::semantic::SemanticPrivateTypeLeak>,
1877}
1878
1879#[derive(Debug, Clone, Serialize, Deserialize)]
1883#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1884pub struct InvalidClientExport {
1885 #[serde(serialize_with = "serde_path::serialize")]
1887 pub path: PathBuf,
1888 pub export_name: String,
1891 pub directive: String,
1894 pub line: u32,
1896 pub col: u32,
1898}
1899
1900#[derive(Debug, Clone, Serialize, Deserialize)]
1905#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1906pub struct MixedClientServerBarrel {
1907 #[serde(serialize_with = "serde_path::serialize")]
1909 pub path: PathBuf,
1910 pub client_origin: String,
1913 pub server_origin: String,
1916 pub line: u32,
1918 pub col: u32,
1920}
1921
1922#[derive(Debug, Clone, Serialize, Deserialize)]
1929#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1930pub struct MisplacedDirective {
1931 #[serde(serialize_with = "serde_path::serialize")]
1933 pub path: PathBuf,
1934 pub directive: String,
1937 pub line: u32,
1939 pub col: u32,
1941}
1942
1943#[derive(Debug, Clone, Serialize, Deserialize)]
1949#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1950pub struct UnprovidedInject {
1951 #[serde(serialize_with = "serde_path::serialize")]
1953 pub path: PathBuf,
1954 pub key_name: String,
1956 pub framework: String,
1958 pub line: u32,
1960 pub col: u32,
1962}
1963
1964#[derive(Debug, Clone, Serialize, Deserialize)]
1973#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1974pub struct UnusedServerAction {
1975 #[serde(serialize_with = "serde_path::serialize")]
1977 pub path: PathBuf,
1978 pub action_name: String,
1980 pub line: u32,
1982 pub col: u32,
1984}
1985
1986#[derive(Debug, Clone, Serialize, Deserialize)]
1993#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1994pub struct UnusedLoadDataKey {
1995 #[serde(serialize_with = "serde_path::serialize")]
1997 pub path: PathBuf,
1998 pub key_name: String,
2000 pub line: u32,
2002 pub col: u32,
2004 #[serde(default, skip_serializing_if = "Option::is_none")]
2008 pub route_dir: Option<String>,
2009}
2010
2011#[derive(Debug, Clone, Serialize, Deserialize)]
2018#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2019pub struct UnrenderedComponent {
2020 #[serde(serialize_with = "serde_path::serialize")]
2022 pub path: PathBuf,
2023 pub component_name: String,
2028 pub framework: String,
2031 #[serde(
2035 serialize_with = "serde_path::serialize_option",
2036 skip_serializing_if = "Option::is_none"
2037 )]
2038 pub reachable_via: Option<PathBuf>,
2039 pub line: u32,
2042 pub col: u32,
2044}
2045
2046#[derive(Debug, Clone, Serialize, Deserialize)]
2051#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2052pub struct UnusedComponentProp {
2053 #[serde(serialize_with = "serde_path::serialize")]
2055 pub path: PathBuf,
2056 pub component_name: String,
2058 pub prop_name: String,
2060 pub line: u32,
2062 pub col: u32,
2064}
2065
2066#[derive(Debug, Clone, Serialize, Deserialize)]
2071#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2072pub struct UnusedComponentEmit {
2073 #[serde(serialize_with = "serde_path::serialize")]
2075 pub path: PathBuf,
2076 pub component_name: String,
2078 pub emit_name: String,
2080 pub line: u32,
2082 pub col: u32,
2084}
2085
2086#[derive(Debug, Clone, Serialize, Deserialize)]
2093#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2094pub struct UnusedSvelteEvent {
2095 #[serde(serialize_with = "serde_path::serialize")]
2097 pub path: PathBuf,
2098 pub component_name: String,
2100 pub event_name: String,
2102 pub line: u32,
2104 pub col: u32,
2106}
2107
2108#[derive(Debug, Clone, Serialize, Deserialize)]
2112#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2113pub struct PropDrillHop {
2114 #[serde(serialize_with = "serde_path::serialize")]
2116 pub file: PathBuf,
2117 pub line: u32,
2120 pub component: String,
2122}
2123
2124#[derive(Debug, Clone, Serialize, Deserialize)]
2135#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2136pub struct PropDrillingChain {
2137 pub prop: String,
2139 pub depth: u32,
2142 pub hops: Vec<PropDrillHop>,
2146}
2147
2148#[derive(Debug, Clone, Serialize, Deserialize)]
2157#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2158pub struct ThinWrapper {
2159 #[serde(serialize_with = "serde_path::serialize")]
2161 pub file: PathBuf,
2162 pub line: u32,
2165 pub component: String,
2167 pub child_component: String,
2170}
2171
2172#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2177#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2178pub struct DuplicatePropShapeMember {
2179 #[serde(serialize_with = "serde_path::serialize")]
2181 pub file: PathBuf,
2182 pub line: u32,
2184 pub component: String,
2186}
2187
2188#[derive(Debug, Clone, Serialize, Deserialize)]
2201#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2202pub struct DuplicatePropShape {
2203 #[serde(serialize_with = "serde_path::serialize")]
2205 pub file: PathBuf,
2206 pub line: u32,
2209 pub component: String,
2211 pub shape: Vec<String>,
2214 pub group_size: u32,
2217 pub sharing_components: Vec<DuplicatePropShapeMember>,
2221}
2222
2223#[derive(Debug, Clone, Serialize, Deserialize)]
2229#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2230pub struct UnusedComponentInput {
2231 #[serde(serialize_with = "serde_path::serialize")]
2233 pub path: PathBuf,
2234 pub component_name: String,
2236 pub input_name: String,
2238 pub line: u32,
2240 pub col: u32,
2242}
2243
2244#[derive(Debug, Clone, Serialize, Deserialize)]
2251#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2252pub struct UnusedComponentOutput {
2253 #[serde(serialize_with = "serde_path::serialize")]
2255 pub path: PathBuf,
2256 pub component_name: String,
2258 pub output_name: String,
2260 pub line: u32,
2262 pub col: u32,
2264}
2265
2266#[derive(Debug, Clone, Serialize, Deserialize)]
2272#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2273pub struct RouteCollision {
2274 #[serde(serialize_with = "serde_path::serialize")]
2276 pub path: PathBuf,
2277 pub url: String,
2281 #[serde(serialize_with = "serde_path::serialize_vec")]
2284 pub conflicting_paths: Vec<PathBuf>,
2285 pub line: u32,
2287 pub col: u32,
2289}
2290
2291#[derive(Debug, Clone, Serialize, Deserialize)]
2299#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2300pub struct DynamicSegmentNameConflict {
2301 #[serde(serialize_with = "serde_path::serialize")]
2303 pub path: PathBuf,
2304 pub position: String,
2308 pub conflicting_segments: Vec<String>,
2311 #[serde(serialize_with = "serde_path::serialize_vec")]
2314 pub conflicting_paths: Vec<PathBuf>,
2315 pub line: u32,
2317 pub col: u32,
2319}
2320
2321#[derive(Debug, Clone, Serialize, Deserialize)]
2323#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2324pub struct UnusedDependency {
2325 pub package_name: String,
2327 pub location: DependencyLocation,
2329 #[serde(serialize_with = "serde_path::serialize")]
2332 pub path: PathBuf,
2333 pub line: u32,
2335 #[serde(
2337 default,
2338 serialize_with = "serde_path::serialize_vec",
2339 skip_serializing_if = "Vec::is_empty"
2340 )]
2341 #[cfg_attr(feature = "schema", schemars(default))]
2342 pub used_in_workspaces: Vec<PathBuf>,
2343}
2344
2345#[derive(Debug, Clone, Serialize, Deserialize)]
2362#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2363#[serde(rename_all = "camelCase")]
2364pub enum DependencyLocation {
2365 Dependencies,
2367 DevDependencies,
2369 OptionalDependencies,
2371}
2372
2373#[derive(Debug, Clone, Serialize, Deserialize)]
2375#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2376pub struct UnusedMember {
2377 #[serde(serialize_with = "serde_path::serialize")]
2379 pub path: PathBuf,
2380 pub parent_name: String,
2382 pub member_name: String,
2384 pub kind: MemberKind,
2386 pub line: u32,
2388 pub col: u32,
2390}
2391
2392#[derive(Debug, Clone, Serialize, Deserialize)]
2394#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2395pub struct UnresolvedImport {
2396 #[serde(serialize_with = "serde_path::serialize")]
2398 pub path: PathBuf,
2399 pub specifier: String,
2401 pub line: u32,
2403 pub col: u32,
2405 pub specifier_col: u32,
2408}
2409
2410#[derive(Debug, Clone, Serialize, Deserialize)]
2412#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2413pub struct UnlistedDependency {
2414 pub package_name: String,
2417 pub imported_from: Vec<ImportSite>,
2419}
2420
2421#[derive(Debug, Clone, Serialize, Deserialize)]
2423#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2424pub struct ImportSite {
2425 #[serde(serialize_with = "serde_path::serialize")]
2427 pub path: PathBuf,
2428 pub line: u32,
2430 pub col: u32,
2432}
2433
2434#[derive(Debug, Clone, Serialize, Deserialize)]
2436#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2437pub struct DuplicateExport {
2438 pub export_name: String,
2440 pub locations: Vec<DuplicateLocation>,
2442}
2443
2444#[derive(Debug, Clone, Serialize, Deserialize)]
2446#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2447pub struct DuplicateLocation {
2448 #[serde(serialize_with = "serde_path::serialize")]
2450 pub path: PathBuf,
2451 pub line: u32,
2453 pub col: u32,
2455}
2456
2457#[derive(Debug, Clone, Serialize, Deserialize)]
2461#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2462pub struct TypeOnlyDependency {
2463 pub package_name: String,
2465 #[serde(serialize_with = "serde_path::serialize")]
2467 pub path: PathBuf,
2468 pub line: u32,
2470}
2471
2472#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2475#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2476#[serde(rename_all = "kebab-case")]
2477pub enum SecurityFindingKind {
2478 ClientServerLeak,
2481 TaintedSink,
2485}
2486
2487#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2489#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2490#[serde(rename_all = "kebab-case")]
2491pub enum TraceHopRole {
2492 ClientBoundary,
2494 UntrustedSource,
2498 ModuleSource,
2505 Intermediate,
2507 SecretSource,
2509 Sink,
2513}
2514
2515#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2519#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2520pub struct TraceHop {
2521 #[serde(serialize_with = "serde_path::serialize")]
2523 pub path: PathBuf,
2524 pub line: u32,
2528 pub col: u32,
2530 pub role: TraceHopRole,
2532}
2533
2534#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2541#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2542#[serde(rename_all = "kebab-case")]
2543pub enum TaintConfidence {
2544 ArgLevel,
2548 ModuleLevel,
2552}
2553
2554#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2563#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2564pub struct SecurityReachability {
2565 pub reachable_from_entry: bool,
2570 #[serde(default)]
2574 pub reachable_from_untrusted_source: bool,
2575 #[serde(default, skip_serializing_if = "Option::is_none")]
2582 pub taint_confidence: Option<TaintConfidence>,
2583 #[serde(default, skip_serializing_if = "Option::is_none")]
2586 pub untrusted_source_hop_count: Option<u32>,
2587 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2591 pub untrusted_source_trace: Vec<TraceHop>,
2592 pub blast_radius: u32,
2596 pub crosses_boundary: bool,
2601}
2602
2603#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2606#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2607pub struct SecurityDeadCodeContext {
2608 pub kind: SecurityDeadCodeKind,
2610 #[serde(default, skip_serializing_if = "Option::is_none")]
2612 pub export_name: Option<String>,
2613 #[serde(default, skip_serializing_if = "Option::is_none")]
2615 pub line: Option<u32>,
2616 pub guidance: String,
2618}
2619
2620#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2622#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2623#[serde(rename_all = "kebab-case")]
2624pub enum SecurityDeadCodeKind {
2625 UnusedFile,
2627 UnusedExport,
2629}
2630
2631#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2634#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2635pub struct SecurityUnresolvedCalleeDiagnostic {
2636 #[serde(serialize_with = "serde_path::serialize")]
2638 pub path: PathBuf,
2639 pub line: u32,
2641 pub col: u32,
2643 pub reason: SkippedSecurityCalleeReason,
2645 pub expression_kind: SkippedSecurityCalleeExpressionKind,
2647}
2648
2649#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
2655#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2656pub struct SecurityCandidateSink {
2657 #[serde(serialize_with = "serde_path::serialize")]
2660 pub path: PathBuf,
2661 pub line: u32,
2663 pub col: u32,
2665 #[serde(default, skip_serializing_if = "Option::is_none")]
2670 pub category: Option<String>,
2671 #[serde(default, skip_serializing_if = "Option::is_none")]
2674 pub cwe: Option<u32>,
2675 #[serde(default, skip_serializing_if = "Option::is_none")]
2679 pub callee: Option<String>,
2680 #[serde(default, skip_serializing_if = "Option::is_none")]
2684 pub url_shape: Option<SecurityUrlShape>,
2685}
2686
2687#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2690#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2691pub struct SecurityZoneCrossing {
2692 pub from: String,
2694 pub to: String,
2696}
2697
2698#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
2710#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2711pub struct SecurityCandidateBoundary {
2712 pub client_server: bool,
2716 pub cross_module: bool,
2719 #[serde(default, skip_serializing_if = "Option::is_none")]
2723 pub architecture_zone: Option<SecurityZoneCrossing>,
2724}
2725
2726#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2732#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2733pub struct SecurityNetworkContext {
2734 #[serde(default, skip_serializing_if = "Option::is_none")]
2739 pub destination: Option<String>,
2740}
2741
2742#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
2750#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2751pub struct SecurityCandidate {
2752 #[serde(default, skip_serializing_if = "Option::is_none")]
2760 pub source_kind: Option<String>,
2761 pub sink: SecurityCandidateSink,
2764 pub boundary: SecurityCandidateBoundary,
2766 #[serde(default, skip_serializing_if = "Option::is_none")]
2770 pub network: Option<SecurityNetworkContext>,
2771}
2772
2773#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2775#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2776pub struct TaintEndpoint {
2777 #[serde(serialize_with = "serde_path::serialize")]
2779 pub path: PathBuf,
2780 pub line: u32,
2782 pub col: u32,
2784}
2785
2786#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2791#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2792pub struct TaintPath {
2793 pub intra_module: bool,
2796 pub cross_module_hops: u32,
2799}
2800
2801#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2806#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2807pub struct SecurityTaintFlow {
2808 pub source: TaintEndpoint,
2810 pub sink: TaintEndpoint,
2812 pub path: TaintPath,
2815}
2816
2817#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2820#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2821#[serde(rename_all = "kebab-case")]
2822pub enum SecurityRuntimeState {
2823 RuntimeHot,
2825 RuntimeCold,
2827 NeverExecuted,
2830 LowTraffic,
2833 CoverageUnavailable,
2835 RuntimeUnknown,
2838}
2839
2840#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2843#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2844pub struct SecurityRuntimeContext {
2845 pub state: SecurityRuntimeState,
2847 pub function: String,
2849 pub line: u32,
2851 #[serde(default, skip_serializing_if = "Option::is_none")]
2853 pub invocations: Option<u64>,
2854 #[serde(default, skip_serializing_if = "Option::is_none")]
2856 pub stable_id: Option<String>,
2857 #[serde(default, skip_serializing_if = "Option::is_none")]
2859 pub evidence: Option<String>,
2860}
2861
2862#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
2865#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2866#[serde(rename_all = "lowercase")]
2867pub enum SecuritySeverity {
2868 High,
2870 Medium,
2872 Low,
2874}
2875
2876#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2878#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2879pub struct SecurityDefensiveControl {
2880 pub kind: SecurityControlKind,
2882 #[serde(serialize_with = "serde_path::serialize")]
2884 pub path: PathBuf,
2885 pub line: u32,
2887 pub col: u32,
2889 pub callee: String,
2891}
2892
2893#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2895#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2896pub struct SecurityDefensiveBoundary {
2897 pub controls: Vec<SecurityDefensiveControl>,
2899 pub verification_prompt: String,
2902}
2903
2904#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2906#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2907pub struct SecurityAttackSurfaceEntry {
2908 pub source: TaintEndpoint,
2910 pub sink: SecurityCandidateSink,
2912 pub path: Vec<TraceHop>,
2915 pub defensive_boundary: SecurityDefensiveBoundary,
2917}
2918
2919#[derive(Debug, Clone, Serialize, Deserialize)]
2925#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2926pub struct SecurityFinding {
2927 pub finding_id: String,
2933 pub kind: SecurityFindingKind,
2935 #[serde(default, skip_serializing_if = "Option::is_none")]
2940 pub category: Option<String>,
2941 #[serde(default, skip_serializing_if = "Option::is_none")]
2944 pub cwe: Option<u32>,
2945 #[serde(serialize_with = "serde_path::serialize")]
2948 pub path: PathBuf,
2949 pub line: u32,
2951 pub col: u32,
2953 pub evidence: String,
2955 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
2964 pub source_backed: bool,
2965 #[serde(skip)]
2974 pub source_read: Option<(u32, u32)>,
2975 pub severity: SecuritySeverity,
2979 pub trace: Vec<TraceHop>,
2983 pub actions: Vec<IssueAction>,
2988 #[serde(default, skip_serializing_if = "Option::is_none")]
2992 pub dead_code: Option<SecurityDeadCodeContext>,
2993 #[serde(default, skip_serializing_if = "Option::is_none")]
2999 pub reachability: Option<SecurityReachability>,
3000 pub candidate: SecurityCandidate,
3005 #[serde(default, skip_serializing_if = "Option::is_none")]
3008 pub taint_flow: Option<SecurityTaintFlow>,
3009 #[serde(default, skip_serializing_if = "Option::is_none")]
3013 pub runtime: Option<SecurityRuntimeContext>,
3014 #[serde(default, skip_serializing_if = "Option::is_none")]
3018 pub attack_surface: Option<SecurityAttackSurfaceEntry>,
3019}
3020
3021#[derive(Debug, Clone, Serialize, Deserialize)]
3029#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3030pub struct UnusedCatalogEntry {
3031 pub entry_name: String,
3033 pub catalog_name: String,
3036 #[serde(serialize_with = "serde_path::serialize")]
3038 pub path: PathBuf,
3039 pub line: u32,
3041 #[serde(
3046 default,
3047 serialize_with = "serde_path::serialize_vec",
3048 skip_serializing_if = "Vec::is_empty"
3049 )]
3050 pub hardcoded_consumers: Vec<PathBuf>,
3051}
3052
3053#[derive(Debug, Clone, Serialize, Deserialize)]
3055#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3056pub struct EmptyCatalogGroup {
3057 pub catalog_name: String,
3059 #[serde(serialize_with = "serde_path::serialize")]
3061 pub path: PathBuf,
3062 pub line: u32,
3064}
3065
3066#[derive(Debug, Clone, Serialize, Deserialize)]
3076#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3077pub struct UnresolvedCatalogReference {
3078 pub entry_name: String,
3080 pub catalog_name: String,
3083 #[serde(serialize_with = "serde_path::serialize")]
3090 pub path: PathBuf,
3091 pub line: u32,
3093 #[serde(default, skip_serializing_if = "Vec::is_empty")]
3098 pub available_in_catalogs: Vec<String>,
3099}
3100
3101#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
3105#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3106pub enum DependencyOverrideSource {
3107 #[serde(rename = "pnpm-workspace.yaml")]
3109 PnpmWorkspaceYaml,
3110 #[serde(rename = "package.json")]
3113 PnpmPackageJson,
3114}
3115
3116impl DependencyOverrideSource {
3117 #[must_use]
3120 pub const fn as_label(&self) -> &'static str {
3121 match self {
3122 Self::PnpmWorkspaceYaml => "pnpm-workspace.yaml",
3123 Self::PnpmPackageJson => "package.json",
3124 }
3125 }
3126}
3127
3128impl std::fmt::Display for DependencyOverrideSource {
3129 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3130 f.write_str(self.as_label())
3131 }
3132}
3133
3134#[derive(Debug, Clone, Serialize, Deserialize)]
3141#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3142pub struct UnusedDependencyOverride {
3143 pub raw_key: String,
3147 pub target_package: String,
3150 #[serde(default, skip_serializing_if = "Option::is_none")]
3152 pub parent_package: Option<String>,
3153 #[serde(default, skip_serializing_if = "Option::is_none")]
3156 pub version_constraint: Option<String>,
3157 pub version_range: String,
3159 pub source: DependencyOverrideSource,
3162 #[serde(serialize_with = "serde_path::serialize")]
3169 pub path: PathBuf,
3170 pub line: u32,
3172 #[serde(default, skip_serializing_if = "Option::is_none")]
3177 pub hint: Option<String>,
3178}
3179
3180#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
3184#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3185#[serde(rename_all = "kebab-case")]
3186pub enum DependencyOverrideMisconfigReason {
3187 UnparsableKey,
3190 EmptyValue,
3192}
3193
3194impl DependencyOverrideMisconfigReason {
3195 #[must_use]
3197 pub const fn describe(self) -> &'static str {
3198 match self {
3199 Self::UnparsableKey => "override key cannot be parsed",
3200 Self::EmptyValue => "override value is missing or empty",
3201 }
3202 }
3203}
3204
3205#[derive(Debug, Clone, Serialize, Deserialize)]
3209#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3210pub struct MisconfiguredDependencyOverride {
3211 pub raw_key: String,
3213 #[serde(default, skip_serializing_if = "Option::is_none")]
3221 pub target_package: Option<String>,
3222 pub raw_value: String,
3225 pub reason: DependencyOverrideMisconfigReason,
3229 pub source: DependencyOverrideSource,
3231 #[serde(serialize_with = "serde_path::serialize")]
3235 pub path: PathBuf,
3236 pub line: u32,
3238}
3239
3240#[derive(Debug, Clone, Serialize, Deserialize)]
3243#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3244pub struct TestOnlyDependency {
3245 pub package_name: String,
3248 #[serde(serialize_with = "serde_path::serialize")]
3250 pub path: PathBuf,
3251 pub line: u32,
3253}
3254
3255#[derive(Debug, Clone, Serialize, Deserialize)]
3261#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3262pub struct DevDependencyInProduction {
3263 pub package_name: String,
3266 #[serde(serialize_with = "serde_path::serialize")]
3268 pub path: PathBuf,
3269 pub line: u32,
3271}
3272
3273#[derive(Debug, Clone, Serialize, Deserialize)]
3286#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3287pub struct CircularDependencyEdge {
3288 #[serde(serialize_with = "serde_path::serialize")]
3290 pub path: PathBuf,
3291 pub line: u32,
3293 pub col: u32,
3295}
3296
3297#[derive(Debug, Clone, Serialize, Deserialize)]
3315#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3316#[cfg_attr(feature = "schema", schemars(extend("required" = ["files", "length", "line", "col"])))]
3317pub struct CircularDependency {
3318 #[serde(serialize_with = "serde_path::serialize_vec")]
3320 pub files: Vec<PathBuf>,
3321 pub length: usize,
3323 #[serde(default)]
3325 pub line: u32,
3326 #[serde(default)]
3328 pub col: u32,
3329 #[serde(default)]
3336 pub edges: Vec<CircularDependencyEdge>,
3337 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
3339 pub is_cross_package: bool,
3340}
3341
3342#[derive(Debug, Clone, Serialize, Deserialize)]
3352#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3353pub struct ReExportCycle {
3354 #[serde(serialize_with = "serde_path::serialize_vec")]
3357 pub files: Vec<PathBuf>,
3358 pub kind: ReExportCycleKind,
3360}
3361
3362#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3364#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3365#[serde(rename_all = "kebab-case")]
3366pub enum ReExportCycleKind {
3367 MultiNode,
3370 SelfLoop,
3372}
3373
3374#[derive(Debug, Clone, Serialize, Deserialize)]
3376#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3377pub struct BoundaryViolation {
3378 #[serde(serialize_with = "serde_path::serialize")]
3380 pub from_path: PathBuf,
3381 #[serde(serialize_with = "serde_path::serialize")]
3383 pub to_path: PathBuf,
3384 pub from_zone: String,
3386 pub to_zone: String,
3388 pub import_specifier: String,
3390 pub line: u32,
3392 pub col: u32,
3394}
3395
3396#[derive(Debug, Clone, Serialize, Deserialize)]
3398#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3399pub struct BoundaryCoverageViolation {
3400 #[serde(serialize_with = "serde_path::serialize")]
3402 pub path: PathBuf,
3403 pub line: u32,
3405 pub col: u32,
3407}
3408
3409#[derive(Debug, Clone, Serialize, Deserialize)]
3413#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3414pub struct BoundaryCallViolation {
3415 #[serde(serialize_with = "serde_path::serialize")]
3417 pub path: PathBuf,
3418 pub line: u32,
3420 pub col: u32,
3422 pub zone: String,
3424 pub callee: String,
3426 pub pattern: String,
3429}
3430
3431#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3433#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3434#[serde(rename_all = "kebab-case")]
3435pub enum PolicyRuleKind {
3436 BannedCall,
3438 BannedImport,
3440 BannedEffect,
3442 BannedExport,
3444}
3445
3446#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3451#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3452#[serde(rename_all = "lowercase")]
3453pub enum PolicyViolationSeverity {
3454 Error,
3456 Warn,
3458}
3459
3460#[derive(Debug, Clone, Serialize, Deserialize)]
3467#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3468pub struct PolicyViolation {
3469 #[serde(serialize_with = "serde_path::serialize")]
3471 pub path: PathBuf,
3472 pub line: u32,
3474 pub col: u32,
3476 pub pack: String,
3478 pub rule_id: String,
3481 pub kind: PolicyRuleKind,
3483 pub matched: String,
3488 pub severity: PolicyViolationSeverity,
3491 #[serde(default, skip_serializing_if = "Option::is_none")]
3493 pub message: Option<String>,
3494}
3495
3496#[derive(Debug, Clone, Serialize, Deserialize)]
3498#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3499#[serde(rename_all = "snake_case", tag = "type")]
3500pub enum SuppressionOrigin {
3501 Comment {
3503 #[serde(default, skip_serializing_if = "Option::is_none")]
3505 issue_kind: Option<String>,
3506 #[serde(default, skip_serializing_if = "Option::is_none")]
3508 reason: Option<String>,
3509 is_file_level: bool,
3511 #[serde(default = "default_true", skip_serializing_if = "is_true")]
3518 kind_known: bool,
3519 },
3520 JsdocTag {
3522 export_name: String,
3524 #[serde(default, skip_serializing_if = "Option::is_none")]
3526 reason: Option<String>,
3527 },
3528}
3529
3530#[expect(
3531 clippy::trivially_copy_pass_by_ref,
3532 reason = "serde skip_serializing_if takes a reference by contract"
3533)]
3534const fn is_true(b: &bool) -> bool {
3535 *b
3536}
3537
3538const fn default_true() -> bool {
3547 true
3548}
3549
3550#[derive(Debug, Clone, Serialize, Deserialize)]
3552#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3553pub struct StaleSuppression {
3554 #[serde(serialize_with = "serde_path::serialize")]
3556 pub path: PathBuf,
3557 pub line: u32,
3559 pub col: u32,
3561 pub origin: SuppressionOrigin,
3563 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
3566 pub missing_reason: bool,
3567 pub actions: Vec<IssueAction>,
3569}
3570
3571impl StaleSuppression {
3572 #[must_use]
3574 pub fn actions_for(missing_reason: bool) -> Vec<IssueAction> {
3575 let (kind, description) = if missing_reason {
3576 (
3577 FixActionType::AddSuppressionReason,
3578 "Add a human-authored reason after `--` on the suppression",
3579 )
3580 } else {
3581 (
3582 FixActionType::RemoveStaleSuppression,
3583 "Remove or update the stale suppression",
3584 )
3585 };
3586 let mut actions = vec![IssueAction::Fix(FixAction {
3587 kind,
3588 auto_fixable: false,
3589 description: description.to_string(),
3590 note: None,
3591 available_in_catalogs: None,
3592 suggested_target: None,
3593 })];
3594 if !missing_reason {
3595 actions.push(IssueAction::SuppressLine(SuppressLineAction {
3596 kind: SuppressLineKind::SuppressLine,
3597 auto_fixable: false,
3598 description:
3599 "Suppress this stale suppression finding with a comment above the suppression"
3600 .to_string(),
3601 comment: "// fallow-ignore-next-line stale-suppression".to_string(),
3602 scope: Some(SuppressLineScope::PerLocation),
3603 }));
3604 }
3605 actions
3606 }
3607
3608 #[must_use]
3610 pub fn description(&self) -> String {
3611 match &self.origin {
3612 SuppressionOrigin::Comment {
3613 issue_kind,
3614 reason,
3615 is_file_level,
3616 ..
3617 } => {
3618 let directive = if *is_file_level {
3619 "fallow-ignore-file"
3620 } else {
3621 "fallow-ignore-next-line"
3622 };
3623 match issue_kind {
3624 Some(kind) => match reason {
3625 Some(reason) => format!("// {directive} {kind} -- {reason}"),
3626 None => format!("// {directive} {kind}"),
3627 },
3628 None => match reason {
3629 Some(reason) => format!("// {directive} -- {reason}"),
3630 None => format!("// {directive}"),
3631 },
3632 }
3633 }
3634 SuppressionOrigin::JsdocTag {
3635 export_name,
3636 reason,
3637 } => match reason {
3638 Some(reason) => format!("@expected-unused on {export_name} -- {reason}"),
3639 None => format!("@expected-unused on {export_name}"),
3640 },
3641 }
3642 }
3643
3644 #[must_use]
3651 pub fn explanation(&self) -> String {
3652 match &self.origin {
3653 SuppressionOrigin::Comment {
3654 issue_kind,
3655 is_file_level,
3656 kind_known,
3657 ..
3658 } => {
3659 if self.missing_reason {
3660 return "suppression is missing a reason".to_string();
3661 }
3662 let scope = if *is_file_level {
3663 "in this file"
3664 } else {
3665 "on the next line"
3666 };
3667 match issue_kind {
3668 Some(kind) if !*kind_known => match closest_known_kind_name(kind) {
3669 Some(suggestion) => format!(
3670 "'{kind}' is not a recognized fallow issue kind. Did you mean '{suggestion}'? Other tokens on this line still apply."
3671 ),
3672 None => format!(
3673 "'{kind}' is not a recognized fallow issue kind. Other tokens on this line still apply."
3674 ),
3675 },
3676 Some(kind) => format!("no {kind} issue found {scope}"),
3677 None => format!("no issues found {scope}"),
3678 }
3679 }
3680 SuppressionOrigin::JsdocTag { export_name, .. } => {
3681 if self.missing_reason {
3682 return "suppression is missing a reason".to_string();
3683 }
3684 format!("{export_name} is now used")
3685 }
3686 }
3687 }
3688
3689 #[must_use]
3694 pub fn suppressed_kind(&self) -> Option<IssueKind> {
3695 match &self.origin {
3696 SuppressionOrigin::Comment {
3697 issue_kind,
3698 kind_known: true,
3699 ..
3700 } => issue_kind.as_deref().and_then(IssueKind::parse),
3701 SuppressionOrigin::Comment { .. } | SuppressionOrigin::JsdocTag { .. } => None,
3702 }
3703 }
3704
3705 #[must_use]
3712 pub fn display_message(&self) -> String {
3713 match &self.origin {
3714 SuppressionOrigin::Comment {
3715 kind_known: false, ..
3716 } => format!("{} ({})", self.description(), self.explanation()),
3717 SuppressionOrigin::Comment { .. } | SuppressionOrigin::JsdocTag { .. }
3718 if self.missing_reason =>
3719 {
3720 format!("{} ({})", self.description(), self.explanation())
3721 }
3722 SuppressionOrigin::Comment { .. } | SuppressionOrigin::JsdocTag { .. } => {
3723 self.description()
3724 }
3725 }
3726 }
3727}
3728
3729#[derive(Debug, Clone)]
3743pub struct ActiveSuppression {
3744 pub path: PathBuf,
3746 pub kind: Option<String>,
3749 pub is_file_level: bool,
3752 pub reason: Option<String>,
3754 pub comment_line: u32,
3756}
3757
3758#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3760#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3761#[serde(rename_all = "snake_case")]
3762pub enum FlagKind {
3763 EnvironmentVariable,
3765 SdkCall,
3767 ConfigObject,
3769}
3770
3771#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
3773#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3774#[serde(rename_all = "snake_case")]
3775pub enum FlagConfidence {
3776 Low,
3778 Medium,
3780 High,
3782}
3783
3784#[derive(Debug, Clone, Serialize, Deserialize)]
3786#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3787pub struct FeatureFlag {
3788 #[serde(serialize_with = "serde_path::serialize")]
3790 pub path: PathBuf,
3791 pub flag_name: String,
3793 pub kind: FlagKind,
3795 pub confidence: FlagConfidence,
3797 pub line: u32,
3799 pub col: u32,
3801 #[serde(skip)]
3803 pub guard_span_start: Option<u32>,
3804 #[serde(skip)]
3806 pub guard_span_end: Option<u32>,
3807 #[serde(default, skip_serializing_if = "Option::is_none")]
3809 pub sdk_name: Option<String>,
3810 #[serde(skip)]
3813 pub guard_line_start: Option<u32>,
3814 #[serde(skip)]
3816 pub guard_line_end: Option<u32>,
3817 #[serde(default, skip_serializing_if = "Vec::is_empty")]
3820 pub guarded_dead_exports: Vec<String>,
3821}
3822
3823const _: () = assert!(std::mem::size_of::<FeatureFlag>() <= 160);
3825
3826#[derive(Debug, Clone, Serialize, Deserialize)]
3829#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3830pub struct ExportUsage {
3831 #[serde(serialize_with = "serde_path::serialize")]
3833 pub path: PathBuf,
3834 pub export_name: String,
3836 pub line: u32,
3838 pub col: u32,
3840 pub reference_count: usize,
3842 pub reference_locations: Vec<ReferenceLocation>,
3845}
3846
3847#[derive(Debug, Clone, Serialize, Deserialize)]
3849#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3850pub struct ReferenceLocation {
3851 #[serde(serialize_with = "serde_path::serialize")]
3853 pub path: PathBuf,
3854 pub line: u32,
3856 pub col: u32,
3858}
3859
3860#[cfg(test)]
3861mod tests {
3862 use super::*;
3863 use crate::output_dead_code::{
3864 BoundaryViolationFinding, CircularDependencyFinding, UnresolvedImportFinding,
3865 UnusedClassMemberFinding, UnusedEnumMemberFinding, UnusedExportFinding, UnusedFileFinding,
3866 UnusedTypeFinding,
3867 };
3868
3869 #[test]
3870 fn empty_results_no_issues() {
3871 let results = AnalysisResults::default();
3872 assert_eq!(results.total_issues(), 0);
3873 assert!(!results.has_issues());
3874 }
3875
3876 #[test]
3877 fn results_with_unused_file() {
3878 let mut results = AnalysisResults::default();
3879 results
3880 .unused_files
3881 .push(UnusedFileFinding::with_actions(UnusedFile {
3882 path: PathBuf::from("test.ts"),
3883 }));
3884 assert_eq!(results.total_issues(), 1);
3885 assert!(results.has_issues());
3886 }
3887
3888 #[test]
3889 fn results_with_unused_export() {
3890 let mut results = AnalysisResults::default();
3891 results
3892 .unused_exports
3893 .push(UnusedExportFinding::with_actions(UnusedExport {
3894 path: PathBuf::from("test.ts"),
3895 export_name: "foo".to_string(),
3896 is_type_only: false,
3897 line: 1,
3898 col: 0,
3899 span_start: 0,
3900 is_re_export: false,
3901 }));
3902 assert_eq!(results.total_issues(), 1);
3903 assert!(results.has_issues());
3904 }
3905
3906 #[test]
3907 fn merge_into_appends_counts_and_preserves_existing_optional_metadata() {
3908 let framework_contract = crate::semantic::SemanticFrameworkContract {
3909 framework: "lit".to_string(),
3910 package: "lit".to_string(),
3911 heritage_symbol: "LitElement".to_string(),
3912 heritage_names: vec!["LitElement".to_string()],
3913 relation: crate::semantic::SemanticFrameworkRelation::Extends,
3914 members: vec!["render".to_string()],
3915 };
3916 let mut target = AnalysisResults {
3917 unused_files: vec![UnusedFileFinding::with_actions(UnusedFile {
3918 path: PathBuf::from("a.ts"),
3919 })],
3920 suppression_count: 2,
3921 security_unresolved_edge_files: 1,
3922 security_unresolved_callee_sites: 3,
3923 entry_point_summary: Some(EntryPointSummary {
3924 total: 1,
3925 by_source: vec![("existing".to_string(), 1)],
3926 }),
3927 semantic_framework_contracts: vec![framework_contract.clone()],
3928 ..AnalysisResults::default()
3929 };
3930 let source = AnalysisResults {
3931 unused_files: vec![UnusedFileFinding::with_actions(UnusedFile {
3932 path: PathBuf::from("b.ts"),
3933 })],
3934 suppression_count: 4,
3935 security_unresolved_edge_files: 5,
3936 security_unresolved_callee_sites: 6,
3937 unused_load_data_keys_global_abstain: true,
3938 entry_point_summary: Some(EntryPointSummary {
3939 total: 1,
3940 by_source: vec![("incoming".to_string(), 1)],
3941 }),
3942 render_fan_in: Some(RenderFanInMetric::default()),
3943 semantic_framework_contracts: vec![framework_contract],
3944 ..AnalysisResults::default()
3945 };
3946
3947 target.merge_into(source);
3948
3949 assert_eq!(target.unused_files.len(), 2);
3950 assert_eq!(target.suppression_count, 6);
3951 assert_eq!(target.security_unresolved_edge_files, 6);
3952 assert_eq!(target.security_unresolved_callee_sites, 9);
3953 assert!(target.unused_load_data_keys_global_abstain);
3954 assert_eq!(
3955 target
3956 .entry_point_summary
3957 .as_ref()
3958 .map(|summary| summary.total),
3959 Some(1)
3960 );
3961 assert_eq!(
3962 target
3963 .entry_point_summary
3964 .as_ref()
3965 .and_then(|summary| summary.by_source.first())
3966 .map(|(name, _)| name.as_str()),
3967 Some("existing")
3968 );
3969 assert!(target.render_fan_in.is_some());
3970 assert_eq!(target.semantic_framework_contracts.len(), 1);
3971 }
3972
3973 fn test_unused_export(path: &str, export_name: &str, is_type_only: bool) -> UnusedExport {
3974 UnusedExport {
3975 path: PathBuf::from(path),
3976 export_name: export_name.to_string(),
3977 is_type_only,
3978 line: 1,
3979 col: 0,
3980 span_start: 0,
3981 is_re_export: false,
3982 }
3983 }
3984
3985 fn test_unused_dependency(
3986 package_name: &str,
3987 location: DependencyLocation,
3988 ) -> UnusedDependency {
3989 UnusedDependency {
3990 package_name: package_name.to_string(),
3991 location,
3992 path: PathBuf::from("package.json"),
3993 line: 5,
3994 used_in_workspaces: Vec::new(),
3995 }
3996 }
3997
3998 fn test_unused_member(member_name: &str, kind: MemberKind) -> UnusedMember {
3999 UnusedMember {
4000 path: PathBuf::from("members.ts"),
4001 parent_name: "Parent".to_string(),
4002 member_name: member_name.to_string(),
4003 kind,
4004 line: 1,
4005 col: 0,
4006 }
4007 }
4008
4009 #[test]
4010 fn results_total_counts_all_types() {
4011 let results = AnalysisResults {
4012 unused_files: vec![UnusedFileFinding::with_actions(UnusedFile {
4013 path: PathBuf::from("a.ts"),
4014 })],
4015 unused_exports: vec![UnusedExportFinding::with_actions(test_unused_export(
4016 "b.ts", "x", false,
4017 ))],
4018 unused_types: vec![UnusedTypeFinding::with_actions(test_unused_export(
4019 "c.ts", "T", true,
4020 ))],
4021 unused_dependencies: vec![UnusedDependencyFinding::with_actions(
4022 test_unused_dependency("dep", DependencyLocation::Dependencies),
4023 )],
4024 unused_dev_dependencies: vec![UnusedDevDependencyFinding::with_actions(
4025 test_unused_dependency("dev", DependencyLocation::DevDependencies),
4026 )],
4027 unused_enum_members: vec![UnusedEnumMemberFinding::with_actions(test_unused_member(
4028 "A",
4029 MemberKind::EnumMember,
4030 ))],
4031 unused_class_members: vec![UnusedClassMemberFinding::with_actions(test_unused_member(
4032 "m",
4033 MemberKind::ClassMethod,
4034 ))],
4035 unresolved_imports: vec![UnresolvedImportFinding::with_actions(UnresolvedImport {
4036 path: PathBuf::from("f.ts"),
4037 specifier: "./missing".to_string(),
4038 line: 1,
4039 col: 0,
4040 specifier_col: 0,
4041 })],
4042 unlisted_dependencies: vec![UnlistedDependencyFinding::with_actions(
4043 UnlistedDependency {
4044 package_name: "unlisted".to_string(),
4045 imported_from: vec![ImportSite {
4046 path: PathBuf::from("g.ts"),
4047 line: 1,
4048 col: 0,
4049 }],
4050 },
4051 )],
4052 duplicate_exports: vec![DuplicateExportFinding::with_actions(DuplicateExport {
4053 export_name: "dup".to_string(),
4054 locations: vec![
4055 DuplicateLocation {
4056 path: PathBuf::from("h.ts"),
4057 line: 15,
4058 col: 0,
4059 },
4060 DuplicateLocation {
4061 path: PathBuf::from("i.ts"),
4062 line: 30,
4063 col: 0,
4064 },
4065 ],
4066 })],
4067 unused_optional_dependencies: vec![UnusedOptionalDependencyFinding::with_actions(
4068 test_unused_dependency("optional", DependencyLocation::OptionalDependencies),
4069 )],
4070 type_only_dependencies: vec![TypeOnlyDependencyFinding::with_actions(
4071 TypeOnlyDependency {
4072 package_name: "type-only".to_string(),
4073 path: PathBuf::from("package.json"),
4074 line: 8,
4075 },
4076 )],
4077 test_only_dependencies: vec![TestOnlyDependencyFinding::with_actions(
4078 TestOnlyDependency {
4079 package_name: "test-only".to_string(),
4080 path: PathBuf::from("package.json"),
4081 line: 9,
4082 },
4083 )],
4084 circular_dependencies: vec![CircularDependencyFinding::with_actions(
4085 CircularDependency {
4086 files: vec![PathBuf::from("a.ts"), PathBuf::from("b.ts")],
4087 length: 2,
4088 line: 3,
4089 col: 0,
4090 edges: Vec::new(),
4091 is_cross_package: false,
4092 },
4093 )],
4094 boundary_violations: vec![BoundaryViolationFinding::with_actions(BoundaryViolation {
4095 from_path: PathBuf::from("src/ui/Button.tsx"),
4096 to_path: PathBuf::from("src/db/queries.ts"),
4097 from_zone: "ui".to_string(),
4098 to_zone: "database".to_string(),
4099 import_specifier: "../db/queries".to_string(),
4100 line: 3,
4101 col: 0,
4102 })],
4103 ..Default::default()
4104 };
4105
4106 assert_eq!(results.total_issues(), 15);
4108 assert!(results.has_issues());
4109 }
4110
4111 #[test]
4114 fn total_issues_and_has_issues_are_consistent() {
4115 let results = AnalysisResults::default();
4116 assert_eq!(results.total_issues(), 0);
4117 assert!(!results.has_issues());
4118 assert_eq!(results.total_issues() > 0, results.has_issues());
4119 }
4120
4121 #[test]
4124 fn total_issues_sums_all_categories_independently() {
4125 let mut results = AnalysisResults::default();
4126 results
4127 .unused_files
4128 .push(UnusedFileFinding::with_actions(UnusedFile {
4129 path: PathBuf::from("a.ts"),
4130 }));
4131 assert_eq!(results.total_issues(), 1);
4132
4133 results
4134 .unused_files
4135 .push(UnusedFileFinding::with_actions(UnusedFile {
4136 path: PathBuf::from("b.ts"),
4137 }));
4138 assert_eq!(results.total_issues(), 2);
4139
4140 results
4141 .unresolved_imports
4142 .push(UnresolvedImportFinding::with_actions(UnresolvedImport {
4143 path: PathBuf::from("c.ts"),
4144 specifier: "./missing".to_string(),
4145 line: 1,
4146 col: 0,
4147 specifier_col: 0,
4148 }));
4149 assert_eq!(results.total_issues(), 3);
4150 }
4151
4152 #[test]
4155 fn default_results_all_fields_empty() {
4156 let r = AnalysisResults::default();
4157 assert!(r.unused_files.is_empty());
4158 assert!(r.unused_exports.is_empty());
4159 assert!(r.unused_types.is_empty());
4160 assert!(r.unused_dependencies.is_empty());
4161 assert!(r.unused_dev_dependencies.is_empty());
4162 assert!(r.unused_optional_dependencies.is_empty());
4163 assert!(r.unused_enum_members.is_empty());
4164 assert!(r.unused_class_members.is_empty());
4165 assert!(r.unresolved_imports.is_empty());
4166 assert!(r.unlisted_dependencies.is_empty());
4167 assert!(r.duplicate_exports.is_empty());
4168 assert!(r.type_only_dependencies.is_empty());
4169 assert!(r.test_only_dependencies.is_empty());
4170 assert!(r.circular_dependencies.is_empty());
4171 assert!(r.boundary_violations.is_empty());
4172 assert!(r.unused_catalog_entries.is_empty());
4173 assert!(r.unresolved_catalog_references.is_empty());
4174 assert!(r.export_usages.is_empty());
4175 }
4176
4177 #[test]
4180 fn entry_point_summary_default() {
4181 let summary = EntryPointSummary::default();
4182 assert_eq!(summary.total, 0);
4183 assert!(summary.by_source.is_empty());
4184 }
4185
4186 #[test]
4187 fn entry_point_summary_not_in_default_results() {
4188 let r = AnalysisResults::default();
4189 assert!(r.entry_point_summary.is_none());
4190 }
4191
4192 #[test]
4193 fn entry_point_summary_some_preserves_data() {
4194 let r = AnalysisResults {
4195 entry_point_summary: Some(EntryPointSummary {
4196 total: 5,
4197 by_source: vec![("package.json".to_string(), 2), ("plugin".to_string(), 3)],
4198 }),
4199 ..AnalysisResults::default()
4200 };
4201 let summary = r.entry_point_summary.as_ref().unwrap();
4202 assert_eq!(summary.total, 5);
4203 assert_eq!(summary.by_source.len(), 2);
4204 assert_eq!(summary.by_source[0], ("package.json".to_string(), 2));
4205 }
4206
4207 #[test]
4210 fn sort_unused_files_by_path() {
4211 let mut r = AnalysisResults::default();
4212 r.unused_files
4213 .push(UnusedFileFinding::with_actions(UnusedFile {
4214 path: PathBuf::from("z.ts"),
4215 }));
4216 r.unused_files
4217 .push(UnusedFileFinding::with_actions(UnusedFile {
4218 path: PathBuf::from("a.ts"),
4219 }));
4220 r.unused_files
4221 .push(UnusedFileFinding::with_actions(UnusedFile {
4222 path: PathBuf::from("m.ts"),
4223 }));
4224 r.sort();
4225 let paths: Vec<_> = r
4226 .unused_files
4227 .iter()
4228 .map(|f| f.file.path.to_string_lossy().to_string())
4229 .collect();
4230 assert_eq!(paths, vec!["a.ts", "m.ts", "z.ts"]);
4231 }
4232
4233 #[test]
4236 fn sort_unused_exports_by_path_line_name() {
4237 let mut r = AnalysisResults::default();
4238 let mk = |path: &str, line: u32, name: &str| {
4239 UnusedExportFinding::with_actions(UnusedExport {
4240 path: PathBuf::from(path),
4241 export_name: name.to_string(),
4242 is_type_only: false,
4243 line,
4244 col: 0,
4245 span_start: 0,
4246 is_re_export: false,
4247 })
4248 };
4249 r.unused_exports.push(mk("b.ts", 5, "beta"));
4250 r.unused_exports.push(mk("a.ts", 10, "zeta"));
4251 r.unused_exports.push(mk("a.ts", 10, "alpha"));
4252 r.unused_exports.push(mk("a.ts", 1, "gamma"));
4253 r.sort();
4254 let keys: Vec<_> = r
4255 .unused_exports
4256 .iter()
4257 .map(|e| {
4258 format!(
4259 "{}:{}:{}",
4260 e.export.path.to_string_lossy(),
4261 e.export.line,
4262 e.export.export_name
4263 )
4264 })
4265 .collect();
4266 assert_eq!(
4267 keys,
4268 vec![
4269 "a.ts:1:gamma",
4270 "a.ts:10:alpha",
4271 "a.ts:10:zeta",
4272 "b.ts:5:beta"
4273 ]
4274 );
4275 }
4276
4277 #[test]
4280 fn sort_unused_types_by_path_line_name() {
4281 let mut r = AnalysisResults::default();
4282 let mk = |path: &str, line: u32, name: &str| {
4283 UnusedTypeFinding::with_actions(UnusedExport {
4284 path: PathBuf::from(path),
4285 export_name: name.to_string(),
4286 is_type_only: true,
4287 line,
4288 col: 0,
4289 span_start: 0,
4290 is_re_export: false,
4291 })
4292 };
4293 r.unused_types.push(mk("z.ts", 1, "Z"));
4294 r.unused_types.push(mk("a.ts", 1, "A"));
4295 r.sort();
4296 assert_eq!(r.unused_types[0].export.path, PathBuf::from("a.ts"));
4297 assert_eq!(r.unused_types[1].export.path, PathBuf::from("z.ts"));
4298 }
4299
4300 #[test]
4303 fn sort_unused_dependencies_by_path_line_name() {
4304 let mut r = AnalysisResults::default();
4305 let mk = |path: &str, line: u32, name: &str| {
4306 UnusedDependencyFinding::with_actions(UnusedDependency {
4307 package_name: name.to_string(),
4308 location: DependencyLocation::Dependencies,
4309 path: PathBuf::from(path),
4310 line,
4311 used_in_workspaces: Vec::new(),
4312 })
4313 };
4314 r.unused_dependencies.push(mk("b/package.json", 3, "zlib"));
4315 r.unused_dependencies.push(mk("a/package.json", 5, "react"));
4316 r.unused_dependencies.push(mk("a/package.json", 5, "axios"));
4317 r.sort();
4318 let names: Vec<_> = r
4319 .unused_dependencies
4320 .iter()
4321 .map(|d| d.dep.package_name.as_str())
4322 .collect();
4323 assert_eq!(names, vec!["axios", "react", "zlib"]);
4324 }
4325
4326 #[test]
4329 fn sort_unused_dev_dependencies() {
4330 let mut r = AnalysisResults::default();
4331 r.unused_dev_dependencies
4332 .push(UnusedDevDependencyFinding::with_actions(UnusedDependency {
4333 package_name: "vitest".to_string(),
4334 location: DependencyLocation::DevDependencies,
4335 path: PathBuf::from("package.json"),
4336 line: 10,
4337 used_in_workspaces: Vec::new(),
4338 }));
4339 r.unused_dev_dependencies
4340 .push(UnusedDevDependencyFinding::with_actions(UnusedDependency {
4341 package_name: "jest".to_string(),
4342 location: DependencyLocation::DevDependencies,
4343 path: PathBuf::from("package.json"),
4344 line: 5,
4345 used_in_workspaces: Vec::new(),
4346 }));
4347 r.sort();
4348 assert_eq!(r.unused_dev_dependencies[0].dep.package_name, "jest");
4349 assert_eq!(r.unused_dev_dependencies[1].dep.package_name, "vitest");
4350 }
4351
4352 #[test]
4355 fn sort_unused_optional_dependencies() {
4356 let mut r = AnalysisResults::default();
4357 r.unused_optional_dependencies
4358 .push(UnusedOptionalDependencyFinding::with_actions(
4359 UnusedDependency {
4360 package_name: "zod".to_string(),
4361 location: DependencyLocation::OptionalDependencies,
4362 path: PathBuf::from("package.json"),
4363 line: 3,
4364 used_in_workspaces: Vec::new(),
4365 },
4366 ));
4367 r.unused_optional_dependencies
4368 .push(UnusedOptionalDependencyFinding::with_actions(
4369 UnusedDependency {
4370 package_name: "ajv".to_string(),
4371 location: DependencyLocation::OptionalDependencies,
4372 path: PathBuf::from("package.json"),
4373 line: 2,
4374 used_in_workspaces: Vec::new(),
4375 },
4376 ));
4377 r.sort();
4378 assert_eq!(r.unused_optional_dependencies[0].dep.package_name, "ajv");
4379 assert_eq!(r.unused_optional_dependencies[1].dep.package_name, "zod");
4380 }
4381
4382 #[test]
4385 fn sort_unused_enum_members_by_path_line_parent_member() {
4386 let mut r = AnalysisResults::default();
4387 let mk = |path: &str, line: u32, parent: &str, member: &str| {
4388 UnusedEnumMemberFinding::with_actions(UnusedMember {
4389 path: PathBuf::from(path),
4390 parent_name: parent.to_string(),
4391 member_name: member.to_string(),
4392 kind: MemberKind::EnumMember,
4393 line,
4394 col: 0,
4395 })
4396 };
4397 r.unused_enum_members.push(mk("a.ts", 5, "Status", "Z"));
4398 r.unused_enum_members.push(mk("a.ts", 5, "Status", "A"));
4399 r.unused_enum_members.push(mk("a.ts", 1, "Direction", "Up"));
4400 r.sort();
4401 let keys: Vec<_> = r
4402 .unused_enum_members
4403 .iter()
4404 .map(|m| format!("{}:{}", m.member.parent_name, m.member.member_name))
4405 .collect();
4406 assert_eq!(keys, vec!["Direction:Up", "Status:A", "Status:Z"]);
4407 }
4408
4409 #[test]
4412 fn sort_unused_class_members() {
4413 let mut r = AnalysisResults::default();
4414 let mk = |path: &str, line: u32, parent: &str, member: &str| {
4415 UnusedClassMemberFinding::with_actions(UnusedMember {
4416 path: PathBuf::from(path),
4417 parent_name: parent.to_string(),
4418 member_name: member.to_string(),
4419 kind: MemberKind::ClassMethod,
4420 line,
4421 col: 0,
4422 })
4423 };
4424 r.unused_class_members.push(mk("b.ts", 1, "Foo", "z"));
4425 r.unused_class_members.push(mk("a.ts", 1, "Bar", "a"));
4426 r.sort();
4427 assert_eq!(r.unused_class_members[0].member.path, PathBuf::from("a.ts"));
4428 assert_eq!(r.unused_class_members[1].member.path, PathBuf::from("b.ts"));
4429 }
4430
4431 #[test]
4434 fn sort_unresolved_imports_by_path_line_col_specifier() {
4435 let mut r = AnalysisResults::default();
4436 let mk = |path: &str, line: u32, col: u32, spec: &str| {
4437 UnresolvedImportFinding::with_actions(UnresolvedImport {
4438 path: PathBuf::from(path),
4439 specifier: spec.to_string(),
4440 line,
4441 col,
4442 specifier_col: 0,
4443 })
4444 };
4445 r.unresolved_imports.push(mk("a.ts", 5, 0, "./z"));
4446 r.unresolved_imports.push(mk("a.ts", 5, 0, "./a"));
4447 r.unresolved_imports.push(mk("a.ts", 1, 0, "./m"));
4448 r.sort();
4449 let specs: Vec<_> = r
4450 .unresolved_imports
4451 .iter()
4452 .map(|i| i.import.specifier.as_str())
4453 .collect();
4454 assert_eq!(specs, vec!["./m", "./a", "./z"]);
4455 }
4456
4457 #[test]
4460 fn sort_unlisted_dependencies_by_name_and_inner_sites() {
4461 let mut r = AnalysisResults::default();
4462 r.unlisted_dependencies
4463 .push(UnlistedDependencyFinding::with_actions(
4464 UnlistedDependency {
4465 package_name: "zod".to_string(),
4466 imported_from: vec![
4467 ImportSite {
4468 path: PathBuf::from("b.ts"),
4469 line: 10,
4470 col: 0,
4471 },
4472 ImportSite {
4473 path: PathBuf::from("a.ts"),
4474 line: 1,
4475 col: 0,
4476 },
4477 ],
4478 },
4479 ));
4480 r.unlisted_dependencies
4481 .push(UnlistedDependencyFinding::with_actions(
4482 UnlistedDependency {
4483 package_name: "axios".to_string(),
4484 imported_from: vec![ImportSite {
4485 path: PathBuf::from("c.ts"),
4486 line: 1,
4487 col: 0,
4488 }],
4489 },
4490 ));
4491 r.sort();
4492
4493 assert_eq!(r.unlisted_dependencies[0].dep.package_name, "axios");
4495 assert_eq!(r.unlisted_dependencies[1].dep.package_name, "zod");
4496
4497 let zod_sites: Vec<_> = r.unlisted_dependencies[1]
4499 .dep
4500 .imported_from
4501 .iter()
4502 .map(|s| s.path.to_string_lossy().to_string())
4503 .collect();
4504 assert_eq!(zod_sites, vec!["a.ts", "b.ts"]);
4505 }
4506
4507 #[test]
4510 fn sort_duplicate_exports_by_name_and_inner_locations() {
4511 let mut r = AnalysisResults::default();
4512 r.duplicate_exports
4513 .push(DuplicateExportFinding::with_actions(DuplicateExport {
4514 export_name: "z".to_string(),
4515 locations: vec![
4516 DuplicateLocation {
4517 path: PathBuf::from("c.ts"),
4518 line: 1,
4519 col: 0,
4520 },
4521 DuplicateLocation {
4522 path: PathBuf::from("a.ts"),
4523 line: 5,
4524 col: 0,
4525 },
4526 ],
4527 }));
4528 r.duplicate_exports
4529 .push(DuplicateExportFinding::with_actions(DuplicateExport {
4530 export_name: "a".to_string(),
4531 locations: vec![DuplicateLocation {
4532 path: PathBuf::from("b.ts"),
4533 line: 1,
4534 col: 0,
4535 }],
4536 }));
4537 r.sort();
4538
4539 assert_eq!(r.duplicate_exports[0].export.export_name, "a");
4541 assert_eq!(r.duplicate_exports[1].export.export_name, "z");
4542
4543 let z_locs: Vec<_> = r.duplicate_exports[1]
4545 .export
4546 .locations
4547 .iter()
4548 .map(|l| l.path.to_string_lossy().to_string())
4549 .collect();
4550 assert_eq!(z_locs, vec!["a.ts", "c.ts"]);
4551 }
4552
4553 #[test]
4556 fn sort_type_only_dependencies() {
4557 let mut r = AnalysisResults::default();
4558 r.type_only_dependencies
4559 .push(TypeOnlyDependencyFinding::with_actions(
4560 TypeOnlyDependency {
4561 package_name: "zod".to_string(),
4562 path: PathBuf::from("package.json"),
4563 line: 10,
4564 },
4565 ));
4566 r.type_only_dependencies
4567 .push(TypeOnlyDependencyFinding::with_actions(
4568 TypeOnlyDependency {
4569 package_name: "ajv".to_string(),
4570 path: PathBuf::from("package.json"),
4571 line: 5,
4572 },
4573 ));
4574 r.sort();
4575 assert_eq!(r.type_only_dependencies[0].dep.package_name, "ajv");
4576 assert_eq!(r.type_only_dependencies[1].dep.package_name, "zod");
4577 }
4578
4579 #[test]
4582 fn sort_test_only_dependencies() {
4583 let mut r = AnalysisResults::default();
4584 r.test_only_dependencies
4585 .push(TestOnlyDependencyFinding::with_actions(
4586 TestOnlyDependency {
4587 package_name: "vitest".to_string(),
4588 path: PathBuf::from("package.json"),
4589 line: 15,
4590 },
4591 ));
4592 r.test_only_dependencies
4593 .push(TestOnlyDependencyFinding::with_actions(
4594 TestOnlyDependency {
4595 package_name: "jest".to_string(),
4596 path: PathBuf::from("package.json"),
4597 line: 10,
4598 },
4599 ));
4600 r.sort();
4601 assert_eq!(r.test_only_dependencies[0].dep.package_name, "jest");
4602 assert_eq!(r.test_only_dependencies[1].dep.package_name, "vitest");
4603 }
4604
4605 #[test]
4608 fn sort_circular_dependencies_by_files_then_length() {
4609 let mut r = AnalysisResults::default();
4610 r.circular_dependencies
4611 .push(CircularDependencyFinding::with_actions(
4612 CircularDependency {
4613 files: vec![PathBuf::from("b.ts"), PathBuf::from("c.ts")],
4614 length: 2,
4615 line: 1,
4616 col: 0,
4617 edges: Vec::new(),
4618 is_cross_package: false,
4619 },
4620 ));
4621 r.circular_dependencies
4622 .push(CircularDependencyFinding::with_actions(
4623 CircularDependency {
4624 files: vec![PathBuf::from("a.ts"), PathBuf::from("b.ts")],
4625 length: 2,
4626 line: 1,
4627 col: 0,
4628 edges: Vec::new(),
4629 is_cross_package: true,
4630 },
4631 ));
4632 r.sort();
4633 assert_eq!(
4634 r.circular_dependencies[0].cycle.files[0],
4635 PathBuf::from("a.ts")
4636 );
4637 assert_eq!(
4638 r.circular_dependencies[1].cycle.files[0],
4639 PathBuf::from("b.ts")
4640 );
4641 }
4642
4643 #[test]
4646 fn sort_boundary_violations() {
4647 let mut r = AnalysisResults::default();
4648 let mk = |from: &str, line: u32, col: u32, to: &str| {
4649 BoundaryViolationFinding::with_actions(BoundaryViolation {
4650 from_path: PathBuf::from(from),
4651 to_path: PathBuf::from(to),
4652 from_zone: "a".to_string(),
4653 to_zone: "b".to_string(),
4654 import_specifier: to.to_string(),
4655 line,
4656 col,
4657 })
4658 };
4659 r.boundary_violations.push(mk("z.ts", 1, 0, "a.ts"));
4660 r.boundary_violations.push(mk("a.ts", 5, 0, "b.ts"));
4661 r.boundary_violations.push(mk("a.ts", 1, 0, "c.ts"));
4662 r.sort();
4663 let from_paths: Vec<_> = r
4664 .boundary_violations
4665 .iter()
4666 .map(|v| {
4667 format!(
4668 "{}:{}",
4669 v.violation.from_path.to_string_lossy(),
4670 v.violation.line
4671 )
4672 })
4673 .collect();
4674 assert_eq!(from_paths, vec!["a.ts:1", "a.ts:5", "z.ts:1"]);
4675 }
4676
4677 #[test]
4680 fn sort_export_usages_and_inner_reference_locations() {
4681 let mut r = AnalysisResults::default();
4682 r.export_usages.push(ExportUsage {
4683 path: PathBuf::from("z.ts"),
4684 export_name: "foo".to_string(),
4685 line: 1,
4686 col: 0,
4687 reference_count: 2,
4688 reference_locations: vec![
4689 ReferenceLocation {
4690 path: PathBuf::from("c.ts"),
4691 line: 10,
4692 col: 0,
4693 },
4694 ReferenceLocation {
4695 path: PathBuf::from("a.ts"),
4696 line: 5,
4697 col: 0,
4698 },
4699 ],
4700 });
4701 r.export_usages.push(ExportUsage {
4702 path: PathBuf::from("a.ts"),
4703 export_name: "bar".to_string(),
4704 line: 1,
4705 col: 0,
4706 reference_count: 1,
4707 reference_locations: vec![ReferenceLocation {
4708 path: PathBuf::from("b.ts"),
4709 line: 1,
4710 col: 0,
4711 }],
4712 });
4713 r.sort();
4714
4715 assert_eq!(r.export_usages[0].path, PathBuf::from("a.ts"));
4717 assert_eq!(r.export_usages[1].path, PathBuf::from("z.ts"));
4718
4719 let refs: Vec<_> = r.export_usages[1]
4721 .reference_locations
4722 .iter()
4723 .map(|l| l.path.to_string_lossy().to_string())
4724 .collect();
4725 assert_eq!(refs, vec!["a.ts", "c.ts"]);
4726 }
4727
4728 #[test]
4731 fn sort_empty_results_is_noop() {
4732 let mut r = AnalysisResults::default();
4733 r.sort(); assert_eq!(r.total_issues(), 0);
4735 }
4736
4737 #[test]
4740 fn sort_single_element_lists_stable() {
4741 let mut r = AnalysisResults::default();
4742 r.unused_files
4743 .push(UnusedFileFinding::with_actions(UnusedFile {
4744 path: PathBuf::from("only.ts"),
4745 }));
4746 r.sort();
4747 assert_eq!(r.unused_files[0].file.path, PathBuf::from("only.ts"));
4748 }
4749
4750 #[test]
4753 fn serialize_empty_results() {
4754 let r = AnalysisResults::default();
4755 let json = serde_json::to_value(&r).unwrap();
4756
4757 assert!(json["unused_files"].as_array().unwrap().is_empty());
4759 assert!(json["unused_exports"].as_array().unwrap().is_empty());
4760 assert!(json["circular_dependencies"].as_array().unwrap().is_empty());
4761
4762 assert!(json.get("export_usages").is_none());
4764 assert!(json.get("entry_point_summary").is_none());
4765 }
4766
4767 #[test]
4768 fn serialize_unused_file_path() {
4769 let r = UnusedFile {
4770 path: PathBuf::from("src/utils/index.ts"),
4771 };
4772 let json = serde_json::to_value(&r).unwrap();
4773 assert_eq!(json["path"], "src/utils/index.ts");
4774 }
4775
4776 #[test]
4777 fn serialize_dependency_location_camel_case() {
4778 let dep = UnusedDependency {
4779 package_name: "react".to_string(),
4780 location: DependencyLocation::DevDependencies,
4781 path: PathBuf::from("package.json"),
4782 line: 5,
4783 used_in_workspaces: Vec::new(),
4784 };
4785 let json = serde_json::to_value(&dep).unwrap();
4786 assert_eq!(json["location"], "devDependencies");
4787
4788 let dep2 = UnusedDependency {
4789 package_name: "react".to_string(),
4790 location: DependencyLocation::Dependencies,
4791 path: PathBuf::from("package.json"),
4792 line: 3,
4793 used_in_workspaces: Vec::new(),
4794 };
4795 let json2 = serde_json::to_value(&dep2).unwrap();
4796 assert_eq!(json2["location"], "dependencies");
4797
4798 let dep3 = UnusedDependency {
4799 package_name: "fsevents".to_string(),
4800 location: DependencyLocation::OptionalDependencies,
4801 path: PathBuf::from("package.json"),
4802 line: 7,
4803 used_in_workspaces: Vec::new(),
4804 };
4805 let json3 = serde_json::to_value(&dep3).unwrap();
4806 assert_eq!(json3["location"], "optionalDependencies");
4807 }
4808
4809 #[test]
4810 fn serialize_circular_dependency_skips_false_cross_package() {
4811 let cd = CircularDependency {
4812 files: vec![PathBuf::from("a.ts"), PathBuf::from("b.ts")],
4813 length: 2,
4814 line: 1,
4815 col: 0,
4816 edges: Vec::new(),
4817 is_cross_package: false,
4818 };
4819 let json = serde_json::to_value(&cd).unwrap();
4820 assert!(json.get("is_cross_package").is_none());
4822 }
4823
4824 #[test]
4825 fn serialize_circular_dependency_includes_true_cross_package() {
4826 let cd = CircularDependency {
4827 files: vec![PathBuf::from("a.ts"), PathBuf::from("b.ts")],
4828 length: 2,
4829 line: 1,
4830 col: 0,
4831 edges: Vec::new(),
4832 is_cross_package: true,
4833 };
4834 let json = serde_json::to_value(&cd).unwrap();
4835 assert_eq!(json["is_cross_package"], true);
4836 }
4837
4838 #[test]
4839 fn serialize_unused_export_fields() {
4840 let e = UnusedExport {
4841 path: PathBuf::from("src/mod.ts"),
4842 export_name: "helper".to_string(),
4843 is_type_only: true,
4844 line: 42,
4845 col: 7,
4846 span_start: 100,
4847 is_re_export: true,
4848 };
4849 let json = serde_json::to_value(&e).unwrap();
4850 assert_eq!(json["path"], "src/mod.ts");
4851 assert_eq!(json["export_name"], "helper");
4852 assert_eq!(json["is_type_only"], true);
4853 assert_eq!(json["line"], 42);
4854 assert_eq!(json["col"], 7);
4855 assert_eq!(json["span_start"], 100);
4856 assert_eq!(json["is_re_export"], true);
4857 }
4858
4859 #[test]
4860 fn serialize_boundary_violation_fields() {
4861 let v = BoundaryViolation {
4862 from_path: PathBuf::from("src/ui/button.tsx"),
4863 to_path: PathBuf::from("src/db/queries.ts"),
4864 from_zone: "ui".to_string(),
4865 to_zone: "db".to_string(),
4866 import_specifier: "../db/queries".to_string(),
4867 line: 3,
4868 col: 0,
4869 };
4870 let json = serde_json::to_value(&v).unwrap();
4871 assert_eq!(json["from_path"], "src/ui/button.tsx");
4872 assert_eq!(json["to_path"], "src/db/queries.ts");
4873 assert_eq!(json["from_zone"], "ui");
4874 assert_eq!(json["to_zone"], "db");
4875 assert_eq!(json["import_specifier"], "../db/queries");
4876 }
4877
4878 #[test]
4879 fn serialize_unlisted_dependency_with_import_sites() {
4880 let d = UnlistedDependency {
4881 package_name: "chalk".to_string(),
4882 imported_from: vec![
4883 ImportSite {
4884 path: PathBuf::from("a.ts"),
4885 line: 1,
4886 col: 0,
4887 },
4888 ImportSite {
4889 path: PathBuf::from("b.ts"),
4890 line: 5,
4891 col: 3,
4892 },
4893 ],
4894 };
4895 let json = serde_json::to_value(&d).unwrap();
4896 assert_eq!(json["package_name"], "chalk");
4897 let sites = json["imported_from"].as_array().unwrap();
4898 assert_eq!(sites.len(), 2);
4899 assert_eq!(sites[0]["path"], "a.ts");
4900 assert_eq!(sites[1]["line"], 5);
4901 }
4902
4903 #[test]
4904 fn serialize_duplicate_export_with_locations() {
4905 let d = DuplicateExport {
4906 export_name: "Button".to_string(),
4907 locations: vec![
4908 DuplicateLocation {
4909 path: PathBuf::from("src/a.ts"),
4910 line: 10,
4911 col: 0,
4912 },
4913 DuplicateLocation {
4914 path: PathBuf::from("src/b.ts"),
4915 line: 20,
4916 col: 5,
4917 },
4918 ],
4919 };
4920 let json = serde_json::to_value(&d).unwrap();
4921 assert_eq!(json["export_name"], "Button");
4922 let locs = json["locations"].as_array().unwrap();
4923 assert_eq!(locs.len(), 2);
4924 assert_eq!(locs[0]["line"], 10);
4925 assert_eq!(locs[1]["col"], 5);
4926 }
4927
4928 #[test]
4929 fn serialize_type_only_dependency() {
4930 let d = TypeOnlyDependency {
4931 package_name: "@types/react".to_string(),
4932 path: PathBuf::from("package.json"),
4933 line: 12,
4934 };
4935 let json = serde_json::to_value(&d).unwrap();
4936 assert_eq!(json["package_name"], "@types/react");
4937 assert_eq!(json["line"], 12);
4938 }
4939
4940 #[test]
4941 fn serialize_test_only_dependency() {
4942 let d = TestOnlyDependency {
4943 package_name: "vitest".to_string(),
4944 path: PathBuf::from("package.json"),
4945 line: 8,
4946 };
4947 let json = serde_json::to_value(&d).unwrap();
4948 assert_eq!(json["package_name"], "vitest");
4949 assert_eq!(json["line"], 8);
4950 }
4951
4952 #[test]
4953 fn serialize_unused_member() {
4954 let m = UnusedMember {
4955 path: PathBuf::from("enums.ts"),
4956 parent_name: "Status".to_string(),
4957 member_name: "Pending".to_string(),
4958 kind: MemberKind::EnumMember,
4959 line: 3,
4960 col: 4,
4961 };
4962 let json = serde_json::to_value(&m).unwrap();
4963 assert_eq!(json["parent_name"], "Status");
4964 assert_eq!(json["member_name"], "Pending");
4965 assert_eq!(json["line"], 3);
4966 }
4967
4968 #[test]
4969 fn serialize_unresolved_import() {
4970 let i = UnresolvedImport {
4971 path: PathBuf::from("app.ts"),
4972 specifier: "./missing-module".to_string(),
4973 line: 7,
4974 col: 0,
4975 specifier_col: 21,
4976 };
4977 let json = serde_json::to_value(&i).unwrap();
4978 assert_eq!(json["specifier"], "./missing-module");
4979 assert_eq!(json["specifier_col"], 21);
4980 }
4981
4982 #[test]
4985 fn deserialize_circular_dependency_with_defaults() {
4986 let json = r#"{"files":["a.ts","b.ts"],"length":2}"#;
4988 let cd: CircularDependency = serde_json::from_str(json).unwrap();
4989 assert_eq!(cd.files.len(), 2);
4990 assert_eq!(cd.length, 2);
4991 assert_eq!(cd.line, 0);
4992 assert_eq!(cd.col, 0);
4993 assert!(!cd.is_cross_package);
4994 }
4995
4996 #[test]
4997 fn deserialize_circular_dependency_with_all_fields() {
4998 let json =
4999 r#"{"files":["a.ts","b.ts"],"length":2,"line":5,"col":10,"is_cross_package":true}"#;
5000 let cd: CircularDependency = serde_json::from_str(json).unwrap();
5001 assert_eq!(cd.line, 5);
5002 assert_eq!(cd.col, 10);
5003 assert!(cd.is_cross_package);
5004 }
5005
5006 #[test]
5009 fn clone_results_are_independent() {
5010 let mut r = AnalysisResults::default();
5011 r.unused_files
5012 .push(UnusedFileFinding::with_actions(UnusedFile {
5013 path: PathBuf::from("a.ts"),
5014 }));
5015 let mut cloned = r.clone();
5016 cloned
5017 .unused_files
5018 .push(UnusedFileFinding::with_actions(UnusedFile {
5019 path: PathBuf::from("b.ts"),
5020 }));
5021 assert_eq!(r.total_issues(), 1);
5022 assert_eq!(cloned.total_issues(), 2);
5023 }
5024
5025 fn protected_architecture_findings(path: &Path) -> AnalysisResults {
5026 AnalysisResults {
5027 boundary_violations: vec![BoundaryViolationFinding::with_actions(BoundaryViolation {
5028 from_path: path.to_path_buf(),
5029 to_path: PathBuf::from("src/target.ts"),
5030 from_zone: "ui".to_string(),
5031 to_zone: "data".to_string(),
5032 import_specifier: "../target".to_string(),
5033 line: 1,
5034 col: 0,
5035 })],
5036 boundary_coverage_violations: vec![BoundaryCoverageViolationFinding::with_actions(
5037 BoundaryCoverageViolation {
5038 path: path.to_path_buf(),
5039 line: 1,
5040 col: 0,
5041 },
5042 )],
5043 boundary_call_violations: vec![BoundaryCallViolationFinding::with_actions(
5044 BoundaryCallViolation {
5045 path: path.to_path_buf(),
5046 line: 1,
5047 col: 0,
5048 zone: "ui".to_string(),
5049 callee: "cp.exec".to_string(),
5050 pattern: "child_process.*".to_string(),
5051 },
5052 )],
5053 policy_violations: vec![PolicyViolationFinding::with_actions(PolicyViolation {
5054 path: path.to_path_buf(),
5055 line: 1,
5056 col: 0,
5057 pack: "security".to_string(),
5058 rule_id: "no-eval".to_string(),
5059 kind: PolicyRuleKind::BannedCall,
5060 matched: "eval".to_string(),
5061 severity: PolicyViolationSeverity::Error,
5062 message: None,
5063 })],
5064 stale_suppressions: vec![StaleSuppression {
5065 path: path.to_path_buf(),
5066 line: 1,
5067 col: 0,
5068 origin: SuppressionOrigin::Comment {
5069 issue_kind: Some("unused-file".to_string()),
5070 reason: None,
5071 is_file_level: false,
5072 kind_known: true,
5073 },
5074 missing_reason: false,
5075 actions: StaleSuppression::actions_for(false),
5076 }],
5077 ..AnalysisResults::default()
5078 }
5079 }
5080
5081 fn protected_framework_findings() -> AnalysisResults {
5082 AnalysisResults {
5083 invalid_client_exports: vec![InvalidClientExportFinding::with_actions(
5084 InvalidClientExport {
5085 path: PathBuf::from("ignored/client.ts"),
5086 export_name: "metadata".to_string(),
5087 directive: "use client".to_string(),
5088 line: 1,
5089 col: 0,
5090 },
5091 )],
5092 mixed_client_server_barrels: vec![MixedClientServerBarrelFinding::with_actions(
5093 MixedClientServerBarrel {
5094 path: PathBuf::from("ignored/barrel.ts"),
5095 client_origin: "./client".to_string(),
5096 server_origin: "./server".to_string(),
5097 line: 1,
5098 col: 0,
5099 },
5100 )],
5101 misplaced_directives: vec![MisplacedDirectiveFinding::with_actions(
5102 MisplacedDirective {
5103 path: PathBuf::from("ignored/directive.ts"),
5104 directive: "use client".to_string(),
5105 line: 2,
5106 col: 0,
5107 },
5108 )],
5109 route_collisions: vec![RouteCollisionFinding::with_actions(RouteCollision {
5110 path: PathBuf::from("ignored/app/about/page.tsx"),
5111 url: "/about".to_string(),
5112 conflicting_paths: vec![PathBuf::from("src/app/about/page.tsx")],
5113 line: 1,
5114 col: 0,
5115 })],
5116 dynamic_segment_name_conflicts: vec![DynamicSegmentNameConflictFinding::with_actions(
5117 DynamicSegmentNameConflict {
5118 path: PathBuf::from("ignored/app/shop/[id]/page.tsx"),
5119 position: "/shop".to_string(),
5120 conflicting_segments: vec!["[id]".to_string(), "[slug]".to_string()],
5121 conflicting_paths: vec![PathBuf::from("src/app/shop/[slug]/page.tsx")],
5122 line: 1,
5123 col: 0,
5124 },
5125 )],
5126 ..AnalysisResults::default()
5127 }
5128 }
5129
5130 #[test]
5131 fn finding_ignore_hides_dead_code_but_retains_protected_findings() {
5132 let ignored_path = PathBuf::from("ignored/dead.ts");
5133 let mut results = protected_architecture_findings(&ignored_path);
5134 results.merge_into(protected_framework_findings());
5135 results.unused_files = vec![
5136 UnusedFileFinding::with_actions(UnusedFile { path: ignored_path }),
5137 UnusedFileFinding::with_actions(UnusedFile {
5138 path: PathBuf::from("src/visible.ts"),
5139 }),
5140 ];
5141
5142 results.remove_ignored_dead_code_findings(|path| path.starts_with("ignored"));
5143
5144 assert_eq!(results.unused_files.len(), 1);
5145 assert_eq!(
5146 results.unused_files[0].file.path,
5147 PathBuf::from("src/visible.ts")
5148 );
5149 assert_eq!(results.boundary_violations.len(), 1);
5150 assert_eq!(results.boundary_coverage_violations.len(), 1);
5151 assert_eq!(results.boundary_call_violations.len(), 1);
5152 assert_eq!(results.policy_violations.len(), 1);
5153 assert_eq!(results.stale_suppressions.len(), 1);
5154 assert_eq!(results.invalid_client_exports.len(), 1);
5155 assert_eq!(results.mixed_client_server_barrels.len(), 1);
5156 assert_eq!(results.misplaced_directives.len(), 1);
5157 assert_eq!(results.route_collisions.len(), 1);
5158 assert_eq!(results.dynamic_segment_name_conflicts.len(), 1);
5159 }
5160
5161 #[test]
5162 fn finding_ignore_requires_every_source_owner_to_match() {
5163 let duplicate = |paths: &[&str]| {
5164 DuplicateExportFinding::with_actions(DuplicateExport {
5165 export_name: "shared".to_string(),
5166 locations: paths
5167 .iter()
5168 .map(|path| DuplicateLocation {
5169 path: PathBuf::from(path),
5170 line: 1,
5171 col: 0,
5172 })
5173 .collect(),
5174 })
5175 };
5176 let mut results = AnalysisResults {
5177 duplicate_exports: vec![
5178 duplicate(&["ignored/a.ts", "ignored/b.ts"]),
5179 duplicate(&["ignored/a.ts", "src/b.ts"]),
5180 duplicate(&[]),
5181 ],
5182 ..AnalysisResults::default()
5183 };
5184
5185 results.remove_ignored_dead_code_findings(|path| path.starts_with("ignored"));
5186
5187 assert_eq!(results.duplicate_exports.len(), 2);
5188 assert_eq!(results.duplicate_exports[0].export.locations.len(), 2);
5189 assert!(results.duplicate_exports[1].export.locations.is_empty());
5190 }
5191
5192 #[test]
5193 fn finding_ignore_retains_unowned_package_issues() {
5194 let mut results = AnalysisResults {
5195 unused_dependencies: vec![UnusedDependencyFinding::with_actions(UnusedDependency {
5196 package_name: "unused-package".to_string(),
5197 location: DependencyLocation::Dependencies,
5198 path: PathBuf::from("ignored/package.json"),
5199 line: 3,
5200 used_in_workspaces: vec![],
5201 })],
5202 ..AnalysisResults::default()
5203 };
5204
5205 results.remove_ignored_dead_code_findings(|path| path.starts_with("ignored"));
5206
5207 assert_eq!(results.unused_dependencies.len(), 1);
5208 }
5209
5210 fn thin_wrapper_finding(path: &str) -> ThinWrapperFinding {
5211 ThinWrapperFinding::with_actions(ThinWrapper {
5212 file: PathBuf::from(path),
5213 line: 1,
5214 component: "Wrapper".to_string(),
5215 child_component: "Child".to_string(),
5216 })
5217 }
5218
5219 fn duplicate_prop_shape_finding(path: &str) -> DuplicatePropShapeFinding {
5220 DuplicatePropShapeFinding::with_actions(DuplicatePropShape {
5221 file: PathBuf::from(path),
5222 line: 1,
5223 component: "Card".to_string(),
5224 shape: vec!["title".to_string(), "subtitle".to_string()],
5225 group_size: 3,
5226 sharing_components: vec![],
5227 })
5228 }
5229
5230 fn prop_drilling_chain_finding(paths: &[&str]) -> PropDrillingChainFinding {
5231 PropDrillingChainFinding::with_actions(PropDrillingChain {
5232 prop: "user".to_string(),
5233 depth: paths.len() as u32,
5234 hops: paths
5235 .iter()
5236 .map(|path| PropDrillHop {
5237 file: PathBuf::from(path),
5238 line: 1,
5239 component: "Hop".to_string(),
5240 })
5241 .collect(),
5242 })
5243 }
5244
5245 #[test]
5246 fn finding_ignore_hides_thin_wrappers_by_wrapper_file() {
5247 let mut results = AnalysisResults {
5248 thin_wrappers: vec![
5249 thin_wrapper_finding("ignored/Wrapper.tsx"),
5250 thin_wrapper_finding("src/Wrapper.tsx"),
5251 ],
5252 ..AnalysisResults::default()
5253 };
5254
5255 results.remove_ignored_dead_code_findings(|path| path.starts_with("ignored"));
5256
5257 assert_eq!(results.thin_wrappers.len(), 1);
5258 assert_eq!(
5259 results.thin_wrappers[0].wrapper.file,
5260 PathBuf::from("src/Wrapper.tsx")
5261 );
5262 }
5263
5264 #[test]
5265 fn finding_ignore_hides_duplicate_prop_shapes_by_component_file() {
5266 let mut results = AnalysisResults {
5267 duplicate_prop_shapes: vec![
5268 duplicate_prop_shape_finding("ignored/Card.tsx"),
5269 duplicate_prop_shape_finding("src/Card.tsx"),
5270 ],
5271 ..AnalysisResults::default()
5272 };
5273
5274 results.remove_ignored_dead_code_findings(|path| path.starts_with("ignored"));
5275
5276 assert_eq!(results.duplicate_prop_shapes.len(), 1);
5277 assert_eq!(
5278 results.duplicate_prop_shapes[0].shape.file,
5279 PathBuf::from("src/Card.tsx")
5280 );
5281 }
5282
5283 #[test]
5284 fn finding_ignore_hides_prop_drilling_chains_only_when_every_hop_matches() {
5285 let mut results = AnalysisResults {
5286 prop_drilling_chains: vec![
5287 prop_drilling_chain_finding(&["ignored/a.tsx", "ignored/b.tsx"]),
5288 prop_drilling_chain_finding(&["ignored/a.tsx", "src/b.tsx"]),
5289 prop_drilling_chain_finding(&[]),
5290 ],
5291 ..AnalysisResults::default()
5292 };
5293
5294 results.remove_ignored_dead_code_findings(|path| path.starts_with("ignored"));
5295
5296 assert_eq!(results.prop_drilling_chains.len(), 2);
5297 assert_eq!(results.prop_drilling_chains[0].chain.hops.len(), 2);
5298 assert!(results.prop_drilling_chains[1].chain.hops.is_empty());
5299 }
5300
5301 #[test]
5302 fn finding_ignore_retains_security_findings_and_blind_spot_diagnostics() {
5303 let path = PathBuf::from("ignored/leak.ts");
5304 let mut results = AnalysisResults {
5305 security_findings: vec![SecurityFinding {
5306 finding_id: "id".to_string(),
5307 kind: SecurityFindingKind::TaintedSink,
5308 category: Some("dangerous-html".to_string()),
5309 cwe: Some(79),
5310 path: path.clone(),
5311 line: 1,
5312 col: 0,
5313 evidence: "candidate".to_string(),
5314 source_backed: false,
5315 source_read: None,
5316 severity: SecuritySeverity::Low,
5317 trace: vec![TraceHop {
5318 path: path.clone(),
5319 line: 1,
5320 col: 0,
5321 role: TraceHopRole::Sink,
5322 }],
5323 actions: vec![],
5324 dead_code: None,
5325 reachability: None,
5326 candidate: SecurityCandidate {
5327 source_kind: None,
5328 sink: SecurityCandidateSink {
5329 path: path.clone(),
5330 line: 1,
5331 col: 0,
5332 category: Some("dangerous-html".to_string()),
5333 cwe: Some(79),
5334 callee: None,
5335 url_shape: None,
5336 },
5337 boundary: SecurityCandidateBoundary::default(),
5338 network: None,
5339 },
5340 taint_flow: None,
5341 runtime: None,
5342 attack_surface: None,
5343 }],
5344 security_unresolved_callee_diagnostics: vec![SecurityUnresolvedCalleeDiagnostic {
5345 path,
5346 line: 1,
5347 col: 0,
5348 reason: SkippedSecurityCalleeReason::DynamicDispatch,
5349 expression_kind: SkippedSecurityCalleeExpressionKind::ComputedMemberExpression,
5350 }],
5351 ..AnalysisResults::default()
5352 };
5353
5354 results.remove_ignored_dead_code_findings(|path| path.starts_with("ignored"));
5355
5356 assert_eq!(results.security_findings.len(), 1);
5357 assert_eq!(results.security_unresolved_callee_diagnostics.len(), 1);
5358 }
5359
5360 #[test]
5363 fn export_usages_not_counted_in_total_issues() {
5364 let mut r = AnalysisResults::default();
5365 r.export_usages.push(ExportUsage {
5366 path: PathBuf::from("mod.ts"),
5367 export_name: "foo".to_string(),
5368 line: 1,
5369 col: 0,
5370 reference_count: 3,
5371 reference_locations: vec![],
5372 });
5373 assert_eq!(r.total_issues(), 0);
5375 assert!(!r.has_issues());
5376 }
5377
5378 #[test]
5381 fn entry_point_summary_not_counted_in_total_issues() {
5382 let r = AnalysisResults {
5383 entry_point_summary: Some(EntryPointSummary {
5384 total: 10,
5385 by_source: vec![("config".to_string(), 10)],
5386 }),
5387 ..AnalysisResults::default()
5388 };
5389 assert_eq!(r.total_issues(), 0);
5390 assert!(!r.has_issues());
5391 }
5392}