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 #[serde(serialize_with = "serde_path::serialize_vec")]
173 pub locations: Vec<PathBuf>,
174}
175
176#[derive(Debug, Clone, Serialize)]
180pub struct TypeOnlyDependency {
181 pub package_name: String,
183 #[serde(serialize_with = "serde_path::serialize")]
185 pub path: PathBuf,
186}
187
188#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct CircularDependency {
191 #[serde(serialize_with = "serde_path::serialize_vec")]
193 pub files: Vec<PathBuf>,
194 pub length: usize,
196}
197
198#[derive(Debug, Clone, Serialize)]
201pub struct ExportUsage {
202 #[serde(serialize_with = "serde_path::serialize")]
204 pub path: PathBuf,
205 pub export_name: String,
207 pub line: u32,
209 pub col: u32,
211 pub reference_count: usize,
213 pub reference_locations: Vec<ReferenceLocation>,
216}
217
218#[derive(Debug, Clone, Serialize)]
220pub struct ReferenceLocation {
221 #[serde(serialize_with = "serde_path::serialize")]
223 pub path: PathBuf,
224 pub line: u32,
226 pub col: u32,
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 #[test]
235 fn empty_results_no_issues() {
236 let results = AnalysisResults::default();
237 assert_eq!(results.total_issues(), 0);
238 assert!(!results.has_issues());
239 }
240
241 #[test]
242 fn results_with_unused_file() {
243 let mut results = AnalysisResults::default();
244 results.unused_files.push(UnusedFile {
245 path: PathBuf::from("test.ts"),
246 });
247 assert_eq!(results.total_issues(), 1);
248 assert!(results.has_issues());
249 }
250
251 #[test]
252 fn results_with_unused_export() {
253 let mut results = AnalysisResults::default();
254 results.unused_exports.push(UnusedExport {
255 path: PathBuf::from("test.ts"),
256 export_name: "foo".to_string(),
257 is_type_only: false,
258 line: 1,
259 col: 0,
260 span_start: 0,
261 is_re_export: false,
262 });
263 assert_eq!(results.total_issues(), 1);
264 assert!(results.has_issues());
265 }
266
267 #[test]
268 fn results_total_counts_all_types() {
269 let mut results = AnalysisResults::default();
270 results.unused_files.push(UnusedFile {
271 path: PathBuf::from("a.ts"),
272 });
273 results.unused_exports.push(UnusedExport {
274 path: PathBuf::from("b.ts"),
275 export_name: "x".to_string(),
276 is_type_only: false,
277 line: 1,
278 col: 0,
279 span_start: 0,
280 is_re_export: false,
281 });
282 results.unused_types.push(UnusedExport {
283 path: PathBuf::from("c.ts"),
284 export_name: "T".to_string(),
285 is_type_only: true,
286 line: 1,
287 col: 0,
288 span_start: 0,
289 is_re_export: false,
290 });
291 results.unused_dependencies.push(UnusedDependency {
292 package_name: "dep".to_string(),
293 location: DependencyLocation::Dependencies,
294 path: PathBuf::from("package.json"),
295 });
296 results.unused_dev_dependencies.push(UnusedDependency {
297 package_name: "dev".to_string(),
298 location: DependencyLocation::DevDependencies,
299 path: PathBuf::from("package.json"),
300 });
301 results.unused_enum_members.push(UnusedMember {
302 path: PathBuf::from("d.ts"),
303 parent_name: "E".to_string(),
304 member_name: "A".to_string(),
305 kind: MemberKind::EnumMember,
306 line: 1,
307 col: 0,
308 });
309 results.unused_class_members.push(UnusedMember {
310 path: PathBuf::from("e.ts"),
311 parent_name: "C".to_string(),
312 member_name: "m".to_string(),
313 kind: MemberKind::ClassMethod,
314 line: 1,
315 col: 0,
316 });
317 results.unresolved_imports.push(UnresolvedImport {
318 path: PathBuf::from("f.ts"),
319 specifier: "./missing".to_string(),
320 line: 1,
321 col: 0,
322 });
323 results.unlisted_dependencies.push(UnlistedDependency {
324 package_name: "unlisted".to_string(),
325 imported_from: vec![PathBuf::from("g.ts")],
326 });
327 results.duplicate_exports.push(DuplicateExport {
328 export_name: "dup".to_string(),
329 locations: vec![PathBuf::from("h.ts"), PathBuf::from("i.ts")],
330 });
331 results.circular_dependencies.push(CircularDependency {
332 files: vec![PathBuf::from("a.ts"), PathBuf::from("b.ts")],
333 length: 2,
334 });
335
336 assert_eq!(results.total_issues(), 11);
337 assert!(results.has_issues());
338 }
339}