Skip to main content

fallow_types/
results.rs

1//! Analysis result types for all dead code issue categories.
2
3use std::path::PathBuf;
4
5use serde::{Deserialize, Serialize};
6
7use crate::extract::MemberKind;
8use crate::serde_path;
9
10/// Complete analysis results.
11#[derive(Debug, Default, Clone, Serialize)]
12pub struct AnalysisResults {
13    /// Files not reachable from any entry point.
14    pub unused_files: Vec<UnusedFile>,
15    /// Exports never imported by other modules.
16    pub unused_exports: Vec<UnusedExport>,
17    /// Type exports never imported by other modules.
18    pub unused_types: Vec<UnusedExport>,
19    /// Dependencies listed in package.json but never imported.
20    pub unused_dependencies: Vec<UnusedDependency>,
21    /// Dev dependencies listed in package.json but never imported.
22    pub unused_dev_dependencies: Vec<UnusedDependency>,
23    /// Enum members never accessed.
24    pub unused_enum_members: Vec<UnusedMember>,
25    /// Class members never accessed.
26    pub unused_class_members: Vec<UnusedMember>,
27    /// Import specifiers that could not be resolved.
28    pub unresolved_imports: Vec<UnresolvedImport>,
29    /// Dependencies used in code but not listed in package.json.
30    pub unlisted_dependencies: Vec<UnlistedDependency>,
31    /// Exports with the same name across multiple modules.
32    pub duplicate_exports: Vec<DuplicateExport>,
33    /// Production dependencies only used via type-only imports (could be devDependencies).
34    /// Only populated in production mode.
35    pub type_only_dependencies: Vec<TypeOnlyDependency>,
36    /// Circular dependency chains detected in the module graph.
37    pub circular_dependencies: Vec<CircularDependency>,
38    /// Usage counts for all exports across the project. Used by the LSP for Code Lens.
39    /// Not included in issue counts -- this is metadata, not an issue type.
40    /// Skipped during serialization: this is internal LSP data, not part of the JSON output schema.
41    #[serde(skip)]
42    pub export_usages: Vec<ExportUsage>,
43}
44
45impl AnalysisResults {
46    /// Total number of issues found.
47    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    /// Whether any issues were found.
63    pub const fn has_issues(&self) -> bool {
64        self.total_issues() > 0
65    }
66}
67
68/// A file that is not reachable from any entry point.
69#[derive(Debug, Clone, Serialize)]
70pub struct UnusedFile {
71    /// Absolute path to the unused file.
72    #[serde(serialize_with = "serde_path::serialize")]
73    pub path: PathBuf,
74}
75
76/// An export that is never imported by other modules.
77#[derive(Debug, Clone, Serialize)]
78pub struct UnusedExport {
79    /// File containing the unused export.
80    #[serde(serialize_with = "serde_path::serialize")]
81    pub path: PathBuf,
82    /// Name of the unused export.
83    pub export_name: String,
84    /// Whether this is a type-only export.
85    pub is_type_only: bool,
86    /// 1-based line number of the export.
87    pub line: u32,
88    /// 0-based byte column offset.
89    pub col: u32,
90    /// Byte offset into the source file (used by the fix command).
91    pub span_start: u32,
92    /// Whether this finding comes from a barrel/index re-export rather than the source definition.
93    pub is_re_export: bool,
94}
95
96/// A dependency that is listed in package.json but never imported.
97#[derive(Debug, Clone, Serialize)]
98pub struct UnusedDependency {
99    /// npm package name.
100    pub package_name: String,
101    /// Whether this is in `dependencies` or `devDependencies`.
102    pub location: DependencyLocation,
103    /// Path to the package.json where this dependency is listed.
104    /// For root deps this is `<root>/package.json`, for workspace deps it is `<ws>/package.json`.
105    #[serde(serialize_with = "serde_path::serialize")]
106    pub path: PathBuf,
107}
108
109/// Where in package.json a dependency is listed.
110#[derive(Debug, Clone, Serialize)]
111#[serde(rename_all = "camelCase")]
112pub enum DependencyLocation {
113    /// Listed in `dependencies`.
114    Dependencies,
115    /// Listed in `devDependencies`.
116    DevDependencies,
117}
118
119/// An unused enum or class member.
120#[derive(Debug, Clone, Serialize)]
121pub struct UnusedMember {
122    /// File containing the unused member.
123    #[serde(serialize_with = "serde_path::serialize")]
124    pub path: PathBuf,
125    /// Name of the parent enum or class.
126    pub parent_name: String,
127    /// Name of the unused member.
128    pub member_name: String,
129    /// Whether this is an enum member, class method, or class property.
130    pub kind: MemberKind,
131    /// 1-based line number.
132    pub line: u32,
133    /// 0-based byte column offset.
134    pub col: u32,
135}
136
137/// An import that could not be resolved.
138#[derive(Debug, Clone, Serialize)]
139pub struct UnresolvedImport {
140    /// File containing the unresolved import.
141    #[serde(serialize_with = "serde_path::serialize")]
142    pub path: PathBuf,
143    /// The import specifier that could not be resolved.
144    pub specifier: String,
145    /// 1-based line number.
146    pub line: u32,
147    /// 0-based byte column offset.
148    pub col: u32,
149}
150
151/// A dependency used in code but not listed in package.json.
152#[derive(Debug, Clone, Serialize)]
153pub struct UnlistedDependency {
154    /// npm package name.
155    pub package_name: String,
156    /// Files that import this unlisted dependency.
157    #[serde(serialize_with = "serde_path::serialize_vec")]
158    pub imported_from: Vec<PathBuf>,
159}
160
161/// An export that appears multiple times across the project.
162#[derive(Debug, Clone, Serialize)]
163pub struct DuplicateExport {
164    /// The duplicated export name.
165    pub export_name: String,
166    /// Files that export this name.
167    #[serde(serialize_with = "serde_path::serialize_vec")]
168    pub locations: Vec<PathBuf>,
169}
170
171/// A production dependency that is only used via type-only imports.
172/// In production builds, type imports are erased, so this dependency
173/// is not needed at runtime and could be moved to devDependencies.
174#[derive(Debug, Clone, Serialize)]
175pub struct TypeOnlyDependency {
176    /// npm package name.
177    pub package_name: String,
178    /// Path to the package.json where the dependency is listed.
179    #[serde(serialize_with = "serde_path::serialize")]
180    pub path: PathBuf,
181}
182
183/// A circular dependency chain detected in the module graph.
184#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct CircularDependency {
186    /// Files forming the cycle, in import order.
187    #[serde(serialize_with = "serde_path::serialize_vec")]
188    pub files: Vec<PathBuf>,
189    /// Number of files in the cycle.
190    pub length: usize,
191}
192
193/// Usage count for an export symbol. Used by the LSP Code Lens to show
194/// reference counts above each export declaration.
195#[derive(Debug, Clone, Serialize)]
196pub struct ExportUsage {
197    /// File containing the export.
198    #[serde(serialize_with = "serde_path::serialize")]
199    pub path: PathBuf,
200    /// Name of the exported symbol.
201    pub export_name: String,
202    /// 1-based line number.
203    pub line: u32,
204    /// 0-based byte column offset.
205    pub col: u32,
206    /// Number of files that reference this export.
207    pub reference_count: usize,
208    /// Locations where this export is referenced. Used by the LSP Code Lens
209    /// to enable click-to-navigate via `editor.action.showReferences`.
210    pub reference_locations: Vec<ReferenceLocation>,
211}
212
213/// A location where an export is referenced (import site in another file).
214#[derive(Debug, Clone, Serialize)]
215pub struct ReferenceLocation {
216    /// File containing the import that references the export.
217    #[serde(serialize_with = "serde_path::serialize")]
218    pub path: PathBuf,
219    /// 1-based line number.
220    pub line: u32,
221    /// 0-based byte column offset.
222    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}