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}
157
158#[derive(Debug, Clone, Serialize)]
160pub struct UnlistedDependency {
161 pub package_name: String,
163 pub imported_from: Vec<ImportSite>,
165}
166
167#[derive(Debug, Clone, Serialize)]
169pub struct ImportSite {
170 #[serde(serialize_with = "serde_path::serialize")]
172 pub path: PathBuf,
173 pub line: u32,
175 pub col: u32,
177}
178
179#[derive(Debug, Clone, Serialize)]
181pub struct DuplicateExport {
182 pub export_name: String,
184 pub locations: Vec<DuplicateLocation>,
186}
187
188#[derive(Debug, Clone, Serialize)]
190pub struct DuplicateLocation {
191 #[serde(serialize_with = "serde_path::serialize")]
193 pub path: PathBuf,
194 pub line: u32,
196 pub col: u32,
198}
199
200#[derive(Debug, Clone, Serialize)]
204pub struct TypeOnlyDependency {
205 pub package_name: String,
207 #[serde(serialize_with = "serde_path::serialize")]
209 pub path: PathBuf,
210 pub line: u32,
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct CircularDependency {
217 #[serde(serialize_with = "serde_path::serialize_vec")]
219 pub files: Vec<PathBuf>,
220 pub length: usize,
222 #[serde(default)]
224 pub line: u32,
225 #[serde(default)]
227 pub col: u32,
228}
229
230#[derive(Debug, Clone, Serialize)]
233pub struct ExportUsage {
234 #[serde(serialize_with = "serde_path::serialize")]
236 pub path: PathBuf,
237 pub export_name: String,
239 pub line: u32,
241 pub col: u32,
243 pub reference_count: usize,
245 pub reference_locations: Vec<ReferenceLocation>,
248}
249
250#[derive(Debug, Clone, Serialize)]
252pub struct ReferenceLocation {
253 #[serde(serialize_with = "serde_path::serialize")]
255 pub path: PathBuf,
256 pub line: u32,
258 pub col: u32,
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265
266 #[test]
267 fn empty_results_no_issues() {
268 let results = AnalysisResults::default();
269 assert_eq!(results.total_issues(), 0);
270 assert!(!results.has_issues());
271 }
272
273 #[test]
274 fn results_with_unused_file() {
275 let mut results = AnalysisResults::default();
276 results.unused_files.push(UnusedFile {
277 path: PathBuf::from("test.ts"),
278 });
279 assert_eq!(results.total_issues(), 1);
280 assert!(results.has_issues());
281 }
282
283 #[test]
284 fn results_with_unused_export() {
285 let mut results = AnalysisResults::default();
286 results.unused_exports.push(UnusedExport {
287 path: PathBuf::from("test.ts"),
288 export_name: "foo".to_string(),
289 is_type_only: false,
290 line: 1,
291 col: 0,
292 span_start: 0,
293 is_re_export: false,
294 });
295 assert_eq!(results.total_issues(), 1);
296 assert!(results.has_issues());
297 }
298
299 #[test]
300 fn results_total_counts_all_types() {
301 let mut results = AnalysisResults::default();
302 results.unused_files.push(UnusedFile {
303 path: PathBuf::from("a.ts"),
304 });
305 results.unused_exports.push(UnusedExport {
306 path: PathBuf::from("b.ts"),
307 export_name: "x".to_string(),
308 is_type_only: false,
309 line: 1,
310 col: 0,
311 span_start: 0,
312 is_re_export: false,
313 });
314 results.unused_types.push(UnusedExport {
315 path: PathBuf::from("c.ts"),
316 export_name: "T".to_string(),
317 is_type_only: true,
318 line: 1,
319 col: 0,
320 span_start: 0,
321 is_re_export: false,
322 });
323 results.unused_dependencies.push(UnusedDependency {
324 package_name: "dep".to_string(),
325 location: DependencyLocation::Dependencies,
326 path: PathBuf::from("package.json"),
327 line: 5,
328 });
329 results.unused_dev_dependencies.push(UnusedDependency {
330 package_name: "dev".to_string(),
331 location: DependencyLocation::DevDependencies,
332 path: PathBuf::from("package.json"),
333 line: 5,
334 });
335 results.unused_enum_members.push(UnusedMember {
336 path: PathBuf::from("d.ts"),
337 parent_name: "E".to_string(),
338 member_name: "A".to_string(),
339 kind: MemberKind::EnumMember,
340 line: 1,
341 col: 0,
342 });
343 results.unused_class_members.push(UnusedMember {
344 path: PathBuf::from("e.ts"),
345 parent_name: "C".to_string(),
346 member_name: "m".to_string(),
347 kind: MemberKind::ClassMethod,
348 line: 1,
349 col: 0,
350 });
351 results.unresolved_imports.push(UnresolvedImport {
352 path: PathBuf::from("f.ts"),
353 specifier: "./missing".to_string(),
354 line: 1,
355 col: 0,
356 });
357 results.unlisted_dependencies.push(UnlistedDependency {
358 package_name: "unlisted".to_string(),
359 imported_from: vec![ImportSite {
360 path: PathBuf::from("g.ts"),
361 line: 1,
362 col: 0,
363 }],
364 });
365 results.duplicate_exports.push(DuplicateExport {
366 export_name: "dup".to_string(),
367 locations: vec![
368 DuplicateLocation {
369 path: PathBuf::from("h.ts"),
370 line: 15,
371 col: 0,
372 },
373 DuplicateLocation {
374 path: PathBuf::from("i.ts"),
375 line: 30,
376 col: 0,
377 },
378 ],
379 });
380 results.circular_dependencies.push(CircularDependency {
381 files: vec![PathBuf::from("a.ts"), PathBuf::from("b.ts")],
382 length: 2,
383 line: 3,
384 col: 0,
385 });
386
387 assert_eq!(results.total_issues(), 11);
388 assert!(results.has_issues());
389 }
390}