1use std::time::Duration;
2
3use rustc_hash::FxHashMap;
4
5use crate::LisetteDiagnostic;
6use crate::diagnostic::IndexedSource;
7use miette::{GraphicalReportHandler, GraphicalTheme, ThemeCharacters, ThemeStyles};
8use owo_colors::{OwoColorize, Style};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
11pub enum OutputFormat {
12 #[default]
13 Graphical,
14 Unix,
15}
16
17pub struct Filter {
18 pub errors_only: bool,
19 pub warnings_only: bool,
20}
21
22impl Filter {
23 pub fn show_errors(&self) -> bool {
24 !self.warnings_only
25 }
26
27 pub fn show_warnings(&self) -> bool {
28 !self.errors_only
29 }
30
31 pub fn show_info(&self) -> bool {
32 !self.errors_only && !self.warnings_only
33 }
34}
35
36fn format_time(elapsed: Duration) -> String {
37 if elapsed.as_secs() >= 1 {
38 format!("{:.2}s", elapsed.as_secs_f64())
39 } else if elapsed.as_millis() > 0 {
40 format!("{}ms", elapsed.as_millis())
41 } else {
42 format!("{}μs", elapsed.as_micros())
43 }
44}
45
46pub fn print_summary(file_count: usize, elapsed: Duration, errors: i32, warnings: i32, info: i32) {
47 let time_string = format_time(elapsed);
48 let use_color = std::env::var("NO_COLOR").is_err();
49 let time_display = if use_color {
50 format!("({})", time_string).dimmed().to_string()
51 } else {
52 format!("({})", time_string)
53 };
54 let files_str = if file_count == 1 {
55 "1 file"
56 } else {
57 &format!("{} files", file_count)
58 };
59
60 if errors == 0 && warnings == 0 && info == 0 {
61 eprintln!(" ✓ No issues · {} {}", files_str, time_display);
62 } else {
63 let mut parts = Vec::new();
64 if errors > 0 {
65 parts.push(if errors == 1 {
66 "1 error".to_string()
67 } else {
68 format!("{} errors", errors)
69 });
70 }
71 if warnings > 0 {
72 parts.push(if warnings == 1 {
73 "1 warning".to_string()
74 } else {
75 format!("{} warnings", warnings)
76 });
77 }
78 if info > 0 {
79 parts.push(if info == 1 {
80 "1 advisory".to_string()
81 } else {
82 format!("{} advisories", info)
83 });
84 }
85 let findings = format!("Found {}", parts.join(", "));
86 let findings_display = if use_color {
87 format!("{}", findings.bold())
88 } else {
89 findings
90 };
91 eprintln!(" ✖ {} · {} {}", findings_display, files_str, time_display);
92 }
93}
94
95fn color_handler(highlight: Style) -> GraphicalReportHandler {
96 let theme = GraphicalTheme {
97 characters: ThemeCharacters {
98 error: "🔴".into(),
99 warning: "🟡".into(),
100 advice: "🔵".into(),
101 ..ThemeCharacters::unicode()
102 },
103 styles: ThemeStyles {
104 error: Style::new().red(),
105 warning: Style::new().yellow(),
106 advice: Style::new().blue(),
107 link: Style::new(),
108 help: Style::new().dimmed(),
109 highlights: vec![highlight],
110 ..ThemeStyles::ansi()
111 },
112 };
113 GraphicalReportHandler::new_themed(theme).with_wrap_lines(false)
114}
115
116fn nocolor_handler() -> GraphicalReportHandler {
117 let theme = GraphicalTheme {
118 characters: ThemeCharacters {
119 error: "[error]".into(),
120 warning: "[warning]".into(),
121 advice: "[info]".into(),
122 ..ThemeCharacters::unicode()
123 },
124 styles: ThemeStyles::none(),
125 };
126 GraphicalReportHandler::new_themed(theme).with_wrap_lines(false)
127}
128
129fn render(
130 handler: &GraphicalReportHandler,
131 diagnostic: &LisetteDiagnostic,
132 source: &IndexedSource,
133 filename: &str,
134 use_color: bool,
135) {
136 let report = diagnostic
137 .clone()
138 .with_color(use_color)
139 .with_source_code(source.clone(), filename.to_string());
140 let mut output = String::new();
141 if handler.render_report(&mut output, report.as_ref()).is_ok() {
142 eprintln!("{}", output);
143 }
144}
145
146fn render_group<F: Fn(u32) -> Option<(String, String)>>(
147 diagnostics: &[&LisetteDiagnostic],
148 highlight: Style,
149 use_color: bool,
150 sources: &mut SourceCache<F>,
151) {
152 if diagnostics.is_empty() {
153 return;
154 }
155 let handler = if use_color {
156 color_handler(highlight)
157 } else {
158 nocolor_handler()
159 };
160 for diagnostic in diagnostics {
161 let (src, name) = sources.get(diagnostic.file_id());
162 render(&handler, diagnostic, &src, &name, use_color);
163 }
164}
165
166pub struct Counts {
167 pub files: usize,
168 pub errors: i32,
169 pub warnings: i32,
170 pub info: i32,
171}
172
173struct SourceCache<F> {
175 get_source: F,
176 default_source: IndexedSource,
177 default_filename: String,
178 cache: FxHashMap<u32, (IndexedSource, String)>,
179}
180
181impl<F: Fn(u32) -> Option<(String, String)>> SourceCache<F> {
182 fn new(get_source: F, default_source: &str, default_filename: &str) -> Self {
183 Self {
184 get_source,
185 default_source: IndexedSource::new(default_source),
186 default_filename: default_filename.to_string(),
187 cache: FxHashMap::default(),
188 }
189 }
190
191 fn get(&mut self, file_id: Option<u32>) -> (IndexedSource, String) {
192 let Some(fid) = file_id else {
193 return (self.default_source.clone(), self.default_filename.clone());
194 };
195 let default_source = &self.default_source;
196 let default_filename = &self.default_filename;
197 let get_source = &self.get_source;
198 let entry = self.cache.entry(fid).or_insert_with(|| {
199 get_source(fid)
200 .map(|(src, name)| (IndexedSource::new(&src), name))
201 .unwrap_or_else(|| (default_source.clone(), default_filename.clone()))
202 });
203 (entry.0.clone(), entry.1.clone())
204 }
205}
206
207fn partition_diagnostics<'a>(
208 errors: &'a [LisetteDiagnostic],
209 lints: &'a [LisetteDiagnostic],
210 filter: &Filter,
211) -> (
212 Vec<&'a LisetteDiagnostic>,
213 Vec<&'a LisetteDiagnostic>,
214 Vec<&'a LisetteDiagnostic>,
215) {
216 let mut error_bucket = Vec::new();
217 let mut warning_bucket = Vec::new();
218 let mut info_bucket = Vec::new();
219
220 for diagnostic in errors.iter().chain(lints.iter()) {
221 if diagnostic.is_error() {
222 if filter.show_errors() {
223 error_bucket.push(diagnostic);
224 }
225 } else if diagnostic.is_info() {
226 if filter.show_info() {
227 info_bucket.push(diagnostic);
228 }
229 } else if filter.show_warnings() {
230 warning_bucket.push(diagnostic);
231 }
232 }
233
234 (error_bucket, warning_bucket, info_bucket)
235}
236
237pub fn render_all(
238 errors: &[LisetteDiagnostic],
239 lints: &[LisetteDiagnostic],
240 get_source: impl Fn(u32) -> Option<(String, String)>,
241 file_count: usize,
242 filter: &Filter,
243 default_source: &str,
244 default_filename: &str,
245) -> Counts {
246 let (errors, warnings, info) = partition_diagnostics(errors, lints, filter);
247
248 let has_diagnostics = !errors.is_empty() || !warnings.is_empty() || !info.is_empty();
249 if has_diagnostics {
250 eprintln!(); }
252
253 let use_color = std::env::var("NO_COLOR").is_err();
254 let mut sources = SourceCache::new(get_source, default_source, default_filename);
255
256 render_group(&errors, Style::new().red(), use_color, &mut sources);
257 render_group(&warnings, Style::new().yellow(), use_color, &mut sources);
258 render_group(&info, Style::new().blue(), use_color, &mut sources);
259
260 Counts {
261 files: file_count.max(1),
262 errors: errors.len() as i32,
263 warnings: warnings.len() as i32,
264 info: info.len() as i32,
265 }
266}
267
268pub fn unix_line(diagnostic: &LisetteDiagnostic, source: &IndexedSource, filename: &str) -> String {
270 let mut line = String::new();
271 if let Some(offset) = diagnostic.location_offset() {
272 let (lineno, col) = source.line_col(offset);
273 line.push_str(&format!("{}:{}:{}: ", filename, lineno, col));
274 }
275 line.push_str(diagnostic.severity_word());
276 line.push_str(": ");
277 line.push_str(diagnostic.plain_message());
278 if let Some(code) = diagnostic.code_str() {
279 line.push_str(&format!(" [{}]", code));
280 }
281 line
282}
283
284pub fn render_unix(
287 errors: &[LisetteDiagnostic],
288 lints: &[LisetteDiagnostic],
289 get_source: impl Fn(u32) -> Option<(String, String)>,
290 file_count: usize,
291 filter: &Filter,
292 default_source: &str,
293 default_filename: &str,
294) -> (String, Counts) {
295 let (errors, warnings, info) = partition_diagnostics(errors, lints, filter);
296
297 let mut sources = SourceCache::new(get_source, default_source, default_filename);
298 let mut output = String::new();
299 for diagnostic in errors.iter().chain(warnings.iter()).chain(info.iter()) {
300 let (src, name) = sources.get(diagnostic.file_id());
301 output.push_str(&unix_line(diagnostic, &src, &name));
302 output.push('\n');
303 }
304
305 let counts = Counts {
306 files: file_count.max(1),
307 errors: errors.len() as i32,
308 warnings: warnings.len() as i32,
309 info: info.len() as i32,
310 };
311 (output, counts)
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317
318 fn show_all() -> Filter {
319 Filter {
320 errors_only: false,
321 warnings_only: false,
322 }
323 }
324
325 #[test]
326 fn each_severity_lands_in_its_own_bucket() {
327 let errors = vec![LisetteDiagnostic::error("e")];
328 let lints = vec![LisetteDiagnostic::warn("w"), LisetteDiagnostic::info("i")];
329 let (errors, warnings, info) = partition_diagnostics(&errors, &lints, &show_all());
330 assert_eq!(errors.len(), 1);
331 assert_eq!(warnings.len(), 1);
332 assert_eq!(info.len(), 1);
333 }
334
335 #[test]
336 fn info_hidden_under_errors_only() {
337 let empty: Vec<LisetteDiagnostic> = Vec::new();
338 let lints = vec![LisetteDiagnostic::info("i")];
339 let filter = Filter {
340 errors_only: true,
341 warnings_only: false,
342 };
343 let (_, _, info) = partition_diagnostics(&empty, &lints, &filter);
344 assert!(info.is_empty());
345 }
346
347 #[test]
348 fn info_hidden_under_warnings_only() {
349 let empty: Vec<LisetteDiagnostic> = Vec::new();
350 let lints = vec![LisetteDiagnostic::info("i")];
351 let filter = Filter {
352 errors_only: false,
353 warnings_only: true,
354 };
355 let (_, _, info) = partition_diagnostics(&empty, &lints, &filter);
356 assert!(info.is_empty());
357 }
358
359 #[test]
360 fn unix_counts_and_labels_info_separately() {
361 let empty: Vec<LisetteDiagnostic> = Vec::new();
362 let lints = vec![LisetteDiagnostic::info("advisory")];
363 let (output, counts) = render_unix(&empty, &lints, |_| None, 1, &show_all(), "", "f.lis");
364 assert_eq!(counts.errors, 0);
365 assert_eq!(counts.warnings, 0);
366 assert_eq!(counts.info, 1);
367 assert!(output.contains("info: advisory"));
368 }
369}