Skip to main content

fallow_types/
results.rs

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