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