1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
//! Subscribe to events in the compiler pipeline
//!
//! The _reporter_ is the component of the [`crate::Project::compile()`] pipeline which is
//! responsible for reporting on specific steps in the process.
//!
//! By default, the current reporter is a noop that does
//! nothing.
//!
//! To use another report implementation, it must be set as the current reporter.
//! There are two methods for doing so: [`with_scoped`] and
//! [`try_init`]. `with_scoped` sets the reporter for the
//! duration of a scope, while `set_global` sets a global default report
//! for the entire process.

// <https://github.com/tokio-rs/tracing/blob/master/tracing-core/src/dispatch.rs>

use crate::{remappings::Remapping, CompilerInput, CompilerOutput, Solc};
use semver::Version;
use std::{
    any::{Any, TypeId},
    cell::RefCell,
    error::Error,
    fmt,
    path::{Path, PathBuf},
    ptr::NonNull,
    sync::{
        atomic::{AtomicBool, AtomicUsize, Ordering},
        Arc,
    },
    time::Duration,
};

mod compiler;
pub use compiler::SolcCompilerIoReporter;

thread_local! {
    static CURRENT_STATE: State = State {
        scoped: RefCell::new(Report::none()),
    };
}

static EXISTS: AtomicBool = AtomicBool::new(false);
static SCOPED_COUNT: AtomicUsize = AtomicUsize::new(0);

// tracks the state of `GLOBAL_REPORTER`
static GLOBAL_REPORTER_STATE: AtomicUsize = AtomicUsize::new(UN_SET);

const UN_SET: usize = 0;
const SETTING: usize = 1;
const SET: usize = 2;

static mut GLOBAL_REPORTER: Option<Report> = None;

/// Install this `Reporter` as the global default if one is
/// not already set.
///
/// # Errors
/// Returns an Error if the initialization was unsuccessful, likely
/// because a global reporter was already installed by another
/// call to `try_init`.
pub fn try_init<T>(reporter: T) -> Result<(), Box<dyn Error + Send + Sync + 'static>>
where
    T: Reporter + Send + Sync + 'static,
{
    set_global_reporter(Report::new(reporter))?;
    Ok(())
}

/// Install this `Reporter` as the global default.
///
/// # Panics
///
/// Panics if the initialization was unsuccessful, likely because a
/// global reporter was already installed by another call to `try_init`.
/// ```rust
/// use ethers_solc::report::BasicStdoutReporter;
/// let subscriber = ethers_solc::report::init(BasicStdoutReporter::default());
/// ```
pub fn init<T>(reporter: T)
where
    T: Reporter + Send + Sync + 'static,
{
    try_init(reporter).expect("Failed to install global reporter")
}

/// Trait representing the functions required to emit information about various steps in the
/// compiler pipeline.
///
/// This trait provides a series of callbacks that are invoked at certain parts of the
/// [`crate::Project::compile()`] process.
///
/// Implementers of this trait can use these callbacks to emit additional information, for example
/// print custom messages to `stdout`.
///
/// A `Reporter` is entirely passive and only listens to incoming "events".
pub trait Reporter: 'static + std::fmt::Debug {
    /// Callback invoked right before [`Solc::compile()`] is called
    ///
    /// This contains the [Solc] its [Version] the complete [CompilerInput] and all files that
    /// triggered the compile job. The dirty files are only provided to give a better feedback what
    /// was actually compiled.
    ///
    /// If caching is enabled and there has been a previous successful solc run, the dirty files set
    /// contains the files that absolutely must be recompiled, while the [CompilerInput] contains
    /// all files, the dirty files and all their dependencies.
    ///
    /// If this is a fresh compile then the [crate::artifacts::Sources] set of the [CompilerInput]
    /// matches the dirty files set.
    fn on_solc_spawn(
        &self,
        _solc: &Solc,
        _version: &Version,
        _input: &CompilerInput,
        _dirty_files: &[PathBuf],
    ) {
    }

    /// Invoked with the `CompilerOutput` if [`Solc::compile()`] was successful
    fn on_solc_success(
        &self,
        _solc: &Solc,
        _version: &Version,
        _output: &CompilerOutput,
        _duration: &Duration,
    ) {
    }

    /// Invoked before a new [`Solc`] bin is installed
    fn on_solc_installation_start(&self, _version: &Version) {}

    /// Invoked after a new [`Solc`] bin was successfully installed
    fn on_solc_installation_success(&self, _version: &Version) {}

    /// Invoked after a [`Solc`] installation failed
    fn on_solc_installation_error(&self, _version: &Version, _error: &str) {}

    /// Invoked if imports couldn't be resolved with the given remappings, where `imports` is the
    /// list of all import paths and the file they occurred in: `(import stmt, file)`
    fn on_unresolved_imports(&self, _imports: &[(&Path, &Path)], _remappings: &[Remapping]) {}

    /// If `self` is the same type as the provided `TypeId`, returns an untyped
    /// [`NonNull`] pointer to that type. Otherwise, returns `None`.
    ///
    /// If you wish to downcast a `Reporter`, it is strongly advised to use
    /// the safe API provided by downcast_ref instead.
    ///
    /// This API is required for `downcast_raw` to be a trait method; a method
    /// signature like downcast_ref (with a generic type parameter) is not
    /// object-safe, and thus cannot be a trait method for `Reporter`. This
    /// means that if we only exposed downcast_ref, `Reporter`
    /// implementations could not override the downcasting behavior
    ///
    /// # Safety
    ///
    /// The downcast_ref method expects that the pointer returned by
    /// `downcast_raw` points to a valid instance of the type
    /// with the provided `TypeId`. Failure to ensure this will result in
    /// undefined behaviour, so implementing `downcast_raw` is unsafe.
    unsafe fn downcast_raw(&self, id: TypeId) -> Option<NonNull<()>> {
        if id == TypeId::of::<Self>() {
            Some(NonNull::from(self).cast())
        } else {
            None
        }
    }
}

