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