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 issues = apply_filters(issues, config);
63
64 let use_colors = config.color_choice.should_use_colors(std::io::stdout().is_terminal());
66
67 let mut buffer = if use_colors { Buffer::ansi() } else { Buffer::no_color() };
69
70 let editor_url = if use_colors { config.editor_url.as_deref() } else { None };
71 let files = DatabaseFiles { database, editor_url, line_hint: Cell::new(None), column_hint: Cell::new(None) };
72
73 let highest_level = issues.get_highest_level();
74 let mut errors = 0;
75 let mut warnings = 0;
76 let mut notes = 0;
77 let mut help = 0;
78 let mut suggestions = 0;
79
80 for issue in issues {
81 match &issue.level {
82 Level::Note => {
83 notes += 1;
84 }
85 Level::Help => {
86 help += 1;
87 }
88 Level::Warning => {
89 warnings += 1;
90 }
91 Level::Error => {
92 errors += 1;
93 }
94 }
95
96 if !issue.edits.is_empty() {
97 suggestions += 1;
98 }
99
100 if editor_url.is_some() {
102 if let Some(annotation) = issue.annotations.iter().find(|a| a.is_primary()) {
103 if let Ok(file) = database.get_ref(&annotation.span.file_id) {
104 let line = file.line_number(annotation.span.start.offset) + 1;
105 let column = file.column_number(annotation.span.start.offset) + 1;
106 files.line_hint.set(Some(line));
107 files.column_hint.set(Some(column));
108 }
109 } else {
110 files.line_hint.set(None);
111 files.column_hint.set(None);
112 }
113 }
114
115 let diagnostic: Diagnostic<FileId> = issue.into();
116
117 term::emit_to_write_style(&mut buffer, codespan_config, &files, &diagnostic)?;
118 }
119
120 if let Some(highest_level) = highest_level {
121 let total_issues = errors + warnings + notes + help;
122 let mut message_notes = vec![];
123 if errors > 0 {
124 message_notes.push(format!("{errors} error(s)"));
125 }
126
127 if warnings > 0 {
128 message_notes.push(format!("{warnings} warning(s)"));
129 }
130
131 if notes > 0 {
132 message_notes.push(format!("{notes} note(s)"));
133 }
134
135 if help > 0 {
136 message_notes.push(format!("{help} help message(s)"));
137 }
138
139 let mut diagnostic: Diagnostic<FileId> = Diagnostic::new(highest_level.into()).with_message(format!(
140 "found {} issues: {}",
141 total_issues,
142 message_notes.join(", ")
143 ));
144
145 if suggestions > 0 {
146 diagnostic = diagnostic.with_notes(vec![format!("{} issues contain auto-fix suggestions", suggestions)]);
147 }
148
149 term::emit_to_write_style(&mut buffer, codespan_config, &files, &diagnostic)?;
150 }
151
152 writer.write_all(buffer.as_slice())?;
154
155 Ok(())
156}
157
158fn apply_filters(issues: &IssueCollection, config: &FormatterConfig) -> IssueCollection {
159 let mut filtered = issues.clone();
160
161 if let Some(min_level) = config.minimum_level {
162 filtered = filtered.with_minimum_level(min_level);
163 }
164
165 if config.filter_fixable {
166 filtered = filtered.with_edits();
167 }
168
169 if config.sort {
170 filtered = filtered.sorted();
171 }
172
173 filtered
174}
175
176struct DatabaseFiles<'a> {
177 database: &'a ReadDatabase,
178 editor_url: Option<&'a str>,
179 line_hint: Cell<Option<u32>>,
180 column_hint: Cell<Option<u32>>,
181}
182
183impl<'a> Files<'a> for DatabaseFiles<'_> {
184 type FileId = FileId;
185 type Name = Cow<'a, str>;
186 type Source = &'a str;
187
188 fn name(&'a self, file_id: FileId) -> Result<Cow<'a, str>, Error> {
189 let file = self.database.get_ref(&file_id).map_err(|_| Error::FileMissing)?;
190 let name = file.name.as_ref();
191
192 if let (Some(template), Some(path)) = (self.editor_url, file.path.as_ref()) {
193 let abs_path = path.display().to_string();
194 let line = self.line_hint.get().unwrap_or(1);
195 let column = self.column_hint.get().unwrap_or(1);
196
197 Ok(Cow::Owned(osc8_hyperlink(template, &abs_path, line, column, name)))
198 } else {
199 Ok(Cow::Borrowed(name))
200 }
201 }
202
203 fn source(&'a self, file_id: FileId) -> Result<&'a str, Error> {
204 self.database.get_ref(&file_id).map(|source| source.contents.as_ref()).map_err(|_| Error::FileMissing)
205 }
206
207 fn line_index(&self, file_id: FileId, byte_index: usize) -> Result<usize, Error> {
208 let file = self.database.get_ref(&file_id).map_err(|_| Error::FileMissing)?;
209
210 Ok(file.line_number(
211 byte_index.try_into().map_err(|_| Error::IndexTooLarge { given: byte_index, max: u32::MAX as usize })?,
212 ) as usize)
213 }
214
215 fn line_range(&self, file_id: FileId, line_index: usize) -> Result<Range<usize>, Error> {
216 let file = self.database.get(&file_id).map_err(|_| Error::FileMissing)?;
217
218 codespan_line_range(&file.lines, file.size, line_index)
219 }
220}
221
222fn codespan_line_start(lines: &[u32], size: u32, line_index: usize) -> Result<usize, Error> {
223 match line_index.cmp(&lines.len()) {
224 Ordering::Less => Ok(lines.get(line_index).copied().expect("failed despite previous check") as usize),
225 Ordering::Equal => Ok(size as usize),
226 Ordering::Greater => Err(Error::LineTooLarge { given: line_index, max: lines.len() - 1 }),
227 }
228}
229
230fn codespan_line_range(lines: &[u32], size: u32, line_index: usize) -> Result<Range<usize>, Error> {
231 let line_start = codespan_line_start(lines, size, line_index)?;
232 let next_line_start = codespan_line_start(lines, size, line_index + 1)?;
233
234 Ok(line_start..next_line_start)
235}
236
237impl From<AnnotationKind> for LabelStyle {
238 fn from(kind: AnnotationKind) -> LabelStyle {
239 match kind {
240 AnnotationKind::Primary => LabelStyle::Primary,
241 AnnotationKind::Secondary => LabelStyle::Secondary,
242 }
243 }
244}
245
246impl From<Annotation> for Label<FileId> {
247 fn from(annotation: Annotation) -> Label<FileId> {
248 let mut label = Label::new(annotation.kind.into(), annotation.span.file_id, annotation.span);
249
250 if let Some(message) = annotation.message {
251 label.message = message;
252 }
253
254 label
255 }
256}
257
258impl From<Level> for Severity {
259 fn from(level: Level) -> Severity {
260 match level {
261 Level::Note => Severity::Note,
262 Level::Help => Severity::Help,
263 Level::Warning => Severity::Warning,
264 Level::Error => Severity::Error,
265 }
266 }
267}
268
269impl From<Issue> for Diagnostic<FileId> {
270 fn from(issue: Issue) -> Diagnostic<FileId> {
271 let mut diagnostic = Diagnostic::new(issue.level.into()).with_message(issue.message);
272
273 if let Some(code) = issue.code {
274 diagnostic.code = Some(code);
275 }
276
277 for annotation in issue.annotations {
278 diagnostic.labels.push(annotation.into());
279 }
280
281 for note in issue.notes {
282 diagnostic.notes.push(note);
283 }
284
285 if let Some(help) = issue.help {
286 diagnostic.notes.push(format!("Help: {help}"));
287 }
288
289 if let Some(link) = issue.link {
290 diagnostic.notes.push(format!("See: {link}"));
291 }
292
293 diagnostic
294 }
295}