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    hash::{Hash, Hasher},
11    ops::Deref,
12    panic::Location,
13};
14
15mod builder;
16pub use builder::{DiagBuilder, EmissionGuarantee};
17
18mod context;
19pub use context::{DiagCtxt, DiagCtxtFlags};
20
21mod emitter;
22#[cfg(feature = "json")]
23pub use emitter::JsonEmitter;
24pub use emitter::{
25    DynEmitter, Emitter, HumanBufferEmitter, HumanEmitter, InMemoryEmitter, LocalEmitter,
26    SilentEmitter,
27};
28
29mod message;
30pub use message::{DiagMsg, MultiSpan, SpanLabel};
31
32/// Represents all the diagnostics emitted up to a certain point.
33///
34/// Returned by [`DiagCtxt::emitted_diagnostics`].
35pub struct EmittedDiagnostics(pub(crate) String);
36
37impl fmt::Debug for EmittedDiagnostics {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        f.write_str(&self.0)
40    }
41}
42
43impl fmt::Display for EmittedDiagnostics {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        f.write_str(&self.0)
46    }
47}
48
49impl std::error::Error for EmittedDiagnostics {}
50
51impl EmittedDiagnostics {
52    /// Returns `true` if no diagnostics have been emitted.
53    pub fn is_empty(&self) -> bool {
54        self.0.is_empty()
55    }
56}
57
58/// Useful type to use with [`Result`] indicate that an error has already been reported to the user,
59/// so no need to continue checking.
60#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
61pub struct ErrorGuaranteed(());
62
63impl fmt::Debug for ErrorGuaranteed {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        f.write_str("ErrorGuaranteed")
66    }
67}
68
69impl ErrorGuaranteed {
70    /// Creates a new `ErrorGuaranteed`.
71    ///
72    /// Use of this method is discouraged.
73    #[inline]
74    pub const fn new_unchecked() -> Self {
75        Self(())
76    }
77}
78
79/// Marker type which enables implementation of `create_bug` and `emit_bug` functions for
80/// bug diagnostics.
81#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
82pub struct BugAbort;
83
84/// Signifies that the compiler died with an explicit call to `.bug` rather than a failed assertion,
85/// etc.
86pub struct ExplicitBug;
87
88/// Marker type which enables implementation of fatal diagnostics.
89pub struct FatalAbort;
90
91/// Diag ID.
92///
93/// Use [`error_code!`](crate::error_code) to create an error code diagnostic ID.
94///
95/// # Examples
96///
97/// ```
98/// # use solar_interface::{diagnostics::DiagId, error_code};
99/// let id: DiagId = error_code!(1234);
100/// ```
101#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
102pub struct DiagId {
103    s: Cow<'static, str>,
104}
105
106impl DiagId {
107    /// Creates a new diagnostic ID from a number.
108    ///
109    /// This should be used for custom lints. For solc-like error codes, use
110    /// the [`error_code!`](crate::error_code) macro.
111    pub fn new_str(s: impl Into<Cow<'static, str>>) -> Self {
112        Self { s: s.into() }
113    }
114
115    /// Creates an error code diagnostic ID.
116    ///
117    /// Use [`error_code!`](crate::error_code) instead.
118    #[doc(hidden)]
119    #[cfg_attr(debug_assertions, track_caller)]
120    pub fn new_from_macro(id: u32) -> Self {
121        debug_assert!((1..=9999).contains(&id), "error code must be in range 0001-9999");
122        Self { s: Cow::Owned(format!("{id:04}")) }
123    }
124
125    /// Returns the string representation of the diagnostic ID.
126    pub fn as_string(&self) -> String {
127        self.s.to_string()
128    }
129}
130
131/// Used for creating an error code. The input must be exactly 4 decimal digits.
132///
133/// # Examples
134///
135/// ```
136/// # use solar_interface::{diagnostics::DiagId, error_code};
137/// let code: DiagId = error_code!(1234);
138/// ```
139#[macro_export]
140macro_rules! error_code {
141    ($id:literal) => {
142        $crate::diagnostics::DiagId::new_from_macro($id)
143    };
144}
145
146/// Diag level.
147#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
148pub enum Level {
149    /// For bugs in the compiler. Manifests as an ICE (internal compiler error) panic.
150    ///
151    /// Its `EmissionGuarantee` is `BugAbort`.
152    Bug,
153
154    /// An error that causes an immediate abort. Used for things like configuration errors,
155    /// internal overflows, some file operation errors.
156    ///
157    /// Its `EmissionGuarantee` is `FatalAbort`.
158    Fatal,
159
160    /// An error in the code being compiled, which prevents compilation from finishing. This is the
161    /// most common case.
162    ///
163    /// Its `EmissionGuarantee` is `ErrorGuaranteed`.
164    Error,
165
166    /// A warning about the code being compiled. Does not prevent compilation from finishing.
167    ///
168    /// Its `EmissionGuarantee` is `()`.
169    Warning,
170
171    /// A message giving additional context. Rare, because notes are more commonly attached to
172    /// other diagnostics such as errors.
173    ///
174    /// Its `EmissionGuarantee` is `()`.
175    Note,
176
177    /// A note that is only emitted once. Rare, mostly used in circumstances relating to lints.
178    ///
179    /// Its `EmissionGuarantee` is `()`.
180    OnceNote,
181
182    /// A message suggesting how to fix something. Rare, because help messages are more commonly
183    /// attached to other diagnostics such as errors.
184    ///
185    /// Its `EmissionGuarantee` is `()`.
186    Help,
187
188    /// A help that is only emitted once. Rare.
189    ///
190    /// Its `EmissionGuarantee` is `()`.
191    OnceHelp,
192
193    /// Similar to `Note`, but used in cases where compilation has failed. Rare.
194    ///
195    /// Its `EmissionGuarantee` is `()`.
196    FailureNote,
197
198    /// Only used for lints.
199    ///
200    /// Its `EmissionGuarantee` is `()`.
201    Allow,
202}
203
204impl Level {
205    /// Returns the string representation of the level.
206    pub fn to_str(self) -> &'static str {
207        match self {
208            Self::Bug => "error: internal compiler error",
209            Self::Fatal | Self::Error => "error",
210            Self::Warning => "warning",
211            Self::Note | Self::OnceNote => "note",
212            Self::Help | Self::OnceHelp => "help",
213            Self::FailureNote => "failure-note",
214            Self::Allow
215            // | Self::Expect(_)
216            => unreachable!(),
217        }
218    }
219
220    /// Returns `true` if this level is an error.
221    #[inline]
222    pub fn is_error(self) -> bool {
223        match self {
224            Self::Bug | Self::Fatal | Self::Error | Self::FailureNote => true,
225
226            Self::Warning
227            | Self::Note
228            | Self::OnceNote
229            | Self::Help
230            | Self::OnceHelp
231            | Self::Allow => false,
232        }
233    }
234
235    /// Returns `true` if this level is a note.
236    #[inline]
237    pub fn is_note(self) -> bool {
238        match self {
239            Self::Note | Self::OnceNote => true,
240
241            Self::Bug
242            | Self::Fatal
243            | Self::Error
244            | Self::FailureNote
245            | Self::Warning
246            | Self::Help
247            | Self::OnceHelp
248            | Self::Allow => false,
249        }
250    }
251
252    /// Returns the style of this level.
253    #[inline]
254    pub const fn style(self) -> anstyle::Style {
255        anstyle::Style::new().fg_color(self.color()).bold()
256    }
257
258    /// Returns the color of this level.
259    #[inline]
260    pub const fn color(self) -> Option<Color> {
261        match self.ansi_color() {
262            Some(c) => Some(Color::Ansi(c)),
263            None => None,
264        }
265    }
266
267    /// Returns the ANSI color of this level.
268    #[inline]
269    pub const fn ansi_color(self) -> Option<AnsiColor> {
270        // https://github.com/rust-lang/rust/blob/99472c7049783605444ab888a97059d0cce93a12/compiler/rustc_errors/src/lib.rs#L1768
271        match self {
272            Self::Bug | Self::Fatal | Self::Error => Some(AnsiColor::BrightRed),
273            Self::Warning => {
274                Some(if cfg!(windows) { AnsiColor::BrightYellow } else { AnsiColor::Yellow })
275            }
276            Self::Note | Self::OnceNote => Some(AnsiColor::BrightGreen),
277            Self::Help | Self::OnceHelp => Some(AnsiColor::BrightCyan),
278            Self::FailureNote | Self::Allow => None,
279        }
280    }
281}
282
283#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
284pub enum Style {
285    MainHeaderMsg,
286    HeaderMsg,
287    LineAndColumn,
288    LineNumber,
289    Quotation,
290    UnderlinePrimary,
291    UnderlineSecondary,
292    LabelPrimary,
293    LabelSecondary,
294    NoStyle,
295    Level(Level),
296    Highlight,
297    Addition,
298    Removal,
299}
300
301impl Style {
302    /// Converts the style to an [`anstyle::Style`].
303    pub const fn to_color_spec(self, level: Level) -> anstyle::Style {
304        use AnsiColor::*;
305
306        /// On Windows, BRIGHT_BLUE is hard to read on black. Use cyan instead.
307        ///
308        /// See [rust-lang/rust#36178](https://github.com/rust-lang/rust/pull/36178).
309        const BRIGHT_BLUE: Color = Color::Ansi(if cfg!(windows) { BrightCyan } else { BrightBlue });
310        const GREEN: Color = Color::Ansi(BrightGreen);
311        const MAGENTA: Color = Color::Ansi(BrightMagenta);
312        const RED: Color = Color::Ansi(BrightRed);
313        const WHITE: Color = Color::Ansi(BrightWhite);
314
315        let s = anstyle::Style::new();
316        match self {
317            Self::Addition => s.fg_color(Some(GREEN)),
318            Self::Removal => s.fg_color(Some(RED)),
319            Self::LineAndColumn => s,
320            Self::LineNumber => s.fg_color(Some(BRIGHT_BLUE)).bold(),
321            Self::Quotation => s,
322            Self::MainHeaderMsg => if cfg!(windows) { s.fg_color(Some(WHITE)) } else { s }.bold(),
323            Self::UnderlinePrimary | Self::LabelPrimary => s.fg_color(level.color()).bold(),
324            Self::UnderlineSecondary | Self::LabelSecondary => s.fg_color(Some(BRIGHT_BLUE)).bold(),
325            Self::HeaderMsg | Self::NoStyle => s,
326            Self::Level(level2) => s.fg_color(level2.color()).bold(),
327            Self::Highlight => s.fg_color(Some(MAGENTA)).bold(),
328        }
329    }
330}
331
332/// Indicates the confidence in the correctness of a suggestion.
333///
334/// All suggestions are marked with an `Applicability`. Tools use the applicability of a suggestion
335/// to determine whether it should be automatically applied or if the user should be consulted
336/// before applying the suggestion.
337#[cfg_attr(feature = "json", derive(serde::Serialize, serde::Deserialize))]
338#[cfg_attr(feature = "json", serde(rename_all = "kebab-case"))]
339#[derive(Copy, Clone, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
340pub enum Applicability {
341    /// The suggestion is definitely what the user intended, or maintains the exact meaning of the
342    /// code. This suggestion should be automatically applied.
343    ///
344    /// In case of multiple `MachineApplicable` suggestions (whether as part of
345    /// the same `multipart_suggestion` or not), all of them should be
346    /// automatically applied.
347    MachineApplicable,
348
349    /// The suggestion may be what the user intended, but it is uncertain. The suggestion should
350    /// compile if it is applied.
351    MaybeIncorrect,
352
353    /// The suggestion contains placeholders like `(...)` or `{ /* fields */ }`. The suggestion
354    /// cannot be applied automatically because it will fail to compile. The user will need to fill
355    /// in the placeholders.
356    HasPlaceholders,
357
358    /// The applicability of the suggestion is unknown.
359    #[default]
360    Unspecified,
361}
362
363#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, Hash)]
364pub enum SuggestionStyle {
365    /// Hide the suggested code when displaying this suggestion inline.
366    HideCodeInline,
367    /// Always hide the suggested code but display the message.
368    HideCodeAlways,
369    /// Do not display this suggestion in the cli output, it is only meant for tools.
370    CompletelyHidden,
371    /// Always show the suggested code.
372    /// This will *not* show the code if the suggestion is inline *and* the suggested code is
373    /// empty.
374    #[default]
375    ShowCode,
376    /// Always show the suggested code independently.
377    ShowAlways,
378}
379
380impl SuggestionStyle {
381    fn hide_inline(&self) -> bool {
382        !matches!(*self, Self::ShowCode)
383    }
384}
385
386/// Represents the help messages seen on a diagnostic.
387#[derive(Clone, Debug, PartialEq, Hash)]
388pub enum Suggestions {
389    /// Indicates that new suggestions can be added or removed from this diagnostic.
390    ///
391    /// `DiagInner`'s new_* methods initialize the `suggestions` field with
392    /// this variant. Also, this is the default variant for `Suggestions`.
393    Enabled(Vec<CodeSuggestion>),
394    /// Indicates that suggestions cannot be added or removed from this diagnostic.
395    ///
396    /// Gets toggled when `.seal_suggestions()` is called on the `DiagInner`.
397    Sealed(Box<[CodeSuggestion]>),
398    /// Indicates that no suggestion is available for this diagnostic.
399    ///
400    /// Gets toggled when `.disable_suggestions()` is called on the `DiagInner`.
401    Disabled,
402}
403
404impl Suggestions {
405    /// Returns the underlying list of suggestions.
406    pub fn unwrap_tag(&self) -> &[CodeSuggestion] {
407        match self {
408            Self::Enabled(suggestions) => suggestions,
409            Self::Sealed(suggestions) => suggestions,
410            Self::Disabled => &[],
411        }
412    }
413}
414
415impl Default for Suggestions {
416    fn default() -> Self {
417        Self::Enabled(vec![])
418    }
419}
420
421impl Deref for Suggestions {
422    type Target = [CodeSuggestion];
423
424    fn deref(&self) -> &Self::Target {
425        self.unwrap_tag()
426    }
427}
428
429/// A structured suggestion for code changes.
430/// Based on rustc's CodeSuggestion structure.
431#[derive(Clone, Debug, PartialEq, Eq, Hash)]
432pub struct CodeSuggestion {
433    /// Each substitute can have multiple variants due to multiple
434    /// applicable suggestions
435    ///
436    /// `foo.bar` might be replaced with `a.b` or `x.y` by replacing
437    /// `foo` and `bar` on their own:
438    ///
439    /// ```ignore (illustrative)
440    /// vec![
441    ///     Substitution { parts: vec![(0..3, "a"), (4..7, "b")] },
442    ///     Substitution { parts: vec![(0..3, "x"), (4..7, "y")] },
443    /// ]
444    /// ```
445    ///
446    /// or by replacing the entire span:
447    ///
448    /// ```ignore (illustrative)
449    /// vec![
450    ///     Substitution { parts: vec![(0..7, "a.b")] },
451    ///     Substitution { parts: vec![(0..7, "x.y")] },
452    /// ]
453    /// ```
454    pub substitutions: Vec<Substitution>,
455    pub msg: DiagMsg,
456    /// Visual representation of this suggestion.
457    pub style: SuggestionStyle,
458    /// Whether or not the suggestion is approximate
459    ///
460    /// Sometimes we may show suggestions with placeholders,
461    /// which are useful for users but not useful for
462    /// tools like rustfix
463    pub applicability: Applicability,
464}
465
466/// A single part of a substitution, indicating a specific span to replace with a snippet.
467#[derive(Clone, Debug, PartialEq, Eq, Hash)]
468pub struct SubstitutionPart {
469    pub span: Span,
470    pub snippet: DiagMsg,
471}
472
473impl SubstitutionPart {
474    pub fn is_addition(&self) -> bool {
475        self.span.lo() == self.span.hi() && !self.snippet.is_empty()
476    }
477
478    pub fn is_deletion(&self) -> bool {
479        self.span.lo() != self.span.hi() && self.snippet.is_empty()
480    }
481
482    pub fn is_replacement(&self) -> bool {
483        self.span.lo() != self.span.hi() && !self.snippet.is_empty()
484    }
485}
486
487/// A substitution represents a single alternative fix consisting of multiple parts.
488#[derive(Clone, Debug, PartialEq, Eq, Hash)]
489pub struct Substitution {
490    pub parts: Vec<SubstitutionPart>,
491}
492
493/// A "sub"-diagnostic attached to a parent diagnostic.
494/// For example, a note attached to an error.
495#[derive(Clone, Debug, PartialEq, Hash)]
496pub struct SubDiagnostic {
497    pub level: Level,
498    pub messages: Vec<(DiagMsg, Style)>,
499    pub span: MultiSpan,
500}
501
502impl SubDiagnostic {
503    /// Formats the diagnostic messages into a single string.
504    pub fn label(&self) -> Cow<'_, str> {
505        self.label_with_style(false)
506    }
507
508    /// Formats the diagnostic messages into a single string with ANSI color codes if applicable.
509    pub fn label_with_style(&self, supports_color: bool) -> Cow<'_, str> {
510        flatten_messages(&self.messages, supports_color, self.level)
511    }
512}
513
514/// A compiler diagnostic.
515#[must_use]
516#[derive(Clone, Debug)]
517pub struct Diag {
518    pub(crate) level: Level,
519
520    pub messages: Vec<(DiagMsg, Style)>,
521    pub span: MultiSpan,
522    pub children: Vec<SubDiagnostic>,
523    pub code: Option<DiagId>,
524    pub suggestions: Suggestions,
525
526    pub created_at: &'static Location<'static>,
527}
528
529impl PartialEq for Diag {
530    fn eq(&self, other: &Self) -> bool {
531        self.keys() == other.keys()
532    }
533}
534
535impl Hash for Diag {
536    fn hash<H: Hasher>(&self, state: &mut H) {
537        self.keys().hash(state);
538    }
539}
540
541impl Diag {
542    /// Creates a new `Diag` with a single message.
543    #[track_caller]
544    pub fn new<M: Into<DiagMsg>>(level: Level, msg: M) -> Self {
545        Self::new_with_messages(level, vec![(msg.into(), Style::NoStyle)])
546    }
547
548    /// Creates a new `Diag` with multiple messages.
549    #[track_caller]
550    pub fn new_with_messages(level: Level, messages: Vec<(DiagMsg, Style)>) -> Self {
551        Self {
552            level,
553            messages,
554            code: None,
555            span: MultiSpan::new(),
556            children: vec![],
557            suggestions: Suggestions::default(),
558            // args: Default::default(),
559            // sort_span: DUMMY_SP,
560            // is_lint: false,
561            created_at: Location::caller(),
562        }
563    }
564
565    /// Returns `true` if this diagnostic is an error.
566    #[inline]
567    pub fn is_error(&self) -> bool {
568        self.level.is_error()
569    }
570
571    /// Returns `true` if this diagnostic is a note.
572    #[inline]
573    pub fn is_note(&self) -> bool {
574        self.level.is_note()
575    }
576
577    /// Formats the diagnostic messages into a single string.
578    pub fn label(&self) -> Cow<'_, str> {
579        flatten_messages(&self.messages, false, self.level)
580    }
581
582    /// Formats the diagnostic messages into a single string with ANSI color codes if applicable.
583    pub fn label_with_style(&self, supports_color: bool) -> Cow<'_, str> {
584        flatten_messages(&self.messages, supports_color, self.level)
585    }
586
587    /// Returns the messages of this diagnostic.
588    pub fn messages(&self) -> &[(DiagMsg, Style)] {
589        &self.messages
590    }
591
592    /// Returns the level of this diagnostic.
593    pub fn level(&self) -> Level {
594        self.level
595    }
596
597    /// Returns the code of this diagnostic as a string.
598    pub fn id(&self) -> Option<String> {
599        self.code.as_ref().map(|code| code.as_string())
600    }
601
602    /// Fields used for `PartialEq` and `Hash` implementations.
603    fn keys(&self) -> impl PartialEq + std::hash::Hash {
604        (
605            &self.level,
606            &self.messages,
607            &self.code,
608            &self.span,
609            &self.children,
610            &self.suggestions,
611            // self.args().collect(),
612            // omit self.sort_span
613            // &self.is_lint,
614            // omit self.created_at
615        )
616    }
617}
618
619/// Setters.
620impl Diag {
621    /// Sets the span of this diagnostic.
622    pub fn span(&mut self, span: impl Into<MultiSpan>) -> &mut Self {
623        self.span = span.into();
624        self
625    }
626
627    /// Sets the code of this diagnostic.
628    pub fn code(&mut self, code: impl Into<DiagId>) -> &mut Self {
629        self.code = Some(code.into());
630        self
631    }
632
633    /// Adds a span/label to be included in the resulting snippet.
634    ///
635    /// This is pushed onto the [`MultiSpan`] that was created when the diagnostic
636    /// was first built. That means it will be shown together with the original
637    /// span/label, *not* a span added by one of the `span_{note,warn,help,suggestions}` methods.
638    ///
639    /// This span is *not* considered a ["primary span"][`MultiSpan`]; only
640    /// the `Span` supplied when creating the diagnostic is primary.
641    pub fn span_label(&mut self, span: Span, label: impl Into<DiagMsg>) -> &mut Self {
642        self.span.push_span_label(span, label);
643        self
644    }
645
646    /// Labels all the given spans with the provided label.
647    /// See [`Self::span_label()`] for more information.
648    pub fn span_labels(
649        &mut self,
650        spans: impl IntoIterator<Item = Span>,
651        label: impl Into<DiagMsg>,
652    ) -> &mut Self {
653        let label = label.into();
654        for span in spans {
655            self.span_label(span, label.clone());
656        }
657        self
658    }
659
660    /// Adds a note with the location where this diagnostic was created and emitted.
661    pub(crate) fn locations_note(&mut self, emitted_at: &Location<'_>) -> &mut Self {
662        let msg = format!(
663            "created at {},\n\
664             emitted at {}",
665            self.created_at, emitted_at
666        );
667        self.note(msg)
668    }
669}
670
671/// Sub-diagnostics.
672impl Diag {
673    /// Add a warning attached to this diagnostic.
674    pub fn warn(&mut self, msg: impl Into<DiagMsg>) -> &mut Self {
675        self.sub(Level::Warning, msg, MultiSpan::new())
676    }
677
678    /// Prints the span with a warning above it.
679    /// This is like [`Diag::warn()`], but it gets its own span.
680    pub fn span_warn(&mut self, span: impl Into<MultiSpan>, msg: impl Into<DiagMsg>) -> &mut Self {
681        self.sub(Level::Warning, msg, span)
682    }
683
684    /// Add a note to this diagnostic.
685    pub fn note(&mut self, msg: impl Into<DiagMsg>) -> &mut Self {
686        self.sub(Level::Note, msg, MultiSpan::new())
687    }
688
689    /// Prints the span with a note above it.
690    /// This is like [`Diag::note()`], but it gets its own span.
691    pub fn span_note(&mut self, span: impl Into<MultiSpan>, msg: impl Into<DiagMsg>) -> &mut Self {
692        self.sub(Level::Note, msg, span)
693    }
694
695    pub fn highlighted_note(&mut self, messages: Vec<(impl Into<DiagMsg>, Style)>) -> &mut Self {
696        self.sub_with_highlights(Level::Note, messages, MultiSpan::new())
697    }
698
699    /// Prints the span with a note above it.
700    /// This is like [`Diag::note()`], but it gets emitted only once.
701    pub fn note_once(&mut self, msg: impl Into<DiagMsg>) -> &mut Self {
702        self.sub(Level::OnceNote, msg, MultiSpan::new())
703    }
704
705    /// Prints the span with a note above it.
706    /// This is like [`Diag::note_once()`], but it gets its own span.
707    pub fn span_note_once(
708        &mut self,
709        span: impl Into<MultiSpan>,
710        msg: impl Into<DiagMsg>,
711    ) -> &mut Self {
712        self.sub(Level::OnceNote, msg, span)
713    }
714
715    /// Add a help message attached to this diagnostic.
716    pub fn help(&mut self, msg: impl Into<DiagMsg>) -> &mut Self {
717        self.sub(Level::Help, msg, MultiSpan::new())
718    }
719
720    /// Prints the span with a help above it.
721    /// This is like [`Diag::help()`], but it gets its own span.
722    pub fn help_once(&mut self, msg: impl Into<DiagMsg>) -> &mut Self {
723        self.sub(Level::OnceHelp, msg, MultiSpan::new())
724    }
725
726    /// Add a help message attached to this diagnostic with a customizable highlighted message.
727    pub fn highlighted_help(&mut self, msgs: Vec<(impl Into<DiagMsg>, Style)>) -> &mut Self {
728        self.sub_with_highlights(Level::Help, msgs, MultiSpan::new())
729    }
730
731    /// Prints the span with some help above it.
732    /// This is like [`Diag::help()`], but it gets its own span.
733    pub fn span_help(&mut self, span: impl Into<MultiSpan>, msg: impl Into<DiagMsg>) -> &mut Self {
734        self.sub(Level::Help, msg, span)
735    }
736
737    fn sub(
738        &mut self,
739        level: Level,
740        msg: impl Into<DiagMsg>,
741        span: impl Into<MultiSpan>,
742    ) -> &mut Self {
743        self.children.push(SubDiagnostic {
744            level,
745            messages: vec![(msg.into(), Style::NoStyle)],
746            span: span.into(),
747        });
748        self
749    }
750
751    fn sub_with_highlights(
752        &mut self,
753        level: Level,
754        messages: Vec<(impl Into<DiagMsg>, Style)>,
755        span: MultiSpan,
756    ) -> &mut Self {
757        let messages = messages.into_iter().map(|(m, s)| (m.into(), s)).collect();
758        self.children.push(SubDiagnostic { level, messages, span });
759        self
760    }
761}
762
763/// Suggestions.
764impl Diag {
765    /// Disallow attaching suggestions to this diagnostic.
766    /// Any suggestions attached e.g. with the `span_suggestion_*` methods
767    /// (before and after the call to `disable_suggestions`) will be ignored.
768    pub fn disable_suggestions(&mut self) -> &mut Self {
769        self.suggestions = Suggestions::Disabled;
770        self
771    }
772
773    /// Prevent new suggestions from being added to this diagnostic.
774    ///
775    /// Suggestions added before the call to `.seal_suggestions()` will be preserved
776    /// and new suggestions will be ignored.
777    pub fn seal_suggestions(&mut self) -> &mut Self {
778        if let Suggestions::Enabled(suggestions) = &mut self.suggestions {
779            let suggestions_slice = std::mem::take(suggestions).into_boxed_slice();
780            self.suggestions = Suggestions::Sealed(suggestions_slice);
781        }
782        self
783    }
784
785    /// Helper for pushing to `self.suggestions`.
786    ///
787    /// A new suggestion is added if suggestions are enabled for this diagnostic.
788    /// Otherwise, they are ignored.
789    fn push_suggestion(&mut self, suggestion: CodeSuggestion) {
790        if let Suggestions::Enabled(suggestions) = &mut self.suggestions {
791            suggestions.push(suggestion);
792        }
793    }
794
795    /// Prints out a message with a suggested edit of the code.
796    ///
797    /// In case of short messages and a simple suggestion, rustc displays it as a label:
798    ///
799    /// ```text
800    /// try adding parentheses: `(tup.0).1`
801    /// ```
802    ///
803    /// The message
804    ///
805    /// * should not end in any punctuation (a `:` is added automatically)
806    /// * should not be a question (avoid language like "did you mean")
807    /// * should not contain any phrases like "the following", "as shown", etc.
808    /// * may look like "to do xyz, use" or "to do xyz, use abc"
809    /// * may contain a name of a function, variable, or type, but not whole expressions
810    ///
811    /// See [`CodeSuggestion`] for more information.
812    pub fn span_suggestion(
813        &mut self,
814        span: Span,
815        msg: impl Into<DiagMsg>,
816        suggestion: impl Into<DiagMsg>,
817        applicability: Applicability,
818    ) -> &mut Self {
819        self.span_suggestion_with_style(
820            span,
821            msg,
822            suggestion,
823            applicability,
824            SuggestionStyle::ShowCode,
825        );
826        self
827    }
828
829    /// [`Diag::span_suggestion()`] but you can set the [`SuggestionStyle`].
830    pub fn span_suggestion_with_style(
831        &mut self,
832        span: Span,
833        msg: impl Into<DiagMsg>,
834        suggestion: impl Into<DiagMsg>,
835        applicability: Applicability,
836        style: SuggestionStyle,
837    ) -> &mut Self {
838        self.push_suggestion(CodeSuggestion {
839            substitutions: vec![Substitution {
840                parts: vec![SubstitutionPart { snippet: suggestion.into(), span }],
841            }],
842            msg: msg.into(),
843            style,
844            applicability,
845        });
846        self
847    }
848
849    /// Show a suggestion that has multiple parts to it.
850    /// In other words, multiple changes need to be applied as part of this suggestion.
851    pub fn multipart_suggestion(
852        &mut self,
853        msg: impl Into<DiagMsg>,
854        substitutions: Vec<(Span, DiagMsg)>,
855        applicability: Applicability,
856    ) -> &mut Self {
857        self.multipart_suggestion_with_style(
858            msg,
859            substitutions,
860            applicability,
861            SuggestionStyle::ShowCode,
862        );
863        self
864    }
865
866    /// [`Diag::multipart_suggestion()`] but you can set the [`SuggestionStyle`].
867    pub fn multipart_suggestion_with_style(
868        &mut self,
869        msg: impl Into<DiagMsg>,
870        substitutions: Vec<(Span, DiagMsg)>,
871        applicability: Applicability,
872        style: SuggestionStyle,
873    ) -> &mut Self {
874        self.push_suggestion(CodeSuggestion {
875            substitutions: vec![Substitution {
876                parts: substitutions
877                    .into_iter()
878                    .map(|(span, snippet)| SubstitutionPart { span, snippet })
879                    .collect(),
880            }],
881            msg: msg.into(),
882            style,
883            applicability,
884        });
885        self
886    }
887}
888
889/// Flattens diagnostic messages, applying ANSI styles if requested.
890fn flatten_messages(messages: &[(DiagMsg, Style)], with_style: bool, level: Level) -> Cow<'_, str> {
891    if with_style {
892        match messages {
893            [] => Cow::Borrowed(""),
894            [(msg, Style::NoStyle)] => Cow::Borrowed(msg.as_str()),
895            [(msg, style)] => {
896                let mut res = String::new();
897                write_fmt(&mut res, msg, style, level);
898                Cow::Owned(res)
899            }
900            messages => {
901                let mut res = String::new();
902                for (msg, style) in messages {
903                    match style {
904                        Style::NoStyle => res.push_str(msg.as_str()),
905                        _ => write_fmt(&mut res, msg, style, level),
906                    }
907                }
908                Cow::Owned(res)
909            }
910        }
911    } else {
912        match messages {
913            [] => Cow::Borrowed(""),
914            [(message, _)] => Cow::Borrowed(message.as_str()),
915            messages => messages.iter().map(|(msg, _)| msg.as_str()).collect(),
916        }
917    }
918}
919
920fn write_fmt(output: &mut String, msg: &DiagMsg, style: &Style, level: Level) {
921    let style = style.to_color_spec(level);
922    let _ = write!(output, "{style}{}{style:#}", msg.as_str());
923}
924
925#[cfg(test)]
926mod tests {
927    use super::*;
928    use crate::{BytePos, ColorChoice, Span, source_map};
929
930    #[test]
931    fn test_styled_messages() {
932        // Create a diagnostic with styled messages
933        let mut diag = Diag::new(Level::Note, "test");
934
935        diag.highlighted_note(vec![
936            ("plain text ", Style::NoStyle),
937            ("removed", Style::Removal),
938            (" middle ", Style::NoStyle),
939            ("added", Style::Addition),
940        ]);
941
942        let sub = &diag.children[0];
943
944        // Without styles - just concatenated text
945        let plain = sub.label();
946        assert_eq!(plain, "plain text removed middle added");
947
948        // With styles - includes ANSI escape codes
949        let styled = sub.label_with_style(true);
950        assert_eq!(
951            styled.to_string(),
952            "plain text \u{1b}[91mremoved\u{1b}[0m middle \u{1b}[92madded\u{1b}[0m".to_string()
953        );
954    }
955
956    #[test]
957    fn test_inline_suggestion() {
958        let (var_span, var_sugg) = (Span::new(BytePos(66), BytePos(72)), "myVar");
959        let mut diag = Diag::new(Level::Note, "mutable variables should use mixedCase");
960        diag.span(var_span).span_suggestion(
961            var_span,
962            "mutable variables should use mixedCase",
963            var_sugg,
964            Applicability::MachineApplicable,
965        );
966
967        assert_eq!(diag.suggestions.len(), 1);
968        assert_eq!(diag.suggestions[0].applicability, Applicability::MachineApplicable);
969        assert_eq!(diag.suggestions[0].style, SuggestionStyle::ShowCode);
970
971        let expected = r#"note: mutable variables should use mixedCase
972 --> <test.sol>:4:17
973  |
9744 |         uint256 my_var = 0;
975  |                 ^^^^^^ help: mutable variables should use mixedCase: `myVar`
976
977"#;
978        assert_eq!(emit_human_diagnostics(diag), expected);
979    }
980
981    #[test]
982    fn test_suggestion() {
983        let (var_span, var_sugg) = (Span::new(BytePos(66), BytePos(72)), "myVar");
984        let mut diag = Diag::new(Level::Note, "mutable variables should use mixedCase");
985        diag.span(var_span).span_suggestion_with_style(
986            var_span,
987            "mutable variables should use mixedCase",
988            var_sugg,
989            Applicability::MachineApplicable,
990            SuggestionStyle::ShowAlways,
991        );
992
993        assert_eq!(diag.suggestions.len(), 1);
994        assert_eq!(diag.suggestions[0].applicability, Applicability::MachineApplicable);
995        assert_eq!(diag.suggestions[0].style, SuggestionStyle::ShowAlways);
996
997        let expected = r#"note: mutable variables should use mixedCase
998 --> <test.sol>:4:17
999  |
10004 |         uint256 my_var = 0;
1001  |                 ^^^^^^
1002  |
1003help: mutable variables should use mixedCase
1004  |
10054 -         uint256 my_var = 0;
10064 +         uint256 myVar = 0;
1007  |
1008
1009"#;
1010        assert_eq!(emit_human_diagnostics(diag), expected);
1011    }
1012
1013    #[test]
1014    fn test_suggestion_with_footer() {
1015        let (var_span, var_sugg) = (Span::new(BytePos(66), BytePos(72)), "myVar");
1016        let mut diag = Diag::new(Level::Note, "mutable variables should use mixedCase");
1017        diag.span(var_span)
1018            .span_suggestion_with_style(
1019                var_span,
1020                "mutable variables should use mixedCase",
1021                var_sugg,
1022                Applicability::MachineApplicable,
1023                SuggestionStyle::ShowAlways,
1024            )
1025            .help("some footer help msg that should be displayed at the very bottom");
1026
1027        assert_eq!(diag.suggestions.len(), 1);
1028        assert_eq!(diag.suggestions[0].applicability, Applicability::MachineApplicable);
1029        assert_eq!(diag.suggestions[0].style, SuggestionStyle::ShowAlways);
1030
1031        let expected = r#"note: mutable variables should use mixedCase
1032 --> <test.sol>:4:17
1033  |
10344 |         uint256 my_var = 0;
1035  |                 ^^^^^^
1036  |
1037help: mutable variables should use mixedCase
1038  |
10394 -         uint256 my_var = 0;
10404 +         uint256 myVar = 0;
1041  |
1042  = help: some footer help msg that should be displayed at the very bottom
1043
1044"#;
1045        assert_eq!(emit_human_diagnostics(diag), expected);
1046    }
1047
1048    #[test]
1049    fn test_multispan_suggestion() {
1050        let (pub_span, pub_sugg) = (Span::new(BytePos(36), BytePos(42)), "external".into());
1051        let (view_span, view_sugg) = (Span::new(BytePos(43), BytePos(47)), "pure".into());
1052        let mut diag = Diag::new(Level::Warning, "inefficient visibility and mutability");
1053        diag.span(vec![pub_span, view_span]).multipart_suggestion(
1054            "consider changing visibility and mutability",
1055            vec![(pub_span, pub_sugg), (view_span, view_sugg)],
1056            Applicability::MaybeIncorrect,
1057        );
1058
1059        assert_eq!(diag.suggestions[0].substitutions.len(), 1);
1060        assert_eq!(diag.suggestions[0].substitutions[0].parts.len(), 2);
1061        assert_eq!(diag.suggestions[0].applicability, Applicability::MaybeIncorrect);
1062        assert_eq!(diag.suggestions[0].style, SuggestionStyle::ShowCode);
1063
1064        let expected = r#"warning: inefficient visibility and mutability
1065 --> <test.sol>:3:20
1066  |
10673 |     function foo() public view {
1068  |                    ^^^^^^ ^^^^
1069  |
1070help: consider changing visibility and mutability
1071  |
10723 -     function foo() public view {
10733 +     function foo() external pure {
1074  |
1075
1076"#;
1077        assert_eq!(emit_human_diagnostics(diag), expected);
1078    }
1079
1080    #[test]
1081    #[cfg(feature = "json")]
1082    fn test_json_suggestion() {
1083        let (var_span, var_sugg) = (Span::new(BytePos(66), BytePos(72)), "myVar");
1084        let mut diag = Diag::new(Level::Note, "mutable variables should use mixedCase");
1085        diag.span(var_span).span_suggestion(
1086            var_span,
1087            "mutable variables should use mixedCase",
1088            var_sugg,
1089            Applicability::MachineApplicable,
1090        );
1091
1092        assert_eq!(diag.suggestions.len(), 1);
1093        assert_eq!(diag.suggestions[0].applicability, Applicability::MachineApplicable);
1094        assert_eq!(diag.suggestions[0].style, SuggestionStyle::ShowCode);
1095
1096        let expected = json!({
1097            "$message_type": "diagnostic",
1098            "message": "mutable variables should use mixedCase",
1099            "code": null,
1100            "level": "note",
1101            "spans": [{
1102                "file_name": "<test.sol>",
1103                "byte_start": 66,
1104                "byte_end": 72,
1105                "line_start": 4,
1106                "line_end": 4,
1107                "column_start": 17,
1108                "column_end": 23,
1109                "is_primary": true,
1110                "text": [{
1111                    "text": "        uint256 my_var = 0;",
1112                    "highlight_start": 17,
1113                    "highlight_end": 23
1114                }],
1115                "label": null,
1116                "suggested_replacement": null
1117            }],
1118            "children": [{
1119                "message": "mutable variables should use mixedCase",
1120                "code": null,
1121                "level": "help",
1122                "spans": [{
1123                    "file_name": "<test.sol>",
1124                    "byte_start": 66,
1125                    "byte_end": 72,
1126                    "line_start": 4,
1127                    "line_end": 4,
1128                    "column_start": 17,
1129                    "column_end": 23,
1130                    "is_primary": true,
1131                    "text": [{
1132                        "text": "        uint256 my_var = 0;",
1133                        "highlight_start": 17,
1134                        "highlight_end": 23
1135                    }],
1136                    "label": null,
1137                    "suggested_replacement": "myVar"
1138                }],
1139                "children": [],
1140                "rendered": null
1141            }],
1142            "rendered": "note: mutable variables should use mixedCase\n --> <test.sol>:4:17\n  |\n4 |         uint256 my_var = 0;\n  |                 ^^^^^^ help: mutable variables should use mixedCase: `myVar`\n\n"
1143        });
1144
1145        assert_eq!(emit_json_diagnostics(diag), expected);
1146    }
1147
1148    #[test]
1149    #[cfg(feature = "json")]
1150    fn test_multispan_json_suggestion() {
1151        let (pub_span, pub_sugg) = (Span::new(BytePos(36), BytePos(42)), "external".into());
1152        let (view_span, view_sugg) = (Span::new(BytePos(43), BytePos(47)), "pure".into());
1153        let mut diag = Diag::new(Level::Warning, "inefficient visibility and mutability");
1154        diag.span(vec![pub_span, view_span]).multipart_suggestion(
1155            "consider changing visibility and mutability",
1156            vec![(pub_span, pub_sugg), (view_span, view_sugg)],
1157            Applicability::MaybeIncorrect,
1158        );
1159
1160        assert_eq!(diag.suggestions[0].substitutions.len(), 1);
1161        assert_eq!(diag.suggestions[0].substitutions[0].parts.len(), 2);
1162        assert_eq!(diag.suggestions[0].applicability, Applicability::MaybeIncorrect);
1163        assert_eq!(diag.suggestions[0].style, SuggestionStyle::ShowCode);
1164
1165        let expected = json!({
1166            "$message_type": "diagnostic",
1167            "message": "inefficient visibility and mutability",
1168            "code": null,
1169            "level": "warning",
1170            "spans": [
1171                {
1172                    "file_name": "<test.sol>",
1173                    "byte_start": 36,
1174                    "byte_end": 42,
1175                    "line_start": 3,
1176                    "line_end": 3,
1177                    "column_start": 20,
1178                    "column_end": 26,
1179                    "is_primary": true,
1180                    "text": [{
1181                        "text": "    function foo() public view {",
1182                        "highlight_start": 20,
1183                        "highlight_end": 26
1184                    }],
1185                    "label": null,
1186                    "suggested_replacement": null
1187                },
1188                {
1189                    "file_name": "<test.sol>",
1190                    "byte_start": 43,
1191                    "byte_end": 47,
1192                    "line_start": 3,
1193                    "line_end": 3,
1194                    "column_start": 27,
1195                    "column_end": 31,
1196                    "is_primary": true,
1197                    "text": [{
1198                        "text": "    function foo() public view {",
1199                        "highlight_start": 27,
1200                        "highlight_end": 31
1201                    }],
1202                    "label": null,
1203                    "suggested_replacement": null
1204                }
1205            ],
1206            "children": [{
1207                "message": "consider changing visibility and mutability",
1208                "code": null,
1209                "level": "help",
1210                "spans": [
1211                    {
1212                        "file_name": "<test.sol>",
1213                        "byte_start": 36,
1214                        "byte_end": 42,
1215                        "line_start": 3,
1216                        "line_end": 3,
1217                        "column_start": 20,
1218                        "column_end": 26,
1219                        "is_primary": true,
1220                        "text": [{
1221                            "text": "    function foo() public view {",
1222                            "highlight_start": 20,
1223                            "highlight_end": 26
1224                        }],
1225                        "label": null,
1226                        "suggested_replacement": "external"
1227                    },
1228                    {
1229                        "file_name": "<test.sol>",
1230                        "byte_start": 43,
1231                        "byte_end": 47,
1232                        "line_start": 3,
1233                        "line_end": 3,
1234                        "column_start": 27,
1235                        "column_end": 31,
1236                        "is_primary": true,
1237                        "text": [{
1238                            "text": "    function foo() public view {",
1239                            "highlight_start": 27,
1240                            "highlight_end": 31
1241                        }],
1242                        "label": null,
1243                        "suggested_replacement": "pure"
1244                    }
1245                ],
1246                "children": [],
1247                "rendered": null
1248            }],
1249            "rendered": "warning: inefficient visibility and mutability\n --> <test.sol>:3:20\n  |\n3 |     function foo() public view {\n  |                    ^^^^^^ ^^^^\n  |\nhelp: consider changing visibility and mutability\n  |\n3 -     function foo() public view {\n3 +     function foo() external pure {\n  |\n\n"
1250        });
1251        assert_eq!(emit_json_diagnostics(diag), expected);
1252    }
1253
1254    // --- HELPERS -------------------------------------------------------------
1255
1256    const CONTRACT: &str = r#"
1257contract Test {
1258    function foo() public view {
1259        uint256 my_var = 0;
1260    }
1261}"#;
1262
1263    // Helper to setup the run the human-readable emitter.
1264    fn emit_human_diagnostics(diag: Diag) -> String {
1265        let sm = source_map::SourceMap::empty();
1266        sm.new_source_file(source_map::FileName::custom("test.sol"), CONTRACT.to_string()).unwrap();
1267
1268        let dcx = DiagCtxt::with_buffer_emitter(Some(std::sync::Arc::new(sm)), ColorChoice::Never);
1269        let _ = dcx.emit_diagnostic(diag);
1270
1271        dcx.emitted_diagnostics().unwrap().0
1272    }
1273
1274    #[cfg(feature = "json")]
1275    use {
1276        serde_json::{Value, json},
1277        std::sync::{Arc, Mutex},
1278    };
1279
1280    // A sharable writer
1281    #[cfg(feature = "json")]
1282    #[derive(Clone)]
1283    struct SharedWriter(Arc<Mutex<Vec<u8>>>);
1284
1285    #[cfg(feature = "json")]
1286    impl std::io::Write for SharedWriter {
1287        fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
1288            self.0.lock().unwrap().write(buf)
1289        }
1290        fn flush(&mut self) -> std::io::Result<()> {
1291            self.0.lock().unwrap().flush()
1292        }
1293    }
1294
1295    // Helper to setup the run the json emitter. Outputs a json object for the given diagnostics.
1296    #[cfg(feature = "json")]
1297    fn emit_json_diagnostics(diag: Diag) -> Value {
1298        let sm = Arc::new(source_map::SourceMap::empty());
1299        sm.new_source_file(source_map::FileName::custom("test.sol"), CONTRACT.to_string()).unwrap();
1300
1301        let writer = Arc::new(Mutex::new(Vec::new()));
1302        let emitter = JsonEmitter::new(Box::new(SharedWriter(writer.clone())), Arc::clone(&sm))
1303            .rustc_like(true);
1304        let dcx = DiagCtxt::new(Box::new(emitter));
1305        let _ = dcx.emit_diagnostic(diag);
1306
1307        let buffer = writer.lock().unwrap();
1308        serde_json::from_str(
1309            &String::from_utf8(buffer.clone()).expect("JSON output was not valid UTF-8"),
1310        )
1311        .expect("failed to deserialize JSON")
1312    }
1313}