1use std::path::Path;
2
3use fallow_core::duplicates::DuplicationReport;
4use fallow_core::results::{AnalysisResults, UnusedExport, UnusedMember};
5
6use super::{normalize_uri, relative_path};
7
8pub(super) fn print_compact(results: &AnalysisResults, root: &Path) {
9 for line in build_compact_lines(results, root) {
10 println!("{line}");
11 }
12}
13
14pub fn build_compact_lines(results: &AnalysisResults, root: &Path) -> Vec<String> {
17 let rel = |p: &Path| normalize_uri(&relative_path(p, root).display().to_string());
18
19 let compact_export = |export: &UnusedExport, kind: &str, re_kind: &str| -> String {
20 let tag = if export.is_re_export { re_kind } else { kind };
21 format!(
22 "{}:{}:{}:{}",
23 tag,
24 rel(&export.path),
25 export.line,
26 export.export_name
27 )
28 };
29
30 let compact_member = |member: &UnusedMember, kind: &str| -> String {
31 format!(
32 "{}:{}:{}:{}.{}",
33 kind,
34 rel(&member.path),
35 member.line,
36 member.parent_name,
37 member.member_name
38 )
39 };
40
41 let mut lines = Vec::new();
42
43 for file in &results.unused_files {
44 lines.push(format!("unused-file:{}", rel(&file.path)));
45 }
46 for export in &results.unused_exports {
47 lines.push(compact_export(export, "unused-export", "unused-re-export"));
48 }
49 for export in &results.unused_types {
50 lines.push(compact_export(
51 export,
52 "unused-type",
53 "unused-re-export-type",
54 ));
55 }
56 for dep in &results.unused_dependencies {
57 lines.push(format!("unused-dep:{}", dep.package_name));
58 }
59 for dep in &results.unused_dev_dependencies {
60 lines.push(format!("unused-devdep:{}", dep.package_name));
61 }
62 for member in &results.unused_enum_members {
63 lines.push(compact_member(member, "unused-enum-member"));
64 }
65 for member in &results.unused_class_members {
66 lines.push(compact_member(member, "unused-class-member"));
67 }
68 for import in &results.unresolved_imports {
69 lines.push(format!(
70 "unresolved-import:{}:{}:{}",
71 rel(&import.path),
72 import.line,
73 import.specifier
74 ));
75 }
76 for dep in &results.unlisted_dependencies {
77 lines.push(format!("unlisted-dep:{}", dep.package_name));
78 }
79 for dup in &results.duplicate_exports {
80 lines.push(format!("duplicate-export:{}", dup.export_name));
81 }
82 for cycle in &results.circular_dependencies {
83 let chain: Vec<String> = cycle.files.iter().map(|p| rel(p)).collect();
84 let mut display_chain = chain.clone();
85 if let Some(first) = chain.first() {
86 display_chain.push(first.clone());
87 }
88 let first_file = chain.first().map_or_else(String::new, Clone::clone);
89 lines.push(format!(
90 "circular-dependency:{}:0:{}",
91 first_file,
92 display_chain.join(" \u{2192} ")
93 ));
94 }
95
96 lines
97}
98
99pub(super) fn print_duplication_compact(report: &DuplicationReport, root: &Path) {
100 for (i, group) in report.clone_groups.iter().enumerate() {
101 for instance in &group.instances {
102 let relative =
103 normalize_uri(&relative_path(&instance.file, root).display().to_string());
104 println!(
105 "clone-group-{}:{}:{}-{}:{}tokens",
106 i + 1,
107 relative,
108 instance.start_line,
109 instance.end_line,
110 group.token_count
111 );
112 }
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use fallow_core::extract::MemberKind;
120 use fallow_core::results::*;
121 use std::path::PathBuf;
122
123 fn sample_results(root: &Path) -> AnalysisResults {
125 let mut r = AnalysisResults::default();
126
127 r.unused_files.push(UnusedFile {
128 path: root.join("src/dead.ts"),
129 });
130 r.unused_exports.push(UnusedExport {
131 path: root.join("src/utils.ts"),
132 export_name: "helperFn".to_string(),
133 is_type_only: false,
134 line: 10,
135 col: 4,
136 span_start: 120,
137 is_re_export: false,
138 });
139 r.unused_types.push(UnusedExport {
140 path: root.join("src/types.ts"),
141 export_name: "OldType".to_string(),
142 is_type_only: true,
143 line: 5,
144 col: 0,
145 span_start: 60,
146 is_re_export: false,
147 });
148 r.unused_dependencies.push(UnusedDependency {
149 package_name: "lodash".to_string(),
150 location: DependencyLocation::Dependencies,
151 path: root.join("package.json"),
152 });
153 r.unused_dev_dependencies.push(UnusedDependency {
154 package_name: "jest".to_string(),
155 location: DependencyLocation::DevDependencies,
156 path: root.join("package.json"),
157 });
158 r.unused_enum_members.push(UnusedMember {
159 path: root.join("src/enums.ts"),
160 parent_name: "Status".to_string(),
161 member_name: "Deprecated".to_string(),
162 kind: MemberKind::EnumMember,
163 line: 8,
164 col: 2,
165 });
166 r.unused_class_members.push(UnusedMember {
167 path: root.join("src/service.ts"),
168 parent_name: "UserService".to_string(),
169 member_name: "legacyMethod".to_string(),
170 kind: MemberKind::ClassMethod,
171 line: 42,
172 col: 4,
173 });
174 r.unresolved_imports.push(UnresolvedImport {
175 path: root.join("src/app.ts"),
176 specifier: "./missing-module".to_string(),
177 line: 3,
178 col: 0,
179 });
180 r.unlisted_dependencies.push(UnlistedDependency {
181 package_name: "chalk".to_string(),
182 imported_from: vec![root.join("src/cli.ts")],
183 });
184 r.duplicate_exports.push(DuplicateExport {
185 export_name: "Config".to_string(),
186 locations: vec![root.join("src/config.ts"), root.join("src/types.ts")],
187 });
188
189 r
190 }
191
192 #[test]
193 fn compact_empty_results_no_lines() {
194 let root = PathBuf::from("/project");
195 let results = AnalysisResults::default();
196 let lines = build_compact_lines(&results, &root);
197 assert!(lines.is_empty());
198 }
199
200 #[test]
201 fn compact_unused_file_format() {
202 let root = PathBuf::from("/project");
203 let mut results = AnalysisResults::default();
204 results.unused_files.push(UnusedFile {
205 path: root.join("src/dead.ts"),
206 });
207
208 let lines = build_compact_lines(&results, &root);
209 assert_eq!(lines.len(), 1);
210 assert_eq!(lines[0], "unused-file:src/dead.ts");
211 }
212
213 #[test]
214 fn compact_unused_export_format() {
215 let root = PathBuf::from("/project");
216 let mut results = AnalysisResults::default();
217 results.unused_exports.push(UnusedExport {
218 path: root.join("src/utils.ts"),
219 export_name: "helperFn".to_string(),
220 is_type_only: false,
221 line: 10,
222 col: 4,
223 span_start: 120,
224 is_re_export: false,
225 });
226
227 let lines = build_compact_lines(&results, &root);
228 assert_eq!(lines[0], "unused-export:src/utils.ts:10:helperFn");
229 }
230
231 #[test]
232 fn compact_unused_type_format() {
233 let root = PathBuf::from("/project");
234 let mut results = AnalysisResults::default();
235 results.unused_types.push(UnusedExport {
236 path: root.join("src/types.ts"),
237 export_name: "OldType".to_string(),
238 is_type_only: true,
239 line: 5,
240 col: 0,
241 span_start: 60,
242 is_re_export: false,
243 });
244
245 let lines = build_compact_lines(&results, &root);
246 assert_eq!(lines[0], "unused-type:src/types.ts:5:OldType");
247 }
248
249 #[test]
250 fn compact_unused_dep_format() {
251 let root = PathBuf::from("/project");
252 let mut results = AnalysisResults::default();
253 results.unused_dependencies.push(UnusedDependency {
254 package_name: "lodash".to_string(),
255 location: DependencyLocation::Dependencies,
256 path: root.join("package.json"),
257 });
258
259 let lines = build_compact_lines(&results, &root);
260 assert_eq!(lines[0], "unused-dep:lodash");
261 }
262
263 #[test]
264 fn compact_unused_devdep_format() {
265 let root = PathBuf::from("/project");
266 let mut results = AnalysisResults::default();
267 results.unused_dev_dependencies.push(UnusedDependency {
268 package_name: "jest".to_string(),
269 location: DependencyLocation::DevDependencies,
270 path: root.join("package.json"),
271 });
272
273 let lines = build_compact_lines(&results, &root);
274 assert_eq!(lines[0], "unused-devdep:jest");
275 }
276
277 #[test]
278 fn compact_unused_enum_member_format() {
279 let root = PathBuf::from("/project");
280 let mut results = AnalysisResults::default();
281 results.unused_enum_members.push(UnusedMember {
282 path: root.join("src/enums.ts"),
283 parent_name: "Status".to_string(),
284 member_name: "Deprecated".to_string(),
285 kind: MemberKind::EnumMember,
286 line: 8,
287 col: 2,
288 });
289
290 let lines = build_compact_lines(&results, &root);
291 assert_eq!(
292 lines[0],
293 "unused-enum-member:src/enums.ts:8:Status.Deprecated"
294 );
295 }
296
297 #[test]
298 fn compact_unused_class_member_format() {
299 let root = PathBuf::from("/project");
300 let mut results = AnalysisResults::default();
301 results.unused_class_members.push(UnusedMember {
302 path: root.join("src/service.ts"),
303 parent_name: "UserService".to_string(),
304 member_name: "legacyMethod".to_string(),
305 kind: MemberKind::ClassMethod,
306 line: 42,
307 col: 4,
308 });
309
310 let lines = build_compact_lines(&results, &root);
311 assert_eq!(
312 lines[0],
313 "unused-class-member:src/service.ts:42:UserService.legacyMethod"
314 );
315 }
316
317 #[test]
318 fn compact_unresolved_import_format() {
319 let root = PathBuf::from("/project");
320 let mut results = AnalysisResults::default();
321 results.unresolved_imports.push(UnresolvedImport {
322 path: root.join("src/app.ts"),
323 specifier: "./missing-module".to_string(),
324 line: 3,
325 col: 0,
326 });
327
328 let lines = build_compact_lines(&results, &root);
329 assert_eq!(lines[0], "unresolved-import:src/app.ts:3:./missing-module");
330 }
331
332 #[test]
333 fn compact_unlisted_dep_format() {
334 let root = PathBuf::from("/project");
335 let mut results = AnalysisResults::default();
336 results.unlisted_dependencies.push(UnlistedDependency {
337 package_name: "chalk".to_string(),
338 imported_from: vec![],
339 });
340
341 let lines = build_compact_lines(&results, &root);
342 assert_eq!(lines[0], "unlisted-dep:chalk");
343 }
344
345 #[test]
346 fn compact_duplicate_export_format() {
347 let root = PathBuf::from("/project");
348 let mut results = AnalysisResults::default();
349 results.duplicate_exports.push(DuplicateExport {
350 export_name: "Config".to_string(),
351 locations: vec![root.join("src/a.ts"), root.join("src/b.ts")],
352 });
353
354 let lines = build_compact_lines(&results, &root);
355 assert_eq!(lines[0], "duplicate-export:Config");
356 }
357
358 #[test]
359 fn compact_all_issue_types_produce_lines() {
360 let root = PathBuf::from("/project");
361 let results = sample_results(&root);
362 let lines = build_compact_lines(&results, &root);
363
364 assert_eq!(lines.len(), 10);
366
367 assert!(lines[0].starts_with("unused-file:"));
369 assert!(lines[1].starts_with("unused-export:"));
370 assert!(lines[2].starts_with("unused-type:"));
371 assert!(lines[3].starts_with("unused-dep:"));
372 assert!(lines[4].starts_with("unused-devdep:"));
373 assert!(lines[5].starts_with("unused-enum-member:"));
374 assert!(lines[6].starts_with("unused-class-member:"));
375 assert!(lines[7].starts_with("unresolved-import:"));
376 assert!(lines[8].starts_with("unlisted-dep:"));
377 assert!(lines[9].starts_with("duplicate-export:"));
378 }
379
380 #[test]
381 fn compact_strips_root_prefix_from_paths() {
382 let root = PathBuf::from("/project");
383 let mut results = AnalysisResults::default();
384 results.unused_files.push(UnusedFile {
385 path: PathBuf::from("/project/src/deep/nested/file.ts"),
386 });
387
388 let lines = build_compact_lines(&results, &root);
389 assert_eq!(lines[0], "unused-file:src/deep/nested/file.ts");
390 }
391}