solar_interface/diagnostics/
context.rs

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