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)]
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::Json | OutputFormat::Sarif | OutputFormat::Markdown => {
129 json::print_health_json(report, &config.root, elapsed)
130 }
131 }
132}
133
134pub fn print_cross_reference_findings(
138 cross_ref: &fallow_core::cross_reference::CrossReferenceResult,
139 root: &Path,
140 quiet: bool,
141 output: &OutputFormat,
142) {
143 human::print_cross_reference_findings(cross_ref, root, quiet, output);
144}
145
146pub fn print_export_trace(trace: &ExportTrace, format: &OutputFormat) {
150 match format {
151 OutputFormat::Json => json::print_trace_json(trace),
152 _ => human::print_export_trace_human(trace),
153 }
154}
155
156pub fn print_file_trace(trace: &FileTrace, format: &OutputFormat) {
158 match format {
159 OutputFormat::Json => json::print_trace_json(trace),
160 _ => human::print_file_trace_human(trace),
161 }
162}
163
164pub fn print_dependency_trace(trace: &DependencyTrace, format: &OutputFormat) {
166 match format {
167 OutputFormat::Json => json::print_trace_json(trace),
168 _ => human::print_dependency_trace_human(trace),
169 }
170}
171
172pub fn print_clone_trace(trace: &CloneTrace, root: &Path, format: &OutputFormat) {
174 match format {
175 OutputFormat::Json => json::print_trace_json(trace),
176 _ => human::print_clone_trace_human(trace, root),
177 }
178}
179
180pub fn print_performance(timings: &PipelineTimings, format: &OutputFormat) {
183 match format {
184 OutputFormat::Json => match serde_json::to_string_pretty(timings) {
185 Ok(json) => eprintln!("{json}"),
186 Err(e) => eprintln!("Error: failed to serialize timings: {e}"),
187 },
188 _ => human::print_performance_human(timings),
189 }
190}
191
192#[allow(unused_imports)]
194pub use compact::build_compact_lines;
195#[allow(unused_imports)]
196pub use json::build_json;
197#[allow(unused_imports)]
198pub use markdown::build_duplication_markdown;
199#[allow(unused_imports)]
200pub use markdown::build_markdown;
201#[allow(unused_imports)]
202pub use sarif::build_sarif;
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207 use std::path::PathBuf;
208
209 #[test]
212 fn normalize_uri_forward_slashes_unchanged() {
213 assert_eq!(normalize_uri("src/utils.ts"), "src/utils.ts");
214 }
215
216 #[test]
217 fn normalize_uri_backslashes_replaced() {
218 assert_eq!(normalize_uri("src\\utils\\index.ts"), "src/utils/index.ts");
219 }
220
221 #[test]
222 fn normalize_uri_mixed_slashes() {
223 assert_eq!(normalize_uri("src\\utils/index.ts"), "src/utils/index.ts");
224 }
225
226 #[test]
227 fn normalize_uri_path_with_spaces() {
228 assert_eq!(
229 normalize_uri("src\\my folder\\file.ts"),
230 "src/my folder/file.ts"
231 );
232 }
233
234 #[test]
235 fn normalize_uri_empty_string() {
236 assert_eq!(normalize_uri(""), "");
237 }
238
239 #[test]
242 fn relative_path_strips_root_prefix() {
243 let root = Path::new("/project");
244 let path = Path::new("/project/src/utils.ts");
245 assert_eq!(relative_path(path, root), Path::new("src/utils.ts"));
246 }
247
248 #[test]
249 fn relative_path_returns_full_path_when_no_prefix() {
250 let root = Path::new("/other");
251 let path = Path::new("/project/src/utils.ts");
252 assert_eq!(relative_path(path, root), path);
253 }
254
255 #[test]
256 fn relative_path_at_root_returns_empty_or_file() {
257 let root = Path::new("/project");
258 let path = Path::new("/project/file.ts");
259 assert_eq!(relative_path(path, root), Path::new("file.ts"));
260 }
261
262 #[test]
263 fn relative_path_deeply_nested() {
264 let root = Path::new("/project");
265 let path = Path::new("/project/packages/ui/src/components/Button.tsx");
266 assert_eq!(
267 relative_path(path, root),
268 Path::new("packages/ui/src/components/Button.tsx")
269 );
270 }
271
272 #[test]
275 fn relative_uri_produces_forward_slash_path() {
276 let root = PathBuf::from("/project");
277 let path = root.join("src").join("utils.ts");
278 let uri = relative_uri(&path, &root);
279 assert_eq!(uri, "src/utils.ts");
280 }
281
282 #[test]
283 fn relative_uri_encodes_brackets() {
284 let root = PathBuf::from("/project");
285 let path = root.join("src/app/[...slug]/page.tsx");
286 let uri = relative_uri(&path, &root);
287 assert_eq!(uri, "src/app/%5B...slug%5D/page.tsx");
288 }
289
290 #[test]
291 fn relative_uri_encodes_nested_dynamic_routes() {
292 let root = PathBuf::from("/project");
293 let path = root.join("src/app/[slug]/[id]/page.tsx");
294 let uri = relative_uri(&path, &root);
295 assert_eq!(uri, "src/app/%5Bslug%5D/%5Bid%5D/page.tsx");
296 }
297
298 #[test]
299 fn relative_uri_no_common_prefix_returns_full() {
300 let root = PathBuf::from("/other");
301 let path = PathBuf::from("/project/src/utils.ts");
302 let uri = relative_uri(&path, &root);
303 assert!(uri.contains("project"));
304 assert!(uri.contains("utils.ts"));
305 }
306
307 #[test]
310 fn severity_error_maps_to_level_error() {
311 assert!(matches!(severity_to_level(Severity::Error), Level::Error));
312 }
313
314 #[test]
315 fn severity_warn_maps_to_level_warn() {
316 assert!(matches!(severity_to_level(Severity::Warn), Level::Warn));
317 }
318
319 #[test]
320 fn severity_off_maps_to_level_info() {
321 assert!(matches!(severity_to_level(Severity::Off), Level::Info));
322 }
323}