1mod compact;
2mod human;
3mod json;
4mod markdown;
5mod sarif;
6
7use std::path::Path;
8use std::process::ExitCode;
9use std::time::Duration;
10
11use fallow_config::{OutputFormat, ResolvedConfig, Severity};
12use fallow_core::duplicates::DuplicationReport;
13use fallow_core::results::AnalysisResults;
14use fallow_core::trace::{CloneTrace, DependencyTrace, ExportTrace, FileTrace, PipelineTimings};
15
16fn relative_path<'a>(path: &'a Path, root: &Path) -> &'a Path {
18 path.strip_prefix(root).unwrap_or(path)
19}
20
21fn relative_uri(path: &Path, root: &Path) -> String {
23 normalize_uri(&relative_path(path, root).display().to_string())
24}
25
26pub fn normalize_uri(path_str: &str) -> String {
31 path_str
32 .replace('\\', "/")
33 .replace('[', "%5B")
34 .replace(']', "%5D")
35}
36
37#[derive(Clone, Copy, Debug)]
39enum Level {
40 Warn,
41 Info,
42 Error,
43}
44
45const fn severity_to_level(s: Severity) -> Level {
46 match s {
47 Severity::Error => Level::Error,
48 Severity::Warn => Level::Warn,
49 Severity::Off => Level::Info,
51 }
52}
53
54pub fn print_results(
57 results: &AnalysisResults,
58 config: &ResolvedConfig,
59 elapsed: Duration,
60 quiet: bool,
61) -> ExitCode {
62 match config.output {
63 OutputFormat::Human => {
64 human::print_human(results, &config.root, &config.rules, elapsed, quiet);
65 ExitCode::SUCCESS
66 }
67 OutputFormat::Json => json::print_json(results, &config.root, elapsed),
68 OutputFormat::Compact => {
69 compact::print_compact(results, &config.root);
70 ExitCode::SUCCESS
71 }
72 OutputFormat::Sarif => sarif::print_sarif(results, &config.root, &config.rules),
73 OutputFormat::Markdown => {
74 markdown::print_markdown(results, &config.root);
75 ExitCode::SUCCESS
76 }
77 }
78}
79
80pub fn print_duplication_report(
84 report: &DuplicationReport,
85 config: &ResolvedConfig,
86 elapsed: Duration,
87 quiet: bool,
88 output: &OutputFormat,
89) -> ExitCode {
90 match output {
91 OutputFormat::Human => {
92 human::print_duplication_human(report, &config.root, elapsed, quiet);
93 ExitCode::SUCCESS
94 }
95 OutputFormat::Json => json::print_duplication_json(report, elapsed),
96 OutputFormat::Compact => {
97 compact::print_duplication_compact(report, &config.root);
98 ExitCode::SUCCESS
99 }
100 OutputFormat::Sarif => sarif::print_duplication_sarif(report, &config.root),
101 OutputFormat::Markdown => {
102 markdown::print_duplication_markdown(report, &config.root);
103 ExitCode::SUCCESS
104 }
105 }
106}
107
108pub fn print_health_report(
112 report: &crate::health_types::HealthReport,
113 config: &ResolvedConfig,
114 elapsed: Duration,
115 quiet: bool,
116 output: &OutputFormat,
117) -> ExitCode {
118 match output {
119 OutputFormat::Human => {
120 human::print_health_human(report, &config.root, elapsed, quiet);
121 ExitCode::SUCCESS
122 }
123 OutputFormat::Compact => {
124 compact::print_health_compact(report, &config.root);
125 ExitCode::SUCCESS
126 }
127 OutputFormat::Markdown => {
128 markdown::print_health_markdown(report, &config.root);
129 ExitCode::SUCCESS
130 }
131 OutputFormat::Sarif => sarif::print_health_sarif(report, &config.root),
132 OutputFormat::Json => json::print_health_json(report, &config.root, elapsed),
133 }
134}
135
136pub fn print_cross_reference_findings(
140 cross_ref: &fallow_core::cross_reference::CrossReferenceResult,
141 root: &Path,
142 quiet: bool,
143 output: &OutputFormat,
144) {
145 human::print_cross_reference_findings(cross_ref, root, quiet, output);
146}
147
148pub fn print_export_trace(trace: &ExportTrace, format: &OutputFormat) {
152 match format {
153 OutputFormat::Json => json::print_trace_json(trace),
154 _ => human::print_export_trace_human(trace),
155 }
156}
157
158pub fn print_file_trace(trace: &FileTrace, format: &OutputFormat) {
160 match format {
161 OutputFormat::Json => json::print_trace_json(trace),
162 _ => human::print_file_trace_human(trace),
163 }
164}
165
166pub fn print_dependency_trace(trace: &DependencyTrace, format: &OutputFormat) {
168 match format {
169 OutputFormat::Json => json::print_trace_json(trace),
170 _ => human::print_dependency_trace_human(trace),
171 }
172}
173
174pub fn print_clone_trace(trace: &CloneTrace, root: &Path, format: &OutputFormat) {
176 match format {
177 OutputFormat::Json => json::print_trace_json(trace),
178 _ => human::print_clone_trace_human(trace, root),
179 }
180}
181
182pub fn print_performance(timings: &PipelineTimings, format: &OutputFormat) {
185 match format {
186 OutputFormat::Json => match serde_json::to_string_pretty(timings) {
187 Ok(json) => eprintln!("{json}"),
188 Err(e) => eprintln!("Error: failed to serialize timings: {e}"),
189 },
190 _ => human::print_performance_human(timings),
191 }
192}
193
194#[allow(unused_imports)]
196pub use compact::build_compact_lines;
197#[allow(unused_imports)]
198pub use json::build_json;
199#[allow(unused_imports)]
200pub use markdown::build_duplication_markdown;
201#[allow(unused_imports)]
202pub use markdown::build_health_markdown;
203#[allow(unused_imports)]
204pub use markdown::build_markdown;
205#[allow(unused_imports)]
206pub use sarif::build_health_sarif;
207#[allow(unused_imports)]
208pub use sarif::build_sarif;
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 use std::path::PathBuf;
214
215 #[test]
218 fn normalize_uri_forward_slashes_unchanged() {
219 assert_eq!(normalize_uri("src/utils.ts"), "src/utils.ts");
220 }
221
222 #[test]
223 fn normalize_uri_backslashes_replaced() {
224 assert_eq!(normalize_uri("src\\utils\\index.ts"), "src/utils/index.ts");
225 }
226
227 #[test]
228 fn normalize_uri_mixed_slashes() {
229 assert_eq!(normalize_uri("src\\utils/index.ts"), "src/utils/index.ts");
230 }
231
232 #[test]
233 fn normalize_uri_path_with_spaces() {
234 assert_eq!(
235 normalize_uri("src\\my folder\\file.ts"),
236 "src/my folder/file.ts"
237 );
238 }
239
240 #[test]
241 fn normalize_uri_empty_string() {
242 assert_eq!(normalize_uri(""), "");
243 }
244
245 #[test]
248 fn relative_path_strips_root_prefix() {
249 let root = Path::new("/project");
250 let path = Path::new("/project/src/utils.ts");
251 assert_eq!(relative_path(path, root), Path::new("src/utils.ts"));
252 }
253
254 #[test]
255 fn relative_path_returns_full_path_when_no_prefix() {
256 let root = Path::new("/other");
257 let path = Path::new("/project/src/utils.ts");
258 assert_eq!(relative_path(path, root), path);
259 }
260
261 #[test]
262 fn relative_path_at_root_returns_empty_or_file() {
263 let root = Path::new("/project");
264 let path = Path::new("/project/file.ts");
265 assert_eq!(relative_path(path, root), Path::new("file.ts"));
266 }
267
268 #[test]
269 fn relative_path_deeply_nested() {
270 let root = Path::new("/project");
271 let path = Path::new("/project/packages/ui/src/components/Button.tsx");
272 assert_eq!(
273 relative_path(path, root),
274 Path::new("packages/ui/src/components/Button.tsx")
275 );
276 }
277
278 #[test]
281 fn relative_uri_produces_forward_slash_path() {
282 let root = PathBuf::from("/project");
283 let path = root.join("src").join("utils.ts");
284 let uri = relative_uri(&path, &root);
285 assert_eq!(uri, "src/utils.ts");
286 }
287
288 #[test]
289 fn relative_uri_encodes_brackets() {
290 let root = PathBuf::from("/project");
291 let path = root.join("src/app/[...slug]/page.tsx");
292 let uri = relative_uri(&path, &root);
293 assert_eq!(uri, "src/app/%5B...slug%5D/page.tsx");
294 }
295
296 #[test]
297 fn relative_uri_encodes_nested_dynamic_routes() {
298 let root = PathBuf::from("/project");
299 let path = root.join("src/app/[slug]/[id]/page.tsx");
300 let uri = relative_uri(&path, &root);
301 assert_eq!(uri, "src/app/%5Bslug%5D/%5Bid%5D/page.tsx");
302 }
303
304 #[test]
305 fn relative_uri_no_common_prefix_returns_full() {
306 let root = PathBuf::from("/other");
307 let path = PathBuf::from("/project/src/utils.ts");
308 let uri = relative_uri(&path, &root);
309 assert!(uri.contains("project"));
310 assert!(uri.contains("utils.ts"));
311 }
312
313 #[test]
316 fn severity_error_maps_to_level_error() {
317 assert!(matches!(severity_to_level(Severity::Error), Level::Error));
318 }
319
320 #[test]
321 fn severity_warn_maps_to_level_warn() {
322 assert!(matches!(severity_to_level(Severity::Warn), Level::Warn));
323 }
324
325 #[test]
326 fn severity_off_maps_to_level_info() {
327 assert!(matches!(severity_to_level(Severity::Off), Level::Info));
328 }
329
330 #[test]
333 fn normalize_uri_single_bracket_pair() {
334 assert_eq!(normalize_uri("app/[id]/page.tsx"), "app/%5Bid%5D/page.tsx");
335 }
336
337 #[test]
338 fn normalize_uri_catch_all_route() {
339 assert_eq!(
340 normalize_uri("app/[...slug]/page.tsx"),
341 "app/%5B...slug%5D/page.tsx"
342 );
343 }
344
345 #[test]
346 fn normalize_uri_optional_catch_all_route() {
347 assert_eq!(
348 normalize_uri("app/[[...slug]]/page.tsx"),
349 "app/%5B%5B...slug%5D%5D/page.tsx"
350 );
351 }
352
353 #[test]
354 fn normalize_uri_multiple_dynamic_segments() {
355 assert_eq!(
356 normalize_uri("app/[lang]/posts/[id]"),
357 "app/%5Blang%5D/posts/%5Bid%5D"
358 );
359 }
360
361 #[test]
362 fn normalize_uri_no_special_chars() {
363 let plain = "src/components/Button.tsx";
364 assert_eq!(normalize_uri(plain), plain);
365 }
366
367 #[test]
368 fn normalize_uri_only_backslashes() {
369 assert_eq!(normalize_uri("a\\b\\c"), "a/b/c");
370 }
371
372 #[test]
375 fn relative_path_identical_paths_returns_empty() {
376 let root = Path::new("/project");
377 assert_eq!(relative_path(root, root), Path::new(""));
378 }
379
380 #[test]
381 fn relative_path_partial_name_match_not_stripped() {
382 let root = Path::new("/project");
385 let path = Path::new("/project-two/src/a.ts");
386 assert_eq!(relative_path(path, root), path);
387 }
388
389 #[test]
392 fn relative_uri_combines_stripping_and_encoding() {
393 let root = PathBuf::from("/project");
394 let path = root.join("src/app/[slug]/page.tsx");
395 let uri = relative_uri(&path, &root);
396 assert_eq!(uri, "src/app/%5Bslug%5D/page.tsx");
398 assert!(!uri.starts_with('/'));
399 }
400
401 #[test]
402 fn relative_uri_at_root_file() {
403 let root = PathBuf::from("/project");
404 let path = root.join("index.ts");
405 assert_eq!(relative_uri(&path, &root), "index.ts");
406 }
407
408 #[test]
411 fn severity_to_level_is_const_evaluable() {
412 const LEVEL_FROM_ERROR: Level = severity_to_level(Severity::Error);
414 const LEVEL_FROM_WARN: Level = severity_to_level(Severity::Warn);
415 const LEVEL_FROM_OFF: Level = severity_to_level(Severity::Off);
416 assert!(matches!(LEVEL_FROM_ERROR, Level::Error));
417 assert!(matches!(LEVEL_FROM_WARN, Level::Warn));
418 assert!(matches!(LEVEL_FROM_OFF, Level::Info));
419 }
420
421 #[test]
424 fn level_is_copy() {
425 let level = severity_to_level(Severity::Error);
426 let copy = level;
427 assert!(matches!(level, Level::Error));
429 assert!(matches!(copy, Level::Error));
430 }
431}