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