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_data_structures::{map::FxHashSet, sync::Lock};
8use std::{borrow::Cow, hash::BuildHasher, num::NonZeroUsize, sync::Arc};
9
10/// Flags that control the behaviour of a [`DiagCtxt`].
11#[derive(Clone, Copy)]
12pub struct DiagCtxtFlags {
13    /// If false, warning-level lints are suppressed.
14    pub can_emit_warnings: bool,
15    /// If Some, the Nth error-level diagnostic is upgraded to bug-level.
16    pub treat_err_as_bug: Option<NonZeroUsize>,
17    /// If true, identical diagnostics are reported only once.
18    pub deduplicate_diagnostics: bool,
19    /// Track where errors are created. Enabled with `-Ztrack-diagnostics`, and by default in debug
20    /// builds.
21    pub track_diagnostics: bool,
22}
23
24impl Default for DiagCtxtFlags {
25    fn default() -> Self {
26        Self {
27            can_emit_warnings: true,
28            treat_err_as_bug: None,
29            deduplicate_diagnostics: true,
30            track_diagnostics: cfg!(debug_assertions),
31        }
32    }
33}
34
35/// A handler deals with errors and other compiler output.
36/// Certain errors (fatal, bug, unimpl) may cause immediate exit,
37/// others log errors for later reporting.
38pub struct DiagCtxt {
39    inner: Lock<DiagCtxtInner>,
40}
41
42struct DiagCtxtInner {
43    emitter: Box<DynEmitter>,
44
45    flags: DiagCtxtFlags,
46
47    /// The number of errors that have been emitted, including duplicates.
48    ///
49    /// This is not necessarily the count that's reported to the user once
50    /// compilation ends.
51    err_count: usize,
52    deduplicated_err_count: usize,
53    warn_count: usize,
54    /// The warning count, used for a recap upon finishing
55    deduplicated_warn_count: usize,
56
57    /// This set contains a hash of every diagnostic that has been emitted by this `DiagCtxt`.
58    /// These hashes are used to avoid emitting the same error twice.
59    emitted_diagnostics: FxHashSet<u64>,
60}
61
62impl DiagCtxt {
63    /// Creates a new `DiagCtxt` with the given diagnostics emitter.
64    pub fn new(emitter: Box<DynEmitter>) -> Self {
65        Self {
66            inner: Lock::new(DiagCtxtInner {
67                emitter,
68                flags: DiagCtxtFlags::default(),
69                err_count: 0,
70                deduplicated_err_count: 0,
71                warn_count: 0,
72                deduplicated_warn_count: 0,
73                emitted_diagnostics: FxHashSet::default(),
74            }),
75        }
76    }
77
78    /// Creates a new `DiagCtxt` with a stderr emitter for emitting one-off/early fatal errors that
79    /// contain no source information.
80    pub fn new_early() -> Self {
81        Self::with_stderr_emitter(None).set_flags(|flags| flags.track_diagnostics = false)
82    }
83
84    /// Creates a new `DiagCtxt` with a test emitter.
85    pub fn with_test_emitter(source_map: Option<Arc<SourceMap>>) -> Self {
86        Self::new(Box::new(HumanEmitter::test().source_map(source_map)))
87    }
88
89    /// Creates a new `DiagCtxt` with a stderr emitter.
90    pub fn with_stderr_emitter(source_map: Option<Arc<SourceMap>>) -> Self {
91        Self::with_stderr_emitter_and_color(source_map, ColorChoice::Auto)
92    }
93
94    /// Creates a new `DiagCtxt` with a stderr emitter and a color choice.
95    pub fn with_stderr_emitter_and_color(
96        source_map: Option<Arc<SourceMap>>,
97        color_choice: ColorChoice,
98    ) -> Self {
99        Self::new(Box::new(HumanEmitter::stderr(color_choice).source_map(source_map)))
100    }
101
102    /// Creates a new `DiagCtxt` with a silent emitter.
103    ///
104    /// Fatal diagnostics will still be emitted, optionally with the given note.
105    pub fn with_silent_emitter(fatal_note: Option<String>) -> Self {
106        let fatal_emitter = HumanEmitter::stderr(Default::default());
107        Self::new(Box::new(SilentEmitter::new(fatal_emitter).with_note(fatal_note)))
108            .disable_warnings()
109    }
110
111    /// Creates a new `DiagCtxt` with a human emitter that emits diagnostics to a local buffer.
112    pub fn with_buffer_emitter(
113        source_map: Option<Arc<SourceMap>>,
114        color_choice: ColorChoice,
115    ) -> Self {
116        Self::new(Box::new(HumanBufferEmitter::new(color_choice).source_map(source_map)))
117    }
118
119    /// Sets the emitter to [`SilentEmitter`].
120    pub fn make_silent(&self, fatal_note: Option<String>, emit_fatal: bool) {
121        self.wrap_emitter(|prev| {
122            Box::new(SilentEmitter::new_boxed(emit_fatal.then_some(prev)).with_note(fatal_note))
123        });
124    }
125
126    /// Sets the inner emitter.
127    pub fn set_emitter(&self, emitter: Box<DynEmitter>) {
128        self.inner.lock().emitter = emitter;
129    }
130
131    /// Wraps the current emitter with the given closure.
132    pub fn wrap_emitter(&self, f: impl FnOnce(Box<DynEmitter>) -> Box<DynEmitter>) {
133        struct FakeEmitter;
134        impl crate::diagnostics::Emitter for FakeEmitter {
135            fn emit_diagnostic(&mut self, _diagnostic: &Diag) {}
136        }
137
138        let mut inner = self.inner.lock();
139        let prev = std::mem::replace(&mut inner.emitter, Box::new(FakeEmitter));
140        inner.emitter = f(prev);
141    }
142
143    /// Gets the source map associated with this context.
144    pub fn source_map(&self) -> Option<Arc<SourceMap>> {
145        self.inner.lock().emitter.source_map().cloned()
146    }
147
148    /// Gets the source map associated with this context.
149    pub fn source_map_mut(&mut self) -> Option<&Arc<SourceMap>> {
150        self.inner.get_mut().emitter.source_map()
151    }
152
153    /// Sets flags.
154    pub fn set_flags(mut self, f: impl FnOnce(&mut DiagCtxtFlags)) -> Self {
155        self.set_flags_mut(f);
156        self
157    }
158
159    /// Sets flags.
160    pub fn set_flags_mut(&mut self, f: impl FnOnce(&mut DiagCtxtFlags)) {
161        f(&mut self.inner.get_mut().flags);
162    }
163
164    /// Disables emitting warnings.
165    pub fn disable_warnings(self) -> Self {
166        self.set_flags(|f| f.can_emit_warnings = false)
167    }
168
169    /// Returns `true` if diagnostics are being tracked.
170    pub fn track_diagnostics(&self) -> bool {
171        self.inner.lock().flags.track_diagnostics
172    }
173
174    /// Emits the given diagnostic with this context.
175    #[inline]
176    pub fn emit_diagnostic(&self, mut diagnostic: Diag) -> Result<(), ErrorGuaranteed> {
177        self.emit_diagnostic_without_consuming(&mut diagnostic)
178    }
179
180    /// Emits the given diagnostic with this context, without consuming the diagnostic.
181    ///
182    /// **Note:** This function is intended to be used only internally in `DiagBuilder`.
183    /// Use [`emit_diagnostic`](Self::emit_diagnostic) instead.
184    pub(super) fn emit_diagnostic_without_consuming(
185        &self,
186        diagnostic: &mut Diag,
187    ) -> Result<(), ErrorGuaranteed> {
188        self.inner.lock().emit_diagnostic_without_consuming(diagnostic)
189    }
190
191    /// Returns the number of errors that have been emitted, including duplicates.
192    pub fn err_count(&self) -> usize {
193        self.inner.lock().err_count
194    }
195
196    /// Returns `Err` if any errors have been emitted.
197    pub fn has_errors(&self) -> Result<(), ErrorGuaranteed> {
198        if self.inner.lock().has_errors() { Err(ErrorGuaranteed::new_unchecked()) } else { Ok(()) }
199    }
200
201    /// Returns the emitted diagnostics. Can be empty.
202    ///
203    /// Returns `None` if the underlying emitter is not a human buffer emitter created with
204    /// [`with_buffer_emitter`](Self::with_buffer_emitter).
205    pub fn emitted_diagnostics(&self) -> Option<EmittedDiagnostics> {
206        let inner = self.inner.lock();
207        Some(EmittedDiagnostics(inner.emitter.local_buffer()?.to_string()))
208    }
209
210    /// Returns `Err` with the printed diagnostics if any errors have been emitted.
211    ///
212    /// Returns `None` if the underlying emitter is not a human buffer emitter created with
213    /// [`with_buffer_emitter`](Self::with_buffer_emitter).
214    pub fn emitted_errors(&self) -> Option<Result<(), EmittedDiagnostics>> {
215        let inner = self.inner.lock();
216        let buffer = inner.emitter.local_buffer()?;
217        Some(if inner.has_errors() { Err(EmittedDiagnostics(buffer.to_string())) } else { Ok(()) })
218    }
219
220    /// Emits a diagnostic if any warnings or errors have been emitted.
221    pub fn print_error_count(&self) -> Result {
222        self.inner.lock().print_error_count()
223    }
224}
225
226/// Diag constructors.
227///
228/// Note that methods returning a [`DiagBuilder`] must also marked with `#[track_caller]`.
229impl DiagCtxt {
230    /// Creates a builder at the given `level` with the given `msg`.
231    #[track_caller]
232    pub fn diag<G: EmissionGuarantee>(
233        &self,
234        level: Level,
235        msg: impl Into<DiagMsg>,
236    ) -> DiagBuilder<'_, G> {
237        DiagBuilder::new(self, level, msg)
238    }
239
240    /// Creates a builder at the `Bug` level with the given `msg`.
241    #[track_caller]
242    pub fn bug(&self, msg: impl Into<DiagMsg>) -> DiagBuilder<'_, BugAbort> {
243        self.diag(Level::Bug, msg)
244    }
245
246    /// Creates a builder at the `Fatal` level with the given `msg`.
247    #[track_caller]
248    pub fn fatal(&self, msg: impl Into<DiagMsg>) -> DiagBuilder<'_, FatalAbort> {
249        self.diag(Level::Fatal, msg)
250    }
251
252    /// Creates a builder at the `Error` level with the given `msg`.
253    #[track_caller]
254    pub fn err(&self, msg: impl Into<DiagMsg>) -> DiagBuilder<'_, ErrorGuaranteed> {
255        self.diag(Level::Error, msg)
256    }
257
258    /// Creates a builder at the `Warning` level with the given `msg`.
259    ///
260    /// Attempting to `.emit()` the builder will only emit if `can_emit_warnings` is `true`.
261    #[track_caller]
262    pub fn warn(&self, msg: impl Into<DiagMsg>) -> DiagBuilder<'_, ()> {
263        self.diag(Level::Warning, msg)
264    }
265
266    /// Creates a builder at the `Help` level with the given `msg`.
267    #[track_caller]
268    pub fn help(&self, msg: impl Into<DiagMsg>) -> DiagBuilder<'_, ()> {
269        self.diag(Level::Help, msg)
270    }
271
272    /// Creates a builder at the `Note` level with the given `msg`.
273    #[track_caller]
274    pub fn note(&self, msg: impl Into<DiagMsg>) -> DiagBuilder<'_, ()> {
275        self.diag(Level::Note, msg)
276    }
277}
278
279impl DiagCtxtInner {
280    fn emit_diagnostic(&mut self, mut diagnostic: Diag) -> Result<(), ErrorGuaranteed> {
281        self.emit_diagnostic_without_consuming(&mut diagnostic)
282    }
283
284    fn emit_diagnostic_without_consuming(
285        &mut self,
286        diagnostic: &mut Diag,
287    ) -> Result<(), ErrorGuaranteed> {
288        if diagnostic.level == Level::Warning && !self.flags.can_emit_warnings {
289            return Ok(());
290        }
291
292        if diagnostic.level == Level::Allow {
293            return Ok(());
294        }
295
296        if matches!(diagnostic.level, Level::Error | Level::Fatal) && self.treat_err_as_bug() {
297            diagnostic.level = Level::Bug;
298        }
299
300        let already_emitted = self.insert_diagnostic(diagnostic);
301        if !(self.flags.deduplicate_diagnostics && already_emitted) {
302            // Remove duplicate `Once*` subdiagnostics.
303            diagnostic.children.retain(|sub| {
304                if !matches!(sub.level, Level::OnceNote | Level::OnceHelp) {
305                    return true;
306                }
307                let sub_already_emitted = self.insert_diagnostic(sub);
308                !sub_already_emitted
309            });
310
311            // if already_emitted {
312            //     diagnostic.note(
313            //         "duplicate diagnostic emitted due to `-Z deduplicate-diagnostics=no`",
314            //     );
315            // }
316
317            self.emitter.emit_diagnostic(diagnostic);
318            if diagnostic.is_error() {
319                self.deduplicated_err_count += 1;
320            } else if diagnostic.level == Level::Warning {
321                self.deduplicated_warn_count += 1;
322            }
323        }
324
325        if diagnostic.is_error() {
326            self.bump_err_count();
327            Err(ErrorGuaranteed::new_unchecked())
328        } else {
329            self.bump_warn_count();
330            Ok(())
331        }
332    }
333
334    fn print_error_count(&mut self) -> Result {
335        // self.emit_stashed_diagnostics();
336
337        if self.treat_err_as_bug() {
338            return Ok(());
339        }
340
341        let warnings = |count| match count {
342            0 => unreachable!(),
343            1 => Cow::from("1 warning emitted"),
344            count => Cow::from(format!("{count} warnings emitted")),
345        };
346        let errors = |count| match count {
347            0 => unreachable!(),
348            1 => Cow::from("aborting due to 1 previous error"),
349            count => Cow::from(format!("aborting due to {count} previous errors")),
350        };
351
352        match (self.deduplicated_err_count, self.deduplicated_warn_count) {
353            (0, 0) => Ok(()),
354            (0, w) => {
355                // TODO: Don't emit in tests since it's not handled by `ui_test`.
356                if self.flags.track_diagnostics {
357                    self.emitter.emit_diagnostic(&Diag::new(Level::Warning, warnings(w)));
358                }
359                Ok(())
360            }
361            (e, 0) => self.emit_diagnostic(Diag::new(Level::Error, errors(e))),
362            (e, w) => self.emit_diagnostic(Diag::new(
363                Level::Error,
364                format!("{}; {}", errors(e), warnings(w)),
365            )),
366        }
367    }
368
369    /// Inserts the given diagnostic into the set of emitted diagnostics.
370    /// Returns `true` if the diagnostic was already emitted.
371    fn insert_diagnostic<H: std::hash::Hash>(&mut self, diag: &H) -> bool {
372        let hash = solar_data_structures::map::rustc_hash::FxBuildHasher.hash_one(diag);
373        !self.emitted_diagnostics.insert(hash)
374    }
375
376    fn treat_err_as_bug(&self) -> bool {
377        self.flags.treat_err_as_bug.is_some_and(|c| self.err_count >= c.get())
378    }
379
380    fn bump_err_count(&mut self) {
381        self.err_count += 1;
382        self.panic_if_treat_err_as_bug();
383    }
384
385    fn bump_warn_count(&mut self) {
386        self.warn_count += 1;
387    }
388
389    fn has_errors(&self) -> bool {
390        self.err_count > 0
391    }
392
393    fn panic_if_treat_err_as_bug(&self) {
394        if self.treat_err_as_bug() {
395            match (self.err_count, self.flags.treat_err_as_bug.unwrap().get()) {
396                (1, 1) => panic!("aborting due to `-Z treat-err-as-bug=1`"),
397                (count, val) => {
398                    panic!("aborting after {count} errors due to `-Z treat-err-as-bug={val}`")
399                }
400            }
401        }
402    }
403}