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