Skip to main content

solar_interface/diagnostics/
context.rs

1use super::{
2    BugAbort, Diag, DiagBuilder, DiagMsg, DynEmitter, EmissionGuarantee, EmittedDiagnostics,
3    ErrorGuaranteed, FatalAbort, HumanBufferEmitter, Level, MultiSpan, SilentEmitter,
4    emitter::HumanEmitter,
5};
6use crate::{Result, SourceMap, Span};
7use anstream::ColorChoice;
8use solar_config::{CompileOpts, ErrorFormat};
9use solar_data_structures::{map::FxHashSet, sync::Mutex};
10use std::{borrow::Cow, fmt, hash::BuildHasher, num::NonZeroUsize, sync::Arc};
11
12/// Flags that control the behaviour of a [`DiagCtxt`].
13#[derive(Clone, Copy, Debug)]
14pub struct DiagCtxtFlags {
15    /// If false, warning-level lints are suppressed.
16    pub can_emit_warnings: bool,
17    /// If Some, the Nth error-level diagnostic is upgraded to bug-level.
18    pub treat_err_as_bug: Option<NonZeroUsize>,
19    /// If true, identical diagnostics are reported only once.
20    pub deduplicate_diagnostics: bool,
21    /// Track where errors are created. Enabled with `-Ztrack-diagnostics`, and by default in debug
22    /// builds.
23    pub track_diagnostics: bool,
24}
25
26impl Default for DiagCtxtFlags {
27    fn default() -> Self {
28        Self {
29            can_emit_warnings: true,
30            treat_err_as_bug: None,
31            deduplicate_diagnostics: true,
32            track_diagnostics: cfg!(debug_assertions),
33        }
34    }
35}
36
37impl DiagCtxtFlags {
38    /// Updates the flags from the given options.
39    ///
40    /// Looks at the following options:
41    /// - `unstable.ui_testing`
42    /// - `unstable.track_diagnostics`
43    /// - `no_warnings`
44    pub fn update_from_opts(&mut self, opts: &CompileOpts) {
45        self.deduplicate_diagnostics &= !opts.unstable.ui_testing;
46        self.track_diagnostics &= !opts.unstable.ui_testing;
47        self.track_diagnostics |= opts.unstable.track_diagnostics;
48        self.can_emit_warnings &= !opts.no_warnings;
49    }
50}
51
52/// A handler that deals with errors and other compiler output.
53///
54/// Certain errors (fatal, bug) may cause immediate exit, others log errors for later reporting.
55pub struct DiagCtxt {
56    inner: Mutex<DiagCtxtInner>,
57}
58
59impl fmt::Debug for DiagCtxt {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        let mut s = f.debug_struct("DiagCtxt");
62        if let Some(inner) = self.inner.try_lock() {
63            s.field("flags", &inner.flags)
64                .field("err_count", &inner.err_count)
65                .field("warn_count", &inner.warn_count)
66                .field("note_count", &inner.note_count);
67        } else {
68            s.field("inner", &format_args!("<locked>"));
69        }
70        s.finish_non_exhaustive()
71    }
72}
73
74struct DiagCtxtInner {
75    emitter: Box<DynEmitter>,
76
77    flags: DiagCtxtFlags,
78    allowed_diagnostic_codes: FxHashSet<String>,
79
80    /// The number of errors that have been emitted, including duplicates.
81    ///
82    /// This is not necessarily the count that's reported to the user once
83    /// compilation ends.
84    err_count: usize,
85    deduplicated_err_count: usize,
86    /// The warning count, used for a recap upon finishing
87    warn_count: usize,
88    deduplicated_warn_count: usize,
89    /// The note count, used for a recap upon finishing
90    note_count: usize,
91    deduplicated_note_count: usize,
92
93    /// This set contains a hash of every diagnostic that has been emitted by this `DiagCtxt`.
94    /// These hashes are used to avoid emitting the same error twice.
95    emitted_diagnostics: FxHashSet<u64>,
96}
97
98impl DiagCtxt {
99    /// Creates a new `DiagCtxt` with the given diagnostics emitter.
100    pub fn new(emitter: Box<DynEmitter>) -> Self {
101        Self {
102            inner: Mutex::new(DiagCtxtInner {
103                emitter,
104                flags: DiagCtxtFlags::default(),
105                allowed_diagnostic_codes: FxHashSet::default(),
106                err_count: 0,
107                deduplicated_err_count: 0,
108                warn_count: 0,
109                deduplicated_warn_count: 0,
110                note_count: 0,
111                deduplicated_note_count: 0,
112                emitted_diagnostics: FxHashSet::default(),
113            }),
114        }
115    }
116
117    /// Creates a new `DiagCtxt` with a stderr emitter for emitting one-off/early fatal errors that
118    /// contain no source information.
119    pub fn new_early() -> Self {
120        Self::with_stderr_emitter(None).with_flags(|flags| flags.track_diagnostics = false)
121    }
122
123    /// Creates a new `DiagCtxt` with a test emitter.
124    pub fn with_test_emitter(source_map: Option<Arc<SourceMap>>) -> Self {
125        Self::new(Box::new(HumanEmitter::test().source_map(source_map)))
126    }
127
128    /// Creates a new `DiagCtxt` with a stderr emitter.
129    pub fn with_stderr_emitter(source_map: Option<Arc<SourceMap>>) -> Self {
130        Self::with_stderr_emitter_and_color(source_map, ColorChoice::Auto)
131    }
132
133    /// Creates a new `DiagCtxt` with a stderr emitter and a color choice.
134    pub fn with_stderr_emitter_and_color(
135        source_map: Option<Arc<SourceMap>>,
136        color_choice: ColorChoice,
137    ) -> Self {
138        Self::new(Box::new(HumanEmitter::stderr(color_choice).source_map(source_map)))
139    }
140
141    /// Creates a new `DiagCtxt` with a silent emitter.
142    ///
143    /// Fatal diagnostics will still be emitted, optionally with the given note.
144    pub fn with_silent_emitter(fatal_note: Option<String>) -> Self {
145        let fatal_emitter = HumanEmitter::stderr(Default::default());
146        Self::new(Box::new(SilentEmitter::new(fatal_emitter).with_note(fatal_note)))
147            .disable_warnings()
148    }
149
150    /// Creates a new `DiagCtxt` with a human emitter that emits diagnostics to a local buffer.
151    pub fn with_buffer_emitter(
152        source_map: Option<Arc<SourceMap>>,
153        color_choice: ColorChoice,
154    ) -> Self {
155        Self::new(Box::new(HumanBufferEmitter::new(color_choice).source_map(source_map)))
156    }
157
158    /// Creates a new `DiagCtxt` from the given options.
159    ///
160    /// This is the default `DiagCtxt` used by the `Session` if one is not provided manually.
161    /// It looks at the following options:
162    /// - `error_format`
163    /// - `color`
164    /// - `unstable.ui_testing`
165    /// - `unstable.track_diagnostics`
166    /// - `no_warnings`
167    /// - `error_format_human`
168    /// - `diagnostic_width`
169    ///
170    /// The default is human emitter to stderr.
171    ///
172    /// See also [`DiagCtxtFlags::update_from_opts`].
173    pub fn from_opts(opts: &solar_config::CompileOpts) -> Self {
174        let source_map = Arc::new(SourceMap::empty());
175        let emitter: Box<DynEmitter> = match opts.error_format {
176            ErrorFormat::Human => {
177                let human = HumanEmitter::stderr(opts.color)
178                    .source_map(Some(source_map))
179                    .ui_testing(opts.unstable.ui_testing)
180                    .human_kind(opts.error_format_human)
181                    .terminal_width(opts.diagnostic_width);
182                Box::new(human)
183            }
184            #[cfg(feature = "json")]
185            ErrorFormat::Json | ErrorFormat::RustcJson => {
186                // `io::Stderr` is not buffered.
187                let writer = Box::new(std::io::BufWriter::new(std::io::stderr()));
188                let json = crate::diagnostics::JsonEmitter::new(writer, source_map)
189                    .pretty(opts.pretty_json_err)
190                    .rustc_like(matches!(opts.error_format, ErrorFormat::RustcJson))
191                    .ui_testing(opts.unstable.ui_testing)
192                    .human_kind(opts.error_format_human)
193                    .terminal_width(opts.diagnostic_width);
194                Box::new(json)
195            }
196            format => unimplemented!("{format:?}"),
197        };
198        Self::new(emitter)
199            .with_flags(|flags| flags.update_from_opts(opts))
200            .with_allowed_diagnostic_codes(opts.allow.iter().cloned())
201    }
202
203    /// Adds diagnostic codes that should be allowed.
204    pub fn with_allowed_diagnostic_codes(
205        mut self,
206        codes: impl IntoIterator<Item = String>,
207    ) -> Self {
208        self.set_allowed_diagnostic_codes_mut(codes);
209        self
210    }
211
212    /// Sets the emitter to [`SilentEmitter`].
213    pub fn make_silent(&self, fatal_note: Option<String>, emit_fatal: bool) {
214        self.wrap_emitter(|prev| {
215            Box::new(SilentEmitter::new_boxed(emit_fatal.then_some(prev)).with_note(fatal_note))
216        });
217    }
218
219    /// Sets the inner emitter. Returns the previous emitter.
220    pub fn set_emitter(&self, emitter: Box<DynEmitter>) -> Box<DynEmitter> {
221        std::mem::replace(&mut self.inner.lock().emitter, emitter)
222    }
223
224    /// Wraps the current emitter with the given closure.
225    pub fn wrap_emitter(&self, f: impl FnOnce(Box<DynEmitter>) -> Box<DynEmitter>) {
226        struct FakeEmitter;
227        impl crate::diagnostics::Emitter for FakeEmitter {
228            fn emit_diagnostic(&mut self, _diagnostic: &mut Diag) {}
229        }
230
231        let mut inner = self.inner.lock();
232        let prev = std::mem::replace(&mut inner.emitter, Box::new(FakeEmitter));
233        inner.emitter = f(prev);
234    }
235
236    /// Gets the source map associated with this context.
237    pub fn source_map(&self) -> Option<Arc<SourceMap>> {
238        self.inner.lock().emitter.source_map().cloned()
239    }
240
241    /// Gets the source map associated with this context.
242    pub fn source_map_mut(&mut self) -> Option<&Arc<SourceMap>> {
243        self.inner.get_mut().emitter.source_map()
244    }
245
246    /// Sets flags.
247    pub fn with_flags(mut self, f: impl FnOnce(&mut DiagCtxtFlags)) -> Self {
248        self.set_flags_mut(f);
249        self
250    }
251
252    /// Sets flags.
253    pub fn set_flags(&self, f: impl FnOnce(&mut DiagCtxtFlags)) {
254        f(&mut self.inner.lock().flags);
255    }
256
257    /// Sets flags.
258    pub fn set_flags_mut(&mut self, f: impl FnOnce(&mut DiagCtxtFlags)) {
259        f(&mut self.inner.get_mut().flags);
260    }
261
262    /// Adds diagnostic codes that should be allowed.
263    pub fn set_allowed_diagnostic_codes(&self, codes: impl IntoIterator<Item = String>) {
264        self.inner.lock().allowed_diagnostic_codes.extend(codes);
265    }
266
267    /// Adds diagnostic codes that should be allowed.
268    pub fn set_allowed_diagnostic_codes_mut(&mut self, codes: impl IntoIterator<Item = String>) {
269        self.inner.get_mut().allowed_diagnostic_codes.extend(codes);
270    }
271
272    /// Disables emitting warnings.
273    pub fn disable_warnings(self) -> Self {
274        self.with_flags(|f| f.can_emit_warnings = false)
275    }
276
277    /// Returns `true` if diagnostics are being tracked.
278    pub fn track_diagnostics(&self) -> bool {
279        self.inner.lock().flags.track_diagnostics
280    }
281
282    /// Emits the given diagnostic with this context.
283    #[inline]
284    pub fn emit_diagnostic(&self, mut diagnostic: Diag) -> Result<(), ErrorGuaranteed> {
285        self.emit_diagnostic_without_consuming(&mut diagnostic)
286    }
287
288    /// Emits the given diagnostic with this context, without consuming the diagnostic.
289    ///
290    /// **Note:** This function is intended to be used only internally in `DiagBuilder`.
291    /// Use [`emit_diagnostic`](Self::emit_diagnostic) instead.
292    pub(super) fn emit_diagnostic_without_consuming(
293        &self,
294        diagnostic: &mut Diag,
295    ) -> Result<(), ErrorGuaranteed> {
296        self.inner.lock().emit_diagnostic_without_consuming(diagnostic)
297    }
298
299    /// Returns the number of errors that have been emitted, including duplicates.
300    pub fn err_count(&self) -> usize {
301        self.inner.lock().err_count
302    }
303
304    /// Returns `Err` if any errors have been emitted.
305    pub fn has_errors(&self) -> Result<(), ErrorGuaranteed> {
306        if self.inner.lock().has_errors() { Err(ErrorGuaranteed::new_unchecked()) } else { Ok(()) }
307    }
308
309    /// Returns the number of warnings that have been emitted, including duplicates.
310    pub fn warn_count(&self) -> usize {
311        self.inner.lock().warn_count
312    }
313
314    /// Returns the number of notes that have been emitted, including duplicates.
315    pub fn note_count(&self) -> usize {
316        self.inner.lock().note_count
317    }
318
319    /// Returns the emitted diagnostics as a result. Can be empty.
320    ///
321    /// Returns `None` if the underlying emitter is not a human buffer emitter created with
322    /// [`with_buffer_emitter`](Self::with_buffer_emitter).
323    ///
324    /// Results `Ok` if there are no errors, `Err` otherwise.
325    ///
326    /// # Examples
327    ///
328    /// Print diagnostics to `stdout` if there are no errors, otherwise propagate with `?`:
329    ///
330    /// ```no_run
331    /// # fn f(dcx: solar_interface::diagnostics::DiagCtxt) -> Result<(), Box<dyn std::error::Error>> {
332    /// println!("{}", dcx.emitted_diagnostics_result().unwrap()?);
333    /// # Ok(())
334    /// # }
335    /// ```
336    #[inline]
337    pub fn emitted_diagnostics_result(
338        &self,
339    ) -> Option<Result<EmittedDiagnostics, EmittedDiagnostics>> {
340        let inner = self.inner.lock();
341        let diags = EmittedDiagnostics(inner.emitter.local_buffer()?.to_string());
342        Some(if inner.has_errors() { Err(diags) } else { Ok(diags) })
343    }
344
345    /// Returns the emitted diagnostics. Can be empty.
346    ///
347    /// Returns `None` if the underlying emitter is not a human buffer emitter created with
348    /// [`with_buffer_emitter`](Self::with_buffer_emitter).
349    pub fn emitted_diagnostics(&self) -> Option<EmittedDiagnostics> {
350        let inner = self.inner.lock();
351        Some(EmittedDiagnostics(inner.emitter.local_buffer()?.to_string()))
352    }
353
354    /// Returns `Err` with the printed diagnostics if any errors have been emitted.
355    ///
356    /// Returns `None` if the underlying emitter is not a human buffer emitter created with
357    /// [`with_buffer_emitter`](Self::with_buffer_emitter).
358    pub fn emitted_errors(&self) -> Option<Result<(), EmittedDiagnostics>> {
359        let inner = self.inner.lock();
360        let buffer = inner.emitter.local_buffer()?;
361        Some(if inner.has_errors() { Err(EmittedDiagnostics(buffer.to_string())) } else { Ok(()) })
362    }
363
364    /// Emits a diagnostic if any warnings or errors have been emitted.
365    pub fn print_error_count(&self) -> Result {
366        self.inner.lock().print_error_count()
367    }
368}
369
370/// Diag constructors.
371///
372/// Note that methods returning a [`DiagBuilder`] must also marked with `#[track_caller]`.
373impl DiagCtxt {
374    /// Creates a builder at the given `level` with the given `msg`.
375    #[track_caller]
376    pub fn diag<G: EmissionGuarantee>(
377        &self,
378        level: Level,
379        msg: impl Into<DiagMsg>,
380    ) -> DiagBuilder<'_, G> {
381        DiagBuilder::new(self, level, msg)
382    }
383
384    /// Creates a builder at the `Bug` level with the given `msg`.
385    #[track_caller]
386    #[cold]
387    pub fn bug(&self, msg: impl Into<DiagMsg>) -> DiagBuilder<'_, BugAbort> {
388        self.diag(Level::Bug, msg)
389    }
390
391    /// Creates a builder at the `Fatal` level with the given `msg`.
392    #[track_caller]
393    #[cold]
394    pub fn fatal(&self, msg: impl Into<DiagMsg>) -> DiagBuilder<'_, FatalAbort> {
395        self.diag(Level::Fatal, msg)
396    }
397
398    /// Creates a builder at the `Error` level with the given `msg`.
399    #[track_caller]
400    pub fn err(&self, msg: impl Into<DiagMsg>) -> DiagBuilder<'_, ErrorGuaranteed> {
401        self.diag(Level::Error, msg)
402    }
403
404    /// Emits an error at `span` with the given `msg`.
405    #[track_caller]
406    pub fn emit_err(&self, span: impl Into<MultiSpan>, msg: impl Into<DiagMsg>) -> ErrorGuaranteed {
407        self.err(msg).span(span).emit()
408    }
409
410    /// Emits an error at `span` with an attached note.
411    #[track_caller]
412    pub fn emit_err_note(
413        &self,
414        span: impl Into<MultiSpan>,
415        msg: impl Into<DiagMsg>,
416        note: impl Into<DiagMsg>,
417    ) -> ErrorGuaranteed {
418        self.err(msg).span(span).note(note).emit()
419    }
420
421    /// Emits an error at `span` with an attached span note.
422    #[track_caller]
423    pub fn emit_err_span_note(
424        &self,
425        span: impl Into<MultiSpan>,
426        msg: impl Into<DiagMsg>,
427        note_span: impl Into<MultiSpan>,
428        note: impl Into<DiagMsg>,
429    ) -> ErrorGuaranteed {
430        self.err(msg).span(span).span_note(note_span, note).emit()
431    }
432
433    /// Emits an error at `span` with an attached span label.
434    #[track_caller]
435    pub fn emit_err_label(
436        &self,
437        span: impl Into<MultiSpan>,
438        msg: impl Into<DiagMsg>,
439        label_span: Span,
440        label: impl Into<DiagMsg>,
441    ) -> ErrorGuaranteed {
442        self.err(msg).span(span).span_label(label_span, label).emit()
443    }
444
445    /// Creates a builder at the `Warning` level with the given `msg`.
446    ///
447    /// Attempting to `.emit()` the builder will only emit if `can_emit_warnings` is `true`.
448    #[track_caller]
449    pub fn warn(&self, msg: impl Into<DiagMsg>) -> DiagBuilder<'_, ()> {
450        self.diag(Level::Warning, msg)
451    }
452
453    /// Creates a builder at the `Help` level with the given `msg`.
454    #[track_caller]
455    pub fn help(&self, msg: impl Into<DiagMsg>) -> DiagBuilder<'_, ()> {
456        self.diag(Level::Help, msg)
457    }
458
459    /// Creates a builder at the `Note` level with the given `msg`.
460    #[track_caller]
461    pub fn note(&self, msg: impl Into<DiagMsg>) -> DiagBuilder<'_, ()> {
462        self.diag(Level::Note, msg)
463    }
464}
465
466impl DiagCtxtInner {
467    fn emit_diagnostic(&mut self, mut diagnostic: Diag) -> Result<(), ErrorGuaranteed> {
468        self.emit_diagnostic_without_consuming(&mut diagnostic)
469    }
470
471    fn emit_diagnostic_without_consuming(
472        &mut self,
473        diagnostic: &mut Diag,
474    ) -> Result<(), ErrorGuaranteed> {
475        if self.is_allowed_diagnostic(diagnostic) {
476            return Ok(());
477        }
478
479        if diagnostic.level == Level::Warning && !self.flags.can_emit_warnings {
480            return Ok(());
481        }
482
483        if diagnostic.level == Level::Allow {
484            return Ok(());
485        }
486
487        if matches!(diagnostic.level, Level::Error | Level::Fatal) && self.treat_err_as_bug() {
488            diagnostic.level = Level::Bug;
489        }
490
491        let already_emitted = self.insert_diagnostic(diagnostic);
492        if !(self.flags.deduplicate_diagnostics && already_emitted) {
493            // Remove duplicate `Once*` subdiagnostics.
494            diagnostic.children.retain(|sub| {
495                if !matches!(sub.level, Level::OnceNote | Level::OnceHelp) {
496                    return true;
497                }
498                let sub_already_emitted = self.insert_diagnostic(sub);
499                !sub_already_emitted
500            });
501
502            // if already_emitted {
503            //     diagnostic.note(
504            //         "duplicate diagnostic emitted due to `-Z deduplicate-diagnostics=no`",
505            //     );
506            // }
507
508            self.emitter.emit_diagnostic(diagnostic);
509            if diagnostic.is_error() {
510                self.deduplicated_err_count += 1;
511            } else if diagnostic.level == Level::Warning {
512                self.deduplicated_warn_count += 1;
513            } else if diagnostic.is_note() {
514                self.deduplicated_note_count += 1;
515            }
516        }
517
518        if diagnostic.is_error() {
519            self.bump_err_count();
520            Err(ErrorGuaranteed::new_unchecked())
521        } else {
522            if diagnostic.level == Level::Warning {
523                self.bump_warn_count();
524            } else if diagnostic.is_note() {
525                self.bump_note_count();
526            }
527            // Don't bump any counters for `Help`, `OnceHelp`, or `Allow`
528            Ok(())
529        }
530    }
531
532    fn print_error_count(&mut self) -> Result {
533        // self.emit_stashed_diagnostics();
534
535        if self.treat_err_as_bug() {
536            return Ok(());
537        }
538
539        let errors = match self.deduplicated_err_count {
540            0 => None,
541            1 => Some(Cow::from("aborting due to 1 previous error")),
542            count => Some(Cow::from(format!("aborting due to {count} previous errors"))),
543        };
544
545        let mut others = Vec::with_capacity(2);
546        match self.deduplicated_warn_count {
547            1 => others.push(Cow::from("1 warning emitted")),
548            count if count > 1 => others.push(Cow::from(format!("{count} warnings emitted"))),
549            _ => {}
550        }
551        match self.deduplicated_note_count {
552            1 => others.push(Cow::from("1 note emitted")),
553            count if count > 1 => others.push(Cow::from(format!("{count} notes emitted"))),
554            _ => {}
555        }
556
557        match (errors, others.is_empty()) {
558            (None, true) => Ok(()),
559            (None, false) => {
560                // TODO: Don't emit in tests since it's not handled by `ui_test`: https://github.com/oli-obk/ui_test/issues/324
561                if self.flags.track_diagnostics {
562                    let msg = others.join(", ");
563                    self.emitter.emit_diagnostic(&mut Diag::new(Level::Warning, msg));
564                }
565                Ok(())
566            }
567            (Some(e), true) => self.emit_diagnostic(Diag::new(Level::Error, e)),
568            (Some(e), false) => self
569                .emit_diagnostic(Diag::new(Level::Error, format!("{}; {}", e, others.join(", ")))),
570        }
571    }
572
573    /// Inserts the given diagnostic into the set of emitted diagnostics.
574    /// Returns `true` if the diagnostic was already emitted.
575    fn insert_diagnostic<H: std::hash::Hash>(&mut self, diag: &H) -> bool {
576        let hash = solar_data_structures::map::rustc_hash::FxBuildHasher.hash_one(diag);
577        !self.emitted_diagnostics.insert(hash)
578    }
579
580    fn treat_err_as_bug(&self) -> bool {
581        self.flags.treat_err_as_bug.is_some_and(|c| self.err_count >= c.get())
582    }
583
584    fn is_allowed_diagnostic(&self, diagnostic: &Diag) -> bool {
585        diagnostic.level == Level::Warning
586            && diagnostic.id().is_some_and(|id| self.allowed_diagnostic_codes.contains(id))
587    }
588
589    fn bump_err_count(&mut self) {
590        self.err_count += 1;
591        self.panic_if_treat_err_as_bug();
592    }
593
594    fn bump_warn_count(&mut self) {
595        self.warn_count += 1;
596    }
597
598    fn bump_note_count(&mut self) {
599        self.note_count += 1;
600    }
601
602    fn has_errors(&self) -> bool {
603        self.err_count > 0
604    }
605
606    fn panic_if_treat_err_as_bug(&self) {
607        if self.treat_err_as_bug() {
608            match (self.err_count, self.flags.treat_err_as_bug.unwrap().get()) {
609                (1, 1) => panic!("aborting due to `-Z treat-err-as-bug=1`"),
610                (count, val) => {
611                    panic!("aborting after {count} errors due to `-Z treat-err-as-bug={val}`")
612                }
613            }
614        }
615    }
616}