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
32fn format_time(elapsed: Duration) -> String {
33 if elapsed.as_secs() >= 1 {
34 format!("{:.2}s", elapsed.as_secs_f64())
35 } else if elapsed.as_millis() > 0 {
36 format!("{}ms", elapsed.as_millis())
37 } else {
38 format!("{}μs", elapsed.as_micros())
39 }
40}
41
42pub fn print_summary(file_count: usize, elapsed: Duration, errors: i32, warnings: i32) {
43 let time_string = format_time(elapsed);
44 let use_color = std::env::var("NO_COLOR").is_err();
45 let time_display = if use_color {
46 format!("({})", time_string).dimmed().to_string()
47 } else {
48 format!("({})", time_string)
49 };
50 let files_str = if file_count == 1 {
51 "1 file"
52 } else {
53 &format!("{} files", file_count)
54 };
55
56 if errors == 0 && warnings == 0 {
57 eprintln!(" ✓ No issues · {} {}", files_str, time_display);
58 } else {
59 let mut parts = Vec::new();
60 if errors > 0 {
61 parts.push(if errors == 1 {
62 "1 error".to_string()
63 } else {
64 format!("{} errors", errors)
65 });
66 }
67 if warnings > 0 {
68 parts.push(if warnings == 1 {
69 "1 warning".to_string()
70 } else {
71 format!("{} warnings", warnings)
72 });
73 }
74 let findings = format!("Found {}", parts.join(", "));
75 let findings_display = if use_color {
76 format!("{}", findings.bold())
77 } else {
78 findings
79 };
80 eprintln!(" ✖ {} · {} {}", findings_display, files_str, time_display);
81 }
82}
83
84fn color_handler(highlight: Style) -> GraphicalReportHandler {
85 let theme = GraphicalTheme {
86 characters: ThemeCharacters {
87 error: "🔴".into(),
88 warning: "🟡".into(),
89 ..ThemeCharacters::unicode()
90 },
91 styles: ThemeStyles {
92 error: Style::new().red(),
93 warning: Style::new().yellow(),
94 link: Style::new(),
95 help: Style::new().dimmed(),
96 highlights: vec![highlight],
97 ..ThemeStyles::ansi()
98 },
99 };
100 GraphicalReportHandler::new_themed(theme).with_wrap_lines(false)
101}
102
103fn nocolor_handler() -> GraphicalReportHandler {
104 let theme = GraphicalTheme {
105 characters: ThemeCharacters {
106 error: "[error]".into(),
107 warning: "[warning]".into(),
108 ..ThemeCharacters::unicode()
109 },
110 styles: ThemeStyles::none(),
111 };
112 GraphicalReportHandler::new_themed(theme).with_wrap_lines(false)
113}
114
115fn render(
116 handler: &GraphicalReportHandler,
117 diagnostic: &LisetteDiagnostic,
118 source: &IndexedSource,
119 filename: &str,
120 use_color: bool,
121) {
122 let report = diagnostic
123 .clone()
124 .with_color(use_color)
125 .with_source_code(source.clone(), filename.to_string());
126 let mut output = String::new();
127 if handler.render_report(&mut output, report.as_ref()).is_ok() {
128 eprintln!("{}", output);
129 }
130}
131
132pub struct Counts {
133 pub files: usize,
134 pub errors: i32,
135 pub warnings: i32,
136}
137
138struct SourceCache<F> {
140 get_source: F,
141 default_source: IndexedSource,
142 default_filename: String,
143 cache: FxHashMap<u32, (IndexedSource, String)>,
144}
145
146impl<F: Fn(u32) -> Option<(String, String)>> SourceCache<F> {
147 fn new(get_source: F, default_source: &str, default_filename: &str) -> Self {
148 Self {
149 get_source,
150 default_source: IndexedSource::new(default_source),
151 default_filename: default_filename.to_string(),
152 cache: FxHashMap::default(),
153 }
154 }
155
156 fn get(&mut self, file_id: Option<u32>) -> (IndexedSource, String) {
157 let Some(fid) = file_id else {
158 return (self.default_source.clone(), self.default_filename.clone());
159 };
160 let default_source = &self.default_source;
161 let default_filename = &self.default_filename;
162 let get_source = &self.get_source;
163 let entry = self.cache.entry(fid).or_insert_with(|| {
164 get_source(fid)
165 .map(|(src, name)| (IndexedSource::new(&src), name))
166 .unwrap_or_else(|| (default_source.clone(), default_filename.clone()))
167 });
168 (entry.0.clone(), entry.1.clone())
169 }
170}
171
172fn partition_diagnostics<'a>(
173 errors: &'a [LisetteDiagnostic],
174 lints: &'a [LisetteDiagnostic],
175 filter: &Filter,
176) -> (Vec<&'a LisetteDiagnostic>, Vec<&'a LisetteDiagnostic>) {
177 let (errors, infer_warnings): (Vec<_>, Vec<_>) = if filter.show_errors() {
178 errors.iter().partition(|d| d.is_error())
179 } else {
180 (Vec::new(), Vec::new())
181 };
182
183 let warnings: Vec<_> = if filter.show_warnings() {
184 infer_warnings.into_iter().chain(lints.iter()).collect()
185 } else {
186 Vec::new()
187 };
188
189 (errors, warnings)
190}
191
192pub fn render_all(
193 errors: &[LisetteDiagnostic],
194 lints: &[LisetteDiagnostic],
195 get_source: impl Fn(u32) -> Option<(String, String)>,
196 file_count: usize,
197 filter: &Filter,
198 default_source: &str,
199 default_filename: &str,
200) -> Counts {
201 let (errors, warnings) = partition_diagnostics(errors, lints, filter);
202
203 let has_diagnostics = !errors.is_empty() || !warnings.is_empty();
204 if has_diagnostics {
205 eprintln!(); }
207
208 let use_color = std::env::var("NO_COLOR").is_err();
209 let mut sources = SourceCache::new(get_source, default_source, default_filename);
210
211 if !errors.is_empty() {
212 let handler = if use_color {
213 color_handler(Style::new().red())
214 } else {
215 nocolor_handler()
216 };
217 for error in &errors {
218 let (src, name) = sources.get(error.file_id());
219 render(&handler, error, &src, &name, use_color);
220 }
221 }
222
223 if !warnings.is_empty() {
224 let handler = if use_color {
225 color_handler(Style::new().yellow())
226 } else {
227 nocolor_handler()
228 };
229 for warning in &warnings {
230 let (src, name) = sources.get(warning.file_id());
231 render(&handler, warning, &src, &name, use_color);
232 }
233 }
234
235 Counts {
236 files: file_count.max(1),
237 errors: errors.len() as i32,
238 warnings: warnings.len() as i32,
239 }
240}
241
242pub fn unix_line(diagnostic: &LisetteDiagnostic, source: &IndexedSource, filename: &str) -> String {
244 let mut line = String::new();
245 if let Some(offset) = diagnostic.location_offset() {
246 let (lineno, col) = source.line_col(offset);
247 line.push_str(&format!("{}:{}:{}: ", filename, lineno, col));
248 }
249 line.push_str(diagnostic.severity_word());
250 line.push_str(": ");
251 line.push_str(diagnostic.plain_message());
252 if let Some(code) = diagnostic.code_str() {
253 line.push_str(&format!(" [{}]", code));
254 }
255 line
256}
257
258pub fn render_unix(
261 errors: &[LisetteDiagnostic],
262 lints: &[LisetteDiagnostic],
263 get_source: impl Fn(u32) -> Option<(String, String)>,
264 file_count: usize,
265 filter: &Filter,
266 default_source: &str,
267 default_filename: &str,
268) -> (String, Counts) {
269 let (errors, warnings) = partition_diagnostics(errors, lints, filter);
270
271 let mut sources = SourceCache::new(get_source, default_source, default_filename);
272 let mut output = String::new();
273 for diagnostic in errors.iter().chain(warnings.iter()) {
274 let (src, name) = sources.get(diagnostic.file_id());
275 output.push_str(&unix_line(diagnostic, &src, &name));
276 output.push('\n');
277 }
278
279 let counts = Counts {
280 files: file_count.max(1),
281 errors: errors.len() as i32,
282 warnings: warnings.len() as i32,
283 };
284 (output, counts)
285}