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 #[must_use]
50 pub const fn total_issues(&self) -> usize {
51 self.unused_files.len()
52 + self.unused_exports.len()
53 + self.unused_types.len()
54 + self.unused_dependencies.len()
55 + self.unused_dev_dependencies.len()
56 + self.unused_optional_dependencies.len()
57 + self.unused_enum_members.len()
58 + self.unused_class_members.len()
59 + self.unresolved_imports.len()
60 + self.unlisted_dependencies.len()
61 + self.duplicate_exports.len()
62 + self.type_only_dependencies.len()
63 + self.circular_dependencies.len()
64 }
65
66 #[must_use]
68 pub const fn has_issues(&self) -> bool {
69 self.total_issues() > 0
70 }
71}
72
73#[derive(Debug, Clone, Serialize)]
75pub struct UnusedFile {
76 #[serde(serialize_with = "serde_path::serialize")]
78 pub path: PathBuf,
79}
80
81#[derive(Debug, Clone, Serialize)]
83pub struct UnusedExport {
84 #[serde(serialize_with = "serde_path::serialize")]
86 pub path: PathBuf,
87 pub export_name: String,
89 pub is_type_only: bool,
91 pub line: u32,
93 pub col: u32,
95 pub span_start: u32,
97 pub is_re_export: bool,
99}
100
101#[derive(Debug, Clone, Serialize)]
103pub struct UnusedDependency {
104 pub package_name: String,
106 pub location: DependencyLocation,
108 #[serde(serialize_with = "serde_path::serialize")]
111 pub path: PathBuf,
112 pub line: u32,
114}
115
116#[derive(Debug, Clone, Serialize)]
118#[serde(rename_all = "camelCase")]
119pub enum DependencyLocation {
120 Dependencies,
122 DevDependencies,
124 OptionalDependencies,
126}
127
128#[derive(Debug, Clone, Serialize)]
130pub struct UnusedMember {
131 #[serde(serialize_with = "serde_path::serialize")]
133 pub path: PathBuf,
134 pub parent_name: String,
136 pub member_name: String,
138 pub kind: MemberKind,
140 pub line: u32,
142 pub col: u32,
144}
145
146#[derive(Debug, Clone, Serialize)]
148pub struct UnresolvedImport {
149 #[serde(serialize_with = "serde_path::serialize")]
151 pub path: PathBuf,
152 pub specifier: String,
154 pub line: u32,
156 pub col: u32,
158 pub specifier_col: u32,
161}
162
163#[derive(Debug, Clone, Serialize)]
165pub struct UnlistedDependency {
166 pub package_name: String,
168 pub imported_from: Vec<ImportSite>,
170}
171
172#[derive(Debug, Clone, Serialize)]
174pub struct ImportSite {
175 #[serde(serialize_with = "serde_path::serialize")]
177 pub path: PathBuf,
178 pub line: u32,
180 pub col: u32,
182}
183
184#[derive(Debug, Clone, Serialize)]
186pub struct DuplicateExport {
187 pub export_name: String,
189 pub locations: Vec<DuplicateLocation>,
191}
192
193#[derive(Debug, Clone, Serialize)]
195pub struct DuplicateLocation {
196 #[serde(serialize_with = "serde_path::serialize")]
198 pub path: PathBuf,
199 pub line: u32,
201 pub col: u32,
203}
204
205#[derive(Debug, Clone, Serialize)]
209pub struct TypeOnlyDependency {
210 pub package_name: String,
212 #[serde(serialize_with = "serde_path::serialize")]
214 pub path: PathBuf,
215 pub line: u32,
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct CircularDependency {
222 #[serde(serialize_with = "serde_path::serialize_vec")]
224 pub files: Vec<PathBuf>,
225 pub length: usize,
227 #[serde(default)]
229 pub line: u32,
230 #[serde(default)]
232 pub col: u32,
233}
234
235#[derive(Debug, Clone, Serialize)]
238pub struct ExportUsage {
239 #[serde(serialize_with = "serde_path::serialize")]
241 pub path: PathBuf,
242 pub export_name: String,
244 pub line: u32,
246 pub col: u32,
248 pub reference_count: usize,
250 pub reference_locations: Vec<ReferenceLocation>,
253}
254
255#[derive(Debug, Clone, Serialize)]
257pub struct ReferenceLocation {
258 #[serde(serialize_with = "serde_path::serialize")]
260 pub path: PathBuf,
261 pub line: u32,
263 pub col: u32,
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270
271 #[test]
272 fn empty_results_no_issues() {
273 let results = AnalysisResults::default();
274 assert_eq!(results.total_issues(), 0);
275 assert!(!results.has_issues());
276 }
277
278 #[test]
279 fn results_with_unused_file() {
280 let mut results = AnalysisResults::default();
281 results.unused_files.push(UnusedFile {
282 path: PathBuf::from("test.ts"),
283 });
284 assert_eq!(results.total_issues(), 1);
285 assert!(results.has_issues());
286 }
287
288 #[test]
289 fn results_with_unused_export() {
290 let mut results = AnalysisResults::default();
291 results.unused_exports.push(UnusedExport {
292 path: PathBuf::from("test.ts"),
293 export_name: "foo".to_string(),
294 is_type_only: false,
295 line: 1,
296 col: 0,
297 span_start: 0,
298 is_re_export: false,
299 });
300 assert_eq!(results.total_issues(), 1);
301 assert!(results.has_issues());
302 }
303
304 #[test]
305 fn results_total_counts_all_types() {
306 let mut results = AnalysisResults::default();
307 results.unused_files.push(UnusedFile {
308 path: PathBuf::from("a.ts"),
309 });
310 results.unused_exports.push(UnusedExport {
311 path: PathBuf::from("b.ts"),
312 export_name: "x".to_string(),
313 is_type_only: false,
314 line: 1,
315 col: 0,
316 span_start: 0,
317 is_re_export: false,
318 });
319 results.unused_types.push(UnusedExport {
320 path: PathBuf::from("c.ts"),
321 export_name: "T".to_string(),
322 is_type_only: true,
323 line: 1,
324 col: 0,
325 span_start: 0,
326 is_re_export: false,
327 });
328 results.unused_dependencies.push(UnusedDependency {
329 package_name: "dep".to_string(),
330 location: DependencyLocation::Dependencies,
331 path: PathBuf::from("package.json"),
332 line: 5,
333 });
334 results.unused_dev_dependencies.push(UnusedDependency {
335 package_name: "dev".to_string(),
336 location: DependencyLocation::DevDependencies,
337 path: PathBuf::from("package.json"),
338 line: 5,
339 });
340 results.unused_enum_members.push(UnusedMember {
341 path: PathBuf::from("d.ts"),
342 parent_name: "E".to_string(),
343 member_name: "A".to_string(),
344 kind: MemberKind::EnumMember,
345 line: 1,
346 col: 0,
347 });
348 results.unused_class_members.push(UnusedMember {
349 path: PathBuf::from("e.ts"),
350 parent_name: "C".to_string(),
351 member_name: "m".to_string(),
352 kind: MemberKind::ClassMethod,
353 line: 1,
354 col: 0,
355 });
356 results.unresolved_imports.push(UnresolvedImport {
357 path: PathBuf::from("f.ts"),
358 specifier: "./missing".to_string(),
359 line: 1,
360 col: 0,
361 specifier_col: 0,
362 });
363 results.unlisted_dependencies.push(UnlistedDependency {
364 package_name: "unlisted".to_string(),
365 imported_from: vec![ImportSite {
366 path: PathBuf::from("g.ts"),
367 line: 1,
368 col: 0,
369 }],
370 });
371 results.duplicate_exports.push(DuplicateExport {
372 export_name: "dup".to_string(),
373 locations: vec![
374 DuplicateLocation {
375 path: PathBuf::from("h.ts"),
376 line: 15,
377 col: 0,
378 },
379 DuplicateLocation {
380 path: PathBuf::from("i.ts"),
381 line: 30,
382 col: 0,
383 },
384 ],
385 });
386 results.unused_optional_dependencies.push(UnusedDependency {
387 package_name: "optional".to_string(),
388 location: DependencyLocation::OptionalDependencies,
389 path: PathBuf::from("package.json"),
390 line: 5,
391 });
392 results.type_only_dependencies.push(TypeOnlyDependency {
393 package_name: "type-only".to_string(),
394 path: PathBuf::from("package.json"),
395 line: 8,
396 });
397 results.circular_dependencies.push(CircularDependency {
398 files: vec![PathBuf::from("a.ts"), PathBuf::from("b.ts")],
399 length: 2,
400 line: 3,
401 col: 0,
402 });
403
404 assert_eq!(results.total_issues(), 13);
406 assert!(results.has_issues());
407 }
408}