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