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