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 {
143 range: 100..110,
144 replacement: "\n# Heading\n".to_string(),
145 }),
146 }];
147
148 let output = formatter.format_warnings(&warnings, "doc.md");
149 assert_eq!(
151 output,
152 "##vso[task.logissue type=warning;sourcepath=doc.md;linenumber=15;columnnumber=1;code=MD022]Headings should be surrounded by blank lines"
153 );
154 }
155
156 #[test]
157 fn test_format_warning_unknown_rule() {
158 let formatter = AzureFormatter::new();
159 let warnings = vec![LintWarning {
160 line: 1,
161 column: 1,
162 end_line: 1,
163 end_column: 5,
164 rule_name: None,
165 message: "Unknown rule warning".to_string(),
166 severity: Severity::Warning,
167 fix: None,
168 }];
169
170 let output = formatter.format_warnings(&warnings, "file.md");
171 assert_eq!(
172 output,
173 "##vso[task.logissue type=warning;sourcepath=file.md;linenumber=1;columnnumber=1;code=unknown]Unknown rule warning"
174 );
175 }
176
177 #[test]
178 fn test_edge_cases() {
179 let formatter = AzureFormatter::new();
180
181 let warnings = vec![LintWarning {
183 line: 99999,
184 column: 12345,
185 end_line: 100000,
186 end_column: 12350,
187 rule_name: Some("MD999".to_string()),
188 message: "Edge case warning".to_string(),
189 severity: Severity::Error,
190 fix: None,
191 }];
192
193 let output = formatter.format_warnings(&warnings, "large.md");
194 assert_eq!(
195 output,
196 "##vso[task.logissue type=error;sourcepath=large.md;linenumber=99999;columnnumber=12345;code=MD999]Edge case warning"
197 );
198 }
199
200 #[test]
201 fn test_special_characters_in_message() {
202 let formatter = AzureFormatter::new();
203 let warnings = vec![LintWarning {
204 line: 1,
205 column: 1,
206 end_line: 1,
207 end_column: 5,
208 rule_name: Some("MD001".to_string()),
209 message: "Warning with \"quotes\" and 'apostrophes' and \n newline".to_string(),
210 severity: Severity::Warning,
211 fix: None,
212 }];
213
214 let output = formatter.format_warnings(&warnings, "test.md");
215 assert_eq!(
217 output,
218 "##vso[task.logissue type=warning;sourcepath=test.md;linenumber=1;columnnumber=1;code=MD001]Warning with \"quotes\" and 'apostrophes' and \n newline"
219 );
220 }
221
222 #[test]
223 fn test_special_characters_in_file_path() {
224 let formatter = AzureFormatter::new();
225 let warnings = vec![LintWarning {
226 line: 1,
227 column: 1,
228 end_line: 1,
229 end_column: 5,
230 rule_name: Some("MD001".to_string()),
231 message: "Test".to_string(),
232 severity: Severity::Warning,
233 fix: None,
234 }];
235
236 let output = formatter.format_warnings(&warnings, "path/with spaces/and-dashes.md");
237 assert_eq!(
238 output,
239 "##vso[task.logissue type=warning;sourcepath=path/with spaces/and-dashes.md;linenumber=1;columnnumber=1;code=MD001]Test"
240 );
241 }
242
243 #[test]
244 fn test_azure_format_structure() {
245 let formatter = AzureFormatter::new();
246 let warnings = vec![LintWarning {
247 line: 42,
248 column: 7,
249 end_line: 42,
250 end_column: 10,
251 rule_name: Some("MD010".to_string()),
252 message: "Hard tabs".to_string(),
253 severity: Severity::Warning,
254 fix: None,
255 }];
256
257 let output = formatter.format_warnings(&warnings, "test.md");
258
259 assert!(output.starts_with("##vso[task.logissue "));
261 assert!(output.contains("type=warning"));
262 assert!(output.contains("sourcepath=test.md"));
263 assert!(output.contains("linenumber=42"));
264 assert!(output.contains("columnnumber=7"));
265 assert!(output.contains("code=MD010"));
266 assert!(output.ends_with("]Hard tabs"));
267 }
268
269 #[test]
270 fn test_severity_levels() {
271 let formatter = AzureFormatter::new();
272
273 let warnings = vec![
276 LintWarning {
277 line: 1,
278 column: 1,
279 end_line: 1,
280 end_column: 5,
281 rule_name: Some("MD001".to_string()),
282 message: "Warning severity".to_string(),
283 severity: Severity::Warning,
284 fix: None,
285 },
286 LintWarning {
287 line: 2,
288 column: 1,
289 end_line: 2,
290 end_column: 5,
291 rule_name: Some("MD002".to_string()),
292 message: "Error severity".to_string(),
293 severity: Severity::Error,
294 fix: None,
295 },
296 LintWarning {
297 line: 3,
298 column: 1,
299 end_line: 3,
300 end_column: 5,
301 rule_name: Some("MD003".to_string()),
302 message: "Info severity".to_string(),
303 severity: Severity::Info,
304 fix: None,
305 },
306 ];
307
308 let output = formatter.format_warnings(&warnings, "test.md");
309 let lines: Vec<&str> = output.lines().collect();
310
311 assert!(lines[0].contains("type=warning"));
313 assert!(lines[1].contains("type=error"));
314 assert!(lines[2].contains("type=warning"));
315 }
316
317 #[test]
318 fn test_semicolons_in_parameters() {
319 let formatter = AzureFormatter::new();
320
321 let warnings = vec![LintWarning {
323 line: 1,
324 column: 1,
325 end_line: 1,
326 end_column: 5,
327 rule_name: Some("MD;001".to_string()), message: "Test message; with semicolon".to_string(),
329 severity: Severity::Warning,
330 fix: None,
331 }];
332
333 let output = formatter.format_warnings(&warnings, "file;with;semicolons.md");
334 assert_eq!(
336 output,
337 "##vso[task.logissue type=warning;sourcepath=file;with;semicolons.md;linenumber=1;columnnumber=1;code=MD;001]Test message; with semicolon"
338 );
339 }
340
341 #[test]
342 fn test_brackets_in_message() {
343 let formatter = AzureFormatter::new();
344
345 let warnings = vec![LintWarning {
347 line: 1,
348 column: 1,
349 end_line: 1,
350 end_column: 5,
351 rule_name: Some("MD001".to_string()),
352 message: "Message with [brackets] and ]unmatched".to_string(),
353 severity: Severity::Warning,
354 fix: None,
355 }];
356
357 let output = formatter.format_warnings(&warnings, "test.md");
358 assert_eq!(
359 output,
360 "##vso[task.logissue type=warning;sourcepath=test.md;linenumber=1;columnnumber=1;code=MD001]Message with [brackets] and ]unmatched"
361 );
362 }
363}