impl dyn Reporter {
    /// Returns `true` if this `Reporter` is the same type as `T`.
    pub fn is<T: Any>(&self) -> bool {
        self.downcast_ref::<T>().is_some()
    }

    /// Returns some reference to this `Reporter` value if it is of type `T`,
    /// or `None` if it isn't.
    pub fn downcast_ref<T: Any>(&self) -> Option<&T> {
        unsafe {
            let raw = self.downcast_raw(TypeId::of::<T>())?;
            Some(&*(raw.cast().as_ptr()))
        }
    }
}

pub(crate) fn solc_spawn(
    solc: &Solc,
    version: &Version,
    input: &CompilerInput,
    dirty_files: &[PathBuf],
) {
    get_default(|r| r.reporter.on_solc_spawn(solc, version, input, dirty_files));
}

pub(crate) fn solc_success(
    solc: &Solc,
    version: &Version,
    output: &CompilerOutput,
    duration: &Duration,
) {
    get_default(|r| r.reporter.on_solc_success(solc, version, output, duration));
}

#[allow(unused)]
pub(crate) fn solc_installation_start(version: &Version) {
    get_default(|r| r.reporter.on_solc_installation_start(version));
}

#[allow(unused)]
pub(crate) fn solc_installation_success(version: &Version) {
    get_default(|r| r.reporter.on_solc_installation_success(version));
}

#[allow(unused)]
pub(crate) fn solc_installation_error(version: &Version, error: &str) {
    get_default(|r| r.reporter.on_solc_installation_error(version, error));
}

pub(crate) fn unresolved_imports(imports: &[(&Path, &Path)], remappings: &[Remapping]) {
    get_default(|r| r.reporter.on_unresolved_imports(imports, remappings));
}

fn get_global() -> Option<&'static Report> {
    if GLOBAL_REPORTER_STATE.load(Ordering::SeqCst) != SET {
        return None
    }
    unsafe {
        // This is safe given the invariant that setting the global reporter
        // also sets `GLOBAL_REPORTER_STATE` to `SET`.
        Some(GLOBAL_REPORTER.as_ref().expect(
            "Reporter invariant violated: GLOBAL_REPORTER must be initialized before GLOBAL_REPORTER_STATE is set",
        ))
    }
}

/// Executes a closure with a reference to this thread's current reporter.
#[inline(always)]
pub fn get_default<T, F>(mut f: F) -> T
where
    F: FnMut(&Report) -> T,
{
    if SCOPED_COUNT.load(Ordering::Acquire) == 0 {
        // fast path if no scoped reporter has been set; use the global
        // default.
        return if let Some(glob) = get_global() { f(glob) } else { f(&Report::none()) }
    }

    get_default_scoped(f)
}

