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(skip)]
64 pub export_usages: Vec<ExportUsage>,
65}
66
67impl AnalysisResults {
68 #[must_use]
92 pub const fn total_issues(&self) -> usize {
93 self.unused_files.len()
94 + self.unused_exports.len()
95 + self.unused_types.len()
96 + self.unused_dependencies.len()
97 + self.unused_dev_dependencies.len()
98 + self.unused_optional_dependencies.len()
99 + self.unused_enum_members.len()
100 + self.unused_class_members.len()
101 + self.unresolved_imports.len()
102 + self.unlisted_dependencies.len()
103 + self.duplicate_exports.len()
104 + self.type_only_dependencies.len()
105 + self.test_only_dependencies.len()
106 + self.circular_dependencies.len()
107 }
108
109 #[must_use]
111 pub const fn has_issues(&self) -> bool {
112 self.total_issues() > 0
113 }
114}
115
116#[derive(Debug, Clone, Serialize)]
118pub struct UnusedFile {
119 #[serde(serialize_with = "serde_path::serialize")]
121 pub path: PathBuf,
122}
123
124#[derive(Debug, Clone, Serialize)]
126pub struct UnusedExport {
127 #[serde(serialize_with = "serde_path::serialize")]
129 pub path: PathBuf,
130 pub export_name: String,
132 pub is_type_only: bool,
134 pub line: u32,
136 pub col: u32,
138 pub span_start: u32,
140 pub is_re_export: bool,
142}
143
144#[derive(Debug, Clone, Serialize)]
146pub struct UnusedDependency {
147 pub package_name: String,
149 pub location: DependencyLocation,
151 #[serde(serialize_with = "serde_path::serialize")]
154 pub path: PathBuf,
155 pub line: u32,
157}
158
159#[derive(Debug, Clone, Serialize)]
176#[serde(rename_all = "camelCase")]
177pub enum DependencyLocation {
178 Dependencies,
180 DevDependencies,
182 OptionalDependencies,
184}
185
186#[derive(Debug, Clone, Serialize)]
188pub struct UnusedMember {
189 #[serde(serialize_with = "serde_path::serialize")]
191 pub path: PathBuf,
192 pub parent_name: String,
194 pub member_name: String,
196 pub kind: MemberKind,
198 pub line: u32,
200 pub col: u32,
202}
203
204#[derive(Debug, Clone, Serialize)]
206pub struct UnresolvedImport {
207 #[serde(serialize_with = "serde_path::serialize")]
209 pub path: PathBuf,
210 pub specifier: String,
212 pub line: u32,
214 pub col: u32,
216 pub specifier_col: u32,
219}
220
221#[derive(Debug, Clone, Serialize)]
223pub struct UnlistedDependency {
224 pub package_name: String,
226 pub imported_from: Vec<ImportSite>,
228}
229
230#[derive(Debug, Clone, Serialize)]
232pub struct ImportSite {
233 #[serde(serialize_with = "serde_path::serialize")]
235 pub path: PathBuf,
236 pub line: u32,
238 pub col: u32,
240}
241
242#[derive(Debug, Clone, Serialize)]
244pub struct DuplicateExport {
245 pub export_name: String,
247 pub locations: Vec<DuplicateLocation>,
249}
250
251#[derive(Debug, Clone, Serialize)]
253pub struct DuplicateLocation {
254 #[serde(serialize_with = "serde_path::serialize")]
256 pub path: PathBuf,
257 pub line: u32,
259 pub col: u32,
261}
262
263#[derive(Debug, Clone, Serialize)]
267pub struct TypeOnlyDependency {
268 pub package_name: String,
270 #[serde(serialize_with = "serde_path::serialize")]
272 pub path: PathBuf,
273 pub line: u32,
275}
276
277#[derive(Debug, Clone, Serialize)]
280pub struct TestOnlyDependency {
281 pub package_name: String,
283 #[serde(serialize_with = "serde_path::serialize")]
285 pub path: PathBuf,
286 pub line: u32,
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct CircularDependency {
293 #[serde(serialize_with = "serde_path::serialize_vec")]
295 pub files: Vec<PathBuf>,
296 pub length: usize,
298 #[serde(default)]
300 pub line: u32,
301 #[serde(default)]
303 pub col: u32,
304}
305
306#[derive(Debug, Clone, Serialize)]
309pub struct ExportUsage {
310 #[serde(serialize_with = "serde_path::serialize")]
312 pub path: PathBuf,
313 pub export_name: String,
315 pub line: u32,
317 pub col: u32,
319 pub reference_count: usize,
321 pub reference_locations: Vec<ReferenceLocation>,
324}
325
326#[derive(Debug, Clone, Serialize)]
328pub struct ReferenceLocation {
329 #[serde(serialize_with = "serde_path::serialize")]
331 pub path: PathBuf,
332 pub line: u32,
334 pub col: u32,
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341
342 #[test]
343 fn empty_results_no_issues() {
344 let results = AnalysisResults::default();
345 assert_eq!(results.total_issues(), 0);
346 assert!(!results.has_issues());
347 }
348
349 #[test]
350 fn results_with_unused_file() {
351 let mut results = AnalysisResults::default();
352 results.unused_files.push(UnusedFile {
353 path: PathBuf::from("test.ts"),
354 });
355 assert_eq!(results.total_issues(), 1);
356 assert!(results.has_issues());
357 }
358
359 #[test]
360 fn results_with_unused_export() {
361 let mut results = AnalysisResults::default();
362 results.unused_exports.push(UnusedExport {
363 path: PathBuf::from("test.ts"),
364 export_name: "foo".to_string(),
365 is_type_only: false,
366 line: 1,
367 col: 0,
368 span_start: 0,
369 is_re_export: false,
370 });
371 assert_eq!(results.total_issues(), 1);
372 assert!(results.has_issues());
373 }
374
375 #[test]
376 fn results_total_counts_all_types() {
377 let mut results = AnalysisResults::default();
378 results.unused_files.push(UnusedFile {
379 path: PathBuf::from("a.ts"),
380 });
381 results.unused_exports.push(UnusedExport {
382 path: PathBuf::from("b.ts"),
383 export_name: "x".to_string(),
384 is_type_only: false,
385 line: 1,
386 col: 0,
387 span_start: 0,
388 is_re_export: false,
389 });
390 results.unused_types.push(UnusedExport {
391 path: PathBuf::from("c.ts"),
392 export_name: "T".to_string(),
393 is_type_only: true,
394 line: 1,
395 col: 0,
396 span_start: 0,
397 is_re_export: false,
398 });
399 results.unused_dependencies.push(UnusedDependency {
400 package_name: "dep".to_string(),
401 location: DependencyLocation::Dependencies,
402 path: PathBuf::from("package.json"),
403 line: 5,
404 });
405 results.unused_dev_dependencies.push(UnusedDependency {
406 package_name: "dev".to_string(),
407 location: DependencyLocation::DevDependencies,
408 path: PathBuf::from("package.json"),
409 line: 5,
410 });
411 results.unused_enum_members.push(UnusedMember {
412 path: PathBuf::from("d.ts"),
413 parent_name: "E".to_string(),
414 member_name: "A".to_string(),
415 kind: MemberKind::EnumMember,
416 line: 1,
417 col: 0,
418 });
419 results.unused_class_members.push(UnusedMember {
420 path: PathBuf::from("e.ts"),
421 parent_name: "C".to_string(),
422 member_name: "m".to_string(),
423 kind: MemberKind::ClassMethod,
424 line: 1,
425 col: 0,
426 });
427 results.unresolved_imports.push(UnresolvedImport {
428 path: PathBuf::from("f.ts"),
429 specifier: "./missing".to_string(),
430 line: 1,
431 col: 0,
432 specifier_col: 0,
433 });
434 results.unlisted_dependencies.push(UnlistedDependency {
435 package_name: "unlisted".to_string(),
436 imported_from: vec![ImportSite {
437 path: PathBuf::from("g.ts"),
438 line: 1,
439 col: 0,
440 }],
441 });
442 results.duplicate_exports.push(DuplicateExport {
443 export_name: "dup".to_string(),
444 locations: vec![
445 DuplicateLocation {
446 path: PathBuf::from("h.ts"),
447 line: 15,
448 col: 0,
449 },
450 DuplicateLocation {
451 path: PathBuf::from("i.ts"),
452 line: 30,
453 col: 0,
454 },
455 ],
456 });
457 results.unused_optional_dependencies.push(UnusedDependency {
458 package_name: "optional".to_string(),
459 location: DependencyLocation::OptionalDependencies,
460 path: PathBuf::from("package.json"),
461 line: 5,
462 });
463 results.type_only_dependencies.push(TypeOnlyDependency {
464 package_name: "type-only".to_string(),
465 path: PathBuf::from("package.json"),
466 line: 8,
467 });
468 results.test_only_dependencies.push(TestOnlyDependency {
469 package_name: "test-only".to_string(),
470 path: PathBuf::from("package.json"),
471 line: 9,
472 });
473 results.circular_dependencies.push(CircularDependency {
474 files: vec![PathBuf::from("a.ts"), PathBuf::from("b.ts")],
475 length: 2,
476 line: 3,
477 col: 0,
478 });
479
480 assert_eq!(results.total_issues(), 14);
482 assert!(results.has_issues());
483 }
484
485 #[test]
488 fn total_issues_and_has_issues_are_consistent() {
489 let results = AnalysisResults::default();
490 assert_eq!(results.total_issues(), 0);
491 assert!(!results.has_issues());
492 assert_eq!(results.total_issues() > 0, results.has_issues());
493 }
494
495 #[test]
498 fn total_issues_sums_all_categories_independently() {
499 let mut results = AnalysisResults::default();
500 results.unused_files.push(UnusedFile {
501 path: PathBuf::from("a.ts"),
502 });
503 assert_eq!(results.total_issues(), 1);
504
505 results.unused_files.push(UnusedFile {
506 path: PathBuf::from("b.ts"),
507 });
508 assert_eq!(results.total_issues(), 2);
509
510 results.unresolved_imports.push(UnresolvedImport {
511 path: PathBuf::from("c.ts"),
512 specifier: "./missing".to_string(),
513 line: 1,
514 col: 0,
515 specifier_col: 0,
516 });
517 assert_eq!(results.total_issues(), 3);
518 }
519
520 #[test]
523 fn default_results_all_fields_empty() {
524 let r = AnalysisResults::default();
525 assert!(r.unused_files.is_empty());
526 assert!(r.unused_exports.is_empty());
527 assert!(r.unused_types.is_empty());
528 assert!(r.unused_dependencies.is_empty());
529 assert!(r.unused_dev_dependencies.is_empty());
530 assert!(r.unused_optional_dependencies.is_empty());
531 assert!(r.unused_enum_members.is_empty());
532 assert!(r.unused_class_members.is_empty());
533 assert!(r.unresolved_imports.is_empty());
534 assert!(r.unlisted_dependencies.is_empty());
535 assert!(r.duplicate_exports.is_empty());
536 assert!(r.type_only_dependencies.is_empty());
537 assert!(r.test_only_dependencies.is_empty());
538 assert!(r.circular_dependencies.is_empty());
539 assert!(r.export_usages.is_empty());
540 }
541}