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