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);
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;
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(())
}
pub fn init<T>(reporter: T)
where
T: Reporter + Send + Sync + 'static,
{
try_init(reporter).expect("Failed to install global reporter")
}
pub trait Reporter: 'static + std::fmt::Debug {
fn on_solc_spawn(
&self,
_solc: &Solc,
_version: &Version,
_input: &CompilerInput,
_dirty_files: &[PathBuf],
) {
}
fn on_solc_success(
&self,
_solc: &Solc,
_version: &Version,
_output: &CompilerOutput,
_duration: &Duration,
) {
}
fn on_solc_installation_start(&self, _version: &Version) {}
fn on_solc_installation_success(&self, _version: &Version) {}
fn on_solc_installation_error(&self, _version: &Version, _error: &str) {}
fn on_unresolved_imports(&self, _imports: &[(&Path, &Path)], _remappings: &[Remapping]) {}
unsafe fn downcast_raw(&self, id: TypeId) -> Option<NonNull<()>> {
if id == TypeId::of::<Self>() {
Some(NonNull::from(self).cast())
} else {
None
}
}
}
impl dyn Reporter {
pub fn is<T: Any>(&self) -> bool {
self.downcast_ref::<T>().is_some()
}
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 {
Some(GLOBAL_REPORTER.as_ref().expect(
"Reporter invariant violated: GLOBAL_REPORTER must be initialized before GLOBAL_REPORTER_STATE is set",
))
}
}
#[inline(always)]
pub fn get_default<T, F>(mut f: F) -> T
where
F: FnMut(&Report) -> T,
{
if SCOPED_COUNT.load(Ordering::Acquire) == 0 {
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()))
}
pub fn with_global<T>(f: impl FnOnce(&Report) -> T) -> Option<T> {
let report = get_global()?;
Some(f(report))
}
pub fn with_scoped<T>(report: &Report, f: impl FnOnce() -> T) -> T {
let _guard = set_scoped(report);
f()
}
struct State {
scoped: RefCell<Report>,
}
impl State {
#[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)
}
}
#[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() {
let prev = CURRENT_STATE.try_with(|state| state.scoped.replace(report));
drop(prev)
}
}
}
#[must_use = "Dropping the guard unregisters the reporter."]
pub fn set_scoped(reporter: &Report) -> ScopeGuard {
State::set_scoped(reporter.clone())
}
#[derive(Copy, Clone, Debug, Default)]
pub struct NoReporter(());
impl Reporter for NoReporter {}
#[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 {
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
);
}
fn on_solc_installation_start(&self, version: &Version) {
println!("installing solc version \"{version}\"");
}
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))
}
}
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 ")
)
}
#[derive(Debug)]
pub struct SetGlobalReporterError {
_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 {}
#[derive(Clone)]
pub struct Report {
reporter: Arc<dyn Reporter + Send + Sync>,
}
impl Report {
pub fn none() -> Self {
Report { reporter: Arc::new(NoReporter::default()) }
}
pub fn new<S>(reporter: S) -> Self
where
S: Reporter + Send + Sync + 'static,
{
Self { reporter: Arc::new(reporter) }
}
#[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(...)")
}
}
fn set_global_reporter(report: Report) -> Result<(), SetGlobalReporterError> {
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()
)
}
}