1use crate::format::Formatter;
5use crate::lint::LintResult;
6use crate::types::FileResult;
7use serde::Serialize;
8use std::env;
9use std::hash::{DefaultHasher, Hash, Hasher};
10use std::path::PathBuf;
11
12pub struct GitlabFormatter {
13 pretty: bool,
14}
15
16impl GitlabFormatter {
17 pub fn new(pretty: bool) -> Self {
18 Self { pretty }
19 }
20}
21
22fn file_violations(file_result: &FileResult, path_relative: &str) -> Vec<GitlabViolation> {
23 file_result
24 .violations
25 .iter()
26 .map(|violation| {
27 let key = format!("{}:{}", path_relative, violation.line);
28 GitlabViolation {
29 description: violation.message.clone(),
30 check_name: violation.rule.clone(),
31 fingerprint: create_fingerprint(&key),
32 location: GitlabLocation {
33 path: path_relative.to_string(),
34 lines: GitlabLines {
35 begin: violation.line,
36 },
37 },
38 severity: if violation.fix.is_some() {
39 Severity::Minor
40 } else {
41 Severity::Major
42 },
43 }
44 })
45 .collect()
46}
47
48fn create_fingerprint(input: &str) -> String {
49 let mut hasher = DefaultHasher::new();
50 input.hash(&mut hasher);
51 let hash_value = hasher.finish();
52 format!("{:x}", hash_value)
53}
54
55#[derive(Serialize)]
56#[serde(rename_all = "lowercase")]
57enum Severity {
58 Minor,
59 Major,
60}
61
62#[derive(Serialize)]
63struct GitlabViolation {
64 description: String,
65 check_name: String,
66 fingerprint: String,
67 location: GitlabLocation,
68 severity: Severity,
69}
70
71#[derive(Serialize)]
72struct GitlabLocation {
73 path: String,
74 lines: GitlabLines,
75}
76
77#[derive(Serialize)]
78struct GitlabLines {
79 begin: usize,
80}
81
82impl Formatter for GitlabFormatter {
83 fn format(&self, result: &LintResult) -> String {
84 let current_dir = env::current_dir().unwrap_or_else(|_| PathBuf::from(""));
85 let violations: Vec<GitlabViolation> = result
86 .file_results
87 .iter()
88 .flat_map(|file_result| {
89 let path_relative = file_result
90 .path
91 .strip_prefix(¤t_dir)
92 .map(|rel_path| rel_path.to_path_buf())
93 .unwrap_or_else(|_| file_result.path.clone())
94 .display()
95 .to_string();
96
97 file_violations(file_result, &path_relative)
98 })
99 .collect();
100
101 if self.pretty {
102 serde_json::to_string_pretty(&violations)
103 .unwrap_or_else(|e| format!("{{\"error\": \"Failed to serialize JSON: {}\"}}", e))
104 } else {
105 serde_json::to_string(&violations)
106 .unwrap_or_else(|e| format!("{{\"error\": \"Failed to serialize JSON: {}\"}}", e))
107 }
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114 use crate::types::Violation;
115
116 #[test]
117 fn test_empty_result() {
118 let formatter = GitlabFormatter::new(false);
119 let result = LintResult::new();
120 let output = formatter.format(&result);
121
122 assert!(output.eq("[]"));
123 }
124
125 #[test]
126 fn test_single_violation() {
127 let formatter = GitlabFormatter::new(false);
128 let mut result = LintResult::new();
129
130 result.add_file_result(
131 PathBuf::from("test.md"),
132 vec![Violation {
133 line: 5,
134 column: Some(10),
135 rule: "MD001".to_string(),
136 message: "Test message".to_string(),
137 fix: None,
138 }],
139 vec![],
140 );
141
142 let output = formatter.format(&result);
143 let fingerprint = create_fingerprint("test.md:5");
144
145 assert!(output.contains("\"description\":\"Test message\""));
146 assert!(output.contains("\"check_name\":\"MD001\""));
147 assert!(output.contains(&format!("\"fingerprint\":\"{}\"", fingerprint)));
148 assert!(output.contains("\"location\":{\"path\":\"test.md\","));
149 assert!(output.contains("\"lines\":{\"begin\":5"));
150 assert!(output.contains("\"severity\":\"major\""));
151 }
152
153 #[test]
154 fn test_pretty_print() {
155 let formatter = GitlabFormatter::new(true);
156 let mut result = LintResult::new();
157
158 result.add_file_result(
159 PathBuf::from("test.md"),
160 vec![Violation {
161 line: 1,
162 column: None,
163 rule: "MD001".to_string(),
164 message: "Test".to_string(),
165 fix: None,
166 }],
167 vec![],
168 );
169
170 let output = formatter.format(&result);
171
172 assert!(output.contains(" ") || output.contains("\n"));
174 }
175
176 #[test]
177 fn test_fixable_severity() {
178 let formatter = GitlabFormatter::new(false);
179 let mut result = LintResult::new();
180
181 result.add_file_result(
182 PathBuf::from("test.md"),
183 vec![Violation {
184 line: 1,
185 column: Some(1),
186 rule: "MD009".to_string(),
187 message: "Trailing spaces".to_string(),
188 fix: Some(crate::types::Fix {
189 line_start: 1,
190 line_end: 1,
191 column_start: None,
192 column_end: None,
193 replacement: "fixed".to_string(),
194 description: "Remove trailing spaces".to_string(),
195 }),
196 }],
197 vec![],
198 );
199
200 let output = formatter.format(&result);
201
202 assert!(output.contains("\"severity\":\"minor\""));
203 }
204}