Skip to main content

foundry_compilers/report/
mod.rs

1//! Subscribe to events in the compiler pipeline
2//!
3//! The _reporter_ is the component of the [`crate::Project::compile()`] pipeline which is
4//! responsible for reporting on specific steps in the process.
5//!
6//! By default, the current reporter is a noop that does
7//! nothing.
8//!
9//! To use another report implementation, it must be set as the current reporter.
10//! There are two methods for doing so: [`with_scoped`] and
11//! [`try_init`]. `with_scoped` sets the reporter for the
12//! duration of a scope, while `set_global` sets a global default report
13//! for the entire process.
14
15// <https://github.com/tokio-rs/tracing/blob/master/tracing-core/src/dispatch.rs>
16
17#![allow(static_mut_refs)] // TODO
18
19use foundry_compilers_artifacts::remappings::Remapping;
20use semver::Version;
21use std::{
22    any::{Any, TypeId},
23    cell::RefCell,
24    error::Error,
25    fmt, io,
26    path::{Path, PathBuf},
27    ptr::NonNull,
28    sync::{
29        Arc,
30        atomic::{AtomicBool, AtomicUsize, Ordering},
31    },
32    time::Duration,
33};
34
35mod compiler;
36pub use compiler::SolcCompilerIoReporter;
37
38thread_local! {
39    static CURRENT_STATE: State = State {
40        scoped: RefCell::new(Report::none()),
41    };
42}
43
44static EXISTS: AtomicBool = AtomicBool::new(false);
45static SCOPED_COUNT: AtomicUsize = AtomicUsize::new(0);
46
47// tracks the state of `GLOBAL_REPORTER`
48static GLOBAL_REPORTER_STATE: AtomicUsize = AtomicUsize::new(UN_SET);
49
50const UN_SET: usize = 0;
51const SETTING: usize = 1;
52const SET: usize = 2;
53
54static mut GLOBAL_REPORTER: Option<Report> = None;
55
56/// Install this `Reporter` as the global default if one is
57/// not already set.
58///
59/// # Errors
60/// Returns an Error if the initialization was unsuccessful, likely
61/// because a global reporter was already installed by another
62/// call to `try_init`.
63pub fn try_init<T>(reporter: T) -> Result<(), Box<dyn Error + Send + Sync + 'static>>
64where
65    T: Reporter + Send + Sync + 'static,
66{
67    set_global_reporter(Report::new(reporter))?;
68    Ok(())
69}
70
71/// Install this `Reporter` as the global default.
72///
73/// # Panics
74///
75/// Panics if the initialization was unsuccessful, likely because a
76/// global reporter was already installed by another call to `try_init`.
77/// ```
78/// use foundry_compilers::report::BasicStdoutReporter;
79/// let subscriber = foundry_compilers::report::init(BasicStdoutReporter::default());
80/// ```
81pub fn init<T>(reporter: T)
82where
83    T: Reporter + Send + Sync + 'static,
84{
85    try_init(reporter).expect("Failed to install global reporter")
86}
87
88/// Trait representing the functions required to emit information about various steps in the
89/// compiler pipeline.
90///
91/// This trait provides a series of callbacks that are invoked at certain parts of the
92/// [`crate::Project::compile()`] process.
93///
94/// Implementers of this trait can use these callbacks to emit additional information, for example
95/// print custom messages to `stdout`.
96///
97/// A `Reporter` is entirely passive and only listens to incoming "events".
98pub trait Reporter: 'static + std::fmt::Debug {
99    /// Callback invoked right before [Compiler::compile] is called
100    ///
101    /// This contains the [Compiler] its [Version] and all files that triggered the compile job. The
102    /// dirty files are only provided to give a better feedback what was actually compiled.
103    ///
104    /// [Compiler]: crate::compilers::Compiler
105    /// [Compiler::compile]: crate::compilers::Compiler::compile
106    fn on_compiler_spawn(
107        &self,
108        _compiler_name: &str,
109        _version: &Version,
110        _dirty_files: &[PathBuf],
111    ) {
112    }
113
114    /// Invoked with the `CompilerOutput` if [`Compiler::compile()`] was successful
115    ///
116    /// [`Compiler::compile()`]: crate::compilers::Compiler::compile
117    fn on_compiler_success(&self, _compiler_name: &str, _version: &Version, _duration: &Duration) {}
118
119    /// Invoked before a new compiler version is installed
120    fn on_solc_installation_start(&self, _version: &Version) {}
121
122    /// Invoked after a new compiler version was successfully installed
123    fn on_solc_installation_success(&self, _version: &Version) {}
124
125    /// Invoked after a compiler installation failed
126    fn on_solc_installation_error(&self, _version: &Version, _error: &str) {}
127
128    /// Invoked if imports couldn't be resolved with the given remappings, where `imports` is the
129    /// list of all import paths and the file they occurred in: `(import stmt, file)`
130    fn on_unresolved_imports(&self, _imports: &[(&Path, &Path)], _remappings: &[Remapping]) {}
131
132    /// If `self` is the same type as the provided `TypeId`, returns an untyped
133    /// [`NonNull`] pointer to that type. Otherwise, returns `None`.
134    ///
135    /// If you wish to downcast a `Reporter`, it is strongly advised to use
136    /// the safe API provided by downcast_ref instead.
137    ///
138    /// This API is required for `downcast_raw` to be a trait method; a method
139    /// signature like downcast_ref (with a generic type parameter) is not
140    /// object-safe, and thus cannot be a trait method for `Reporter`. This
141    /// means that if we only exposed downcast_ref, `Reporter`
142    /// implementations could not override the downcasting behavior
143    ///
144    /// # Safety
145    ///
146    /// The downcast_ref method expects that the pointer returned by
147    /// `downcast_raw` points to a valid instance of the type
148    /// with the provided `TypeId`. Failure to ensure this will result in
149    /// undefined behaviour, so implementing `downcast_raw` is unsafe.
150    unsafe fn downcast_raw(&self, id: TypeId) -> Option<NonNull<()>> {
151        (id == TypeId::of::<Self>()).then(|| NonNull::from(self).cast())
152    }
153}
154
155impl dyn Reporter {
156    /// Returns `true` if this `Reporter` is the same type as `T`.
157    pub fn is<T: Any>(&self) -> bool {
158        self.downcast_ref::<T>().is_some()
159    }
160
161    /// Returns some reference to this `Reporter` value if it is of type `T`,
162    /// or `None` if it isn't.
163    pub fn downcast_ref<T: Any>(&self) -> Option<&T> {
164        unsafe {
165            let raw = self.downcast_raw(TypeId::of::<T>())?;
166            Some(&*(raw.cast().as_ptr()))
167        }
168    }
169}
170
171pub(crate) fn compiler_spawn(compiler_name: &str, version: &Version, dirty_files: &[PathBuf]) {
172    get_default(|r| r.reporter.on_compiler_spawn(compiler_name, version, dirty_files));
173}
174
175pub(crate) fn compiler_success(compiler_name: &str, version: &Version, duration: &Duration) {
176    get_default(|r| r.reporter.on_compiler_success(compiler_name, version, duration));
177}
178
179#[allow(dead_code)]
180pub(crate) fn solc_installation_start(version: &Version) {
181    get_default(|r| r.reporter.on_solc_installation_start(version));
182}
183
184#[allow(dead_code)]
185pub(crate) fn solc_installation_success(version: &Version) {
186    get_default(|r| r.reporter.on_solc_installation_success(version));
187}
188
189#[allow(dead_code)]
190pub(crate) fn solc_installation_error(version: &Version, error: &str) {
191    get_default(|r| r.reporter.on_solc_installation_error(version, error));
192}
193
194pub(crate) fn unresolved_imports(imports: &[(&Path, &Path)], remappings: &[Remapping]) {
195    get_default(|r| r.reporter.on_unresolved_imports(imports, remappings));
196}
197
198fn get_global() -> Option<&'static Report> {
199    if GLOBAL_REPORTER_STATE.load(Ordering::SeqCst) != SET {
200        return None;
201    }
202    unsafe {
203        // This is safe given the invariant that setting the global reporter
204        // also sets `GLOBAL_REPORTER_STATE` to `SET`.
205        Some(GLOBAL_REPORTER.as_ref().expect(
206            "Reporter invariant violated: GLOBAL_REPORTER must be initialized before GLOBAL_REPORTER_STATE is set",
207        ))
208    }
209}
210
211/// Executes a closure with a reference to this thread's current reporter.
212#[inline(always)]
213pub fn get_default<T, F>(mut f: F) -> T
214where
215    F: FnMut(&Report) -> T,
216{
217    if SCOPED_COUNT.load(Ordering::Acquire) == 0 {
218        // fast path if no scoped reporter has been set; use the global
219        // default.
220        return if let Some(glob) = get_global() { f(glob) } else { f(&Report::none()) };
221    }
222
223    get_default_scoped(f)
224}
225
226#[inline(never)]
227fn get_default_scoped<T, F>(mut f: F) -> T
228where
229    F: FnMut(&Report) -> T,
230{
231    CURRENT_STATE
232        .try_with(|state| {
233            let scoped = state.scoped.borrow_mut();
234            f(&scoped)
235        })
236        .unwrap_or_else(|_| f(&Report::none()))
237}
238
239/// Executes a closure with a reference to the `Reporter`.
240pub fn with_global<T>(f: impl FnOnce(&Report) -> T) -> Option<T> {
241    let report = get_global()?;
242    Some(f(report))
243}
244
245/// Sets this reporter as the scoped reporter for the duration of a closure.
246pub fn with_scoped<T>(report: &Report, f: impl FnOnce() -> T) -> T {
247    // When this guard is dropped, the scoped reporter will be reset to the
248    // prior reporter. Using this (rather than simply resetting after calling
249    // `f`) ensures that we always reset to the prior reporter even if `f`
250    // panics.
251    let _guard = set_scoped(report);
252    f()
253}
254
255/// The report state of a thread.
256struct State {
257    /// This thread's current scoped reporter.
258    scoped: RefCell<Report>,
259}
260
261impl State {
262    /// Replaces the current scoped reporter on this thread with the provided
263    /// reporter.
264    ///
265    /// Dropping the returned `ResetGuard` will reset the scoped reporter to
266    /// the previous value.
267    #[inline]
268    fn set_scoped(new_report: Report) -> ScopeGuard {
269        let prior = CURRENT_STATE.try_with(|state| state.scoped.replace(new_report)).ok();
270        EXISTS.store(true, Ordering::Release);
271        SCOPED_COUNT.fetch_add(1, Ordering::Release);
272        ScopeGuard(prior)
273    }
274}
275
276/// A guard that resets the current scoped reporter to the prior
277/// scoped reporter when dropped.
278#[derive(Debug)]
279pub struct ScopeGuard(Option<Report>);
280
281impl Drop for ScopeGuard {
282    #[inline]
283    fn drop(&mut self) {
284        SCOPED_COUNT.fetch_sub(1, Ordering::Release);
285        if let Some(report) = self.0.take() {
286            // Replace the reporter and then drop the old one outside
287            // of the thread-local context.
288            let prev = CURRENT_STATE.try_with(|state| state.scoped.replace(report));
289            drop(prev)
290        }
291    }
292}
293
294/// Sets the reporter as the scoped reporter for the duration of the lifetime
295/// of the returned DefaultGuard
296#[must_use = "Dropping the guard unregisters the reporter."]
297pub fn set_scoped(reporter: &Report) -> ScopeGuard {
298    // When this guard is dropped, the scoped reporter will be reset to the
299    // prior default. Using this ensures that we always reset to the prior
300    // reporter even if the thread calling this function panics.
301    State::set_scoped(reporter.clone())
302}
303
304/// A no-op [`Reporter`] that does nothing.
305#[derive(Clone, Copy, Debug, Default)]
306pub struct NoReporter(());
307
308impl Reporter for NoReporter {}
309
310/// A [`Reporter`] that emits some general information to `stdout`.
311///
312/// `BrokenPipe` errors are silently ignored so that piping compiler output
313/// through consumers that may close the pipe early (e.g. `tee`, `head`) does
314/// not cause a panic.
315#[derive(Clone, Debug, Default)]
316pub struct BasicStdoutReporter {
317    _priv: (),
318}
319
320impl Reporter for BasicStdoutReporter {
321    /// Callback invoked right before [`Compiler::compile()`] is called
322    ///
323    /// [`Compiler::compile()`]: crate::compilers::Compiler::compile
324    fn on_compiler_spawn(&self, compiler_name: &str, version: &Version, dirty_files: &[PathBuf]) {
325        write_line(
326            io::stdout().lock(),
327            format_args!(
328                "Compiling {} files with {} {}.{}.{}",
329                dirty_files.len(),
330                compiler_name,
331                version.major,
332                version.minor,
333                version.patch
334            ),
335        );
336    }
337
338    fn on_compiler_success(&self, compiler_name: &str, version: &Version, duration: &Duration) {
339        write_line(
340            io::stdout().lock(),
341            format_args!(
342                "{} {}.{}.{} finished in {duration:.2?}",
343                compiler_name, version.major, version.minor, version.patch
344            ),
345        );
346    }
347
348    /// Invoked before a new compiler is installed
349    fn on_solc_installation_start(&self, version: &Version) {
350        write_line(io::stdout().lock(), format_args!("installing solc version \"{version}\""));
351    }
352
353    /// Invoked before a new compiler was successfully installed
354    fn on_solc_installation_success(&self, version: &Version) {
355        write_line(io::stdout().lock(), format_args!("Successfully installed solc {version}"));
356    }
357
358    fn on_solc_installation_error(&self, version: &Version, error: &str) {
359        write_line(io::stderr().lock(), format_args!("Failed to install solc {version}: {error}"));
360    }
361
362    fn on_unresolved_imports(&self, imports: &[(&Path, &Path)], remappings: &[Remapping]) {
363        if imports.is_empty() {
364            return;
365        }
366        write_line(
367            io::stdout().lock(),
368            format_args!("{}", format_unresolved_imports(imports, remappings)),
369        );
370    }
371}
372
373/// Write a single line to `writer`, silently discarding `BrokenPipe` errors.
374///
375/// Non-`BrokenPipe` errors still panic, matching the prior `println!` behavior.
376fn write_line(mut writer: impl io::Write, args: fmt::Arguments<'_>) {
377    if let Err(err) = writeln!(writer, "{args}")
378        && err.kind() != io::ErrorKind::BrokenPipe
379    {
380        panic!("failed to write reporter output: {err}");
381    }
382}
383
384/// Creates a meaningful message for all unresolved imports
385pub fn format_unresolved_imports(imports: &[(&Path, &Path)], remappings: &[Remapping]) -> String {
386    let info = imports
387        .iter()
388        .map(|(import, file)| format!("\"{}\" in \"{}\"", import.display(), file.display()))
389        .collect::<Vec<_>>()
390        .join("\n      ");
391    format!(
392        "Unable to resolve imports:\n      {}\nwith remappings:\n      {}",
393        info,
394        remappings.iter().map(|r| r.to_string()).collect::<Vec<_>>().join("\n      ")
395    )
396}
397
398/// Returned if setting the global reporter fails.
399#[derive(Debug)]
400pub struct SetGlobalReporterError {
401    // private marker so this type can't be initiated
402    _priv: (),
403}
404
405impl fmt::Display for SetGlobalReporterError {
406    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
407        f.pad("a global reporter has already been set")
408    }
409}
410
411impl Error for SetGlobalReporterError {}
412
413/// `Report` trace data to a [`Reporter`].
414#[derive(Clone)]
415pub struct Report {
416    reporter: Arc<dyn Reporter + Send + Sync>,
417}
418
419impl Report {
420    /// Returns a new `Report` that does nothing
421    pub fn none() -> Self {
422        Self { reporter: Arc::new(NoReporter::default()) }
423    }
424
425    /// Returns a `Report` that forwards to the given [`Reporter`].
426    ///
427    /// [`Reporter`]: ../reporter/trait.Reporter.html
428    pub fn new<S>(reporter: S) -> Self
429    where
430        S: Reporter + Send + Sync + 'static,
431    {
432        Self { reporter: Arc::new(reporter) }
433    }
434
435    /// Returns `true` if this `Report` forwards to a reporter of type
436    /// `T`.
437    #[inline]
438    pub fn is<T: Any>(&self) -> bool {
439        <dyn Reporter>::is::<T>(&*self.reporter)
440    }
441}
442
443impl fmt::Debug for Report {
444    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
445        f.pad("Report(...)")
446    }
447}
448
449/// Sets this report as the global default for the duration of the entire program.
450///
451/// The global reporter can only be set once; additional attempts to set the global reporter will
452/// fail. Returns `Err` if the global reporter has already been set.
453fn set_global_reporter(report: Report) -> Result<(), SetGlobalReporterError> {
454    // `compare_exchange` tries to store `SETTING` if the current value is `UN_SET`
455    // this returns `Ok(_)` if the current value of `GLOBAL_REPORTER_STATE` was `UN_SET` and
456    // `SETTING` was written, this guarantees the value is `SETTING`.
457    if GLOBAL_REPORTER_STATE
458        .compare_exchange(UN_SET, SETTING, Ordering::SeqCst, Ordering::SeqCst)
459        .is_ok()
460    {
461        unsafe {
462            GLOBAL_REPORTER = Some(report);
463        }
464        GLOBAL_REPORTER_STATE.store(SET, Ordering::SeqCst);
465        Ok(())
466    } else {
467        Err(SetGlobalReporterError { _priv: () })
468    }
469}
470
471#[cfg(test)]
472mod tests {
473    use super::*;
474    use std::str::FromStr;
475
476    #[test]
477    fn scoped_reporter_works() {
478        #[derive(Debug)]
479        struct TestReporter;
480        impl Reporter for TestReporter {}
481
482        with_scoped(&Report::new(TestReporter), || {
483            get_default(|reporter| assert!(reporter.is::<TestReporter>()))
484        });
485    }
486
487    #[test]
488    fn global_and_scoped_reporter_works() {
489        get_default(|reporter| {
490            assert!(reporter.is::<NoReporter>());
491        });
492
493        set_global_reporter(Report::new(BasicStdoutReporter::default())).unwrap();
494        #[derive(Debug)]
495        struct TestReporter;
496        impl Reporter for TestReporter {}
497
498        with_scoped(&Report::new(TestReporter), || {
499            get_default(|reporter| assert!(reporter.is::<TestReporter>()))
500        });
501
502        get_default(|reporter| assert!(reporter.is::<BasicStdoutReporter>()))
503    }
504
505    #[test]
506    fn write_line_ignores_broken_pipe() {
507        struct BrokenPipeWriter;
508
509        impl io::Write for BrokenPipeWriter {
510            fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
511                Err(io::Error::new(io::ErrorKind::BrokenPipe, "broken pipe"))
512            }
513
514            fn flush(&mut self) -> io::Result<()> {
515                Ok(())
516            }
517        }
518
519        // Should not panic.
520        write_line(BrokenPipeWriter, format_args!("hello"));
521    }
522
523    #[test]
524    #[should_panic(expected = "failed to write reporter output")]
525    fn write_line_panics_on_non_broken_pipe_errors() {
526        struct FailingWriter;
527
528        impl io::Write for FailingWriter {
529            fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
530                Err(io::Error::other("write failed"))
531            }
532
533            fn flush(&mut self) -> io::Result<()> {
534                Ok(())
535            }
536        }
537
538        write_line(FailingWriter, format_args!("hello"));
539    }
540
541    #[test]
542    fn write_line_writes_newline_terminated_output() {
543        #[derive(Clone, Default)]
544        struct BufferWriter(std::sync::Arc<std::sync::Mutex<Vec<u8>>>);
545
546        impl io::Write for BufferWriter {
547            fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
548                self.0.lock().unwrap().extend_from_slice(buf);
549                Ok(buf.len())
550            }
551
552            fn flush(&mut self) -> io::Result<()> {
553                Ok(())
554            }
555        }
556
557        let writer = BufferWriter::default();
558        let buffer = writer.0.clone();
559        write_line(writer, format_args!("hello"));
560
561        assert_eq!(String::from_utf8(buffer.lock().unwrap().clone()).unwrap(), "hello\n");
562    }
563
564    #[test]
565    fn test_unresolved_message() {
566        let unresolved = vec![(Path::new("./src/Import.sol"), Path::new("src/File.col"))];
567
568        let remappings = vec![Remapping::from_str("oz=a/b/c/d").unwrap()];
569
570        assert_eq!(
571            format_unresolved_imports(&unresolved, &remappings).trim(),
572            r#"
573Unable to resolve imports:
574      "./src/Import.sol" in "src/File.col"
575with remappings:
576      oz/=a/b/c/d/"#
577                .trim()
578        )
579    }
580}