Skip to main content

mago_reporting/formatter/
rich.rs

1use std::cmp::Ordering;
2use std::io::IsTerminal;
3use std::io::Write;
4use std::ops::Range;
5
6use codespan_reporting::diagnostic::Diagnostic;
7use codespan_reporting::diagnostic::Label;
8use codespan_reporting::diagnostic::LabelStyle;
9use codespan_reporting::diagnostic::Severity;
10use codespan_reporting::files::Error;
11use codespan_reporting::files::Files;
12use codespan_reporting::term;
13use codespan_reporting::term::Config;
14use codespan_reporting::term::DisplayStyle;
15use mago_database::file::FileId;
16use termcolor::Buffer;
17
18use mago_database::DatabaseReader;
19use mago_database::ReadDatabase;
20
21use crate::Annotation;
22use crate::AnnotationKind;
23use crate::Issue;
24use crate::IssueCollection;
25use crate::Level;
26use crate::error::ReportingError;
27use crate::formatter::Formatter;
28use crate::formatter::FormatterConfig;
29
30/// Formatter that outputs issues in rich diagnostic format with full context.
31pub(crate) struct RichFormatter;
32
33impl Formatter for RichFormatter {
34    fn format(
35        &self,
36        writer: &mut dyn Write,
37        issues: &IssueCollection,
38        database: &ReadDatabase,
39        config: &FormatterConfig,
40    ) -> Result<(), ReportingError> {
41        codespan_format_with_config(
42            writer,
43            issues,
44            database,
45            config,
46            &Config { display_style: DisplayStyle::Rich, ..Default::default() },
47        )
48    }
49}
50
51pub(super) fn codespan_format_with_config(
52    writer: &mut dyn Write,
53    issues: &IssueCollection,
54    database: &ReadDatabase,
55    config: &FormatterConfig,
56    codespan_config: &Config,
57) -> Result<(), ReportingError> {
58    // Apply filters
59    let issues = apply_filters(issues, config);
60
61    // Determine if we should use colors
62    let use_colors = config.color_choice.should_use_colors(std::io::stdout().is_terminal());
63
64    // Create a buffer for codespan (it requires WriteColor)
65    let mut buffer = if use_colors { Buffer::ansi() } else { Buffer::no_color() };
66
67    let files = DatabaseFiles(database);
68
69    let highest_level = issues.get_highest_level();
70    let mut errors = 0;
71    let mut warnings = 0;
72    let mut notes = 0;
73    let mut help = 0;
74    let mut suggestions = 0;
75
76    for issue in issues {
77        match &issue.level {
78            Level::Note => {
79                notes += 1;
80            }
81            Level::Help => {
82                help += 1;
83            }
84            Level::Warning => {
85                warnings += 1;
86            }
87            Level::Error => {
88                errors += 1;
89            }
90        }
91
92        if !issue.edits.is_empty() {
93            suggestions += 1;
94        }
95
96        let diagnostic: Diagnostic<FileId> = issue.into();
97
98        term::emit_to_write_style(&mut buffer, codespan_config, &files, &diagnostic)?;
99    }
100
101    if let Some(highest_level) = highest_level {
102        let total_issues = errors + warnings + notes + help;
103        let mut message_notes = vec![];
104        if errors > 0 {
105            message_notes.push(format!("{errors} error(s)"));
106        }
107
108        if warnings > 0 {
109            message_notes.push(format!("{warnings} warning(s)"));
110        }
111
112        if notes > 0 {
113            message_notes.push(format!("{notes} note(s)"));
114        }
115
116        if help > 0 {
117            message_notes.push(format!("{help} help message(s)"));
118        }
119
120        let mut diagnostic: Diagnostic<FileId> = Diagnostic::new(highest_level.into()).with_message(format!(
121            "found {} issues: {}",
122            total_issues,
123            message_notes.join(", ")
124        ));
125
126        if suggestions > 0 {
127            diagnostic = diagnostic.with_notes(vec![format!("{} issues contain auto-fix suggestions", suggestions)]);
128        }
129
130        term::emit_to_write_style(&mut buffer, codespan_config, &files, &diagnostic)?;
131    }
132
133    // Write buffer to writer
134    writer.write_all(buffer.as_slice())?;
135
136    Ok(())
137}
138
139fn apply_filters(issues: &IssueCollection, config: &FormatterConfig) -> IssueCollection {
140    let mut filtered = issues.clone();
141
142    if let Some(min_level) = config.minimum_level {
143        filtered = filtered.with_minimum_level(min_level);
144    }
145
146    if config.filter_fixable {
147        filtered = filtered.with_edits();
148    }
149
150    if config.sort {
151        filtered = filtered.sorted();
152    }
153
154    filtered
155}
156
157struct DatabaseFiles<'a>(&'a ReadDatabase);
158
159impl<'a> Files<'a> for DatabaseFiles<'_> {
160    type FileId = FileId;
161    type Name = &'a str;
162    type Source = &'a str;
163
164    fn name(&'a self, file_id: FileId) -> Result<&'a str, Error> {
165        self.0.get_ref(&file_id).map(|source| source.name.as_ref()).map_err(|_| Error::FileMissing)
166    }
167
168    fn source(&'a self, file_id: FileId) -> Result<&'a str, Error> {
169        self.0.get_ref(&file_id).map(|source| source.contents.as_ref()).map_err(|_| Error::FileMissing)
170    }
171
172    fn line_index(&self, file_id: FileId, byte_index: usize) -> Result<usize, Error> {
173        let file = self.0.get_ref(&file_id).map_err(|_| Error::FileMissing)?;
174
175        Ok(file.line_number(
176            byte_index.try_into().map_err(|_| Error::IndexTooLarge { given: byte_index, max: u32::MAX as usize })?,
177        ) as usize)
178    }
179
180    fn line_range(&self, file_id: FileId, line_index: usize) -> Result<Range<usize>, Error> {
181        let file = self.0.get(&file_id).map_err(|_| Error::FileMissing)?;
182
183        codespan_line_range(&file.lines, file.size, line_index)
184    }
185}
186
187fn codespan_line_start(lines: &[u32], size: u32, line_index: usize) -> Result<usize, Error> {
188    match line_index.cmp(&lines.len()) {
189        Ordering::Less => Ok(lines.get(line_index).copied().expect("failed despite previous check") as usize),
190        Ordering::Equal => Ok(size as usize),
191        Ordering::Greater => Err(Error::LineTooLarge { given: line_index, max: lines.len() - 1 }),
192    }
193}
194
195fn codespan_line_range(lines: &[u32], size: u32, line_index: usize) -> Result<Range<usize>, Error> {
196    let line_start = codespan_line_start(lines, size, line_index)?;
197    let next_line_start = codespan_line_start(lines, size, line_index + 1)?;
198
199    Ok(line_start..next_line_start)
200}
201
202impl From<AnnotationKind> for LabelStyle {
203    fn from(kind: AnnotationKind) -> LabelStyle {
204        match kind {
205            AnnotationKind::Primary => LabelStyle::Primary,
206            AnnotationKind::Secondary => LabelStyle::Secondary,
207        }
208    }
209}
210
211impl From<Annotation> for Label<FileId> {
212    fn from(annotation: Annotation) -> Label<FileId> {
213        let mut label = Label::new(annotation.kind.into(), annotation.span.file_id, annotation.span);
214
215        if let Some(message) = annotation.message {
216            label.message = message;
217        }
218
219        label
220    }
221}
222
223impl From<Level> for Severity {
224    fn from(level: Level) -> Severity {
225        match level {
226            Level::Note => Severity::Note,
227            Level::Help => Severity::Help,
228            Level::Warning => Severity::Warning,
229            Level::Error => Severity::Error,
230        }
231    }
232}
233
234impl From<Issue> for Diagnostic<FileId> {
235    fn from(issue: Issue) -> Diagnostic<FileId> {
236        let mut diagnostic = Diagnostic::new(issue.level.into()).with_message(issue.message);
237
238        if let Some(code) = issue.code {
239            diagnostic.code = Some(code);
240        }
241
242        for annotation in issue.annotations {
243            diagnostic.labels.push(annotation.into());
244        }
245
246        for note in issue.notes {
247            diagnostic.notes.push(note);
248        }
249
250        if let Some(help) = issue.help {
251            diagnostic.notes.push(format!("Help: {help}"));
252        }
253
254        if let Some(link) = issue.link {
255            diagnostic.notes.push(format!("See: {link}"));
256        }
257
258        diagnostic
259    }
260}