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