1use std::path::PathBuf;
4
5use serde::{Deserialize, Serialize};
6
7use crate::extract::MemberKind;
8use crate::serde_path;
9
10#[derive(Debug, Default, Clone, Serialize)]
29pub struct AnalysisResults {
30 pub unused_files: Vec<UnusedFile>,
32 pub unused_exports: Vec<UnusedExport>,
34 pub unused_types: Vec<UnusedExport>,
36 pub unused_dependencies: Vec<UnusedDependency>,
38 pub unused_dev_dependencies: Vec<UnusedDependency>,
40 pub unused_optional_dependencies: Vec<UnusedDependency>,
42 pub unused_enum_members: Vec<UnusedMember>,
44 pub unused_class_members: Vec<UnusedMember>,
46 pub unresolved_imports: Vec<UnresolvedImport>,
48 pub unlisted_dependencies: Vec<UnlistedDependency>,
50 pub duplicate_exports: Vec<DuplicateExport>,
52 pub type_only_dependencies: Vec<TypeOnlyDependency>,
55 #[serde(default)]
57 pub test_only_dependencies: Vec<TestOnlyDependency>,
58 pub circular_dependencies: Vec<CircularDependency>,
60 #[serde(default)]
62 pub boundary_violations: Vec<BoundaryViolation>,
63 #[serde(skip)]
67 pub export_usages: Vec<ExportUsage>,
68}
69
70impl AnalysisResults {
71 #[must_use]
95 pub const fn total_issues(&self) -> usize {
96 self.unused_files.len()
97 + self.unused_exports.len()
98 + self.unused_types.len()
99 + self.unused_dependencies.len()
100 + self.unused_dev_dependencies.len()
101 + self.unused_optional_dependencies.len()
102 + self.unused_enum_members.len()
103 + self.unused_class_members.len()
104 + self.unresolved_imports.len()
105 + self.unlisted_dependencies.len()
106 + self.duplicate_exports.len()
107 + self.type_only_dependencies.len()
108 + self.test_only_dependencies.len()
109 + self.circular_dependencies.len()
110 + self.boundary_violations.len()
111 }
112
113 #[must_use]
115 pub const fn has_issues(&self) -> bool {
116 self.total_issues() > 0
117 }
118
119 pub fn sort(&mut self) {
126 self.unused_files.sort_by(|a, b| a.path.cmp(&b.path));
127
128 self.unused_exports.sort_by(|a, b| {
129 a.path
130 .cmp(&b.path)
131 .then(a.line.cmp(&b.line))
132 .then(a.export_name.cmp(&b.export_name))
133 });
134
135 self.unused_types.sort_by(|a, b| {
136 a.path
137 .cmp(&b.path)
138 .then(a.line.cmp(&b.line))
139 .then(a.export_name.cmp(&b.export_name))
140 });
141
142 self.unused_dependencies.sort_by(|a, b| {
143 a.path
144 .cmp(&b.path)
145 .then(a.line.cmp(&b.line))
146 .then(a.package_name.cmp(&b.package_name))
147 });
148
149 self.unused_dev_dependencies.sort_by(|a, b| {
150 a.path
151 .cmp(&b.path)
152 .then(a.line.cmp(&b.line))
153 .then(a.package_name.cmp(&b.package_name))
154 });
155
156 self.unused_optional_dependencies.sort_by(|a, b| {
157 a.path
158 .cmp(&b.path)
159 .then(a.line.cmp(&b.line))
160 .then(a.package_name.cmp(&b.package_name))
161 });
162
163 self.unused_enum_members.sort_by(|a, b| {
164 a.path
165 .cmp(&b.path)
166 .then(a.line.cmp(&b.line))
167 .then(a.parent_name.cmp(&b.parent_name))
168 .then(a.member_name.cmp(&b.member_name))
169 });
170
171 self.unused_class_members.sort_by(|a, b| {
172 a.path
173 .cmp(&b.path)
174 .then(a.line.cmp(&b.line))
175 .then(a.parent_name.cmp(&b.parent_name))
176 .then(a.member_name.cmp(&b.member_name))
177 });
178
179 self.unresolved_imports.sort_by(|a, b| {
180 a.path
181 .cmp(&b.path)
182 .then(a.line.cmp(&b.line))
183 .then(a.col.cmp(&b.col))
184 .then(a.specifier.cmp(&b.specifier))
185 });
186
187 self.unlisted_dependencies
188 .sort_by(|a, b| a.package_name.cmp(&b.package_name));
189 for dep in &mut self.unlisted_dependencies {
190 dep.imported_from
191 .sort_by(|a, b| a.path.cmp(&b.path).then(a.line.cmp(&b.line)));
192 }
193
194 self.duplicate_exports
195 .sort_by(|a, b| a.export_name.cmp(&b.export_name));
196 for dup in &mut self.duplicate_exports {
197 dup.locations
198 .sort_by(|a, b| a.path.cmp(&b.path).then(a.line.cmp(&b.line)));
199 }
200
201 self.type_only_dependencies.sort_by(|a, b| {
202 a.path
203 .cmp(&b.path)
204 .then(a.line.cmp(&b.line))
205 .then(a.package_name.cmp(&b.package_name))
206 });
207
208 self.test_only_dependencies.sort_by(|a, b| {
209 a.path
210 .cmp(&b.path)
211 .then(a.line.cmp(&b.line))
212 .then(a.package_name.cmp(&b.package_name))
213 });
214
215 self.circular_dependencies
216 .sort_by(|a, b| a.files.cmp(&b.files).then(a.length.cmp(&b.length)));
217
218 self.boundary_violations.sort_by(|a, b| {
219 a.from_path
220 .cmp(&b.from_path)
221 .then(a.line.cmp(&b.line))
222 .then(a.col.cmp(&b.col))
223 .then(a.to_path.cmp(&b.to_path))
224 });
225
226 for usage in &mut self.export_usages {
227 usage.reference_locations.sort_by(|a, b| {
228 a.path
229 .cmp(&b.path)
230 .then(a.line.cmp(&b.line))
231 .then(a.col.cmp(&b.col))
232 });
233 }
234 self.export_usages.sort_by(|a, b| {
235 a.path
236 .cmp(&b.path)
237 .then(a.line.cmp(&b.line))
238 .then(a.export_name.cmp(&b.export_name))
239 });
240 }
241}
242
243#[derive(Debug, Clone, Serialize)]
245pub struct UnusedFile {
246 #[serde(serialize_with = "serde_path::serialize")]
248 pub path: PathBuf,
249}
250
251#[derive(Debug, Clone, Serialize)]
253pub struct UnusedExport {
254 #[serde(serialize_with = "serde_path::serialize")]
256 pub path: PathBuf,
257 pub export_name: String,
259 pub is_type_only: bool,
261 pub line: u32,
263 pub col: u32,
265 pub span_start: u32,
267 pub is_re_export: bool,
269}
270
271#[derive(Debug, Clone, Serialize)]
273pub struct UnusedDependency {
274 pub package_name: String,
276 pub location: DependencyLocation,
278 #[serde(serialize_with = "serde_path::serialize")]
281 pub path: PathBuf,
282 pub line: u32,
284}
285
286#[derive(Debug, Clone, Serialize)]
303#[serde(rename_all = "camelCase")]
304pub enum DependencyLocation {
305 Dependencies,
307 DevDependencies,
309 OptionalDependencies,
311}
312
313#[derive(Debug, Clone, Serialize)]
315pub struct UnusedMember {
316 #[serde(serialize_with = "serde_path::serialize")]
318 pub path: PathBuf,
319 pub parent_name: String,
321 pub member_name: String,
323 pub kind: MemberKind,
325 pub line: u32,
327 pub col: u32,
329}
330
331#[derive(Debug, Clone, Serialize)]
333pub struct UnresolvedImport {
334 #[serde(serialize_with = "serde_path::serialize")]
336 pub path: PathBuf,
337 pub specifier: String,
339 pub line: u32,
341 pub col: u32,
343 pub specifier_col: u32,
346}
347
348#[derive(Debug, Clone, Serialize)]
350pub struct UnlistedDependency {
351 pub package_name: String,
353 pub imported_from: Vec<ImportSite>,
355}
356
357#[derive(Debug, Clone, Serialize)]
359pub struct ImportSite {
360 #[serde(serialize_with = "serde_path::serialize")]
362 pub path: PathBuf,
363 pub line: u32,
365 pub col: u32,
367}
368
369#[derive(Debug, Clone, Serialize)]
371pub struct DuplicateExport {
372 pub export_name: String,
374 pub locations: Vec<DuplicateLocation>,
376}
377
378#[derive(Debug, Clone, Serialize)]
380pub struct DuplicateLocation {
381 #[serde(serialize_with = "serde_path::serialize")]
383 pub path: PathBuf,
384 pub line: u32,
386 pub col: u32,
388}
389
390#[derive(Debug, Clone, Serialize)]
394pub struct TypeOnlyDependency {
395 pub package_name: String,
397 #[serde(serialize_with = "serde_path::serialize")]
399 pub path: PathBuf,
400 pub line: u32,
402}
403
404#[derive(Debug, Clone, Serialize)]
407pub struct TestOnlyDependency {
408 pub package_name: String,
410 #[serde(serialize_with = "serde_path::serialize")]
412 pub path: PathBuf,
413 pub line: u32,
415}
416
417#[derive(Debug, Clone, Serialize, Deserialize)]
419pub struct CircularDependency {
420 #[serde(serialize_with = "serde_path::serialize_vec")]
422 pub files: Vec<PathBuf>,
423 pub length: usize,
425 #[serde(default)]
427 pub line: u32,
428 #[serde(default)]
430 pub col: u32,
431}
432
433#[derive(Debug, Clone, Serialize)]
435pub struct BoundaryViolation {
436 #[serde(serialize_with = "serde_path::serialize")]
438 pub from_path: PathBuf,
439 #[serde(serialize_with = "serde_path::serialize")]
441 pub to_path: PathBuf,
442 pub from_zone: String,
444 pub to_zone: String,
446 pub import_specifier: String,
448 pub line: u32,
450 pub col: u32,
452}
453
454#[derive(Debug, Clone, Serialize)]
457pub struct ExportUsage {
458 #[serde(serialize_with = "serde_path::serialize")]
460 pub path: PathBuf,
461 pub export_name: String,
463 pub line: u32,
465 pub col: u32,
467 pub reference_count: usize,
469 pub reference_locations: Vec<ReferenceLocation>,
472}
473
474#[derive(Debug, Clone, Serialize)]
476pub struct ReferenceLocation {
477 #[serde(serialize_with = "serde_path::serialize")]
479 pub path: PathBuf,
480 pub line: u32,
482 pub col: u32,
484}
485
486#[cfg(test)]
487mod tests {
488 use super::*;
489
490 #[test]
491 fn empty_results_no_issues() {
492 let results = AnalysisResults::default();
493 assert_eq!(results.total_issues(), 0);
494 assert!(!results.has_issues());
495 }
496
497 #[test]
498 fn results_with_unused_file() {
499 let mut results = AnalysisResults::default();
500 results.unused_files.push(UnusedFile {
501 path: PathBuf::from("test.ts"),
502 });
503 assert_eq!(results.total_issues(), 1);
504 assert!(results.has_issues());
505 }
506
507 #[test]
508 fn results_with_unused_export() {
509 let mut results = AnalysisResults::default();
510 results.unused_exports.push(UnusedExport {
511 path: PathBuf::from("test.ts"),
512 export_name: "foo".to_string(),
513 is_type_only: false,
514 line: 1,
515 col: 0,
516 span_start: 0,
517 is_re_export: false,
518 });
519 assert_eq!(results.total_issues(), 1);
520 assert!(results.has_issues());
521 }
522
523 #[test]
524 fn results_total_counts_all_types() {
525 let mut results = AnalysisResults::default();
526 results.unused_files.push(UnusedFile {
527 path: PathBuf::from("a.ts"),
528 });
529 results.unused_exports.push(UnusedExport {
530 path: PathBuf::from("b.ts"),
531 export_name: "x".to_string(),
532 is_type_only: false,
533 line: 1,
534 col: 0,
535 span_start: 0,
536 is_re_export: false,
537 });
538 results.unused_types.push(UnusedExport {
539 path: PathBuf::from("c.ts"),
540 export_name: "T".to_string(),
541 is_type_only: true,
542 line: 1,
543 col: 0,
544 span_start: 0,
545 is_re_export: false,
546 });
547 results.unused_dependencies.push(UnusedDependency {
548 package_name: "dep".to_string(),
549 location: DependencyLocation::Dependencies,
550 path: PathBuf::from("package.json"),
551 line: 5,
552 });
553 results.unused_dev_dependencies.push(UnusedDependency {
554 package_name: "dev".to_string(),
555 location: DependencyLocation::DevDependencies,
556 path: PathBuf::from("package.json"),
557 line: 5,
558 });
559 results.unused_enum_members.push(UnusedMember {
560 path: PathBuf::from("d.ts"),
561 parent_name: "E".to_string(),
562 member_name: "A".to_string(),
563 kind: MemberKind::EnumMember,
564 line: 1,
565 col: 0,
566 });
567 results.unused_class_members.push(UnusedMember {
568 path: PathBuf::from("e.ts"),
569 parent_name: "C".to_string(),
570 member_name: "m".to_string(),
571 kind: MemberKind::ClassMethod,
572 line: 1,
573 col: 0,
574 });
575 results.unresolved_imports.push(UnresolvedImport {
576 path: PathBuf::from("f.ts"),
577 specifier: "./missing".to_string(),
578 line: 1,
579 col: 0,
580 specifier_col: 0,
581 });
582 results.unlisted_dependencies.push(UnlistedDependency {
583 package_name: "unlisted".to_string(),
584 imported_from: vec![ImportSite {
585 path: PathBuf::from("g.ts"),
586 line: 1,
587 col: 0,
588 }],
589 });
590 results.duplicate_exports.push(DuplicateExport {
591 export_name: "dup".to_string(),
592 locations: vec![
593 DuplicateLocation {
594 path: PathBuf::from("h.ts"),
595 line: 15,
596 col: 0,
597 },
598 DuplicateLocation {
599 path: PathBuf::from("i.ts"),
600 line: 30,
601 col: 0,
602 },
603 ],
604 });
605 results.unused_optional_dependencies.push(UnusedDependency {
606 package_name: "optional".to_string(),
607 location: DependencyLocation::OptionalDependencies,
608 path: PathBuf::from("package.json"),
609 line: 5,
610 });
611 results.type_only_dependencies.push(TypeOnlyDependency {
612 package_name: "type-only".to_string(),
613 path: PathBuf::from("package.json"),
614 line: 8,
615 });
616 results.test_only_dependencies.push(TestOnlyDependency {
617 package_name: "test-only".to_string(),
618 path: PathBuf::from("package.json"),
619 line: 9,
620 });
621 results.circular_dependencies.push(CircularDependency {
622 files: vec![PathBuf::from("a.ts"), PathBuf::from("b.ts")],
623 length: 2,
624 line: 3,
625 col: 0,
626 });
627 results.boundary_violations.push(BoundaryViolation {
628 from_path: PathBuf::from("src/ui/Button.tsx"),
629 to_path: PathBuf::from("src/db/queries.ts"),
630 from_zone: "ui".to_string(),
631 to_zone: "database".to_string(),
632 import_specifier: "../db/queries".to_string(),
633 line: 3,
634 col: 0,
635 });
636
637 assert_eq!(results.total_issues(), 15);
639 assert!(results.has_issues());
640 }
641
642 #[test]
645 fn total_issues_and_has_issues_are_consistent() {
646 let results = AnalysisResults::default();
647 assert_eq!(results.total_issues(), 0);
648 assert!(!results.has_issues());
649 assert_eq!(results.total_issues() > 0, results.has_issues());
650 }
651
652 #[test]
655 fn total_issues_sums_all_categories_independently() {
656 let mut results = AnalysisResults::default();
657 results.unused_files.push(UnusedFile {
658 path: PathBuf::from("a.ts"),
659 });
660 assert_eq!(results.total_issues(), 1);
661
662 results.unused_files.push(UnusedFile {
663 path: PathBuf::from("b.ts"),
664 });
665 assert_eq!(results.total_issues(), 2);
666
667 results.unresolved_imports.push(UnresolvedImport {
668 path: PathBuf::from("c.ts"),
669 specifier: "./missing".to_string(),
670 line: 1,
671 col: 0,
672 specifier_col: 0,
673 });
674 assert_eq!(results.total_issues(), 3);
675 }
676
677 #[test]
680 fn default_results_all_fields_empty() {
681 let r = AnalysisResults::default();
682 assert!(r.unused_files.is_empty());
683 assert!(r.unused_exports.is_empty());
684 assert!(r.unused_types.is_empty());
685 assert!(r.unused_dependencies.is_empty());
686 assert!(r.unused_dev_dependencies.is_empty());
687 assert!(r.unused_optional_dependencies.is_empty());
688 assert!(r.unused_enum_members.is_empty());
689 assert!(r.unused_class_members.is_empty());
690 assert!(r.unresolved_imports.is_empty());
691 assert!(r.unlisted_dependencies.is_empty());
692 assert!(r.duplicate_exports.is_empty());
693 assert!(r.type_only_dependencies.is_empty());
694 assert!(r.test_only_dependencies.is_empty());
695 assert!(r.circular_dependencies.is_empty());
696 assert!(r.boundary_violations.is_empty());
697 assert!(r.export_usages.is_empty());
698 }
699}