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(skip)]
86 pub suppression_count: usize,
87 #[serde(skip)]
90 pub feature_flags: Vec<FeatureFlag>,
91 #[serde(skip)]
95 pub export_usages: Vec<ExportUsage>,
96 #[serde(skip)]
100 pub entry_point_summary: Option<EntryPointSummary>,
101}
102
103impl AnalysisResults {
104 #[must_use]
128 pub const fn total_issues(&self) -> usize {
129 self.unused_files.len()
130 + self.unused_exports.len()
131 + self.unused_types.len()
132 + self.private_type_leaks.len()
133 + self.unused_dependencies.len()
134 + self.unused_dev_dependencies.len()
135 + self.unused_optional_dependencies.len()
136 + self.unused_enum_members.len()
137 + self.unused_class_members.len()
138 + self.unresolved_imports.len()
139 + self.unlisted_dependencies.len()
140 + self.duplicate_exports.len()
141 + self.type_only_dependencies.len()
142 + self.test_only_dependencies.len()
143 + self.circular_dependencies.len()
144 + self.boundary_violations.len()
145 + self.stale_suppressions.len()
146 }
147
148 #[must_use]
150 pub const fn has_issues(&self) -> bool {
151 self.total_issues() > 0
152 }
153
154 pub fn sort(&mut self) {
161 self.unused_files.sort_by(|a, b| a.path.cmp(&b.path));
162
163 self.unused_exports.sort_by(|a, b| {
164 a.path
165 .cmp(&b.path)
166 .then(a.line.cmp(&b.line))
167 .then(a.export_name.cmp(&b.export_name))
168 });
169
170 self.unused_types.sort_by(|a, b| {
171 a.path
172 .cmp(&b.path)
173 .then(a.line.cmp(&b.line))
174 .then(a.export_name.cmp(&b.export_name))
175 });
176
177 self.private_type_leaks.sort_by(|a, b| {
178 a.path
179 .cmp(&b.path)
180 .then(a.line.cmp(&b.line))
181 .then(a.export_name.cmp(&b.export_name))
182 .then(a.type_name.cmp(&b.type_name))
183 });
184
185 self.unused_dependencies.sort_by(|a, b| {
186 a.path
187 .cmp(&b.path)
188 .then(a.line.cmp(&b.line))
189 .then(a.package_name.cmp(&b.package_name))
190 });
191
192 self.unused_dev_dependencies.sort_by(|a, b| {
193 a.path
194 .cmp(&b.path)
195 .then(a.line.cmp(&b.line))
196 .then(a.package_name.cmp(&b.package_name))
197 });
198
199 self.unused_optional_dependencies.sort_by(|a, b| {
200 a.path
201 .cmp(&b.path)
202 .then(a.line.cmp(&b.line))
203 .then(a.package_name.cmp(&b.package_name))
204 });
205
206 self.unused_enum_members.sort_by(|a, b| {
207 a.path
208 .cmp(&b.path)
209 .then(a.line.cmp(&b.line))
210 .then(a.parent_name.cmp(&b.parent_name))
211 .then(a.member_name.cmp(&b.member_name))
212 });
213
214 self.unused_class_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.unresolved_imports.sort_by(|a, b| {
223 a.path
224 .cmp(&b.path)
225 .then(a.line.cmp(&b.line))
226 .then(a.col.cmp(&b.col))
227 .then(a.specifier.cmp(&b.specifier))
228 });
229
230 self.unlisted_dependencies
231 .sort_by(|a, b| a.package_name.cmp(&b.package_name));
232 for dep in &mut self.unlisted_dependencies {
233 dep.imported_from
234 .sort_by(|a, b| a.path.cmp(&b.path).then(a.line.cmp(&b.line)));
235 }
236
237 self.duplicate_exports
238 .sort_by(|a, b| a.export_name.cmp(&b.export_name));
239 for dup in &mut self.duplicate_exports {
240 dup.locations
241 .sort_by(|a, b| a.path.cmp(&b.path).then(a.line.cmp(&b.line)));
242 }
243
244 self.type_only_dependencies.sort_by(|a, b| {
245 a.path
246 .cmp(&b.path)
247 .then(a.line.cmp(&b.line))
248 .then(a.package_name.cmp(&b.package_name))
249 });
250
251 self.test_only_dependencies.sort_by(|a, b| {
252 a.path
253 .cmp(&b.path)
254 .then(a.line.cmp(&b.line))
255 .then(a.package_name.cmp(&b.package_name))
256 });
257
258 self.circular_dependencies
259 .sort_by(|a, b| a.files.cmp(&b.files).then(a.length.cmp(&b.length)));
260
261 self.boundary_violations.sort_by(|a, b| {
262 a.from_path
263 .cmp(&b.from_path)
264 .then(a.line.cmp(&b.line))
265 .then(a.col.cmp(&b.col))
266 .then(a.to_path.cmp(&b.to_path))
267 });
268
269 self.stale_suppressions.sort_by(|a, b| {
270 a.path
271 .cmp(&b.path)
272 .then(a.line.cmp(&b.line))
273 .then(a.col.cmp(&b.col))
274 });
275
276 self.feature_flags.sort_by(|a, b| {
277 a.path
278 .cmp(&b.path)
279 .then(a.line.cmp(&b.line))
280 .then(a.flag_name.cmp(&b.flag_name))
281 });
282
283 for usage in &mut self.export_usages {
284 usage.reference_locations.sort_by(|a, b| {
285 a.path
286 .cmp(&b.path)
287 .then(a.line.cmp(&b.line))
288 .then(a.col.cmp(&b.col))
289 });
290 }
291 self.export_usages.sort_by(|a, b| {
292 a.path
293 .cmp(&b.path)
294 .then(a.line.cmp(&b.line))
295 .then(a.export_name.cmp(&b.export_name))
296 });
297 }
298}
299
300#[derive(Debug, Clone, Serialize)]
302pub struct UnusedFile {
303 #[serde(serialize_with = "serde_path::serialize")]
305 pub path: PathBuf,
306}
307
308#[derive(Debug, Clone, Serialize)]
310pub struct UnusedExport {
311 #[serde(serialize_with = "serde_path::serialize")]
313 pub path: PathBuf,
314 pub export_name: String,
316 pub is_type_only: bool,
318 pub line: u32,
320 pub col: u32,
322 pub span_start: u32,
324 pub is_re_export: bool,
326}
327
328#[derive(Debug, Clone, Serialize)]
330pub struct PrivateTypeLeak {
331 #[serde(serialize_with = "serde_path::serialize")]
333 pub path: PathBuf,
334 pub export_name: String,
336 pub type_name: String,
338 pub line: u32,
340 pub col: u32,
342 pub span_start: u32,
344}
345
346#[derive(Debug, Clone, Serialize)]
348pub struct UnusedDependency {
349 pub package_name: String,
351 pub location: DependencyLocation,
353 #[serde(serialize_with = "serde_path::serialize")]
356 pub path: PathBuf,
357 pub line: u32,
359 #[serde(
361 serialize_with = "serde_path::serialize_vec",
362 skip_serializing_if = "Vec::is_empty"
363 )]
364 pub used_in_workspaces: Vec<PathBuf>,
365}
366
367#[derive(Debug, Clone, Serialize)]
384#[serde(rename_all = "camelCase")]
385pub enum DependencyLocation {
386 Dependencies,
388 DevDependencies,
390 OptionalDependencies,
392}
393
394#[derive(Debug, Clone, Serialize)]
396pub struct UnusedMember {
397 #[serde(serialize_with = "serde_path::serialize")]
399 pub path: PathBuf,
400 pub parent_name: String,
402 pub member_name: String,
404 pub kind: MemberKind,
406 pub line: u32,
408 pub col: u32,
410}
411
412#[derive(Debug, Clone, Serialize)]
414pub struct UnresolvedImport {
415 #[serde(serialize_with = "serde_path::serialize")]
417 pub path: PathBuf,
418 pub specifier: String,
420 pub line: u32,
422 pub col: u32,
424 pub specifier_col: u32,
427}
428
429#[derive(Debug, Clone, Serialize)]
431pub struct UnlistedDependency {
432 pub package_name: String,
434 pub imported_from: Vec<ImportSite>,
436}
437
438#[derive(Debug, Clone, Serialize)]
440pub struct ImportSite {
441 #[serde(serialize_with = "serde_path::serialize")]
443 pub path: PathBuf,
444 pub line: u32,
446 pub col: u32,
448}
449
450#[derive(Debug, Clone, Serialize)]
452pub struct DuplicateExport {
453 pub export_name: String,
455 pub locations: Vec<DuplicateLocation>,
457}
458
459#[derive(Debug, Clone, Serialize)]
461pub struct DuplicateLocation {
462 #[serde(serialize_with = "serde_path::serialize")]
464 pub path: PathBuf,
465 pub line: u32,
467 pub col: u32,
469}
470
471#[derive(Debug, Clone, Serialize)]
475pub struct TypeOnlyDependency {
476 pub package_name: String,
478 #[serde(serialize_with = "serde_path::serialize")]
480 pub path: PathBuf,
481 pub line: u32,
483}
484
485#[derive(Debug, Clone, Serialize)]
488pub struct TestOnlyDependency {
489 pub package_name: String,
491 #[serde(serialize_with = "serde_path::serialize")]
493 pub path: PathBuf,
494 pub line: u32,
496}
497
498#[derive(Debug, Clone, Serialize, Deserialize)]
500pub struct CircularDependency {
501 #[serde(serialize_with = "serde_path::serialize_vec")]
503 pub files: Vec<PathBuf>,
504 pub length: usize,
506 #[serde(default)]
508 pub line: u32,
509 #[serde(default)]
511 pub col: u32,
512 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
514 pub is_cross_package: bool,
515}
516
517#[derive(Debug, Clone, Serialize)]
519pub struct BoundaryViolation {
520 #[serde(serialize_with = "serde_path::serialize")]
522 pub from_path: PathBuf,
523 #[serde(serialize_with = "serde_path::serialize")]
525 pub to_path: PathBuf,
526 pub from_zone: String,
528 pub to_zone: String,
530 pub import_specifier: String,
532 pub line: u32,
534 pub col: u32,
536}
537
538#[derive(Debug, Clone, Serialize)]
540#[serde(rename_all = "snake_case", tag = "type")]
541pub enum SuppressionOrigin {
542 Comment {
544 #[serde(skip_serializing_if = "Option::is_none")]
546 issue_kind: Option<String>,
547 is_file_level: bool,
549 },
550 JsdocTag {
552 export_name: String,
554 },
555}
556
557#[derive(Debug, Clone, Serialize)]
559pub struct StaleSuppression {
560 #[serde(serialize_with = "serde_path::serialize")]
562 pub path: PathBuf,
563 pub line: u32,
565 pub col: u32,
567 pub origin: SuppressionOrigin,
569}
570
571impl StaleSuppression {
572 #[must_use]
574 pub fn description(&self) -> String {
575 match &self.origin {
576 SuppressionOrigin::Comment {
577 issue_kind,
578 is_file_level,
579 } => {
580 let directive = if *is_file_level {
581 "fallow-ignore-file"
582 } else {
583 "fallow-ignore-next-line"
584 };
585 match issue_kind {
586 Some(kind) => format!("// {directive} {kind}"),
587 None => format!("// {directive}"),
588 }
589 }
590 SuppressionOrigin::JsdocTag { export_name } => {
591 format!("@expected-unused on {export_name}")
592 }
593 }
594 }
595
596 #[must_use]
598 pub fn explanation(&self) -> String {
599 match &self.origin {
600 SuppressionOrigin::Comment {
601 issue_kind,
602 is_file_level,
603 } => {
604 let scope = if *is_file_level {
605 "in this file"
606 } else {
607 "on the next line"
608 };
609 match issue_kind {
610 Some(kind) => format!("no {kind} issue found {scope}"),
611 None => format!("no issues found {scope}"),
612 }
613 }
614 SuppressionOrigin::JsdocTag { export_name } => {
615 format!("{export_name} is now used")
616 }
617 }
618 }
619
620 #[must_use]
622 pub fn suppressed_kind(&self) -> Option<IssueKind> {
623 match &self.origin {
624 SuppressionOrigin::Comment { issue_kind, .. } => {
625 issue_kind.as_deref().and_then(IssueKind::parse)
626 }
627 SuppressionOrigin::JsdocTag { .. } => None,
628 }
629 }
630}
631
632#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
634#[serde(rename_all = "snake_case")]
635pub enum FlagKind {
636 EnvironmentVariable,
638 SdkCall,
640 ConfigObject,
642}
643
644#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
646#[serde(rename_all = "snake_case")]
647pub enum FlagConfidence {
648 Low,
650 Medium,
652 High,
654}
655
656#[derive(Debug, Clone, Serialize)]
658pub struct FeatureFlag {
659 #[serde(serialize_with = "serde_path::serialize")]
661 pub path: PathBuf,
662 pub flag_name: String,
664 pub kind: FlagKind,
666 pub confidence: FlagConfidence,
668 pub line: u32,
670 pub col: u32,
672 #[serde(skip)]
674 pub guard_span_start: Option<u32>,
675 #[serde(skip)]
677 pub guard_span_end: Option<u32>,
678 #[serde(skip_serializing_if = "Option::is_none")]
680 pub sdk_name: Option<String>,
681 #[serde(skip)]
684 pub guard_line_start: Option<u32>,
685 #[serde(skip)]
687 pub guard_line_end: Option<u32>,
688 #[serde(skip_serializing_if = "Vec::is_empty")]
691 pub guarded_dead_exports: Vec<String>,
692}
693
694const _: () = assert!(std::mem::size_of::<FeatureFlag>() <= 160);
696
697#[derive(Debug, Clone, Serialize)]
700pub struct ExportUsage {
701 #[serde(serialize_with = "serde_path::serialize")]
703 pub path: PathBuf,
704 pub export_name: String,
706 pub line: u32,
708 pub col: u32,
710 pub reference_count: usize,
712 pub reference_locations: Vec<ReferenceLocation>,
715}
716
717#[derive(Debug, Clone, Serialize)]
719pub struct ReferenceLocation {
720 #[serde(serialize_with = "serde_path::serialize")]
722 pub path: PathBuf,
723 pub line: u32,
725 pub col: u32,
727}
728
729#[cfg(test)]
730mod tests {
731 use super::*;
732
733 #[test]
734 fn empty_results_no_issues() {
735 let results = AnalysisResults::default();
736 assert_eq!(results.total_issues(), 0);
737 assert!(!results.has_issues());
738 }
739
740 #[test]
741 fn results_with_unused_file() {
742 let mut results = AnalysisResults::default();
743 results.unused_files.push(UnusedFile {
744 path: PathBuf::from("test.ts"),
745 });
746 assert_eq!(results.total_issues(), 1);
747 assert!(results.has_issues());
748 }
749
750 #[test]
751 fn results_with_unused_export() {
752 let mut results = AnalysisResults::default();
753 results.unused_exports.push(UnusedExport {
754 path: PathBuf::from("test.ts"),
755 export_name: "foo".to_string(),
756 is_type_only: false,
757 line: 1,
758 col: 0,
759 span_start: 0,
760 is_re_export: false,
761 });
762 assert_eq!(results.total_issues(), 1);
763 assert!(results.has_issues());
764 }
765
766 #[test]
767 fn results_total_counts_all_types() {
768 let mut results = AnalysisResults::default();
769 results.unused_files.push(UnusedFile {
770 path: PathBuf::from("a.ts"),
771 });
772 results.unused_exports.push(UnusedExport {
773 path: PathBuf::from("b.ts"),
774 export_name: "x".to_string(),
775 is_type_only: false,
776 line: 1,
777 col: 0,
778 span_start: 0,
779 is_re_export: false,
780 });
781 results.unused_types.push(UnusedExport {
782 path: PathBuf::from("c.ts"),
783 export_name: "T".to_string(),
784 is_type_only: true,
785 line: 1,
786 col: 0,
787 span_start: 0,
788 is_re_export: false,
789 });
790 results.unused_dependencies.push(UnusedDependency {
791 package_name: "dep".to_string(),
792 location: DependencyLocation::Dependencies,
793 path: PathBuf::from("package.json"),
794 line: 5,
795 used_in_workspaces: Vec::new(),
796 });
797 results.unused_dev_dependencies.push(UnusedDependency {
798 package_name: "dev".to_string(),
799 location: DependencyLocation::DevDependencies,
800 path: PathBuf::from("package.json"),
801 line: 5,
802 used_in_workspaces: Vec::new(),
803 });
804 results.unused_enum_members.push(UnusedMember {
805 path: PathBuf::from("d.ts"),
806 parent_name: "E".to_string(),
807 member_name: "A".to_string(),
808 kind: MemberKind::EnumMember,
809 line: 1,
810 col: 0,
811 });
812 results.unused_class_members.push(UnusedMember {
813 path: PathBuf::from("e.ts"),
814 parent_name: "C".to_string(),
815 member_name: "m".to_string(),
816 kind: MemberKind::ClassMethod,
817 line: 1,
818 col: 0,
819 });
820 results.unresolved_imports.push(UnresolvedImport {
821 path: PathBuf::from("f.ts"),
822 specifier: "./missing".to_string(),
823 line: 1,
824 col: 0,
825 specifier_col: 0,
826 });
827 results.unlisted_dependencies.push(UnlistedDependency {
828 package_name: "unlisted".to_string(),
829 imported_from: vec![ImportSite {
830 path: PathBuf::from("g.ts"),
831 line: 1,
832 col: 0,
833 }],
834 });
835 results.duplicate_exports.push(DuplicateExport {
836 export_name: "dup".to_string(),
837 locations: vec![
838 DuplicateLocation {
839 path: PathBuf::from("h.ts"),
840 line: 15,
841 col: 0,
842 },
843 DuplicateLocation {
844 path: PathBuf::from("i.ts"),
845 line: 30,
846 col: 0,
847 },
848 ],
849 });
850 results.unused_optional_dependencies.push(UnusedDependency {
851 package_name: "optional".to_string(),
852 location: DependencyLocation::OptionalDependencies,
853 path: PathBuf::from("package.json"),
854 line: 5,
855 used_in_workspaces: Vec::new(),
856 });
857 results.type_only_dependencies.push(TypeOnlyDependency {
858 package_name: "type-only".to_string(),
859 path: PathBuf::from("package.json"),
860 line: 8,
861 });
862 results.test_only_dependencies.push(TestOnlyDependency {
863 package_name: "test-only".to_string(),
864 path: PathBuf::from("package.json"),
865 line: 9,
866 });
867 results.circular_dependencies.push(CircularDependency {
868 files: vec![PathBuf::from("a.ts"), PathBuf::from("b.ts")],
869 length: 2,
870 line: 3,
871 col: 0,
872 is_cross_package: false,
873 });
874 results.boundary_violations.push(BoundaryViolation {
875 from_path: PathBuf::from("src/ui/Button.tsx"),
876 to_path: PathBuf::from("src/db/queries.ts"),
877 from_zone: "ui".to_string(),
878 to_zone: "database".to_string(),
879 import_specifier: "../db/queries".to_string(),
880 line: 3,
881 col: 0,
882 });
883
884 assert_eq!(results.total_issues(), 15);
886 assert!(results.has_issues());
887 }
888
889 #[test]
892 fn total_issues_and_has_issues_are_consistent() {
893 let results = AnalysisResults::default();
894 assert_eq!(results.total_issues(), 0);
895 assert!(!results.has_issues());
896 assert_eq!(results.total_issues() > 0, results.has_issues());
897 }
898
899 #[test]
902 fn total_issues_sums_all_categories_independently() {
903 let mut results = AnalysisResults::default();
904 results.unused_files.push(UnusedFile {
905 path: PathBuf::from("a.ts"),
906 });
907 assert_eq!(results.total_issues(), 1);
908
909 results.unused_files.push(UnusedFile {
910 path: PathBuf::from("b.ts"),
911 });
912 assert_eq!(results.total_issues(), 2);
913
914 results.unresolved_imports.push(UnresolvedImport {
915 path: PathBuf::from("c.ts"),
916 specifier: "./missing".to_string(),
917 line: 1,
918 col: 0,
919 specifier_col: 0,
920 });
921 assert_eq!(results.total_issues(), 3);
922 }
923
924 #[test]
927 fn default_results_all_fields_empty() {
928 let r = AnalysisResults::default();
929 assert!(r.unused_files.is_empty());
930 assert!(r.unused_exports.is_empty());
931 assert!(r.unused_types.is_empty());
932 assert!(r.unused_dependencies.is_empty());
933 assert!(r.unused_dev_dependencies.is_empty());
934 assert!(r.unused_optional_dependencies.is_empty());
935 assert!(r.unused_enum_members.is_empty());
936 assert!(r.unused_class_members.is_empty());
937 assert!(r.unresolved_imports.is_empty());
938 assert!(r.unlisted_dependencies.is_empty());
939 assert!(r.duplicate_exports.is_empty());
940 assert!(r.type_only_dependencies.is_empty());
941 assert!(r.test_only_dependencies.is_empty());
942 assert!(r.circular_dependencies.is_empty());
943 assert!(r.boundary_violations.is_empty());
944 assert!(r.export_usages.is_empty());
945 }
946
947 #[test]
950 fn entry_point_summary_default() {
951 let summary = EntryPointSummary::default();
952 assert_eq!(summary.total, 0);
953 assert!(summary.by_source.is_empty());
954 }
955
956 #[test]
957 fn entry_point_summary_not_in_default_results() {
958 let r = AnalysisResults::default();
959 assert!(r.entry_point_summary.is_none());
960 }
961
962 #[test]
963 fn entry_point_summary_some_preserves_data() {
964 let r = AnalysisResults {
965 entry_point_summary: Some(EntryPointSummary {
966 total: 5,
967 by_source: vec![("package.json".to_string(), 2), ("plugin".to_string(), 3)],
968 }),
969 ..AnalysisResults::default()
970 };
971 let summary = r.entry_point_summary.as_ref().unwrap();
972 assert_eq!(summary.total, 5);
973 assert_eq!(summary.by_source.len(), 2);
974 assert_eq!(summary.by_source[0], ("package.json".to_string(), 2));
975 }
976
977 #[test]
980 fn sort_unused_files_by_path() {
981 let mut r = AnalysisResults::default();
982 r.unused_files.push(UnusedFile {
983 path: PathBuf::from("z.ts"),
984 });
985 r.unused_files.push(UnusedFile {
986 path: PathBuf::from("a.ts"),
987 });
988 r.unused_files.push(UnusedFile {
989 path: PathBuf::from("m.ts"),
990 });
991 r.sort();
992 let paths: Vec<_> = r
993 .unused_files
994 .iter()
995 .map(|f| f.path.to_string_lossy().to_string())
996 .collect();
997 assert_eq!(paths, vec!["a.ts", "m.ts", "z.ts"]);
998 }
999
1000 #[test]
1003 fn sort_unused_exports_by_path_line_name() {
1004 let mut r = AnalysisResults::default();
1005 let mk = |path: &str, line: u32, name: &str| UnusedExport {
1006 path: PathBuf::from(path),
1007 export_name: name.to_string(),
1008 is_type_only: false,
1009 line,
1010 col: 0,
1011 span_start: 0,
1012 is_re_export: false,
1013 };
1014 r.unused_exports.push(mk("b.ts", 5, "beta"));
1015 r.unused_exports.push(mk("a.ts", 10, "zeta"));
1016 r.unused_exports.push(mk("a.ts", 10, "alpha"));
1017 r.unused_exports.push(mk("a.ts", 1, "gamma"));
1018 r.sort();
1019 let keys: Vec<_> = r
1020 .unused_exports
1021 .iter()
1022 .map(|e| format!("{}:{}:{}", e.path.to_string_lossy(), e.line, e.export_name))
1023 .collect();
1024 assert_eq!(
1025 keys,
1026 vec![
1027 "a.ts:1:gamma",
1028 "a.ts:10:alpha",
1029 "a.ts:10:zeta",
1030 "b.ts:5:beta"
1031 ]
1032 );
1033 }
1034
1035 #[test]
1038 fn sort_unused_types_by_path_line_name() {
1039 let mut r = AnalysisResults::default();
1040 let mk = |path: &str, line: u32, name: &str| UnusedExport {
1041 path: PathBuf::from(path),
1042 export_name: name.to_string(),
1043 is_type_only: true,
1044 line,
1045 col: 0,
1046 span_start: 0,
1047 is_re_export: false,
1048 };
1049 r.unused_types.push(mk("z.ts", 1, "Z"));
1050 r.unused_types.push(mk("a.ts", 1, "A"));
1051 r.sort();
1052 assert_eq!(r.unused_types[0].path, PathBuf::from("a.ts"));
1053 assert_eq!(r.unused_types[1].path, PathBuf::from("z.ts"));
1054 }
1055
1056 #[test]
1059 fn sort_unused_dependencies_by_path_line_name() {
1060 let mut r = AnalysisResults::default();
1061 let mk = |path: &str, line: u32, name: &str| UnusedDependency {
1062 package_name: name.to_string(),
1063 location: DependencyLocation::Dependencies,
1064 path: PathBuf::from(path),
1065 line,
1066 used_in_workspaces: Vec::new(),
1067 };
1068 r.unused_dependencies.push(mk("b/package.json", 3, "zlib"));
1069 r.unused_dependencies.push(mk("a/package.json", 5, "react"));
1070 r.unused_dependencies.push(mk("a/package.json", 5, "axios"));
1071 r.sort();
1072 let names: Vec<_> = r
1073 .unused_dependencies
1074 .iter()
1075 .map(|d| d.package_name.as_str())
1076 .collect();
1077 assert_eq!(names, vec!["axios", "react", "zlib"]);
1078 }
1079
1080 #[test]
1083 fn sort_unused_dev_dependencies() {
1084 let mut r = AnalysisResults::default();
1085 r.unused_dev_dependencies.push(UnusedDependency {
1086 package_name: "vitest".to_string(),
1087 location: DependencyLocation::DevDependencies,
1088 path: PathBuf::from("package.json"),
1089 line: 10,
1090 used_in_workspaces: Vec::new(),
1091 });
1092 r.unused_dev_dependencies.push(UnusedDependency {
1093 package_name: "jest".to_string(),
1094 location: DependencyLocation::DevDependencies,
1095 path: PathBuf::from("package.json"),
1096 line: 5,
1097 used_in_workspaces: Vec::new(),
1098 });
1099 r.sort();
1100 assert_eq!(r.unused_dev_dependencies[0].package_name, "jest");
1101 assert_eq!(r.unused_dev_dependencies[1].package_name, "vitest");
1102 }
1103
1104 #[test]
1107 fn sort_unused_optional_dependencies() {
1108 let mut r = AnalysisResults::default();
1109 r.unused_optional_dependencies.push(UnusedDependency {
1110 package_name: "zod".to_string(),
1111 location: DependencyLocation::OptionalDependencies,
1112 path: PathBuf::from("package.json"),
1113 line: 3,
1114 used_in_workspaces: Vec::new(),
1115 });
1116 r.unused_optional_dependencies.push(UnusedDependency {
1117 package_name: "ajv".to_string(),
1118 location: DependencyLocation::OptionalDependencies,
1119 path: PathBuf::from("package.json"),
1120 line: 2,
1121 used_in_workspaces: Vec::new(),
1122 });
1123 r.sort();
1124 assert_eq!(r.unused_optional_dependencies[0].package_name, "ajv");
1125 assert_eq!(r.unused_optional_dependencies[1].package_name, "zod");
1126 }
1127
1128 #[test]
1131 fn sort_unused_enum_members_by_path_line_parent_member() {
1132 let mut r = AnalysisResults::default();
1133 let mk = |path: &str, line: u32, parent: &str, member: &str| UnusedMember {
1134 path: PathBuf::from(path),
1135 parent_name: parent.to_string(),
1136 member_name: member.to_string(),
1137 kind: MemberKind::EnumMember,
1138 line,
1139 col: 0,
1140 };
1141 r.unused_enum_members.push(mk("a.ts", 5, "Status", "Z"));
1142 r.unused_enum_members.push(mk("a.ts", 5, "Status", "A"));
1143 r.unused_enum_members.push(mk("a.ts", 1, "Direction", "Up"));
1144 r.sort();
1145 let keys: Vec<_> = r
1146 .unused_enum_members
1147 .iter()
1148 .map(|m| format!("{}:{}", m.parent_name, m.member_name))
1149 .collect();
1150 assert_eq!(keys, vec!["Direction:Up", "Status:A", "Status:Z"]);
1151 }
1152
1153 #[test]
1156 fn sort_unused_class_members() {
1157 let mut r = AnalysisResults::default();
1158 let mk = |path: &str, line: u32, parent: &str, member: &str| UnusedMember {
1159 path: PathBuf::from(path),
1160 parent_name: parent.to_string(),
1161 member_name: member.to_string(),
1162 kind: MemberKind::ClassMethod,
1163 line,
1164 col: 0,
1165 };
1166 r.unused_class_members.push(mk("b.ts", 1, "Foo", "z"));
1167 r.unused_class_members.push(mk("a.ts", 1, "Bar", "a"));
1168 r.sort();
1169 assert_eq!(r.unused_class_members[0].path, PathBuf::from("a.ts"));
1170 assert_eq!(r.unused_class_members[1].path, PathBuf::from("b.ts"));
1171 }
1172
1173 #[test]
1176 fn sort_unresolved_imports_by_path_line_col_specifier() {
1177 let mut r = AnalysisResults::default();
1178 let mk = |path: &str, line: u32, col: u32, spec: &str| UnresolvedImport {
1179 path: PathBuf::from(path),
1180 specifier: spec.to_string(),
1181 line,
1182 col,
1183 specifier_col: 0,
1184 };
1185 r.unresolved_imports.push(mk("a.ts", 5, 0, "./z"));
1186 r.unresolved_imports.push(mk("a.ts", 5, 0, "./a"));
1187 r.unresolved_imports.push(mk("a.ts", 1, 0, "./m"));
1188 r.sort();
1189 let specs: Vec<_> = r
1190 .unresolved_imports
1191 .iter()
1192 .map(|i| i.specifier.as_str())
1193 .collect();
1194 assert_eq!(specs, vec!["./m", "./a", "./z"]);
1195 }
1196
1197 #[test]
1200 fn sort_unlisted_dependencies_by_name_and_inner_sites() {
1201 let mut r = AnalysisResults::default();
1202 r.unlisted_dependencies.push(UnlistedDependency {
1203 package_name: "zod".to_string(),
1204 imported_from: vec![
1205 ImportSite {
1206 path: PathBuf::from("b.ts"),
1207 line: 10,
1208 col: 0,
1209 },
1210 ImportSite {
1211 path: PathBuf::from("a.ts"),
1212 line: 1,
1213 col: 0,
1214 },
1215 ],
1216 });
1217 r.unlisted_dependencies.push(UnlistedDependency {
1218 package_name: "axios".to_string(),
1219 imported_from: vec![ImportSite {
1220 path: PathBuf::from("c.ts"),
1221 line: 1,
1222 col: 0,
1223 }],
1224 });
1225 r.sort();
1226
1227 assert_eq!(r.unlisted_dependencies[0].package_name, "axios");
1229 assert_eq!(r.unlisted_dependencies[1].package_name, "zod");
1230
1231 let zod_sites: Vec<_> = r.unlisted_dependencies[1]
1233 .imported_from
1234 .iter()
1235 .map(|s| s.path.to_string_lossy().to_string())
1236 .collect();
1237 assert_eq!(zod_sites, vec!["a.ts", "b.ts"]);
1238 }
1239
1240 #[test]
1243 fn sort_duplicate_exports_by_name_and_inner_locations() {
1244 let mut r = AnalysisResults::default();
1245 r.duplicate_exports.push(DuplicateExport {
1246 export_name: "z".to_string(),
1247 locations: vec![
1248 DuplicateLocation {
1249 path: PathBuf::from("c.ts"),
1250 line: 1,
1251 col: 0,
1252 },
1253 DuplicateLocation {
1254 path: PathBuf::from("a.ts"),
1255 line: 5,
1256 col: 0,
1257 },
1258 ],
1259 });
1260 r.duplicate_exports.push(DuplicateExport {
1261 export_name: "a".to_string(),
1262 locations: vec![DuplicateLocation {
1263 path: PathBuf::from("b.ts"),
1264 line: 1,
1265 col: 0,
1266 }],
1267 });
1268 r.sort();
1269
1270 assert_eq!(r.duplicate_exports[0].export_name, "a");
1272 assert_eq!(r.duplicate_exports[1].export_name, "z");
1273
1274 let z_locs: Vec<_> = r.duplicate_exports[1]
1276 .locations
1277 .iter()
1278 .map(|l| l.path.to_string_lossy().to_string())
1279 .collect();
1280 assert_eq!(z_locs, vec!["a.ts", "c.ts"]);
1281 }
1282
1283 #[test]
1286 fn sort_type_only_dependencies() {
1287 let mut r = AnalysisResults::default();
1288 r.type_only_dependencies.push(TypeOnlyDependency {
1289 package_name: "zod".to_string(),
1290 path: PathBuf::from("package.json"),
1291 line: 10,
1292 });
1293 r.type_only_dependencies.push(TypeOnlyDependency {
1294 package_name: "ajv".to_string(),
1295 path: PathBuf::from("package.json"),
1296 line: 5,
1297 });
1298 r.sort();
1299 assert_eq!(r.type_only_dependencies[0].package_name, "ajv");
1300 assert_eq!(r.type_only_dependencies[1].package_name, "zod");
1301 }
1302
1303 #[test]
1306 fn sort_test_only_dependencies() {
1307 let mut r = AnalysisResults::default();
1308 r.test_only_dependencies.push(TestOnlyDependency {
1309 package_name: "vitest".to_string(),
1310 path: PathBuf::from("package.json"),
1311 line: 15,
1312 });
1313 r.test_only_dependencies.push(TestOnlyDependency {
1314 package_name: "jest".to_string(),
1315 path: PathBuf::from("package.json"),
1316 line: 10,
1317 });
1318 r.sort();
1319 assert_eq!(r.test_only_dependencies[0].package_name, "jest");
1320 assert_eq!(r.test_only_dependencies[1].package_name, "vitest");
1321 }
1322
1323 #[test]
1326 fn sort_circular_dependencies_by_files_then_length() {
1327 let mut r = AnalysisResults::default();
1328 r.circular_dependencies.push(CircularDependency {
1329 files: vec![PathBuf::from("b.ts"), PathBuf::from("c.ts")],
1330 length: 2,
1331 line: 1,
1332 col: 0,
1333 is_cross_package: false,
1334 });
1335 r.circular_dependencies.push(CircularDependency {
1336 files: vec![PathBuf::from("a.ts"), PathBuf::from("b.ts")],
1337 length: 2,
1338 line: 1,
1339 col: 0,
1340 is_cross_package: true,
1341 });
1342 r.sort();
1343 assert_eq!(r.circular_dependencies[0].files[0], PathBuf::from("a.ts"));
1344 assert_eq!(r.circular_dependencies[1].files[0], PathBuf::from("b.ts"));
1345 }
1346
1347 #[test]
1350 fn sort_boundary_violations() {
1351 let mut r = AnalysisResults::default();
1352 let mk = |from: &str, line: u32, col: u32, to: &str| BoundaryViolation {
1353 from_path: PathBuf::from(from),
1354 to_path: PathBuf::from(to),
1355 from_zone: "a".to_string(),
1356 to_zone: "b".to_string(),
1357 import_specifier: to.to_string(),
1358 line,
1359 col,
1360 };
1361 r.boundary_violations.push(mk("z.ts", 1, 0, "a.ts"));
1362 r.boundary_violations.push(mk("a.ts", 5, 0, "b.ts"));
1363 r.boundary_violations.push(mk("a.ts", 1, 0, "c.ts"));
1364 r.sort();
1365 let from_paths: Vec<_> = r
1366 .boundary_violations
1367 .iter()
1368 .map(|v| format!("{}:{}", v.from_path.to_string_lossy(), v.line))
1369 .collect();
1370 assert_eq!(from_paths, vec!["a.ts:1", "a.ts:5", "z.ts:1"]);
1371 }
1372
1373 #[test]
1376 fn sort_export_usages_and_inner_reference_locations() {
1377 let mut r = AnalysisResults::default();
1378 r.export_usages.push(ExportUsage {
1379 path: PathBuf::from("z.ts"),
1380 export_name: "foo".to_string(),
1381 line: 1,
1382 col: 0,
1383 reference_count: 2,
1384 reference_locations: vec![
1385 ReferenceLocation {
1386 path: PathBuf::from("c.ts"),
1387 line: 10,
1388 col: 0,
1389 },
1390 ReferenceLocation {
1391 path: PathBuf::from("a.ts"),
1392 line: 5,
1393 col: 0,
1394 },
1395 ],
1396 });
1397 r.export_usages.push(ExportUsage {
1398 path: PathBuf::from("a.ts"),
1399 export_name: "bar".to_string(),
1400 line: 1,
1401 col: 0,
1402 reference_count: 1,
1403 reference_locations: vec![ReferenceLocation {
1404 path: PathBuf::from("b.ts"),
1405 line: 1,
1406 col: 0,
1407 }],
1408 });
1409 r.sort();
1410
1411 assert_eq!(r.export_usages[0].path, PathBuf::from("a.ts"));
1413 assert_eq!(r.export_usages[1].path, PathBuf::from("z.ts"));
1414
1415 let refs: Vec<_> = r.export_usages[1]
1417 .reference_locations
1418 .iter()
1419 .map(|l| l.path.to_string_lossy().to_string())
1420 .collect();
1421 assert_eq!(refs, vec!["a.ts", "c.ts"]);
1422 }
1423
1424 #[test]
1427 fn sort_empty_results_is_noop() {
1428 let mut r = AnalysisResults::default();
1429 r.sort(); assert_eq!(r.total_issues(), 0);
1431 }
1432
1433 #[test]
1436 fn sort_single_element_lists_stable() {
1437 let mut r = AnalysisResults::default();
1438 r.unused_files.push(UnusedFile {
1439 path: PathBuf::from("only.ts"),
1440 });
1441 r.sort();
1442 assert_eq!(r.unused_files[0].path, PathBuf::from("only.ts"));
1443 }
1444
1445 #[test]
1448 fn serialize_empty_results() {
1449 let r = AnalysisResults::default();
1450 let json = serde_json::to_value(&r).unwrap();
1451
1452 assert!(json["unused_files"].as_array().unwrap().is_empty());
1454 assert!(json["unused_exports"].as_array().unwrap().is_empty());
1455 assert!(json["circular_dependencies"].as_array().unwrap().is_empty());
1456
1457 assert!(json.get("export_usages").is_none());
1459 assert!(json.get("entry_point_summary").is_none());
1460 }
1461
1462 #[test]
1463 fn serialize_unused_file_path() {
1464 let r = UnusedFile {
1465 path: PathBuf::from("src/utils/index.ts"),
1466 };
1467 let json = serde_json::to_value(&r).unwrap();
1468 assert_eq!(json["path"], "src/utils/index.ts");
1469 }
1470
1471 #[test]
1472 fn serialize_dependency_location_camel_case() {
1473 let dep = UnusedDependency {
1474 package_name: "react".to_string(),
1475 location: DependencyLocation::DevDependencies,
1476 path: PathBuf::from("package.json"),
1477 line: 5,
1478 used_in_workspaces: Vec::new(),
1479 };
1480 let json = serde_json::to_value(&dep).unwrap();
1481 assert_eq!(json["location"], "devDependencies");
1482
1483 let dep2 = UnusedDependency {
1484 package_name: "react".to_string(),
1485 location: DependencyLocation::Dependencies,
1486 path: PathBuf::from("package.json"),
1487 line: 3,
1488 used_in_workspaces: Vec::new(),
1489 };
1490 let json2 = serde_json::to_value(&dep2).unwrap();
1491 assert_eq!(json2["location"], "dependencies");
1492
1493 let dep3 = UnusedDependency {
1494 package_name: "fsevents".to_string(),
1495 location: DependencyLocation::OptionalDependencies,
1496 path: PathBuf::from("package.json"),
1497 line: 7,
1498 used_in_workspaces: Vec::new(),
1499 };
1500 let json3 = serde_json::to_value(&dep3).unwrap();
1501 assert_eq!(json3["location"], "optionalDependencies");
1502 }
1503
1504 #[test]
1505 fn serialize_circular_dependency_skips_false_cross_package() {
1506 let cd = CircularDependency {
1507 files: vec![PathBuf::from("a.ts"), PathBuf::from("b.ts")],
1508 length: 2,
1509 line: 1,
1510 col: 0,
1511 is_cross_package: false,
1512 };
1513 let json = serde_json::to_value(&cd).unwrap();
1514 assert!(json.get("is_cross_package").is_none());
1516 }
1517
1518 #[test]
1519 fn serialize_circular_dependency_includes_true_cross_package() {
1520 let cd = CircularDependency {
1521 files: vec![PathBuf::from("a.ts"), PathBuf::from("b.ts")],
1522 length: 2,
1523 line: 1,
1524 col: 0,
1525 is_cross_package: true,
1526 };
1527 let json = serde_json::to_value(&cd).unwrap();
1528 assert_eq!(json["is_cross_package"], true);
1529 }
1530
1531 #[test]
1532 fn serialize_unused_export_fields() {
1533 let e = UnusedExport {
1534 path: PathBuf::from("src/mod.ts"),
1535 export_name: "helper".to_string(),
1536 is_type_only: true,
1537 line: 42,
1538 col: 7,
1539 span_start: 100,
1540 is_re_export: true,
1541 };
1542 let json = serde_json::to_value(&e).unwrap();
1543 assert_eq!(json["path"], "src/mod.ts");
1544 assert_eq!(json["export_name"], "helper");
1545 assert_eq!(json["is_type_only"], true);
1546 assert_eq!(json["line"], 42);
1547 assert_eq!(json["col"], 7);
1548 assert_eq!(json["span_start"], 100);
1549 assert_eq!(json["is_re_export"], true);
1550 }
1551
1552 #[test]
1553 fn serialize_boundary_violation_fields() {
1554 let v = BoundaryViolation {
1555 from_path: PathBuf::from("src/ui/button.tsx"),
1556 to_path: PathBuf::from("src/db/queries.ts"),
1557 from_zone: "ui".to_string(),
1558 to_zone: "db".to_string(),
1559 import_specifier: "../db/queries".to_string(),
1560 line: 3,
1561 col: 0,
1562 };
1563 let json = serde_json::to_value(&v).unwrap();
1564 assert_eq!(json["from_path"], "src/ui/button.tsx");
1565 assert_eq!(json["to_path"], "src/db/queries.ts");
1566 assert_eq!(json["from_zone"], "ui");
1567 assert_eq!(json["to_zone"], "db");
1568 assert_eq!(json["import_specifier"], "../db/queries");
1569 }
1570
1571 #[test]
1572 fn serialize_unlisted_dependency_with_import_sites() {
1573 let d = UnlistedDependency {
1574 package_name: "chalk".to_string(),
1575 imported_from: vec![
1576 ImportSite {
1577 path: PathBuf::from("a.ts"),
1578 line: 1,
1579 col: 0,
1580 },
1581 ImportSite {
1582 path: PathBuf::from("b.ts"),
1583 line: 5,
1584 col: 3,
1585 },
1586 ],
1587 };
1588 let json = serde_json::to_value(&d).unwrap();
1589 assert_eq!(json["package_name"], "chalk");
1590 let sites = json["imported_from"].as_array().unwrap();
1591 assert_eq!(sites.len(), 2);
1592 assert_eq!(sites[0]["path"], "a.ts");
1593 assert_eq!(sites[1]["line"], 5);
1594 }
1595
1596 #[test]
1597 fn serialize_duplicate_export_with_locations() {
1598 let d = DuplicateExport {
1599 export_name: "Button".to_string(),
1600 locations: vec![
1601 DuplicateLocation {
1602 path: PathBuf::from("src/a.ts"),
1603 line: 10,
1604 col: 0,
1605 },
1606 DuplicateLocation {
1607 path: PathBuf::from("src/b.ts"),
1608 line: 20,
1609 col: 5,
1610 },
1611 ],
1612 };
1613 let json = serde_json::to_value(&d).unwrap();
1614 assert_eq!(json["export_name"], "Button");
1615 let locs = json["locations"].as_array().unwrap();
1616 assert_eq!(locs.len(), 2);
1617 assert_eq!(locs[0]["line"], 10);
1618 assert_eq!(locs[1]["col"], 5);
1619 }
1620
1621 #[test]
1622 fn serialize_type_only_dependency() {
1623 let d = TypeOnlyDependency {
1624 package_name: "@types/react".to_string(),
1625 path: PathBuf::from("package.json"),
1626 line: 12,
1627 };
1628 let json = serde_json::to_value(&d).unwrap();
1629 assert_eq!(json["package_name"], "@types/react");
1630 assert_eq!(json["line"], 12);
1631 }
1632
1633 #[test]
1634 fn serialize_test_only_dependency() {
1635 let d = TestOnlyDependency {
1636 package_name: "vitest".to_string(),
1637 path: PathBuf::from("package.json"),
1638 line: 8,
1639 };
1640 let json = serde_json::to_value(&d).unwrap();
1641 assert_eq!(json["package_name"], "vitest");
1642 assert_eq!(json["line"], 8);
1643 }
1644
1645 #[test]
1646 fn serialize_unused_member() {
1647 let m = UnusedMember {
1648 path: PathBuf::from("enums.ts"),
1649 parent_name: "Status".to_string(),
1650 member_name: "Pending".to_string(),
1651 kind: MemberKind::EnumMember,
1652 line: 3,
1653 col: 4,
1654 };
1655 let json = serde_json::to_value(&m).unwrap();
1656 assert_eq!(json["parent_name"], "Status");
1657 assert_eq!(json["member_name"], "Pending");
1658 assert_eq!(json["line"], 3);
1659 }
1660
1661 #[test]
1662 fn serialize_unresolved_import() {
1663 let i = UnresolvedImport {
1664 path: PathBuf::from("app.ts"),
1665 specifier: "./missing-module".to_string(),
1666 line: 7,
1667 col: 0,
1668 specifier_col: 21,
1669 };
1670 let json = serde_json::to_value(&i).unwrap();
1671 assert_eq!(json["specifier"], "./missing-module");
1672 assert_eq!(json["specifier_col"], 21);
1673 }
1674
1675 #[test]
1678 fn deserialize_circular_dependency_with_defaults() {
1679 let json = r#"{"files":["a.ts","b.ts"],"length":2}"#;
1681 let cd: CircularDependency = serde_json::from_str(json).unwrap();
1682 assert_eq!(cd.files.len(), 2);
1683 assert_eq!(cd.length, 2);
1684 assert_eq!(cd.line, 0);
1685 assert_eq!(cd.col, 0);
1686 assert!(!cd.is_cross_package);
1687 }
1688
1689 #[test]
1690 fn deserialize_circular_dependency_with_all_fields() {
1691 let json =
1692 r#"{"files":["a.ts","b.ts"],"length":2,"line":5,"col":10,"is_cross_package":true}"#;
1693 let cd: CircularDependency = serde_json::from_str(json).unwrap();
1694 assert_eq!(cd.line, 5);
1695 assert_eq!(cd.col, 10);
1696 assert!(cd.is_cross_package);
1697 }
1698
1699 #[test]
1702 fn clone_results_are_independent() {
1703 let mut r = AnalysisResults::default();
1704 r.unused_files.push(UnusedFile {
1705 path: PathBuf::from("a.ts"),
1706 });
1707 let mut cloned = r.clone();
1708 cloned.unused_files.push(UnusedFile {
1709 path: PathBuf::from("b.ts"),
1710 });
1711 assert_eq!(r.total_issues(), 1);
1712 assert_eq!(cloned.total_issues(), 2);
1713 }
1714
1715 #[test]
1718 fn export_usages_not_counted_in_total_issues() {
1719 let mut r = AnalysisResults::default();
1720 r.export_usages.push(ExportUsage {
1721 path: PathBuf::from("mod.ts"),
1722 export_name: "foo".to_string(),
1723 line: 1,
1724 col: 0,
1725 reference_count: 3,
1726 reference_locations: vec![],
1727 });
1728 assert_eq!(r.total_issues(), 0);
1730 assert!(!r.has_issues());
1731 }
1732
1733 #[test]
1736 fn entry_point_summary_not_counted_in_total_issues() {
1737 let r = AnalysisResults {
1738 entry_point_summary: Some(EntryPointSummary {
1739 total: 10,
1740 by_source: vec![("config".to_string(), 10)],
1741 }),
1742 ..AnalysisResults::default()
1743 };
1744 assert_eq!(r.total_issues(), 0);
1745 assert!(!r.has_issues());
1746 }
1747}