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_by_id(&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_by_id(&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_by_id(&file_id).map_err(|_| Error::FileMissing)?;
157
158 Ok(file.line_number(byte_index))
159 }
160
161 fn line_range(&self, file_id: FileId, line_index: usize) -> Result<Range<usize>, Error> {
162 let file = self.0.get_by_id(&file_id).map_err(|_| Error::FileMissing)?;
163
164 codespan_line_range(&file.lines, file.size, line_index)
165 }
166}
167
168fn codespan_line_start(lines: &[usize], size: usize, line_index: usize) -> Result<usize, Error> {
169 match line_index.cmp(&lines.len()) {
170 Ordering::Less => Ok(lines.get(line_index).cloned().expect("failed despite previous check")),
171 Ordering::Equal => Ok(size),
172 Ordering::Greater => Err(Error::LineTooLarge { given: line_index, max: lines.len() - 1 }),
173 }
174}
175
176fn codespan_line_range(lines: &[usize], size: usize, line_index: usize) -> Result<Range<usize>, Error> {
177 let line_start = codespan_line_start(lines, size, line_index)?;
178 let next_line_start = codespan_line_start(lines, size, line_index + 1)?;
179
180 Ok(line_start..next_line_start)
181}
182
183impl From<AnnotationKind> for LabelStyle {
184 fn from(kind: AnnotationKind) -> LabelStyle {
185 match kind {
186 AnnotationKind::Primary => LabelStyle::Primary,
187 AnnotationKind::Secondary => LabelStyle::Secondary,
188 }
189 }
190}
191
192impl From<Annotation> for Label<FileId> {
193 fn from(annotation: Annotation) -> Label<FileId> {
194 let mut label = Label::new(annotation.kind.into(), annotation.span.start.file_id, annotation.span);
195
196 if let Some(message) = annotation.message {
197 label.message = message;
198 }
199
200 label
201 }
202}
203
204impl From<Level> for Severity {
205 fn from(level: Level) -> Severity {
206 match level {
207 Level::Note => Severity::Note,
208 Level::Help => Severity::Help,
209 Level::Warning => Severity::Warning,
210 Level::Error => Severity::Error,
211 }
212 }
213}
214
215impl From<Issue> for Diagnostic<FileId> {
216 fn from(issue: Issue) -> Diagnostic<FileId> {
217 let mut diagnostic = Diagnostic::new(issue.level.into()).with_message(issue.message);
218
219 if let Some(code) = issue.code {
220 diagnostic.code = Some(code);
221 }
222
223 for annotation in issue.annotations {
224 diagnostic.labels.push(annotation.into());
225 }
226
227 for note in issue.notes {
228 diagnostic.notes.push(note);
229 }
230
231 if let Some(help) = issue.help {
232 diagnostic.notes.push(format!("Help: {help}"));
233 }
234
235 if let Some(link) = issue.link {
236 diagnostic.notes.push(format!("See: {link}"));
237 }
238
239 diagnostic
240 }
241}