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)]
12pub struct AnalysisResults {
13 pub unused_files: Vec<UnusedFile>,
15 pub unused_exports: Vec<UnusedExport>,
17 pub unused_types: Vec<UnusedExport>,
19 pub unused_dependencies: Vec<UnusedDependency>,
21 pub unused_dev_dependencies: Vec<UnusedDependency>,
23 pub unused_optional_dependencies: Vec<UnusedDependency>,
25 pub unused_enum_members: Vec<UnusedMember>,
27 pub unused_class_members: Vec<UnusedMember>,
29 pub unresolved_imports: Vec<UnresolvedImport>,
31 pub unlisted_dependencies: Vec<UnlistedDependency>,
33 pub duplicate_exports: Vec<DuplicateExport>,
35 pub type_only_dependencies: Vec<TypeOnlyDependency>,
38 pub circular_dependencies: Vec<CircularDependency>,
40 #[serde(skip)]
44 pub export_usages: Vec<ExportUsage>,
45}
46
47impl AnalysisResults {
48 pub const fn total_issues(&self) -> usize {
50 self.unused_files.len()
51 + self.unused_exports.len()
52 + self.unused_types.len()
53 + self.unused_dependencies.len()
54 + self.unused_dev_dependencies.len()
55 + self.unused_optional_dependencies.len()
56 + self.unused_enum_members.len()
57 + self.unused_class_members.len()
58 + self.unresolved_imports.len()
59 + self.unlisted_dependencies.len()
60 + self.duplicate_exports.len()
61 + self.type_only_dependencies.len()
62 + self.circular_dependencies.len()
63 }
64
65 pub const fn has_issues(&self) -> bool {
67 self.total_issues() > 0
68 }
69}
70
71#[derive(Debug, Clone, Serialize)]
73pub struct UnusedFile {
74 #[serde(serialize_with = "serde_path::serialize")]
76 pub path: PathBuf,
77}
78
79#[derive(Debug, Clone, Serialize)]
81pub struct UnusedExport {
82 #[serde(serialize_with = "serde_path::serialize")]
84 pub path: PathBuf,
85 pub export_name: String,
87 pub is_type_only: bool,
89 pub line: u32,
91 pub col: u32,
93 pub span_start: u32,
95 pub is_re_export: bool,
97}
98
99#[derive(Debug, Clone, Serialize)]
101pub struct UnusedDependency {
102 pub package_name: String,
104 pub location: DependencyLocation,
106 #[serde(serialize_with = "serde_path::serialize")]
109 pub path: PathBuf,
110 pub line: u32,
112}
113
114#[derive(Debug, Clone, Serialize)]
116#[serde(rename_all = "camelCase")]
117pub enum DependencyLocation {
118 Dependencies,
120 DevDependencies,
122 OptionalDependencies,
124}
125
126#[derive(Debug, Clone, Serialize)]
128pub struct UnusedMember {
129 #[serde(serialize_with = "serde_path::serialize")]
131 pub path: PathBuf,
132 pub parent_name: String,
134 pub member_name: String,
136 pub kind: MemberKind,
138 pub line: u32,
140 pub col: u32,
142}
143
144#[derive(Debug, Clone, Serialize)]
146pub struct UnresolvedImport {
147 #[serde(serialize_with = "serde_path::serialize")]
149 pub path: PathBuf,
150 pub specifier: String,
152 pub line: u32,
154 pub col: u32,
156 pub specifier_col: u32,
159}
160
161#[derive(Debug, Clone, Serialize)]
163pub struct UnlistedDependency {
164 pub package_name: String,
166 pub imported_from: Vec<ImportSite>,
168}
169
170#[derive(Debug, Clone, Serialize)]
172pub struct ImportSite {
173 #[serde(serialize_with = "serde_path::serialize")]
175 pub path: PathBuf,
176 pub line: u32,
178 pub col: u32,
180}
181
182#[derive(Debug, Clone, Serialize)]
184pub struct DuplicateExport {
185 pub export_name: String,
187 pub locations: Vec<DuplicateLocation>,
189}
190
191#[derive(Debug, Clone, Serialize)]
193pub struct DuplicateLocation {
194 #[serde(serialize_with = "serde_path::serialize")]
196 pub path: PathBuf,
197 pub line: u32,
199 pub col: u32,
201}
202
203#[derive(Debug, Clone, Serialize)]
207pub struct TypeOnlyDependency {
208 pub package_name: String,
210 #[serde(serialize_with = "serde_path::serialize")]
212 pub path: PathBuf,
213 pub line: u32,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct CircularDependency {
220 #[serde(serialize_with = "serde_path::serialize_vec")]
222 pub files: Vec<PathBuf>,
223 pub length: usize,
225 #[serde(default)]
227 pub line: u32,
228 #[serde(default)]
230 pub col: u32,
231}
232
233#[derive(Debug, Clone, Serialize)]
236pub struct ExportUsage {
237 #[serde(serialize_with = "serde_path::serialize")]
239 pub path: PathBuf,
240 pub export_name: String,
242 pub line: u32,
244 pub col: u32,
246 pub reference_count: usize,
248 pub reference_locations: Vec<ReferenceLocation>,
251}
252
253#[derive(Debug, Clone, Serialize)]
255pub struct ReferenceLocation {
256 #[serde(serialize_with = "serde_path::serialize")]
258 pub path: PathBuf,
259 pub line: u32,
261 pub col: u32,
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268
269 #[test]
270 fn empty_results_no_issues() {
271 let results = AnalysisResults::default();
272 assert_eq!(results.total_issues(), 0);
273 assert!(!results.has_issues());
274 }
275
276 #[test]
277 fn results_with_unused_file() {
278 let mut results = AnalysisResults::default();
279 results.unused_files.push(UnusedFile {
280 path: PathBuf::from("test.ts"),
281 });
282 assert_eq!(results.total_issues(), 1);
283 assert!(results.has_issues());
284 }
285
286 #[test]
287 fn results_with_unused_export() {
288 let mut results = AnalysisResults::default();
289 results.unused_exports.push(UnusedExport {
290 path: PathBuf::from("test.ts"),
291 export_name: "foo".to_string(),
292 is_type_only: false,
293 line: 1,
294 col: 0,
295 span_start: 0,
296 is_re_export: false,
297 });
298 assert_eq!(results.total_issues(), 1);
299 assert!(results.has_issues());
300 }
301
302 #[test]
303 fn results_total_counts_all_types() {
304 let mut results = AnalysisResults::default();
305 results.unused_files.push(UnusedFile {
306 path: PathBuf::from("a.ts"),
307 });
308 results.unused_exports.push(UnusedExport {
309 path: PathBuf::from("b.ts"),
310 export_name: "x".to_string(),
311 is_type_only: false,
312 line: 1,
313 col: 0,
314 span_start: 0,
315 is_re_export: false,
316 });
317 results.unused_types.push(UnusedExport {
318 path: PathBuf::from("c.ts"),
319 export_name: "T".to_string(),
320 is_type_only: true,
321 line: 1,
322 col: 0,
323 span_start: 0,
324 is_re_export: false,
325 });
326 results.unused_dependencies.push(UnusedDependency {
327 package_name: "dep".to_string(),
328 location: DependencyLocation::Dependencies,
329 path: PathBuf::from("package.json"),
330 line: 5,
331 });
332 results.unused_dev_dependencies.push(UnusedDependency {
333 package_name: "dev".to_string(),
334 location: DependencyLocation::DevDependencies,
335 path: PathBuf::from("package.json"),
336 line: 5,
337 });
338 results.unused_enum_members.push(UnusedMember {
339 path: PathBuf::from("d.ts"),
340 parent_name: "E".to_string(),
341 member_name: "A".to_string(),
342 kind: MemberKind::EnumMember,
343 line: 1,
344 col: 0,
345 });
346 results.unused_class_members.push(UnusedMember {
347 path: PathBuf::from("e.ts"),
348 parent_name: "C".to_string(),
349 member_name: "m".to_string(),
350 kind: MemberKind::ClassMethod,
351 line: 1,
352 col: 0,
353 });
354 results.unresolved_imports.push(UnresolvedImport {
355 path: PathBuf::from("f.ts"),
356 specifier: "./missing".to_string(),
357 line: 1,
358 col: 0,
359 specifier_col: 0,
360 });
361 results.unlisted_dependencies.push(UnlistedDependency {
362 package_name: "unlisted".to_string(),
363 imported_from: vec![ImportSite {
364 path: PathBuf::from("g.ts"),
365 line: 1,
366 col: 0,
367 }],
368 });
369 results.duplicate_exports.push(DuplicateExport {
370 export_name: "dup".to_string(),
371 locations: vec![
372 DuplicateLocation {
373 path: PathBuf::from("h.ts"),
374 line: 15,
375 col: 0,
376 },
377 DuplicateLocation {
378 path: PathBuf::from("i.ts"),
379 line: 30,
380 col: 0,
381 },
382 ],
383 });
384 results.circular_dependencies.push(CircularDependency {
385 files: vec![PathBuf::from("a.ts"), PathBuf::from("b.ts")],
386 length: 2,
387 line: 3,
388 col: 0,
389 });
390
391 assert_eq!(results.total_issues(), 11);
392 assert!(results.has_issues());
393 }
394}