1use crate::output::OutputFormatter;
4use crate::rule::LintWarning;
5use serde_json::json;
6
7pub struct GitLabFormatter;
10
11impl Default for GitLabFormatter {
12 fn default() -> Self {
13 Self
14 }
15}
16
17impl GitLabFormatter {
18 pub fn new() -> Self {
19 Self
20 }
21}
22
23impl OutputFormatter for GitLabFormatter {
24 fn format_warnings(&self, warnings: &[LintWarning], file_path: &str) -> String {
25 let issues: Vec<_> = warnings
27 .iter()
28 .map(|warning| {
29 let rule_name = warning.rule_name.as_deref().unwrap_or("unknown");
30 let fingerprint = format!("{}-{}-{}-{}", file_path, warning.line, warning.column, rule_name);
31
32 json!({
33 "description": warning.message,
34 "check_name": rule_name,
35 "fingerprint": fingerprint,
36 "severity": "minor",
37 "location": {
38 "path": file_path,
39 "lines": {
40 "begin": warning.line
41 }
42 }
43 })
44 })
45 .collect();
46
47 serde_json::to_string_pretty(&issues).unwrap_or_else(|_| "[]".to_string())
48 }
49}
50
51pub fn format_gitlab_report(all_warnings: &[(String, Vec<LintWarning>)]) -> String {
53 let mut issues = Vec::new();
54
55 for (file_path, warnings) in all_warnings {
56 for warning in warnings {
57 let rule_name = warning.rule_name.as_deref().unwrap_or("unknown");
58
59 let fingerprint = format!("{}-{}-{}-{}", file_path, warning.line, warning.column, rule_name);
61
62 let issue = json!({
63 "description": warning.message,
64 "check_name": rule_name,
65 "fingerprint": fingerprint,
66 "severity": "minor",
67 "location": {
68 "path": file_path,
69 "lines": {
70 "begin": warning.line
71 }
72 }
73 });
74
75 issues.push(issue);
76 }
77 }
78
79 serde_json::to_string_pretty(&issues).unwrap_or_else(|_| "[]".to_string())
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85 use crate::rule::{Fix, Severity};
86 use serde_json::Value;
87
88 #[test]
89 fn test_gitlab_formatter_default() {
90 let _formatter = GitLabFormatter;
91 }
93
94 #[test]
95 fn test_gitlab_formatter_new() {
96 let _formatter = GitLabFormatter::new();
97 }
99
100 #[test]
101 fn test_format_warnings_empty() {
102 let formatter = GitLabFormatter::new();
103 let warnings = vec![];
104 let output = formatter.format_warnings(&warnings, "test.md");
105 assert_eq!(output, "[]");
106 }
107
108 #[test]
109 fn test_format_single_warning() {
110 let formatter = GitLabFormatter::new();
111 let warnings = vec![LintWarning {
112 line: 10,
113 column: 5,
114 end_line: 10,
115 end_column: 15,
116 rule_name: Some("MD001".to_string()),
117 message: "Heading levels should only increment by one level at a time".to_string(),
118 severity: Severity::Warning,
119 fix: None,
120 }];
121
122 let output = formatter.format_warnings(&warnings, "README.md");
123 let issues: Vec<Value> = serde_json::from_str(&output).unwrap();
124
125 assert_eq!(issues.len(), 1);
126 let issue = &issues[0];
127 assert_eq!(
128 issue["description"],
129 "Heading levels should only increment by one level at a time"
130 );
131 assert_eq!(issue["check_name"], "MD001");
132 assert_eq!(issue["fingerprint"], "README.md-10-5-MD001");
133 assert_eq!(issue["severity"], "minor");
134 assert_eq!(issue["location"]["path"], "README.md");
135 assert_eq!(issue["location"]["lines"]["begin"], 10);
136 }
137
138 #[test]
139 fn test_format_single_warning_with_fix() {
140 let formatter = GitLabFormatter::new();
141 let warnings = vec![LintWarning {
142 line: 10,
143 column: 5,
144 end_line: 10,
145 end_column: 15,
146 rule_name: Some("MD001".to_string()),
147 message: "Heading levels should only increment by one level at a time".to_string(),
148 severity: Severity::Warning,
149 fix: Some(Fix::new(100..110, "## Heading".to_string())),
150 }];
151
152 let output = formatter.format_warnings(&warnings, "README.md");
153 let issues: Vec<Value> = serde_json::from_str(&output).unwrap();
154
155 assert_eq!(issues.len(), 1);
157 assert_eq!(issues[0]["check_name"], "MD001");
158 }
159
160 #[test]
161 fn test_format_multiple_warnings() {
162 let formatter = GitLabFormatter::new();
163 let warnings = vec![
164 LintWarning {
165 line: 5,
166 column: 1,
167 end_line: 5,
168 end_column: 10,
169 rule_name: Some("MD001".to_string()),
170 message: "First warning".to_string(),
171 severity: Severity::Warning,
172 fix: None,
173 },
174 LintWarning {
175 line: 10,
176 column: 3,
177 end_line: 10,
178 end_column: 20,
179 rule_name: Some("MD013".to_string()),
180 message: "Second warning".to_string(),
181 severity: Severity::Error,
182 fix: None,
183 },
184 ];
185
186 let output = formatter.format_warnings(&warnings, "test.md");
187 let issues: Vec<Value> = serde_json::from_str(&output).unwrap();
188
189 assert_eq!(issues.len(), 2);
190 assert_eq!(issues[0]["check_name"], "MD001");
191 assert_eq!(issues[0]["location"]["lines"]["begin"], 5);
192 assert_eq!(issues[1]["check_name"], "MD013");
193 assert_eq!(issues[1]["location"]["lines"]["begin"], 10);
194 }
195
196 #[test]
197 fn test_format_warning_unknown_rule() {
198 let formatter = GitLabFormatter::new();
199 let warnings = vec![LintWarning {
200 line: 1,
201 column: 1,
202 end_line: 1,
203 end_column: 5,
204 rule_name: None,
205 message: "Unknown rule warning".to_string(),
206 severity: Severity::Warning,
207 fix: None,
208 }];
209
210 let output = formatter.format_warnings(&warnings, "file.md");
211 let issues: Vec<Value> = serde_json::from_str(&output).unwrap();
212
213 assert_eq!(issues[0]["check_name"], "unknown");
214 assert_eq!(issues[0]["fingerprint"], "file.md-1-1-unknown");
215 }
216
217 #[test]
218 fn test_gitlab_report_empty() {
219 let warnings = vec![];
220 let output = format_gitlab_report(&warnings);
221 assert_eq!(output, "[]");
222 }
223
224 #[test]
225 fn test_gitlab_report_single_file() {
226 let warnings = vec![(
227 "test.md".to_string(),
228 vec![LintWarning {
229 line: 10,
230 column: 5,
231 end_line: 10,
232 end_column: 15,
233 rule_name: Some("MD001".to_string()),
234 message: "Test warning".to_string(),
235 severity: Severity::Warning,
236 fix: None,
237 }],
238 )];
239
240 let output = format_gitlab_report(&warnings);
241 let issues: Vec<Value> = serde_json::from_str(&output).unwrap();
242
243 assert_eq!(issues.len(), 1);
244 assert_eq!(issues[0]["location"]["path"], "test.md");
245 }
246
247 #[test]
248 fn test_gitlab_report_multiple_files() {
249 let warnings = vec![
250 (
251 "file1.md".to_string(),
252 vec![LintWarning {
253 line: 1,
254 column: 1,
255 end_line: 1,
256 end_column: 5,
257 rule_name: Some("MD001".to_string()),
258 message: "Warning in file 1".to_string(),
259 severity: Severity::Warning,
260 fix: None,
261 }],
262 ),
263 (
264 "file2.md".to_string(),
265 vec![
266 LintWarning {
267 line: 5,
268 column: 1,
269 end_line: 5,
270 end_column: 10,
271 rule_name: Some("MD013".to_string()),
272 message: "Warning 1 in file 2".to_string(),
273 severity: Severity::Warning,
274 fix: None,
275 },
276 LintWarning {
277 line: 10,
278 column: 1,
279 end_line: 10,
280 end_column: 10,
281 rule_name: Some("MD022".to_string()),
282 message: "Warning 2 in file 2".to_string(),
283 severity: Severity::Error,
284 fix: None,
285 },
286 ],
287 ),
288 ];
289
290 let output = format_gitlab_report(&warnings);
291 let issues: Vec<Value> = serde_json::from_str(&output).unwrap();
292
293 assert_eq!(issues.len(), 3);
294 assert_eq!(issues[0]["location"]["path"], "file1.md");
295 assert_eq!(issues[1]["location"]["path"], "file2.md");
296 assert_eq!(issues[2]["location"]["path"], "file2.md");
297 }
298
299 #[test]
300 fn test_fingerprint_uniqueness() {
301 let formatter = GitLabFormatter::new();
302
303 let warnings = vec![
305 LintWarning {
306 line: 10,
307 column: 5,
308 end_line: 10,
309 end_column: 15,
310 rule_name: Some("MD001".to_string()),
311 message: "First rule".to_string(),
312 severity: Severity::Warning,
313 fix: None,
314 },
315 LintWarning {
316 line: 10,
317 column: 5,
318 end_line: 10,
319 end_column: 15,
320 rule_name: Some("MD002".to_string()),
321 message: "Second rule".to_string(),
322 severity: Severity::Warning,
323 fix: None,
324 },
325 ];
326
327 let output = formatter.format_warnings(&warnings, "test.md");
328 let issues: Vec<Value> = serde_json::from_str(&output).unwrap();
329
330 assert_ne!(issues[0]["fingerprint"], issues[1]["fingerprint"]);
331 assert_eq!(issues[0]["fingerprint"], "test.md-10-5-MD001");
332 assert_eq!(issues[1]["fingerprint"], "test.md-10-5-MD002");
333 }
334
335 #[test]
336 fn test_severity_always_minor() {
337 let formatter = GitLabFormatter::new();
338
339 let warnings = vec![
341 LintWarning {
342 line: 1,
343 column: 1,
344 end_line: 1,
345 end_column: 5,
346 rule_name: Some("MD001".to_string()),
347 message: "Warning severity".to_string(),
348 severity: Severity::Warning,
349 fix: None,
350 },
351 LintWarning {
352 line: 2,
353 column: 1,
354 end_line: 2,
355 end_column: 5,
356 rule_name: Some("MD002".to_string()),
357 message: "Error severity".to_string(),
358 severity: Severity::Error,
359 fix: None,
360 },
361 ];
362
363 let output = formatter.format_warnings(&warnings, "test.md");
364 let issues: Vec<Value> = serde_json::from_str(&output).unwrap();
365
366 assert_eq!(issues[0]["severity"], "minor");
368 assert_eq!(issues[1]["severity"], "minor");
369 }
370
371 #[test]
372 fn test_special_characters_in_message() {
373 let formatter = GitLabFormatter::new();
374 let warnings = vec![LintWarning {
375 line: 1,
376 column: 1,
377 end_line: 1,
378 end_column: 5,
379 rule_name: Some("MD001".to_string()),
380 message: "Warning with \"quotes\" and 'apostrophes' and \n newline".to_string(),
381 severity: Severity::Warning,
382 fix: None,
383 }];
384
385 let output = formatter.format_warnings(&warnings, "test.md");
386 let issues: Vec<Value> = serde_json::from_str(&output).unwrap();
387
388 assert_eq!(
390 issues[0]["description"],
391 "Warning with \"quotes\" and 'apostrophes' and \n newline"
392 );
393 }
394
395 #[test]
396 fn test_special_characters_in_file_path() {
397 let formatter = GitLabFormatter::new();
398 let warnings = vec![LintWarning {
399 line: 1,
400 column: 1,
401 end_line: 1,
402 end_column: 5,
403 rule_name: Some("MD001".to_string()),
404 message: "Test".to_string(),
405 severity: Severity::Warning,
406 fix: None,
407 }];
408
409 let output = formatter.format_warnings(&warnings, "path/with spaces/and-dashes.md");
410 let issues: Vec<Value> = serde_json::from_str(&output).unwrap();
411
412 assert_eq!(issues[0]["location"]["path"], "path/with spaces/and-dashes.md");
413 assert_eq!(issues[0]["fingerprint"], "path/with spaces/and-dashes.md-1-1-MD001");
414 }
415
416 #[test]
417 fn test_json_pretty_formatting() {
418 let formatter = GitLabFormatter::new();
419 let warnings = vec![LintWarning {
420 line: 1,
421 column: 1,
422 end_line: 1,
423 end_column: 5,
424 rule_name: Some("MD001".to_string()),
425 message: "Test".to_string(),
426 severity: Severity::Warning,
427 fix: None,
428 }];
429
430 let output = formatter.format_warnings(&warnings, "test.md");
431
432 assert!(output.contains('\n'));
434 assert!(output.contains(" "));
435 }
436}