lisette_diagnostics/
render.rs1use 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
10pub struct Filter {
11 pub errors_only: bool,
12 pub warnings_only: bool,
13}
14
15impl Filter {
16 pub fn show_errors(&self) -> bool {
17 !self.warnings_only
18 }
19
20 pub fn show_warnings(&self) -> bool {
21 !self.errors_only
22 }
23}
24
25fn format_time(elapsed: Duration) -> String {
26 if elapsed.as_secs() >= 1 {
27 format!("{:.2}s", elapsed.as_secs_f64())
28 } else if elapsed.as_millis() > 0 {
29 format!("{}ms", elapsed.as_millis())
30 } else {
31 format!("{}μs", elapsed.as_micros())
32 }
33}
34
35pub fn print_summary(file_count: usize, elapsed: Duration, errors: i32, warnings: i32) {
36 let time_string = format_time(elapsed);
37 let use_color = std::env::var("NO_COLOR").is_err();
38 let time_display = if use_color {
39 format!("({})", time_string).dimmed().to_string()
40 } else {
41 format!("({})", time_string)
42 };
43 let files_str = if file_count == 1 {
44 "1 file"
45 } else {
46 &format!("{} files", file_count)
47 };
48
49 if errors == 0 && warnings == 0 {
50 eprintln!(" ✓ No issues · {} {}", files_str, time_display);
51 } else {
52 let mut parts = Vec::new();
53 if errors > 0 {
54 parts.push(if errors == 1 {
55 "1 error".to_string()
56 } else {
57 format!("{} errors", errors)
58 });
59 }
60 if warnings > 0 {
61 parts.push(if warnings == 1 {
62 "1 warning".to_string()
63 } else {
64 format!("{} warnings", warnings)
65 });
66 }
67 let findings = format!("Found {}", parts.join(", "));
68 let findings_display = if use_color {
69 format!("{}", findings.bold())
70 } else {
71 findings
72 };
73 eprintln!(" ✖ {} · {} {}", findings_display, files_str, time_display);
74 }
75}
76
77fn color_handler(highlight: Style) -> GraphicalReportHandler {
78 let theme = GraphicalTheme {
79 characters: ThemeCharacters {
80 error: "🔴".into(),
81 warning: "🟡".into(),
82 ..ThemeCharacters::unicode()
83 },
84 styles: ThemeStyles {
85 error: Style::new().red(),
86 warning: Style::new().yellow(),
87 link: Style::new(),
88 help: Style::new().dimmed(),
89 highlights: vec![highlight],
90 ..ThemeStyles::ansi()
91 },
92 };
93 GraphicalReportHandler::new_themed(theme).with_wrap_lines(false)
94}
95
96fn nocolor_handler() -> GraphicalReportHandler {
97 let theme = GraphicalTheme {
98 characters: ThemeCharacters {
99 error: "[error]".into(),
100 warning: "[warning]".into(),
101 ..ThemeCharacters::unicode()
102 },
103 styles: ThemeStyles::none(),
104 };
105 GraphicalReportHandler::new_themed(theme).with_wrap_lines(false)
106}
107
108fn render(
109 handler: &GraphicalReportHandler,
110 diagnostic: &LisetteDiagnostic,
111 source: &IndexedSource,
112 filename: &str,
113 use_color: bool,
114) {
115 let report = diagnostic
116 .clone()
117 .with_color(use_color)
118 .with_source_code(source.clone(), filename.to_string());
119 let mut output = String::new();
120 if handler.render_report(&mut output, report.as_ref()).is_ok() {
121 eprintln!("{}", output);
122 }
123}
124
125pub struct Counts {
126 pub files: usize,
127 pub errors: i32,
128 pub warnings: i32,
129}
130
131pub fn render_all(
132 errors: &[LisetteDiagnostic],
133 lints: &[LisetteDiagnostic],
134 get_source: impl Fn(u32) -> Option<(String, String)>,
135 file_count: usize,
136 filter: &Filter,
137 default_source: &str,
138 default_filename: &str,
139) -> Counts {
140 let show_errors = filter.show_errors();
141 let show_warnings = filter.show_warnings();
142
143 let (errors, infer_warnings): (Vec<_>, Vec<_>) = if show_errors {
144 errors.iter().partition(|d| d.is_error())
145 } else {
146 (Vec::new(), Vec::new())
147 };
148
149 let warnings: Vec<_> = if show_warnings {
150 infer_warnings.into_iter().chain(lints.iter()).collect()
151 } else {
152 Vec::new()
153 };
154
155 let has_diagnostics = !errors.is_empty() || !warnings.is_empty();
156 if has_diagnostics {
157 eprintln!(); }
159
160 let use_color = std::env::var("NO_COLOR").is_err();
161
162 let default_source = IndexedSource::new(default_source);
163 let default_filename = default_filename.to_string();
164 let mut source_cache: FxHashMap<u32, (IndexedSource, String)> = FxHashMap::default();
165 let get_cached_source =
166 |file_id: Option<u32>, cache: &mut FxHashMap<u32, (IndexedSource, String)>| {
167 if let Some(fid) = file_id {
168 let entry = cache.entry(fid).or_insert_with(|| {
169 get_source(fid)
170 .map(|(src, name)| (IndexedSource::new(&src), name))
171 .unwrap_or_else(|| (default_source.clone(), default_filename.clone()))
172 });
173 (entry.0.clone(), entry.1.clone())
174 } else {
175 (default_source.clone(), default_filename.clone())
176 }
177 };
178
179 if !errors.is_empty() {
180 let handler = if use_color {
181 color_handler(Style::new().red())
182 } else {
183 nocolor_handler()
184 };
185 for error in &errors {
186 let (src, name) = get_cached_source(error.file_id(), &mut source_cache);
187 render(&handler, error, &src, &name, use_color);
188 }
189 }
190
191 if !warnings.is_empty() {
192 let handler = if use_color {
193 color_handler(Style::new().yellow())
194 } else {
195 nocolor_handler()
196 };
197 for warning in &warnings {
198 let (src, name) = get_cached_source(warning.file_id(), &mut source_cache);
199 render(&handler, warning, &src, &name, use_color);
200 }
201 }
202
203 Counts {
204 files: file_count.max(1),
205 errors: errors.len() as i32,
206 warnings: warnings.len() as i32,
207 }
208}