mago_reporting/internal/emitter/
codespan.rs1use std::cmp::Ordering;
2use std::ops::Range;
3
4use codespan_reporting::diagnostic::Diagnostic;
5use codespan_reporting::diagnostic::Label;
6use codespan_reporting::diagnostic::LabelStyle;
7use codespan_reporting::diagnostic::Severity;
8use codespan_reporting::files::Error;
9use codespan_reporting::files::Files;
10use codespan_reporting::term;
11use codespan_reporting::term::Config;
12use codespan_reporting::term::DisplayStyle;
13use mago_database::file::FileId;
14use termcolor::WriteColor;
15
16use mago_database::DatabaseReader;
17use mago_database::ReadDatabase;
18
19use crate::Annotation;
20use crate::AnnotationKind;
21use crate::Issue;
22use crate::IssueCollection;
23use crate::Level;
24use crate::error::ReportingError;
25
26pub fn rich_format(
27 writer: &mut dyn WriteColor,
28 database: &ReadDatabase,
29 issues: IssueCollection,
30) -> Result<Option<Level>, ReportingError> {
31 codespan_format_with_config(
32 writer,
33 database,
34 issues,
35 Config { display_style: DisplayStyle::Rich, ..Default::default() },
36 )
37}
38
39pub fn medium_format(
40 writer: &mut dyn WriteColor,
41 database: &ReadDatabase,
42 issues: IssueCollection,
43) -> Result<Option<Level>, ReportingError> {
44 codespan_format_with_config(
45 writer,
46 database,
47 issues,
48 Config { display_style: DisplayStyle::Medium, ..Default::default() },
49 )
50}
51
52pub fn short_format(
53 writer: &mut dyn WriteColor,
54 database: &ReadDatabase,
55 issues: IssueCollection,
56) -> Result<Option<Level>, ReportingError> {
57 codespan_format_with_config(
58 writer,
59 database,
60 issues,
61 Config { display_style: DisplayStyle::Short, ..Default::default() },
62 )
63}
64
65fn codespan_format_with_config(
66 writer: &mut dyn WriteColor,
67 database: &ReadDatabase,
68 issues: IssueCollection,
69 config: Config,
70) -> Result<Option<Level>, ReportingError> {
71 let files = DatabaseFiles(database);
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.suggestions.is_empty() {
97 suggestions += 1;
98 }
99
100 let diagnostic: Diagnostic<FileId> = issue.into();
101
102 term::emit(writer, &config, &files, &diagnostic)?;
103 }
104
105 if let Some(highest_level) = highest_level {
106 let total_issues = errors + warnings + notes + help;
107 let mut message_notes = vec![];
108 if errors > 0 {
109 message_notes.push(format!("{errors} error(s)"));
110 }
111
112 if warnings > 0 {
113 message_notes.push(format!("{warnings} warning(s)"));
114 }
115
116 if notes > 0 {
117 message_notes.push(format!("{notes} note(s)"));
118 }
119
120 if help > 0 {
121 message_notes.push(format!("{help} help message(s)"));
122 }
123
124 let mut diagnostic: Diagnostic<FileId> = Diagnostic::new(highest_level.into()).with_message(format!(
125 "found {} issues: {}",
126 total_issues,
127 message_notes.join(", ")
128 ));
129
130 if suggestions > 0 {
131 diagnostic = diagnostic.with_notes(vec![format!("{} issues contain auto-fix suggestions", suggestions)]);
132 }
133
134 term::emit(writer, &config, &files, &diagnostic)?;
135 }
136
137 Ok(highest_level)
138}
139
140struct DatabaseFiles<'a>(&'a ReadDatabase);
141
142impl<'a> Files<'a> for DatabaseFiles<'_> {
143 type FileId = FileId;
144 type Name = &'a str;
145 type Source = &'a str;
146
147 fn name(&'a self, file_id: FileId) -> Result<&'a str, Error> {
148 self.0.get_ref(&file_id).map(|source| source.name.as_ref()).map_err(|_| Error::FileMissing)
149 }
150
151 fn source(&'a self, file_id: FileId) -> Result<&'a str, Error> {
152 self.0.get_ref(&file_id).map(|source| source.contents.as_ref()).map_err(|_| Error::FileMissing)
153 }
154
155 fn line_index(&self, file_id: FileId, byte_index: usize) -> Result<usize, Error> {
156 let file = self.0.get_ref(&file_id).map_err(|_| Error::FileMissing)?;
157
158 Ok(file.line_number(
159 byte_index.try_into().map_err(|_| Error::IndexTooLarge { given: byte_index, max: u32::MAX as usize })?,
160 ) as usize)
161 }
162
163 fn line_range(&self, file_id: FileId, line_index: usize) -> Result<Range<usize>, Error> {
164 let file = self.0.get(&file_id).map_err(|_| Error::FileMissing)?;
165
166 codespan_line_range(&file.lines, file.size, line_index)
167 }
168}
169
170fn codespan_line_start(lines: &[u32], size: u32, line_index: usize) -> Result<usize, Error> {
171 match line_index.cmp(&lines.len()) {
172 Ordering::Less => Ok(lines.get(line_index).cloned().expect("failed despite previous check") as usize),
173 Ordering::Equal => Ok(size as usize),
174 Ordering::Greater => Err(Error::LineTooLarge { given: line_index, max: lines.len() - 1 }),
175 }
176}
177
178fn codespan_line_range(lines: &[u32], size: u32, line_index: usize) -> Result<Range<usize>, Error> {
179 let line_start = codespan_line_start(lines, size, line_index)?;
180 let next_line_start = codespan_line_start(lines, size, line_index + 1)?;
181
182 Ok(line_start..next_line_start)
183}
184
185impl From<AnnotationKind> for LabelStyle {
186 fn from(kind: AnnotationKind) -> LabelStyle {
187 match kind {
188 AnnotationKind::Primary => LabelStyle::Primary,
189 AnnotationKind::Secondary => LabelStyle::Secondary,
190 }
191 }
192}
193
194impl From<Annotation> for Label<FileId> {
195 fn from(annotation: Annotation) -> Label<FileId> {
196 let mut label = Label::new(annotation.kind.into(), annotation.span.file_id, annotation.span);
197
198 if let Some(message) = annotation.message {
199 label.message = message;
200 }
201
202 label
203 }
204}
205
206impl From<Level> for Severity {
207 fn from(level: Level) -> Severity {
208 match level {
209 Level::Note => Severity::Note,
210 Level::Help => Severity::Help,
211 Level::Warning => Severity::Warning,
212 Level::Error => Severity::Error,
213 }
214 }
215}
216
217impl From<Issue> for Diagnostic<FileId> {
218 fn from(issue: Issue) -> Diagnostic<FileId> {
219 let mut diagnostic = Diagnostic::new(issue.level.into()).with_message(issue.message);
220
221 if let Some(code) = issue.code {
222 diagnostic.code = Some(code);
223 }
224
225 for annotation in issue.annotations {
226 diagnostic.labels.push(annotation.into());
227 }
228
229 for note in issue.notes {
230 diagnostic.notes.push(note);
231 }
232
233 if let Some(help) = issue.help {
234 diagnostic.notes.push(format!("Help: {help}"));
235 }
236
237 if let Some(link) = issue.link {
238 diagnostic.notes.push(format!("See: {link}"));
239 }
240
241 diagnostic
242 }
243}