1use anyhow::Result;
2
3use crate::{output::OutputFormatter, ConfigMetadata};
4
5use super::{LintOutput, OutputSummary};
6
7#[derive(Debug, Clone)]
17pub struct SimpleFormatter;
18
19impl OutputFormatter for SimpleFormatter {
20 fn id(&self) -> &'static str {
21 "simple"
22 }
23
24 fn format(&self, output: &[LintOutput], _metadata: &ConfigMetadata) -> Result<String> {
25 let mut result = String::new();
26 let mut written = false;
29
30 for output in output.iter() {
31 for error in output.errors.iter() {
32 written |= true;
33
34 result.push_str(&format!(
35 "{}:{}:{}: [{}] {}\n",
36 output.file_path,
37 error.location.start.row + 1,
38 error.location.start.column + 1,
39 error.level,
40 error.message,
41 ));
42 }
43 }
44
45 if written {
46 result.push('\n');
47 }
48 result.push_str(&self.format_summary(output));
49
50 Ok(result)
51 }
52
53 fn should_log_metadata(&self) -> bool {
54 true
55 }
56}
57
58impl SimpleFormatter {
59 fn format_summary(&self, output: &[LintOutput]) -> String {
60 let mut result = String::new();
61 let OutputSummary {
62 num_errors,
63 num_files,
64 num_warnings,
65 } = self.get_summary(output);
66
67 let diagnostic_message = match (num_errors, num_warnings) {
68 (0, 0) => "🟢 No errors or warnings found",
69 (0, num_warnings) => &format!(
70 "🟡 Found {} warning{}",
71 num_warnings,
72 if num_warnings != 1 { "s" } else { "" }
73 ),
74 (num_errors, 0) => &format!(
75 "🔴 Found {} error{}",
76 num_errors,
77 if num_errors != 1 { "s" } else { "" }
78 ),
79 (num_errors, num_warnings) => &format!(
80 "🔴 Found {} error{} and {} warning{}",
81 num_errors,
82 if num_errors != 1 { "s" } else { "" },
83 num_warnings,
84 if num_warnings != 1 { "s" } else { "" }
85 ),
86 };
87
88 result.push_str(&format!(
89 "🔍 {} source{} linted\n",
90 num_files,
91 if num_files != 1 { "s" } else { "" }
92 ));
93 result.push_str(&format!("{}\n", diagnostic_message));
94 result
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use crate::{
102 errors::{LintError, LintLevel},
103 location::DenormalizedLocation,
104 };
105
106 #[test]
107 fn test_simple_formatter() {
108 let file_path = "test.md".to_string();
109 let error = LintError::from_raw_location()
110 .rule("MockRule")
111 .level(LintLevel::Error)
112 .message("This is an error")
113 .location(DenormalizedLocation::dummy(0, 7, 0, 0, 1, 0))
114 .call();
115
116 let output = LintOutput {
117 file_path,
118 errors: vec![error],
119 };
120 let output = vec![output];
121
122 let formatter = SimpleFormatter;
123 let result = formatter
124 .format(&output, &ConfigMetadata::default())
125 .unwrap();
126 assert_eq!(
127 result,
128 "test.md:1:1: [ERROR] This is an error\n\n🔍 1 source linted\n🔴 Found 1 error\n"
129 );
130 }
131
132 #[test]
133 fn test_simple_formatter_warning() {
134 let file_path = "test.md".to_string();
135 let error = LintError::from_raw_location()
136 .rule("MockRule")
137 .level(LintLevel::Warning)
138 .message("This is a warning")
139 .location(DenormalizedLocation::dummy(0, 7, 0, 0, 1, 0))
140 .call();
141 let output = LintOutput {
142 file_path,
143 errors: vec![error],
144 };
145 let output = vec![output];
146
147 let formatter = SimpleFormatter;
148 let result = formatter
149 .format(&output, &ConfigMetadata::default())
150 .unwrap();
151 assert_eq!(
152 result,
153 "test.md:1:1: [WARN] This is a warning\n\n🔍 1 source linted\n🟡 Found 1 warning\n"
154 );
155 }
156
157 #[test]
158 fn test_simple_formatter_warning_and_error() {
159 let file_path = "test.md".to_string();
160 let error1 = LintError::from_raw_location()
161 .rule("MockRule")
162 .level(LintLevel::Error)
163 .message("This is an error")
164 .location(DenormalizedLocation::dummy(0, 7, 0, 0, 1, 0))
165 .call();
166 let error2 = LintError::from_raw_location()
167 .rule("MockRule")
168 .level(LintLevel::Warning)
169 .message("This is a warning")
170 .location(DenormalizedLocation::dummy(14, 46, 3, 0, 4, 2))
171 .call();
172 let output = LintOutput {
173 file_path,
174 errors: vec![error1, error2],
175 };
176 let output = vec![output];
177
178 let formatter = SimpleFormatter;
179 let result = formatter
180 .format(&output, &ConfigMetadata::default())
181 .unwrap();
182 assert_eq!(
183 result,
184 "test.md:1:1: [ERROR] This is an error\ntest.md:4:1: [WARN] This is a warning\n\n🔍 1 source linted\n🔴 Found 1 error and 1 warning\n"
185 );
186 }
187
188 #[test]
189 fn test_simple_formatter_no_errors() {
190 let file_path = "test.md".to_string();
191 let output = LintOutput {
192 file_path,
193 errors: vec![],
194 };
195 let output = vec![output];
196
197 let formatter = SimpleFormatter;
198 let result = formatter
199 .format(&output, &ConfigMetadata::default())
200 .unwrap();
201 assert_eq!(
202 result,
203 "🔍 1 source linted\n🟢 No errors or warnings found\n"
204 );
205 }
206
207 #[test]
208 fn test_simple_formatter_multiple_errors() {
209 let file_path = "test.md".to_string();
210 let error_1 = LintError::from_raw_location()
211 .rule("MockRule")
212 .level(LintLevel::Error)
213 .message("This is an error")
214 .location(DenormalizedLocation::dummy(0, 7, 0, 0, 1, 0))
215 .call();
216 let error_2 = LintError::from_raw_location()
217 .rule("MockRule")
218 .level(LintLevel::Error)
219 .message("This is another error")
220 .location(DenormalizedLocation::dummy(14, 46, 3, 0, 4, 2))
221 .call();
222
223 let output = LintOutput {
224 file_path,
225 errors: vec![error_1, error_2],
226 };
227 let output = vec![output];
228
229 let formatter = SimpleFormatter;
230 let result = formatter
231 .format(&output, &ConfigMetadata::default())
232 .unwrap();
233 assert_eq!(
234 result,
235 "test.md:1:1: [ERROR] This is an error\ntest.md:4:1: [ERROR] This is another error\n\n🔍 1 source linted\n🔴 Found 2 errors\n"
236 );
237 }
238
239 #[test]
240 fn test_simple_formatter_multiple_files() {
241 let file_path_1 = "test.md".to_string();
242 let error_1 = LintError::from_raw_location()
243 .rule("MockRule")
244 .level(LintLevel::Error)
245 .message("This is an error")
246 .location(DenormalizedLocation::dummy(0, 7, 0, 0, 1, 0))
247 .call();
248 let error_2 = LintError::from_raw_location()
249 .rule("MockRule")
250 .level(LintLevel::Error)
251 .message("This is another error")
252 .location(DenormalizedLocation::dummy(14, 46, 3, 0, 4, 2))
253 .call();
254
255 let output_1 = LintOutput {
256 file_path: file_path_1,
257 errors: vec![error_1, error_2],
258 };
259
260 let file_path_2 = "test2.md".to_string();
261 let error_3 = LintError::from_raw_location()
262 .rule("MockRule")
263 .level(LintLevel::Error)
264 .message("This is an error")
265 .location(DenormalizedLocation::dummy(0, 7, 0, 0, 1, 0))
266 .call();
267 let error_4 = LintError::from_raw_location()
268 .rule("MockRule")
269 .level(LintLevel::Error)
270 .message("This is another error")
271 .location(DenormalizedLocation::dummy(14, 46, 3, 0, 4, 2))
272 .call();
273
274 let output_2 = LintOutput {
275 file_path: file_path_2,
276 errors: vec![error_3, error_4],
277 };
278
279 let output = vec![output_1, output_2];
280
281 let formatter = SimpleFormatter;
282 let result = formatter
283 .format(&output, &ConfigMetadata::default())
284 .unwrap();
285 assert_eq!(
286 result,
287 "test.md:1:1: [ERROR] This is an error\ntest.md:4:1: [ERROR] This is another error\ntest2.md:1:1: [ERROR] This is an error\ntest2.md:4:1: [ERROR] This is another error\n\n🔍 2 sources linted\n🔴 Found 4 errors\n"
288 );
289 }
290}