spade_diagnostics/emitter/
codespan_emitter.rs

1use std::io::Write;
2
3use spade_codespan_reporting as codespan_reporting;
4use spade_codespan_reporting::diagnostic::{
5    Diagnostic as CodespanDiagnostic, SpannedNote, Subdiagnostic as CodespanSubdiagnostic,
6    Suggestion, SuggestionPart,
7};
8use spade_codespan_reporting::files::Files;
9use spade_codespan_reporting::term::termcolor::{Color, ColorChoice, ColorSpec, WriteColor};
10use spade_codespan_reporting::term::{self, termcolor::Buffer};
11
12use itertools::Itertools;
13use spade_common::location_info::AsLabel;
14
15use crate::diagnostic::{DiagnosticLevel, Subdiagnostic};
16use crate::{CodeBundle, Diagnostic, Emitter};
17
18pub fn color_choice(no_color: bool) -> ColorChoice {
19    if no_color {
20        ColorChoice::Never
21    } else {
22        ColorChoice::Auto
23    }
24}
25
26pub fn codespan_config() -> codespan_reporting::term::Config {
27    let mut primary_label_error = ColorSpec::new();
28    primary_label_error
29        .set_fg(Some(Color::Red))
30        .set_intense(true);
31
32    let style = codespan_reporting::term::Styles {
33        primary_label_error,
34        ..Default::default()
35    };
36    codespan_reporting::term::Config {
37        styles: style,
38        ..Default::default()
39    }
40}
41
42pub struct CodespanEmitter;
43
44impl Emitter for CodespanEmitter {
45    fn emit_diagnostic(&mut self, diag: &Diagnostic, buffer: &mut Buffer, code: &CodeBundle) {
46        let severity = diag.level.severity();
47        let is_bug = diag.level == DiagnosticLevel::Bug;
48        let message = diag.labels.message.as_str();
49        let primary_label = if let Some(primary_label_message) = diag.labels.primary_label.as_ref()
50        {
51            diag.labels
52                .span
53                .primary_label()
54                .with_message(primary_label_message.as_str())
55        } else {
56            diag.labels.span.primary_label()
57        };
58        let mut labels = vec![primary_label];
59        labels.extend(
60            diag.labels
61                .secondary_labels
62                .iter()
63                .map(|(sp, msg)| sp.secondary_label().with_message(msg.as_str())),
64        );
65        let mut simple_notes = vec![];
66        let mut subdiagnostics = vec![];
67        for subdiag in &diag.subdiagnostics {
68            match subdiag {
69                Subdiagnostic::Note { level, message } => {
70                    if message.as_str().contains('\n') {
71                        // For text spanning multiple lines, we want to align it.
72                        // Example:
73                        //
74                        // = note: This note contains some text that
75                        //         spans multiple lines
76                        //
77                        // Manual alignment is very hard to do otherwise, since the message
78                        // would need to know the length of the level. "= warning:" needs
79                        // more alignment than "= note:" does.
80                        let level_len = level.as_str().len() + ": ".len();
81                        simple_notes.push(format!(
82                            "{}: {}",
83                            level.as_str(),
84                            message
85                                .as_str()
86                                .lines()
87                                .collect::<Vec<_>>()
88                                .join(&("\n".to_string() + &str::repeat(" ", level_len)))
89                        ));
90                    } else {
91                        simple_notes.push(format!("{}: {}", level.as_str(), message.as_str()));
92                    }
93                }
94                Subdiagnostic::SpannedNote {
95                    level,
96                    labels: note_labels,
97                } => {
98                    let primary_label =
99                        if let Some(primary_label_message) = note_labels.primary_label.as_ref() {
100                            note_labels
101                                .span
102                                .primary_label()
103                                .with_message(primary_label_message.as_str())
104                        } else {
105                            note_labels.span.primary_label()
106                        };
107                    let mut labels = vec![primary_label];
108                    labels.extend(
109                        note_labels
110                            .secondary_labels
111                            .iter()
112                            .map(|(sp, msg)| sp.secondary_label().with_message(msg.as_str())),
113                    );
114                    subdiagnostics.push(CodespanSubdiagnostic::SpannedNote(SpannedNote {
115                        severity: level.severity(),
116                        message: note_labels.message.as_str().to_string(),
117                        labels,
118                    }));
119                }
120                Subdiagnostic::Suggestion { parts, message } => {
121                    subdiagnostics.push(CodespanSubdiagnostic::Suggestion(Suggestion {
122                        file_id: parts[0].0 .1,
123                        message: message.as_str().to_string(),
124                        parts: parts
125                            .iter()
126                            .map(|((sp, _), replacement)| SuggestionPart {
127                                range: (*sp).into(),
128                                replacement: replacement.to_string(),
129                            })
130                            .collect(),
131                    }))
132                }
133                Subdiagnostic::TemplateTraceback { .. } => {
134                    // Handled separately
135                }
136                Subdiagnostic::TypeMismatch {
137                    got,
138                    got_outer,
139                    expected,
140                    expected_outer,
141                } => {
142                    let mut note = vec![];
143                    note.push(format!("note: Expected: {expected}"));
144                    if let Some(expected_outer) = expected_outer {
145                        note.push(format!("            in: {expected_outer}"));
146                    }
147                    note.push(format!("           Got: {got}"));
148                    if let Some(got_outer) = got_outer {
149                        note.push(format!("            in: {got_outer}"));
150                    }
151                    simple_notes.push(note.join("\n"))
152                }
153            }
154        }
155
156        let type_tracebacks = diag
157            .subdiagnostics
158            .iter()
159            .filter_map(|d| {
160                if let Subdiagnostic::TemplateTraceback { span, message } = d {
161                    let filename = code.files.name(span.1).unwrap();
162                    let line = code
163                        .files
164                        .location(span.1, span.0.start().0 as usize)
165                        .unwrap()
166                        .line_number;
167                    Some(format!("{filename}:{line} {}", message.as_str()))
168                } else {
169                    None
170                }
171            })
172            .collect::<Vec<_>>();
173
174        if !type_tracebacks.is_empty() {
175            let message = vec!["The error is in a generic unit instantiated at".to_string()]
176                .into_iter()
177                .chain(type_tracebacks)
178                .join("\n╰ ");
179
180            simple_notes.push(message)
181        };
182
183        let diag = CodespanDiagnostic::new(severity)
184            .with_message(message)
185            .with_labels(labels)
186            .with_notes(simple_notes)
187            .with_subdiagnostics(subdiagnostics);
188
189        if buffer.supports_color() && is_bug {
190            // Ignore errors from trying to print the panik.
191            let _ = writeln!(buffer, "{}", super::panik::PANIK);
192        }
193
194        term::emit(buffer, &codespan_config(), &code.files, &diag).unwrap();
195    }
196}