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 {
150 range: 100..110,
151 replacement: "## Heading".to_string(),
152 }),
153 }];
154
155 let output = formatter.format_warnings(&warnings, "README.md");
156 let issues: Vec<Value> = serde_json::from_str(&output).unwrap();
157
158 assert_eq!(issues.len(), 1);
160 assert_eq!(issues[0]["check_name"], "MD001");
161 }
162
163 #[test]
164 fn test_format_multiple_warnings() {
165 let formatter = GitLabFormatter::new();
166 let warnings = vec![
167 LintWarning {
168 line: 5,
169 column: 1,
170 end_line: 5,
171 end_column: 10,
172 rule_name: Some("MD001".to_string()),
173 message: "First warning".to_string(),
174 severity: Severity::Warning,
175 fix: None,
176 },
177 LintWarning {
178 line: 10,
179 column: 3,
180 end_line: 10,
181 end_column: 20,
182 rule_name: Some("MD013".to_string()),
183 message: "Second warning".to_string(),
184 severity: Severity::Error,
185 fix: None,
186 },
187 ];
188
189 let output = formatter.format_warnings(&warnings, "test.md");
190 let issues: Vec<Value> = serde_json::from_str(&output).unwrap();
191
192 assert_eq!(issues.len(), 2);
193 assert_eq!(issues[0]["check_name"], "MD001");
194 assert_eq!(issues[0]["location"]["lines"]["begin"], 5);
195 assert_eq!(issues[1]["check_name"], "MD013");
196 assert_eq!(issues[1]["location"]["lines"]["begin"], 10);
197 }
198
199 #[test]
200 fn test_format_warning_unknown_rule() {
201 let formatter = GitLabFormatter::new();
202 let warnings = vec![LintWarning {
203 line: 1,
204 column: 1,
205 end_line: 1,
206 end_column: 5,
207 rule_name: None,
208 message: "Unknown rule warning".to_string(),
209 severity: Severity::Warning,
210 fix: None,
211 }];
212
213 let output = formatter.format_warnings(&warnings, "file.md");
214 let issues: Vec<Value> = serde_json::from_str(&output).unwrap();
215
216 assert_eq!(issues[0]["check_name"], "unknown");
217 assert_eq!(issues[0]["fingerprint"], "file.md-1-1-unknown");
218 }
219
220 #[test]
221 fn test_gitlab_report_empty() {
222 let warnings = vec![];
223 let output = format_gitlab_report(&warnings);
224 assert_eq!(output, "[]");
225 }
226
227 #[test]
228 fn test_gitlab_report_single_file() {
229 let warnings = vec![(
230 "test.md".to_string(),
231 vec![LintWarning {
232 line: 10,
233 column: 5,
234 end_line: 10,
235 end_column: 15,
236 rule_name: Some("MD001".to_string()),
237 message: "Test warning".to_string(),
238 severity: Severity::Warning,
239 fix: None,
240 }],
241 )];
242
243 let output = format_gitlab_report(&warnings);
244 let issues: Vec<Value> = serde_json::from_str(&output).unwrap();
245
246 assert_eq!(issues.len(), 1);
247 assert_eq!(issues[0]["location"]["path"], "test.md");
248 }
249
250 #[test]
251 fn test_gitlab_report_multiple_files() {
252 let warnings = vec![
253 (
254 "file1.md".to_string(),
255 vec![LintWarning {
256 line: 1,
257 column: 1,
258 end_line: 1,
259 end_column: 5,
260 rule_name: Some("MD001".to_string()),
261 message: "Warning in file 1".to_string(),
262 severity: Severity::Warning,
263 fix: None,
264 }],
265 ),
266 (
267 "file2.md".to_string(),
268 vec![
269 LintWarning {
270 line: 5,
271 column: 1,
272 end_line: 5,
273 end_column: 10,
274 rule_name: Some("MD013".to_string()),
275 message: "Warning 1 in file 2".to_string(),
276 severity: Severity::Warning,
277 fix: None,
278 },
279 LintWarning {
280 line: 10,
281 column: 1,
282 end_line: 10,
283 end_column: 10,
284 rule_name: Some("MD022".to_string()),
285 message: "Warning 2 in file 2".to_string(),
286 severity: Severity::Error,
287 fix: None,
288 },
289 ],
290 ),
291 ];
292
293 let output = format_gitlab_report(&warnings);
294 let issues: Vec<Value> = serde_json::from_str(&output).unwrap();
295
296 assert_eq!(issues.len(), 3);
297 assert_eq!(issues[0]["location"]["path"], "file1.md");
298 assert_eq!(issues[1]["location"]["path"], "file2.md");
299 assert_eq!(issues[2]["location"]["path"], "file2.md");
300 }
301
302 #[test]
303 fn test_fingerprint_uniqueness() {
304 let formatter = GitLabFormatter::new();
305
306 let warnings = vec![
308 LintWarning {
309 line: 10,
310 column: 5,
311 end_line: 10,
312 end_column: 15,
313 rule_name: Some("MD001".to_string()),
314 message: "First rule".to_string(),
315 severity: Severity::Warning,
316 fix: None,
317 },
318 LintWarning {
319 line: 10,
320 column: 5,
321 end_line: 10,
322 end_column: 15,
323 rule_name: Some("MD002".to_string()),
324 message: "Second rule".to_string(),
325 severity: Severity::Warning,
326 fix: None,
327 },
328 ];
329
330 let output = formatter.format_warnings(&warnings, "test.md");
331 let issues: Vec<Value> = serde_json::from_str(&output).unwrap();
332
333 assert_ne!(issues[0]["fingerprint"], issues[1]["fingerprint"]);
334 assert_eq!(issues[0]["fingerprint"], "test.md-10-5-MD001");
335 assert_eq!(issues[1]["fingerprint"], "test.md-10-5-MD002");
336 }
337
338 #[test]
339 fn test_severity_always_minor() {
340 let formatter = GitLabFormatter::new();
341
342 let warnings = vec![
344 LintWarning {
345 line: 1,
346 column: 1,
347 end_line: 1,
348 end_column: 5,
349 rule_name: Some("MD001".to_string()),
350 message: "Warning severity".to_string(),
351 severity: Severity::Warning,
352 fix: None,
353 },
354 LintWarning {
355 line: 2,
356 column: 1,
357 end_line: 2,
358 end_column: 5,
359 rule_name: Some("MD002".to_string()),
360 message: "Error severity".to_string(),
361 severity: Severity::Error,
362 fix: None,
363 },
364 ];
365
366 let output = formatter.format_warnings(&warnings, "test.md");
367 let issues: Vec<Value> = serde_json::from_str(&output).unwrap();
368
369 assert_eq!(issues[0]["severity"], "minor");
371 assert_eq!(issues[1]["severity"], "minor");
372 }
373
374 #[test]
375 fn test_special_characters_in_message() {
376 let formatter = GitLabFormatter::new();
377 let warnings = vec![LintWarning {
378 line: 1,
379 column: 1,
380 end_line: 1,
381 end_column: 5,
382 rule_name: Some("MD001".to_string()),
383 message: "Warning with \"quotes\" and 'apostrophes' and \n newline".to_string(),
384 severity: Severity::Warning,
385 fix: None,
386 }];
387
388 let output = formatter.format_warnings(&warnings, "test.md");
389 let issues: Vec<Value> = serde_json::from_str(&output).unwrap();
390
391 assert_eq!(
393 issues[0]["description"],
394 "Warning with \"quotes\" and 'apostrophes' and \n newline"
395 );
396 }
397
398 #[test]
399 fn test_special_characters_in_file_path() {
400 let formatter = GitLabFormatter::new();
401 let warnings = vec![LintWarning {
402 line: 1,
403 column: 1,
404 end_line: 1,
405 end_column: 5,
406 rule_name: Some("MD001".to_string()),
407 message: "Test".to_string(),
408 severity: Severity::Warning,
409 fix: None,
410 }];
411
412 let output = formatter.format_warnings(&warnings, "path/with spaces/and-dashes.md");
413 let issues: Vec<Value> = serde_json::from_str(&output).unwrap();
414
415 assert_eq!(issues[0]["location"]["path"], "path/with spaces/and-dashes.md");
416 assert_eq!(issues[0]["fingerprint"], "path/with spaces/and-dashes.md-1-1-MD001");
417 }
418
419 #[test]
420 fn test_json_pretty_formatting() {
421 let formatter = GitLabFormatter::new();
422 let warnings = vec![LintWarning {
423 line: 1,
424 column: 1,
425 end_line: 1,
426 end_column: 5,
427 rule_name: Some("MD001".to_string()),
428 message: "Test".to_string(),
429 severity: Severity::Warning,
430 fix: None,
431 }];
432
433 let output = formatter.format_warnings(&warnings, "test.md");
434
435 assert!(output.contains('\n'));
437 assert!(output.contains(" "));
438 }
439}