1use std::path::PathBuf;
4
5use serde::{Deserialize, Serialize};
6
7use crate::extract::MemberKind;
8use crate::serde_path;
9use crate::suppress::IssueKind;
10
11#[derive(Debug, Clone, Default)]
16pub struct EntryPointSummary {
17 pub total: usize,
19 pub by_source: Vec<(String, usize)>,
22}
23
24#[derive(Debug, Default, Clone, Serialize)]
43pub struct AnalysisResults {
44 pub unused_files: Vec<UnusedFile>,
46 pub unused_exports: Vec<UnusedExport>,
48 pub unused_types: Vec<UnusedExport>,
50 pub private_type_leaks: Vec<PrivateTypeLeak>,
52 pub unused_dependencies: Vec<UnusedDependency>,
54 pub unused_dev_dependencies: Vec<UnusedDependency>,
56 pub unused_optional_dependencies: Vec<UnusedDependency>,
58 pub unused_enum_members: Vec<UnusedMember>,
60 pub unused_class_members: Vec<UnusedMember>,
62 pub unresolved_imports: Vec<UnresolvedImport>,
64 pub unlisted_dependencies: Vec<UnlistedDependency>,
66 pub duplicate_exports: Vec<DuplicateExport>,
68 pub type_only_dependencies: Vec<TypeOnlyDependency>,
71 #[serde(default)]
73 pub test_only_dependencies: Vec<TestOnlyDependency>,
74 pub circular_dependencies: Vec<CircularDependency>,
76 #[serde(default)]
78 pub boundary_violations: Vec<BoundaryViolation>,
79 #[serde(default)]
81 pub stale_suppressions: Vec<StaleSuppression>,
82 #[serde(default)]
84 pub unused_catalog_entries: Vec<UnusedCatalogEntry>,
85 #[serde(default)]
87 pub unresolved_catalog_references: Vec<UnresolvedCatalogReference>,
88 #[serde(skip)]
92 pub suppression_count: usize,
93 #[serde(skip)]
96 pub feature_flags: Vec<FeatureFlag>,
97 #[serde(skip)]
101 pub export_usages: Vec<ExportUsage>,
102 #[serde(skip)]
106 pub entry_point_summary: Option<EntryPointSummary>,
107}
108
109impl AnalysisResults {
110 #[must_use]
134 pub const fn total_issues(&self) -> usize {
135 self.unused_files.len()
136 + self.unused_exports.len()
137 + self.unused_types.len()
138 + self.private_type_leaks.len()
139 + self.unused_dependencies.len()
140 + self.unused_dev_dependencies.len()
141 + self.unused_optional_dependencies.len()
142 + self.unused_enum_members.len()
143 + self.unused_class_members.len()
144 + self.unresolved_imports.len()
145 + self.unlisted_dependencies.len()
146 + self.duplicate_exports.len()
147 + self.type_only_dependencies.len()
148 + self.test_only_dependencies.len()
149 + self.circular_dependencies.len()
150 + self.boundary_violations.len()
151 + self.stale_suppressions.len()
152 + self.unused_catalog_entries.len()
153 + self.unresolved_catalog_references.len()
154 }
155
156 #[must_use]
158 pub const fn has_issues(&self) -> bool {
159 self.total_issues() > 0
160 }
161
162 pub fn sort(&mut self) {
169 self.unused_files.sort_by(|a, b| a.path.cmp(&b.path));
170
171 self.unused_exports.sort_by(|a, b| {
172 a.path
173 .cmp(&b.path)
174 .then(a.line.cmp(&b.line))
175 .then(a.export_name.cmp(&b.export_name))
176 });
177
178 self.unused_types.sort_by(|a, b| {
179 a.path
180 .cmp(&b.path)
181 .then(a.line.cmp(&b.line))
182 .then(a.export_name.cmp(&b.export_name))
183 });
184
185 self.private_type_leaks.sort_by(|a, b| {
186 a.path
187 .cmp(&b.path)
188 .then(a.line.cmp(&b.line))
189 .then(a.export_name.cmp(&b.export_name))
190 .then(a.type_name.cmp(&b.type_name))
191 });
192
193 self.unused_dependencies.sort_by(|a, b| {
194 a.path
195 .cmp(&b.path)
196 .then(a.line.cmp(&b.line))
197 .then(a.package_name.cmp(&b.package_name))
198 });
199
200 self.unused_dev_dependencies.sort_by(|a, b| {
201 a.path
202 .cmp(&b.path)
203 .then(a.line.cmp(&b.line))
204 .then(a.package_name.cmp(&b.package_name))
205 });
206
207 self.unused_optional_dependencies.sort_by(|a, b| {
208 a.path
209 .cmp(&b.path)
210 .then(a.line.cmp(&b.line))
211 .then(a.package_name.cmp(&b.package_name))
212 });
213
214 self.unused_enum_members.sort_by(|a, b| {
215 a.path
216 .cmp(&b.path)
217 .then(a.line.cmp(&b.line))
218 .then(a.parent_name.cmp(&b.parent_name))
219 .then(a.member_name.cmp(&b.member_name))
220 });
221
222 self.unused_class_members.sort_by(|a, b| {
223 a.path
224 .cmp(&b.path)
225 .then(a.line.cmp(&b.line))
226 .then(a.parent_name.cmp(&b.parent_name))
227 .then(a.member_name.cmp(&b.member_name))
228 });
229
230 self.unresolved_imports.sort_by(|a, b| {
231 a.path
232 .cmp(&b.path)
233 .then(a.line.cmp(&b.line))
234 .then(a.col.cmp(&b.col))
235 .then(a.specifier.cmp(&b.specifier))
236 });
237
238 self.unlisted_dependencies
239 .sort_by(|a, b| a.package_name.cmp(&b.package_name));
240 for dep in &mut self.unlisted_dependencies {
241 dep.imported_from
242 .sort_by(|a, b| a.path.cmp(&b.path).then(a.line.cmp(&b.line)));
243 }
244
245 self.duplicate_exports
246 .sort_by(|a, b| a.export_name.cmp(&b.export_name));
247 for dup in &mut self.duplicate_exports {
248 dup.locations
249 .sort_by(|a, b| a.path.cmp(&b.path).then(a.line.cmp(&b.line)));
250 }
251
252 self.type_only_dependencies.sort_by(|a, b| {
253 a.path
254 .cmp(&b.path)
255 .then(a.line.cmp(&b.line))
256 .then(a.package_name.cmp(&b.package_name))
257 });
258
259 self.test_only_dependencies.sort_by(|a, b| {
260 a.path
261 .cmp(&b.path)
262 .then(a.line.cmp(&b.line))
263 .then(a.package_name.cmp(&b.package_name))
264 });
265
266 self.circular_dependencies
267 .sort_by(|a, b| a.files.cmp(&b.files).then(a.length.cmp(&b.length)));
268
269 self.boundary_violations.sort_by(|a, b| {
270 a.from_path
271 .cmp(&b.from_path)
272 .then(a.line.cmp(&b.line))
273 .then(a.col.cmp(&b.col))
274 .then(a.to_path.cmp(&b.to_path))
275 });
276
277 self.stale_suppressions.sort_by(|a, b| {
278 a.path
279 .cmp(&b.path)
280 .then(a.line.cmp(&b.line))
281 .then(a.col.cmp(&b.col))
282 });
283
284 self.unused_catalog_entries.sort_by(|a, b| {
285 a.path
286 .cmp(&b.path)
287 .then_with(|| {
288 catalog_sort_key(&a.catalog_name).cmp(&catalog_sort_key(&b.catalog_name))
289 })
290 .then(a.catalog_name.cmp(&b.catalog_name))
291 .then(a.entry_name.cmp(&b.entry_name))
292 });
293 for entry in &mut self.unused_catalog_entries {
294 entry.hardcoded_consumers.sort();
295 entry.hardcoded_consumers.dedup();
296 }
297
298 self.unresolved_catalog_references.sort_by(|a, b| {
299 a.path
300 .cmp(&b.path)
301 .then(a.line.cmp(&b.line))
302 .then_with(|| {
303 catalog_sort_key(&a.catalog_name).cmp(&catalog_sort_key(&b.catalog_name))
304 })
305 .then(a.catalog_name.cmp(&b.catalog_name))
306 .then(a.entry_name.cmp(&b.entry_name))
307 });
308 for finding in &mut self.unresolved_catalog_references {
309 finding.available_in_catalogs.sort();
310 finding.available_in_catalogs.dedup();
311 }
312
313 self.feature_flags.sort_by(|a, b| {
314 a.path
315 .cmp(&b.path)
316 .then(a.line.cmp(&b.line))
317 .then(a.flag_name.cmp(&b.flag_name))
318 });
319
320 for usage in &mut self.export_usages {
321 usage.reference_locations.sort_by(|a, b| {
322 a.path
323 .cmp(&b.path)
324 .then(a.line.cmp(&b.line))
325 .then(a.col.cmp(&b.col))
326 });
327 }
328 self.export_usages.sort_by(|a, b| {
329 a.path
330 .cmp(&b.path)
331 .then(a.line.cmp(&b.line))
332 .then(a.export_name.cmp(&b.export_name))
333 });
334 }
335}
336
337fn catalog_sort_key(name: &str) -> (u8, &str) {
339 if name == "default" {
340 (0, name)
341 } else {
342 (1, name)
343 }
344}
345
346#[derive(Debug, Clone, Serialize)]
348pub struct UnusedFile {
349 #[serde(serialize_with = "serde_path::serialize")]
351 pub path: PathBuf,
352}
353
354#[derive(Debug, Clone, Serialize)]
356pub struct UnusedExport {
357 #[serde(serialize_with = "serde_path::serialize")]
359 pub path: PathBuf,
360 pub export_name: String,
362 pub is_type_only: bool,
364 pub line: u32,
366 pub col: u32,
368 pub span_start: u32,
370 pub is_re_export: bool,
372}
373
374#[derive(Debug, Clone, Serialize)]
376pub struct PrivateTypeLeak {
377 #[serde(serialize_with = "serde_path::serialize")]
379 pub path: PathBuf,
380 pub export_name: String,
382 pub type_name: String,
384 pub line: u32,
386 pub col: u32,
388 pub span_start: u32,
390}
391
392#[derive(Debug, Clone, Serialize)]
394pub struct UnusedDependency {
395 pub package_name: String,
397 pub location: DependencyLocation,
399 #[serde(serialize_with = "serde_path::serialize")]
402 pub path: PathBuf,
403 pub line: u32,
405 #[serde(
407 serialize_with = "serde_path::serialize_vec",
408 skip_serializing_if = "Vec::is_empty"
409 )]
410 pub used_in_workspaces: Vec<PathBuf>,
411}
412
413#[derive(Debug, Clone, Serialize)]
430#[serde(rename_all = "camelCase")]
431pub enum DependencyLocation {
432 Dependencies,
434 DevDependencies,
436 OptionalDependencies,
438}
439
440#[derive(Debug, Clone, Serialize)]
442pub struct UnusedMember {
443 #[serde(serialize_with = "serde_path::serialize")]
445 pub path: PathBuf,
446 pub parent_name: String,
448 pub member_name: String,
450 pub kind: MemberKind,
452 pub line: u32,
454 pub col: u32,
456}
457
458#[derive(Debug, Clone, Serialize)]
460pub struct UnresolvedImport {
461 #[serde(serialize_with = "serde_path::serialize")]
463 pub path: PathBuf,
464 pub specifier: String,
466 pub line: u32,
468 pub col: u32,
470 pub specifier_col: u32,
473}
474
475#[derive(Debug, Clone, Serialize)]
477pub struct UnlistedDependency {
478 pub package_name: String,
480 pub imported_from: Vec<ImportSite>,
482}
483
484#[derive(Debug, Clone, Serialize)]
486pub struct ImportSite {
487 #[serde(serialize_with = "serde_path::serialize")]
489 pub path: PathBuf,
490 pub line: u32,
492 pub col: u32,
494}
495
496#[derive(Debug, Clone, Serialize)]
498pub struct DuplicateExport {
499 pub export_name: String,
501 pub locations: Vec<DuplicateLocation>,
503}
504
505#[derive(Debug, Clone, Serialize)]
507pub struct DuplicateLocation {
508 #[serde(serialize_with = "serde_path::serialize")]
510 pub path: PathBuf,
511 pub line: u32,
513 pub col: u32,
515}
516
517#[derive(Debug, Clone, Serialize)]
521pub struct TypeOnlyDependency {
522 pub package_name: String,
524 #[serde(serialize_with = "serde_path::serialize")]
526 pub path: PathBuf,
527 pub line: u32,
529}
530
531#[derive(Debug, Clone, Serialize)]
537pub struct UnusedCatalogEntry {
538 pub entry_name: String,
540 pub catalog_name: String,
543 #[serde(serialize_with = "serde_path::serialize")]
545 pub path: PathBuf,
546 pub line: u32,
548 #[serde(
553 default,
554 serialize_with = "serde_path::serialize_vec",
555 skip_serializing_if = "Vec::is_empty"
556 )]
557 pub hardcoded_consumers: Vec<PathBuf>,
558}
559
560#[derive(Debug, Clone, Serialize)]
571pub struct UnresolvedCatalogReference {
572 pub entry_name: String,
574 pub catalog_name: String,
577 #[serde(serialize_with = "serde_path::serialize")]
584 pub path: PathBuf,
585 pub line: u32,
587 #[serde(default, skip_serializing_if = "Vec::is_empty")]
592 pub available_in_catalogs: Vec<String>,
593}
594
595#[derive(Debug, Clone, Serialize)]
598pub struct TestOnlyDependency {
599 pub package_name: String,
601 #[serde(serialize_with = "serde_path::serialize")]
603 pub path: PathBuf,
604 pub line: u32,
606}
607
608#[derive(Debug, Clone, Serialize, Deserialize)]
610pub struct CircularDependency {
611 #[serde(serialize_with = "serde_path::serialize_vec")]
613 pub files: Vec<PathBuf>,
614 pub length: usize,
616 #[serde(default)]
618 pub line: u32,
619 #[serde(default)]
621 pub col: u32,
622 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
624 pub is_cross_package: bool,
625}
626
627#[derive(Debug, Clone, Serialize)]
629pub struct BoundaryViolation {
630 #[serde(serialize_with = "serde_path::serialize")]
632 pub from_path: PathBuf,
633 #[serde(serialize_with = "serde_path::serialize")]
635 pub to_path: PathBuf,
636 pub from_zone: String,
638 pub to_zone: String,
640 pub import_specifier: String,
642 pub line: u32,
644 pub col: u32,
646}
647
648#[derive(Debug, Clone, Serialize)]
650#[serde(rename_all = "snake_case", tag = "type")]
651pub enum SuppressionOrigin {
652 Comment {
654 #[serde(skip_serializing_if = "Option::is_none")]
656 issue_kind: Option<String>,
657 is_file_level: bool,
659 },
660 JsdocTag {
662 export_name: String,
664 },
665}
666
667#[derive(Debug, Clone, Serialize)]
669pub struct StaleSuppression {
670 #[serde(serialize_with = "serde_path::serialize")]
672 pub path: PathBuf,
673 pub line: u32,
675 pub col: u32,
677 pub origin: SuppressionOrigin,
679}
680
681impl StaleSuppression {
682 #[must_use]
684 pub fn description(&self) -> String {
685 match &self.origin {
686 SuppressionOrigin::Comment {
687 issue_kind,
688 is_file_level,
689 } => {
690 let directive = if *is_file_level {
691 "fallow-ignore-file"
692 } else {
693 "fallow-ignore-next-line"
694 };
695 match issue_kind {
696 Some(kind) => format!("// {directive} {kind}"),
697 None => format!("// {directive}"),
698 }
699 }
700 SuppressionOrigin::JsdocTag { export_name } => {
701 format!("@expected-unused on {export_name}")
702 }
703 }
704 }
705
706 #[must_use]
708 pub fn explanation(&self) -> String {
709 match &self.origin {
710 SuppressionOrigin::Comment {
711 issue_kind,
712 is_file_level,
713 } => {
714 let scope = if *is_file_level {
715 "in this file"
716 } else {
717 "on the next line"
718 };
719 match issue_kind {
720 Some(kind) => format!("no {kind} issue found {scope}"),
721 None => format!("no issues found {scope}"),
722 }
723 }
724 SuppressionOrigin::JsdocTag { export_name } => {
725 format!("{export_name} is now used")
726 }
727 }
728 }
729
730 #[must_use]
732 pub fn suppressed_kind(&self) -> Option<IssueKind> {
733 match &self.origin {
734 SuppressionOrigin::Comment { issue_kind, .. } => {
735 issue_kind.as_deref().and_then(IssueKind::parse)
736 }
737 SuppressionOrigin::JsdocTag { .. } => None,
738 }
739 }
740}
741
742#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
744#[serde(rename_all = "snake_case")]
745pub enum FlagKind {
746 EnvironmentVariable,
748 SdkCall,
750 ConfigObject,
752}
753
754#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
756#[serde(rename_all = "snake_case")]
757pub enum FlagConfidence {
758 Low,
760 Medium,
762 High,
764}
765
766#[derive(Debug, Clone, Serialize)]
768pub struct FeatureFlag {
769 #[serde(serialize_with = "serde_path::serialize")]
771 pub path: PathBuf,
772 pub flag_name: String,
774 pub kind: FlagKind,
776 pub confidence: FlagConfidence,
778 pub line: u32,
780 pub col: u32,
782 #[serde(skip)]
784 pub guard_span_start: Option<u32>,
785 #[serde(skip)]
787 pub guard_span_end: Option<u32>,
788 #[serde(skip_serializing_if = "Option::is_none")]
790 pub sdk_name: Option<String>,
791 #[serde(skip)]
794 pub guard_line_start: Option<u32>,
795 #[serde(skip)]
797 pub guard_line_end: Option<u32>,
798 #[serde(skip_serializing_if = "Vec::is_empty")]
801 pub guarded_dead_exports: Vec<String>,
802}
803
804const _: () = assert!(std::mem::size_of::<FeatureFlag>() <= 160);
806
807#[derive(Debug, Clone, Serialize)]
810pub struct ExportUsage {
811 #[serde(serialize_with = "serde_path::serialize")]
813 pub path: PathBuf,
814 pub export_name: String,
816 pub line: u32,
818 pub col: u32,
820 pub reference_count: usize,
822 pub reference_locations: Vec<ReferenceLocation>,
825}
826
827#[derive(Debug, Clone, Serialize)]
829pub struct ReferenceLocation {
830 #[serde(serialize_with = "serde_path::serialize")]
832 pub path: PathBuf,
833 pub line: u32,
835 pub col: u32,
837}
838
839#[cfg(test)]
840mod tests {
841 use super::*;
842
843 #[test]
844 fn empty_results_no_issues() {
845 let results = AnalysisResults::default();
846 assert_eq!(results.total_issues(), 0);
847 assert!(!results.has_issues());
848 }
849
850 #[test]
851 fn results_with_unused_file() {
852 let mut results = AnalysisResults::default();
853 results.unused_files.push(UnusedFile {
854 path: PathBuf::from("test.ts"),
855 });
856 assert_eq!(results.total_issues(), 1);
857 assert!(results.has_issues());
858 }
859
860 #[test]
861 fn results_with_unused_export() {
862 let mut results = AnalysisResults::default();
863 results.unused_exports.push(UnusedExport {
864 path: PathBuf::from("test.ts"),
865 export_name: "foo".to_string(),
866 is_type_only: false,
867 line: 1,
868 col: 0,
869 span_start: 0,
870 is_re_export: false,
871 });
872 assert_eq!(results.total_issues(), 1);
873 assert!(results.has_issues());
874 }
875
876 #[test]
877 fn results_total_counts_all_types() {
878 let mut results = AnalysisResults::default();
879 results.unused_files.push(UnusedFile {
880 path: PathBuf::from("a.ts"),
881 });
882 results.unused_exports.push(UnusedExport {
883 path: PathBuf::from("b.ts"),
884 export_name: "x".to_string(),
885 is_type_only: false,
886 line: 1,
887 col: 0,
888 span_start: 0,
889 is_re_export: false,
890 });
891 results.unused_types.push(UnusedExport {
892 path: PathBuf::from("c.ts"),
893 export_name: "T".to_string(),
894 is_type_only: true,
895 line: 1,
896 col: 0,
897 span_start: 0,
898 is_re_export: false,
899 });
900 results.unused_dependencies.push(UnusedDependency {
901 package_name: "dep".to_string(),
902 location: DependencyLocation::Dependencies,
903 path: PathBuf::from("package.json"),
904 line: 5,
905 used_in_workspaces: Vec::new(),
906 });
907 results.unused_dev_dependencies.push(UnusedDependency {
908 package_name: "dev".to_string(),
909 location: DependencyLocation::DevDependencies,
910 path: PathBuf::from("package.json"),
911 line: 5,
912 used_in_workspaces: Vec::new(),
913 });
914 results.unused_enum_members.push(UnusedMember {
915 path: PathBuf::from("d.ts"),
916 parent_name: "E".to_string(),
917 member_name: "A".to_string(),
918 kind: MemberKind::EnumMember,
919 line: 1,
920 col: 0,
921 });
922 results.unused_class_members.push(UnusedMember {
923 path: PathBuf::from("e.ts"),
924 parent_name: "C".to_string(),
925 member_name: "m".to_string(),
926 kind: MemberKind::ClassMethod,
927 line: 1,
928 col: 0,
929 });
930 results.unresolved_imports.push(UnresolvedImport {
931 path: PathBuf::from("f.ts"),
932 specifier: "./missing".to_string(),
933 line: 1,
934 col: 0,
935 specifier_col: 0,
936 });
937 results.unlisted_dependencies.push(UnlistedDependency {
938 package_name: "unlisted".to_string(),
939 imported_from: vec![ImportSite {
940 path: PathBuf::from("g.ts"),
941 line: 1,
942 col: 0,
943 }],
944 });
945 results.duplicate_exports.push(DuplicateExport {
946 export_name: "dup".to_string(),
947 locations: vec![
948 DuplicateLocation {
949 path: PathBuf::from("h.ts"),
950 line: 15,
951 col: 0,
952 },
953 DuplicateLocation {
954 path: PathBuf::from("i.ts"),
955 line: 30,
956 col: 0,
957 },
958 ],
959 });
960 results.unused_optional_dependencies.push(UnusedDependency {
961 package_name: "optional".to_string(),
962 location: DependencyLocation::OptionalDependencies,
963 path: PathBuf::from("package.json"),
964 line: 5,
965 used_in_workspaces: Vec::new(),
966 });
967 results.type_only_dependencies.push(TypeOnlyDependency {
968 package_name: "type-only".to_string(),
969 path: PathBuf::from("package.json"),
970 line: 8,
971 });
972 results.test_only_dependencies.push(TestOnlyDependency {
973 package_name: "test-only".to_string(),
974 path: PathBuf::from("package.json"),
975 line: 9,
976 });
977 results.circular_dependencies.push(CircularDependency {
978 files: vec![PathBuf::from("a.ts"), PathBuf::from("b.ts")],
979 length: 2,
980 line: 3,
981 col: 0,
982 is_cross_package: false,
983 });
984 results.boundary_violations.push(BoundaryViolation {
985 from_path: PathBuf::from("src/ui/Button.tsx"),
986 to_path: PathBuf::from("src/db/queries.ts"),
987 from_zone: "ui".to_string(),
988 to_zone: "database".to_string(),
989 import_specifier: "../db/queries".to_string(),
990 line: 3,
991 col: 0,
992 });
993
994 assert_eq!(results.total_issues(), 15);
996 assert!(results.has_issues());
997 }
998
999 #[test]
1002 fn total_issues_and_has_issues_are_consistent() {
1003 let results = AnalysisResults::default();
1004 assert_eq!(results.total_issues(), 0);
1005 assert!(!results.has_issues());
1006 assert_eq!(results.total_issues() > 0, results.has_issues());
1007 }
1008
1009 #[test]
1012 fn total_issues_sums_all_categories_independently() {
1013 let mut results = AnalysisResults::default();
1014 results.unused_files.push(UnusedFile {
1015 path: PathBuf::from("a.ts"),
1016 });
1017 assert_eq!(results.total_issues(), 1);
1018
1019 results.unused_files.push(UnusedFile {
1020 path: PathBuf::from("b.ts"),
1021 });
1022 assert_eq!(results.total_issues(), 2);
1023
1024 results.unresolved_imports.push(UnresolvedImport {
1025 path: PathBuf::from("c.ts"),
1026 specifier: "./missing".to_string(),
1027 line: 1,
1028 col: 0,
1029 specifier_col: 0,
1030 });
1031 assert_eq!(results.total_issues(), 3);
1032 }
1033
1034 #[test]
1037 fn default_results_all_fields_empty() {
1038 let r = AnalysisResults::default();
1039 assert!(r.unused_files.is_empty());
1040 assert!(r.unused_exports.is_empty());
1041 assert!(r.unused_types.is_empty());
1042 assert!(r.unused_dependencies.is_empty());
1043 assert!(r.unused_dev_dependencies.is_empty());
1044 assert!(r.unused_optional_dependencies.is_empty());
1045 assert!(r.unused_enum_members.is_empty());
1046 assert!(r.unused_class_members.is_empty());
1047 assert!(r.unresolved_imports.is_empty());
1048 assert!(r.unlisted_dependencies.is_empty());
1049 assert!(r.duplicate_exports.is_empty());
1050 assert!(r.type_only_dependencies.is_empty());
1051 assert!(r.test_only_dependencies.is_empty());
1052 assert!(r.circular_dependencies.is_empty());
1053 assert!(r.boundary_violations.is_empty());
1054 assert!(r.unused_catalog_entries.is_empty());
1055 assert!(r.unresolved_catalog_references.is_empty());
1056 assert!(r.export_usages.is_empty());
1057 }
1058
1059 #[test]
1062 fn entry_point_summary_default() {
1063 let summary = EntryPointSummary::default();
1064 assert_eq!(summary.total, 0);
1065 assert!(summary.by_source.is_empty());
1066 }
1067
1068 #[test]
1069 fn entry_point_summary_not_in_default_results() {
1070 let r = AnalysisResults::default();
1071 assert!(r.entry_point_summary.is_none());
1072 }
1073
1074 #[test]
1075 fn entry_point_summary_some_preserves_data() {
1076 let r = AnalysisResults {
1077 entry_point_summary: Some(EntryPointSummary {
1078 total: 5,
1079 by_source: vec![("package.json".to_string(), 2), ("plugin".to_string(), 3)],
1080 }),
1081 ..AnalysisResults::default()
1082 };
1083 let summary = r.entry_point_summary.as_ref().unwrap();
1084 assert_eq!(summary.total, 5);
1085 assert_eq!(summary.by_source.len(), 2);
1086 assert_eq!(summary.by_source[0], ("package.json".to_string(), 2));
1087 }
1088
1089 #[test]
1092 fn sort_unused_files_by_path() {
1093 let mut r = AnalysisResults::default();
1094 r.unused_files.push(UnusedFile {
1095 path: PathBuf::from("z.ts"),
1096 });
1097 r.unused_files.push(UnusedFile {
1098 path: PathBuf::from("a.ts"),
1099 });
1100 r.unused_files.push(UnusedFile {
1101 path: PathBuf::from("m.ts"),
1102 });
1103 r.sort();
1104 let paths: Vec<_> = r
1105 .unused_files
1106 .iter()
1107 .map(|f| f.path.to_string_lossy().to_string())
1108 .collect();
1109 assert_eq!(paths, vec!["a.ts", "m.ts", "z.ts"]);
1110 }
1111
1112 #[test]
1115 fn sort_unused_exports_by_path_line_name() {
1116 let mut r = AnalysisResults::default();
1117 let mk = |path: &str, line: u32, name: &str| UnusedExport {
1118 path: PathBuf::from(path),
1119 export_name: name.to_string(),
1120 is_type_only: false,
1121 line,
1122 col: 0,
1123 span_start: 0,
1124 is_re_export: false,
1125 };
1126 r.unused_exports.push(mk("b.ts", 5, "beta"));
1127 r.unused_exports.push(mk("a.ts", 10, "zeta"));
1128 r.unused_exports.push(mk("a.ts", 10, "alpha"));
1129 r.unused_exports.push(mk("a.ts", 1, "gamma"));
1130 r.sort();
1131 let keys: Vec<_> = r
1132 .unused_exports
1133 .iter()
1134 .map(|e| format!("{}:{}:{}", e.path.to_string_lossy(), e.line, e.export_name))
1135 .collect();
1136 assert_eq!(
1137 keys,
1138 vec![
1139 "a.ts:1:gamma",
1140 "a.ts:10:alpha",
1141 "a.ts:10:zeta",
1142 "b.ts:5:beta"
1143 ]
1144 );
1145 }
1146
1147 #[test]
1150 fn sort_unused_types_by_path_line_name() {
1151 let mut r = AnalysisResults::default();
1152 let mk = |path: &str, line: u32, name: &str| UnusedExport {
1153 path: PathBuf::from(path),
1154 export_name: name.to_string(),
1155 is_type_only: true,
1156 line,
1157 col: 0,
1158 span_start: 0,
1159 is_re_export: false,
1160 };
1161 r.unused_types.push(mk("z.ts", 1, "Z"));
1162 r.unused_types.push(mk("a.ts", 1, "A"));
1163 r.sort();
1164 assert_eq!(r.unused_types[0].path, PathBuf::from("a.ts"));
1165 assert_eq!(r.unused_types[1].path, PathBuf::from("z.ts"));
1166 }
1167
1168 #[test]
1171 fn sort_unused_dependencies_by_path_line_name() {
1172 let mut r = AnalysisResults::default();
1173 let mk = |path: &str, line: u32, name: &str| UnusedDependency {
1174 package_name: name.to_string(),
1175 location: DependencyLocation::Dependencies,
1176 path: PathBuf::from(path),
1177 line,
1178 used_in_workspaces: Vec::new(),
1179 };
1180 r.unused_dependencies.push(mk("b/package.json", 3, "zlib"));
1181 r.unused_dependencies.push(mk("a/package.json", 5, "react"));
1182 r.unused_dependencies.push(mk("a/package.json", 5, "axios"));
1183 r.sort();
1184 let names: Vec<_> = r
1185 .unused_dependencies
1186 .iter()
1187 .map(|d| d.package_name.as_str())
1188 .collect();
1189 assert_eq!(names, vec!["axios", "react", "zlib"]);
1190 }
1191
1192 #[test]
1195 fn sort_unused_dev_dependencies() {
1196 let mut r = AnalysisResults::default();
1197 r.unused_dev_dependencies.push(UnusedDependency {
1198 package_name: "vitest".to_string(),
1199 location: DependencyLocation::DevDependencies,
1200 path: PathBuf::from("package.json"),
1201 line: 10,
1202 used_in_workspaces: Vec::new(),
1203 });
1204 r.unused_dev_dependencies.push(UnusedDependency {
1205 package_name: "jest".to_string(),
1206 location: DependencyLocation::DevDependencies,
1207 path: PathBuf::from("package.json"),
1208 line: 5,
1209 used_in_workspaces: Vec::new(),
1210 });
1211 r.sort();
1212 assert_eq!(r.unused_dev_dependencies[0].package_name, "jest");
1213 assert_eq!(r.unused_dev_dependencies[1].package_name, "vitest");
1214 }
1215
1216 #[test]
1219 fn sort_unused_optional_dependencies() {
1220 let mut r = AnalysisResults::default();
1221 r.unused_optional_dependencies.push(UnusedDependency {
1222 package_name: "zod".to_string(),
1223 location: DependencyLocation::OptionalDependencies,
1224 path: PathBuf::from("package.json"),
1225 line: 3,
1226 used_in_workspaces: Vec::new(),
1227 });
1228 r.unused_optional_dependencies.push(UnusedDependency {
1229 package_name: "ajv".to_string(),
1230 location: DependencyLocation::OptionalDependencies,
1231 path: PathBuf::from("package.json"),
1232 line: 2,
1233 used_in_workspaces: Vec::new(),
1234 });
1235 r.sort();
1236 assert_eq!(r.unused_optional_dependencies[0].package_name, "ajv");
1237 assert_eq!(r.unused_optional_dependencies[1].package_name, "zod");
1238 }
1239
1240 #[test]
1243 fn sort_unused_enum_members_by_path_line_parent_member() {
1244 let mut r = AnalysisResults::default();
1245 let mk = |path: &str, line: u32, parent: &str, member: &str| UnusedMember {
1246 path: PathBuf::from(path),
1247 parent_name: parent.to_string(),
1248 member_name: member.to_string(),
1249 kind: MemberKind::EnumMember,
1250 line,
1251 col: 0,
1252 };
1253 r.unused_enum_members.push(mk("a.ts", 5, "Status", "Z"));
1254 r.unused_enum_members.push(mk("a.ts", 5, "Status", "A"));
1255 r.unused_enum_members.push(mk("a.ts", 1, "Direction", "Up"));
1256 r.sort();
1257 let keys: Vec<_> = r
1258 .unused_enum_members
1259 .iter()
1260 .map(|m| format!("{}:{}", m.parent_name, m.member_name))
1261 .collect();
1262 assert_eq!(keys, vec!["Direction:Up", "Status:A", "Status:Z"]);
1263 }
1264
1265 #[test]
1268 fn sort_unused_class_members() {
1269 let mut r = AnalysisResults::default();
1270 let mk = |path: &str, line: u32, parent: &str, member: &str| UnusedMember {
1271 path: PathBuf::from(path),
1272 parent_name: parent.to_string(),
1273 member_name: member.to_string(),
1274 kind: MemberKind::ClassMethod,
1275 line,
1276 col: 0,
1277 };
1278 r.unused_class_members.push(mk("b.ts", 1, "Foo", "z"));
1279 r.unused_class_members.push(mk("a.ts", 1, "Bar", "a"));
1280 r.sort();
1281 assert_eq!(r.unused_class_members[0].path, PathBuf::from("a.ts"));
1282 assert_eq!(r.unused_class_members[1].path, PathBuf::from("b.ts"));
1283 }
1284
1285 #[test]
1288 fn sort_unresolved_imports_by_path_line_col_specifier() {
1289 let mut r = AnalysisResults::default();
1290 let mk = |path: &str, line: u32, col: u32, spec: &str| UnresolvedImport {
1291 path: PathBuf::from(path),
1292 specifier: spec.to_string(),
1293 line,
1294 col,
1295 specifier_col: 0,
1296 };
1297 r.unresolved_imports.push(mk("a.ts", 5, 0, "./z"));
1298 r.unresolved_imports.push(mk("a.ts", 5, 0, "./a"));
1299 r.unresolved_imports.push(mk("a.ts", 1, 0, "./m"));
1300 r.sort();
1301 let specs: Vec<_> = r
1302 .unresolved_imports
1303 .iter()
1304 .map(|i| i.specifier.as_str())
1305 .collect();
1306 assert_eq!(specs, vec!["./m", "./a", "./z"]);
1307 }
1308
1309 #[test]
1312 fn sort_unlisted_dependencies_by_name_and_inner_sites() {
1313 let mut r = AnalysisResults::default();
1314 r.unlisted_dependencies.push(UnlistedDependency {
1315 package_name: "zod".to_string(),
1316 imported_from: vec![
1317 ImportSite {
1318 path: PathBuf::from("b.ts"),
1319 line: 10,
1320 col: 0,
1321 },
1322 ImportSite {
1323 path: PathBuf::from("a.ts"),
1324 line: 1,
1325 col: 0,
1326 },
1327 ],
1328 });
1329 r.unlisted_dependencies.push(UnlistedDependency {
1330 package_name: "axios".to_string(),
1331 imported_from: vec![ImportSite {
1332 path: PathBuf::from("c.ts"),
1333 line: 1,
1334 col: 0,
1335 }],
1336 });
1337 r.sort();
1338
1339 assert_eq!(r.unlisted_dependencies[0].package_name, "axios");
1341 assert_eq!(r.unlisted_dependencies[1].package_name, "zod");
1342
1343 let zod_sites: Vec<_> = r.unlisted_dependencies[1]
1345 .imported_from
1346 .iter()
1347 .map(|s| s.path.to_string_lossy().to_string())
1348 .collect();
1349 assert_eq!(zod_sites, vec!["a.ts", "b.ts"]);
1350 }
1351
1352 #[test]
1355 fn sort_duplicate_exports_by_name_and_inner_locations() {
1356 let mut r = AnalysisResults::default();
1357 r.duplicate_exports.push(DuplicateExport {
1358 export_name: "z".to_string(),
1359 locations: vec![
1360 DuplicateLocation {
1361 path: PathBuf::from("c.ts"),
1362 line: 1,
1363 col: 0,
1364 },
1365 DuplicateLocation {
1366 path: PathBuf::from("a.ts"),
1367 line: 5,
1368 col: 0,
1369 },
1370 ],
1371 });
1372 r.duplicate_exports.push(DuplicateExport {
1373 export_name: "a".to_string(),
1374 locations: vec![DuplicateLocation {
1375 path: PathBuf::from("b.ts"),
1376 line: 1,
1377 col: 0,
1378 }],
1379 });
1380 r.sort();
1381
1382 assert_eq!(r.duplicate_exports[0].export_name, "a");
1384 assert_eq!(r.duplicate_exports[1].export_name, "z");
1385
1386 let z_locs: Vec<_> = r.duplicate_exports[1]
1388 .locations
1389 .iter()
1390 .map(|l| l.path.to_string_lossy().to_string())
1391 .collect();
1392 assert_eq!(z_locs, vec!["a.ts", "c.ts"]);
1393 }
1394
1395 #[test]
1398 fn sort_type_only_dependencies() {
1399 let mut r = AnalysisResults::default();
1400 r.type_only_dependencies.push(TypeOnlyDependency {
1401 package_name: "zod".to_string(),
1402 path: PathBuf::from("package.json"),
1403 line: 10,
1404 });
1405 r.type_only_dependencies.push(TypeOnlyDependency {
1406 package_name: "ajv".to_string(),
1407 path: PathBuf::from("package.json"),
1408 line: 5,
1409 });
1410 r.sort();
1411 assert_eq!(r.type_only_dependencies[0].package_name, "ajv");
1412 assert_eq!(r.type_only_dependencies[1].package_name, "zod");
1413 }
1414
1415 #[test]
1418 fn sort_test_only_dependencies() {
1419 let mut r = AnalysisResults::default();
1420 r.test_only_dependencies.push(TestOnlyDependency {
1421 package_name: "vitest".to_string(),
1422 path: PathBuf::from("package.json"),
1423 line: 15,
1424 });
1425 r.test_only_dependencies.push(TestOnlyDependency {
1426 package_name: "jest".to_string(),
1427 path: PathBuf::from("package.json"),
1428 line: 10,
1429 });
1430 r.sort();
1431 assert_eq!(r.test_only_dependencies[0].package_name, "jest");
1432 assert_eq!(r.test_only_dependencies[1].package_name, "vitest");
1433 }
1434
1435 #[test]
1438 fn sort_circular_dependencies_by_files_then_length() {
1439 let mut r = AnalysisResults::default();
1440 r.circular_dependencies.push(CircularDependency {
1441 files: vec![PathBuf::from("b.ts"), PathBuf::from("c.ts")],
1442 length: 2,
1443 line: 1,
1444 col: 0,
1445 is_cross_package: false,
1446 });
1447 r.circular_dependencies.push(CircularDependency {
1448 files: vec![PathBuf::from("a.ts"), PathBuf::from("b.ts")],
1449 length: 2,
1450 line: 1,
1451 col: 0,
1452 is_cross_package: true,
1453 });
1454 r.sort();
1455 assert_eq!(r.circular_dependencies[0].files[0], PathBuf::from("a.ts"));
1456 assert_eq!(r.circular_dependencies[1].files[0], PathBuf::from("b.ts"));
1457 }
1458
1459 #[test]
1462 fn sort_boundary_violations() {
1463 let mut r = AnalysisResults::default();
1464 let mk = |from: &str, line: u32, col: u32, to: &str| BoundaryViolation {
1465 from_path: PathBuf::from(from),
1466 to_path: PathBuf::from(to),
1467 from_zone: "a".to_string(),
1468 to_zone: "b".to_string(),
1469 import_specifier: to.to_string(),
1470 line,
1471 col,
1472 };
1473 r.boundary_violations.push(mk("z.ts", 1, 0, "a.ts"));
1474 r.boundary_violations.push(mk("a.ts", 5, 0, "b.ts"));
1475 r.boundary_violations.push(mk("a.ts", 1, 0, "c.ts"));
1476 r.sort();
1477 let from_paths: Vec<_> = r
1478 .boundary_violations
1479 .iter()
1480 .map(|v| format!("{}:{}", v.from_path.to_string_lossy(), v.line))
1481 .collect();
1482 assert_eq!(from_paths, vec!["a.ts:1", "a.ts:5", "z.ts:1"]);
1483 }
1484
1485 #[test]
1488 fn sort_export_usages_and_inner_reference_locations() {
1489 let mut r = AnalysisResults::default();
1490 r.export_usages.push(ExportUsage {
1491 path: PathBuf::from("z.ts"),
1492 export_name: "foo".to_string(),
1493 line: 1,
1494 col: 0,
1495 reference_count: 2,
1496 reference_locations: vec![
1497 ReferenceLocation {
1498 path: PathBuf::from("c.ts"),
1499 line: 10,
1500 col: 0,
1501 },
1502 ReferenceLocation {
1503 path: PathBuf::from("a.ts"),
1504 line: 5,
1505 col: 0,
1506 },
1507 ],
1508 });
1509 r.export_usages.push(ExportUsage {
1510 path: PathBuf::from("a.ts"),
1511 export_name: "bar".to_string(),
1512 line: 1,
1513 col: 0,
1514 reference_count: 1,
1515 reference_locations: vec![ReferenceLocation {
1516 path: PathBuf::from("b.ts"),
1517 line: 1,
1518 col: 0,
1519 }],
1520 });
1521 r.sort();
1522
1523 assert_eq!(r.export_usages[0].path, PathBuf::from("a.ts"));
1525 assert_eq!(r.export_usages[1].path, PathBuf::from("z.ts"));
1526
1527 let refs: Vec<_> = r.export_usages[1]
1529 .reference_locations
1530 .iter()
1531 .map(|l| l.path.to_string_lossy().to_string())
1532 .collect();
1533 assert_eq!(refs, vec!["a.ts", "c.ts"]);
1534 }
1535
1536 #[test]
1539 fn sort_empty_results_is_noop() {
1540 let mut r = AnalysisResults::default();
1541 r.sort(); assert_eq!(r.total_issues(), 0);
1543 }
1544
1545 #[test]
1548 fn sort_single_element_lists_stable() {
1549 let mut r = AnalysisResults::default();
1550 r.unused_files.push(UnusedFile {
1551 path: PathBuf::from("only.ts"),
1552 });
1553 r.sort();
1554 assert_eq!(r.unused_files[0].path, PathBuf::from("only.ts"));
1555 }
1556
1557 #[test]
1560 fn serialize_empty_results() {
1561 let r = AnalysisResults::default();
1562 let json = serde_json::to_value(&r).unwrap();
1563
1564 assert!(json["unused_files"].as_array().unwrap().is_empty());
1566 assert!(json["unused_exports"].as_array().unwrap().is_empty());
1567 assert!(json["circular_dependencies"].as_array().unwrap().is_empty());
1568
1569 assert!(json.get("export_usages").is_none());
1571 assert!(json.get("entry_point_summary").is_none());
1572 }
1573
1574 #[test]
1575 fn serialize_unused_file_path() {
1576 let r = UnusedFile {
1577 path: PathBuf::from("src/utils/index.ts"),
1578 };
1579 let json = serde_json::to_value(&r).unwrap();
1580 assert_eq!(json["path"], "src/utils/index.ts");
1581 }
1582
1583 #[test]
1584 fn serialize_dependency_location_camel_case() {
1585 let dep = UnusedDependency {
1586 package_name: "react".to_string(),
1587 location: DependencyLocation::DevDependencies,
1588 path: PathBuf::from("package.json"),
1589 line: 5,
1590 used_in_workspaces: Vec::new(),
1591 };
1592 let json = serde_json::to_value(&dep).unwrap();
1593 assert_eq!(json["location"], "devDependencies");
1594
1595 let dep2 = UnusedDependency {
1596 package_name: "react".to_string(),
1597 location: DependencyLocation::Dependencies,
1598 path: PathBuf::from("package.json"),
1599 line: 3,
1600 used_in_workspaces: Vec::new(),
1601 };
1602 let json2 = serde_json::to_value(&dep2).unwrap();
1603 assert_eq!(json2["location"], "dependencies");
1604
1605 let dep3 = UnusedDependency {
1606 package_name: "fsevents".to_string(),
1607 location: DependencyLocation::OptionalDependencies,
1608 path: PathBuf::from("package.json"),
1609 line: 7,
1610 used_in_workspaces: Vec::new(),
1611 };
1612 let json3 = serde_json::to_value(&dep3).unwrap();
1613 assert_eq!(json3["location"], "optionalDependencies");
1614 }
1615
1616 #[test]
1617 fn serialize_circular_dependency_skips_false_cross_package() {
1618 let cd = CircularDependency {
1619 files: vec![PathBuf::from("a.ts"), PathBuf::from("b.ts")],
1620 length: 2,
1621 line: 1,
1622 col: 0,
1623 is_cross_package: false,
1624 };
1625 let json = serde_json::to_value(&cd).unwrap();
1626 assert!(json.get("is_cross_package").is_none());
1628 }
1629
1630 #[test]
1631 fn serialize_circular_dependency_includes_true_cross_package() {
1632 let cd = CircularDependency {
1633 files: vec![PathBuf::from("a.ts"), PathBuf::from("b.ts")],
1634 length: 2,
1635 line: 1,
1636 col: 0,
1637 is_cross_package: true,
1638 };
1639 let json = serde_json::to_value(&cd).unwrap();
1640 assert_eq!(json["is_cross_package"], true);
1641 }
1642
1643 #[test]
1644 fn serialize_unused_export_fields() {
1645 let e = UnusedExport {
1646 path: PathBuf::from("src/mod.ts"),
1647 export_name: "helper".to_string(),
1648 is_type_only: true,
1649 line: 42,
1650 col: 7,
1651 span_start: 100,
1652 is_re_export: true,
1653 };
1654 let json = serde_json::to_value(&e).unwrap();
1655 assert_eq!(json["path"], "src/mod.ts");
1656 assert_eq!(json["export_name"], "helper");
1657 assert_eq!(json["is_type_only"], true);
1658 assert_eq!(json["line"], 42);
1659 assert_eq!(json["col"], 7);
1660 assert_eq!(json["span_start"], 100);
1661 assert_eq!(json["is_re_export"], true);
1662 }
1663
1664 #[test]
1665 fn serialize_boundary_violation_fields() {
1666 let v = BoundaryViolation {
1667 from_path: PathBuf::from("src/ui/button.tsx"),
1668 to_path: PathBuf::from("src/db/queries.ts"),
1669 from_zone: "ui".to_string(),
1670 to_zone: "db".to_string(),
1671 import_specifier: "../db/queries".to_string(),
1672 line: 3,
1673 col: 0,
1674 };
1675 let json = serde_json::to_value(&v).unwrap();
1676 assert_eq!(json["from_path"], "src/ui/button.tsx");
1677 assert_eq!(json["to_path"], "src/db/queries.ts");
1678 assert_eq!(json["from_zone"], "ui");
1679 assert_eq!(json["to_zone"], "db");
1680 assert_eq!(json["import_specifier"], "../db/queries");
1681 }
1682
1683 #[test]
1684 fn serialize_unlisted_dependency_with_import_sites() {
1685 let d = UnlistedDependency {
1686 package_name: "chalk".to_string(),
1687 imported_from: vec![
1688 ImportSite {
1689 path: PathBuf::from("a.ts"),
1690 line: 1,
1691 col: 0,
1692 },
1693 ImportSite {
1694 path: PathBuf::from("b.ts"),
1695 line: 5,
1696 col: 3,
1697 },
1698 ],
1699 };
1700 let json = serde_json::to_value(&d).unwrap();
1701 assert_eq!(json["package_name"], "chalk");
1702 let sites = json["imported_from"].as_array().unwrap();
1703 assert_eq!(sites.len(), 2);
1704 assert_eq!(sites[0]["path"], "a.ts");
1705 assert_eq!(sites[1]["line"], 5);
1706 }
1707
1708 #[test]
1709 fn serialize_duplicate_export_with_locations() {
1710 let d = DuplicateExport {
1711 export_name: "Button".to_string(),
1712 locations: vec![
1713 DuplicateLocation {
1714 path: PathBuf::from("src/a.ts"),
1715 line: 10,
1716 col: 0,
1717 },
1718 DuplicateLocation {
1719 path: PathBuf::from("src/b.ts"),
1720 line: 20,
1721 col: 5,
1722 },
1723 ],
1724 };
1725 let json = serde_json::to_value(&d).unwrap();
1726 assert_eq!(json["export_name"], "Button");
1727 let locs = json["locations"].as_array().unwrap();
1728 assert_eq!(locs.len(), 2);
1729 assert_eq!(locs[0]["line"], 10);
1730 assert_eq!(locs[1]["col"], 5);
1731 }
1732
1733 #[test]
1734 fn serialize_type_only_dependency() {
1735 let d = TypeOnlyDependency {
1736 package_name: "@types/react".to_string(),
1737 path: PathBuf::from("package.json"),
1738 line: 12,
1739 };
1740 let json = serde_json::to_value(&d).unwrap();
1741 assert_eq!(json["package_name"], "@types/react");
1742 assert_eq!(json["line"], 12);
1743 }
1744
1745 #[test]
1746 fn serialize_test_only_dependency() {
1747 let d = TestOnlyDependency {
1748 package_name: "vitest".to_string(),
1749 path: PathBuf::from("package.json"),
1750 line: 8,
1751 };
1752 let json = serde_json::to_value(&d).unwrap();
1753 assert_eq!(json["package_name"], "vitest");
1754 assert_eq!(json["line"], 8);
1755 }
1756
1757 #[test]
1758 fn serialize_unused_member() {
1759 let m = UnusedMember {
1760 path: PathBuf::from("enums.ts"),
1761 parent_name: "Status".to_string(),
1762 member_name: "Pending".to_string(),
1763 kind: MemberKind::EnumMember,
1764 line: 3,
1765 col: 4,
1766 };
1767 let json = serde_json::to_value(&m).unwrap();
1768 assert_eq!(json["parent_name"], "Status");
1769 assert_eq!(json["member_name"], "Pending");
1770 assert_eq!(json["line"], 3);
1771 }
1772
1773 #[test]
1774 fn serialize_unresolved_import() {
1775 let i = UnresolvedImport {
1776 path: PathBuf::from("app.ts"),
1777 specifier: "./missing-module".to_string(),
1778 line: 7,
1779 col: 0,
1780 specifier_col: 21,
1781 };
1782 let json = serde_json::to_value(&i).unwrap();
1783 assert_eq!(json["specifier"], "./missing-module");
1784 assert_eq!(json["specifier_col"], 21);
1785 }
1786
1787 #[test]
1790 fn deserialize_circular_dependency_with_defaults() {
1791 let json = r#"{"files":["a.ts","b.ts"],"length":2}"#;
1793 let cd: CircularDependency = serde_json::from_str(json).unwrap();
1794 assert_eq!(cd.files.len(), 2);
1795 assert_eq!(cd.length, 2);
1796 assert_eq!(cd.line, 0);
1797 assert_eq!(cd.col, 0);
1798 assert!(!cd.is_cross_package);
1799 }
1800
1801 #[test]
1802 fn deserialize_circular_dependency_with_all_fields() {
1803 let json =
1804 r#"{"files":["a.ts","b.ts"],"length":2,"line":5,"col":10,"is_cross_package":true}"#;
1805 let cd: CircularDependency = serde_json::from_str(json).unwrap();
1806 assert_eq!(cd.line, 5);
1807 assert_eq!(cd.col, 10);
1808 assert!(cd.is_cross_package);
1809 }
1810
1811 #[test]
1814 fn clone_results_are_independent() {
1815 let mut r = AnalysisResults::default();
1816 r.unused_files.push(UnusedFile {
1817 path: PathBuf::from("a.ts"),
1818 });
1819 let mut cloned = r.clone();
1820 cloned.unused_files.push(UnusedFile {
1821 path: PathBuf::from("b.ts"),
1822 });
1823 assert_eq!(r.total_issues(), 1);
1824 assert_eq!(cloned.total_issues(), 2);
1825 }
1826
1827 #[test]
1830 fn export_usages_not_counted_in_total_issues() {
1831 let mut r = AnalysisResults::default();
1832 r.export_usages.push(ExportUsage {
1833 path: PathBuf::from("mod.ts"),
1834 export_name: "foo".to_string(),
1835 line: 1,
1836 col: 0,
1837 reference_count: 3,
1838 reference_locations: vec![],
1839 });
1840 assert_eq!(r.total_issues(), 0);
1842 assert!(!r.has_issues());
1843 }
1844
1845 #[test]
1848 fn entry_point_summary_not_counted_in_total_issues() {
1849 let r = AnalysisResults {
1850 entry_point_summary: Some(EntryPointSummary {
1851 total: 10,
1852 by_source: vec![("config".to_string(), 10)],
1853 }),
1854 ..AnalysisResults::default()
1855 };
1856 assert_eq!(r.total_issues(), 0);
1857 assert!(!r.has_issues());
1858 }
1859}