mago_reporting/internal/emitter/
codespan.rs

1use 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}