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(skip)]
89 pub suppression_count: usize,
90 #[serde(skip)]
93 pub feature_flags: Vec<FeatureFlag>,
94 #[serde(skip)]
98 pub export_usages: Vec<ExportUsage>,
99 #[serde(skip)]
103 pub entry_point_summary: Option<EntryPointSummary>,
104}
105
106impl AnalysisResults {
107 #[must_use]
131 pub const fn total_issues(&self) -> usize {
132 self.unused_files.len()
133 + self.unused_exports.len()
134 + self.unused_types.len()
135 + self.private_type_leaks.len()
136 + self.unused_dependencies.len()
137 + self.unused_dev_dependencies.len()
138 + self.unused_optional_dependencies.len()
139 + self.unused_enum_members.len()
140 + self.unused_class_members.len()
141 + self.unresolved_imports.len()
142 + self.unlisted_dependencies.len()
143 + self.duplicate_exports.len()
144 + self.type_only_dependencies.len()
145 + self.test_only_dependencies.len()
146 + self.circular_dependencies.len()
147 + self.boundary_violations.len()
148 + self.stale_suppressions.len()
149 + self.unused_catalog_entries.len()
150 }
151
152 #[must_use]
154 pub const fn has_issues(&self) -> bool {
155 self.total_issues() > 0
156 }
157
158 pub fn sort(&mut self) {
165 self.unused_files.sort_by(|a, b| a.path.cmp(&b.path));
166
167 self.unused_exports.sort_by(|a, b| {
168 a.path
169 .cmp(&b.path)
170 .then(a.line.cmp(&b.line))
171 .then(a.export_name.cmp(&b.export_name))
172 });
173
174 self.unused_types.sort_by(|a, b| {
175 a.path
176 .cmp(&b.path)
177 .then(a.line.cmp(&b.line))
178 .then(a.export_name.cmp(&b.export_name))
179 });
180
181 self.private_type_leaks.sort_by(|a, b| {
182 a.path
183 .cmp(&b.path)
184 .then(a.line.cmp(&b.line))
185 .then(a.export_name.cmp(&b.export_name))
186 .then(a.type_name.cmp(&b.type_name))
187 });
188
189 self.unused_dependencies.sort_by(|a, b| {
190 a.path
191 .cmp(&b.path)
192 .then(a.line.cmp(&b.line))
193 .then(a.package_name.cmp(&b.package_name))
194 });
195
196 self.unused_dev_dependencies.sort_by(|a, b| {
197 a.path
198 .cmp(&b.path)
199 .then(a.line.cmp(&b.line))
200 .then(a.package_name.cmp(&b.package_name))
201 });
202
203 self.unused_optional_dependencies.sort_by(|a, b| {
204 a.path
205 .cmp(&b.path)
206 .then(a.line.cmp(&b.line))
207 .then(a.package_name.cmp(&b.package_name))
208 });
209
210 self.unused_enum_members.sort_by(|a, b| {
211 a.path
212 .cmp(&b.path)
213 .then(a.line.cmp(&b.line))
214 .then(a.parent_name.cmp(&b.parent_name))
215 .then(a.member_name.cmp(&b.member_name))
216 });
217
218 self.unused_class_members.sort_by(|a, b| {
219 a.path
220 .cmp(&b.path)
221 .then(a.line.cmp(&b.line))
222 .then(a.parent_name.cmp(&b.parent_name))
223 .then(a.member_name.cmp(&b.member_name))
224 });
225
226 self.unresolved_imports.sort_by(|a, b| {
227 a.path
228 .cmp(&b.path)
229 .then(a.line.cmp(&b.line))
230 .then(a.col.cmp(&b.col))
231 .then(a.specifier.cmp(&b.specifier))
232 });
233
234 self.unlisted_dependencies
235 .sort_by(|a, b| a.package_name.cmp(&b.package_name));
236 for dep in &mut self.unlisted_dependencies {
237 dep.imported_from
238 .sort_by(|a, b| a.path.cmp(&b.path).then(a.line.cmp(&b.line)));
239 }
240
241 self.duplicate_exports
242 .sort_by(|a, b| a.export_name.cmp(&b.export_name));
243 for dup in &mut self.duplicate_exports {
244 dup.locations
245 .sort_by(|a, b| a.path.cmp(&b.path).then(a.line.cmp(&b.line)));
246 }
247
248 self.type_only_dependencies.sort_by(|a, b| {
249 a.path
250 .cmp(&b.path)
251 .then(a.line.cmp(&b.line))
252 .then(a.package_name.cmp(&b.package_name))
253 });
254
255 self.test_only_dependencies.sort_by(|a, b| {
256 a.path
257 .cmp(&b.path)
258 .then(a.line.cmp(&b.line))
259 .then(a.package_name.cmp(&b.package_name))
260 });
261
262 self.circular_dependencies
263 .sort_by(|a, b| a.files.cmp(&b.files).then(a.length.cmp(&b.length)));
264
265 self.boundary_violations.sort_by(|a, b| {
266 a.from_path
267 .cmp(&b.from_path)
268 .then(a.line.cmp(&b.line))
269 .then(a.col.cmp(&b.col))
270 .then(a.to_path.cmp(&b.to_path))
271 });
272
273 self.stale_suppressions.sort_by(|a, b| {
274 a.path
275 .cmp(&b.path)
276 .then(a.line.cmp(&b.line))
277 .then(a.col.cmp(&b.col))
278 });
279
280 self.unused_catalog_entries.sort_by(|a, b| {
281 a.path
282 .cmp(&b.path)
283 .then_with(|| {
284 catalog_sort_key(&a.catalog_name).cmp(&catalog_sort_key(&b.catalog_name))
285 })
286 .then(a.catalog_name.cmp(&b.catalog_name))
287 .then(a.entry_name.cmp(&b.entry_name))
288 });
289 for entry in &mut self.unused_catalog_entries {
290 entry.hardcoded_consumers.sort();
291 entry.hardcoded_consumers.dedup();
292 }
293
294 self.feature_flags.sort_by(|a, b| {
295 a.path
296 .cmp(&b.path)
297 .then(a.line.cmp(&b.line))
298 .then(a.flag_name.cmp(&b.flag_name))
299 });
300
301 for usage in &mut self.export_usages {
302 usage.reference_locations.sort_by(|a, b| {
303 a.path
304 .cmp(&b.path)
305 .then(a.line.cmp(&b.line))
306 .then(a.col.cmp(&b.col))
307 });
308 }
309 self.export_usages.sort_by(|a, b| {
310 a.path
311 .cmp(&b.path)
312 .then(a.line.cmp(&b.line))
313 .then(a.export_name.cmp(&b.export_name))
314 });
315 }
316}
317
318fn catalog_sort_key(name: &str) -> (u8, &str) {
320 if name == "default" {
321 (0, name)
322 } else {
323 (1, name)
324 }
325}
326
327#[derive(Debug, Clone, Serialize)]
329pub struct UnusedFile {
330 #[serde(serialize_with = "serde_path::serialize")]
332 pub path: PathBuf,
333}
334
335#[derive(Debug, Clone, Serialize)]
337pub struct UnusedExport {
338 #[serde(serialize_with = "serde_path::serialize")]
340 pub path: PathBuf,
341 pub export_name: String,
343 pub is_type_only: bool,
345 pub line: u32,
347 pub col: u32,
349 pub span_start: u32,
351 pub is_re_export: bool,
353}
354
355#[derive(Debug, Clone, Serialize)]
357pub struct PrivateTypeLeak {
358 #[serde(serialize_with = "serde_path::serialize")]
360 pub path: PathBuf,
361 pub export_name: String,
363 pub type_name: String,
365 pub line: u32,
367 pub col: u32,
369 pub span_start: u32,
371}
372
373#[derive(Debug, Clone, Serialize)]
375pub struct UnusedDependency {
376 pub package_name: String,
378 pub location: DependencyLocation,
380 #[serde(serialize_with = "serde_path::serialize")]
383 pub path: PathBuf,
384 pub line: u32,
386 #[serde(
388 serialize_with = "serde_path::serialize_vec",
389 skip_serializing_if = "Vec::is_empty"
390 )]
391 pub used_in_workspaces: Vec<PathBuf>,
392}
393
394#[derive(Debug, Clone, Serialize)]
411#[serde(rename_all = "camelCase")]
412pub enum DependencyLocation {
413 Dependencies,
415 DevDependencies,
417 OptionalDependencies,
419}
420
421#[derive(Debug, Clone, Serialize)]
423pub struct UnusedMember {
424 #[serde(serialize_with = "serde_path::serialize")]
426 pub path: PathBuf,
427 pub parent_name: String,
429 pub member_name: String,
431 pub kind: MemberKind,
433 pub line: u32,
435 pub col: u32,
437}
438
439#[derive(Debug, Clone, Serialize)]
441pub struct UnresolvedImport {
442 #[serde(serialize_with = "serde_path::serialize")]
444 pub path: PathBuf,
445 pub specifier: String,
447 pub line: u32,
449 pub col: u32,
451 pub specifier_col: u32,
454}
455
456#[derive(Debug, Clone, Serialize)]
458pub struct UnlistedDependency {
459 pub package_name: String,
461 pub imported_from: Vec<ImportSite>,
463}
464
465#[derive(Debug, Clone, Serialize)]
467pub struct ImportSite {
468 #[serde(serialize_with = "serde_path::serialize")]
470 pub path: PathBuf,
471 pub line: u32,
473 pub col: u32,
475}
476
477#[derive(Debug, Clone, Serialize)]
479pub struct DuplicateExport {
480 pub export_name: String,
482 pub locations: Vec<DuplicateLocation>,
484}
485
486#[derive(Debug, Clone, Serialize)]
488pub struct DuplicateLocation {
489 #[serde(serialize_with = "serde_path::serialize")]
491 pub path: PathBuf,
492 pub line: u32,
494 pub col: u32,
496}
497
498#[derive(Debug, Clone, Serialize)]
502pub struct TypeOnlyDependency {
503 pub package_name: String,
505 #[serde(serialize_with = "serde_path::serialize")]
507 pub path: PathBuf,
508 pub line: u32,
510}
511
512#[derive(Debug, Clone, Serialize)]
518pub struct UnusedCatalogEntry {
519 pub entry_name: String,
521 pub catalog_name: String,
524 #[serde(serialize_with = "serde_path::serialize")]
526 pub path: PathBuf,
527 pub line: u32,
529 #[serde(
534 default,
535 serialize_with = "serde_path::serialize_vec",
536 skip_serializing_if = "Vec::is_empty"
537 )]
538 pub hardcoded_consumers: Vec<PathBuf>,
539}
540
541#[derive(Debug, Clone, Serialize)]
544pub struct TestOnlyDependency {
545 pub package_name: String,
547 #[serde(serialize_with = "serde_path::serialize")]
549 pub path: PathBuf,
550 pub line: u32,
552}
553
554#[derive(Debug, Clone, Serialize, Deserialize)]
556pub struct CircularDependency {
557 #[serde(serialize_with = "serde_path::serialize_vec")]
559 pub files: Vec<PathBuf>,
560 pub length: usize,
562 #[serde(default)]
564 pub line: u32,
565 #[serde(default)]
567 pub col: u32,
568 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
570 pub is_cross_package: bool,
571}
572
573#[derive(Debug, Clone, Serialize)]
575pub struct BoundaryViolation {
576 #[serde(serialize_with = "serde_path::serialize")]
578 pub from_path: PathBuf,
579 #[serde(serialize_with = "serde_path::serialize")]
581 pub to_path: PathBuf,
582 pub from_zone: String,
584 pub to_zone: String,
586 pub import_specifier: String,
588 pub line: u32,
590 pub col: u32,
592}
593
594#[derive(Debug, Clone, Serialize)]
596#[serde(rename_all = "snake_case", tag = "type")]
597pub enum SuppressionOrigin {
598 Comment {
600 #[serde(skip_serializing_if = "Option::is_none")]
602 issue_kind: Option<String>,
603 is_file_level: bool,
605 },
606 JsdocTag {
608 export_name: String,
610 },
611}
612
613#[derive(Debug, Clone, Serialize)]
615pub struct StaleSuppression {
616 #[serde(serialize_with = "serde_path::serialize")]
618 pub path: PathBuf,
619 pub line: u32,
621 pub col: u32,
623 pub origin: SuppressionOrigin,
625}
626
627impl StaleSuppression {
628 #[must_use]
630 pub fn description(&self) -> String {
631 match &self.origin {
632 SuppressionOrigin::Comment {
633 issue_kind,
634 is_file_level,
635 } => {
636 let directive = if *is_file_level {
637 "fallow-ignore-file"
638 } else {
639 "fallow-ignore-next-line"
640 };
641 match issue_kind {
642 Some(kind) => format!("// {directive} {kind}"),
643 None => format!("// {directive}"),
644 }
645 }
646 SuppressionOrigin::JsdocTag { export_name } => {
647 format!("@expected-unused on {export_name}")
648 }
649 }
650 }
651
652 #[must_use]
654 pub fn explanation(&self) -> String {
655 match &self.origin {
656 SuppressionOrigin::Comment {
657 issue_kind,
658 is_file_level,
659 } => {
660 let scope = if *is_file_level {
661 "in this file"
662 } else {
663 "on the next line"
664 };
665 match issue_kind {
666 Some(kind) => format!("no {kind} issue found {scope}"),
667 None => format!("no issues found {scope}"),
668 }
669 }
670 SuppressionOrigin::JsdocTag { export_name } => {
671 format!("{export_name} is now used")
672 }
673 }
674 }
675
676 #[must_use]
678 pub fn suppressed_kind(&self) -> Option<IssueKind> {
679 match &self.origin {
680 SuppressionOrigin::Comment { issue_kind, .. } => {
681 issue_kind.as_deref().and_then(IssueKind::parse)
682 }
683 SuppressionOrigin::JsdocTag { .. } => None,
684 }
685 }
686}
687
688#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
690#[serde(rename_all = "snake_case")]
691pub enum FlagKind {
692 EnvironmentVariable,
694 SdkCall,
696 ConfigObject,
698}
699
700#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
702#[serde(rename_all = "snake_case")]
703pub enum FlagConfidence {
704 Low,
706 Medium,
708 High,
710}
711
712#[derive(Debug, Clone, Serialize)]
714pub struct FeatureFlag {
715 #[serde(serialize_with = "serde_path::serialize")]
717 pub path: PathBuf,
718 pub flag_name: String,
720 pub kind: FlagKind,
722 pub confidence: FlagConfidence,
724 pub line: u32,
726 pub col: u32,
728 #[serde(skip)]
730 pub guard_span_start: Option<u32>,
731 #[serde(skip)]
733 pub guard_span_end: Option<u32>,
734 #[serde(skip_serializing_if = "Option::is_none")]
736 pub sdk_name: Option<String>,
737 #[serde(skip)]
740 pub guard_line_start: Option<u32>,
741 #[serde(skip)]
743 pub guard_line_end: Option<u32>,
744 #[serde(skip_serializing_if = "Vec::is_empty")]
747 pub guarded_dead_exports: Vec<String>,
748}
749
750const _: () = assert!(std::mem::size_of::<FeatureFlag>() <= 160);
752
753#[derive(Debug, Clone, Serialize)]
756pub struct ExportUsage {
757 #[serde(serialize_with = "serde_path::serialize")]
759 pub path: PathBuf,
760 pub export_name: String,
762 pub line: u32,
764 pub col: u32,
766 pub reference_count: usize,
768 pub reference_locations: Vec<ReferenceLocation>,
771}
772
773#[derive(Debug, Clone, Serialize)]
775pub struct ReferenceLocation {
776 #[serde(serialize_with = "serde_path::serialize")]
778 pub path: PathBuf,
779 pub line: u32,
781 pub col: u32,
783}
784
785#[cfg(test)]
786mod tests {
787 use super::*;
788
789 #[test]
790 fn empty_results_no_issues() {
791 let results = AnalysisResults::default();
792 assert_eq!(results.total_issues(), 0);
793 assert!(!results.has_issues());
794 }
795
796 #[test]
797 fn results_with_unused_file() {
798 let mut results = AnalysisResults::default();
799 results.unused_files.push(UnusedFile {
800 path: PathBuf::from("test.ts"),
801 });
802 assert_eq!(results.total_issues(), 1);
803 assert!(results.has_issues());
804 }
805
806 #[test]
807 fn results_with_unused_export() {
808 let mut results = AnalysisResults::default();
809 results.unused_exports.push(UnusedExport {
810 path: PathBuf::from("test.ts"),
811 export_name: "foo".to_string(),
812 is_type_only: false,
813 line: 1,
814 col: 0,
815 span_start: 0,
816 is_re_export: false,
817 });
818 assert_eq!(results.total_issues(), 1);
819 assert!(results.has_issues());
820 }
821
822 #[test]
823 fn results_total_counts_all_types() {
824 let mut results = AnalysisResults::default();
825 results.unused_files.push(UnusedFile {
826 path: PathBuf::from("a.ts"),
827 });
828 results.unused_exports.push(UnusedExport {
829 path: PathBuf::from("b.ts"),
830 export_name: "x".to_string(),
831 is_type_only: false,
832 line: 1,
833 col: 0,
834 span_start: 0,
835 is_re_export: false,
836 });
837 results.unused_types.push(UnusedExport {
838 path: PathBuf::from("c.ts"),
839 export_name: "T".to_string(),
840 is_type_only: true,
841 line: 1,
842 col: 0,
843 span_start: 0,
844 is_re_export: false,
845 });
846 results.unused_dependencies.push(UnusedDependency {
847 package_name: "dep".to_string(),
848 location: DependencyLocation::Dependencies,
849 path: PathBuf::from("package.json"),
850 line: 5,
851 used_in_workspaces: Vec::new(),
852 });
853 results.unused_dev_dependencies.push(UnusedDependency {
854 package_name: "dev".to_string(),
855 location: DependencyLocation::DevDependencies,
856 path: PathBuf::from("package.json"),
857 line: 5,
858 used_in_workspaces: Vec::new(),
859 });
860 results.unused_enum_members.push(UnusedMember {
861 path: PathBuf::from("d.ts"),
862 parent_name: "E".to_string(),
863 member_name: "A".to_string(),
864 kind: MemberKind::EnumMember,
865 line: 1,
866 col: 0,
867 });
868 results.unused_class_members.push(UnusedMember {
869 path: PathBuf::from("e.ts"),
870 parent_name: "C".to_string(),
871 member_name: "m".to_string(),
872 kind: MemberKind::ClassMethod,
873 line: 1,
874 col: 0,
875 });
876 results.unresolved_imports.push(UnresolvedImport {
877 path: PathBuf::from("f.ts"),
878 specifier: "./missing".to_string(),
879 line: 1,
880 col: 0,
881 specifier_col: 0,
882 });
883 results.unlisted_dependencies.push(UnlistedDependency {
884 package_name: "unlisted".to_string(),
885 imported_from: vec![ImportSite {
886 path: PathBuf::from("g.ts"),
887 line: 1,
888 col: 0,
889 }],
890 });
891 results.duplicate_exports.push(DuplicateExport {
892 export_name: "dup".to_string(),
893 locations: vec![
894 DuplicateLocation {
895 path: PathBuf::from("h.ts"),
896 line: 15,
897 col: 0,
898 },
899 DuplicateLocation {
900 path: PathBuf::from("i.ts"),
901 line: 30,
902 col: 0,
903 },
904 ],
905 });
906 results.unused_optional_dependencies.push(UnusedDependency {
907 package_name: "optional".to_string(),
908 location: DependencyLocation::OptionalDependencies,
909 path: PathBuf::from("package.json"),
910 line: 5,
911 used_in_workspaces: Vec::new(),
912 });
913 results.type_only_dependencies.push(TypeOnlyDependency {
914 package_name: "type-only".to_string(),
915 path: PathBuf::from("package.json"),
916 line: 8,
917 });
918 results.test_only_dependencies.push(TestOnlyDependency {
919 package_name: "test-only".to_string(),
920 path: PathBuf::from("package.json"),
921 line: 9,
922 });
923 results.circular_dependencies.push(CircularDependency {
924 files: vec![PathBuf::from("a.ts"), PathBuf::from("b.ts")],
925 length: 2,
926 line: 3,
927 col: 0,
928 is_cross_package: false,
929 });
930 results.boundary_violations.push(BoundaryViolation {
931 from_path: PathBuf::from("src/ui/Button.tsx"),
932 to_path: PathBuf::from("src/db/queries.ts"),
933 from_zone: "ui".to_string(),
934 to_zone: "database".to_string(),
935 import_specifier: "../db/queries".to_string(),
936 line: 3,
937 col: 0,
938 });
939
940 assert_eq!(results.total_issues(), 15);
942 assert!(results.has_issues());
943 }
944
945 #[test]
948 fn total_issues_and_has_issues_are_consistent() {
949 let results = AnalysisResults::default();
950 assert_eq!(results.total_issues(), 0);
951 assert!(!results.has_issues());
952 assert_eq!(results.total_issues() > 0, results.has_issues());
953 }
954
955 #[test]
958 fn total_issues_sums_all_categories_independently() {
959 let mut results = AnalysisResults::default();
960 results.unused_files.push(UnusedFile {
961 path: PathBuf::from("a.ts"),
962 });
963 assert_eq!(results.total_issues(), 1);
964
965 results.unused_files.push(UnusedFile {
966 path: PathBuf::from("b.ts"),
967 });
968 assert_eq!(results.total_issues(), 2);
969
970 results.unresolved_imports.push(UnresolvedImport {
971 path: PathBuf::from("c.ts"),
972 specifier: "./missing".to_string(),
973 line: 1,
974 col: 0,
975 specifier_col: 0,
976 });
977 assert_eq!(results.total_issues(), 3);
978 }
979
980 #[test]
983 fn default_results_all_fields_empty() {
984 let r = AnalysisResults::default();
985 assert!(r.unused_files.is_empty());
986 assert!(r.unused_exports.is_empty());
987 assert!(r.unused_types.is_empty());
988 assert!(r.unused_dependencies.is_empty());
989 assert!(r.unused_dev_dependencies.is_empty());
990 assert!(r.unused_optional_dependencies.is_empty());
991 assert!(r.unused_enum_members.is_empty());
992 assert!(r.unused_class_members.is_empty());
993 assert!(r.unresolved_imports.is_empty());
994 assert!(r.unlisted_dependencies.is_empty());
995 assert!(r.duplicate_exports.is_empty());
996 assert!(r.type_only_dependencies.is_empty());
997 assert!(r.test_only_dependencies.is_empty());
998 assert!(r.circular_dependencies.is_empty());
999 assert!(r.boundary_violations.is_empty());
1000 assert!(r.export_usages.is_empty());
1001 }
1002
1003 #[test]
1006 fn entry_point_summary_default() {
1007 let summary = EntryPointSummary::default();
1008 assert_eq!(summary.total, 0);
1009 assert!(summary.by_source.is_empty());
1010 }
1011
1012 #[test]
1013 fn entry_point_summary_not_in_default_results() {
1014 let r = AnalysisResults::default();
1015 assert!(r.entry_point_summary.is_none());
1016 }
1017
1018 #[test]
1019 fn entry_point_summary_some_preserves_data() {
1020 let r = AnalysisResults {
1021 entry_point_summary: Some(EntryPointSummary {
1022 total: 5,
1023 by_source: vec![("package.json".to_string(), 2), ("plugin".to_string(), 3)],
1024 }),
1025 ..AnalysisResults::default()
1026 };
1027 let summary = r.entry_point_summary.as_ref().unwrap();
1028 assert_eq!(summary.total, 5);
1029 assert_eq!(summary.by_source.len(), 2);
1030 assert_eq!(summary.by_source[0], ("package.json".to_string(), 2));
1031 }
1032
1033 #[test]
1036 fn sort_unused_files_by_path() {
1037 let mut r = AnalysisResults::default();
1038 r.unused_files.push(UnusedFile {
1039 path: PathBuf::from("z.ts"),
1040 });
1041 r.unused_files.push(UnusedFile {
1042 path: PathBuf::from("a.ts"),
1043 });
1044 r.unused_files.push(UnusedFile {
1045 path: PathBuf::from("m.ts"),
1046 });
1047 r.sort();
1048 let paths: Vec<_> = r
1049 .unused_files
1050 .iter()
1051 .map(|f| f.path.to_string_lossy().to_string())
1052 .collect();
1053 assert_eq!(paths, vec!["a.ts", "m.ts", "z.ts"]);
1054 }
1055
1056 #[test]
1059 fn sort_unused_exports_by_path_line_name() {
1060 let mut r = AnalysisResults::default();
1061 let mk = |path: &str, line: u32, name: &str| UnusedExport {
1062 path: PathBuf::from(path),
1063 export_name: name.to_string(),
1064 is_type_only: false,
1065 line,
1066 col: 0,
1067 span_start: 0,
1068 is_re_export: false,
1069 };
1070 r.unused_exports.push(mk("b.ts", 5, "beta"));
1071 r.unused_exports.push(mk("a.ts", 10, "zeta"));
1072 r.unused_exports.push(mk("a.ts", 10, "alpha"));
1073 r.unused_exports.push(mk("a.ts", 1, "gamma"));
1074 r.sort();
1075 let keys: Vec<_> = r
1076 .unused_exports
1077 .iter()
1078 .map(|e| format!("{}:{}:{}", e.path.to_string_lossy(), e.line, e.export_name))
1079 .collect();
1080 assert_eq!(
1081 keys,
1082 vec![
1083 "a.ts:1:gamma",
1084 "a.ts:10:alpha",
1085 "a.ts:10:zeta",
1086 "b.ts:5:beta"
1087 ]
1088 );
1089 }
1090
1091 #[test]
1094 fn sort_unused_types_by_path_line_name() {
1095 let mut r = AnalysisResults::default();
1096 let mk = |path: &str, line: u32, name: &str| UnusedExport {
1097 path: PathBuf::from(path),
1098 export_name: name.to_string(),
1099 is_type_only: true,
1100 line,
1101 col: 0,
1102 span_start: 0,
1103 is_re_export: false,
1104 };
1105 r.unused_types.push(mk("z.ts", 1, "Z"));
1106 r.unused_types.push(mk("a.ts", 1, "A"));
1107 r.sort();
1108 assert_eq!(r.unused_types[0].path, PathBuf::from("a.ts"));
1109 assert_eq!(r.unused_types[1].path, PathBuf::from("z.ts"));
1110 }
1111
1112 #[test]
1115 fn sort_unused_dependencies_by_path_line_name() {
1116 let mut r = AnalysisResults::default();
1117 let mk = |path: &str, line: u32, name: &str| UnusedDependency {
1118 package_name: name.to_string(),
1119 location: DependencyLocation::Dependencies,
1120 path: PathBuf::from(path),
1121 line,
1122 used_in_workspaces: Vec::new(),
1123 };
1124 r.unused_dependencies.push(mk("b/package.json", 3, "zlib"));
1125 r.unused_dependencies.push(mk("a/package.json", 5, "react"));
1126 r.unused_dependencies.push(mk("a/package.json", 5, "axios"));
1127 r.sort();
1128 let names: Vec<_> = r
1129 .unused_dependencies
1130 .iter()
1131 .map(|d| d.package_name.as_str())
1132 .collect();
1133 assert_eq!(names, vec!["axios", "react", "zlib"]);
1134 }
1135
1136 #[test]
1139 fn sort_unused_dev_dependencies() {
1140 let mut r = AnalysisResults::default();
1141 r.unused_dev_dependencies.push(UnusedDependency {
1142 package_name: "vitest".to_string(),
1143 location: DependencyLocation::DevDependencies,
1144 path: PathBuf::from("package.json"),
1145 line: 10,
1146 used_in_workspaces: Vec::new(),
1147 });
1148 r.unused_dev_dependencies.push(UnusedDependency {
1149 package_name: "jest".to_string(),
1150 location: DependencyLocation::DevDependencies,
1151 path: PathBuf::from("package.json"),
1152 line: 5,
1153 used_in_workspaces: Vec::new(),
1154 });
1155 r.sort();
1156 assert_eq!(r.unused_dev_dependencies[0].package_name, "jest");
1157 assert_eq!(r.unused_dev_dependencies[1].package_name, "vitest");
1158 }
1159
1160 #[test]
1163 fn sort_unused_optional_dependencies() {
1164 let mut r = AnalysisResults::default();
1165 r.unused_optional_dependencies.push(UnusedDependency {
1166 package_name: "zod".to_string(),
1167 location: DependencyLocation::OptionalDependencies,
1168 path: PathBuf::from("package.json"),
1169 line: 3,
1170 used_in_workspaces: Vec::new(),
1171 });
1172 r.unused_optional_dependencies.push(UnusedDependency {
1173 package_name: "ajv".to_string(),
1174 location: DependencyLocation::OptionalDependencies,
1175 path: PathBuf::from("package.json"),
1176 line: 2,
1177 used_in_workspaces: Vec::new(),
1178 });
1179 r.sort();
1180 assert_eq!(r.unused_optional_dependencies[0].package_name, "ajv");
1181 assert_eq!(r.unused_optional_dependencies[1].package_name, "zod");
1182 }
1183
1184 #[test]
1187 fn sort_unused_enum_members_by_path_line_parent_member() {
1188 let mut r = AnalysisResults::default();
1189 let mk = |path: &str, line: u32, parent: &str, member: &str| UnusedMember {
1190 path: PathBuf::from(path),
1191 parent_name: parent.to_string(),
1192 member_name: member.to_string(),
1193 kind: MemberKind::EnumMember,
1194 line,
1195 col: 0,
1196 };
1197 r.unused_enum_members.push(mk("a.ts", 5, "Status", "Z"));
1198 r.unused_enum_members.push(mk("a.ts", 5, "Status", "A"));
1199 r.unused_enum_members.push(mk("a.ts", 1, "Direction", "Up"));
1200 r.sort();
1201 let keys: Vec<_> = r
1202 .unused_enum_members
1203 .iter()
1204 .map(|m| format!("{}:{}", m.parent_name, m.member_name))
1205 .collect();
1206 assert_eq!(keys, vec!["Direction:Up", "Status:A", "Status:Z"]);
1207 }
1208
1209 #[test]
1212 fn sort_unused_class_members() {
1213 let mut r = AnalysisResults::default();
1214 let mk = |path: &str, line: u32, parent: &str, member: &str| UnusedMember {
1215 path: PathBuf::from(path),
1216 parent_name: parent.to_string(),
1217 member_name: member.to_string(),
1218 kind: MemberKind::ClassMethod,
1219 line,
1220 col: 0,
1221 };
1222 r.unused_class_members.push(mk("b.ts", 1, "Foo", "z"));
1223 r.unused_class_members.push(mk("a.ts", 1, "Bar", "a"));
1224 r.sort();
1225 assert_eq!(r.unused_class_members[0].path, PathBuf::from("a.ts"));
1226 assert_eq!(r.unused_class_members[1].path, PathBuf::from("b.ts"));
1227 }
1228
1229 #[test]
1232 fn sort_unresolved_imports_by_path_line_col_specifier() {
1233 let mut r = AnalysisResults::default();
1234 let mk = |path: &str, line: u32, col: u32, spec: &str| UnresolvedImport {
1235 path: PathBuf::from(path),
1236 specifier: spec.to_string(),
1237 line,
1238 col,
1239 specifier_col: 0,
1240 };
1241 r.unresolved_imports.push(mk("a.ts", 5, 0, "./z"));
1242 r.unresolved_imports.push(mk("a.ts", 5, 0, "./a"));
1243 r.unresolved_imports.push(mk("a.ts", 1, 0, "./m"));
1244 r.sort();
1245 let specs: Vec<_> = r
1246 .unresolved_imports
1247 .iter()
1248 .map(|i| i.specifier.as_str())
1249 .collect();
1250 assert_eq!(specs, vec!["./m", "./a", "./z"]);
1251 }
1252
1253 #[test]
1256 fn sort_unlisted_dependencies_by_name_and_inner_sites() {
1257 let mut r = AnalysisResults::default();
1258 r.unlisted_dependencies.push(UnlistedDependency {
1259 package_name: "zod".to_string(),
1260 imported_from: vec![
1261 ImportSite {
1262 path: PathBuf::from("b.ts"),
1263 line: 10,
1264 col: 0,
1265 },
1266 ImportSite {
1267 path: PathBuf::from("a.ts"),
1268 line: 1,
1269 col: 0,
1270 },
1271 ],
1272 });
1273 r.unlisted_dependencies.push(UnlistedDependency {
1274 package_name: "axios".to_string(),
1275 imported_from: vec![ImportSite {
1276 path: PathBuf::from("c.ts"),
1277 line: 1,
1278 col: 0,
1279 }],
1280 });
1281 r.sort();
1282
1283 assert_eq!(r.unlisted_dependencies[0].package_name, "axios");
1285 assert_eq!(r.unlisted_dependencies[1].package_name, "zod");
1286
1287 let zod_sites: Vec<_> = r.unlisted_dependencies[1]
1289 .imported_from
1290 .iter()
1291 .map(|s| s.path.to_string_lossy().to_string())
1292 .collect();
1293 assert_eq!(zod_sites, vec!["a.ts", "b.ts"]);
1294 }
1295
1296 #[test]
1299 fn sort_duplicate_exports_by_name_and_inner_locations() {
1300 let mut r = AnalysisResults::default();
1301 r.duplicate_exports.push(DuplicateExport {
1302 export_name: "z".to_string(),
1303 locations: vec![
1304 DuplicateLocation {
1305 path: PathBuf::from("c.ts"),
1306 line: 1,
1307 col: 0,
1308 },
1309 DuplicateLocation {
1310 path: PathBuf::from("a.ts"),
1311 line: 5,
1312 col: 0,
1313 },
1314 ],
1315 });
1316 r.duplicate_exports.push(DuplicateExport {
1317 export_name: "a".to_string(),
1318 locations: vec![DuplicateLocation {
1319 path: PathBuf::from("b.ts"),
1320 line: 1,
1321 col: 0,
1322 }],
1323 });
1324 r.sort();
1325
1326 assert_eq!(r.duplicate_exports[0].export_name, "a");
1328 assert_eq!(r.duplicate_exports[1].export_name, "z");
1329
1330 let z_locs: Vec<_> = r.duplicate_exports[1]
1332 .locations
1333 .iter()
1334 .map(|l| l.path.to_string_lossy().to_string())
1335 .collect();
1336 assert_eq!(z_locs, vec!["a.ts", "c.ts"]);
1337 }
1338
1339 #[test]
1342 fn sort_type_only_dependencies() {
1343 let mut r = AnalysisResults::default();
1344 r.type_only_dependencies.push(TypeOnlyDependency {
1345 package_name: "zod".to_string(),
1346 path: PathBuf::from("package.json"),
1347 line: 10,
1348 });
1349 r.type_only_dependencies.push(TypeOnlyDependency {
1350 package_name: "ajv".to_string(),
1351 path: PathBuf::from("package.json"),
1352 line: 5,
1353 });
1354 r.sort();
1355 assert_eq!(r.type_only_dependencies[0].package_name, "ajv");
1356 assert_eq!(r.type_only_dependencies[1].package_name, "zod");
1357 }
1358
1359 #[test]
1362 fn sort_test_only_dependencies() {
1363 let mut r = AnalysisResults::default();
1364 r.test_only_dependencies.push(TestOnlyDependency {
1365 package_name: "vitest".to_string(),
1366 path: PathBuf::from("package.json"),
1367 line: 15,
1368 });
1369 r.test_only_dependencies.push(TestOnlyDependency {
1370 package_name: "jest".to_string(),
1371 path: PathBuf::from("package.json"),
1372 line: 10,
1373 });
1374 r.sort();
1375 assert_eq!(r.test_only_dependencies[0].package_name, "jest");
1376 assert_eq!(r.test_only_dependencies[1].package_name, "vitest");
1377 }
1378
1379 #[test]
1382 fn sort_circular_dependencies_by_files_then_length() {
1383 let mut r = AnalysisResults::default();
1384 r.circular_dependencies.push(CircularDependency {
1385 files: vec![PathBuf::from("b.ts"), PathBuf::from("c.ts")],
1386 length: 2,
1387 line: 1,
1388 col: 0,
1389 is_cross_package: false,
1390 });
1391 r.circular_dependencies.push(CircularDependency {
1392 files: vec![PathBuf::from("a.ts"), PathBuf::from("b.ts")],
1393 length: 2,
1394 line: 1,
1395 col: 0,
1396 is_cross_package: true,
1397 });
1398 r.sort();
1399 assert_eq!(r.circular_dependencies[0].files[0], PathBuf::from("a.ts"));
1400 assert_eq!(r.circular_dependencies[1].files[0], PathBuf::from("b.ts"));
1401 }
1402
1403 #[test]
1406 fn sort_boundary_violations() {
1407 let mut r = AnalysisResults::default();
1408 let mk = |from: &str, line: u32, col: u32, to: &str| BoundaryViolation {
1409 from_path: PathBuf::from(from),
1410 to_path: PathBuf::from(to),
1411 from_zone: "a".to_string(),
1412 to_zone: "b".to_string(),
1413 import_specifier: to.to_string(),
1414 line,
1415 col,
1416 };
1417 r.boundary_violations.push(mk("z.ts", 1, 0, "a.ts"));
1418 r.boundary_violations.push(mk("a.ts", 5, 0, "b.ts"));
1419 r.boundary_violations.push(mk("a.ts", 1, 0, "c.ts"));
1420 r.sort();
1421 let from_paths: Vec<_> = r
1422 .boundary_violations
1423 .iter()
1424 .map(|v| format!("{}:{}", v.from_path.to_string_lossy(), v.line))
1425 .collect();
1426 assert_eq!(from_paths, vec!["a.ts:1", "a.ts:5", "z.ts:1"]);
1427 }
1428
1429 #[test]
1432 fn sort_export_usages_and_inner_reference_locations() {
1433 let mut r = AnalysisResults::default();
1434 r.export_usages.push(ExportUsage {
1435 path: PathBuf::from("z.ts"),
1436 export_name: "foo".to_string(),
1437 line: 1,
1438 col: 0,
1439 reference_count: 2,
1440 reference_locations: vec![
1441 ReferenceLocation {
1442 path: PathBuf::from("c.ts"),
1443 line: 10,
1444 col: 0,
1445 },
1446 ReferenceLocation {
1447 path: PathBuf::from("a.ts"),
1448 line: 5,
1449 col: 0,
1450 },
1451 ],
1452 });
1453 r.export_usages.push(ExportUsage {
1454 path: PathBuf::from("a.ts"),
1455 export_name: "bar".to_string(),
1456 line: 1,
1457 col: 0,
1458 reference_count: 1,
1459 reference_locations: vec![ReferenceLocation {
1460 path: PathBuf::from("b.ts"),
1461 line: 1,
1462 col: 0,
1463 }],
1464 });
1465 r.sort();
1466
1467 assert_eq!(r.export_usages[0].path, PathBuf::from("a.ts"));
1469 assert_eq!(r.export_usages[1].path, PathBuf::from("z.ts"));
1470
1471 let refs: Vec<_> = r.export_usages[1]
1473 .reference_locations
1474 .iter()
1475 .map(|l| l.path.to_string_lossy().to_string())
1476 .collect();
1477 assert_eq!(refs, vec!["a.ts", "c.ts"]);
1478 }
1479
1480 #[test]
1483 fn sort_empty_results_is_noop() {
1484 let mut r = AnalysisResults::default();
1485 r.sort(); assert_eq!(r.total_issues(), 0);
1487 }
1488
1489 #[test]
1492 fn sort_single_element_lists_stable() {
1493 let mut r = AnalysisResults::default();
1494 r.unused_files.push(UnusedFile {
1495 path: PathBuf::from("only.ts"),
1496 });
1497 r.sort();
1498 assert_eq!(r.unused_files[0].path, PathBuf::from("only.ts"));
1499 }
1500
1501 #[test]
1504 fn serialize_empty_results() {
1505 let r = AnalysisResults::default();
1506 let json = serde_json::to_value(&r).unwrap();
1507
1508 assert!(json["unused_files"].as_array().unwrap().is_empty());
1510 assert!(json["unused_exports"].as_array().unwrap().is_empty());
1511 assert!(json["circular_dependencies"].as_array().unwrap().is_empty());
1512
1513 assert!(json.get("export_usages").is_none());
1515 assert!(json.get("entry_point_summary").is_none());
1516 }
1517
1518 #[test]
1519 fn serialize_unused_file_path() {
1520 let r = UnusedFile {
1521 path: PathBuf::from("src/utils/index.ts"),
1522 };
1523 let json = serde_json::to_value(&r).unwrap();
1524 assert_eq!(json["path"], "src/utils/index.ts");
1525 }
1526
1527 #[test]
1528 fn serialize_dependency_location_camel_case() {
1529 let dep = UnusedDependency {
1530 package_name: "react".to_string(),
1531 location: DependencyLocation::DevDependencies,
1532 path: PathBuf::from("package.json"),
1533 line: 5,
1534 used_in_workspaces: Vec::new(),
1535 };
1536 let json = serde_json::to_value(&dep).unwrap();
1537 assert_eq!(json["location"], "devDependencies");
1538
1539 let dep2 = UnusedDependency {
1540 package_name: "react".to_string(),
1541 location: DependencyLocation::Dependencies,
1542 path: PathBuf::from("package.json"),
1543 line: 3,
1544 used_in_workspaces: Vec::new(),
1545 };
1546 let json2 = serde_json::to_value(&dep2).unwrap();
1547 assert_eq!(json2["location"], "dependencies");
1548
1549 let dep3 = UnusedDependency {
1550 package_name: "fsevents".to_string(),
1551 location: DependencyLocation::OptionalDependencies,
1552 path: PathBuf::from("package.json"),
1553 line: 7,
1554 used_in_workspaces: Vec::new(),
1555 };
1556 let json3 = serde_json::to_value(&dep3).unwrap();
1557 assert_eq!(json3["location"], "optionalDependencies");
1558 }
1559
1560 #[test]
1561 fn serialize_circular_dependency_skips_false_cross_package() {
1562 let cd = CircularDependency {
1563 files: vec![PathBuf::from("a.ts"), PathBuf::from("b.ts")],
1564 length: 2,
1565 line: 1,
1566 col: 0,
1567 is_cross_package: false,
1568 };
1569 let json = serde_json::to_value(&cd).unwrap();
1570 assert!(json.get("is_cross_package").is_none());
1572 }
1573
1574 #[test]
1575 fn serialize_circular_dependency_includes_true_cross_package() {
1576 let cd = CircularDependency {
1577 files: vec![PathBuf::from("a.ts"), PathBuf::from("b.ts")],
1578 length: 2,
1579 line: 1,
1580 col: 0,
1581 is_cross_package: true,
1582 };
1583 let json = serde_json::to_value(&cd).unwrap();
1584 assert_eq!(json["is_cross_package"], true);
1585 }
1586
1587 #[test]
1588 fn serialize_unused_export_fields() {
1589 let e = UnusedExport {
1590 path: PathBuf::from("src/mod.ts"),
1591 export_name: "helper".to_string(),
1592 is_type_only: true,
1593 line: 42,
1594 col: 7,
1595 span_start: 100,
1596 is_re_export: true,
1597 };
1598 let json = serde_json::to_value(&e).unwrap();
1599 assert_eq!(json["path"], "src/mod.ts");
1600 assert_eq!(json["export_name"], "helper");
1601 assert_eq!(json["is_type_only"], true);
1602 assert_eq!(json["line"], 42);
1603 assert_eq!(json["col"], 7);
1604 assert_eq!(json["span_start"], 100);
1605 assert_eq!(json["is_re_export"], true);
1606 }
1607
1608 #[test]
1609 fn serialize_boundary_violation_fields() {
1610 let v = BoundaryViolation {
1611 from_path: PathBuf::from("src/ui/button.tsx"),
1612 to_path: PathBuf::from("src/db/queries.ts"),
1613 from_zone: "ui".to_string(),
1614 to_zone: "db".to_string(),
1615 import_specifier: "../db/queries".to_string(),
1616 line: 3,
1617 col: 0,
1618 };
1619 let json = serde_json::to_value(&v).unwrap();
1620 assert_eq!(json["from_path"], "src/ui/button.tsx");
1621 assert_eq!(json["to_path"], "src/db/queries.ts");
1622 assert_eq!(json["from_zone"], "ui");
1623 assert_eq!(json["to_zone"], "db");
1624 assert_eq!(json["import_specifier"], "../db/queries");
1625 }
1626
1627 #[test]
1628 fn serialize_unlisted_dependency_with_import_sites() {
1629 let d = UnlistedDependency {
1630 package_name: "chalk".to_string(),
1631 imported_from: vec![
1632 ImportSite {
1633 path: PathBuf::from("a.ts"),
1634 line: 1,
1635 col: 0,
1636 },
1637 ImportSite {
1638 path: PathBuf::from("b.ts"),
1639 line: 5,
1640 col: 3,
1641 },
1642 ],
1643 };
1644 let json = serde_json::to_value(&d).unwrap();
1645 assert_eq!(json["package_name"], "chalk");
1646 let sites = json["imported_from"].as_array().unwrap();
1647 assert_eq!(sites.len(), 2);
1648 assert_eq!(sites[0]["path"], "a.ts");
1649 assert_eq!(sites[1]["line"], 5);
1650 }
1651
1652 #[test]
1653 fn serialize_duplicate_export_with_locations() {
1654 let d = DuplicateExport {
1655 export_name: "Button".to_string(),
1656 locations: vec![
1657 DuplicateLocation {
1658 path: PathBuf::from("src/a.ts"),
1659 line: 10,
1660 col: 0,
1661 },
1662 DuplicateLocation {
1663 path: PathBuf::from("src/b.ts"),
1664 line: 20,
1665 col: 5,
1666 },
1667 ],
1668 };
1669 let json = serde_json::to_value(&d).unwrap();
1670 assert_eq!(json["export_name"], "Button");
1671 let locs = json["locations"].as_array().unwrap();
1672 assert_eq!(locs.len(), 2);
1673 assert_eq!(locs[0]["line"], 10);
1674 assert_eq!(locs[1]["col"], 5);
1675 }
1676
1677 #[test]
1678 fn serialize_type_only_dependency() {
1679 let d = TypeOnlyDependency {
1680 package_name: "@types/react".to_string(),
1681 path: PathBuf::from("package.json"),
1682 line: 12,
1683 };
1684 let json = serde_json::to_value(&d).unwrap();
1685 assert_eq!(json["package_name"], "@types/react");
1686 assert_eq!(json["line"], 12);
1687 }
1688
1689 #[test]
1690 fn serialize_test_only_dependency() {
1691 let d = TestOnlyDependency {
1692 package_name: "vitest".to_string(),
1693 path: PathBuf::from("package.json"),
1694 line: 8,
1695 };
1696 let json = serde_json::to_value(&d).unwrap();
1697 assert_eq!(json["package_name"], "vitest");
1698 assert_eq!(json["line"], 8);
1699 }
1700
1701 #[test]
1702 fn serialize_unused_member() {
1703 let m = UnusedMember {
1704 path: PathBuf::from("enums.ts"),
1705 parent_name: "Status".to_string(),
1706 member_name: "Pending".to_string(),
1707 kind: MemberKind::EnumMember,
1708 line: 3,
1709 col: 4,
1710 };
1711 let json = serde_json::to_value(&m).unwrap();
1712 assert_eq!(json["parent_name"], "Status");
1713 assert_eq!(json["member_name"], "Pending");
1714 assert_eq!(json["line"], 3);
1715 }
1716
1717 #[test]
1718 fn serialize_unresolved_import() {
1719 let i = UnresolvedImport {
1720 path: PathBuf::from("app.ts"),
1721 specifier: "./missing-module".to_string(),
1722 line: 7,
1723 col: 0,
1724 specifier_col: 21,
1725 };
1726 let json = serde_json::to_value(&i).unwrap();
1727 assert_eq!(json["specifier"], "./missing-module");
1728 assert_eq!(json["specifier_col"], 21);
1729 }
1730
1731 #[test]
1734 fn deserialize_circular_dependency_with_defaults() {
1735 let json = r#"{"files":["a.ts","b.ts"],"length":2}"#;
1737 let cd: CircularDependency = serde_json::from_str(json).unwrap();
1738 assert_eq!(cd.files.len(), 2);
1739 assert_eq!(cd.length, 2);
1740 assert_eq!(cd.line, 0);
1741 assert_eq!(cd.col, 0);
1742 assert!(!cd.is_cross_package);
1743 }
1744
1745 #[test]
1746 fn deserialize_circular_dependency_with_all_fields() {
1747 let json =
1748 r#"{"files":["a.ts","b.ts"],"length":2,"line":5,"col":10,"is_cross_package":true}"#;
1749 let cd: CircularDependency = serde_json::from_str(json).unwrap();
1750 assert_eq!(cd.line, 5);
1751 assert_eq!(cd.col, 10);
1752 assert!(cd.is_cross_package);
1753 }
1754
1755 #[test]
1758 fn clone_results_are_independent() {
1759 let mut r = AnalysisResults::default();
1760 r.unused_files.push(UnusedFile {
1761 path: PathBuf::from("a.ts"),
1762 });
1763 let mut cloned = r.clone();
1764 cloned.unused_files.push(UnusedFile {
1765 path: PathBuf::from("b.ts"),
1766 });
1767 assert_eq!(r.total_issues(), 1);
1768 assert_eq!(cloned.total_issues(), 2);
1769 }
1770
1771 #[test]
1774 fn export_usages_not_counted_in_total_issues() {
1775 let mut r = AnalysisResults::default();
1776 r.export_usages.push(ExportUsage {
1777 path: PathBuf::from("mod.ts"),
1778 export_name: "foo".to_string(),
1779 line: 1,
1780 col: 0,
1781 reference_count: 3,
1782 reference_locations: vec![],
1783 });
1784 assert_eq!(r.total_issues(), 0);
1786 assert!(!r.has_issues());
1787 }
1788
1789 #[test]
1792 fn entry_point_summary_not_counted_in_total_issues() {
1793 let r = AnalysisResults {
1794 entry_point_summary: Some(EntryPointSummary {
1795 total: 10,
1796 by_source: vec![("config".to_string(), 10)],
1797 }),
1798 ..AnalysisResults::default()
1799 };
1800 assert_eq!(r.total_issues(), 0);
1801 assert!(!r.has_issues());
1802 }
1803}