Skip to main content

lisette_diagnostics/
render.rs

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
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!(); // Blank line before first diagnostic
158    }
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}