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