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    /// Files that export this name.
172    #[serde(serialize_with = "serde_path::serialize_vec")]
173    pub locations: Vec<PathBuf>,
174}
175
176/// A production dependency that is only used via type-only imports.
177/// In production builds, type imports are erased, so this dependency
178/// is not needed at runtime and could be moved to devDependencies.
179#[derive(Debug, Clone, Serialize)]
180pub struct TypeOnlyDependency {
181    /// npm package name.
182    pub package_name: String,
183    /// Path to the package.json where the dependency is listed.
184    #[serde(serialize_with = "serde_path::serialize")]
185    pub path: PathBuf,
186}
187
188/// A circular dependency chain detected in the module graph.
189#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct CircularDependency {
191    /// Files forming the cycle, in import order.
192    #[serde(serialize_with = "serde_path::serialize_vec")]
193    pub files: Vec<PathBuf>,
194    /// Number of files in the cycle.
195    pub length: usize,
196}
197
198/// Usage count for an export symbol. Used by the LSP Code Lens to show
199/// reference counts above each export declaration.
200#[derive(Debug, Clone, Serialize)]
201pub struct ExportUsage {
202    /// File containing the export.
203    #[serde(serialize_with = "serde_path::serialize")]
204    pub path: PathBuf,
205    /// Name of the exported symbol.
206    pub export_name: String,
207    /// 1-based line number.
208    pub line: u32,
209    /// 0-based byte column offset.
210    pub col: u32,
211    /// Number of files that reference this export.
212    pub reference_count: usize,
213    /// Locations where this export is referenced. Used by the LSP Code Lens
214    /// to enable click-to-navigate via `editor.action.showReferences`.
215    pub reference_locations: Vec<ReferenceLocation>,
216}
217
218/// A location where an export is referenced (import site in another file).
219#[derive(Debug, Clone, Serialize)]
220pub struct ReferenceLocation {
221    /// File containing the import that references the export.
222    #[serde(serialize_with = "serde_path::serialize")]
223    pub path: PathBuf,
224    /// 1-based line number.
225    pub line: u32,
226    /// 0-based byte column offset.
227    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}