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}
111
112#[derive(Debug, Clone, Serialize)]
114#[serde(rename_all = "camelCase")]
115pub enum DependencyLocation {
116 Dependencies,
118 DevDependencies,
120 OptionalDependencies,
122}
123
124#[derive(Debug, Clone, Serialize)]
126pub struct UnusedMember {
127 #[serde(serialize_with = "serde_path::serialize")]
129 pub path: PathBuf,
130 pub parent_name: String,
132 pub member_name: String,
134 pub kind: MemberKind,
136 pub line: u32,
138 pub col: u32,
140}
141
142#[derive(Debug, Clone, Serialize)]
144pub struct UnresolvedImport {
145 #[serde(serialize_with = "serde_path::serialize")]
147 pub path: PathBuf,
148 pub specifier: String,
150 pub line: u32,
152 pub col: u32,
154}
155
156#[derive(Debug, Clone, Serialize)]
158pub struct UnlistedDependency {
159 pub package_name: String,
161 #[serde(serialize_with = "serde_path::serialize_vec")]
163 pub imported_from: Vec<PathBuf>,
164}
165
166#[derive(Debug, Clone, Serialize)]
168pub struct DuplicateExport {
169 pub export_name: String,
171 pub locations: Vec<DuplicateLocation>,
173}
174
175#[derive(Debug, Clone, Serialize)]
177pub struct DuplicateLocation {
178 #[serde(serialize_with = "serde_path::serialize")]
180 pub path: PathBuf,
181 pub line: u32,
183 pub col: u32,
185}
186
187#[derive(Debug, Clone, Serialize)]
191pub struct TypeOnlyDependency {
192 pub package_name: String,
194 #[serde(serialize_with = "serde_path::serialize")]
196 pub path: PathBuf,
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct CircularDependency {
202 #[serde(serialize_with = "serde_path::serialize_vec")]
204 pub files: Vec<PathBuf>,
205 pub length: usize,
207}
208
209#[derive(Debug, Clone, Serialize)]
212pub struct ExportUsage {
213 #[serde(serialize_with = "serde_path::serialize")]
215 pub path: PathBuf,
216 pub export_name: String,
218 pub line: u32,
220 pub col: u32,
222 pub reference_count: usize,
224 pub reference_locations: Vec<ReferenceLocation>,
227}
228
229#[derive(Debug, Clone, Serialize)]
231pub struct ReferenceLocation {
232 #[serde(serialize_with = "serde_path::serialize")]
234 pub path: PathBuf,
235 pub line: u32,
237 pub col: u32,
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn empty_results_no_issues() {
247 let results = AnalysisResults::default();
248 assert_eq!(results.total_issues(), 0);
249 assert!(!results.has_issues());
250 }
251
252 #[test]
253 fn results_with_unused_file() {
254 let mut results = AnalysisResults::default();
255 results.unused_files.push(UnusedFile {
256 path: PathBuf::from("test.ts"),
257 });
258 assert_eq!(results.total_issues(), 1);
259 assert!(results.has_issues());
260 }
261
262 #[test]
263 fn results_with_unused_export() {
264 let mut results = AnalysisResults::default();
265 results.unused_exports.push(UnusedExport {
266 path: PathBuf::from("test.ts"),
267 export_name: "foo".to_string(),
268 is_type_only: false,
269 line: 1,
270 col: 0,
271 span_start: 0,
272 is_re_export: false,
273 });
274 assert_eq!(results.total_issues(), 1);
275 assert!(results.has_issues());
276 }
277
278 #[test]
279 fn results_total_counts_all_types() {
280 let mut results = AnalysisResults::default();
281 results.unused_files.push(UnusedFile {
282 path: PathBuf::from("a.ts"),
283 });
284 results.unused_exports.push(UnusedExport {
285 path: PathBuf::from("b.ts"),
286 export_name: "x".to_string(),
287 is_type_only: false,
288 line: 1,
289 col: 0,
290 span_start: 0,
291 is_re_export: false,
292 });
293 results.unused_types.push(UnusedExport {
294 path: PathBuf::from("c.ts"),
295 export_name: "T".to_string(),
296 is_type_only: true,
297 line: 1,
298 col: 0,
299 span_start: 0,
300 is_re_export: false,
301 });
302 results.unused_dependencies.push(UnusedDependency {
303 package_name: "dep".to_string(),
304 location: DependencyLocation::Dependencies,
305 path: PathBuf::from("package.json"),
306 });
307 results.unused_dev_dependencies.push(UnusedDependency {
308 package_name: "dev".to_string(),
309 location: DependencyLocation::DevDependencies,
310 path: PathBuf::from("package.json"),
311 });
312 results.unused_enum_members.push(UnusedMember {
313 path: PathBuf::from("d.ts"),
314 parent_name: "E".to_string(),
315 member_name: "A".to_string(),
316 kind: MemberKind::EnumMember,
317 line: 1,
318 col: 0,
319 });
320 results.unused_class_members.push(UnusedMember {
321 path: PathBuf::from("e.ts"),
322 parent_name: "C".to_string(),
323 member_name: "m".to_string(),
324 kind: MemberKind::ClassMethod,
325 line: 1,
326 col: 0,
327 });
328 results.unresolved_imports.push(UnresolvedImport {
329 path: PathBuf::from("f.ts"),
330 specifier: "./missing".to_string(),
331 line: 1,
332 col: 0,
333 });
334 results.unlisted_dependencies.push(UnlistedDependency {
335 package_name: "unlisted".to_string(),
336 imported_from: vec![PathBuf::from("g.ts")],
337 });
338 results.duplicate_exports.push(DuplicateExport {
339 export_name: "dup".to_string(),
340 locations: vec![
341 DuplicateLocation {
342 path: PathBuf::from("h.ts"),
343 line: 15,
344 col: 0,
345 },
346 DuplicateLocation {
347 path: PathBuf::from("i.ts"),
348 line: 30,
349 col: 0,
350 },
351 ],
352 });
353 results.circular_dependencies.push(CircularDependency {
354 files: vec![PathBuf::from("a.ts"), PathBuf::from("b.ts")],
355 length: 2,
356 });
357
358 assert_eq!(results.total_issues(), 11);
359 assert!(results.has_issues());
360 }
361}