Skip to main content

fallow_core/
results.rs

1use std::path::PathBuf;
2
3use serde::Serialize;
4
5use crate::extract::MemberKind;
6
7/// Complete analysis results.
8#[derive(Debug, Default, Serialize)]
9pub struct AnalysisResults {
10    pub unused_files: Vec<UnusedFile>,
11    pub unused_exports: Vec<UnusedExport>,
12    pub unused_types: Vec<UnusedExport>,
13    pub unused_dependencies: Vec<UnusedDependency>,
14    pub unused_dev_dependencies: Vec<UnusedDependency>,
15    pub unused_enum_members: Vec<UnusedMember>,
16    pub unused_class_members: Vec<UnusedMember>,
17    pub unresolved_imports: Vec<UnresolvedImport>,
18    pub unlisted_dependencies: Vec<UnlistedDependency>,
19    pub duplicate_exports: Vec<DuplicateExport>,
20    /// Production dependencies only used via type-only imports (could be devDependencies).
21    /// Only populated in production mode.
22    pub type_only_dependencies: Vec<TypeOnlyDependency>,
23}
24
25impl AnalysisResults {
26    /// Total number of issues found.
27    pub fn total_issues(&self) -> usize {
28        self.unused_files.len()
29            + self.unused_exports.len()
30            + self.unused_types.len()
31            + self.unused_dependencies.len()
32            + self.unused_dev_dependencies.len()
33            + self.unused_enum_members.len()
34            + self.unused_class_members.len()
35            + self.unresolved_imports.len()
36            + self.unlisted_dependencies.len()
37            + self.duplicate_exports.len()
38            + self.type_only_dependencies.len()
39    }
40
41    /// Whether any issues were found.
42    pub fn has_issues(&self) -> bool {
43        self.total_issues() > 0
44    }
45}
46
47/// A file that is not reachable from any entry point.
48#[derive(Debug, Serialize)]
49pub struct UnusedFile {
50    pub path: PathBuf,
51}
52
53/// An export that is never imported by other modules.
54#[derive(Debug, Serialize)]
55pub struct UnusedExport {
56    pub path: PathBuf,
57    pub export_name: String,
58    pub is_type_only: bool,
59    pub line: u32,
60    pub col: u32,
61    /// Byte offset into the source file (used by the fix command).
62    pub span_start: u32,
63    /// Whether this finding comes from a barrel/index re-export rather than the source definition.
64    pub is_re_export: bool,
65}
66
67/// A dependency that is listed in package.json but never imported.
68#[derive(Debug, Serialize)]
69pub struct UnusedDependency {
70    pub package_name: String,
71    pub location: DependencyLocation,
72    /// Path to the package.json where this dependency is listed.
73    /// For root deps this is `<root>/package.json`, for workspace deps it is `<ws>/package.json`.
74    pub path: PathBuf,
75}
76
77/// Where in package.json a dependency is listed.
78#[derive(Debug, Serialize)]
79#[serde(rename_all = "camelCase")]
80pub enum DependencyLocation {
81    Dependencies,
82    DevDependencies,
83}
84
85/// An unused enum or class member.
86#[derive(Debug, Serialize)]
87pub struct UnusedMember {
88    pub path: PathBuf,
89    pub parent_name: String,
90    pub member_name: String,
91    pub kind: MemberKind,
92    pub line: u32,
93    pub col: u32,
94}
95
96/// An import that could not be resolved.
97#[derive(Debug, Serialize)]
98pub struct UnresolvedImport {
99    pub path: PathBuf,
100    pub specifier: String,
101    pub line: u32,
102    pub col: u32,
103}
104
105/// A dependency used in code but not listed in package.json.
106#[derive(Debug, Serialize)]
107pub struct UnlistedDependency {
108    pub package_name: String,
109    pub imported_from: Vec<PathBuf>,
110}
111
112/// An export that appears multiple times across the project.
113#[derive(Debug, Serialize)]
114pub struct DuplicateExport {
115    pub export_name: String,
116    pub locations: Vec<PathBuf>,
117}
118
119/// A production dependency that is only used via type-only imports.
120/// In production builds, type imports are erased, so this dependency
121/// is not needed at runtime and could be moved to devDependencies.
122#[derive(Debug, Serialize)]
123pub struct TypeOnlyDependency {
124    pub package_name: String,
125    /// Path to the package.json where the dependency is listed.
126    pub path: PathBuf,
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn empty_results_no_issues() {
135        let results = AnalysisResults::default();
136        assert_eq!(results.total_issues(), 0);
137        assert!(!results.has_issues());
138    }
139
140    #[test]
141    fn results_with_unused_file() {
142        let mut results = AnalysisResults::default();
143        results.unused_files.push(UnusedFile {
144            path: PathBuf::from("test.ts"),
145        });
146        assert_eq!(results.total_issues(), 1);
147        assert!(results.has_issues());
148    }
149
150    #[test]
151    fn results_with_unused_export() {
152        let mut results = AnalysisResults::default();
153        results.unused_exports.push(UnusedExport {
154            path: PathBuf::from("test.ts"),
155            export_name: "foo".to_string(),
156            is_type_only: false,
157            line: 1,
158            col: 0,
159            span_start: 0,
160            is_re_export: false,
161        });
162        assert_eq!(results.total_issues(), 1);
163        assert!(results.has_issues());
164    }
165
166    #[test]
167    fn results_total_counts_all_types() {
168        let mut results = AnalysisResults::default();
169        results.unused_files.push(UnusedFile {
170            path: PathBuf::from("a.ts"),
171        });
172        results.unused_exports.push(UnusedExport {
173            path: PathBuf::from("b.ts"),
174            export_name: "x".to_string(),
175            is_type_only: false,
176            line: 1,
177            col: 0,
178            span_start: 0,
179            is_re_export: false,
180        });
181        results.unused_types.push(UnusedExport {
182            path: PathBuf::from("c.ts"),
183            export_name: "T".to_string(),
184            is_type_only: true,
185            line: 1,
186            col: 0,
187            span_start: 0,
188            is_re_export: false,
189        });
190        results.unused_dependencies.push(UnusedDependency {
191            package_name: "dep".to_string(),
192            location: DependencyLocation::Dependencies,
193            path: PathBuf::from("package.json"),
194        });
195        results.unused_dev_dependencies.push(UnusedDependency {
196            package_name: "dev".to_string(),
197            location: DependencyLocation::DevDependencies,
198            path: PathBuf::from("package.json"),
199        });
200        results.unused_enum_members.push(UnusedMember {
201            path: PathBuf::from("d.ts"),
202            parent_name: "E".to_string(),
203            member_name: "A".to_string(),
204            kind: MemberKind::EnumMember,
205            line: 1,
206            col: 0,
207        });
208        results.unused_class_members.push(UnusedMember {
209            path: PathBuf::from("e.ts"),
210            parent_name: "C".to_string(),
211            member_name: "m".to_string(),
212            kind: MemberKind::ClassMethod,
213            line: 1,
214            col: 0,
215        });
216        results.unresolved_imports.push(UnresolvedImport {
217            path: PathBuf::from("f.ts"),
218            specifier: "./missing".to_string(),
219            line: 1,
220            col: 0,
221        });
222        results.unlisted_dependencies.push(UnlistedDependency {
223            package_name: "unlisted".to_string(),
224            imported_from: vec![PathBuf::from("g.ts")],
225        });
226        results.duplicate_exports.push(DuplicateExport {
227            export_name: "dup".to_string(),
228            locations: vec![PathBuf::from("h.ts"), PathBuf::from("i.ts")],
229        });
230
231        assert_eq!(results.total_issues(), 10);
232        assert!(results.has_issues());
233    }
234}