mago_reporting/formatter/
rich.rs1use 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
30pub(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 let issues = apply_filters(issues, config);
60
61 let use_colors = config.color_choice.should_use_colors(std::io::stdout().is_terminal());
63
64 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 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}