1mod compact;
2mod human;
3mod json;
4mod sarif;
5
6use std::path::Path;
7use std::process::ExitCode;
8use std::time::Duration;
9
10use fallow_config::{OutputFormat, ResolvedConfig, Severity};
11use fallow_core::duplicates::DuplicationReport;
12use fallow_core::results::AnalysisResults;
13use fallow_core::trace::{CloneTrace, DependencyTrace, ExportTrace, FileTrace, PipelineTimings};
14
15fn relative_path<'a>(path: &'a Path, root: &Path) -> &'a Path {
17 path.strip_prefix(root).unwrap_or(path)
18}
19
20fn relative_uri(path: &Path, root: &Path) -> String {
22 normalize_uri(&relative_path(path, root).display().to_string())
23}
24
25pub fn normalize_uri(path_str: &str) -> String {
27 path_str.replace('\\', "/")
28}
29
30#[derive(Clone, Copy)]
32enum Level {
33 Warn,
34 Info,
35 Error,
36}
37
38const fn severity_to_level(s: Severity) -> Level {
39 match s {
40 Severity::Error => Level::Error,
41 Severity::Warn => Level::Warn,
42 Severity::Off => Level::Info,
44 }
45}
46
47pub fn print_results(
50 results: &AnalysisResults,
51 config: &ResolvedConfig,
52 elapsed: Duration,
53 quiet: bool,
54) -> ExitCode {
55 match config.output {
56 OutputFormat::Human => {
57 human::print_human(results, &config.root, &config.rules, elapsed, quiet);
58 ExitCode::SUCCESS
59 }
60 OutputFormat::Json => json::print_json(results, elapsed),
61 OutputFormat::Compact => {
62 compact::print_compact(results, &config.root);
63 ExitCode::SUCCESS
64 }
65 OutputFormat::Sarif => sarif::print_sarif(results, &config.root, &config.rules),
66 }
67}
68
69pub fn print_duplication_report(
73 report: &DuplicationReport,
74 config: &ResolvedConfig,
75 elapsed: Duration,
76 quiet: bool,
77 output: &OutputFormat,
78) -> ExitCode {
79 match output {
80 OutputFormat::Human => {
81 human::print_duplication_human(report, &config.root, elapsed, quiet);
82 ExitCode::SUCCESS
83 }
84 OutputFormat::Json => json::print_duplication_json(report, elapsed),
85 OutputFormat::Compact => {
86 compact::print_duplication_compact(report, &config.root);
87 ExitCode::SUCCESS
88 }
89 OutputFormat::Sarif => sarif::print_duplication_sarif(report, &config.root),
90 }
91}
92
93pub fn print_cross_reference_findings(
97 cross_ref: &fallow_core::cross_reference::CrossReferenceResult,
98 root: &Path,
99 quiet: bool,
100 output: &OutputFormat,
101) {
102 human::print_cross_reference_findings(cross_ref, root, quiet, output);
103}
104
105pub fn print_export_trace(trace: &ExportTrace, format: &OutputFormat) {
109 match format {
110 OutputFormat::Json => json::print_trace_json(trace),
111 _ => human::print_export_trace_human(trace),
112 }
113}
114
115pub fn print_file_trace(trace: &FileTrace, format: &OutputFormat) {
117 match format {
118 OutputFormat::Json => json::print_trace_json(trace),
119 _ => human::print_file_trace_human(trace),
120 }
121}
122
123pub fn print_dependency_trace(trace: &DependencyTrace, format: &OutputFormat) {
125 match format {
126 OutputFormat::Json => json::print_trace_json(trace),
127 _ => human::print_dependency_trace_human(trace),
128 }
129}
130
131pub fn print_clone_trace(trace: &CloneTrace, root: &Path, format: &OutputFormat) {
133 match format {
134 OutputFormat::Json => json::print_trace_json(trace),
135 _ => human::print_clone_trace_human(trace, root),
136 }
137}
138
139pub fn print_performance(timings: &PipelineTimings, format: &OutputFormat) {
142 match format {
143 OutputFormat::Json => match serde_json::to_string_pretty(timings) {
144 Ok(json) => eprintln!("{json}"),
145 Err(e) => eprintln!("Error: failed to serialize timings: {e}"),
146 },
147 _ => human::print_performance_human(timings),
148 }
149}
150
151#[allow(unused_imports)]
153pub use compact::build_compact_lines;
154#[allow(unused_imports)]
155pub use json::build_json;
156#[allow(unused_imports)]
157pub use sarif::build_sarif;
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162 use std::path::PathBuf;
163
164 #[test]
167 fn normalize_uri_forward_slashes_unchanged() {
168 assert_eq!(normalize_uri("src/utils.ts"), "src/utils.ts");
169 }
170
171 #[test]
172 fn normalize_uri_backslashes_replaced() {
173 assert_eq!(normalize_uri("src\\utils\\index.ts"), "src/utils/index.ts");
174 }
175
176 #[test]
177 fn normalize_uri_mixed_slashes() {
178 assert_eq!(normalize_uri("src\\utils/index.ts"), "src/utils/index.ts");
179 }
180
181 #[test]
182 fn normalize_uri_path_with_spaces() {
183 assert_eq!(
184 normalize_uri("src\\my folder\\file.ts"),
185 "src/my folder/file.ts"
186 );
187 }
188
189 #[test]
190 fn normalize_uri_empty_string() {
191 assert_eq!(normalize_uri(""), "");
192 }
193
194 #[test]
197 fn relative_path_strips_root_prefix() {
198 let root = Path::new("/project");
199 let path = Path::new("/project/src/utils.ts");
200 assert_eq!(relative_path(path, root), Path::new("src/utils.ts"));
201 }
202
203 #[test]
204 fn relative_path_returns_full_path_when_no_prefix() {
205 let root = Path::new("/other");
206 let path = Path::new("/project/src/utils.ts");
207 assert_eq!(relative_path(path, root), path);
208 }
209
210 #[test]
211 fn relative_path_at_root_returns_empty_or_file() {
212 let root = Path::new("/project");
213 let path = Path::new("/project/file.ts");
214 assert_eq!(relative_path(path, root), Path::new("file.ts"));
215 }
216
217 #[test]
218 fn relative_path_deeply_nested() {
219 let root = Path::new("/project");
220 let path = Path::new("/project/packages/ui/src/components/Button.tsx");
221 assert_eq!(
222 relative_path(path, root),
223 Path::new("packages/ui/src/components/Button.tsx")
224 );
225 }
226
227 #[test]
230 fn relative_uri_produces_forward_slash_path() {
231 let root = PathBuf::from("/project");
232 let path = root.join("src").join("utils.ts");
233 let uri = relative_uri(&path, &root);
234 assert_eq!(uri, "src/utils.ts");
235 }
236
237 #[test]
238 fn relative_uri_no_common_prefix_returns_full() {
239 let root = PathBuf::from("/other");
240 let path = PathBuf::from("/project/src/utils.ts");
241 let uri = relative_uri(&path, &root);
242 assert!(uri.contains("project"));
243 assert!(uri.contains("utils.ts"));
244 }
245
246 #[test]
249 fn severity_error_maps_to_level_error() {
250 assert!(matches!(severity_to_level(Severity::Error), Level::Error));
251 }
252
253 #[test]
254 fn severity_warn_maps_to_level_warn() {
255 assert!(matches!(severity_to_level(Severity::Warn), Level::Warn));
256 }
257
258 #[test]
259 fn severity_off_maps_to_level_info() {
260 assert!(matches!(severity_to_level(Severity::Off), Level::Info));
261 }
262}