solar_interface/diagnostics/
context.rs

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