swc_error_reporters/
diagnostic.rs

1use std::{fmt, mem::transmute};
2
3use miette::{GraphicalReportHandler, Severity, SourceOffset, SourceSpan};
4use swc_common::{
5    errors::{Diagnostic, DiagnosticId, Level, SubDiagnostic},
6    BytePos, FileName, SourceMap, Span,
7};
8
9pub struct PrettyDiagnostic<'a> {
10    source_code: PrettySourceCode<'a>,
11    d: &'a Diagnostic,
12
13    children: Vec<PrettySubDiagnostic<'a>>,
14}
15
16impl<'a> PrettyDiagnostic<'a> {
17    pub fn new(d: &'a Diagnostic, cm: &'a SourceMap, skip_filename: bool) -> Self {
18        let source_code = PrettySourceCode { cm, skip_filename };
19
20        let children = d
21            .children
22            .iter()
23            .filter(|d| !matches!(d.level, Level::Help))
24            .map(|d| PrettySubDiagnostic { source_code, d })
25            .collect();
26        Self {
27            source_code,
28            d,
29            children,
30        }
31    }
32}
33
34impl miette::Diagnostic for PrettyDiagnostic<'_> {
35    fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
36        self.d
37            .code
38            .as_ref()
39            .map(|v| match v {
40                DiagnosticId::Error(v) => v,
41                DiagnosticId::Lint(v) => v,
42            })
43            .map(|code| Box::new(code) as Box<dyn fmt::Display>)
44    }
45
46    fn severity(&self) -> Option<Severity> {
47        level_to_severity(self.d.level)
48    }
49
50    fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
51        self.d
52            .children
53            .iter()
54            .filter(|s| s.level == Level::Help)
55            .map(|s| Box::new(&s.message[0].0) as Box<_>)
56            .next()
57    }
58
59    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
60        if let Some(span) = self.d.span.primary_span() {
61            if span.lo.is_dummy() || span.hi.is_dummy() {
62                return None;
63            }
64        } else {
65            return None;
66        }
67
68        Some(&self.source_code as &dyn miette::SourceCode)
69    }
70
71    fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
72        let iter = self.d.span.span_labels().into_iter().map(|span_label| {
73            miette::LabeledSpan::new_with_span(span_label.label, convert_span(span_label.span))
74        });
75
76        Some(Box::new(iter))
77    }
78
79    fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn miette::Diagnostic> + 'a>> {
80        if self.children.is_empty() {
81            None
82        } else {
83            Some(Box::new(
84                self.children.iter().map(|d| d as &dyn miette::Diagnostic),
85            ))
86        }
87    }
88}
89
90impl<'a> PrettyDiagnostic<'a> {
91    pub fn to_pretty_string(&self, handler: &'a GraphicalReportHandler) -> String {
92        let mut wr = String::new();
93        handler.render_report(&mut wr, self).unwrap();
94        wr
95    }
96}
97
98impl std::error::Error for PrettyDiagnostic<'_> {}
99
100/// Delegates to `Diagnostics`
101impl fmt::Debug for PrettyDiagnostic<'_> {
102    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
103        fmt::Debug::fmt(&self.d, f)
104    }
105}
106
107impl fmt::Display for PrettyDiagnostic<'_> {
108    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
109        self.d.message[0].0.fmt(f)
110    }
111}
112
113#[derive(Clone, Copy)]
114pub struct PrettySourceCode<'a> {
115    cm: &'a SourceMap,
116    skip_filename: bool,
117}
118
119impl miette::SourceCode for PrettySourceCode<'_> {
120    fn read_span<'a>(
121        &'a self,
122        span: &SourceSpan,
123        context_lines_before: usize,
124        context_lines_after: usize,
125    ) -> Result<Box<dyn miette::SpanContents<'a> + 'a>, miette::MietteError> {
126        let lo = span.offset();
127        let hi = lo + span.len();
128
129        let mut span = Span::new(BytePos(lo as _), BytePos(hi as _));
130
131        span = self
132            .cm
133            .with_span_to_prev_source(span, |src| {
134                let len = src
135                    .rsplit('\n')
136                    .take(context_lines_before + 1)
137                    .map(|s| s.len() + 1)
138                    .sum::<usize>();
139
140                span.lo.0 -= (len as u32) - 1;
141                span
142            })
143            .unwrap_or(span);
144
145        span = self
146            .cm
147            .with_span_to_next_source(span, |src| {
148                let len = src
149                    .split('\n')
150                    .take(context_lines_after + 1)
151                    .map(|s| s.len() + 1)
152                    .sum::<usize>();
153
154                span.hi.0 += (len as u32) - 1;
155                span
156            })
157            .unwrap_or(span);
158
159        span = self
160            .cm
161            .with_snippet_of_span(span, |src| {
162                if src.lines().next().is_some() {
163                    return span;
164                }
165                let lo = src.len() - src.trim_start().len();
166                let hi = src.len() - src.trim_end().len();
167
168                span.lo.0 += lo as u32;
169                span.hi.0 -= hi as u32;
170
171                span
172            })
173            .unwrap_or(span);
174
175        let mut src = self
176            .cm
177            .with_snippet_of_span(span, |s| unsafe { transmute::<&str, &str>(s) })
178            .unwrap_or(" ");
179
180        if span.lo == span.hi {
181            src = " ";
182        }
183
184        let loc = self.cm.lookup_char_pos(span.lo());
185        let line_count = loc.file.analyze().lines.len();
186
187        let name = if self.skip_filename {
188            None
189        } else {
190            match &*loc.file.name {
191                FileName::Real(ref path) => Some(path.to_string_lossy().into_owned()),
192                FileName::Custom(ref name) => Some(name.clone()),
193                FileName::Anon => None,
194                _ => Some(loc.file.name.to_string()),
195            }
196        };
197
198        Ok(Box::new(SpanContentsImpl {
199            _cm: self.cm,
200            data: src,
201            span: convert_span(span),
202            line: loc.line.saturating_sub(1),
203            column: loc.col_display,
204            line_count,
205            name,
206        }))
207    }
208}
209
210pub fn to_pretty_source_code(cm: &SourceMap, skip_filename: bool) -> PrettySourceCode<'_> {
211    PrettySourceCode { cm, skip_filename }
212}
213struct PrettySubDiagnostic<'a> {
214    source_code: PrettySourceCode<'a>,
215    d: &'a SubDiagnostic,
216}
217
218impl std::error::Error for PrettySubDiagnostic<'_> {}
219
220/// Delegates to `Diagnostics`
221impl fmt::Debug for PrettySubDiagnostic<'_> {
222    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
223        fmt::Debug::fmt(&self.d, f)
224    }
225}
226
227impl fmt::Display for PrettySubDiagnostic<'_> {
228    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
229        fmt::Display::fmt(&self.d.message[0].0, f)
230    }
231}
232
233impl miette::Diagnostic for PrettySubDiagnostic<'_> {
234    fn severity(&self) -> Option<Severity> {
235        level_to_severity(self.d.level)
236    }
237
238    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
239        Some(&self.source_code)
240    }
241
242    fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
243        let iter = self.d.span.span_labels().into_iter().map(|span_label| {
244            miette::LabeledSpan::new_with_span(span_label.label, convert_span(span_label.span))
245        });
246
247        Some(Box::new(iter))
248    }
249}
250
251struct SpanContentsImpl<'a> {
252    /// This ensures that the underlying sourcemap is not dropped.
253    _cm: &'a SourceMap,
254
255    // Data from a [`SourceCode`], in bytes.
256    data: &'a str,
257    // span actually covered by this SpanContents.
258    span: SourceSpan,
259    // The 0-indexed line where the associated [`SourceSpan`] _starts_.
260    line: usize,
261    // The 0-indexed column where the associated [`SourceSpan`] _starts_.
262    column: usize,
263    // Number of line in this snippet.
264    line_count: usize,
265    // Optional filename
266    name: Option<String>,
267}
268
269impl<'a> miette::SpanContents<'a> for SpanContentsImpl<'a> {
270    fn data(&self) -> &'a [u8] {
271        self.data.as_bytes()
272    }
273
274    fn span(&self) -> &SourceSpan {
275        &self.span
276    }
277
278    fn line(&self) -> usize {
279        self.line
280    }
281
282    fn column(&self) -> usize {
283        self.column
284    }
285
286    fn line_count(&self) -> usize {
287        self.line_count
288    }
289
290    fn name(&self) -> Option<&str> {
291        self.name.as_deref()
292    }
293}
294
295fn level_to_severity(level: Level) -> Option<Severity> {
296    match level {
297        Level::FailureNote | Level::Bug | Level::Fatal | Level::PhaseFatal | Level::Error => {
298            Some(Severity::Error)
299        }
300        Level::Warning => Some(Severity::Warning),
301        Level::Note | Level::Help => Some(Severity::Advice),
302        Level::Cancelled => None,
303    }
304}
305
306pub fn convert_span(span: Span) -> SourceSpan {
307    let len = span.hi - span.lo;
308    let start = SourceOffset::from(span.lo.0 as usize);
309    SourceSpan::new(start, len.0 as usize)
310}
311
312pub trait ToPrettyDiagnostic {
313    fn to_pretty_diagnostic<'a>(
314        &'a self,
315        cm: &'a SourceMap,
316        skip_filename: bool,
317    ) -> PrettyDiagnostic<'a>;
318
319    fn to_pretty_string<'a>(
320        &self,
321        cm: &'a SourceMap,
322        skip_filename: bool,
323        handler: &'a GraphicalReportHandler,
324    ) -> String;
325}
326
327// For readable diagnostic, then render by miette
328impl ToPrettyDiagnostic for Diagnostic {
329    /// Returns a pretty-printed of the diagnostic.
330    fn to_pretty_diagnostic<'a>(
331        &'a self,
332        cm: &'a SourceMap,
333        skip_filename: bool,
334    ) -> PrettyDiagnostic<'a> {
335        PrettyDiagnostic::new(self, cm, skip_filename)
336    }
337
338    /// Converts the diagnostic into a pretty-printed string, suitable for
339    /// display.
340    ///
341    /// This method is used to generate a human-readable string representation
342    /// of the diagnostic. It utilizes the `PrettyDiagnostic` struct to
343    /// format the diagnostic information.
344    ///
345    /// # Parameters
346    ///
347    /// - `cm`: A reference to the `SourceMap` used for mapping source code
348    ///   locations.
349    /// - `skip_filename`: A boolean indicating whether to skip including
350    ///   filenames in the output.
351    /// - `handler`: A reference to the `GraphicalReportHandler` used for
352    ///   handling graphical reports.
353    ///
354    /// # Returns
355    ///
356    /// A `String` containing the pretty-printed diagnostic information.
357    fn to_pretty_string<'a>(
358        &self,
359        cm: &'a SourceMap,
360        skip_filename: bool,
361        handler: &'a GraphicalReportHandler,
362    ) -> String {
363        let pretty_diagnostic = PrettyDiagnostic::new(self, cm, skip_filename);
364        pretty_diagnostic.to_pretty_string(handler)
365    }
366}