#[inline(never)]
fn get_default_scoped<T, F>(mut f: F) -> T
where
    F: FnMut(&Report) -> T,
{
    CURRENT_STATE
        .try_with(|state| {
            let scoped = state.scoped.borrow_mut();
            f(&scoped)
        })
        .unwrap_or_else(|_| f(&Report::none()))
}

/// Executes a closure with a reference to the `Reporter`.
pub fn with_global<T>(f: impl FnOnce(&Report) -> T) -> Option<T> {
    let report = get_global()?;
    Some(f(report))
}

/// Sets this reporter as the scoped reporter for the duration of a closure.
pub fn with_scoped<T>(report: &Report, f: impl FnOnce() -> T) -> T {
    // When this guard is dropped, the scoped reporter will be reset to the
    // prior reporter. Using this (rather than simply resetting after calling
    // `f`) ensures that we always reset to the prior reporter even if `f`
    // panics.
    let _guard = set_scoped(report);
    f()
}

/// The report state of a thread.
struct State {
    /// This thread's current scoped reporter.
    scoped: RefCell<Report>,
}

impl State {
    /// Replaces the current scoped reporter on this thread with the provided
    /// reporter.
    ///
    /// Dropping the returned `ResetGuard` will reset the scoped reporter to
    /// the previous value.
    #[inline]
    fn set_scoped(new_report: Report) -> ScopeGuard {
        let prior = CURRENT_STATE.try_with(|state| state.scoped.replace(new_report)).ok();
        EXISTS.store(true, Ordering::Release);
        SCOPED_COUNT.fetch_add(1, Ordering::Release);
        ScopeGuard(prior)
    }
}

/// A guard that resets the current scoped reporter to the prior
/// scoped reporter when dropped.
#[derive(Debug)]
pub struct ScopeGuard(Option<Report>);

impl Drop for ScopeGuard {
    #[inline]
    fn drop(&mut self) {
        SCOPED_COUNT.fetch_sub(1, Ordering::Release);
        if let Some(report) = self.0.take() {
            // Replace the reporter and then drop the old one outside
            // of the thread-local context.
            let prev = CURRENT_STATE.try_with(|state| state.scoped.replace(report));
            drop(prev)
        }
    }
}

/// Sets the reporter as the scoped reporter for the duration of the lifetime
/// of the returned DefaultGuard
#[must_use = "Dropping the guard unregisters the reporter."]
pub fn set_scoped(reporter: &Report) -> ScopeGuard {
    // When this guard is dropped, the scoped reporter will be reset to the
    // prior default. Using this ensures that we always reset to the prior
    // reporter even if the thread calling this function panics.
    State::set_scoped(reporter.clone())
}

/// A no-op [`Reporter`] that does nothing.
#[derive(Copy, Clone, Debug, Default)]
pub struct NoReporter(());

impl Reporter for NoReporter {}

/// A [`Reporter`] that emits some general information to `stdout`
#[derive(Clone, Debug)]
pub struct BasicStdoutReporter {
    solc_io_report: SolcCompilerIoReporter,
}

impl Default for BasicStdoutReporter {
    fn default() -> Self {
        Self { solc_io_report: SolcCompilerIoReporter::from_default_env() }
    }
}

impl Reporter for BasicStdoutReporter {
    /// Callback invoked right before [`Solc::compile()`] is called
    fn on_solc_spawn(
        &self,
        _solc: &Solc,
        version: &Version,
        input: &CompilerInput,
        dirty_files: &[PathBuf],
    ) {
        self.solc_io_report.log_compiler_input(input, version);
        println!(
            "Compiling {} files with {}.{}.{}",
            dirty_files.len(),
            version.major,
            version.minor,
            version.patch
        );
    }

    fn on_solc_success(
        &self,
        _solc: &Solc,
        version: &Version,
        output: &CompilerOutput,
        duration: &Duration,
    ) {
        self.solc_io_report.log_compiler_output(output, version);
        println!(
            "Solc {}.{}.{} finished in {duration:.2?}",
            version.major, version.minor, version.patch
        );
    }

    /// Invoked before a new [`Solc`] bin is installed
    fn on_solc_installation_start(&self, version: &Version) {
        println!("installing solc version \"{version}\"");
    }

    /// Invoked before a new [`Solc`] bin was successfully installed
    fn on_solc_installation_success(&self, version: &Version) {
        println!("Successfully installed solc {version}");
    }

    fn on_solc_installation_error(&self, version: &Version, error: &str) {
        eprintln!("Failed to install solc {version}: {error}");
    }

