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_enum_members: Vec<UnusedMember>,
25 pub unused_class_members: Vec<UnusedMember>,
27 pub unresolved_imports: Vec<UnresolvedImport>,
29 pub unlisted_dependencies: Vec<UnlistedDependency>,
31 pub duplicate_exports: Vec<DuplicateExport>,
33 pub type_only_dependencies: Vec<TypeOnlyDependency>,
36 pub circular_dependencies: Vec<CircularDependency>,
38 #[serde(skip)]
42 pub export_usages: Vec<ExportUsage>,
43}
44
45impl AnalysisResults {
46 pub const fn total_issues(&self) -> usize {
48 self.unused_files.len()
49 + self.unused_exports.len()
50 + self.unused_types.len()
51 + self.unused_dependencies.len()
52 + self.unused_dev_dependencies.len()
53 + self.unused_enum_members.len()
54 + self.unused_class_members.len()
55 + self.unresolved_imports.len()
56 + self.unlisted_dependencies.len()
57 + self.duplicate_exports.len()
58 + self.type_only_dependencies.len()
59 + self.circular_dependencies.len()
60 }
61
62 pub const fn has_issues(&self) -> bool {
64 self.total_issues() > 0
65 }
66}
67
68#[derive(Debug, Clone, Serialize)]
70pub struct UnusedFile {
71 #[serde(serialize_with = "serde_path::serialize")]
73 pub path: PathBuf,
74}
75
76#[derive(Debug, Clone, Serialize)]
78pub struct UnusedExport {
79 #[serde(serialize_with = "serde_path::serialize")]
81 pub path: PathBuf,
82 pub export_name: String,
84 pub is_type_only: bool,
86 pub line: u32,
88 pub col: u32,
90 pub span_start: u32,
92 pub is_re_export: bool,
94}
95
96#[derive(Debug, Clone, Serialize)]
98pub struct UnusedDependency {
99 pub package_name: String,
101 pub location: DependencyLocation,
103 #[serde(serialize_with = "serde_path::serialize")]
106 pub path: PathBuf,
107}
108
109#[derive(Debug, Clone, Serialize)]
111#[serde(rename_all = "camelCase")]
112pub enum DependencyLocation {
113 Dependencies,
115 DevDependencies,
117}
118
119#[derive(Debug, Clone, Serialize)]
121pub struct UnusedMember {
122 #[serde(serialize_with = "serde_path::serialize")]
124 pub path: PathBuf,
125 pub parent_name: String,
127 pub member_name: String,
129 pub kind: MemberKind,
131 pub line: u32,
133 pub col: u32,
135}
136
137#[derive(Debug, Clone, Serialize)]
139pub struct UnresolvedImport {
140 #[serde(serialize_with = "serde_path::serialize")]
142 pub path: PathBuf,
143 pub specifier: String,
145 pub line: u32,
147 pub col: u32,
149}
150
151#[derive(Debug, Clone, Serialize)]
153pub struct UnlistedDependency {
154 pub package_name: String,
156 #[serde(serialize_with = "serde_path::serialize_vec")]
158 pub imported_from: Vec<PathBuf>,
159}
160
161#[derive(Debug, Clone, Serialize)]
163pub struct DuplicateExport {
164 pub export_name: String,
166 #[serde(serialize_with = "serde_path::serialize_vec")]
168 pub locations: Vec<PathBuf>,
169}
170
171#[derive(Debug, Clone, Serialize)]
175pub struct TypeOnlyDependency {
176 pub package_name: String,
178 #[serde(serialize_with = "serde_path::serialize")]
180 pub path: PathBuf,
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct CircularDependency {
186 #[serde(serialize_with = "serde_path::serialize_vec")]
188 pub files: Vec<PathBuf>,
189 pub length: usize,
191}
192
193#[derive(Debug, Clone, Serialize)]
196pub struct ExportUsage {
197 #[serde(serialize_with = "serde_path::serialize")]
199 pub path: PathBuf,
200 pub export_name: String,
202 pub line: u32,
204 pub col: u32,
206 pub reference_count: usize,
208 pub reference_locations: Vec<ReferenceLocation>,
211}
212
213#[derive(Debug, Clone, Serialize)]
215pub struct ReferenceLocation {
216 #[serde(serialize_with = "serde_path::serialize")]
218 pub path: PathBuf,
219 pub line: u32,
221 pub col: u32,
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn empty_results_no_issues() {
231 let results = AnalysisResults::default();
232 assert_eq!(results.total_issues(), 0);
233 assert!(!results.has_issues());
234 }
235
236 #[test]
237 fn results_with_unused_file() {
238 let mut results = AnalysisResults::default();
239 results.unused_files.push(UnusedFile {
240 path: PathBuf::from("test.ts"),
241 });
242 assert_eq!(results.total_issues(), 1);
243 assert!(results.has_issues());
244 }
245
246 #[test]
247 fn results_with_unused_export() {
248 let mut results = AnalysisResults::default();
249 results.unused_exports.push(UnusedExport {
250 path: PathBuf::from("test.ts"),
251 export_name: "foo".to_string(),
252 is_type_only: false,
253 line: 1,
254 col: 0,
255 span_start: 0,
256 is_re_export: false,
257 });
258 assert_eq!(results.total_issues(), 1);
259 assert!(results.has_issues());
260 }
261
262 #[test]
263 fn results_total_counts_all_types() {
264 let mut results = AnalysisResults::default();
265 results.unused_files.push(UnusedFile {
266 path: PathBuf::from("a.ts"),
267 });
268 results.unused_exports.push(UnusedExport {
269 path: PathBuf::from("b.ts"),
270 export_name: "x".to_string(),
271 is_type_only: false,
272 line: 1,
273 col: 0,
274 span_start: 0,
275 is_re_export: false,
276 });
277 results.unused_types.push(UnusedExport {
278 path: PathBuf::from("c.ts"),
279 export_name: "T".to_string(),
280 is_type_only: true,
281 line: 1,
282 col: 0,
283 span_start: 0,
284 is_re_export: false,
285 });
286 results.unused_dependencies.push(UnusedDependency {
287 package_name: "dep".to_string(),
288 location: DependencyLocation::Dependencies,
289 path: PathBuf::from("package.json"),
290 });
291 results.unused_dev_dependencies.push(UnusedDependency {
292 package_name: "dev".to_string(),
293 location: DependencyLocation::DevDependencies,
294 path: PathBuf::from("package.json"),
295 });
296 results.unused_enum_members.push(UnusedMember {
297 path: PathBuf::from("d.ts"),
298 parent_name: "E".to_string(),
299 member_name: "A".to_string(),
300 kind: MemberKind::EnumMember,
301 line: 1,
302 col: 0,
303 });
304 results.unused_class_members.push(UnusedMember {
305 path: PathBuf::from("e.ts"),
306 parent_name: "C".to_string(),
307 member_name: "m".to_string(),
308 kind: MemberKind::ClassMethod,
309 line: 1,
310 col: 0,
311 });
312 results.unresolved_imports.push(UnresolvedImport {
313 path: PathBuf::from("f.ts"),
314 specifier: "./missing".to_string(),
315 line: 1,
316 col: 0,
317 });
318 results.unlisted_dependencies.push(UnlistedDependency {
319 package_name: "unlisted".to_string(),
320 imported_from: vec![PathBuf::from("g.ts")],
321 });
322 results.duplicate_exports.push(DuplicateExport {
323 export_name: "dup".to_string(),
324 locations: vec![PathBuf::from("h.ts"), PathBuf::from("i.ts")],
325 });
326 results.circular_dependencies.push(CircularDependency {
327 files: vec![PathBuf::from("a.ts"), PathBuf::from("b.ts")],
328 length: 2,
329 });
330
331 assert_eq!(results.total_issues(), 11);
332 assert!(results.has_issues());
333 }
334}