rumdl_lib/output/formatters/
azure.rs1use crate::output::OutputFormatter;
4use crate::rule::{LintWarning, Severity};
5
6pub struct AzureFormatter;
9
10impl Default for AzureFormatter {
11 fn default() -> Self {
12 Self
13 }
14}
15
16impl AzureFormatter {
17 pub fn new() -> Self {
18 Self
19 }
20}
21
22impl OutputFormatter for AzureFormatter {
23 fn format_warnings(&self, warnings: &[LintWarning], file_path: &str) -> String {
24 let mut output = String::new();
25
26 for warning in warnings {
27 let rule_name = warning.rule_name.as_deref().unwrap_or("unknown");
28
29 let issue_type = match warning.severity {
31 Severity::Error => "error",
32 Severity::Warning | Severity::Info => "warning",
33 };
34
35 let line = format!(
37 "##vso[task.logissue type={};sourcepath={};linenumber={};columnnumber={};code={}]{}",
38 issue_type, file_path, warning.line, warning.column, rule_name, warning.message
39 );
40
41 output.push_str(&line);
42 output.push('\n');
43 }
44
45 if output.ends_with('\n') {
47 output.pop();
48 }
49
50 output
51 }
52}
53
54#[cfg(test)]
55mod tests {
56 use super::*;
57 use crate::rule::{Fix, Severity};
58
59 #[test]
60 fn test_azure_formatter_default() {
61 let _formatter = AzureFormatter;
62 }
64
65 #[test]
66 fn test_azure_formatter_new() {
67 let _formatter = AzureFormatter::new();
68 }
70
71 #[test]
72 fn test_format_warnings_empty() {
73 let formatter = AzureFormatter::new();
74 let warnings = vec![];
75 let output = formatter.format_warnings(&warnings, "test.md");
76 assert_eq!(output, "");
77 }
78
79 #[test]
80 fn test_format_single_warning() {
81 let formatter = AzureFormatter::new();
82 let warnings = vec![LintWarning {
83 line: 10,
84 column: 5,
85 end_line: 10,
86 end_column: 15,
87 rule_name: Some("MD001".to_string()),
88 message: "Heading levels should only increment by one level at a time".to_string(),
89 severity: Severity::Warning,
90 fix: None,
91 }];
92
93 let output = formatter.format_warnings(&warnings, "README.md");
94 assert_eq!(
95 output,
96 "##vso[task.logissue type=warning;sourcepath=README.md;linenumber=10;columnnumber=5;code=MD001]Heading levels should only increment by one level at a time"
97 );
98 }
99
100 #[test]
101 fn test_format_multiple_warnings() {
102 let formatter = AzureFormatter::new();
103 let warnings = vec![
104 LintWarning {
105 line: 5,
106 column: 1,
107 end_line: 5,
108 end_column: 10,
109 rule_name: Some("MD001".to_string()),
110 message: "First warning".to_string(),
111 severity: Severity::Warning,
112 fix: None,
113 },
114 LintWarning {
115 line: 10,
116 column: 3,
117 end_line: 10,
118 end_column: 20,
119 rule_name: Some("MD013".to_string()),
120 message: "Second warning".to_string(),
121 severity: Severity::Error,
122 fix: None,
123 },
124 ];
125
126 let output = formatter.format_warnings(&warnings, "test.md");
127 let expected = "##vso[task.logissue type=warning;sourcepath=test.md;linenumber=5;columnnumber=1;code=MD001]First warning\n##vso[task.logissue type=error;sourcepath=test.md;linenumber=10;columnnumber=3;code=MD013]Second warning";
128 assert_eq!(output, expected);
129 }
130
131 #[test]
132 fn test_format_warning_with_fix() {
133 let formatter = AzureFormatter::new();
134 let warnings = vec![LintWarning {
135 line: 15,
136 column: 1,
137 end_line: 15,
138 end_column: 10,
139 rule_name: Some("MD022".to_string()),
140 message: "Headings should be surrounded by blank lines".to_string(),
141 severity: Severity::Warning,
142 fix: Some(Fix::new(100..110, "\n# Heading\n".to_string())),
143 }];
144
145 let output = formatter.format_warnings(&warnings, "doc.md");
146 assert_eq!(
148 output,
149 "##vso[task.logissue type=warning;sourcepath=doc.md;linenumber=15;columnnumber=1;code=MD022]Headings should be surrounded by blank lines"
150 );
151 }
152
153 #[test]
154 fn test_format_warning_unknown_rule() {
155 let formatter = AzureFormatter::new();
156 let warnings = vec![LintWarning {
157 line: 1,
158 column: 1,
159 end_line: 1,
160 end_column: 5,
161 rule_name: None,
162 message: "Unknown rule warning".to_string(),
163 severity: Severity::Warning,
164 fix: None,
165 }];
166
167 let output = formatter.format_warnings(&warnings, "file.md");
168 assert_eq!(
169 output,
170 "##vso[task.logissue type=warning;sourcepath=file.md;linenumber=1;columnnumber=1;code=unknown]Unknown rule warning"
171 );
172 }
173
174 #[test]
175 fn test_edge_cases() {
176 let formatter = AzureFormatter::new();
177
178 let warnings = vec![LintWarning {
180 line: 99999,
181 column: 12345,
182 end_line: 100000,
183 end_column: 12350,
184 rule_name: Some("MD999".to_string()),
185 message: "Edge case warning".to_string(),
186 severity: Severity::Error,
187 fix: None,
188 }];
189
190 let output = formatter.format_warnings(&warnings, "large.md");
191 assert_eq!(
192 output,
193 "##vso[task.logissue type=error;sourcepath=large.md;linenumber=99999;columnnumber=12345;code=MD999]Edge case warning"
194 );
195 }
196
197 #[test]
198 fn test_special_characters_in_message() {
199 let formatter = AzureFormatter::new();
200 let warnings = vec![LintWarning {
201 line: 1,
202 column: 1,
203 end_line: 1,
204 end_column: 5,
205 rule_name: Some("MD001".to_string()),
206 message: "Warning with \"quotes\" and 'apostrophes' and \n newline".to_string(),
207 severity: Severity::Warning,
208 fix: None,
209 }];
210
211 let output = formatter.format_warnings(&warnings, "test.md");
212 assert_eq!(
214 output,
215 "##vso[task.logissue type=warning;sourcepath=test.md;linenumber=1;columnnumber=1;code=MD001]Warning with \"quotes\" and 'apostrophes' and \n newline"
216 );
217 }
218
219 #[test]
220 fn test_special_characters_in_file_path() {
221 let formatter = AzureFormatter::new();
222 let warnings = vec![LintWarning {
223 line: 1,
224 column: 1,
225 end_line: 1,
226 end_column: 5,
227 rule_name: Some("MD001".to_string()),
228 message: "Test".to_string(),
229 severity: Severity::Warning,
230 fix: None,
231 }];
232
233 let output = formatter.format_warnings(&warnings, "path/with spaces/and-dashes.md");
234 assert_eq!(
235 output,
236 "##vso[task.logissue type=warning;sourcepath=path/with spaces/and-dashes.md;linenumber=1;columnnumber=1;code=MD001]Test"
237 );
238 }
239
240 #[test]
241 fn test_azure_format_structure() {
242 let formatter = AzureFormatter::new();
243 let warnings = vec![LintWarning {
244 line: 42,
245 column: 7,
246 end_line: 42,
247 end_column: 10,
248 rule_name: Some("MD010".to_string()),
249 message: "Hard tabs".to_string(),
250 severity: Severity::Warning,
251 fix: None,
252 }];
253
254 let output = formatter.format_warnings(&warnings, "test.md");
255
256 assert!(output.starts_with("##vso[task.logissue "));
258 assert!(output.contains("type=warning"));
259 assert!(output.contains("sourcepath=test.md"));
260 assert!(output.contains("linenumber=42"));
261 assert!(output.contains("columnnumber=7"));
262 assert!(output.contains("code=MD010"));
263 assert!(output.ends_with("]Hard tabs"));
264 }
265
266 #[test]
267 fn test_severity_levels() {
268 let formatter = AzureFormatter::new();
269
270 let warnings = vec![
273 LintWarning {
274 line: 1,
275 column: 1,
276 end_line: 1,
277 end_column: 5,
278 rule_name: Some("MD001".to_string()),
279 message: "Warning severity".to_string(),
280 severity: Severity::Warning,
281 fix: None,
282 },
283 LintWarning {
284 line: 2,
285 column: 1,
286 end_line: 2,
287 end_column: 5,
288 rule_name: Some("MD002".to_string()),
289 message: "Error severity".to_string(),
290 severity: Severity::Error,
291 fix: None,
292 },
293 LintWarning {
294 line: 3,
295 column: 1,
296 end_line: 3,
297 end_column: 5,
298 rule_name: Some("MD003".to_string()),
299 message: "Info severity".to_string(),
300 severity: Severity::Info,
301 fix: None,
302 },
303 ];
304
305 let output = formatter.format_warnings(&warnings, "test.md");
306 let lines: Vec<&str> = output.lines().collect();
307
308 assert!(lines[0].contains("type=warning"));
310 assert!(lines[1].contains("type=error"));
311 assert!(lines[2].contains("type=warning"));
312 }
313
314 #[test]
315 fn test_semicolons_in_parameters() {
316 let formatter = AzureFormatter::new();
317
318 let warnings = vec![LintWarning {
320 line: 1,
321 column: 1,
322 end_line: 1,
323 end_column: 5,
324 rule_name: Some("MD;001".to_string()), message: "Test message; with semicolon".to_string(),
326 severity: Severity::Warning,
327 fix: None,
328 }];
329
330 let output = formatter.format_warnings(&warnings, "file;with;semicolons.md");
331 assert_eq!(
333 output,
334 "##vso[task.logissue type=warning;sourcepath=file;with;semicolons.md;linenumber=1;columnnumber=1;code=MD;001]Test message; with semicolon"
335 );
336 }
337
338 #[test]
339 fn test_brackets_in_message() {
340 let formatter = AzureFormatter::new();
341
342 let warnings = vec![LintWarning {
344 line: 1,
345 column: 1,
346 end_line: 1,
347 end_column: 5,
348 rule_name: Some("MD001".to_string()),
349 message: "Message with [brackets] and ]unmatched".to_string(),
350 severity: Severity::Warning,
351 fix: None,
352 }];
353
354 let output = formatter.format_warnings(&warnings, "test.md");
355 assert_eq!(
356 output,
357 "##vso[task.logissue type=warning;sourcepath=test.md;linenumber=1;columnnumber=1;code=MD001]Message with [brackets] and ]unmatched"
358 );
359 }
360}