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 termcolor::WriteColor;
14
15use mago_interner::ThreadedInterner;
16use mago_source::SourceIdentifier;
17use mago_source::SourceManager;
18use mago_source::error::SourceError;
19
20use crate::Annotation;
21use crate::AnnotationKind;
22use crate::Issue;
23use crate::IssueCollection;
24use crate::Level;
25use crate::error::ReportingError;
26
27pub fn rich_format(
28 writer: &mut dyn WriteColor,
29 sources: &SourceManager,
30 interner: &ThreadedInterner,
31 issues: IssueCollection,
32) -> Result<Option<Level>, ReportingError> {
33 codespan_format_with_config(
34 writer,
35 sources,
36 interner,
37 issues,
38 Config { display_style: DisplayStyle::Rich, ..Default::default() },
39 )
40}
41
42pub fn medium_format(
43 writer: &mut dyn WriteColor,
44 sources: &SourceManager,
45 interner: &ThreadedInterner,
46 issues: IssueCollection,
47) -> Result<Option<Level>, ReportingError> {
48 codespan_format_with_config(
49 writer,
50 sources,
51 interner,
52 issues,
53 Config { display_style: DisplayStyle::Medium, ..Default::default() },
54 )
55}
56
57pub fn short_format(
58 writer: &mut dyn WriteColor,
59 sources: &SourceManager,
60 interner: &ThreadedInterner,
61 issues: IssueCollection,
62) -> Result<Option<Level>, ReportingError> {
63 codespan_format_with_config(
64 writer,
65 sources,
66 interner,
67 issues,
68 Config { display_style: DisplayStyle::Short, ..Default::default() },
69 )
70}
71
72fn codespan_format_with_config(
73 writer: &mut dyn WriteColor,
74 sources: &SourceManager,
75 interner: &ThreadedInterner,
76 issues: IssueCollection,
77 config: Config,
78) -> Result<Option<Level>, ReportingError> {
79 let files = SourceManagerFile(sources, interner);
80
81 let highest_level = issues.get_highest_level();
82 let mut errors = 0;
83 let mut warnings = 0;
84 let mut notes = 0;
85 let mut help = 0;
86 let mut suggestions = 0;
87
88 for issue in issues {
89 match &issue.level {
90 Level::Note => {
91 notes += 1;
92 }
93 Level::Help => {
94 help += 1;
95 }
96 Level::Warning => {
97 warnings += 1;
98 }
99 Level::Error => {
100 errors += 1;
101 }
102 }
103
104 if !issue.suggestions.is_empty() {
105 suggestions += 1;
106 }
107
108 let diagnostic: Diagnostic<SourceIdentifier> = issue.into();
109
110 term::emit(writer, &config, &files, &diagnostic)?;
111 }
112
113 if let Some(highest_level) = highest_level {
114 let total_issues = errors + warnings + notes + help;
115 let mut message_notes = vec![];
116 if errors > 0 {
117 message_notes.push(format!("{} error(s)", errors));
118 }
119
120 if warnings > 0 {
121 message_notes.push(format!("{} warning(s)", warnings));
122 }
123
124 if notes > 0 {
125 message_notes.push(format!("{} note(s)", notes));
126 }
127
128 if help > 0 {
129 message_notes.push(format!("{} help message(s)", help));
130 }
131
132 let mut diagnostic: Diagnostic<SourceIdentifier> = Diagnostic::new(highest_level.into()).with_message(format!(
133 "found {} issues: {}",
134 total_issues,
135 message_notes.join(", ")
136 ));
137
138 if suggestions > 0 {
139 diagnostic = diagnostic.with_notes(vec![format!("{} issues contain auto-fix suggestions", suggestions)]);
140 }
141
142 term::emit(writer, &config, &files, &diagnostic)?;
143 }
144
145 Ok(highest_level)
146}
147
148struct SourceManagerFile<'a>(&'a SourceManager, &'a ThreadedInterner);
149
150impl<'a> Files<'a> for SourceManagerFile<'_> {
151 type FileId = SourceIdentifier;
152 type Name = &'a str;
153 type Source = &'a str;
154
155 fn name(&'a self, file_id: SourceIdentifier) -> Result<&'a str, Error> {
156 self.0.load(&file_id).map(|source| self.1.lookup(&source.identifier.value())).map_err(|e| match e {
157 SourceError::UnavailableSource(_) => Error::FileMissing,
158 SourceError::IOError(error) => Error::Io(error),
159 })
160 }
161
162 fn source(&'a self, file_id: SourceIdentifier) -> Result<&'a str, Error> {
163 self.0.load(&file_id).map(|source| self.1.lookup(&source.content)).map_err(|e| match e {
164 SourceError::UnavailableSource(_) => Error::FileMissing,
165 SourceError::IOError(error) => Error::Io(error),
166 })
167 }
168
169 fn line_index(&self, file_id: SourceIdentifier, byte_index: usize) -> Result<usize, Error> {
170 let source = self.0.load(&file_id).map_err(|e| match e {
171 SourceError::UnavailableSource(_) => Error::FileMissing,
172 SourceError::IOError(error) => Error::Io(error),
173 })?;
174
175 Ok(source.line_number(byte_index))
176 }
177
178 fn line_range(&self, file_id: SourceIdentifier, line_index: usize) -> Result<Range<usize>, Error> {
179 let source = self.0.load(&file_id).map_err(|e| match e {
180 SourceError::UnavailableSource(_) => Error::FileMissing,
181 SourceError::IOError(error) => Error::Io(error),
182 })?;
183
184 codespan_line_range(&source.lines, source.size, line_index)
185 }
186}
187
188fn codespan_line_start(lines: &[usize], size: usize, line_index: usize) -> Result<usize, Error> {
189 match line_index.cmp(&lines.len()) {
190 Ordering::Less => Ok(lines.get(line_index).cloned().expect("failed despite previous check")),
191 Ordering::Equal => Ok(size),
192 Ordering::Greater => Err(Error::LineTooLarge { given: line_index, max: lines.len() - 1 }),
193 }
194}
195
196fn codespan_line_range(lines: &[usize], size: usize, line_index: usize) -> Result<Range<usize>, Error> {
197 let line_start = codespan_line_start(lines, size, line_index)?;
198 let next_line_start = codespan_line_start(lines, size, line_index + 1)?;
199
200 Ok(line_start..next_line_start)
201}
202
203impl From<AnnotationKind> for LabelStyle {
204 fn from(kind: AnnotationKind) -> LabelStyle {
205 match kind {
206 AnnotationKind::Primary => LabelStyle::Primary,
207 AnnotationKind::Secondary => LabelStyle::Secondary,
208 }
209 }
210}
211
212impl From<Annotation> for Label<SourceIdentifier> {
213 fn from(annotation: Annotation) -> Label<SourceIdentifier> {
214 let mut label = Label::new(annotation.kind.into(), annotation.span.start.source, annotation.span);
215
216 if let Some(message) = annotation.message {
217 label.message = message;
218 }
219
220 label
221 }
222}
223
224impl From<Level> for Severity {
225 fn from(level: Level) -> Severity {
226 match level {
227 Level::Note => Severity::Note,
228 Level::Help => Severity::Help,
229 Level::Warning => Severity::Warning,
230 Level::Error => Severity::Error,
231 }
232 }
233}
234
235impl From<Issue> for Diagnostic<SourceIdentifier> {
236 fn from(issue: Issue) -> Diagnostic<SourceIdentifier> {
237 let mut diagnostic = Diagnostic::new(issue.level.into()).with_message(issue.message);
238
239 if let Some(code) = issue.code {
240 diagnostic.code = Some(code);
241 }
242
243 for annotation in issue.annotations {
244 diagnostic.labels.push(annotation.into());
245 }
246
247 for note in issue.notes {
248 diagnostic.notes.push(note);
249 }
250
251 if let Some(help) = issue.help {
252 diagnostic.notes.push(format!("Help: {}", help));
253 }
254
255 if let Some(link) = issue.link {
256 diagnostic.notes.push(format!("See: {}", link));
257 }
258
259 diagnostic
260 }
261}