Skip to main content

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