    fn on_unresolved_imports(&self, imports: &[(&Path, &Path)], remappings: &[Remapping]) {
        if imports.is_empty() {
            return
        }
        println!("{}", format_unresolved_imports(imports, remappings))
    }
}

/// Creates a meaningful message for all unresolved imports
pub fn format_unresolved_imports(imports: &[(&Path, &Path)], remappings: &[Remapping]) -> String {
    let info = imports
        .iter()
        .map(|(import, file)| format!("\"{}\" in \"{}\"", import.display(), file.display()))
        .collect::<Vec<_>>()
        .join("\n      ");
    format!(
        "Unable to resolve imports:\n      {}\nwith remappings:\n      {}",
        info,
        remappings.iter().map(|r| r.to_string()).collect::<Vec<_>>().join("\n      ")
    )
}

/// Returned if setting the global reporter fails.
#[derive(Debug)]
pub struct SetGlobalReporterError {
    // private marker so this type can't be initiated
    _priv: (),
}

impl fmt::Display for SetGlobalReporterError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.pad("a global reporter has already been set")
    }
}

impl Error for SetGlobalReporterError {}

/// `Report` trace data to a [`Reporter`].
#[derive(Clone)]
pub struct Report {
    reporter: Arc<dyn Reporter + Send + Sync>,
}

impl Report {
    /// Returns a new `Report` that does nothing
    pub fn none() -> Self {
        Report { reporter: Arc::new(NoReporter::default()) }
    }

    /// Returns a `Report` that forwards to the given [`Reporter`].
    ///
    /// [`Reporter`]: ../reporter/trait.Reporter.html
    pub fn new<S>(reporter: S) -> Self
    where
        S: Reporter + Send + Sync + 'static,
    {
        Self { reporter: Arc::new(reporter) }
    }

    /// Returns `true` if this `Report` forwards to a reporter of type
    /// `T`.
    #[inline]
    pub fn is<T: Any>(&self) -> bool {
        <dyn Reporter>::is::<T>(&*self.reporter)
    }
}

impl fmt::Debug for Report {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.pad("Report(...)")
    }
}

/// Sets this report as the global default for the duration of the entire program.
///
/// The global reporter can only be set once; additional attempts to set the global reporter will
/// fail. Returns `Err` if the global reporter has already been set.
fn set_global_reporter(report: Report) -> Result<(), SetGlobalReporterError> {
    // `compare_exchange` tries to store `SETTING` if the current value is `UN_SET`
    // this returns `Ok(_)` if the current value of `GLOBAL_REPORTER_STATE` was `UN_SET` and
    // `SETTING` was written, this guarantees the value is `SETTING`.
    if GLOBAL_REPORTER_STATE
        .compare_exchange(UN_SET, SETTING, Ordering::SeqCst, Ordering::SeqCst)
        .is_ok()
    {
        unsafe {
            GLOBAL_REPORTER = Some(report);
        }
        GLOBAL_REPORTER_STATE.store(SET, Ordering::SeqCst);
        Ok(())
    } else {
        Err(SetGlobalReporterError { _priv: () })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::str::FromStr;

    #[test]
    fn scoped_reporter_works() {
        #[derive(Debug)]
        struct TestReporter;
        impl Reporter for TestReporter {}

        with_scoped(&Report::new(TestReporter), || {
            get_default(|reporter| assert!(reporter.is::<TestReporter>()))
        });
    }

    #[test]
    fn global_and_scoped_reporter_works() {
        get_default(|reporter| {
            assert!(reporter.is::<NoReporter>());
        });

        set_global_reporter(Report::new(BasicStdoutReporter::default())).unwrap();
        #[derive(Debug)]
        struct TestReporter;
        impl Reporter for TestReporter {}

        with_scoped(&Report::new(TestReporter), || {
            get_default(|reporter| assert!(reporter.is::<TestReporter>()))
        });

        get_default(|reporter| assert!(reporter.is::<BasicStdoutReporter>()))
    }

    #[test]
    fn test_unresolved_message() {
        let unresolved = vec![(Path::new("./src/Import.sol"), Path::new("src/File.col"))];

        let remappings = vec![Remapping::from_str("oz=a/b/c/d").unwrap()];

        assert_eq!(
            format_unresolved_imports(&unresolved, &remappings).trim(),
            r#"
Unable to resolve imports:
      "./src/Import.sol" in "src/File.col"
with remappings:
      oz=a/b/c/d/"#
                .trim()
        )
    }
}