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 pub circular_dependencies: Vec<CircularDependency>,
57 #[serde(skip)]
61 pub export_usages: Vec<ExportUsage>,
62}
63
64impl AnalysisResults {
65 #[must_use]
89 pub const fn total_issues(&self) -> usize {
90 self.unused_files.len()
91 + self.unused_exports.len()
92 + self.unused_types.len()
93 + self.unused_dependencies.len()
94 + self.unused_dev_dependencies.len()
95 + self.unused_optional_dependencies.len()
96 + self.unused_enum_members.len()
97 + self.unused_class_members.len()
98 + self.unresolved_imports.len()
99 + self.unlisted_dependencies.len()
100 + self.duplicate_exports.len()
101 + self.type_only_dependencies.len()
102 + self.circular_dependencies.len()
103 }
104
105 #[must_use]
107 pub const fn has_issues(&self) -> bool {
108 self.total_issues() > 0
109 }
110}
111
112#[derive(Debug, Clone, Serialize)]
114pub struct UnusedFile {
115 #[serde(serialize_with = "serde_path::serialize")]
117 pub path: PathBuf,
118}
119
120#[derive(Debug, Clone, Serialize)]
122pub struct UnusedExport {
123 #[serde(serialize_with = "serde_path::serialize")]
125 pub path: PathBuf,
126 pub export_name: String,
128 pub is_type_only: bool,
130 pub line: u32,
132 pub col: u32,
134 pub span_start: u32,
136 pub is_re_export: bool,
138}
139
140#[derive(Debug, Clone, Serialize)]
142pub struct UnusedDependency {
143 pub package_name: String,
145 pub location: DependencyLocation,
147 #[serde(serialize_with = "serde_path::serialize")]
150 pub path: PathBuf,
151 pub line: u32,
153}
154
155#[derive(Debug, Clone, Serialize)]
172#[serde(rename_all = "camelCase")]
173pub enum DependencyLocation {
174 Dependencies,
176 DevDependencies,
178 OptionalDependencies,
180}
181
182#[derive(Debug, Clone, Serialize)]
184pub struct UnusedMember {
185 #[serde(serialize_with = "serde_path::serialize")]
187 pub path: PathBuf,
188 pub parent_name: String,
190 pub member_name: String,
192 pub kind: MemberKind,
194 pub line: u32,
196 pub col: u32,
198}
199
200#[derive(Debug, Clone, Serialize)]
202pub struct UnresolvedImport {
203 #[serde(serialize_with = "serde_path::serialize")]
205 pub path: PathBuf,
206 pub specifier: String,
208 pub line: u32,
210 pub col: u32,
212 pub specifier_col: u32,
215}
216
217#[derive(Debug, Clone, Serialize)]
219pub struct UnlistedDependency {
220 pub package_name: String,
222 pub imported_from: Vec<ImportSite>,
224}
225
226#[derive(Debug, Clone, Serialize)]
228pub struct ImportSite {
229 #[serde(serialize_with = "serde_path::serialize")]
231 pub path: PathBuf,
232 pub line: u32,
234 pub col: u32,
236}
237
238#[derive(Debug, Clone, Serialize)]
240pub struct DuplicateExport {
241 pub export_name: String,
243 pub locations: Vec<DuplicateLocation>,
245}
246
247#[derive(Debug, Clone, Serialize)]
249pub struct DuplicateLocation {
250 #[serde(serialize_with = "serde_path::serialize")]
252 pub path: PathBuf,
253 pub line: u32,
255 pub col: u32,
257}
258
259#[derive(Debug, Clone, Serialize)]
263pub struct TypeOnlyDependency {
264 pub package_name: String,
266 #[serde(serialize_with = "serde_path::serialize")]
268 pub path: PathBuf,
269 pub line: u32,
271}
272
273#[derive(Debug, Clone, Serialize, Deserialize)]
275pub struct CircularDependency {
276 #[serde(serialize_with = "serde_path::serialize_vec")]
278 pub files: Vec<PathBuf>,
279 pub length: usize,
281 #[serde(default)]
283 pub line: u32,
284 #[serde(default)]
286 pub col: u32,
287}
288
289#[derive(Debug, Clone, Serialize)]
292pub struct ExportUsage {
293 #[serde(serialize_with = "serde_path::serialize")]
295 pub path: PathBuf,
296 pub export_name: String,
298 pub line: u32,
300 pub col: u32,
302 pub reference_count: usize,
304 pub reference_locations: Vec<ReferenceLocation>,
307}
308
309#[derive(Debug, Clone, Serialize)]
311pub struct ReferenceLocation {
312 #[serde(serialize_with = "serde_path::serialize")]
314 pub path: PathBuf,
315 pub line: u32,
317 pub col: u32,
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324
325 #[test]
326 fn empty_results_no_issues() {
327 let results = AnalysisResults::default();
328 assert_eq!(results.total_issues(), 0);
329 assert!(!results.has_issues());
330 }
331
332 #[test]
333 fn results_with_unused_file() {
334 let mut results = AnalysisResults::default();
335 results.unused_files.push(UnusedFile {
336 path: PathBuf::from("test.ts"),
337 });
338 assert_eq!(results.total_issues(), 1);
339 assert!(results.has_issues());
340 }
341
342 #[test]
343 fn results_with_unused_export() {
344 let mut results = AnalysisResults::default();
345 results.unused_exports.push(UnusedExport {
346 path: PathBuf::from("test.ts"),
347 export_name: "foo".to_string(),
348 is_type_only: false,
349 line: 1,
350 col: 0,
351 span_start: 0,
352 is_re_export: false,
353 });
354 assert_eq!(results.total_issues(), 1);
355 assert!(results.has_issues());
356 }
357
358 #[test]
359 fn results_total_counts_all_types() {
360 let mut results = AnalysisResults::default();
361 results.unused_files.push(UnusedFile {
362 path: PathBuf::from("a.ts"),
363 });
364 results.unused_exports.push(UnusedExport {
365 path: PathBuf::from("b.ts"),
366 export_name: "x".to_string(),
367 is_type_only: false,
368 line: 1,
369 col: 0,
370 span_start: 0,
371 is_re_export: false,
372 });
373 results.unused_types.push(UnusedExport {
374 path: PathBuf::from("c.ts"),
375 export_name: "T".to_string(),
376 is_type_only: true,
377 line: 1,
378 col: 0,
379 span_start: 0,
380 is_re_export: false,
381 });
382 results.unused_dependencies.push(UnusedDependency {
383 package_name: "dep".to_string(),
384 location: DependencyLocation::Dependencies,
385 path: PathBuf::from("package.json"),
386 line: 5,
387 });
388 results.unused_dev_dependencies.push(UnusedDependency {
389 package_name: "dev".to_string(),
390 location: DependencyLocation::DevDependencies,
391 path: PathBuf::from("package.json"),
392 line: 5,
393 });
394 results.unused_enum_members.push(UnusedMember {
395 path: PathBuf::from("d.ts"),
396 parent_name: "E".to_string(),
397 member_name: "A".to_string(),
398 kind: MemberKind::EnumMember,
399 line: 1,
400 col: 0,
401 });
402 results.unused_class_members.push(UnusedMember {
403 path: PathBuf::from("e.ts"),
404 parent_name: "C".to_string(),
405 member_name: "m".to_string(),
406 kind: MemberKind::ClassMethod,
407 line: 1,
408 col: 0,
409 });
410 results.unresolved_imports.push(UnresolvedImport {
411 path: PathBuf::from("f.ts"),
412 specifier: "./missing".to_string(),
413 line: 1,
414 col: 0,
415 specifier_col: 0,
416 });
417 results.unlisted_dependencies.push(UnlistedDependency {
418 package_name: "unlisted".to_string(),
419 imported_from: vec![ImportSite {
420 path: PathBuf::from("g.ts"),
421 line: 1,
422 col: 0,
423 }],
424 });
425 results.duplicate_exports.push(DuplicateExport {
426 export_name: "dup".to_string(),
427 locations: vec![
428 DuplicateLocation {
429 path: PathBuf::from("h.ts"),
430 line: 15,
431 col: 0,
432 },
433 DuplicateLocation {
434 path: PathBuf::from("i.ts"),
435 line: 30,
436 col: 0,
437 },
438 ],
439 });
440 results.unused_optional_dependencies.push(UnusedDependency {
441 package_name: "optional".to_string(),
442 location: DependencyLocation::OptionalDependencies,
443 path: PathBuf::from("package.json"),
444 line: 5,
445 });
446 results.type_only_dependencies.push(TypeOnlyDependency {
447 package_name: "type-only".to_string(),
448 path: PathBuf::from("package.json"),
449 line: 8,
450 });
451 results.circular_dependencies.push(CircularDependency {
452 files: vec![PathBuf::from("a.ts"), PathBuf::from("b.ts")],
453 length: 2,
454 line: 3,
455 col: 0,
456 });
457
458 assert_eq!(results.total_issues(), 13);
460 assert!(results.has_issues());
461 }
462
463 #[test]
466 fn total_issues_and_has_issues_are_consistent() {
467 let results = AnalysisResults::default();
468 assert_eq!(results.total_issues(), 0);
469 assert!(!results.has_issues());
470 assert_eq!(results.total_issues() > 0, results.has_issues());
471 }
472
473 #[test]
476 fn total_issues_sums_all_categories_independently() {
477 let mut results = AnalysisResults::default();
478 results.unused_files.push(UnusedFile {
479 path: PathBuf::from("a.ts"),
480 });
481 assert_eq!(results.total_issues(), 1);
482
483 results.unused_files.push(UnusedFile {
484 path: PathBuf::from("b.ts"),
485 });
486 assert_eq!(results.total_issues(), 2);
487
488 results.unresolved_imports.push(UnresolvedImport {
489 path: PathBuf::from("c.ts"),
490 specifier: "./missing".to_string(),
491 line: 1,
492 col: 0,
493 specifier_col: 0,
494 });
495 assert_eq!(results.total_issues(), 3);
496 }
497
498 #[test]
501 fn default_results_all_fields_empty() {
502 let r = AnalysisResults::default();
503 assert!(r.unused_files.is_empty());
504 assert!(r.unused_exports.is_empty());
505 assert!(r.unused_types.is_empty());
506 assert!(r.unused_dependencies.is_empty());
507 assert!(r.unused_dev_dependencies.is_empty());
508 assert!(r.unused_optional_dependencies.is_empty());
509 assert!(r.unused_enum_members.is_empty());
510 assert!(r.unused_class_members.is_empty());
511 assert!(r.unresolved_imports.is_empty());
512 assert!(r.unlisted_dependencies.is_empty());
513 assert!(r.duplicate_exports.is_empty());
514 assert!(r.type_only_dependencies.is_empty());
515 assert!(r.circular_dependencies.is_empty());
516 assert!(r.export_usages.is_empty());
517 }
518}