1use std::path::PathBuf;
4
5use serde::{Deserialize, Serialize};
6
7use crate::extract::MemberKind;
8use crate::serde_path;
9
10#[derive(Debug, Clone, Default)]
15pub struct EntryPointSummary {
16 pub total: usize,
18 pub by_source: Vec<(String, usize)>,
21}
22
23#[derive(Debug, Default, Clone, Serialize)]
42pub struct AnalysisResults {
43 pub unused_files: Vec<UnusedFile>,
45 pub unused_exports: Vec<UnusedExport>,
47 pub unused_types: Vec<UnusedExport>,
49 pub unused_dependencies: Vec<UnusedDependency>,
51 pub unused_dev_dependencies: Vec<UnusedDependency>,
53 pub unused_optional_dependencies: Vec<UnusedDependency>,
55 pub unused_enum_members: Vec<UnusedMember>,
57 pub unused_class_members: Vec<UnusedMember>,
59 pub unresolved_imports: Vec<UnresolvedImport>,
61 pub unlisted_dependencies: Vec<UnlistedDependency>,
63 pub duplicate_exports: Vec<DuplicateExport>,
65 pub type_only_dependencies: Vec<TypeOnlyDependency>,
68 #[serde(default)]
70 pub test_only_dependencies: Vec<TestOnlyDependency>,
71 pub circular_dependencies: Vec<CircularDependency>,
73 #[serde(default)]
75 pub boundary_violations: Vec<BoundaryViolation>,
76 #[serde(skip)]
80 pub export_usages: Vec<ExportUsage>,
81 #[serde(skip)]
85 pub entry_point_summary: Option<EntryPointSummary>,
86}
87
88impl AnalysisResults {
89 #[must_use]
113 pub const fn total_issues(&self) -> usize {
114 self.unused_files.len()
115 + self.unused_exports.len()
116 + self.unused_types.len()
117 + self.unused_dependencies.len()
118 + self.unused_dev_dependencies.len()
119 + self.unused_optional_dependencies.len()
120 + self.unused_enum_members.len()
121 + self.unused_class_members.len()
122 + self.unresolved_imports.len()
123 + self.unlisted_dependencies.len()
124 + self.duplicate_exports.len()
125 + self.type_only_dependencies.len()
126 + self.test_only_dependencies.len()
127 + self.circular_dependencies.len()
128 + self.boundary_violations.len()
129 }
130
131 #[must_use]
133 pub const fn has_issues(&self) -> bool {
134 self.total_issues() > 0
135 }
136
137 pub fn sort(&mut self) {
144 self.unused_files.sort_by(|a, b| a.path.cmp(&b.path));
145
146 self.unused_exports.sort_by(|a, b| {
147 a.path
148 .cmp(&b.path)
149 .then(a.line.cmp(&b.line))
150 .then(a.export_name.cmp(&b.export_name))
151 });
152
153 self.unused_types.sort_by(|a, b| {
154 a.path
155 .cmp(&b.path)
156 .then(a.line.cmp(&b.line))
157 .then(a.export_name.cmp(&b.export_name))
158 });
159
160 self.unused_dependencies.sort_by(|a, b| {
161 a.path
162 .cmp(&b.path)
163 .then(a.line.cmp(&b.line))
164 .then(a.package_name.cmp(&b.package_name))
165 });
166
167 self.unused_dev_dependencies.sort_by(|a, b| {
168 a.path
169 .cmp(&b.path)
170 .then(a.line.cmp(&b.line))
171 .then(a.package_name.cmp(&b.package_name))
172 });
173
174 self.unused_optional_dependencies.sort_by(|a, b| {
175 a.path
176 .cmp(&b.path)
177 .then(a.line.cmp(&b.line))
178 .then(a.package_name.cmp(&b.package_name))
179 });
180
181 self.unused_enum_members.sort_by(|a, b| {
182 a.path
183 .cmp(&b.path)
184 .then(a.line.cmp(&b.line))
185 .then(a.parent_name.cmp(&b.parent_name))
186 .then(a.member_name.cmp(&b.member_name))
187 });
188
189 self.unused_class_members.sort_by(|a, b| {
190 a.path
191 .cmp(&b.path)
192 .then(a.line.cmp(&b.line))
193 .then(a.parent_name.cmp(&b.parent_name))
194 .then(a.member_name.cmp(&b.member_name))
195 });
196
197 self.unresolved_imports.sort_by(|a, b| {
198 a.path
199 .cmp(&b.path)
200 .then(a.line.cmp(&b.line))
201 .then(a.col.cmp(&b.col))
202 .then(a.specifier.cmp(&b.specifier))
203 });
204
205 self.unlisted_dependencies
206 .sort_by(|a, b| a.package_name.cmp(&b.package_name));
207 for dep in &mut self.unlisted_dependencies {
208 dep.imported_from
209 .sort_by(|a, b| a.path.cmp(&b.path).then(a.line.cmp(&b.line)));
210 }
211
212 self.duplicate_exports
213 .sort_by(|a, b| a.export_name.cmp(&b.export_name));
214 for dup in &mut self.duplicate_exports {
215 dup.locations
216 .sort_by(|a, b| a.path.cmp(&b.path).then(a.line.cmp(&b.line)));
217 }
218
219 self.type_only_dependencies.sort_by(|a, b| {
220 a.path
221 .cmp(&b.path)
222 .then(a.line.cmp(&b.line))
223 .then(a.package_name.cmp(&b.package_name))
224 });
225
226 self.test_only_dependencies.sort_by(|a, b| {
227 a.path
228 .cmp(&b.path)
229 .then(a.line.cmp(&b.line))
230 .then(a.package_name.cmp(&b.package_name))
231 });
232
233 self.circular_dependencies
234 .sort_by(|a, b| a.files.cmp(&b.files).then(a.length.cmp(&b.length)));
235
236 self.boundary_violations.sort_by(|a, b| {
237 a.from_path
238 .cmp(&b.from_path)
239 .then(a.line.cmp(&b.line))
240 .then(a.col.cmp(&b.col))
241 .then(a.to_path.cmp(&b.to_path))
242 });
243
244 for usage in &mut self.export_usages {
245 usage.reference_locations.sort_by(|a, b| {
246 a.path
247 .cmp(&b.path)
248 .then(a.line.cmp(&b.line))
249 .then(a.col.cmp(&b.col))
250 });
251 }
252 self.export_usages.sort_by(|a, b| {
253 a.path
254 .cmp(&b.path)
255 .then(a.line.cmp(&b.line))
256 .then(a.export_name.cmp(&b.export_name))
257 });
258 }
259}
260
261#[derive(Debug, Clone, Serialize)]
263pub struct UnusedFile {
264 #[serde(serialize_with = "serde_path::serialize")]
266 pub path: PathBuf,
267}
268
269#[derive(Debug, Clone, Serialize)]
271pub struct UnusedExport {
272 #[serde(serialize_with = "serde_path::serialize")]
274 pub path: PathBuf,
275 pub export_name: String,
277 pub is_type_only: bool,
279 pub line: u32,
281 pub col: u32,
283 pub span_start: u32,
285 pub is_re_export: bool,
287}
288
289#[derive(Debug, Clone, Serialize)]
291pub struct UnusedDependency {
292 pub package_name: String,
294 pub location: DependencyLocation,
296 #[serde(serialize_with = "serde_path::serialize")]
299 pub path: PathBuf,
300 pub line: u32,
302}
303
304#[derive(Debug, Clone, Serialize)]
321#[serde(rename_all = "camelCase")]
322pub enum DependencyLocation {
323 Dependencies,
325 DevDependencies,
327 OptionalDependencies,
329}
330
331#[derive(Debug, Clone, Serialize)]
333pub struct UnusedMember {
334 #[serde(serialize_with = "serde_path::serialize")]
336 pub path: PathBuf,
337 pub parent_name: String,
339 pub member_name: String,
341 pub kind: MemberKind,
343 pub line: u32,
345 pub col: u32,
347}
348
349#[derive(Debug, Clone, Serialize)]
351pub struct UnresolvedImport {
352 #[serde(serialize_with = "serde_path::serialize")]
354 pub path: PathBuf,
355 pub specifier: String,
357 pub line: u32,
359 pub col: u32,
361 pub specifier_col: u32,
364}
365
366#[derive(Debug, Clone, Serialize)]
368pub struct UnlistedDependency {
369 pub package_name: String,
371 pub imported_from: Vec<ImportSite>,
373}
374
375#[derive(Debug, Clone, Serialize)]
377pub struct ImportSite {
378 #[serde(serialize_with = "serde_path::serialize")]
380 pub path: PathBuf,
381 pub line: u32,
383 pub col: u32,
385}
386
387#[derive(Debug, Clone, Serialize)]
389pub struct DuplicateExport {
390 pub export_name: String,
392 pub locations: Vec<DuplicateLocation>,
394}
395
396#[derive(Debug, Clone, Serialize)]
398pub struct DuplicateLocation {
399 #[serde(serialize_with = "serde_path::serialize")]
401 pub path: PathBuf,
402 pub line: u32,
404 pub col: u32,
406}
407
408#[derive(Debug, Clone, Serialize)]
412pub struct TypeOnlyDependency {
413 pub package_name: String,
415 #[serde(serialize_with = "serde_path::serialize")]
417 pub path: PathBuf,
418 pub line: u32,
420}
421
422#[derive(Debug, Clone, Serialize)]
425pub struct TestOnlyDependency {
426 pub package_name: String,
428 #[serde(serialize_with = "serde_path::serialize")]
430 pub path: PathBuf,
431 pub line: u32,
433}
434
435#[derive(Debug, Clone, Serialize, Deserialize)]
437pub struct CircularDependency {
438 #[serde(serialize_with = "serde_path::serialize_vec")]
440 pub files: Vec<PathBuf>,
441 pub length: usize,
443 #[serde(default)]
445 pub line: u32,
446 #[serde(default)]
448 pub col: u32,
449 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
451 pub is_cross_package: bool,
452}
453
454#[derive(Debug, Clone, Serialize)]
456pub struct BoundaryViolation {
457 #[serde(serialize_with = "serde_path::serialize")]
459 pub from_path: PathBuf,
460 #[serde(serialize_with = "serde_path::serialize")]
462 pub to_path: PathBuf,
463 pub from_zone: String,
465 pub to_zone: String,
467 pub import_specifier: String,
469 pub line: u32,
471 pub col: u32,
473}
474
475#[derive(Debug, Clone, Serialize)]
478pub struct ExportUsage {
479 #[serde(serialize_with = "serde_path::serialize")]
481 pub path: PathBuf,
482 pub export_name: String,
484 pub line: u32,
486 pub col: u32,
488 pub reference_count: usize,
490 pub reference_locations: Vec<ReferenceLocation>,
493}
494
495#[derive(Debug, Clone, Serialize)]
497pub struct ReferenceLocation {
498 #[serde(serialize_with = "serde_path::serialize")]
500 pub path: PathBuf,
501 pub line: u32,
503 pub col: u32,
505}
506
507#[cfg(test)]
508mod tests {
509 use super::*;
510
511 #[test]
512 fn empty_results_no_issues() {
513 let results = AnalysisResults::default();
514 assert_eq!(results.total_issues(), 0);
515 assert!(!results.has_issues());
516 }
517
518 #[test]
519 fn results_with_unused_file() {
520 let mut results = AnalysisResults::default();
521 results.unused_files.push(UnusedFile {
522 path: PathBuf::from("test.ts"),
523 });
524 assert_eq!(results.total_issues(), 1);
525 assert!(results.has_issues());
526 }
527
528 #[test]
529 fn results_with_unused_export() {
530 let mut results = AnalysisResults::default();
531 results.unused_exports.push(UnusedExport {
532 path: PathBuf::from("test.ts"),
533 export_name: "foo".to_string(),
534 is_type_only: false,
535 line: 1,
536 col: 0,
537 span_start: 0,
538 is_re_export: false,
539 });
540 assert_eq!(results.total_issues(), 1);
541 assert!(results.has_issues());
542 }
543
544 #[test]
545 fn results_total_counts_all_types() {
546 let mut results = AnalysisResults::default();
547 results.unused_files.push(UnusedFile {
548 path: PathBuf::from("a.ts"),
549 });
550 results.unused_exports.push(UnusedExport {
551 path: PathBuf::from("b.ts"),
552 export_name: "x".to_string(),
553 is_type_only: false,
554 line: 1,
555 col: 0,
556 span_start: 0,
557 is_re_export: false,
558 });
559 results.unused_types.push(UnusedExport {
560 path: PathBuf::from("c.ts"),
561 export_name: "T".to_string(),
562 is_type_only: true,
563 line: 1,
564 col: 0,
565 span_start: 0,
566 is_re_export: false,
567 });
568 results.unused_dependencies.push(UnusedDependency {
569 package_name: "dep".to_string(),
570 location: DependencyLocation::Dependencies,
571 path: PathBuf::from("package.json"),
572 line: 5,
573 });
574 results.unused_dev_dependencies.push(UnusedDependency {
575 package_name: "dev".to_string(),
576 location: DependencyLocation::DevDependencies,
577 path: PathBuf::from("package.json"),
578 line: 5,
579 });
580 results.unused_enum_members.push(UnusedMember {
581 path: PathBuf::from("d.ts"),
582 parent_name: "E".to_string(),
583 member_name: "A".to_string(),
584 kind: MemberKind::EnumMember,
585 line: 1,
586 col: 0,
587 });
588 results.unused_class_members.push(UnusedMember {
589 path: PathBuf::from("e.ts"),
590 parent_name: "C".to_string(),
591 member_name: "m".to_string(),
592 kind: MemberKind::ClassMethod,
593 line: 1,
594 col: 0,
595 });
596 results.unresolved_imports.push(UnresolvedImport {
597 path: PathBuf::from("f.ts"),
598 specifier: "./missing".to_string(),
599 line: 1,
600 col: 0,
601 specifier_col: 0,
602 });
603 results.unlisted_dependencies.push(UnlistedDependency {
604 package_name: "unlisted".to_string(),
605 imported_from: vec![ImportSite {
606 path: PathBuf::from("g.ts"),
607 line: 1,
608 col: 0,
609 }],
610 });
611 results.duplicate_exports.push(DuplicateExport {
612 export_name: "dup".to_string(),
613 locations: vec![
614 DuplicateLocation {
615 path: PathBuf::from("h.ts"),
616 line: 15,
617 col: 0,
618 },
619 DuplicateLocation {
620 path: PathBuf::from("i.ts"),
621 line: 30,
622 col: 0,
623 },
624 ],
625 });
626 results.unused_optional_dependencies.push(UnusedDependency {
627 package_name: "optional".to_string(),
628 location: DependencyLocation::OptionalDependencies,
629 path: PathBuf::from("package.json"),
630 line: 5,
631 });
632 results.type_only_dependencies.push(TypeOnlyDependency {
633 package_name: "type-only".to_string(),
634 path: PathBuf::from("package.json"),
635 line: 8,
636 });
637 results.test_only_dependencies.push(TestOnlyDependency {
638 package_name: "test-only".to_string(),
639 path: PathBuf::from("package.json"),
640 line: 9,
641 });
642 results.circular_dependencies.push(CircularDependency {
643 files: vec![PathBuf::from("a.ts"), PathBuf::from("b.ts")],
644 length: 2,
645 line: 3,
646 col: 0,
647 is_cross_package: false,
648 });
649 results.boundary_violations.push(BoundaryViolation {
650 from_path: PathBuf::from("src/ui/Button.tsx"),
651 to_path: PathBuf::from("src/db/queries.ts"),
652 from_zone: "ui".to_string(),
653 to_zone: "database".to_string(),
654 import_specifier: "../db/queries".to_string(),
655 line: 3,
656 col: 0,
657 });
658
659 assert_eq!(results.total_issues(), 15);
661 assert!(results.has_issues());
662 }
663
664 #[test]
667 fn total_issues_and_has_issues_are_consistent() {
668 let results = AnalysisResults::default();
669 assert_eq!(results.total_issues(), 0);
670 assert!(!results.has_issues());
671 assert_eq!(results.total_issues() > 0, results.has_issues());
672 }
673
674 #[test]
677 fn total_issues_sums_all_categories_independently() {
678 let mut results = AnalysisResults::default();
679 results.unused_files.push(UnusedFile {
680 path: PathBuf::from("a.ts"),
681 });
682 assert_eq!(results.total_issues(), 1);
683
684 results.unused_files.push(UnusedFile {
685 path: PathBuf::from("b.ts"),
686 });
687 assert_eq!(results.total_issues(), 2);
688
689 results.unresolved_imports.push(UnresolvedImport {
690 path: PathBuf::from("c.ts"),
691 specifier: "./missing".to_string(),
692 line: 1,
693 col: 0,
694 specifier_col: 0,
695 });
696 assert_eq!(results.total_issues(), 3);
697 }
698
699 #[test]
702 fn default_results_all_fields_empty() {
703 let r = AnalysisResults::default();
704 assert!(r.unused_files.is_empty());
705 assert!(r.unused_exports.is_empty());
706 assert!(r.unused_types.is_empty());
707 assert!(r.unused_dependencies.is_empty());
708 assert!(r.unused_dev_dependencies.is_empty());
709 assert!(r.unused_optional_dependencies.is_empty());
710 assert!(r.unused_enum_members.is_empty());
711 assert!(r.unused_class_members.is_empty());
712 assert!(r.unresolved_imports.is_empty());
713 assert!(r.unlisted_dependencies.is_empty());
714 assert!(r.duplicate_exports.is_empty());
715 assert!(r.type_only_dependencies.is_empty());
716 assert!(r.test_only_dependencies.is_empty());
717 assert!(r.circular_dependencies.is_empty());
718 assert!(r.boundary_violations.is_empty());
719 assert!(r.export_usages.is_empty());
720 }
721}