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    /// 1-based line number of the dependency entry in package.json.
111    pub line: u32,
112}
113
114/// Where in package.json a dependency is listed.
115#[derive(Debug, Clone, Serialize)]
116#[serde(rename_all = "camelCase")]
117pub enum DependencyLocation {
118    /// Listed in `dependencies`.
119    Dependencies,
120    /// Listed in `devDependencies`.
121    DevDependencies,
122    /// Listed in `optionalDependencies`.
123    OptionalDependencies,
124}
125
126/// An unused enum or class member.
127#[derive(Debug, Clone, Serialize)]
128pub struct UnusedMember {
129    /// File containing the unused member.
130    #[serde(serialize_with = "serde_path::serialize")]
131    pub path: PathBuf,
132    /// Name of the parent enum or class.
133    pub parent_name: String,
134    /// Name of the unused member.
135    pub member_name: String,
136    /// Whether this is an enum member, class method, or class property.
137    pub kind: MemberKind,
138    /// 1-based line number.
139    pub line: u32,
140    /// 0-based byte column offset.
141    pub col: u32,
142}
143
144/// An import that could not be resolved.
145#[derive(Debug, Clone, Serialize)]
146pub struct UnresolvedImport {
147    /// File containing the unresolved import.
148    #[serde(serialize_with = "serde_path::serialize")]
149    pub path: PathBuf,
150    /// The import specifier that could not be resolved.
151    pub specifier: String,
152    /// 1-based line number.
153    pub line: u32,
154    /// 0-based byte column offset.
155    pub col: u32,
156}
157
158/// A dependency used in code but not listed in package.json.
159#[derive(Debug, Clone, Serialize)]
160pub struct UnlistedDependency {
161    /// npm package name.
162    pub package_name: String,
163    /// Import sites where this unlisted dependency is used (file path, line, column).
164    pub imported_from: Vec<ImportSite>,
165}
166
167/// A location where an import occurs.
168#[derive(Debug, Clone, Serialize)]
169pub struct ImportSite {
170    /// File containing the import.
171    #[serde(serialize_with = "serde_path::serialize")]
172    pub path: PathBuf,
173    /// 1-based line number.
174    pub line: u32,
175    /// 0-based byte column offset.
176    pub col: u32,
177}
178
179/// An export that appears multiple times across the project.
180#[derive(Debug, Clone, Serialize)]
181pub struct DuplicateExport {
182    /// The duplicated export name.
183    pub export_name: String,
184    /// Locations where this export name appears.
185    pub locations: Vec<DuplicateLocation>,
186}
187
188/// A location where a duplicate export appears.
189#[derive(Debug, Clone, Serialize)]
190pub struct DuplicateLocation {
191    /// File containing the duplicate export.
192    #[serde(serialize_with = "serde_path::serialize")]
193    pub path: PathBuf,
194    /// 1-based line number.
195    pub line: u32,
196    /// 0-based byte column offset.
197    pub col: u32,
198}
199
200/// A production dependency that is only used via type-only imports.
201/// In production builds, type imports are erased, so this dependency
202/// is not needed at runtime and could be moved to devDependencies.
203#[derive(Debug, Clone, Serialize)]
204pub struct TypeOnlyDependency {
205    /// npm package name.
206    pub package_name: String,
207    /// Path to the package.json where the dependency is listed.
208    #[serde(serialize_with = "serde_path::serialize")]
209    pub path: PathBuf,
210    /// 1-based line number of the dependency entry in package.json.
211    pub line: u32,
212}
213
214/// A circular dependency chain detected in the module graph.
215#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct CircularDependency {
217    /// Files forming the cycle, in import order.
218    #[serde(serialize_with = "serde_path::serialize_vec")]
219    pub files: Vec<PathBuf>,
220    /// Number of files in the cycle.
221    pub length: usize,
222    /// 1-based line number of the import that starts the cycle (in the first file).
223    #[serde(default)]
224    pub line: u32,
225    /// 0-based byte column offset of the import that starts the cycle.
226    #[serde(default)]
227    pub col: u32,
228}
229
230/// Usage count for an export symbol. Used by the LSP Code Lens to show
231/// reference counts above each export declaration.
232#[derive(Debug, Clone, Serialize)]
233pub struct ExportUsage {
234    /// File containing the export.
235    #[serde(serialize_with = "serde_path::serialize")]
236    pub path: PathBuf,
237    /// Name of the exported symbol.
238    pub export_name: String,
239    /// 1-based line number.
240    pub line: u32,
241    /// 0-based byte column offset.
242    pub col: u32,
243    /// Number of files that reference this export.
244    pub reference_count: usize,
245    /// Locations where this export is referenced. Used by the LSP Code Lens
246    /// to enable click-to-navigate via `editor.action.showReferences`.
247    pub reference_locations: Vec<ReferenceLocation>,
248}
249
250/// A location where an export is referenced (import site in another file).
251#[derive(Debug, Clone, Serialize)]
252pub struct ReferenceLocation {
253    /// File containing the import that references the export.
254    #[serde(serialize_with = "serde_path::serialize")]
255    pub path: PathBuf,
256    /// 1-based line number.
257    pub line: u32,
258    /// 0-based byte column offset.
259    pub col: u32,
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265
266    #[test]
267    fn empty_results_no_issues() {
268        let results = AnalysisResults::default();
269        assert_eq!(results.total_issues(), 0);
270        assert!(!results.has_issues());
271    }
272
273    #[test]
274    fn results_with_unused_file() {
275        let mut results = AnalysisResults::default();
276        results.unused_files.push(UnusedFile {
277            path: PathBuf::from("test.ts"),
278        });
279        assert_eq!(results.total_issues(), 1);
280        assert!(results.has_issues());
281    }
282
283    #[test]
284    fn results_with_unused_export() {
285        let mut results = AnalysisResults::default();
286        results.unused_exports.push(UnusedExport {
287            path: PathBuf::from("test.ts"),
288            export_name: "foo".to_string(),
289            is_type_only: false,
290            line: 1,
291            col: 0,
292            span_start: 0,
293            is_re_export: false,
294        });
295        assert_eq!(results.total_issues(), 1);
296        assert!(results.has_issues());
297    }
298
299    #[test]
300    fn results_total_counts_all_types() {
301        let mut results = AnalysisResults::default();
302        results.unused_files.push(UnusedFile {
303            path: PathBuf::from("a.ts"),
304        });
305        results.unused_exports.push(UnusedExport {
306            path: PathBuf::from("b.ts"),
307            export_name: "x".to_string(),
308            is_type_only: false,
309            line: 1,
310            col: 0,
311            span_start: 0,
312            is_re_export: false,
313        });
314        results.unused_types.push(UnusedExport {
315            path: PathBuf::from("c.ts"),
316            export_name: "T".to_string(),
317            is_type_only: true,
318            line: 1,
319            col: 0,
320            span_start: 0,
321            is_re_export: false,
322        });
323        results.unused_dependencies.push(UnusedDependency {
324            package_name: "dep".to_string(),
325            location: DependencyLocation::Dependencies,
326            path: PathBuf::from("package.json"),
327            line: 5,
328        });
329        results.unused_dev_dependencies.push(UnusedDependency {
330            package_name: "dev".to_string(),
331            location: DependencyLocation::DevDependencies,
332            path: PathBuf::from("package.json"),
333            line: 5,
334        });
335        results.unused_enum_members.push(UnusedMember {
336            path: PathBuf::from("d.ts"),
337            parent_name: "E".to_string(),
338            member_name: "A".to_string(),
339            kind: MemberKind::EnumMember,
340            line: 1,
341            col: 0,
342        });
343        results.unused_class_members.push(UnusedMember {
344            path: PathBuf::from("e.ts"),
345            parent_name: "C".to_string(),
346            member_name: "m".to_string(),
347            kind: MemberKind::ClassMethod,
348            line: 1,
349            col: 0,
350        });
351        results.unresolved_imports.push(UnresolvedImport {
352            path: PathBuf::from("f.ts"),
353            specifier: "./missing".to_string(),
354            line: 1,
355            col: 0,
356        });
357        results.unlisted_dependencies.push(UnlistedDependency {
358            package_name: "unlisted".to_string(),
359            imported_from: vec![ImportSite {
360                path: PathBuf::from("g.ts"),
361                line: 1,
362                col: 0,
363            }],
364        });
365        results.duplicate_exports.push(DuplicateExport {
366            export_name: "dup".to_string(),
367            locations: vec![
368                DuplicateLocation {
369                    path: PathBuf::from("h.ts"),
370                    line: 15,
371                    col: 0,
372                },
373                DuplicateLocation {
374                    path: PathBuf::from("i.ts"),
375                    line: 30,
376                    col: 0,
377                },
378            ],
379        });
380        results.circular_dependencies.push(CircularDependency {
381            files: vec![PathBuf::from("a.ts"), PathBuf::from("b.ts")],
382            length: 2,
383            line: 3,
384            col: 0,
385        });
386
387        assert_eq!(results.total_issues(), 11);
388        assert!(results.has_issues());
389    }
390}