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 {
28 path_str.replace('\\', "/")
29}
30
31#[derive(Clone, Copy)]
33enum Level {
34 Warn,
35 Info,
36 Error,
37}
38
39const fn severity_to_level(s: Severity) -> Level {
40 match s {
41 Severity::Error => Level::Error,
42 Severity::Warn => Level::Warn,
43 Severity::Off => Level::Info,
45 }
46}
47
48pub fn print_results(
51 results: &AnalysisResults,
52 config: &ResolvedConfig,
53 elapsed: Duration,
54 quiet: bool,
55) -> ExitCode {
56 match config.output {
57 OutputFormat::Human => {
58 human::print_human(results, &config.root, &config.rules, elapsed, quiet);
59 ExitCode::SUCCESS
60 }
61 OutputFormat::Json => json::print_json(results, elapsed),
62 OutputFormat::Compact => {
63 compact::print_compact(results, &config.root);
64 ExitCode::SUCCESS
65 }
66 OutputFormat::Sarif => sarif::print_sarif(results, &config.root, &config.rules),
67 OutputFormat::Markdown => {
68 markdown::print_markdown(results, &config.root);
69 ExitCode::SUCCESS
70 }
71 }
72}
73
74pub fn print_duplication_report(
78 report: &DuplicationReport,
79 config: &ResolvedConfig,
80 elapsed: Duration,
81 quiet: bool,
82 output: &OutputFormat,
83) -> ExitCode {
84 match output {
85 OutputFormat::Human => {
86 human::print_duplication_human(report, &config.root, elapsed, quiet);
87 ExitCode::SUCCESS
88 }
89 OutputFormat::Json => json::print_duplication_json(report, elapsed),
90 OutputFormat::Compact => {
91 compact::print_duplication_compact(report, &config.root);
92 ExitCode::SUCCESS
93 }
94 OutputFormat::Sarif => sarif::print_duplication_sarif(report, &config.root),
95 OutputFormat::Markdown => {
96 markdown::print_duplication_markdown(report, &config.root);
97 ExitCode::SUCCESS
98 }
99 }
100}
101
102pub fn print_cross_reference_findings(
106 cross_ref: &fallow_core::cross_reference::CrossReferenceResult,
107 root: &Path,
108 quiet: bool,
109 output: &OutputFormat,
110) {
111 human::print_cross_reference_findings(cross_ref, root, quiet, output);
112}
113
114pub fn print_export_trace(trace: &ExportTrace, format: &OutputFormat) {
118 match format {
119 OutputFormat::Json => json::print_trace_json(trace),
120 _ => human::print_export_trace_human(trace),
121 }
122}
123
124pub fn print_file_trace(trace: &FileTrace, format: &OutputFormat) {
126 match format {
127 OutputFormat::Json => json::print_trace_json(trace),
128 _ => human::print_file_trace_human(trace),
129 }
130}
131
132pub fn print_dependency_trace(trace: &DependencyTrace, format: &OutputFormat) {
134 match format {
135 OutputFormat::Json => json::print_trace_json(trace),
136 _ => human::print_dependency_trace_human(trace),
137 }
138}
139
140pub fn print_clone_trace(trace: &CloneTrace, root: &Path, format: &OutputFormat) {
142 match format {
143 OutputFormat::Json => json::print_trace_json(trace),
144 _ => human::print_clone_trace_human(trace, root),
145 }
146}
147
148pub fn print_performance(timings: &PipelineTimings, format: &OutputFormat) {
151 match format {
152 OutputFormat::Json => match serde_json::to_string_pretty(timings) {
153 Ok(json) => eprintln!("{json}"),
154 Err(e) => eprintln!("Error: failed to serialize timings: {e}"),
155 },
156 _ => human::print_performance_human(timings),
157 }
158}
159
160#[allow(unused_imports)]
162pub use compact::build_compact_lines;
163#[allow(unused_imports)]
164pub use json::build_json;
165#[allow(unused_imports)]
166pub use markdown::build_duplication_markdown;
167#[allow(unused_imports)]
168pub use markdown::build_markdown;
169#[allow(unused_imports)]
170pub use sarif::build_sarif;
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175 use std::path::PathBuf;
176
177 #[test]
180 fn normalize_uri_forward_slashes_unchanged() {
181 assert_eq!(normalize_uri("src/utils.ts"), "src/utils.ts");
182 }
183
184 #[test]
185 fn normalize_uri_backslashes_replaced() {
186 assert_eq!(normalize_uri("src\\utils\\index.ts"), "src/utils/index.ts");
187 }
188
189 #[test]
190 fn normalize_uri_mixed_slashes() {
191 assert_eq!(normalize_uri("src\\utils/index.ts"), "src/utils/index.ts");
192 }
193
194 #[test]
195 fn normalize_uri_path_with_spaces() {
196 assert_eq!(
197 normalize_uri("src\\my folder\\file.ts"),
198 "src/my folder/file.ts"
199 );
200 }
201
202 #[test]
203 fn normalize_uri_empty_string() {
204 assert_eq!(normalize_uri(""), "");
205 }
206
207 #[test]
210 fn relative_path_strips_root_prefix() {
211 let root = Path::new("/project");
212 let path = Path::new("/project/src/utils.ts");
213 assert_eq!(relative_path(path, root), Path::new("src/utils.ts"));
214 }
215
216 #[test]
217 fn relative_path_returns_full_path_when_no_prefix() {
218 let root = Path::new("/other");
219 let path = Path::new("/project/src/utils.ts");
220 assert_eq!(relative_path(path, root), path);
221 }
222
223 #[test]
224 fn relative_path_at_root_returns_empty_or_file() {
225 let root = Path::new("/project");
226 let path = Path::new("/project/file.ts");
227 assert_eq!(relative_path(path, root), Path::new("file.ts"));
228 }
229
230 #[test]
231 fn relative_path_deeply_nested() {
232 let root = Path::new("/project");
233 let path = Path::new("/project/packages/ui/src/components/Button.tsx");
234 assert_eq!(
235 relative_path(path, root),
236 Path::new("packages/ui/src/components/Button.tsx")
237 );
238 }
239
240 #[test]
243 fn relative_uri_produces_forward_slash_path() {
244 let root = PathBuf::from("/project");
245 let path = root.join("src").join("utils.ts");
246 let uri = relative_uri(&path, &root);
247 assert_eq!(uri, "src/utils.ts");
248 }
249
250 #[test]
251 fn relative_uri_no_common_prefix_returns_full() {
252 let root = PathBuf::from("/other");
253 let path = PathBuf::from("/project/src/utils.ts");
254 let uri = relative_uri(&path, &root);
255 assert!(uri.contains("project"));
256 assert!(uri.contains("utils.ts"));
257 }
258
259 #[test]
262 fn severity_error_maps_to_level_error() {
263 assert!(matches!(severity_to_level(Severity::Error), Level::Error));
264 }
265
266 #[test]
267 fn severity_warn_maps_to_level_warn() {
268 assert!(matches!(severity_to_level(Severity::Warn), Level::Warn));
269 }
270
271 #[test]
272 fn severity_off_maps_to_level_info() {
273 assert!(matches!(severity_to_level(Severity::Off), Level::Info));
274 }
275}