solar_interface/diagnostics/
mod.rs

1//! Diagnostics implementation.
2//!
3//! Modified from [`rustc_errors`](https://github.com/rust-lang/rust/blob/520e30be83b4ed57b609d33166c988d1512bf4f3/compiler/rustc_errors/src/diagnostic.rs).
4
5use crate::Span;
6use anstyle::{AnsiColor, Color};
7use std::{
8    borrow::Cow,
9    fmt::{self, Write},
10    panic::Location,
11};
12
13mod builder;
14pub use builder::{DiagBuilder, EmissionGuarantee};
15
16mod context;
17pub use context::{DiagCtxt, DiagCtxtFlags};
18
19mod emitter;
20#[cfg(feature = "json")]
21pub use emitter::JsonEmitter;
22pub use emitter::{
23    DynEmitter, Emitter, HumanBufferEmitter, HumanEmitter, LocalEmitter, SilentEmitter,
24};
25
26mod message;
27pub use message::{DiagMsg, MultiSpan, SpanLabel};
28
29/// Represents all the diagnostics emitted up to a certain point.
30///
31/// Returned by [`DiagCtxt::emitted_diagnostics`].
32pub struct EmittedDiagnostics(pub(crate) String);
33
34impl fmt::Debug for EmittedDiagnostics {
35    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36        f.write_str(&self.0)
37    }
38}
39
40impl fmt::Display for EmittedDiagnostics {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        f.write_str(&self.0)
43    }
44}
45
46impl std::error::Error for EmittedDiagnostics {}
47
48impl EmittedDiagnostics {
49    /// Returns `true` if no diagnostics have been emitted.
50    pub fn is_empty(&self) -> bool {
51        self.0.is_empty()
52    }
53}
54
55/// Useful type to use with [`Result`] indicate that an error has already been reported to the user,
56/// so no need to continue checking.
57#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
58pub struct ErrorGuaranteed(());
59
60impl fmt::Debug for ErrorGuaranteed {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        f.write_str("ErrorGuaranteed")
63    }
64}
65
66impl ErrorGuaranteed {
67    /// Creates a new `ErrorGuaranteed`.
68    ///
69    /// Use of this method is discouraged.
70    #[inline]
71    pub const fn new_unchecked() -> Self {
72        Self(())
73    }
74}
75
76/// Marker type which enables implementation of `create_bug` and `emit_bug` functions for
77/// bug diagnostics.
78#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
79pub struct BugAbort;
80
81/// Signifies that the compiler died with an explicit call to `.bug` rather than a failed assertion,
82/// etc.
83pub struct ExplicitBug;
84
85/// Marker type which enables implementation of fatal diagnostics.
86pub struct FatalAbort;
87
88/// Diag ID.
89///
90/// Use [`error_code!`](crate::error_code) to create an error code diagnostic ID.
91///
92/// # Examples
93///
94/// ```
95/// # use solar_interface::{diagnostics::DiagId, error_code};
96/// let id: DiagId = error_code!(1234);
97/// ```
98#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
99pub struct DiagId {
100    s: Cow<'static, str>,
101}
102
103impl DiagId {
104    /// Creates a new diagnostic ID from a number.
105    ///
106    /// This should be used for custom lints. For solc-like error codes, use
107    /// the [`error_code!`](crate::error_code) macro.
108    pub fn new_str(s: impl Into<Cow<'static, str>>) -> Self {
109        Self { s: s.into() }
110    }
111
112    /// Creates an error code diagnostic ID.
113    ///
114    /// Use [`error_code!`](crate::error_code) instead.
115    #[doc(hidden)]
116    #[cfg_attr(debug_assertions, track_caller)]
117    pub fn new_from_macro(id: u32) -> Self {
118        debug_assert!((1..=9999).contains(&id), "error code must be in range 0001-9999");
119        Self { s: Cow::Owned(format!("{id:04}")) }
120    }
121
122    /// Returns the string representation of the diagnostic ID.
123    pub fn as_string(&self) -> String {
124        self.s.to_string()
125    }
126}
127
128/// Used for creating an error code. The input must be exactly 4 decimal digits.
129///
130/// # Examples
131///
132/// ```
133/// # use solar_interface::{diagnostics::DiagId, error_code};
134/// let code: DiagId = error_code!(1234);
135/// ```
136#[macro_export]
137macro_rules! error_code {
138    ($id:literal) => {
139        $crate::diagnostics::DiagId::new_from_macro($id)
140    };
141}
142
143/// Diag level.
144#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
145pub enum Level {
146    /// For bugs in the compiler. Manifests as an ICE (internal compiler error) panic.
147    ///
148    /// Its `EmissionGuarantee` is `BugAbort`.
149    Bug,
150
151    /// An error that causes an immediate abort. Used for things like configuration errors,
152    /// internal overflows, some file operation errors.
153    ///
154    /// Its `EmissionGuarantee` is `FatalAbort`.
155    Fatal,
156
157    /// An error in the code being compiled, which prevents compilation from finishing. This is the
158    /// most common case.
159    ///
160    /// Its `EmissionGuarantee` is `ErrorGuaranteed`.
161    Error,
162
163    /// A warning about the code being compiled. Does not prevent compilation from finishing.
164    ///
165    /// Its `EmissionGuarantee` is `()`.
166    Warning,
167
168    /// A message giving additional context. Rare, because notes are more commonly attached to
169    /// other diagnostics such as errors.
170    ///
171    /// Its `EmissionGuarantee` is `()`.
172    Note,
173
174    /// A note that is only emitted once. Rare, mostly used in circumstances relating to lints.
175    ///
176    /// Its `EmissionGuarantee` is `()`.
177    OnceNote,
178
179    /// A message suggesting how to fix something. Rare, because help messages are more commonly
180    /// attached to other diagnostics such as errors.
181    ///
182    /// Its `EmissionGuarantee` is `()`.
183    Help,
184
185    /// A help that is only emitted once. Rare.
186    ///
187    /// Its `EmissionGuarantee` is `()`.
188    OnceHelp,
189
190    /// Similar to `Note`, but used in cases where compilation has failed. Rare.
191    ///
192    /// Its `EmissionGuarantee` is `()`.
193    FailureNote,
194
195    /// Only used for lints.
196    ///
197    /// Its `EmissionGuarantee` is `()`.
198    Allow,
199}
200
201impl Level {
202    /// Returns the string representation of the level.
203    pub fn to_str(self) -> &'static str {
204        match self {
205            Self::Bug => "error: internal compiler error",
206            Self::Fatal | Self::Error => "error",
207            Self::Warning => "warning",
208            Self::Note | Self::OnceNote => "note",
209            Self::Help | Self::OnceHelp => "help",
210            Self::FailureNote => "failure-note",
211            Self::Allow
212            // | Self::Expect(_)
213            => unreachable!(),
214        }
215    }
216
217    /// Returns `true` if this level is an error.
218    #[inline]
219    pub fn is_error(self) -> bool {
220        match self {
221            Self::Bug | Self::Fatal | Self::Error | Self::FailureNote => true,
222
223            Self::Warning
224            | Self::Note
225            | Self::OnceNote
226            | Self::Help
227            | Self::OnceHelp
228            | Self::Allow => false,
229        }
230    }
231
232    /// Returns the style of this level.
233    #[inline]
234    pub const fn style(self) -> anstyle::Style {
235        anstyle::Style::new().fg_color(self.color()).bold()
236    }
237
238    /// Returns the color of this level.
239    #[inline]
240    pub const fn color(self) -> Option<Color> {
241        match self.ansi_color() {
242            Some(c) => Some(Color::Ansi(c)),
243            None => None,
244        }
245    }
246
247    /// Returns the ANSI color of this level.
248    #[inline]
249    pub const fn ansi_color(self) -> Option<AnsiColor> {
250        // https://github.com/rust-lang/rust/blob/99472c7049783605444ab888a97059d0cce93a12/compiler/rustc_errors/src/lib.rs#L1768
251        match self {
252            Self::Bug | Self::Fatal | Self::Error => Some(AnsiColor::BrightRed),
253            Self::Warning => Some(AnsiColor::BrightYellow),
254            Self::Note | Self::OnceNote => Some(AnsiColor::BrightGreen),
255            Self::Help | Self::OnceHelp => Some(AnsiColor::BrightCyan),
256            Self::FailureNote | Self::Allow => None,
257        }
258    }
259}
260
261#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
262pub enum Style {
263    MainHeaderMsg,
264    HeaderMsg,
265    LineAndColumn,
266    LineNumber,
267    Quotation,
268    UnderlinePrimary,
269    UnderlineSecondary,
270    LabelPrimary,
271    LabelSecondary,
272    NoStyle,
273    Level(Level),
274    Highlight,
275    Addition,
276    Removal,
277}
278
279impl Style {
280    /// Converts the style to an [`anstyle::Style`].
281    pub const fn to_color_spec(self, level: Level) -> anstyle::Style {
282        use AnsiColor::*;
283
284        /// On Windows, BRIGHT_BLUE is hard to read on black. Use cyan instead.
285        ///
286        /// See [rust-lang/rust#36178](https://github.com/rust-lang/rust/pull/36178).
287        const BRIGHT_BLUE: Color = Color::Ansi(if cfg!(windows) { BrightCyan } else { BrightBlue });
288        const GREEN: Color = Color::Ansi(BrightGreen);
289        const MAGENTA: Color = Color::Ansi(BrightMagenta);
290        const RED: Color = Color::Ansi(BrightRed);
291        const WHITE: Color = Color::Ansi(BrightWhite);
292
293        let s = anstyle::Style::new();
294        match self {
295            Self::Addition => s.fg_color(Some(GREEN)),
296            Self::Removal => s.fg_color(Some(RED)),
297            Self::LineAndColumn => s,
298            Self::LineNumber => s.fg_color(Some(BRIGHT_BLUE)).bold(),
299            Self::Quotation => s,
300            Self::MainHeaderMsg => if cfg!(windows) { s.fg_color(Some(WHITE)) } else { s }.bold(),
301            Self::UnderlinePrimary | Self::LabelPrimary => s.fg_color(level.color()).bold(),
302            Self::UnderlineSecondary | Self::LabelSecondary => s.fg_color(Some(BRIGHT_BLUE)).bold(),
303            Self::HeaderMsg | Self::NoStyle => s,
304            Self::Level(level2) => s.fg_color(level2.color()).bold(),
305            Self::Highlight => s.fg_color(Some(MAGENTA)).bold(),
306        }
307    }
308}
309
310/// A "sub"-diagnostic attached to a parent diagnostic.
311/// For example, a note attached to an error.
312#[derive(Clone, Debug, PartialEq, Hash)]
313pub struct SubDiagnostic {
314    pub level: Level,
315    pub messages: Vec<(DiagMsg, Style)>,
316    pub span: MultiSpan,
317}
318
319impl SubDiagnostic {
320    /// Formats the diagnostic messages into a single string.
321    pub fn label(&self) -> Cow<'_, str> {
322        flatten_messages(&self.messages, false, self.level)
323    }
324
325    /// Formats the diagnostic messages into a single string with ANSI color codes if applicable.
326    pub fn label_with_style(&self, supports_color: bool) -> Cow<'_, str> {
327        flatten_messages(&self.messages, supports_color, self.level)
328    }
329}
330
331/// A compiler diagnostic.
332#[must_use]
333#[derive(Clone, Debug)]
334pub struct Diag {
335    pub(crate) level: Level,
336
337    pub messages: Vec<(DiagMsg, Style)>,
338    pub span: MultiSpan,
339    pub children: Vec<SubDiagnostic>,
340    pub code: Option<DiagId>,
341
342    pub created_at: &'static Location<'static>,
343}
344
345impl PartialEq for Diag {
346    fn eq(&self, other: &Self) -> bool {
347        self.keys() == other.keys()
348    }
349}
350
351impl std::hash::Hash for Diag {
352    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
353        self.keys().hash(state);
354    }
355}
356
357impl Diag {
358    /// Creates a new `Diag` with a single message.
359    #[track_caller]
360    pub fn new<M: Into<DiagMsg>>(level: Level, msg: M) -> Self {
361        Self::new_with_messages(level, vec![(msg.into(), Style::NoStyle)])
362    }
363
364    /// Creates a new `Diag` with multiple messages.
365    #[track_caller]
366    pub fn new_with_messages(level: Level, messages: Vec<(DiagMsg, Style)>) -> Self {
367        Self {
368            level,
369            messages,
370            code: None,
371            span: MultiSpan::new(),
372            children: vec![],
373            // suggestions: Ok(vec![]),
374            // args: Default::default(),
375            // sort_span: DUMMY_SP,
376            // is_lint: false,
377            created_at: Location::caller(),
378        }
379    }
380
381    /// Returns `true` if this diagnostic is an error.
382    #[inline]
383    pub fn is_error(&self) -> bool {
384        self.level.is_error()
385    }
386
387    /// Formats the diagnostic messages into a single string.
388    pub fn label(&self) -> Cow<'_, str> {
389        flatten_messages(&self.messages, false, self.level)
390    }
391
392    /// Formats the diagnostic messages into a single string with ANSI color codes if applicable.
393    pub fn label_with_style(&self, supports_color: bool) -> Cow<'_, str> {
394        flatten_messages(&self.messages, supports_color, self.level)
395    }
396
397    /// Returns the messages of this diagnostic.
398    pub fn messages(&self) -> &[(DiagMsg, Style)] {
399        &self.messages
400    }
401
402    /// Returns the level of this diagnostic.
403    pub fn level(&self) -> Level {
404        self.level
405    }
406
407    /// Returns the code of this diagnostic as a string.
408    pub fn id(&self) -> Option<String> {
409        self.code.as_ref().map(|code| code.as_string())
410    }
411
412    /// Fields used for `PartialEq` and `Hash` implementations.
413    fn keys(&self) -> impl PartialEq + std::hash::Hash + '_ {
414        (
415            &self.level,
416            &self.messages,
417            // self.args().collect(),
418            &self.code,
419            &self.span,
420            // &self.suggestions,
421            // (if self.is_lint { None } else { Some(&self.children) }),
422            &self.children,
423        )
424    }
425}
426
427/// Setters.
428impl Diag {
429    /// Sets the span of this diagnostic.
430    pub fn span(&mut self, span: impl Into<MultiSpan>) -> &mut Self {
431        self.span = span.into();
432        self
433    }
434
435    /// Sets the code of this diagnostic.
436    pub fn code(&mut self, code: impl Into<DiagId>) -> &mut Self {
437        self.code = Some(code.into());
438        self
439    }
440
441    /// Adds a span/label to be included in the resulting snippet.
442    ///
443    /// This is pushed onto the [`MultiSpan`] that was created when the diagnostic
444    /// was first built. That means it will be shown together with the original
445    /// span/label, *not* a span added by one of the `span_{note,warn,help,suggestions}` methods.
446    ///
447    /// This span is *not* considered a ["primary span"][`MultiSpan`]; only
448    /// the `Span` supplied when creating the diagnostic is primary.
449    pub fn span_label(&mut self, span: Span, label: impl Into<DiagMsg>) -> &mut Self {
450        self.span.push_span_label(span, label);
451        self
452    }
453
454    /// Labels all the given spans with the provided label.
455    /// See [`Self::span_label()`] for more information.
456    pub fn span_labels(
457        &mut self,
458        spans: impl IntoIterator<Item = Span>,
459        label: impl Into<DiagMsg>,
460    ) -> &mut Self {
461        let label = label.into();
462        for span in spans {
463            self.span_label(span, label.clone());
464        }
465        self
466    }
467
468    /// Adds a note with the location where this diagnostic was created and emitted.
469    pub(crate) fn locations_note(&mut self, emitted_at: &Location<'_>) -> &mut Self {
470        let msg = format!(
471            "created at {},\n\
472             emitted at {}",
473            self.created_at, emitted_at
474        );
475        self.note(msg)
476    }
477}
478
479/// Sub-diagnostics.
480impl Diag {
481    /// Add a warning attached to this diagnostic.
482    pub fn warn(&mut self, msg: impl Into<DiagMsg>) -> &mut Self {
483        self.sub(Level::Warning, msg, MultiSpan::new())
484    }
485
486    /// Prints the span with a warning above it.
487    /// This is like [`Diag::warn()`], but it gets its own span.
488    pub fn span_warn(&mut self, span: impl Into<MultiSpan>, msg: impl Into<DiagMsg>) -> &mut Self {
489        self.sub(Level::Warning, msg, span)
490    }
491
492    /// Add a note to this diagnostic.
493    pub fn note(&mut self, msg: impl Into<DiagMsg>) -> &mut Self {
494        self.sub(Level::Note, msg, MultiSpan::new())
495    }
496
497    /// Prints the span with a note above it.
498    /// This is like [`Diag::note()`], but it gets its own span.
499    pub fn span_note(&mut self, span: impl Into<MultiSpan>, msg: impl Into<DiagMsg>) -> &mut Self {
500        self.sub(Level::Note, msg, span)
501    }
502
503    pub fn highlighted_note(&mut self, messages: Vec<(impl Into<DiagMsg>, Style)>) -> &mut Self {
504        self.sub_with_highlights(Level::Note, messages, MultiSpan::new())
505    }
506
507    /// Prints the span with a note above it.
508    /// This is like [`Diag::note()`], but it gets emitted only once.
509    pub fn note_once(&mut self, msg: impl Into<DiagMsg>) -> &mut Self {
510        self.sub(Level::OnceNote, msg, MultiSpan::new())
511    }
512
513    /// Prints the span with a note above it.
514    /// This is like [`Diag::note_once()`], but it gets its own span.
515    pub fn span_note_once(
516        &mut self,
517        span: impl Into<MultiSpan>,
518        msg: impl Into<DiagMsg>,
519    ) -> &mut Self {
520        self.sub(Level::OnceNote, msg, span)
521    }
522
523    /// Add a help message attached to this diagnostic.
524    pub fn help(&mut self, msg: impl Into<DiagMsg>) -> &mut Self {
525        self.sub(Level::Help, msg, MultiSpan::new())
526    }
527
528    /// Prints the span with a help above it.
529    /// This is like [`Diag::help()`], but it gets its own span.
530    pub fn help_once(&mut self, msg: impl Into<DiagMsg>) -> &mut Self {
531        self.sub(Level::OnceHelp, msg, MultiSpan::new())
532    }
533
534    /// Add a help message attached to this diagnostic with a customizable highlighted message.
535    pub fn highlighted_help(&mut self, msgs: Vec<(impl Into<DiagMsg>, Style)>) -> &mut Self {
536        self.sub_with_highlights(Level::Help, msgs, MultiSpan::new())
537    }
538
539    /// Prints the span with some help above it.
540    /// This is like [`Diag::help()`], but it gets its own span.
541    pub fn span_help(&mut self, span: impl Into<MultiSpan>, msg: impl Into<DiagMsg>) -> &mut Self {
542        self.sub(Level::Help, msg, span)
543    }
544
545    fn sub(
546        &mut self,
547        level: Level,
548        msg: impl Into<DiagMsg>,
549        span: impl Into<MultiSpan>,
550    ) -> &mut Self {
551        self.children.push(SubDiagnostic {
552            level,
553            messages: vec![(msg.into(), Style::NoStyle)],
554            span: span.into(),
555        });
556        self
557    }
558
559    fn sub_with_highlights(
560        &mut self,
561        level: Level,
562        messages: Vec<(impl Into<DiagMsg>, Style)>,
563        span: MultiSpan,
564    ) -> &mut Self {
565        let messages = messages.into_iter().map(|(m, s)| (m.into(), s)).collect();
566        self.children.push(SubDiagnostic { level, messages, span });
567        self
568    }
569}
570
571/// Flattens diagnostic messages, applying ANSI styles if requested.
572fn flatten_messages(messages: &[(DiagMsg, Style)], with_style: bool, level: Level) -> Cow<'_, str> {
573    if with_style {
574        match messages {
575            [] => Cow::Borrowed(""),
576            [(msg, Style::NoStyle)] => Cow::Borrowed(msg.as_str()),
577            [(msg, style)] => {
578                let mut res = String::new();
579                write_fmt(&mut res, msg, style, level);
580                Cow::Owned(res)
581            }
582            messages => {
583                let mut res = String::new();
584                for (msg, style) in messages {
585                    match style {
586                        Style::NoStyle => res.push_str(msg.as_str()),
587                        _ => write_fmt(&mut res, msg, style, level),
588                    }
589                }
590                Cow::Owned(res)
591            }
592        }
593    } else {
594        match messages {
595            [] => Cow::Borrowed(""),
596            [(message, _)] => Cow::Borrowed(message.as_str()),
597            messages => messages.iter().map(|(msg, _)| msg.as_str()).collect(),
598        }
599    }
600}
601
602fn write_fmt(output: &mut String, msg: &DiagMsg, style: &Style, level: Level) {
603    let ansi_style = style.to_color_spec(level);
604    write!(output, "{}{}{}", ansi_style.render(), msg.as_str(), ansi_style.render_reset()).unwrap();
605}
606
607#[cfg(test)]
608mod tests {
609    use super::*;
610
611    #[test]
612    fn test_styled_messages() {
613        // Create a diagnostic with styled messages
614        let mut diag = Diag::new(Level::Note, "test");
615
616        diag.highlighted_note(vec![
617            ("plain text ", Style::NoStyle),
618            ("removed", Style::Removal),
619            (" middle ", Style::NoStyle),
620            ("added", Style::Addition),
621        ]);
622
623        let sub = &diag.children[0];
624
625        // Without styles - just concatenated text
626        let plain = sub.label();
627        assert_eq!(plain, "plain text removed middle added");
628
629        // With styles - includes ANSI escape codes
630        let styled = sub.label_with_style(true);
631        assert_eq!(
632            styled.to_string(),
633            "plain text \u{1b}[91mremoved\u{1b}[0m middle \u{1b}[92madded\u{1b}[0m".to_string()
634        );
635    }
636}