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
120#[derive(Debug, Clone, Serialize)]
122pub struct UnusedFile {
123 #[serde(serialize_with = "serde_path::serialize")]
125 pub path: PathBuf,
126}
127
128#[derive(Debug, Clone, Serialize)]
130pub struct UnusedExport {
131 #[serde(serialize_with = "serde_path::serialize")]
133 pub path: PathBuf,
134 pub export_name: String,
136 pub is_type_only: bool,
138 pub line: u32,
140 pub col: u32,
142 pub span_start: u32,
144 pub is_re_export: bool,
146}
147
148#[derive(Debug, Clone, Serialize)]
150pub struct UnusedDependency {
151 pub package_name: String,
153 pub location: DependencyLocation,
155 #[serde(serialize_with = "serde_path::serialize")]
158 pub path: PathBuf,
159 pub line: u32,
161}
162
163#[derive(Debug, Clone, Serialize)]
180#[serde(rename_all = "camelCase")]
181pub enum DependencyLocation {
182 Dependencies,
184 DevDependencies,
186 OptionalDependencies,
188}
189
190#[derive(Debug, Clone, Serialize)]
192pub struct UnusedMember {
193 #[serde(serialize_with = "serde_path::serialize")]
195 pub path: PathBuf,
196 pub parent_name: String,
198 pub member_name: String,
200 pub kind: MemberKind,
202 pub line: u32,
204 pub col: u32,
206}
207
208#[derive(Debug, Clone, Serialize)]
210pub struct UnresolvedImport {
211 #[serde(serialize_with = "serde_path::serialize")]
213 pub path: PathBuf,
214 pub specifier: String,
216 pub line: u32,
218 pub col: u32,
220 pub specifier_col: u32,
223}
224
225#[derive(Debug, Clone, Serialize)]
227pub struct UnlistedDependency {
228 pub package_name: String,
230 pub imported_from: Vec<ImportSite>,
232}
233
234#[derive(Debug, Clone, Serialize)]
236pub struct ImportSite {
237 #[serde(serialize_with = "serde_path::serialize")]
239 pub path: PathBuf,
240 pub line: u32,
242 pub col: u32,
244}
245
246#[derive(Debug, Clone, Serialize)]
248pub struct DuplicateExport {
249 pub export_name: String,
251 pub locations: Vec<DuplicateLocation>,
253}
254
255#[derive(Debug, Clone, Serialize)]
257pub struct DuplicateLocation {
258 #[serde(serialize_with = "serde_path::serialize")]
260 pub path: PathBuf,
261 pub line: u32,
263 pub col: u32,
265}
266
267#[derive(Debug, Clone, Serialize)]
271pub struct TypeOnlyDependency {
272 pub package_name: String,
274 #[serde(serialize_with = "serde_path::serialize")]
276 pub path: PathBuf,
277 pub line: u32,
279}
280
281#[derive(Debug, Clone, Serialize)]
284pub struct TestOnlyDependency {
285 pub package_name: String,
287 #[serde(serialize_with = "serde_path::serialize")]
289 pub path: PathBuf,
290 pub line: u32,
292}
293
294#[derive(Debug, Clone, Serialize, Deserialize)]
296pub struct CircularDependency {
297 #[serde(serialize_with = "serde_path::serialize_vec")]
299 pub files: Vec<PathBuf>,
300 pub length: usize,
302 #[serde(default)]
304 pub line: u32,
305 #[serde(default)]
307 pub col: u32,
308}
309
310#[derive(Debug, Clone, Serialize)]
312pub struct BoundaryViolation {
313 #[serde(serialize_with = "serde_path::serialize")]
315 pub from_path: PathBuf,
316 #[serde(serialize_with = "serde_path::serialize")]
318 pub to_path: PathBuf,
319 pub from_zone: String,
321 pub to_zone: String,
323 pub import_specifier: String,
325 pub line: u32,
327 pub col: u32,
329}
330
331#[derive(Debug, Clone, Serialize)]
334pub struct ExportUsage {
335 #[serde(serialize_with = "serde_path::serialize")]
337 pub path: PathBuf,
338 pub export_name: String,
340 pub line: u32,
342 pub col: u32,
344 pub reference_count: usize,
346 pub reference_locations: Vec<ReferenceLocation>,
349}
350
351#[derive(Debug, Clone, Serialize)]
353pub struct ReferenceLocation {
354 #[serde(serialize_with = "serde_path::serialize")]
356 pub path: PathBuf,
357 pub line: u32,
359 pub col: u32,
361}
362
363#[cfg(test)]
364mod tests {
365 use super::*;
366
367 #[test]
368 fn empty_results_no_issues() {
369 let results = AnalysisResults::default();
370 assert_eq!(results.total_issues(), 0);
371 assert!(!results.has_issues());
372 }
373
374 #[test]
375 fn results_with_unused_file() {
376 let mut results = AnalysisResults::default();
377 results.unused_files.push(UnusedFile {
378 path: PathBuf::from("test.ts"),
379 });
380 assert_eq!(results.total_issues(), 1);
381 assert!(results.has_issues());
382 }
383
384 #[test]
385 fn results_with_unused_export() {
386 let mut results = AnalysisResults::default();
387 results.unused_exports.push(UnusedExport {
388 path: PathBuf::from("test.ts"),
389 export_name: "foo".to_string(),
390 is_type_only: false,
391 line: 1,
392 col: 0,
393 span_start: 0,
394 is_re_export: false,
395 });
396 assert_eq!(results.total_issues(), 1);
397 assert!(results.has_issues());
398 }
399
400 #[test]
401 fn results_total_counts_all_types() {
402 let mut results = AnalysisResults::default();
403 results.unused_files.push(UnusedFile {
404 path: PathBuf::from("a.ts"),
405 });
406 results.unused_exports.push(UnusedExport {
407 path: PathBuf::from("b.ts"),
408 export_name: "x".to_string(),
409 is_type_only: false,
410 line: 1,
411 col: 0,
412 span_start: 0,
413 is_re_export: false,
414 });
415 results.unused_types.push(UnusedExport {
416 path: PathBuf::from("c.ts"),
417 export_name: "T".to_string(),
418 is_type_only: true,
419 line: 1,
420 col: 0,
421 span_start: 0,
422 is_re_export: false,
423 });
424 results.unused_dependencies.push(UnusedDependency {
425 package_name: "dep".to_string(),
426 location: DependencyLocation::Dependencies,
427 path: PathBuf::from("package.json"),
428 line: 5,
429 });
430 results.unused_dev_dependencies.push(UnusedDependency {
431 package_name: "dev".to_string(),
432 location: DependencyLocation::DevDependencies,
433 path: PathBuf::from("package.json"),
434 line: 5,
435 });
436 results.unused_enum_members.push(UnusedMember {
437 path: PathBuf::from("d.ts"),
438 parent_name: "E".to_string(),
439 member_name: "A".to_string(),
440 kind: MemberKind::EnumMember,
441 line: 1,
442 col: 0,
443 });
444 results.unused_class_members.push(UnusedMember {
445 path: PathBuf::from("e.ts"),
446 parent_name: "C".to_string(),
447 member_name: "m".to_string(),
448 kind: MemberKind::ClassMethod,
449 line: 1,
450 col: 0,
451 });
452 results.unresolved_imports.push(UnresolvedImport {
453 path: PathBuf::from("f.ts"),
454 specifier: "./missing".to_string(),
455 line: 1,
456 col: 0,
457 specifier_col: 0,
458 });
459 results.unlisted_dependencies.push(UnlistedDependency {
460 package_name: "unlisted".to_string(),
461 imported_from: vec![ImportSite {
462 path: PathBuf::from("g.ts"),
463 line: 1,
464 col: 0,
465 }],
466 });
467 results.duplicate_exports.push(DuplicateExport {
468 export_name: "dup".to_string(),
469 locations: vec![
470 DuplicateLocation {
471 path: PathBuf::from("h.ts"),
472 line: 15,
473 col: 0,
474 },
475 DuplicateLocation {
476 path: PathBuf::from("i.ts"),
477 line: 30,
478 col: 0,
479 },
480 ],
481 });
482 results.unused_optional_dependencies.push(UnusedDependency {
483 package_name: "optional".to_string(),
484 location: DependencyLocation::OptionalDependencies,
485 path: PathBuf::from("package.json"),
486 line: 5,
487 });
488 results.type_only_dependencies.push(TypeOnlyDependency {
489 package_name: "type-only".to_string(),
490 path: PathBuf::from("package.json"),
491 line: 8,
492 });
493 results.test_only_dependencies.push(TestOnlyDependency {
494 package_name: "test-only".to_string(),
495 path: PathBuf::from("package.json"),
496 line: 9,
497 });
498 results.circular_dependencies.push(CircularDependency {
499 files: vec![PathBuf::from("a.ts"), PathBuf::from("b.ts")],
500 length: 2,
501 line: 3,
502 col: 0,
503 });
504 results.boundary_violations.push(BoundaryViolation {
505 from_path: PathBuf::from("src/ui/Button.tsx"),
506 to_path: PathBuf::from("src/db/queries.ts"),
507 from_zone: "ui".to_string(),
508 to_zone: "database".to_string(),
509 import_specifier: "../db/queries".to_string(),
510 line: 3,
511 col: 0,
512 });
513
514 assert_eq!(results.total_issues(), 15);
516 assert!(results.has_issues());
517 }
518
519 #[test]
522 fn total_issues_and_has_issues_are_consistent() {
523 let results = AnalysisResults::default();
524 assert_eq!(results.total_issues(), 0);
525 assert!(!results.has_issues());
526 assert_eq!(results.total_issues() > 0, results.has_issues());
527 }
528
529 #[test]
532 fn total_issues_sums_all_categories_independently() {
533 let mut results = AnalysisResults::default();
534 results.unused_files.push(UnusedFile {
535 path: PathBuf::from("a.ts"),
536 });
537 assert_eq!(results.total_issues(), 1);
538
539 results.unused_files.push(UnusedFile {
540 path: PathBuf::from("b.ts"),
541 });
542 assert_eq!(results.total_issues(), 2);
543
544 results.unresolved_imports.push(UnresolvedImport {
545 path: PathBuf::from("c.ts"),
546 specifier: "./missing".to_string(),
547 line: 1,
548 col: 0,
549 specifier_col: 0,
550 });
551 assert_eq!(results.total_issues(), 3);
552 }
553
554 #[test]
557 fn default_results_all_fields_empty() {
558 let r = AnalysisResults::default();
559 assert!(r.unused_files.is_empty());
560 assert!(r.unused_exports.is_empty());
561 assert!(r.unused_types.is_empty());
562 assert!(r.unused_dependencies.is_empty());
563 assert!(r.unused_dev_dependencies.is_empty());
564 assert!(r.unused_optional_dependencies.is_empty());
565 assert!(r.unused_enum_members.is_empty());
566 assert!(r.unused_class_members.is_empty());
567 assert!(r.unresolved_imports.is_empty());
568 assert!(r.unlisted_dependencies.is_empty());
569 assert!(r.duplicate_exports.is_empty());
570 assert!(r.type_only_dependencies.is_empty());
571 assert!(r.test_only_dependencies.is_empty());
572 assert!(r.circular_dependencies.is_empty());
573 assert!(r.boundary_violations.is_empty());
574 assert!(r.export_usages.is_empty());
575 }
576}