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}
21
22impl AnalysisResults {
23    /// Total number of issues found.
24    pub fn total_issues(&self) -> usize {
25        self.unused_files.len()
26            + self.unused_exports.len()
27            + self.unused_types.len()
28            + self.unused_dependencies.len()
29            + self.unused_dev_dependencies.len()
30            + self.unused_enum_members.len()
31            + self.unused_class_members.len()
32            + self.unresolved_imports.len()
33            + self.unlisted_dependencies.len()
34            + self.duplicate_exports.len()
35    }
36
37    /// Whether any issues were found.
38    pub fn has_issues(&self) -> bool {
39        self.total_issues() > 0
40    }
41}
42
43/// A file that is not reachable from any entry point.
44#[derive(Debug, Serialize)]
45pub struct UnusedFile {
46    pub path: PathBuf,
47}
48
49/// An export that is never imported by other modules.
50#[derive(Debug, Serialize)]
51pub struct UnusedExport {
52    pub path: PathBuf,
53    pub export_name: String,
54    pub is_type_only: bool,
55    pub line: u32,
56    pub col: u32,
57    /// Byte offset into the source file (used by the fix command).
58    pub span_start: u32,
59}
60
61/// A dependency that is listed in package.json but never imported.
62#[derive(Debug, Serialize)]
63pub struct UnusedDependency {
64    pub package_name: String,
65    pub location: DependencyLocation,
66}
67
68/// Where in package.json a dependency is listed.
69#[derive(Debug, Serialize)]
70#[serde(rename_all = "camelCase")]
71pub enum DependencyLocation {
72    Dependencies,
73    DevDependencies,
74}
75
76/// An unused enum or class member.
77#[derive(Debug, Serialize)]
78pub struct UnusedMember {
79    pub path: PathBuf,
80    pub parent_name: String,
81    pub member_name: String,
82    pub kind: MemberKind,
83    pub line: u32,
84    pub col: u32,
85}
86
87/// An import that could not be resolved.
88#[derive(Debug, Serialize)]
89pub struct UnresolvedImport {
90    pub path: PathBuf,
91    pub specifier: String,
92    pub line: u32,
93    pub col: u32,
94}
95
96/// A dependency used in code but not listed in package.json.
97#[derive(Debug, Serialize)]
98pub struct UnlistedDependency {
99    pub package_name: String,
100    pub imported_from: Vec<PathBuf>,
101}
102
103/// An export that appears multiple times across the project.
104#[derive(Debug, Serialize)]
105pub struct DuplicateExport {
106    pub export_name: String,
107    pub locations: Vec<PathBuf>,
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn empty_results_no_issues() {
116        let results = AnalysisResults::default();
117        assert_eq!(results.total_issues(), 0);
118        assert!(!results.has_issues());
119    }
120
121    #[test]
122    fn results_with_unused_file() {
123        let mut results = AnalysisResults::default();
124        results.unused_files.push(UnusedFile {
125            path: PathBuf::from("test.ts"),
126        });
127        assert_eq!(results.total_issues(), 1);
128        assert!(results.has_issues());
129    }
130
131    #[test]
132    fn results_with_unused_export() {
133        let mut results = AnalysisResults::default();
134        results.unused_exports.push(UnusedExport {
135            path: PathBuf::from("test.ts"),
136            export_name: "foo".to_string(),
137            is_type_only: false,
138            line: 1,
139            col: 0,
140            span_start: 0,
141        });
142        assert_eq!(results.total_issues(), 1);
143        assert!(results.has_issues());
144    }
145
146    #[test]
147    fn results_total_counts_all_types() {
148        let mut results = AnalysisResults::default();
149        results.unused_files.push(UnusedFile {
150            path: PathBuf::from("a.ts"),
151        });
152        results.unused_exports.push(UnusedExport {
153            path: PathBuf::from("b.ts"),
154            export_name: "x".to_string(),
155            is_type_only: false,
156            line: 1,
157            col: 0,
158            span_start: 0,
159        });
160        results.unused_types.push(UnusedExport {
161            path: PathBuf::from("c.ts"),
162            export_name: "T".to_string(),
163            is_type_only: true,
164            line: 1,
165            col: 0,
166            span_start: 0,
167        });
168        results.unused_dependencies.push(UnusedDependency {
169            package_name: "dep".to_string(),
170            location: DependencyLocation::Dependencies,
171        });
172        results.unused_dev_dependencies.push(UnusedDependency {
173            package_name: "dev".to_string(),
174            location: DependencyLocation::DevDependencies,
175        });
176        results.unused_enum_members.push(UnusedMember {
177            path: PathBuf::from("d.ts"),
178            parent_name: "E".to_string(),
179            member_name: "A".to_string(),
180            kind: MemberKind::EnumMember,
181            line: 1,
182            col: 0,
183        });
184        results.unused_class_members.push(UnusedMember {
185            path: PathBuf::from("e.ts"),
186            parent_name: "C".to_string(),
187            member_name: "m".to_string(),
188            kind: MemberKind::ClassMethod,
189            line: 1,
190            col: 0,
191        });
192        results.unresolved_imports.push(UnresolvedImport {
193            path: PathBuf::from("f.ts"),
194            specifier: "./missing".to_string(),
195            line: 1,
196            col: 0,
197        });
198        results.unlisted_dependencies.push(UnlistedDependency {
199            package_name: "unlisted".to_string(),
200            imported_from: vec![PathBuf::from("g.ts")],
201        });
202        results.duplicate_exports.push(DuplicateExport {
203            export_name: "dup".to_string(),
204            locations: vec![PathBuf::from("h.ts"), PathBuf::from("i.ts")],
205        });
206
207        assert_eq!(results.total_issues(), 10);
208        assert!(results.has_issues());
209    }
210}