From 0c2c589925b723210de18ee7b845bc10210da5eb Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Thu, 24 Aug 2023 17:44:39 +0200 Subject: [PATCH 01/14] feat(cli): add shell utils --- crates/cli/src/io/mod.rs | 2 + crates/cli/src/io/shell.rs | 622 +++++++++++++++++++++++++++++++ crates/cli/src/{ => io}/stdin.rs | 0 crates/cli/src/lib.rs | 4 +- 4 files changed, 627 insertions(+), 1 deletion(-) create mode 100644 crates/cli/src/io/mod.rs create mode 100644 crates/cli/src/io/shell.rs rename crates/cli/src/{ => io}/stdin.rs (100%) diff --git a/crates/cli/src/io/mod.rs b/crates/cli/src/io/mod.rs new file mode 100644 index 0000000000000..6e21af92966f5 --- /dev/null +++ b/crates/cli/src/io/mod.rs @@ -0,0 +1,2 @@ +pub mod shell; +pub mod stdin; diff --git a/crates/cli/src/io/shell.rs b/crates/cli/src/io/shell.rs new file mode 100644 index 0000000000000..10c287323fbee --- /dev/null +++ b/crates/cli/src/io/shell.rs @@ -0,0 +1,622 @@ +use std::{ + fmt, + io::{prelude::*, IsTerminal}, +}; + +use termcolor::{ + self, Color, + Color::{Cyan, Green, Red, Yellow}, + ColorSpec, StandardStream, WriteColor, +}; + +use crate::util::errors::CargoResult; + +pub enum TtyWidth { + NoTty, + Known(usize), + Guess(usize), +} + +impl TtyWidth { + /// Returns the width of the terminal to use for diagnostics (which is + /// relayed to rustc via `--diagnostic-width`). + pub fn diagnostic_terminal_width(&self) -> Option { + // ALLOWED: For testing cargo itself only. + #[allow(clippy::disallowed_methods)] + if let Ok(width) = std::env::var("__CARGO_TEST_TTY_WIDTH_DO_NOT_USE_THIS") { + return Some(width.parse().unwrap()) + } + match *self { + TtyWidth::NoTty | TtyWidth::Guess(_) => None, + TtyWidth::Known(width) => Some(width), + } + } + + /// Returns the width used by progress bars for the tty. + pub fn progress_max_width(&self) -> Option { + match *self { + TtyWidth::NoTty => None, + TtyWidth::Known(width) | TtyWidth::Guess(width) => Some(width), + } + } +} + +/// The requested verbosity of output. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Verbosity { + Verbose, + Normal, + Quiet, +} + +/// An abstraction around console output that remembers preferences for output +/// verbosity and color. +pub struct Shell { + /// Wrapper around stdout/stderr. This helps with supporting sending + /// output to a memory buffer which is useful for tests. + output: ShellOut, + /// How verbose messages should be. + verbosity: Verbosity, + /// Flag that indicates the current line needs to be cleared before + /// printing. Used when a progress bar is currently displayed. + needs_clear: bool, +} + +impl fmt::Debug for Shell { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.output { + ShellOut::Write(_) => { + f.debug_struct("Shell").field("verbosity", &self.verbosity).finish() + } + ShellOut::Stream { color_choice, .. } => f + .debug_struct("Shell") + .field("verbosity", &self.verbosity) + .field("color_choice", &color_choice) + .finish(), + } + } +} + +/// A `Write`able object, either with or without color support +enum ShellOut { + /// A plain write object without color support + Write(Box), + /// Color-enabled stdio, with information on whether color should be used + Stream { + stdout: StandardStream, + stderr: StandardStream, + stderr_tty: bool, + color_choice: ColorChoice, + }, +} + +/// Whether messages should use color output +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum ColorChoice { + /// Force color output + Always, + /// Force disable color output + Never, + /// Intelligently guess whether to use color output + CargoAuto, +} + +impl Shell { + /// Creates a new shell (color choice and verbosity), defaulting to 'auto' color and verbose + /// output. + pub fn new() -> Shell { + let auto_clr = ColorChoice::CargoAuto; + Shell { + output: ShellOut::Stream { + stdout: StandardStream::stdout(auto_clr.to_termcolor_color_choice(Stream::Stdout)), + stderr: StandardStream::stderr(auto_clr.to_termcolor_color_choice(Stream::Stderr)), + color_choice: ColorChoice::CargoAuto, + stderr_tty: std::io::stderr().is_terminal(), + }, + verbosity: Verbosity::Verbose, + needs_clear: false, + } + } + + /// Creates a shell from a plain writable object, with no color, and max verbosity. + pub fn from_write(out: Box) -> Shell { + Shell { output: ShellOut::Write(out), verbosity: Verbosity::Verbose, needs_clear: false } + } + + /// Prints a message, where the status will have `color` color, and can be justified. The + /// messages follows without color. + fn print( + &mut self, + status: &dyn fmt::Display, + message: Option<&dyn fmt::Display>, + color: Color, + justified: bool, + ) -> CargoResult<()> { + match self.verbosity { + Verbosity::Quiet => Ok(()), + _ => { + if self.needs_clear { + self.err_erase_line(); + } + self.output.message_stderr(status, message, color, justified) + } + } + } + + /// Sets whether the next print should clear the current line. + pub fn set_needs_clear(&mut self, needs_clear: bool) { + self.needs_clear = needs_clear; + } + + /// Returns `true` if the `needs_clear` flag is unset. + pub fn is_cleared(&self) -> bool { + !self.needs_clear + } + + /// Returns the width of the terminal in spaces, if any. + pub fn err_width(&self) -> TtyWidth { + match self.output { + ShellOut::Stream { stderr_tty: true, .. } => imp::stderr_width(), + _ => TtyWidth::NoTty, + } + } + + /// Returns `true` if stderr is a tty. + pub fn is_err_tty(&self) -> bool { + match self.output { + ShellOut::Stream { stderr_tty, .. } => stderr_tty, + _ => false, + } + } + + /// Gets a reference to the underlying stdout writer. + pub fn out(&mut self) -> &mut dyn Write { + if self.needs_clear { + self.err_erase_line(); + } + self.output.stdout() + } + + /// Gets a reference to the underlying stderr writer. + pub fn err(&mut self) -> &mut dyn Write { + if self.needs_clear { + self.err_erase_line(); + } + self.output.stderr() + } + + /// Erase from cursor to end of line. + pub fn err_erase_line(&mut self) { + if self.err_supports_color() { + imp::err_erase_line(self); + self.needs_clear = false; + } + } + + /// Shortcut to right-align and color green a status message. + pub fn status(&mut self, status: T, message: U) -> CargoResult<()> + where + T: fmt::Display, + U: fmt::Display, + { + self.print(&status, Some(&message), Green, true) + } + + pub fn status_header(&mut self, status: T) -> CargoResult<()> + where + T: fmt::Display, + { + self.print(&status, None, Cyan, true) + } + + /// Shortcut to right-align a status message. + pub fn status_with_color( + &mut self, + status: T, + message: U, + color: Color, + ) -> CargoResult<()> + where + T: fmt::Display, + U: fmt::Display, + { + self.print(&status, Some(&message), color, true) + } + + /// Runs the callback only if we are in verbose mode. + pub fn verbose(&mut self, mut callback: F) -> CargoResult<()> + where + F: FnMut(&mut Shell) -> CargoResult<()>, + { + match self.verbosity { + Verbosity::Verbose => callback(self), + _ => Ok(()), + } + } + + /// Runs the callback if we are not in verbose mode. + pub fn concise(&mut self, mut callback: F) -> CargoResult<()> + where + F: FnMut(&mut Shell) -> CargoResult<()>, + { + match self.verbosity { + Verbosity::Verbose => Ok(()), + _ => callback(self), + } + } + + /// Prints a red 'error' message. + pub fn error(&mut self, message: T) -> CargoResult<()> { + if self.needs_clear { + self.err_erase_line(); + } + self.output.message_stderr(&"error", Some(&message), Red, false) + } + + /// Prints an amber 'warning' message. + pub fn warn(&mut self, message: T) -> CargoResult<()> { + match self.verbosity { + Verbosity::Quiet => Ok(()), + _ => self.print(&"warning", Some(&message), Yellow, false), + } + } + + /// Prints a cyan 'note' message. + pub fn note(&mut self, message: T) -> CargoResult<()> { + self.print(&"note", Some(&message), Cyan, false) + } + + /// Updates the verbosity of the shell. + pub fn set_verbosity(&mut self, verbosity: Verbosity) { + self.verbosity = verbosity; + } + + /// Gets the verbosity of the shell. + pub fn verbosity(&self) -> Verbosity { + self.verbosity + } + + /// Updates the color choice (always, never, or auto) from a string.. + pub fn set_color_choice(&mut self, color: Option<&str>) -> CargoResult<()> { + if let ShellOut::Stream { ref mut stdout, ref mut stderr, ref mut color_choice, .. } = + self.output + { + let cfg = match color { + Some("always") => ColorChoice::Always, + Some("never") => ColorChoice::Never, + + Some("auto") | None => ColorChoice::CargoAuto, + + Some(arg) => anyhow::bail!( + "argument for --color must be auto, always, or \ + never, but found `{}`", + arg + ), + }; + *color_choice = cfg; + *stdout = StandardStream::stdout(cfg.to_termcolor_color_choice(Stream::Stdout)); + *stderr = StandardStream::stderr(cfg.to_termcolor_color_choice(Stream::Stderr)); + } + Ok(()) + } + + /// Gets the current color choice. + /// + /// If we are not using a color stream, this will always return `Never`, even if the color + /// choice has been set to something else. + pub fn color_choice(&self) -> ColorChoice { + match self.output { + ShellOut::Stream { color_choice, .. } => color_choice, + ShellOut::Write(_) => ColorChoice::Never, + } + } + + /// Whether the shell supports color. + pub fn err_supports_color(&self) -> bool { + match &self.output { + ShellOut::Write(_) => false, + ShellOut::Stream { stderr, .. } => stderr.supports_color(), + } + } + + pub fn out_supports_color(&self) -> bool { + match &self.output { + ShellOut::Write(_) => false, + ShellOut::Stream { stdout, .. } => stdout.supports_color(), + } + } + + /// Write a styled fragment + /// + /// Caller is responsible for deciding whether [`Shell::verbosity`] is affects output. + pub fn write_stdout( + &mut self, + fragment: impl fmt::Display, + color: &ColorSpec, + ) -> CargoResult<()> { + self.output.write_stdout(fragment, color) + } + + /// Write a styled fragment + /// + /// Caller is responsible for deciding whether [`Shell::verbosity`] is affects output. + pub fn write_stderr( + &mut self, + fragment: impl fmt::Display, + color: &ColorSpec, + ) -> CargoResult<()> { + self.output.write_stderr(fragment, color) + } + + /// Prints a message to stderr and translates ANSI escape code into console colors. + pub fn print_ansi_stderr(&mut self, message: &[u8]) -> CargoResult<()> { + if self.needs_clear { + self.err_erase_line(); + } + #[cfg(windows)] + { + if let ShellOut::Stream { stderr, .. } = &mut self.output { + ::fwdansi::write_ansi(stderr, message)?; + return Ok(()) + } + } + self.err().write_all(message)?; + Ok(()) + } + + /// Prints a message to stdout and translates ANSI escape code into console colors. + pub fn print_ansi_stdout(&mut self, message: &[u8]) -> CargoResult<()> { + if self.needs_clear { + self.err_erase_line(); + } + #[cfg(windows)] + { + if let ShellOut::Stream { stdout, .. } = &mut self.output { + ::fwdansi::write_ansi(stdout, message)?; + return Ok(()) + } + } + self.out().write_all(message)?; + Ok(()) + } + + pub fn print_json(&mut self, obj: &T) -> CargoResult<()> { + // Path may fail to serialize to JSON ... + let encoded = serde_json::to_string(&obj)?; + // ... but don't fail due to a closed pipe. + drop(writeln!(self.out(), "{}", encoded)); + Ok(()) + } +} + +impl Default for Shell { + fn default() -> Self { + Self::new() + } +} + +impl ShellOut { + /// Prints out a message with a status. The status comes first, and is bold plus the given + /// color. The status can be justified, in which case the max width that will right align is + /// 12 chars. + fn message_stderr( + &mut self, + status: &dyn fmt::Display, + message: Option<&dyn fmt::Display>, + color: Color, + justified: bool, + ) -> CargoResult<()> { + match *self { + ShellOut::Stream { ref mut stderr, .. } => { + stderr.reset()?; + stderr.set_color(ColorSpec::new().set_bold(true).set_fg(Some(color)))?; + if justified { + write!(stderr, "{:>12}", status)?; + } else { + write!(stderr, "{}", status)?; + stderr.set_color(ColorSpec::new().set_bold(true))?; + write!(stderr, ":")?; + } + stderr.reset()?; + match message { + Some(message) => writeln!(stderr, " {}", message)?, + None => write!(stderr, " ")?, + } + } + ShellOut::Write(ref mut w) => { + if justified { + write!(w, "{:>12}", status)?; + } else { + write!(w, "{}:", status)?; + } + match message { + Some(message) => writeln!(w, " {}", message)?, + None => write!(w, " ")?, + } + } + } + Ok(()) + } + + /// Write a styled fragment + fn write_stdout(&mut self, fragment: impl fmt::Display, color: &ColorSpec) -> CargoResult<()> { + match *self { + ShellOut::Stream { ref mut stdout, .. } => { + stdout.reset()?; + stdout.set_color(&color)?; + write!(stdout, "{}", fragment)?; + stdout.reset()?; + } + ShellOut::Write(ref mut w) => { + write!(w, "{}", fragment)?; + } + } + Ok(()) + } + + /// Write a styled fragment + fn write_stderr(&mut self, fragment: impl fmt::Display, color: &ColorSpec) -> CargoResult<()> { + match *self { + ShellOut::Stream { ref mut stderr, .. } => { + stderr.reset()?; + stderr.set_color(&color)?; + write!(stderr, "{}", fragment)?; + stderr.reset()?; + } + ShellOut::Write(ref mut w) => { + write!(w, "{}", fragment)?; + } + } + Ok(()) + } + + /// Gets stdout as a `io::Write`. + fn stdout(&mut self) -> &mut dyn Write { + match *self { + ShellOut::Stream { ref mut stdout, .. } => stdout, + ShellOut::Write(ref mut w) => w, + } + } + + /// Gets stderr as a `io::Write`. + fn stderr(&mut self) -> &mut dyn Write { + match *self { + ShellOut::Stream { ref mut stderr, .. } => stderr, + ShellOut::Write(ref mut w) => w, + } + } +} + +impl ColorChoice { + /// Converts our color choice to termcolor's version. + fn to_termcolor_color_choice(self, stream: Stream) -> termcolor::ColorChoice { + match self { + ColorChoice::Always => termcolor::ColorChoice::Always, + ColorChoice::Never => termcolor::ColorChoice::Never, + ColorChoice::CargoAuto => { + if stream.is_terminal() { + termcolor::ColorChoice::Auto + } else { + termcolor::ColorChoice::Never + } + } + } + } +} + +enum Stream { + Stdout, + Stderr, +} + +impl Stream { + fn is_terminal(self) -> bool { + match self { + Self::Stdout => std::io::stdout().is_terminal(), + Self::Stderr => std::io::stderr().is_terminal(), + } + } +} + +#[cfg(unix)] +mod imp { + use super::{Shell, TtyWidth}; + use std::mem; + + pub fn stderr_width() -> TtyWidth { + unsafe { + let mut winsize: libc::winsize = mem::zeroed(); + // The .into() here is needed for FreeBSD which defines TIOCGWINSZ + // as c_uint but ioctl wants c_ulong. + if libc::ioctl(libc::STDERR_FILENO, libc::TIOCGWINSZ.into(), &mut winsize) < 0 { + return TtyWidth::NoTty + } + if winsize.ws_col > 0 { + TtyWidth::Known(winsize.ws_col as usize) + } else { + TtyWidth::NoTty + } + } + } + + pub fn err_erase_line(shell: &mut Shell) { + // This is the "EL - Erase in Line" sequence. It clears from the cursor + // to the end of line. + // https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences + let _ = shell.output.stderr().write_all(b"\x1B[K"); + } +} + +#[cfg(windows)] +mod imp { + use std::{cmp, mem, ptr}; + + use windows_sys::{ + core::PCSTR, + Win32::{ + Foundation::{CloseHandle, GENERIC_READ, GENERIC_WRITE, INVALID_HANDLE_VALUE}, + Storage::FileSystem::{CreateFileA, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING}, + System::Console::{ + GetConsoleScreenBufferInfo, GetStdHandle, CONSOLE_SCREEN_BUFFER_INFO, + STD_ERROR_HANDLE, + }, + }, + }; + + pub(super) use super::{default_err_erase_line as err_erase_line, TtyWidth}; + + pub fn stderr_width() -> TtyWidth { + unsafe { + let stdout = GetStdHandle(STD_ERROR_HANDLE); + let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed(); + if GetConsoleScreenBufferInfo(stdout, &mut csbi) != 0 { + return TtyWidth::Known((csbi.srWindow.Right - csbi.srWindow.Left) as usize) + } + + // On mintty/msys/cygwin based terminals, the above fails with + // INVALID_HANDLE_VALUE. Use an alternate method which works + // in that case as well. + let h = CreateFileA( + "CONOUT$\0".as_ptr() as PCSTR, + GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, + ptr::null_mut(), + OPEN_EXISTING, + 0, + 0, + ); + if h == INVALID_HANDLE_VALUE { + return TtyWidth::NoTty + } + + let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed(); + let rc = GetConsoleScreenBufferInfo(h, &mut csbi); + CloseHandle(h); + if rc != 0 { + let width = (csbi.srWindow.Right - csbi.srWindow.Left) as usize; + // Unfortunately cygwin/mintty does not set the size of the + // backing console to match the actual window size. This + // always reports a size of 80 or 120 (not sure what + // determines that). Use a conservative max of 60 which should + // work in most circumstances. ConEmu does some magic to + // resize the console correctly, but there's no reasonable way + // to detect which kind of terminal we are running in, or if + // GetConsoleScreenBufferInfo returns accurate information. + return TtyWidth::Guess(cmp::min(60, width)) + } + + TtyWidth::NoTty + } + } +} + +#[cfg(windows)] +fn default_err_erase_line(shell: &mut Shell) { + match imp::stderr_width() { + TtyWidth::Known(max_width) | TtyWidth::Guess(max_width) => { + let blank = " ".repeat(max_width); + drop(write!(shell.output.stderr(), "{}\r", blank)); + } + _ => (), + } +} diff --git a/crates/cli/src/stdin.rs b/crates/cli/src/io/stdin.rs similarity index 100% rename from crates/cli/src/stdin.rs rename to crates/cli/src/io/stdin.rs diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 39f4e95765dd2..d8619293f1646 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -2,5 +2,7 @@ pub mod handler; pub mod opts; -pub mod stdin; pub mod utils; + +mod io; +pub use io::{shell, stdin}; From bcf9947f7782eafd5bbd7c2e8144fd756f95b4db Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Fri, 25 Aug 2023 01:38:34 +0200 Subject: [PATCH 02/14] adapt shell to Foundry --- Cargo.lock | 14 ++ crates/cast/src/base.rs | 10 +- crates/cli/Cargo.toml | 6 + crates/cli/src/io/macros.rs | 123 +++++++++++ crates/cli/src/io/mod.rs | 5 + crates/cli/src/io/shell.rs | 397 ++++++++++++++--------------------- crates/cli/src/io/stdin.rs | 31 --- crates/cli/src/lib.rs | 2 +- crates/cli/src/opts/mod.rs | 2 + crates/cli/src/opts/shell.rs | 34 +++ crates/cli/src/utils/mod.rs | 9 + 11 files changed, 354 insertions(+), 279 deletions(-) create mode 100644 crates/cli/src/io/macros.rs create mode 100644 crates/cli/src/opts/shell.rs diff --git a/Cargo.lock b/Cargo.lock index 2b384951b10a5..ffd0fc8839dc6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2445,6 +2445,7 @@ dependencies = [ "foundry-common", "foundry-config", "foundry-evm", + "fwdansi", "indicatif", "itertools 0.11.0", "once_cell", @@ -2453,9 +2454,12 @@ dependencies = [ "rusoto_core", "rusoto_kms", "serde", + "serde_json", "strsim", "strum 0.25.0", "tempfile", + "termcolor", + "terminal_size", "thiserror", "tokio", "tracing", @@ -2773,6 +2777,16 @@ dependencies = [ "slab", ] +[[package]] +name = "fwdansi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c1f5787fe85505d1f7777268db5103d80a7a374d2316a7ce262e57baf8f208" +dependencies = [ + "memchr", + "termcolor", +] + [[package]] name = "fxhash" version = "0.2.1" diff --git a/crates/cast/src/base.rs b/crates/cast/src/base.rs index 016d7402aea37..1a7e87853d32e 100644 --- a/crates/cast/src/base.rs +++ b/crates/cast/src/base.rs @@ -44,12 +44,12 @@ impl FromStr for Base { "10" | "d" | "dec" | "decimal" => Ok(Self::Decimal), "16" | "h" | "hex" | "hexadecimal" => Ok(Self::Hexadecimal), s => Err(eyre::eyre!( - r#"Invalid base "{}". Possible values: -2, b, bin, binary -8, o, oct, octal + "\ +Invalid base \"{s}\". Possible values: + 2, b, bin, binary + 8, o, oct, octal 10, d, dec, decimal -16, h, hex, hexadecimal"#, - s +16, h, hex, hexadecimal" )), } } diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 07734dd122ac6..bd106efc5ea92 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -35,8 +35,11 @@ once_cell = "1" regex = { version = "1", default-features = false } rpassword = "7" serde = { version = "1", features = ["derive"] } +serde_json = "1" strsim = "0.10" strum = { version = "0.25", features = ["derive"] } +termcolor = "1" +terminal_size = "0.2" thiserror = "1" tokio = { version = "1", features = ["macros"] } tracing = "0.1" @@ -44,6 +47,9 @@ tracing-error = "0.2" tracing-subscriber = { version = "0.3", features = ["registry", "env-filter", "fmt"] } yansi = "0.5" +[target.'cfg(windows)'.dependencies] +fwdansi = "1" + [dev-dependencies] tempfile = "3.7" diff --git a/crates/cli/src/io/macros.rs b/crates/cli/src/io/macros.rs new file mode 100644 index 0000000000000..654795ed33095 --- /dev/null +++ b/crates/cli/src/io/macros.rs @@ -0,0 +1,123 @@ +/// Prints a message to [`stdout`][io::stdout] and [reads a line from stdin into a String](read). +/// +/// Returns `Result`, so sometimes `T` must be explicitly specified, like in `str::parse`. +/// +/// # Examples +/// +/// ```no_run +/// # use foundry_cli::prompt; +/// let response: String = prompt!("Would you like to continue? [y/N] ")?; +/// if !matches!(response.as_str(), "y" | "Y") { +/// return Ok(()) +/// } +/// # Ok::<(), Box>(()) +/// ``` +#[macro_export] +macro_rules! prompt { + () => { + $crate::stdin::parse_line() + }; + + ($($tt:tt)+) => {{ + ::std::print!($($tt)+); + match ::std::io::Write::flush(&mut ::std::io::stdout()) { + ::core::result::Result::Ok(()) => $crate::prompt!(), + ::core::result::Result::Err(e) => ::core::result::Result::Err(::eyre::eyre!("Could not flush stdout: {e}")) + } + }}; +} + +/// Prints a formatted error to stderr. +#[macro_export] +macro_rules! sh_err { + ($($args:tt)*) => { + $crate::Shell::error(&mut *$crate::Shell::get(), ::core::format_args!($($args)*)) + }; +} + +/// Prints a formatted warning to stderr. +#[macro_export] +macro_rules! sh_warn { + ($($args:tt)*) => { + $crate::Shell::warn(&mut *$crate::Shell::get(), ::core::format_args!($($args)*)) + }; +} + +/// Prints a formatted note to stderr. +#[macro_export] +macro_rules! sh_note { + ($($args:tt)*) => { + $crate::Shell::note(&mut *$crate::Shell::get(), ::core::format_args!($($args)*)) + }; +} + +/// Prints a raw formatted message to stdout. +#[macro_export] +macro_rules! sh_print { + ($($args:tt)*) => { + $crate::Shell::print_out(&mut *$crate::Shell::get(), ::core::format_args!($($args)*)) + }; +} + +/// Prints a raw formatted message to stderr. +#[macro_export] +macro_rules! sh_eprint { + ($($args:tt)*) => { + $crate::Shell::print_err(&mut *$crate::Shell::get(), ::core::format_args!($($args)*)) + }; +} + +/// Prints a raw formatted message to stdout, with a trailing newline. +#[macro_export] +macro_rules! sh_println { + () => { + $crate::sh_print!("\n") + }; + + ($($t:tt)*) => { + $crate::sh_print!("{}\n", ::core::format_args!($($t)*)) + }; +} + +/// Prints a raw formatted message to stderr, with a trailing newline. +#[macro_export] +macro_rules! sh_eprintln { + () => { + $crate::sh_eprint!("\n") + }; + + ($($t:tt)+) => { + $crate::sh_eprint!("{}\n", ::core::format_args!($($t)+)) + }; +} + +#[cfg(test)] +mod tests { + use crate::Shell; + + #[test] + fn macros() { + Shell::new().set(); + + sh_err!("err").unwrap(); + sh_err!("err {}", "arg").unwrap(); + + sh_warn!("warn").unwrap(); + sh_warn!("warn {}", "arg").unwrap(); + + sh_note!("note").unwrap(); + sh_note!("note {}", "arg").unwrap(); + + sh_print!("print -").unwrap(); + sh_print!("print {} -", "arg").unwrap(); + + sh_println!("println").unwrap(); + sh_println!("println {}", "arg").unwrap(); + + sh_eprint!("eprint -").unwrap(); + sh_eprint!("eprint {} -", "arg").unwrap(); + + sh_eprintln!("eprintln").unwrap(); + sh_eprintln!("eprintln {}", "arg").unwrap(); + } +} diff --git a/crates/cli/src/io/mod.rs b/crates/cli/src/io/mod.rs index 6e21af92966f5..bc5f0c05a1c8e 100644 --- a/crates/cli/src/io/mod.rs +++ b/crates/cli/src/io/mod.rs @@ -1,2 +1,7 @@ +mod macros; + pub mod shell; pub mod stdin; + +#[doc(no_inline)] +pub use shell::Shell; diff --git a/crates/cli/src/io/shell.rs b/crates/cli/src/io/shell.rs index 10c287323fbee..0b49fd1803392 100644 --- a/crates/cli/src/io/shell.rs +++ b/crates/cli/src/io/shell.rs @@ -1,15 +1,22 @@ +//! Utility functions for writing to [`stdout`](std::io::stdout) and [`stderr`](std::io::stderr). +//! +//! Originally from [cargo](https://github.com/rust-lang/cargo/blob/35814255a1dbaeca9219fae81d37a8190050092c/src/cargo/core/shell.rs). + +use clap::ValueEnum; +use eyre::Result; +use once_cell::sync::Lazy; use std::{ fmt, io::{prelude::*, IsTerminal}, + ops::DerefMut, + sync::Mutex, }; - use termcolor::{ - self, Color, - Color::{Cyan, Green, Red, Yellow}, + Color::{self, Cyan, Green, Red, Yellow}, ColorSpec, StandardStream, WriteColor, }; -use crate::util::errors::CargoResult; +static GLOBAL_SHELL: Lazy> = Lazy::new(|| Mutex::new(Shell::new())); pub enum TtyWidth { NoTty, @@ -18,17 +25,15 @@ pub enum TtyWidth { } impl TtyWidth { - /// Returns the width of the terminal to use for diagnostics (which is - /// relayed to rustc via `--diagnostic-width`). - pub fn diagnostic_terminal_width(&self) -> Option { - // ALLOWED: For testing cargo itself only. - #[allow(clippy::disallowed_methods)] - if let Ok(width) = std::env::var("__CARGO_TEST_TTY_WIDTH_DO_NOT_USE_THIS") { - return Some(width.parse().unwrap()) - } - match *self { - TtyWidth::NoTty | TtyWidth::Guess(_) => None, - TtyWidth::Known(width) => Some(width), + pub fn get() -> Self { + // use stderr + #[cfg(unix)] + let opt = terminal_size::terminal_size_using_fd(2.into()); + #[cfg(not(unix))] + let opt = terminal_size::terminal_size(); + match opt { + Some((w, _)) => Self::Known(w.0 as usize), + None => Self::NoTty, } } @@ -42,10 +47,14 @@ impl TtyWidth { } /// The requested verbosity of output. -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Default, Clone, Copy, PartialEq)] pub enum Verbosity { + /// All output Verbose, + /// Default output + #[default] Normal, + /// No output Quiet, } @@ -64,24 +73,20 @@ pub struct Shell { impl fmt::Debug for Shell { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self.output { - ShellOut::Write(_) => { - f.debug_struct("Shell").field("verbosity", &self.verbosity).finish() - } - ShellOut::Stream { color_choice, .. } => f - .debug_struct("Shell") - .field("verbosity", &self.verbosity) - .field("color_choice", &color_choice) - .finish(), + let mut s = f.debug_struct("Shell"); + s.field("verbosity", &self.verbosity); + if let ShellOut::Stream { color_choice, .. } = self.output { + s.field("color_choice", &color_choice); } + s.finish() } } -/// A `Write`able object, either with or without color support +/// A `Write`able object, either with or without color support. enum ShellOut { - /// A plain write object without color support - Write(Box), - /// Color-enabled stdio, with information on whether color should be used + /// A plain write object without color support. + Write(Box), + /// Color-enabled stdio, with information on whether color should be used. Stream { stdout: StandardStream, stderr: StandardStream, @@ -90,37 +95,62 @@ enum ShellOut { }, } -/// Whether messages should use color output -#[derive(Debug, PartialEq, Clone, Copy)] +/// Whether messages should use color output. +#[derive(Debug, Default, PartialEq, Clone, Copy, ValueEnum)] pub enum ColorChoice { - /// Force color output + /// Force color output. Always, - /// Force disable color output + /// Force disable color output. Never, - /// Intelligently guess whether to use color output - CargoAuto, + /// Intelligently guess whether to use color output. + #[default] + Auto, +} + +impl Default for Shell { + #[inline] + fn default() -> Self { + Self::new() + } } impl Shell { /// Creates a new shell (color choice and verbosity), defaulting to 'auto' color and verbose /// output. - pub fn new() -> Shell { - let auto_clr = ColorChoice::CargoAuto; - Shell { + pub fn new() -> Self { + Self::new_with(ColorChoice::Auto, Verbosity::Verbose) + } + + pub fn new_with(color: ColorChoice, verbosity: Verbosity) -> Self { + Self { output: ShellOut::Stream { - stdout: StandardStream::stdout(auto_clr.to_termcolor_color_choice(Stream::Stdout)), - stderr: StandardStream::stderr(auto_clr.to_termcolor_color_choice(Stream::Stderr)), - color_choice: ColorChoice::CargoAuto, + stdout: StandardStream::stdout(color.to_termcolor_color_choice(Stream::Stdout)), + stderr: StandardStream::stderr(color.to_termcolor_color_choice(Stream::Stderr)), + color_choice: color, stderr_tty: std::io::stderr().is_terminal(), }, - verbosity: Verbosity::Verbose, + verbosity, needs_clear: false, } } /// Creates a shell from a plain writable object, with no color, and max verbosity. - pub fn from_write(out: Box) -> Shell { - Shell { output: ShellOut::Write(out), verbosity: Verbosity::Verbose, needs_clear: false } + pub fn from_write(out: Box) -> Self { + Self { output: ShellOut::Write(out), verbosity: Verbosity::Verbose, needs_clear: false } + } + + /// Get a static reference to the global shell. + #[inline] + #[track_caller] + pub fn get() -> impl DerefMut + 'static { + GLOBAL_SHELL.lock().unwrap() + } + + /// Set the global shell. + #[inline] + #[track_caller] + pub fn set(self) { + *GLOBAL_SHELL.lock().unwrap() = self; } /// Prints a message, where the status will have `color` color, and can be justified. The @@ -131,7 +161,7 @@ impl Shell { message: Option<&dyn fmt::Display>, color: Color, justified: bool, - ) -> CargoResult<()> { + ) -> Result<()> { match self.verbosity { Verbosity::Quiet => Ok(()), _ => { @@ -156,7 +186,7 @@ impl Shell { /// Returns the width of the terminal in spaces, if any. pub fn err_width(&self) -> TtyWidth { match self.output { - ShellOut::Stream { stderr_tty: true, .. } => imp::stderr_width(), + ShellOut::Stream { stderr_tty: true, .. } => TtyWidth::get(), _ => TtyWidth::NoTty, } } @@ -170,6 +200,7 @@ impl Shell { } /// Gets a reference to the underlying stdout writer. + #[inline] pub fn out(&mut self) -> &mut dyn Write { if self.needs_clear { self.err_erase_line(); @@ -178,6 +209,7 @@ impl Shell { } /// Gets a reference to the underlying stderr writer. + #[inline] pub fn err(&mut self) -> &mut dyn Write { if self.needs_clear { self.err_erase_line(); @@ -188,13 +220,16 @@ impl Shell { /// Erase from cursor to end of line. pub fn err_erase_line(&mut self) { if self.err_supports_color() { - imp::err_erase_line(self); - self.needs_clear = false; + // This is the "EL - Erase in Line" sequence. It clears from the cursor + // to the end of line. + // https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences + let _ = self.output.stderr().write_all(b"\x1B[K"); + self.set_needs_clear(false); } } /// Shortcut to right-align and color green a status message. - pub fn status(&mut self, status: T, message: U) -> CargoResult<()> + pub fn status(&mut self, status: T, message: U) -> Result<()> where T: fmt::Display, U: fmt::Display, @@ -202,7 +237,7 @@ impl Shell { self.print(&status, Some(&message), Green, true) } - pub fn status_header(&mut self, status: T) -> CargoResult<()> + pub fn status_header(&mut self, status: T) -> Result<()> where T: fmt::Display, { @@ -210,12 +245,7 @@ impl Shell { } /// Shortcut to right-align a status message. - pub fn status_with_color( - &mut self, - status: T, - message: U, - color: Color, - ) -> CargoResult<()> + pub fn status_with_color(&mut self, status: T, message: U, color: Color) -> Result<()> where T: fmt::Display, U: fmt::Display, @@ -224,9 +254,9 @@ impl Shell { } /// Runs the callback only if we are in verbose mode. - pub fn verbose(&mut self, mut callback: F) -> CargoResult<()> + pub fn verbose(&mut self, mut callback: F) -> Result<()> where - F: FnMut(&mut Shell) -> CargoResult<()>, + F: FnMut(&mut Shell) -> Result<()>, { match self.verbosity { Verbosity::Verbose => callback(self), @@ -235,9 +265,9 @@ impl Shell { } /// Runs the callback if we are not in verbose mode. - pub fn concise(&mut self, mut callback: F) -> CargoResult<()> + pub fn concise(&mut self, mut callback: F) -> Result<()> where - F: FnMut(&mut Shell) -> CargoResult<()>, + F: FnMut(&mut Shell) -> Result<()>, { match self.verbosity { Verbosity::Verbose => Ok(()), @@ -246,7 +276,7 @@ impl Shell { } /// Prints a red 'error' message. - pub fn error(&mut self, message: T) -> CargoResult<()> { + pub fn error(&mut self, message: T) -> Result<()> { if self.needs_clear { self.err_erase_line(); } @@ -254,7 +284,7 @@ impl Shell { } /// Prints an amber 'warning' message. - pub fn warn(&mut self, message: T) -> CargoResult<()> { + pub fn warn(&mut self, message: T) -> Result<()> { match self.verbosity { Verbosity::Quiet => Ok(()), _ => self.print(&"warning", Some(&message), Yellow, false), @@ -262,7 +292,7 @@ impl Shell { } /// Prints a cyan 'note' message. - pub fn note(&mut self, message: T) -> CargoResult<()> { + pub fn note(&mut self, message: T) -> Result<()> { self.print(&"note", Some(&message), Cyan, false) } @@ -277,20 +307,17 @@ impl Shell { } /// Updates the color choice (always, never, or auto) from a string.. - pub fn set_color_choice(&mut self, color: Option<&str>) -> CargoResult<()> { - if let ShellOut::Stream { ref mut stdout, ref mut stderr, ref mut color_choice, .. } = - self.output - { + pub fn set_color_choice(&mut self, color: Option<&str>) -> Result<()> { + if let ShellOut::Stream { stdout, stderr, color_choice, .. } = &mut self.output { let cfg = match color { Some("always") => ColorChoice::Always, Some("never") => ColorChoice::Never, - Some("auto") | None => ColorChoice::CargoAuto, + Some("auto") | None => ColorChoice::Auto, - Some(arg) => anyhow::bail!( + Some(arg) => eyre::bail!( "argument for --color must be auto, always, or \ - never, but found `{}`", - arg + never, but found `{arg}`", ), }; *color_choice = cfg; @@ -329,72 +356,62 @@ impl Shell { /// Write a styled fragment /// /// Caller is responsible for deciding whether [`Shell::verbosity`] is affects output. - pub fn write_stdout( - &mut self, - fragment: impl fmt::Display, - color: &ColorSpec, - ) -> CargoResult<()> { + pub fn write_stdout(&mut self, fragment: impl fmt::Display, color: &ColorSpec) -> Result<()> { self.output.write_stdout(fragment, color) } + pub fn print_out(&mut self, fragment: impl fmt::Display) -> Result<()> { + self.write_stdout(fragment, &ColorSpec::new()) + } + /// Write a styled fragment /// /// Caller is responsible for deciding whether [`Shell::verbosity`] is affects output. - pub fn write_stderr( - &mut self, - fragment: impl fmt::Display, - color: &ColorSpec, - ) -> CargoResult<()> { + pub fn write_stderr(&mut self, fragment: impl fmt::Display, color: &ColorSpec) -> Result<()> { self.output.write_stderr(fragment, color) } + pub fn print_err(&mut self, fragment: impl fmt::Display) -> Result<()> { + self.write_stderr(fragment, &ColorSpec::new()) + } + /// Prints a message to stderr and translates ANSI escape code into console colors. - pub fn print_ansi_stderr(&mut self, message: &[u8]) -> CargoResult<()> { + pub fn print_ansi_stderr(&mut self, message: &[u8]) -> Result<()> { if self.needs_clear { self.err_erase_line(); } #[cfg(windows)] - { - if let ShellOut::Stream { stderr, .. } = &mut self.output { - ::fwdansi::write_ansi(stderr, message)?; - return Ok(()) - } + if let ShellOut::Stream { stderr, .. } = &self.output { + ::fwdansi::write_ansi(stderr, message)?; + return Ok(()) } self.err().write_all(message)?; Ok(()) } /// Prints a message to stdout and translates ANSI escape code into console colors. - pub fn print_ansi_stdout(&mut self, message: &[u8]) -> CargoResult<()> { + pub fn print_ansi_stdout(&mut self, message: &[u8]) -> Result<()> { if self.needs_clear { self.err_erase_line(); } #[cfg(windows)] - { - if let ShellOut::Stream { stdout, .. } = &mut self.output { - ::fwdansi::write_ansi(stdout, message)?; - return Ok(()) - } + if let ShellOut::Stream { stdout, .. } = &self.output { + ::fwdansi::write_ansi(stdout, message)?; + return Ok(()) } self.out().write_all(message)?; Ok(()) } - pub fn print_json(&mut self, obj: &T) -> CargoResult<()> { + pub fn print_json(&mut self, obj: &T) -> Result<()> { // Path may fail to serialize to JSON ... let encoded = serde_json::to_string(&obj)?; // ... but don't fail due to a closed pipe. - drop(writeln!(self.out(), "{}", encoded)); + let _ = writeln!(self.out(), "{encoded}"); Ok(()) } } -impl Default for Shell { - fn default() -> Self { - Self::new() - } -} - impl ShellOut { /// Prints out a message with a status. The status comes first, and is bold plus the given /// color. The status can be justified, in which case the max width that will right align is @@ -405,33 +422,30 @@ impl ShellOut { message: Option<&dyn fmt::Display>, color: Color, justified: bool, - ) -> CargoResult<()> { - match *self { - ShellOut::Stream { ref mut stderr, .. } => { + ) -> Result<()> { + match self { + Self::Stream { stderr, .. } => { stderr.reset()?; stderr.set_color(ColorSpec::new().set_bold(true).set_fg(Some(color)))?; if justified { - write!(stderr, "{:>12}", status)?; + write!(stderr, "{status:>12}") } else { - write!(stderr, "{}", status)?; + write!(stderr, "{status}")?; stderr.set_color(ColorSpec::new().set_bold(true))?; - write!(stderr, ":")?; - } + write!(stderr, ":") + }?; stderr.reset()?; - match message { - Some(message) => writeln!(stderr, " {}", message)?, - None => write!(stderr, " ")?, + + stderr.write_all(b" ")?; + if let Some(message) = message { + writeln!(stderr, "{message}")?; } } - ShellOut::Write(ref mut w) => { - if justified { - write!(w, "{:>12}", status)?; - } else { - write!(w, "{}:", status)?; - } - match message { - Some(message) => writeln!(w, " {}", message)?, - None => write!(w, " ")?, + Self::Write(w) => { + if justified { write!(w, "{status:>12}") } else { write!(w, "{status}:") }?; + w.write_all(b" ")?; + if let Some(message) = message { + writeln!(w, "{message}")?; } } } @@ -439,50 +453,52 @@ impl ShellOut { } /// Write a styled fragment - fn write_stdout(&mut self, fragment: impl fmt::Display, color: &ColorSpec) -> CargoResult<()> { - match *self { - ShellOut::Stream { ref mut stdout, .. } => { + fn write_stdout(&mut self, fragment: impl fmt::Display, color: &ColorSpec) -> Result<()> { + match self { + Self::Stream { stdout, .. } => { stdout.reset()?; stdout.set_color(&color)?; - write!(stdout, "{}", fragment)?; + write!(stdout, "{fragment}")?; stdout.reset()?; } - ShellOut::Write(ref mut w) => { - write!(w, "{}", fragment)?; + Self::Write(w) => { + write!(w, "{fragment}")?; } } Ok(()) } /// Write a styled fragment - fn write_stderr(&mut self, fragment: impl fmt::Display, color: &ColorSpec) -> CargoResult<()> { - match *self { - ShellOut::Stream { ref mut stderr, .. } => { + fn write_stderr(&mut self, fragment: impl fmt::Display, color: &ColorSpec) -> Result<()> { + match self { + Self::Stream { stderr, .. } => { stderr.reset()?; stderr.set_color(&color)?; - write!(stderr, "{}", fragment)?; + write!(stderr, "{fragment}")?; stderr.reset()?; } - ShellOut::Write(ref mut w) => { - write!(w, "{}", fragment)?; + Self::Write(w) => { + write!(w, "{fragment}")?; } } Ok(()) } /// Gets stdout as a `io::Write`. + #[inline] fn stdout(&mut self) -> &mut dyn Write { - match *self { - ShellOut::Stream { ref mut stdout, .. } => stdout, - ShellOut::Write(ref mut w) => w, + match self { + Self::Stream { stdout, .. } => stdout, + Self::Write(w) => w, } } /// Gets stderr as a `io::Write`. + #[inline] fn stderr(&mut self) -> &mut dyn Write { - match *self { - ShellOut::Stream { ref mut stderr, .. } => stderr, - ShellOut::Write(ref mut w) => w, + match self { + Self::Stream { stderr, .. } => stderr, + Self::Write(w) => w, } } } @@ -493,7 +509,7 @@ impl ColorChoice { match self { ColorChoice::Always => termcolor::ColorChoice::Always, ColorChoice::Never => termcolor::ColorChoice::Never, - ColorChoice::CargoAuto => { + ColorChoice::Auto => { if stream.is_terminal() { termcolor::ColorChoice::Auto } else { @@ -517,106 +533,3 @@ impl Stream { } } } - -#[cfg(unix)] -mod imp { - use super::{Shell, TtyWidth}; - use std::mem; - - pub fn stderr_width() -> TtyWidth { - unsafe { - let mut winsize: libc::winsize = mem::zeroed(); - // The .into() here is needed for FreeBSD which defines TIOCGWINSZ - // as c_uint but ioctl wants c_ulong. - if libc::ioctl(libc::STDERR_FILENO, libc::TIOCGWINSZ.into(), &mut winsize) < 0 { - return TtyWidth::NoTty - } - if winsize.ws_col > 0 { - TtyWidth::Known(winsize.ws_col as usize) - } else { - TtyWidth::NoTty - } - } - } - - pub fn err_erase_line(shell: &mut Shell) { - // This is the "EL - Erase in Line" sequence. It clears from the cursor - // to the end of line. - // https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences - let _ = shell.output.stderr().write_all(b"\x1B[K"); - } -} - -#[cfg(windows)] -mod imp { - use std::{cmp, mem, ptr}; - - use windows_sys::{ - core::PCSTR, - Win32::{ - Foundation::{CloseHandle, GENERIC_READ, GENERIC_WRITE, INVALID_HANDLE_VALUE}, - Storage::FileSystem::{CreateFileA, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING}, - System::Console::{ - GetConsoleScreenBufferInfo, GetStdHandle, CONSOLE_SCREEN_BUFFER_INFO, - STD_ERROR_HANDLE, - }, - }, - }; - - pub(super) use super::{default_err_erase_line as err_erase_line, TtyWidth}; - - pub fn stderr_width() -> TtyWidth { - unsafe { - let stdout = GetStdHandle(STD_ERROR_HANDLE); - let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed(); - if GetConsoleScreenBufferInfo(stdout, &mut csbi) != 0 { - return TtyWidth::Known((csbi.srWindow.Right - csbi.srWindow.Left) as usize) - } - - // On mintty/msys/cygwin based terminals, the above fails with - // INVALID_HANDLE_VALUE. Use an alternate method which works - // in that case as well. - let h = CreateFileA( - "CONOUT$\0".as_ptr() as PCSTR, - GENERIC_READ | GENERIC_WRITE, - FILE_SHARE_READ | FILE_SHARE_WRITE, - ptr::null_mut(), - OPEN_EXISTING, - 0, - 0, - ); - if h == INVALID_HANDLE_VALUE { - return TtyWidth::NoTty - } - - let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed(); - let rc = GetConsoleScreenBufferInfo(h, &mut csbi); - CloseHandle(h); - if rc != 0 { - let width = (csbi.srWindow.Right - csbi.srWindow.Left) as usize; - // Unfortunately cygwin/mintty does not set the size of the - // backing console to match the actual window size. This - // always reports a size of 80 or 120 (not sure what - // determines that). Use a conservative max of 60 which should - // work in most circumstances. ConEmu does some magic to - // resize the console correctly, but there's no reasonable way - // to detect which kind of terminal we are running in, or if - // GetConsoleScreenBufferInfo returns accurate information. - return TtyWidth::Guess(cmp::min(60, width)) - } - - TtyWidth::NoTty - } - } -} - -#[cfg(windows)] -fn default_err_erase_line(shell: &mut Shell) { - match imp::stderr_width() { - TtyWidth::Known(max_width) | TtyWidth::Guess(max_width) => { - let blank = " ".repeat(max_width); - drop(write!(shell.output.stderr(), "{}\r", blank)); - } - _ => (), - } -} diff --git a/crates/cli/src/io/stdin.rs b/crates/cli/src/io/stdin.rs index 8242cc8057724..3f78c5e495999 100644 --- a/crates/cli/src/io/stdin.rs +++ b/crates/cli/src/io/stdin.rs @@ -7,37 +7,6 @@ use std::{ str::FromStr, }; -/// Prints a message to [`stdout`][io::stdout] and [reads a line from stdin into a String](read). -/// -/// Returns `Result`, so sometimes `T` must be explicitly specified, like in `str::parse`. -/// -/// # Examples -/// -/// ```no_run -/// # use foundry_cli::prompt; -/// let response: String = prompt!("Would you like to continue? [y/N] ")?; -/// if !matches!(response.as_str(), "y" | "Y") { -/// return Ok(()) -/// } -/// # Ok::<(), Box>(()) -/// ``` -#[macro_export] -macro_rules! prompt { - () => { - $crate::stdin::parse_line() - }; - - ($($tt:tt)+) => { - { - ::std::print!($($tt)+); - match ::std::io::Write::flush(&mut ::std::io::stdout()) { - ::core::result::Result::Ok(_) => $crate::prompt!(), - ::core::result::Result::Err(e) => ::core::result::Result::Err(::eyre::eyre!("Could not flush stdout: {}", e)) - } - } - }; -} - /// Unwraps the given `Option` or [reads stdin into a String](read) and parses it as `T`. pub fn unwrap(value: Option, read_line: bool) -> Result where diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index d8619293f1646..a6b2f36fb74d4 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -5,4 +5,4 @@ pub mod opts; pub mod utils; mod io; -pub use io::{shell, stdin}; +pub use io::{shell, stdin, Shell}; diff --git a/crates/cli/src/opts/mod.rs b/crates/cli/src/opts/mod.rs index 76480f400ed84..c00943bd57e8d 100644 --- a/crates/cli/src/opts/mod.rs +++ b/crates/cli/src/opts/mod.rs @@ -2,6 +2,7 @@ mod build; mod chain; mod dependency; mod ethereum; +mod shell; mod transaction; mod wallet; @@ -9,5 +10,6 @@ pub use build::*; pub use chain::*; pub use dependency::*; pub use ethereum::*; +pub use shell::*; pub use transaction::*; pub use wallet::*; diff --git a/crates/cli/src/opts/shell.rs b/crates/cli/src/opts/shell.rs new file mode 100644 index 0000000000000..2cb60f16cb786 --- /dev/null +++ b/crates/cli/src/opts/shell.rs @@ -0,0 +1,34 @@ +use crate::shell::{ColorChoice, Shell, Verbosity}; +use clap::Parser; + +/// Shell options. +#[derive(Clone, Copy, Debug, Parser)] +pub struct ShellOptions { + /// Use verbose output. + #[clap(long, short, global = true, conflicts_with = "quiet")] + pub verbose: bool, + + /// Do not print log messages. + #[clap(long, short, global = true, conflicts_with = "verbose")] + pub quiet: bool, + + /// Log messages coloring. + #[clap(long, global = true)] + pub color: ColorChoice, +} + +impl ShellOptions { + pub fn shell(self) -> Shell { + let verbosity = match (self.verbose, self.quiet) { + (true, false) => Verbosity::Verbose, + (false, true) => Verbosity::Quiet, + (false, false) => Verbosity::Normal, + (true, true) => unreachable!(), + }; + Shell::new_with(self.color, verbosity) + } + + pub fn set_global_shell(self) { + self.shell().set(); + } +} diff --git a/crates/cli/src/utils/mod.rs b/crates/cli/src/utils/mod.rs index 5215b88562b77..99231de7cc91a 100644 --- a/crates/cli/src/utils/mod.rs +++ b/crates/cli/src/utils/mod.rs @@ -30,6 +30,8 @@ pub use suggestions::*; #[doc(hidden)] pub use foundry_config::utils::*; +use crate::Shell; + /// Deterministic fuzzer seed used for gas snapshots and coverage reports. /// /// The keccak256 hash of "foundry rulez" @@ -179,6 +181,13 @@ macro_rules! p_println { }} } +pub fn exit_on_err(run: impl FnOnce() -> Result<()>) { + if let Err(err) = run() { + let _ = Shell::get().error(&err); + std::process::exit(1); + } +} + /// Loads a dotenv file, from the cwd and the project root, ignoring potential failure. /// /// We could use `tracing::warn!` here, but that would imply that the dotenv file can't configure From 39cc1084e52146cddb289b33ba4a5e212ac1ea7a Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Fri, 25 Aug 2023 17:58:50 +0200 Subject: [PATCH 03/14] add global shell options, use macros --- crates/cast/bin/main.rs | 10 +- crates/cast/bin/opts.rs | 21 +- crates/cli/src/io/shell.rs | 22 +- crates/cli/src/opts/build/core.rs | 5 - crates/cli/src/opts/shell.rs | 10 +- crates/cli/src/utils/mod.rs | 50 +--- crates/common/src/compile.rs | 2 +- crates/common/src/lib.rs | 1 - crates/common/src/shell.rs | 314 ----------------------- crates/forge/bin/cmd/build.rs | 4 +- crates/forge/bin/cmd/coverage.rs | 26 +- crates/forge/bin/cmd/flatten.rs | 1 - crates/forge/bin/cmd/fourbyte.rs | 4 +- crates/forge/bin/cmd/init.rs | 16 +- crates/forge/bin/cmd/install.rs | 42 ++- crates/forge/bin/cmd/script/broadcast.rs | 37 ++- crates/forge/bin/cmd/script/executor.rs | 6 +- crates/forge/bin/cmd/script/mod.rs | 67 +++-- crates/forge/bin/cmd/script/sequence.rs | 6 +- crates/forge/bin/cmd/test/mod.rs | 9 +- crates/forge/bin/main.rs | 22 +- crates/forge/bin/opts.rs | 20 +- 22 files changed, 177 insertions(+), 518 deletions(-) delete mode 100644 crates/common/src/shell.rs diff --git a/crates/cast/bin/main.rs b/crates/cast/bin/main.rs index a431b9213b9f7..2ad6f02ca02b4 100644 --- a/crates/cast/bin/main.rs +++ b/crates/cast/bin/main.rs @@ -26,13 +26,21 @@ pub mod opts; use opts::{Opts, Subcommands, ToBaseArgs}; #[tokio::main] -async fn main() -> Result<()> { +async fn main() { + if let Err(err) = run().await { + let _ = foundry_cli::Shell::get().error(&err); + std::process::exit(1); + } +} + +async fn run() -> Result<()> { utils::load_dotenv(); handler::install()?; utils::subscriber(); utils::enable_paint(); let opts = Opts::parse(); + opts.shell.set_global_shell(); match opts.sub { // Constants Subcommands::MaxInt { r#type } => { diff --git a/crates/cast/bin/opts.rs b/crates/cast/bin/opts.rs index c2d1fede147a6..49063b4923168 100644 --- a/crates/cast/bin/opts.rs +++ b/crates/cast/bin/opts.rs @@ -10,7 +10,7 @@ use ethers::{ }; use eyre::Result; use foundry_cli::{ - opts::{EtherscanOpts, RpcOpts}, + opts::{EtherscanOpts, RpcOpts, ShellOptions}, utils::parse_u256, }; use std::{path::PathBuf, str::FromStr}; @@ -24,19 +24,22 @@ const VERSION_MESSAGE: &str = concat!( ")" ); -#[derive(Debug, Parser)] -#[clap(name = "cast", version = VERSION_MESSAGE)] +/// Perform Ethereum RPC calls from the comfort of your command line. +#[derive(Parser)] +#[clap( + name = "cast", + version = VERSION_MESSAGE, + after_help = "Find more information in the book: http://book.getfoundry.sh/reference/cast/cast.html", + next_display_order = None, +)] pub struct Opts { #[clap(subcommand)] pub sub: Subcommands, + #[clap(flatten)] + pub shell: ShellOptions, } -/// Perform Ethereum RPC calls from the comfort of your command line. -#[derive(Debug, Subcommand)] -#[clap( - after_help = "Find more information in the book: http://book.getfoundry.sh/reference/cast/cast.html", - next_display_order = None -)] +#[derive(Subcommand)] pub enum Subcommands { /// Prints the maximum value of the given integer type. #[clap(visible_aliases = &["--max-int", "maxi"])] diff --git a/crates/cli/src/io/shell.rs b/crates/cli/src/io/shell.rs index 0b49fd1803392..171f0dc4842db 100644 --- a/crates/cli/src/io/shell.rs +++ b/crates/cli/src/io/shell.rs @@ -58,6 +58,26 @@ pub enum Verbosity { Quiet, } +impl Verbosity { + /// Returns true if the verbosity level is `Verbose`. + #[inline] + pub fn is_verbose(self) -> bool { + self == Verbosity::Verbose + } + + /// Returns true if the verbosity level is `Normal`. + #[inline] + pub fn is_normal(self) -> bool { + self == Verbosity::Normal + } + + /// Returns true if the verbosity level is `Quiet`. + #[inline] + pub fn is_quiet(self) -> bool { + self == Verbosity::Quiet + } +} + /// An abstraction around console output that remembers preferences for output /// verbosity and color. pub struct Shell { @@ -102,7 +122,7 @@ pub enum ColorChoice { Always, /// Force disable color output. Never, - /// Intelligently guess whether to use color output. + /// Intelligently guess whether to use color output (default). #[default] Auto, } diff --git a/crates/cli/src/opts/build/core.rs b/crates/cli/src/opts/build/core.rs index a9fbffabb16b3..0156b1a6abae1 100644 --- a/crates/cli/src/opts/build/core.rs +++ b/crates/cli/src/opts/build/core.rs @@ -84,11 +84,6 @@ pub struct CoreBuildArgs { #[serde(skip)] pub revert_strings: Option, - /// Don't print anything on startup. - #[clap(long, help_heading = "Compiler options")] - #[serde(skip)] - pub silent: bool, - /// Generate build info files. #[clap(long, help_heading = "Project options")] #[serde(skip)] diff --git a/crates/cli/src/opts/shell.rs b/crates/cli/src/opts/shell.rs index 2cb60f16cb786..b60a955e406b0 100644 --- a/crates/cli/src/opts/shell.rs +++ b/crates/cli/src/opts/shell.rs @@ -1,7 +1,7 @@ use crate::shell::{ColorChoice, Shell, Verbosity}; use clap::Parser; -/// Shell options. +/// Global shell options. #[derive(Clone, Copy, Debug, Parser)] pub struct ShellOptions { /// Use verbose output. @@ -9,12 +9,12 @@ pub struct ShellOptions { pub verbose: bool, /// Do not print log messages. - #[clap(long, short, global = true, conflicts_with = "verbose")] + #[clap(long, short, global = true, alias = "silent", conflicts_with = "verbose")] pub quiet: bool, /// Log messages coloring. - #[clap(long, global = true)] - pub color: ColorChoice, + #[clap(long, global = true, value_enum)] + pub color: Option, } impl ShellOptions { @@ -25,7 +25,7 @@ impl ShellOptions { (false, false) => Verbosity::Normal, (true, true) => unreachable!(), }; - Shell::new_with(self.color, verbosity) + Shell::new_with(self.color.unwrap_or_default(), verbosity) } pub fn set_global_shell(self) { diff --git a/crates/cli/src/utils/mod.rs b/crates/cli/src/utils/mod.rs index 99231de7cc91a..c26665d23b2e4 100644 --- a/crates/cli/src/utils/mod.rs +++ b/crates/cli/src/utils/mod.rs @@ -30,8 +30,6 @@ pub use suggestions::*; #[doc(hidden)] pub use foundry_config::utils::*; -use crate::Shell; - /// Deterministic fuzzer seed used for gas snapshots and coverage reports. /// /// The keccak256 hash of "foundry rulez" @@ -164,30 +162,6 @@ pub fn block_on(future: F) -> F::Output { rt.block_on(future) } -/// Conditionally print a message -/// -/// This macro accepts a predicate and the message to print if the predicate is tru -/// -/// ```ignore -/// let quiet = true; -/// p_println!(!quiet => "message"); -/// ``` -#[macro_export] -macro_rules! p_println { - ($p:expr => $($arg:tt)*) => {{ - if $p { - println!($($arg)*) - } - }} -} - -pub fn exit_on_err(run: impl FnOnce() -> Result<()>) { - if let Err(err) = run() { - let _ = Shell::get().error(&err); - std::process::exit(1); - } -} - /// Loads a dotenv file, from the cwd and the project root, ignoring potential failure. /// /// We could use `tracing::warn!` here, but that would imply that the dotenv file can't configure @@ -228,7 +202,7 @@ pub fn enable_paint() { pub fn print_receipt(chain: Chain, receipt: &TransactionReceipt) { let gas_used = receipt.gas_used.unwrap_or_default(); let gas_price = receipt.effective_gas_price.unwrap_or_default(); - foundry_common::shell::println(format!( + crate::sh_println!( "\n##### {chain}\n{status}Hash: {tx_hash:?}{caddr}\nBlock: {bn}\n{gas}\n", status = if receipt.status.map_or(true, |s| s.is_zero()) { "❌ [Failed]" @@ -253,7 +227,7 @@ pub fn print_receipt(chain: Chain, receipt: &TransactionReceipt) { gas_price.trim_end_matches('0').trim_end_matches('.') ) }, - )) + ) .expect("could not print receipt"); } @@ -318,14 +292,13 @@ impl CommandUtils for Command { #[derive(Clone, Copy, Debug)] pub struct Git<'a> { pub root: &'a Path, - pub quiet: bool, pub shallow: bool, } impl<'a> Git<'a> { #[inline] pub fn new(root: &'a Path) -> Self { - Self { root, quiet: false, shallow: false } + Self { root, shallow: false } } #[inline] @@ -362,11 +335,6 @@ impl<'a> Git<'a> { Git { root, ..self } } - #[inline] - pub fn quiet(self, quiet: bool) -> Self { - Self { quiet, ..self } - } - /// True to perform shallow clones #[inline] pub fn shallow(self, shallow: bool) -> Self { @@ -488,7 +456,7 @@ https://github.com/foundry-rs/foundry/issues/new/choose" path: impl AsRef, ) -> Result<()> { self.cmd() - .stderr(self.stderr()) + .stderr(Self::stderr()) .args(["submodule", "add"]) .args(self.shallow.then_some("--depth=1")) .args(force.then_some("--force")) @@ -504,7 +472,7 @@ https://github.com/foundry-rs/foundry/issues/new/choose" S: AsRef, { self.cmd() - .stderr(self.stderr()) + .stderr(Self::stderr()) .args(["submodule", "update", "--progress", "--init", "--recursive"]) .args(self.shallow.then_some("--depth=1")) .args(force.then_some("--force")) @@ -527,11 +495,11 @@ https://github.com/foundry-rs/foundry/issues/new/choose" } // don't set this in cmd() because it's not wanted for all commands - fn stderr(self) -> Stdio { - if self.quiet { - Stdio::piped() - } else { + fn stderr() -> Stdio { + if crate::Shell::get().verbosity().is_verbose() { Stdio::inherit() + } else { + Stdio::piped() } } } diff --git a/crates/common/src/compile.rs b/crates/common/src/compile.rs index 37d765ae2b747..6f02c367093e2 100644 --- a/crates/common/src/compile.rs +++ b/crates/common/src/compile.rs @@ -384,7 +384,7 @@ pub fn compile_target_with_filter( let graph = Graph::resolve(&project.paths)?; // Checking if it's a standalone script, or part of a project. - if graph.files().get(target_path).is_none() { + if !graph.files().contains_key(target_path) { if verify { eyre::bail!("You can only verify deployments from inside a project! Make sure it exists with `forge tree`."); } diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index cb045a25bb279..5cba49c267a6a 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -15,7 +15,6 @@ pub mod fs; pub mod glob; pub mod provider; pub mod selectors; -pub mod shell; pub mod term; pub mod traits; pub mod transactions; diff --git a/crates/common/src/shell.rs b/crates/common/src/shell.rs deleted file mode 100644 index 9b359fbc49350..0000000000000 --- a/crates/common/src/shell.rs +++ /dev/null @@ -1,314 +0,0 @@ -//! Helpers for printing to output - -use once_cell::sync::OnceCell; -use serde::Serialize; -use std::{ - error::Error, - fmt, io, - io::Write, - sync::{Arc, Mutex}, -}; - -/// Stores the configured shell for the duration of the program -static SHELL: OnceCell = OnceCell::new(); - -/// Error indicating that `set_hook` was unable to install the provided ErrorHook -#[derive(Debug, Clone, Copy)] -pub struct InstallError; - -impl fmt::Display for InstallError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("cannot install provided Shell, a shell has already been installed") - } -} - -impl Error for InstallError {} - -/// Install the provided shell -pub fn set_shell(shell: Shell) -> Result<(), InstallError> { - SHELL.set(shell).map_err(|_| InstallError) -} - -/// Runs the given closure with the current shell, or default shell if none was set -pub fn with_shell(f: F) -> R -where - F: FnOnce(&Shell) -> R, -{ - if let Some(shell) = SHELL.get() { - f(shell) - } else { - let shell = Shell::default(); - f(&shell) - } -} - -/// Prints the given message to the shell -pub fn println(msg: impl fmt::Display) -> io::Result<()> { - with_shell(|shell| if !shell.verbosity.is_silent() { shell.write_stdout(msg) } else { Ok(()) }) -} -/// Prints the given message to the shell -pub fn print_json(obj: &T) -> serde_json::Result<()> { - with_shell(|shell| shell.print_json(obj)) -} - -/// Prints the given message to the shell -pub fn eprintln(msg: impl fmt::Display) -> io::Result<()> { - with_shell(|shell| if !shell.verbosity.is_silent() { shell.write_stderr(msg) } else { Ok(()) }) -} - -/// Returns the configured verbosity -pub fn verbosity() -> Verbosity { - with_shell(|shell| shell.verbosity) -} - -/// An abstraction around console output that also considers verbosity -#[derive(Default)] -pub struct Shell { - /// Wrapper around stdout/stderr. - output: ShellOut, - /// How to emit messages. - verbosity: Verbosity, -} - -// === impl Shell === - -impl Shell { - /// Creates a new shell instance - pub fn new(output: ShellOut, verbosity: Verbosity) -> Self { - Self { output, verbosity } - } - - /// Returns a new shell that conforms to the specified verbosity arguments, where `json` takes - /// higher precedence - pub fn from_args(silent: bool, json: bool) -> Self { - match (silent, json) { - (_, true) => Self::json(), - (true, _) => Self::silent(), - _ => Default::default(), - } - } - - /// Returns a new shell that won't emit anything - pub fn silent() -> Self { - Self::from_verbosity(Verbosity::Silent) - } - - /// Returns a new shell that'll only emit json - pub fn json() -> Self { - Self::from_verbosity(Verbosity::Json) - } - - /// Creates a new shell instance with default output and the given verbosity - pub fn from_verbosity(verbosity: Verbosity) -> Self { - Self::new(Default::default(), verbosity) - } - - /// Write a fragment to stdout - /// - /// Caller is responsible for deciding whether [`Shell::verbosity`] is affects output. - pub fn write_stdout(&self, fragment: impl fmt::Display) -> io::Result<()> { - self.output.write_stdout(fragment) - } - - /// Write a fragment to stderr - /// - /// Caller is responsible for deciding whether [`Shell::verbosity`] is affects output. - pub fn write_stderr(&self, fragment: impl fmt::Display) -> io::Result<()> { - self.output.write_stderr(fragment) - } - - /// Prints the object to stdout as json - pub fn print_json(&self, obj: &T) -> serde_json::Result<()> { - if self.verbosity.is_json() { - let json = serde_json::to_string(&obj)?; - let _ = self.output.with_stdout(|out| writeln!(out, "{json}")); - } - Ok(()) - } - /// Prints the object to stdout as pretty json - pub fn pretty_print_json(&self, obj: &T) -> serde_json::Result<()> { - if self.verbosity.is_json() { - let json = serde_json::to_string_pretty(&obj)?; - let _ = self.output.with_stdout(|out| writeln!(out, "{json}")); - } - Ok(()) - } -} - -impl fmt::Debug for Shell { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self.output { - ShellOut::Write(_) => { - f.debug_struct("Shell").field("verbosity", &self.verbosity).finish() - } - ShellOut::Stream => { - f.debug_struct("Shell").field("verbosity", &self.verbosity).finish() - } - } - } -} - -/// Helper trait for custom shell output -/// -/// Can be used for debugging -pub trait ShellWrite { - /// Write the fragment - fn write(&self, fragment: impl fmt::Display) -> io::Result<()>; - - /// Executes a closure on the current stdout - fn with_stdout(&self, f: F) -> R - where - for<'r> F: FnOnce(&'r mut (dyn Write + 'r)) -> R; - - /// Executes a closure on the current stderr - fn with_err(&self, f: F) -> R - where - for<'r> F: FnOnce(&'r mut (dyn Write + 'r)) -> R; -} - -/// A guarded shell output type -pub struct WriteShellOut(Arc>>); - -unsafe impl Send for WriteShellOut {} -unsafe impl Sync for WriteShellOut {} - -impl ShellWrite for WriteShellOut { - fn write(&self, fragment: impl fmt::Display) -> io::Result<()> { - if let Ok(mut lock) = self.0.lock() { - writeln!(lock, "{fragment}")?; - } - Ok(()) - } - /// Executes a closure on the current stdout - fn with_stdout(&self, f: F) -> R - where - for<'r> F: FnOnce(&'r mut (dyn Write + 'r)) -> R, - { - let mut lock = self.0.lock().unwrap(); - f(&mut *lock) - } - - /// Executes a closure on the current stderr - fn with_err(&self, f: F) -> R - where - for<'r> F: FnOnce(&'r mut (dyn Write + 'r)) -> R, - { - let mut lock = self.0.lock().unwrap(); - f(&mut *lock) - } -} - -/// A `Write`able object, either with or without color support -#[derive(Default)] -pub enum ShellOut { - /// A plain write object - /// - /// Can be used for debug purposes - Write(WriteShellOut), - /// Streams to `stdio` - #[default] - Stream, -} - -// === impl ShellOut === - -impl ShellOut { - /// Creates a new shell that writes to memory - pub fn memory() -> Self { - #[allow(clippy::box_default)] - #[allow(clippy::arc_with_non_send_sync)] - ShellOut::Write(WriteShellOut(Arc::new(Mutex::new(Box::new(Vec::new()))))) - } - - /// Write a fragment to stdout - fn write_stdout(&self, fragment: impl fmt::Display) -> io::Result<()> { - match *self { - ShellOut::Stream => { - let stdout = io::stdout(); - let mut handle = stdout.lock(); - writeln!(handle, "{fragment}")?; - } - ShellOut::Write(ref w) => { - w.write(fragment)?; - } - } - Ok(()) - } - - /// Write output to stderr - fn write_stderr(&self, fragment: impl fmt::Display) -> io::Result<()> { - match *self { - ShellOut::Stream => { - let stderr = io::stderr(); - let mut handle = stderr.lock(); - writeln!(handle, "{fragment}")?; - } - ShellOut::Write(ref w) => { - w.write(fragment)?; - } - } - Ok(()) - } - - /// Executes a closure on the current stdout - fn with_stdout(&self, f: F) -> R - where - for<'r> F: FnOnce(&'r mut (dyn Write + 'r)) -> R, - { - match *self { - ShellOut::Stream => { - let stdout = io::stdout(); - let mut handler = stdout.lock(); - f(&mut handler) - } - ShellOut::Write(ref w) => w.with_stdout(f), - } - } - - /// Executes a closure on the current stderr - #[allow(unused)] - fn with_err(&self, f: F) -> R - where - for<'r> F: FnOnce(&'r mut (dyn Write + 'r)) -> R, - { - match *self { - ShellOut::Stream => { - let stderr = io::stderr(); - let mut handler = stderr.lock(); - f(&mut handler) - } - ShellOut::Write(ref w) => w.with_err(f), - } - } -} - -/// The requested verbosity of output. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub enum Verbosity { - /// only allow json output - Json, - /// print as is - #[default] - Normal, - /// print nothing - Silent, -} - -// === impl Verbosity === - -impl Verbosity { - /// Returns true if json mode - pub fn is_json(&self) -> bool { - matches!(self, Verbosity::Json) - } - - /// Returns true if silent - pub fn is_silent(&self) -> bool { - matches!(self, Verbosity::Silent) - } - - /// Returns true if normal verbosity - pub fn is_normal(&self) -> bool { - matches!(self, Verbosity::Normal) - } -} diff --git a/crates/forge/bin/cmd/build.rs b/crates/forge/bin/cmd/build.rs index 7c4e8fce2ca33..01ce380f71b9c 100644 --- a/crates/forge/bin/cmd/build.rs +++ b/crates/forge/bin/cmd/build.rs @@ -76,9 +76,7 @@ impl BuildArgs { let mut config = self.try_load_config_emit_warnings()?; let mut project = config.project()?; - if install::install_missing_dependencies(&mut config, self.args.silent) && - config.auto_detect_remappings - { + if install::install_missing_dependencies(&mut config) && config.auto_detect_remappings { // need to re-configure here to also catch additional remappings config = self.load_config(); project = config.project()?; diff --git a/crates/forge/bin/cmd/coverage.rs b/crates/forge/bin/cmd/coverage.rs index ca0580f92331e..3f512241915c0 100644 --- a/crates/forge/bin/cmd/coverage.rs +++ b/crates/forge/bin/cmd/coverage.rs @@ -22,7 +22,6 @@ use forge::{ }; use foundry_cli::{ opts::CoreBuildArgs, - p_println, utils::{LoadConfig, STATIC_FUZZ_SEED}, }; use foundry_common::{compile::ProjectCompiler, evm::EvmArgs, fs}; @@ -31,7 +30,6 @@ use foundry_evm::utils::evm_spec; use semver::Version; use std::{collections::HashMap, sync::mpsc::channel}; use tracing::trace; -use yansi::Paint; /// A map, keyed by contract ID, to a tuple of the deployment source map and the runtime source map. type SourceMaps = HashMap; @@ -70,9 +68,7 @@ impl CoverageArgs { let (mut config, evm_opts) = self.load_config_and_evm_opts_emit_warnings()?; // install missing dependencies - if install::install_missing_dependencies(&mut config, self.build_args().silent) && - config.auto_detect_remappings - { + if install::install_missing_dependencies(&mut config) && config.auto_detect_remappings { // need to re-configure here to also catch additional remappings config = self.load_config(); } @@ -81,10 +77,10 @@ impl CoverageArgs { config.fuzz.seed = Some(U256::from_big_endian(&STATIC_FUZZ_SEED)); let (project, output) = self.build(&config)?; - p_println!(!self.opts.silent => "Analysing contracts..."); + sh_eprintln!("Analysing contracts...")?; let report = self.prepare(&config, output.clone())?; - p_println!(!self.opts.silent => "Running tests..."); + sh_eprintln!("Running tests...")?; self.collect(project, output, report, config, evm_opts).await } @@ -107,15 +103,13 @@ impl CoverageArgs { } // print warning message - p_println!(!self.opts.silent => "{}", - Paint::yellow( - concat!( - "Warning! \"--ir-minimum\" flag enables viaIR with minimum optimization, which can result in inaccurate source mappings.\n", - "Only use this flag as a workaround if you are experiencing \"stack too deep\" errors.\n", - "Note that \"viaIR\" is only available in Solidity 0.8.13 and above.\n", - "See more:\n", - "https://github.com/foundry-rs/foundry/issues/3357\n" - ))); + sh_warn!(concat!( + "Warning! \"--ir-minimum\" flag enables viaIR with minimum optimization, which can result in inaccurate source mappings.\n", + "Only use this flag as a workaround if you are experiencing \"stack too deep\" errors.\n", + "Note that \"viaIR\" is only available in Solidity 0.8.13 and above.\n", + "See more:\n", + "https://github.com/foundry-rs/foundry/issues/3357\n" + ))?; // Enable viaIR with minimum optimization // https://github.com/ethereum/solidity/issues/12533#issuecomment-1013073350 diff --git a/crates/forge/bin/cmd/flatten.rs b/crates/forge/bin/cmd/flatten.rs index 2f750877694ea..45386bdf2c7a5 100644 --- a/crates/forge/bin/cmd/flatten.rs +++ b/crates/forge/bin/cmd/flatten.rs @@ -47,7 +47,6 @@ impl FlattenArgs { libraries: vec![], via_ir: false, revert_strings: None, - silent: false, build_info: false, build_info_path: None, }; diff --git a/crates/forge/bin/cmd/fourbyte.rs b/crates/forge/bin/cmd/fourbyte.rs index 86a889d6fe635..bf58b79a77425 100644 --- a/crates/forge/bin/cmd/fourbyte.rs +++ b/crates/forge/bin/cmd/fourbyte.rs @@ -8,9 +8,7 @@ use foundry_cli::{ use foundry_common::{ compile, selectors::{import_selectors, SelectorImportData}, - shell, }; -use yansi::Paint; /// CLI arguments for `forge upload-selectors`. #[derive(Debug, Clone, Parser)] @@ -30,7 +28,7 @@ pub struct UploadSelectorsArgs { impl UploadSelectorsArgs { /// Builds a contract and uploads the ABI to selector database pub async fn run(self) -> Result<()> { - shell::println(Paint::yellow("Warning! This command is deprecated and will be removed in v1, use `forge selectors upload` instead"))?; + sh_warn!("This command is deprecated and will be removed in v1, use `forge selectors upload` instead")?; let UploadSelectorsArgs { contract, all, project_paths } = self; diff --git a/crates/forge/bin/cmd/init.rs b/crates/forge/bin/cmd/init.rs index 2ae3c91844e0c..2f208029c2dd5 100644 --- a/crates/forge/bin/cmd/init.rs +++ b/crates/forge/bin/cmd/init.rs @@ -2,7 +2,7 @@ use super::install::DependencyInstallOpts; use clap::{Parser, ValueHint}; use ethers::solc::remappings::Remapping; use eyre::Result; -use foundry_cli::{p_println, utils::Git}; +use foundry_cli::utils::Git; use foundry_common::fs; use foundry_config::Config; use std::path::{Path, PathBuf}; @@ -39,14 +39,14 @@ pub struct InitArgs { impl InitArgs { pub fn run(self) -> Result<()> { let InitArgs { root, template, opts, offline, force, vscode } = self; - let DependencyInstallOpts { shallow, no_git, no_commit, quiet } = opts; + let DependencyInstallOpts { shallow, no_git, no_commit } = opts; // create the root dir if it does not exist if !root.exists() { fs::create_dir_all(&root)?; } let root = dunce::canonicalize(root)?; - let git = Git::new(&root).quiet(quiet).shallow(shallow); + let git = Git::new(&root).shallow(shallow); // if a template is provided, then this command clones the template repo, removes the .git // folder, and initializes a new git repo—-this ensures there is no history from the @@ -57,7 +57,7 @@ impl InitArgs { } else { "https://github.com/".to_string() + &template }; - p_println!(!quiet => "Initializing {} from {}...", root.display(), template); + sh_eprintln!("Initializing {} from {}...", root.display(), template)?; Git::clone(shallow, &template, Some(&root))?; @@ -79,7 +79,7 @@ impl InitArgs { ); } - p_println!(!quiet => "Target directory is not empty, but `--force` was specified"); + sh_eprintln!("Target directory is not empty, but `--force` was specified")?; } // ensure git status is clean before generating anything @@ -87,7 +87,7 @@ impl InitArgs { git.ensure_clean()?; } - p_println!(!quiet => "Initializing {}...", root.display()); + sh_eprintln!("Initializing {}...", root.display())?; // make the dirs let src = root.join("src"); @@ -128,7 +128,7 @@ impl InitArgs { // install forge-std if !offline { if root.join("lib/forge-std").exists() { - p_println!(!quiet => "\"lib/forge-std\" already exists, skipping install...."); + sh_eprintln!("\"lib/forge-std\" already exists, skipping install....")?; self.opts.install(&mut config, vec![])?; } else { let dep = "https://github.com/foundry-rs/forge-std".parse()?; @@ -142,7 +142,7 @@ impl InitArgs { } } - p_println!(!quiet => " {} forge project", Paint::green("Initialized")); + sh_eprintln!(" {} forge project", Paint::green("Initialized"))?; Ok(()) } } diff --git a/crates/forge/bin/cmd/install.rs b/crates/forge/bin/cmd/install.rs index 5dcf3960adf17..28848188da2ac 100644 --- a/crates/forge/bin/cmd/install.rs +++ b/crates/forge/bin/cmd/install.rs @@ -2,7 +2,7 @@ use clap::{Parser, ValueHint}; use eyre::{Context, Result}; use foundry_cli::{ opts::Dependency, - p_println, prompt, + prompt, utils::{CommandUtils, Git, LoadConfig}, }; use foundry_common::fs; @@ -78,15 +78,11 @@ pub struct DependencyInstallOpts { /// Do not create a commit. #[clap(long)] pub no_commit: bool, - - /// Do not print any messages. - #[clap(short, long)] - pub quiet: bool, } impl DependencyInstallOpts { pub fn git(self, config: &Config) -> Git<'_> { - Git::from_config(config).quiet(self.quiet).shallow(self.shallow) + Git::from_config(config).shallow(self.shallow) } /// Installs all missing dependencies. @@ -95,19 +91,14 @@ impl DependencyInstallOpts { /// /// Returns true if any dependency was installed. pub fn install_missing_dependencies(mut self, config: &mut Config) -> bool { - let DependencyInstallOpts { quiet, .. } = self; let lib = config.install_lib_dir(); if self.git(config).has_missing_dependencies(Some(lib)).unwrap_or(false) { // The extra newline is needed, otherwise the compiler output will overwrite the message - p_println!(!quiet => "Missing dependencies found. Installing now...\n"); + let _ = sh_eprintln!("Missing dependencies found. Installing now...\n"); self.no_commit = true; - if self.install(config, Vec::new()).is_err() && !quiet { - eprintln!( - "{}", - Paint::yellow( - "Your project has missing dependencies that could not be installed." - ) - ) + if self.install(config, Vec::new()).is_err() { + let _ = + sh_warn!("Your project has missing dependencies that could not be installed."); } true } else { @@ -117,7 +108,7 @@ impl DependencyInstallOpts { /// Installs all dependencies pub fn install(self, config: &mut Config, dependencies: Vec) -> Result<()> { - let DependencyInstallOpts { no_git, no_commit, quiet, .. } = self; + let DependencyInstallOpts { no_git, no_commit, .. } = self; let git = self.git(config); @@ -125,7 +116,7 @@ impl DependencyInstallOpts { let libs = git.root.join(install_lib_dir); if dependencies.is_empty() && !self.no_git { - p_println!(!self.quiet => "Updating dependencies in {}", libs.display()); + sh_eprintln!("Updating dependencies in {}", libs.display())?; git.submodule_update(false, false, Some(&libs))?; } fs::create_dir_all(&libs)?; @@ -136,7 +127,13 @@ impl DependencyInstallOpts { let rel_path = path .strip_prefix(git.root) .wrap_err("Library directory is not relative to the repository root")?; - p_println!(!quiet => "Installing {} in {} (url: {:?}, tag: {:?})", dep.name, path.display(), dep.url, dep.tag); + sh_eprintln!( + "Installing {} in {} (url: {:?}, tag: {:?})", + dep.name, + path.display(), + dep.url, + dep.tag + )?; // this tracks the actual installed tag let installed_tag; @@ -178,13 +175,14 @@ impl DependencyInstallOpts { } } - if !quiet { + // TODO: make this a shell note + if !foundry_cli::Shell::get().verbosity().is_quiet() { let mut msg = format!(" {} {}", Paint::green("Installed"), dep.name); if let Some(tag) = dep.tag.or(installed_tag) { msg.push(' '); msg.push_str(tag.as_str()); } - println!("{msg}"); + sh_eprintln!("{msg}")?; } } @@ -197,8 +195,8 @@ impl DependencyInstallOpts { } } -pub fn install_missing_dependencies(config: &mut Config, quiet: bool) -> bool { - DependencyInstallOpts { quiet, ..Default::default() }.install_missing_dependencies(config) +pub fn install_missing_dependencies(config: &mut Config) -> bool { + DependencyInstallOpts::default().install_missing_dependencies(config) } #[derive(Clone, Copy, Debug)] diff --git a/crates/forge/bin/cmd/script/broadcast.rs b/crates/forge/bin/cmd/script/broadcast.rs index 5414c0d9284a5..6402d4955007e 100644 --- a/crates/forge/bin/cmd/script/broadcast.rs +++ b/crates/forge/bin/cmd/script/broadcast.rs @@ -14,7 +14,7 @@ use foundry_cli::{ update_progress, utils::{has_batch_support, has_different_gas_calc}, }; -use foundry_common::{estimate_eip1559_fees, shell, try_get_http_provider, RetryProvider}; +use foundry_common::{estimate_eip1559_fees, try_get_http_provider, RetryProvider}; use futures::StreamExt; use std::{cmp::min, collections::HashSet, ops::Mul, sync::Arc}; use tracing::trace; @@ -136,11 +136,11 @@ impl ScriptArgs { { let mut pending_transactions = vec![]; - shell::println(format!( + sh_eprintln!( "##\nSending transactions [{} - {}].", batch_number * batch_size, batch_number * batch_size + min(batch_size, batch.len()) - 1 - ))?; + )?; for (tx, kind, is_fixed_gas_limit) in batch.into_iter() { let tx_hash = self.send_transaction( provider.clone(), @@ -180,7 +180,7 @@ impl ScriptArgs { deployment_sequence.save()?; if !sequential_broadcast { - shell::println("##\nWaiting for receipts.")?; + sh_eprintln!("##\nWaiting for receipts.")?; clear_pendings(provider.clone(), deployment_sequence, None).await?; } } @@ -190,8 +190,8 @@ impl ScriptArgs { } } - shell::println("\n\n==========================")?; - shell::println("\nONCHAIN EXECUTION COMPLETE & SUCCESSFUL.")?; + sh_eprintln!("\n\n==========================")?; + sh_eprintln!("\nONCHAIN EXECUTION COMPLETE & SUCCESSFUL.")?; let (total_gas, total_gas_price, total_paid) = deployment_sequence.receipts.iter().fold( (U256::zero(), U256::zero(), U256::zero()), @@ -204,12 +204,12 @@ impl ScriptArgs { let paid = format_units(total_paid, 18).unwrap_or_else(|_| "N/A".to_string()); let avg_gas_price = format_units(total_gas_price / deployment_sequence.receipts.len(), 9) .unwrap_or_else(|_| "N/A".to_string()); - shell::println(format!( + sh_eprintln!( "Total Paid: {} ETH ({} gas * avg {} gwei)", paid.trim_end_matches('0'), total_gas, avg_gas_price.trim_end_matches('0').trim_end_matches('.') - ))?; + )?; Ok(()) } @@ -320,10 +320,10 @@ impl ScriptArgs { } if !self.broadcast { - shell::println("\nSIMULATION COMPLETE. To broadcast these transactions, add --broadcast and wallet configuration(s) to the previous command. See forge script --help for more.")?; + sh_eprintln!("\nSIMULATION COMPLETE. To broadcast these transactions, add --broadcast and wallet configuration(s) to the previous command. See forge script --help for more.")?; } } else { - shell::println("\nIf you wish to simulate on-chain transactions pass a RPC URL.")?; + sh_eprintln!("\nIf you wish to simulate on-chain transactions pass a RPC URL.")?; } } Ok(()) @@ -398,7 +398,7 @@ impl ScriptArgs { known_contracts: &ContractsByArtifact, ) -> Result> { let gas_filled_txs = if self.skip_simulation { - shell::println("\nSKIPPING ON CHAIN SIMULATION.")?; + sh_eprintln!("\nSKIPPING ON CHAIN SIMULATION.")?; txs.into_iter() .map(|btx| { let mut tx = TransactionWithMetadata::from_typed_transaction(btx.transaction); @@ -538,24 +538,23 @@ impl ScriptArgs { provider_info.gas_price()? }; - shell::println("\n==========================")?; - shell::println(format!("\nChain {}", provider_info.chain))?; + sh_eprintln!("\nChain {}", provider_info.chain)?; - shell::println(format!( + sh_eprintln!( "\nEstimated gas price: {} gwei", format_units(per_gas, 9) .unwrap_or_else(|_| "[Could not calculate]".to_string()) .trim_end_matches('0') .trim_end_matches('.') - ))?; - shell::println(format!("\nEstimated total gas used for script: {total_gas}"))?; - shell::println(format!( + )?; + sh_eprintln!("\nEstimated total gas used for script: {total_gas}")?; + sh_eprintln!( "\nEstimated amount required: {} ETH", format_units(total_gas.saturating_mul(per_gas), 18) .unwrap_or_else(|_| "[Could not calculate]".to_string()) .trim_end_matches('0') - ))?; - shell::println("\n==========================")?; + )?; + sh_eprintln!("\n==========================")?; } } Ok(deployments) diff --git a/crates/forge/bin/cmd/script/executor.rs b/crates/forge/bin/cmd/script/executor.rs index ac42e3c2a9ee1..745cdf8a20ed6 100644 --- a/crates/forge/bin/cmd/script/executor.rs +++ b/crates/forge/bin/cmd/script/executor.rs @@ -19,7 +19,7 @@ use forge::{ CallKind, }; use foundry_cli::utils::{ensure_clean_constructor, needs_setup}; -use foundry_common::{shell, RpcUrl}; +use foundry_common::RpcUrl; use foundry_evm::utils::evm_spec; use futures::future::join_all; use parking_lot::RwLock; @@ -248,9 +248,7 @@ impl ScriptArgs { async fn build_runners(&self, script_config: &ScriptConfig) -> HashMap { let sender = script_config.evm_opts.sender; - if !shell::verbosity().is_silent() { - eprintln!("\n## Setting up ({}) EVMs.", script_config.total_rpcs.len()); - } + let _ = sh_eprintln!("\n## Setting up ({}) EVMs.", script_config.total_rpcs.len()); let futs = script_config .total_rpcs diff --git a/crates/forge/bin/cmd/script/mod.rs b/crates/forge/bin/cmd/script/mod.rs index 4e10066332f3c..df9cab3a3c48a 100644 --- a/crates/forge/bin/cmd/script/mod.rs +++ b/crates/forge/bin/cmd/script/mod.rs @@ -37,7 +37,7 @@ use foundry_common::{ contracts::get_contract_name, errors::UnlinkedByteCode, evm::{Breakpoints, EvmArgs}, - shell, ContractsByArtifact, RpcUrl, CONTRACT_MAX_SIZE, SELECTOR_LEN, + ContractsByArtifact, RpcUrl, CONTRACT_MAX_SIZE, SELECTOR_LEN, }; use foundry_config::{ figment, @@ -288,7 +288,7 @@ impl ScriptArgs { } } Err(_) => { - shell::println(format!("{:x?}", (&returned)))?; + sh_eprintln!("{:x?}", (&returned))?; } } @@ -309,7 +309,7 @@ impl ScriptArgs { eyre::bail!("Unexpected error: No traces despite verbosity level. Please report this as a bug: https://github.com/foundry-rs/foundry/issues/new?assignees=&labels=T-bug&template=BUG-FORM.yml"); } - shell::println("Traces:")?; + sh_eprintln!("Traces:")?; for (kind, trace) in &mut result.traces { let should_include = match kind { TraceKind::Setup => verbosity >= 5, @@ -319,22 +319,22 @@ impl ScriptArgs { if should_include { decoder.decode(trace).await; - shell::println(format!("{trace}"))?; + sh_eprintln!("{trace}")?; } } - shell::println(String::new())?; + sh_eprintln!()?; } if result.success { - shell::println(format!("{}", Paint::green("Script ran successfully.")))?; + sh_eprintln!("{}", Paint::green("Script ran successfully."))?; } if script_config.evm_opts.fork_url.is_none() { - shell::println(format!("Gas used: {}", result.gas_used))?; + sh_eprintln!("Gas used: {}", result.gas_used)?; } if result.success && !result.returned.is_empty() { - shell::println("\n== Return ==")?; + sh_eprintln!("\n== Return ==")?; match func.decode_output(&result.returned) { Ok(decoded) => { for (index, (token, output)) in decoded.iter().zip(&func.outputs).enumerate() { @@ -345,24 +345,24 @@ impl ScriptArgs { } else { index.to_string() }; - shell::println(format!( + sh_eprintln!( "{}: {internal_type} {}", label.trim_end(), format_token(token) - ))?; + )?; } } Err(_) => { - shell::println(format!("{:x?}", (&result.returned)))?; + sh_eprintln!("{:x?}", (&result.returned))?; } } } let console_logs = decode_console_logs(&result.logs); if !console_logs.is_empty() { - shell::println("\n== Logs ==")?; + sh_eprintln!("\n== Logs ==")?; for log in console_logs { - shell::println(format!(" {log}"))?; + sh_eprintln!(" {log}")?; } } @@ -382,10 +382,7 @@ impl ScriptArgs { let console_logs = decode_console_logs(&result.logs); let output = JsonResult { logs: console_logs, gas_used: result.gas_used, returns }; - let j = serde_json::to_string(&output)?; - shell::println(j)?; - - Ok(()) + foundry_cli::Shell::get().print_json(&output) } /// It finds the deployer from the running script and uses it to predeploy libraries. @@ -410,7 +407,7 @@ impl ScriptArgs { let sender = tx.from.expect("no sender"); if let Some(ns) = new_sender { if sender != ns { - shell::println("You have more than one deployer who could predeploy libraries. Using `--sender` instead.")?; + sh_eprintln!("You have more than one deployer who could predeploy libraries. Using `--sender` instead.")?; return Ok(None) } } else if sender != evm_opts.sender { @@ -617,12 +614,12 @@ impl ScriptArgs { if deployment_size > max_size { prompt_user = self.broadcast; - shell::println(format!( + sh_eprintln!( "{}", Paint::red(format!( "`{name}` is above the contract size limit ({deployment_size} > {max_size})." )) - ))?; + )?; } } } @@ -719,12 +716,7 @@ impl ScriptConfig { /// error. [library support] fn check_multi_chain_constraints(&self, libraries: &Libraries) -> Result<()> { if self.has_multiple_rpcs() || (self.missing_rpc && !self.total_rpcs.is_empty()) { - shell::eprintln(format!( - "{}", - Paint::yellow( - "Multi chain deployment is still under development. Use with caution." - ) - ))?; + sh_warn!("Multi chain deployment is still under development. Use with caution.")?; if !libraries.libs.is_empty() { eyre::bail!( "Multi chain deployment does not support library linking at the moment." @@ -761,20 +753,19 @@ impl ScriptConfig { // At least one chain ID is unsupported, therefore we print the message. if chain_id_unsupported { - let msg = format!( - r#" + let ids = chain_ids + .iter() + .filter(|(supported, _)| !supported) + .map(|(_, chain)| format!("{}", *chain as u64)) + .collect::>() + .join(", "); + sh_warn!( + " EIP-3855 is not supported in one or more of the RPCs used. -Unsupported Chain IDs: {}. +Unsupported Chain IDs: {ids}. Contracts deployed with a Solidity version equal or higher than 0.8.20 might not work properly. -For more information, please see https://eips.ethereum.org/EIPS/eip-3855"#, - chain_ids - .iter() - .filter(|(supported, _)| !supported) - .map(|(_, chain)| format!("{}", *chain as u64)) - .collect::>() - .join(", ") - ); - shell::println(Paint::yellow(msg))?; +For more information, please see https://eips.ethereum.org/EIPS/eip-3855", + )?; } Ok(()) } diff --git a/crates/forge/bin/cmd/script/sequence.rs b/crates/forge/bin/cmd/script/sequence.rs index 0c9d0ad20bde3..531216c8b3591 100644 --- a/crates/forge/bin/cmd/script/sequence.rs +++ b/crates/forge/bin/cmd/script/sequence.rs @@ -14,7 +14,7 @@ use ethers::{ }; use eyre::{ContextCompat, Result, WrapErr}; use foundry_cli::utils::now; -use foundry_common::{fs, shell, SELECTOR_LEN}; +use foundry_common::{fs, SELECTOR_LEN}; use foundry_config::Config; use serde::{Deserialize, Serialize}; use std::{ @@ -174,8 +174,8 @@ impl ScriptSequence { //../run-[timestamp].json fs::copy(&self.sensitive_path, self.sensitive_path.with_file_name(&ts_name))?; - shell::println(format!("\nTransactions saved to: {}\n", self.path.display()))?; - shell::println(format!("Sensitive values saved to: {}\n", self.sensitive_path.display()))?; + sh_eprintln!("\nTransactions saved to: {}\n", self.path.display())?; + sh_eprintln!("Sensitive values saved to: {}\n", self.sensitive_path.display())?; Ok(()) } diff --git a/crates/forge/bin/cmd/test/mod.rs b/crates/forge/bin/cmd/test/mod.rs index d1686f166a3a0..7e755db937fcd 100644 --- a/crates/forge/bin/cmd/test/mod.rs +++ b/crates/forge/bin/cmd/test/mod.rs @@ -20,7 +20,7 @@ use foundry_cli::{ use foundry_common::{ compile::{self, ProjectCompiler}, evm::EvmArgs, - get_contract_name, get_file_name, shell, + get_contract_name, get_file_name, }; use foundry_config::{ figment, @@ -117,7 +117,6 @@ impl TestArgs { pub async fn run(self) -> Result { trace!(target: "forge::test", "executing test command"); - shell::set_shell(shell::Shell::from_args(self.opts.silent, self.json))?; self.execute_tests().await } @@ -139,9 +138,7 @@ impl TestArgs { let mut project = config.project()?; // install missing dependencies - if install::install_missing_dependencies(&mut config, self.build_args().silent) && - config.auto_detect_remappings - { + if install::install_missing_dependencies(&mut config) && config.auto_detect_remappings { // need to re-configure here to also catch additional remappings config = self.load_config(); project = config.project()?; @@ -377,7 +374,7 @@ impl TestOutcome { return Ok(()) } - if !shell::verbosity().is_normal() { + if foundry_cli::Shell::get().verbosity().is_quiet() { // skip printing and exit early std::process::exit(1); } diff --git a/crates/forge/bin/main.rs b/crates/forge/bin/main.rs index 633b7b8002ef0..2f9c3f77236f5 100644 --- a/crates/forge/bin/main.rs +++ b/crates/forge/bin/main.rs @@ -3,19 +3,30 @@ use clap_complete::generate; use eyre::Result; use foundry_cli::{handler, utils}; +#[macro_use] +extern crate foundry_cli; + mod cmd; mod opts; use cmd::{cache::CacheSubcommands, generate::GenerateSubcommands, watch}; use opts::{Opts, Subcommands}; -fn main() -> Result<()> { +fn main() { + if let Err(err) = run() { + let _ = foundry_cli::Shell::get().error(&err); + std::process::exit(1); + } +} + +fn run() -> Result<()> { utils::load_dotenv(); handler::install()?; utils::subscriber(); utils::enable_paint(); let opts = Opts::parse(); + opts.shell.set_global_shell(); match opts.sub { Subcommands::Test(cmd) => { if cmd.is_watch() { @@ -25,14 +36,7 @@ fn main() -> Result<()> { outcome.ensure_ok() } } - Subcommands::Script(cmd) => { - // install the shell before executing the command - foundry_common::shell::set_shell(foundry_common::shell::Shell::from_args( - cmd.opts.args.silent, - cmd.json, - ))?; - utils::block_on(cmd.run_script(Default::default())) - } + Subcommands::Script(cmd) => utils::block_on(cmd.run_script(Default::default())), Subcommands::Coverage(cmd) => utils::block_on(cmd.run()), Subcommands::Bind(cmd) => cmd.run(), Subcommands::Build(cmd) => { diff --git a/crates/forge/bin/opts.rs b/crates/forge/bin/opts.rs index 68c2683ca14e2..c492359b95ffd 100644 --- a/crates/forge/bin/opts.rs +++ b/crates/forge/bin/opts.rs @@ -21,6 +21,7 @@ use crate::cmd::{ verify::{VerifyArgs, VerifyCheckArgs}, }; use clap::{Parser, Subcommand, ValueHint}; +use foundry_cli::opts::ShellOptions; use std::path::PathBuf; const VERSION_MESSAGE: &str = concat!( @@ -32,19 +33,22 @@ const VERSION_MESSAGE: &str = concat!( ")" ); -#[derive(Debug, Parser)] -#[clap(name = "forge", version = VERSION_MESSAGE)] +/// Build, test, fuzz, debug and deploy Solidity contracts. +#[derive(Parser)] +#[clap( + name = "forge", + version = VERSION_MESSAGE, + after_help = "Find more information in the book: http://book.getfoundry.sh/reference/forge/forge.html", + next_display_order = None, +)] pub struct Opts { #[clap(subcommand)] pub sub: Subcommands, + #[clap(flatten)] + pub shell: ShellOptions, } -#[derive(Debug, Subcommand)] -#[clap( - about = "Build, test, fuzz, debug and deploy Solidity contracts.", - after_help = "Find more information in the book: http://book.getfoundry.sh/reference/forge/forge.html", - next_display_order = None -)] +#[derive(Subcommand)] #[allow(clippy::large_enum_variant)] pub enum Subcommands { /// Run the project's tests. From 6154e7a87fa7bd58371e54f72110938107941c93 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Fri, 25 Aug 2023 21:16:26 +0200 Subject: [PATCH 04/14] chore: rewrite common::compile as a builder --- crates/cast/bin/cmd/send.rs | 3 +- crates/cast/bin/cmd/storage.rs | 8 +- crates/cast/bin/main.rs | 3 + crates/cli/src/utils/cmd.rs | 19 +- crates/common/src/compile.rs | 336 +++++++++++++-------------- crates/common/src/term.rs | 15 -- crates/config/src/warning.rs | 45 ++-- crates/forge/bin/cmd/bind.rs | 4 +- crates/forge/bin/cmd/build.rs | 18 +- crates/forge/bin/cmd/config.rs | 4 +- crates/forge/bin/cmd/create.rs | 9 +- crates/forge/bin/cmd/fmt.rs | 9 +- crates/forge/bin/cmd/fourbyte.rs | 8 +- crates/forge/bin/cmd/inspect.rs | 14 +- crates/forge/bin/cmd/script/build.rs | 18 +- crates/forge/bin/cmd/selectors.rs | 72 +++--- crates/forge/bin/cmd/test/mod.rs | 22 +- 17 files changed, 277 insertions(+), 330 deletions(-) diff --git a/crates/cast/bin/cmd/send.rs b/crates/cast/bin/cmd/send.rs index cf9d95e56f114..c62da2feb1762 100644 --- a/crates/cast/bin/cmd/send.rs +++ b/crates/cast/bin/cmd/send.rs @@ -8,7 +8,6 @@ use foundry_cli::{ opts::{EthereumOpts, TransactionOpts}, utils, }; -use foundry_common::cli_warn; use foundry_config::{Chain, Config}; use std::str::FromStr; @@ -119,7 +118,7 @@ impl SendTxArgs { // switch chain if current chain id is not the same as the one specified in the // config if config_chain_id != current_chain_id { - cli_warn!("Switching to chain {}", config_chain); + sh_warn!("Switching to chain {config_chain}")?; provider .request( "wallet_switchEthereumChain", diff --git a/crates/cast/bin/cmd/storage.rs b/crates/cast/bin/cmd/storage.rs index 3afc441360c32..ff6d385f05ce7 100644 --- a/crates/cast/bin/cmd/storage.rs +++ b/crates/cast/bin/cmd/storage.rs @@ -13,7 +13,7 @@ use foundry_cli::{ }; use foundry_common::{ abi::find_source, - compile::{compile, etherscan_project, suppress_compile}, + compile::{etherscan_project, ProjectCompiler}, RetryProvider, }; use foundry_config::{ @@ -103,7 +103,7 @@ impl StorageArgs { if project.paths.has_input_files() { // Find in artifacts and pretty print add_storage_layout_output(&mut project); - let out = compile(&project, false, false)?; + let out = ProjectCompiler::new().compile(&project)?; let match_code = |artifact: &ConfigurableContractArtifact| -> Option { let bytes = artifact.deployed_bytecode.as_ref()?.bytecode.as_ref()?.object.as_bytes()?; @@ -141,7 +141,7 @@ impl StorageArgs { project.auto_detect = auto_detect; // Compile - let mut out = suppress_compile(&project)?; + let mut out = ProjectCompiler::new().quiet(true).compile(&project)?; let artifact = { let (_, mut artifact) = out .artifacts() @@ -154,7 +154,7 @@ impl StorageArgs { let solc = Solc::find_or_install_svm_version(MIN_SOLC.to_string())?; project.solc = solc; project.auto_detect = false; - if let Ok(output) = suppress_compile(&project) { + if let Ok(output) = ProjectCompiler::new().quiet(true).compile(&project) { out = output; let (_, new_artifact) = out .artifacts() diff --git a/crates/cast/bin/main.rs b/crates/cast/bin/main.rs index 2ad6f02ca02b4..6d40b84c5f88f 100644 --- a/crates/cast/bin/main.rs +++ b/crates/cast/bin/main.rs @@ -20,6 +20,9 @@ use foundry_common::{ use foundry_config::Config; use std::time::Instant; +#[macro_use] +extern crate foundry_cli; + pub mod cmd; pub mod opts; diff --git a/crates/cli/src/utils/cmd.rs b/crates/cli/src/utils/cmd.rs index 4323ddb2c6463..20738fcef69d6 100644 --- a/crates/cli/src/utils/cmd.rs +++ b/crates/cli/src/utils/cmd.rs @@ -1,3 +1,4 @@ +use crate::sh_warn; use ethers::{ abi::Abi, core::types::Chain, @@ -10,7 +11,7 @@ use ethers::{ }, }; use eyre::{Result, WrapErr}; -use foundry_common::{cli_warn, fs, TestFunctionExt}; +use foundry_common::{fs, TestFunctionExt}; use foundry_config::{error::ExtractConfigError, figment::Figment, Chain as ConfigChain, Config}; use foundry_evm::{ debug::DebugArena, @@ -256,35 +257,41 @@ where fn load_config_emit_warnings(self) -> Config { let config = self.load_config(); - config.__warnings.iter().for_each(|w| cli_warn!("{w}")); + emit_warnings(&config); config } fn try_load_config_emit_warnings(self) -> Result { let config = self.try_load_config()?; - config.__warnings.iter().for_each(|w| cli_warn!("{w}")); + emit_warnings(&config); Ok(config) } fn load_config_and_evm_opts_emit_warnings(self) -> Result<(Config, EvmOpts)> { let (config, evm_opts) = self.load_config_and_evm_opts()?; - config.__warnings.iter().for_each(|w| cli_warn!("{w}")); + emit_warnings(&config); Ok((config, evm_opts)) } fn load_config_unsanitized_emit_warnings(self) -> Config { let config = self.load_config_unsanitized(); - config.__warnings.iter().for_each(|w| cli_warn!("{w}")); + emit_warnings(&config); config } fn try_load_config_unsanitized_emit_warnings(self) -> Result { let config = self.try_load_config_unsanitized()?; - config.__warnings.iter().for_each(|w| cli_warn!("{w}")); + emit_warnings(&config); Ok(config) } } +fn emit_warnings(config: &Config) { + for warning in &config.__warnings { + let _ = sh_warn!("{warning}"); + } +} + /// Read contract constructor arguments from the given file. pub fn read_constructor_args_file(constructor_args_path: PathBuf) -> Result> { if !constructor_args_path.exists() { diff --git a/crates/common/src/compile.rs b/crates/common/src/compile.rs index 6f02c367093e2..17735d74f18c6 100644 --- a/crates/common/src/compile.rs +++ b/crates/common/src/compile.rs @@ -1,11 +1,12 @@ -//! Support for compiling [ethers::solc::Project] -use crate::{glob::GlobMatcher, term, TestFunctionExt}; +//! Support for compiling [ethers::solc::Project]. + +use crate::{glob::GlobMatcher, term::SpinnerReporter, TestFunctionExt}; use comfy_table::{presets::ASCII_MARKDOWN, *}; use ethers_etherscan::contract::Metadata; use ethers_solc::{ artifacts::{BytecodeObject, ContractBytecodeSome}, remappings::Remapping, - report::NoReporter, + report::{BasicStdoutReporter, NoReporter, Report}, Artifact, ArtifactId, FileFilter, Graph, Project, ProjectCompileOutput, ProjectPathsConfig, Solc, SolcConfig, }; @@ -19,59 +20,146 @@ use std::{ str::FromStr, }; -/// Helper type to configure how to compile a project +/// Builder type to configure how to compile a project. /// -/// This is merely a wrapper for [Project::compile()] which also prints to stdout dependent on its -/// settings -#[derive(Debug, Clone, Default)] +/// This is merely a wrapper for [`Project::compile()`] which also prints to stdout depending on its +/// settings. +#[derive(Clone, Debug)] +#[must_use = "this builder does nothing unless you call a `compile*` method"] pub struct ProjectCompiler { - /// whether to also print the contract names - print_names: bool, - /// whether to also print the contract sizes - print_sizes: bool, - /// files to exclude + /// Whether to compile in sparse mode. + sparse: Option, + + /// Whether we are going to verify the contracts after compilation. + verify: Option, + + /// Whether to also print contract names. + print_names: Option, + + /// Whether to also print contract sizes. + print_sizes: Option, + + /// Whether to print anything at all. Overrides other `print` options. + quiet: Option, + + /// Files to exclude. filters: Vec, + + /// Extra files to include, that are not necessarily in the project's source dir. + files: Vec, +} + +impl Default for ProjectCompiler { + #[inline] + fn default() -> Self { + Self::new() + } } impl ProjectCompiler { - /// Create a new instance with the settings - pub fn new(print_names: bool, print_sizes: bool) -> Self { - Self::with_filter(print_names, print_sizes, Vec::new()) + /// Create a new builder with the default settings. + #[inline] + pub fn new() -> Self { + Self { + sparse: None, + verify: None, + print_names: None, + print_sizes: None, + quiet: None, // TODO: set quiet from Shell + filters: Vec::new(), + files: Vec::new(), + } } - /// Create a new instance with all settings - pub fn with_filter( - print_names: bool, - print_sizes: bool, - filters: Vec, - ) -> Self { - Self { print_names, print_sizes, filters } + /// Sets whether to compile in sparse mode. + #[inline] + pub fn sparse(mut self, yes: bool) -> Self { + self.sparse = Some(yes); + self + } + + /// Sets whether we are going to verify the contracts after compilation. + #[inline] + pub fn verify(mut self, yes: bool) -> Self { + self.verify = Some(yes); + self + } + + /// Sets whether to print contract names. + #[inline] + pub fn print_names(mut self, yes: bool) -> Self { + self.print_names = Some(yes); + self + } + + /// Sets whether to print contract sizes. + #[inline] + pub fn print_sizes(mut self, yes: bool) -> Self { + self.print_sizes = Some(yes); + self + } + + /// Sets whether to print anything at all. Overrides other `print` options. + #[inline] + #[doc(alias = "silent")] + pub fn quiet(mut self, yes: bool) -> Self { + self.quiet = Some(yes); + self + } + + /// Do not print anything at all if true. Overrides other `print` options. + #[inline] + pub fn quiet_if(mut self, maybe: bool) -> Self { + if maybe { + self.quiet = Some(true); + } + self } - /// Compiles the project with [`Project::compile()`] - pub fn compile(self, project: &Project) -> Result { - let filters = self.filters.clone(); - self.compile_with(project, |prj| { - let output = if filters.is_empty() { - prj.compile() + /// Sets files to exclude. + #[inline] + pub fn filters(mut self, filters: impl IntoIterator) -> Self { + self.filters.extend(filters); + self + } + + /// Sets extra files to include, that are not necessarily in the project's source dir. + #[inline] + pub fn files(mut self, files: impl IntoIterator) -> Self { + self.files.extend(files); + self + } + + /// Compiles the project with [`Project::compile()`]. + pub fn compile(mut self, project: &Project) -> Result { + // Taking is fine since we don't need these in `compile_with`. + let filters = std::mem::take(&mut self.filters); + let files = std::mem::take(&mut self.files); + self.compile_with(project, || { + if !files.is_empty() { + project.compile_files(files) + } else if !filters.is_empty() { + project.compile_sparse(SkipBuildFilters(filters)) } else { - prj.compile_sparse(SkipBuildFilters(filters)) - }?; - Ok(output) + project.compile() + } + .map_err(Into::into) }) } - /// Compiles the project with [`Project::compile_parse()`] and the given filter. + /// Compiles the project with [`Project::compile_sparse()`] and the given filter. /// /// This will emit artifacts only for files that match the given filter. /// Files that do _not_ match the filter are given a pruned output selection and do not generate /// artifacts. + /// + /// Note that this ignores previously set `filters` and `files`. pub fn compile_sparse( self, project: &Project, filter: F, ) -> Result { - self.compile_with(project, |prj| Ok(prj.compile_sparse(filter)?)) + self.compile_with(project, || project.compile_sparse(filter).map_err(Into::into)) } /// Compiles the project with the given closure @@ -87,7 +175,7 @@ impl ProjectCompiler { #[tracing::instrument(target = "forge::compile", skip_all)] pub fn compile_with(self, project: &Project, f: F) -> Result where - F: FnOnce(&Project) -> Result, + F: FnOnce() -> Result, { if !project.paths.has_input_files() { println!("Nothing to compile"); @@ -95,23 +183,40 @@ impl ProjectCompiler { std::process::exit(0); } - let now = std::time::Instant::now(); - tracing::trace!("start compiling project"); + let quiet = self.quiet.unwrap_or(false); + let reporter = if quiet { + Report::new(NoReporter::default()) + } else { + // TODO: stderr.is_terminal() + if true { + Report::new(SpinnerReporter::spawn()) + } else { + Report::new(BasicStdoutReporter::default()) + } + }; + + let output = ethers_solc::report::with_scoped(&reporter, || { + tracing::debug!("compiling project"); - let output = term::with_spinner_reporter(|| f(project))?; + let timer = std::time::Instant::now(); + let r = f(); + let elapsed = timer.elapsed(); - let elapsed = now.elapsed(); - tracing::trace!(?elapsed, "finished compiling"); + tracing::debug!("finished compiling in {:.3}s", elapsed.as_secs_f64()); + r + })?; if output.has_compiler_errors() { - tracing::warn!("compiled with errors"); - eyre::bail!(output.to_string()) - } else if output.is_unchanged() { - println!("No files changed, compilation skipped"); - self.handle_output(&output); - } else { - // print the compiler output / warnings - println!("{output}"); + eyre::bail!("{output}") + } + + if !quiet { + if output.is_unchanged() { + println!("No files changed, compilation skipped"); + } else { + // print the compiler output / warnings + println!("{output}"); + } self.handle_output(&output); } @@ -121,8 +226,11 @@ impl ProjectCompiler { /// If configured, this will print sizes or names fn handle_output(&self, output: &ProjectCompileOutput) { + let print_names = self.print_names.unwrap_or(false); + let print_sizes = self.print_sizes.unwrap_or(false); + // print any sizes or names - if self.print_names { + if print_names { let mut artifacts: BTreeMap<_, Vec<_>> = BTreeMap::new(); for (name, (_, version)) in output.versioned_artifacts() { artifacts.entry(version).or_default().push(name); @@ -137,9 +245,10 @@ impl ProjectCompiler { } } } - if self.print_sizes { + + if print_sizes { // add extra newline if names were already printed - if self.print_names { + if print_names { println!(); } let mut size_report = SizeReport { contracts: BTreeMap::new() }; @@ -176,7 +285,7 @@ const CONTRACT_SIZE_LIMIT: usize = 24576; /// Contracts with info about their size pub struct SizeReport { - /// `:info>` + /// `contract name -> info` pub contracts: BTreeMap, } @@ -257,145 +366,28 @@ pub struct ContractInfo { pub is_dev_contract: bool, } -/// Compiles the provided [`Project`], throws if there's any compiler error and logs whether -/// compilation was successful or if there was a cache hit. -pub fn compile( - project: &Project, - print_names: bool, - print_sizes: bool, -) -> Result { - ProjectCompiler::new(print_names, print_sizes).compile(project) -} - -/// Compiles the provided [`Project`], throws if there's any compiler error and logs whether -/// compilation was successful or if there was a cache hit. -/// -/// Takes a list of [`SkipBuildFilter`] for files to exclude from the build. -pub fn compile_with_filter( - project: &Project, - print_names: bool, - print_sizes: bool, - skip: Vec, -) -> Result { - ProjectCompiler::with_filter(print_names, print_sizes, skip).compile(project) -} - -/// Compiles the provided [`Project`], throws if there's any compiler error and logs whether -/// compilation was successful or if there was a cache hit. -/// Doesn't print anything to stdout, thus is "suppressed". -pub fn suppress_compile(project: &Project) -> Result { - let output = ethers_solc::report::with_scoped( - ðers_solc::report::Report::new(NoReporter::default()), - || project.compile(), - )?; - - if output.has_compiler_errors() { - eyre::bail!(output.to_string()) - } - - Ok(output) -} - -/// Depending on whether the `skip` is empty this will [`suppress_compile_sparse`] or -/// [`suppress_compile`] -pub fn suppress_compile_with_filter( - project: &Project, - skip: Vec, -) -> Result { - if skip.is_empty() { - suppress_compile(project) - } else { - suppress_compile_sparse(project, SkipBuildFilters(skip)) - } -} - -/// Compiles the provided [`Project`], throws if there's any compiler error and logs whether -/// compilation was successful or if there was a cache hit. -/// Doesn't print anything to stdout, thus is "suppressed". -/// -/// See [`Project::compile_sparse`] -pub fn suppress_compile_sparse( - project: &Project, - filter: F, -) -> Result { - let output = ethers_solc::report::with_scoped( - ðers_solc::report::Report::new(NoReporter::default()), - || project.compile_sparse(filter), - )?; - - if output.has_compiler_errors() { - eyre::bail!(output.to_string()) - } - - Ok(output) -} - -/// Compile a set of files not necessarily included in the `project`'s source dir -/// -/// If `silent` no solc related output will be emitted to stdout -pub fn compile_files( - project: &Project, - files: Vec, - silent: bool, -) -> Result { - let output = if silent { - ethers_solc::report::with_scoped( - ðers_solc::report::Report::new(NoReporter::default()), - || project.compile_files(files), - ) - } else { - term::with_spinner_reporter(|| project.compile_files(files)) - }?; - - if output.has_compiler_errors() { - eyre::bail!(output.to_string()) - } - if !silent { - println!("{output}"); - } - - Ok(output) -} - /// Compiles target file path. /// -/// If `silent` no solc related output will be emitted to stdout. -/// /// If `verify` and it's a standalone script, throw error. Only allowed for projects. /// /// **Note:** this expects the `target_path` to be absolute -pub fn compile_target( - target_path: &Path, - project: &Project, - silent: bool, - verify: bool, -) -> Result { - compile_target_with_filter(target_path, project, silent, verify, Vec::new()) -} - -/// Compiles target file path. pub fn compile_target_with_filter( target_path: &Path, project: &Project, - silent: bool, verify: bool, skip: Vec, ) -> Result { let graph = Graph::resolve(&project.paths)?; // Checking if it's a standalone script, or part of a project. + let mut compiler = ProjectCompiler::new().filters(skip); if !graph.files().contains_key(target_path) { if verify { eyre::bail!("You can only verify deployments from inside a project! Make sure it exists with `forge tree`."); } - return compile_files(project, vec![target_path.to_path_buf()], silent) - } - - if silent { - suppress_compile_with_filter(project, skip) - } else { - compile_with_filter(project, false, false, skip) + compiler = compiler.files([target_path.into()]); } + compiler.compile(project) } /// Creates and compiles a project from an Etherscan source. @@ -409,7 +401,7 @@ pub async fn compile_from_source( let project_output = project.compile()?; if project_output.has_compiler_errors() { - eyre::bail!(project_output.to_string()) + eyre::bail!("{project_output}") } let (artifact_id, contract) = project_output diff --git a/crates/common/src/term.rs b/crates/common/src/term.rs index 40f36da1fe65c..8b3063f45ad13 100644 --- a/crates/common/src/term.rs +++ b/crates/common/src/term.rs @@ -217,21 +217,6 @@ pub fn with_spinner_reporter(f: impl FnOnce() -> T) -> T { report::with_scoped(&reporter, f) } -#[macro_export] -/// Displays warnings on the cli -macro_rules! cli_warn { - ($($arg:tt)*) => { - eprintln!( - "{}{} {}", - yansi::Paint::yellow("warning").bold(), - yansi::Paint::new(":").bold(), - format_args!($($arg)*) - ) - } -} - -pub use cli_warn; - #[cfg(test)] mod tests { use super::*; diff --git a/crates/config/src/warning.rs b/crates/config/src/warning.rs index fc98be3d4fa25..980014df0d3a0 100644 --- a/crates/config/src/warning.rs +++ b/crates/config/src/warning.rs @@ -53,30 +53,37 @@ impl fmt::Display for Warning { match self { Self::UnknownSection { unknown_section, source } => { let source = source.as_ref().map(|src| format!(" in {src}")).unwrap_or_default(); - f.write_fmt(format_args!("Unknown section [{unknown_section}] found{source}. This notation for profiles has been deprecated and may result in the profile not being registered in future versions. Please use [profile.{unknown_section}] instead or run `forge config --fix`.")) - } - Self::NoLocalToml(tried) => { - let path = tried.display(); - f.write_fmt(format_args!("No local TOML found to fix at {path}. Change the current directory to a project path or set the foundry.toml path with the FOUNDRY_CONFIG environment variable")) + write!( + f, + "Found unknown config section{source}: [{unknown_section}]\n\ + This notation for profiles has been deprecated and may result in the profile \ + not being registered in future versions.\n\ + Please use [profile.{unknown_section}] instead or run `forge config --fix`." + ) } + Self::NoLocalToml(path) => write!( + f, + "No local TOML found to fix at {}.\n\ + Change the current directory to a project path or set the foundry.toml path with \ + the `FOUNDRY_CONFIG` environment variable", + path.display() + ), + Self::CouldNotReadToml { path, err } => { - f.write_fmt(format_args!("Could not read TOML at {}: {err}", path.display())) + write!(f, "Could not read TOML at {}: {err}", path.display()) } Self::CouldNotWriteToml { path, err } => { - f.write_fmt(format_args!("Could not write TOML to {}: {err}", path.display())) + write!(f, "Could not write TOML to {}: {err}", path.display()) + } + Self::CouldNotFixProfile { path, profile, err } => { + write!(f, "Could not fix [{profile}] in TOML at {}: {err}", path.display()) + } + Self::DeprecatedKey { old, new } if new.is_empty() => { + write!(f, "Key `{old}` is being deprecated and will be removed in future versions.") + } + Self::DeprecatedKey { old, new } => { + write!(f, "Key `{old}` is being deprecated in favor of `{new}`. It will be removed in future versions.") } - Self::CouldNotFixProfile { path, profile, err } => f.write_fmt(format_args!( - "Could not fix [{}] in TOML at {}: {}", - profile, - path.display(), - err - )), - Self::DeprecatedKey { old, new } if new.is_empty() => f.write_fmt(format_args!( - "Key `{old}` is being deprecated and will be removed in future versions.", - )), - Self::DeprecatedKey { old, new } => f.write_fmt(format_args!( - "Key `{old}` is being deprecated in favor of `{new}`. It will be removed in future versions.", - )), } } } diff --git a/crates/forge/bin/cmd/bind.rs b/crates/forge/bin/cmd/bind.rs index 237af98b87534..7c0a8748c9172 100644 --- a/crates/forge/bin/cmd/bind.rs +++ b/crates/forge/bin/cmd/bind.rs @@ -2,7 +2,7 @@ use clap::{Parser, ValueHint}; use ethers::contract::{Abigen, ContractFilter, ExcludeContracts, MultiAbigen, SelectContracts}; use eyre::Result; use foundry_cli::{opts::CoreBuildArgs, utils::LoadConfig}; -use foundry_common::{compile, fs::json_files}; +use foundry_common::{compile::ProjectCompiler, fs::json_files}; use foundry_config::impl_figment_convert; use std::{ fs, @@ -184,7 +184,7 @@ No contract artifacts found. Hint: Have you built your contracts yet? `forge bin if !self.skip_build { // run `forge build` let project = self.build_args.project()?; - compile::compile(&project, false, false)?; + ProjectCompiler::new().compile(&project)?; } let artifacts = self.try_load_config_emit_warnings()?.out; diff --git a/crates/forge/bin/cmd/build.rs b/crates/forge/bin/cmd/build.rs index 01ce380f71b9c..8fb1b25e810b3 100644 --- a/crates/forge/bin/cmd/build.rs +++ b/crates/forge/bin/cmd/build.rs @@ -3,10 +3,7 @@ use clap::Parser; use ethers::solc::{Project, ProjectCompileOutput}; use eyre::Result; use foundry_cli::{opts::CoreBuildArgs, utils::LoadConfig}; -use foundry_common::{ - compile, - compile::{ProjectCompiler, SkipBuildFilter}, -}; +use foundry_common::compile::{ProjectCompiler, SkipBuildFilter}; use foundry_config::{ figment::{ self, @@ -82,14 +79,11 @@ impl BuildArgs { project = config.project()?; } - let filters = self.skip.unwrap_or_default(); - - if self.args.silent { - compile::suppress_compile_with_filter(&project, filters) - } else { - let compiler = ProjectCompiler::with_filter(self.names, self.sizes, filters); - compiler.compile(&project) - } + ProjectCompiler::new() + .print_names(self.names) + .print_sizes(self.sizes) + .filters(self.skip.unwrap_or_default()) + .compile(&project) } /// Returns the `Project` for the current workspace diff --git a/crates/forge/bin/cmd/config.rs b/crates/forge/bin/cmd/config.rs index a8e33cdba38ca..7cd7ae2b10ec1 100644 --- a/crates/forge/bin/cmd/config.rs +++ b/crates/forge/bin/cmd/config.rs @@ -2,7 +2,7 @@ use super::build::BuildArgs; use clap::Parser; use eyre::Result; use foundry_cli::utils::LoadConfig; -use foundry_common::{evm::EvmArgs, term::cli_warn}; +use foundry_common::evm::EvmArgs; use foundry_config::fix::fix_tomls; foundry_config::impl_figment_convert!(ConfigArgs, opts, evm_opts); @@ -34,7 +34,7 @@ impl ConfigArgs { pub fn run(self) -> Result<()> { if self.fix { for warning in fix_tomls() { - cli_warn!("{warning}"); + sh_warn!("{warning}")?; } return Ok(()) } diff --git a/crates/forge/bin/cmd/create.rs b/crates/forge/bin/cmd/create.rs index ad82163aa72f6..8d992b6b72ec4 100644 --- a/crates/forge/bin/cmd/create.rs +++ b/crates/forge/bin/cmd/create.rs @@ -12,7 +12,7 @@ use foundry_cli::{ opts::{CoreBuildArgs, EthereumOpts, EtherscanOpts, TransactionOpts}, utils::{self, read_constructor_args_file, remove_contract, LoadConfig}, }; -use foundry_common::{abi::parse_tokens, compile, estimate_eip1559_fees}; +use foundry_common::{abi::parse_tokens, compile::ProjectCompiler, estimate_eip1559_fees}; use serde_json::json; use std::{path::PathBuf, sync::Arc}; @@ -72,12 +72,7 @@ impl CreateArgs { pub async fn run(mut self) -> Result<()> { // Find Project & Compile let project = self.opts.project()?; - let mut output = if self.json || self.opts.silent { - // Suppress compile stdout messages when printing json output or when silent - compile::suppress_compile(&project) - } else { - compile::compile(&project, false, false) - }?; + let mut output = ProjectCompiler::new().quiet_if(self.json).compile(&project)?; if let Some(ref mut path) = self.contract.path { // paths are absolute in the project's output diff --git a/crates/forge/bin/cmd/fmt.rs b/crates/forge/bin/cmd/fmt.rs index bf8334d70b9fb..cb9d5b011a0d2 100644 --- a/crates/forge/bin/cmd/fmt.rs +++ b/crates/forge/bin/cmd/fmt.rs @@ -2,7 +2,7 @@ use clap::{Parser, ValueHint}; use eyre::Result; use forge_fmt::{format, parse, print_diagnostics_report}; use foundry_cli::utils::{FoundryPathExt, LoadConfig}; -use foundry_common::{fs, term::cli_warn}; +use foundry_common::fs; use foundry_config::impl_figment_convert_basic; use foundry_utils::glob::expand_globs; use rayon::prelude::*; @@ -112,7 +112,7 @@ impl FmtArgs { let mut lines = source[..loc.start().min(source.len())].split('\n'); let col = lines.next_back().unwrap().len() + 1; let row = lines.count() + 1; - cli_warn!("[{}:{}:{}] {}", name, row, col, warning); + sh_warn!("[{name}:{row}:{col}] {warning}")?; } } @@ -145,12 +145,11 @@ impl FmtArgs { Input::Stdin(source) => format(source, None).map(|diff| vec![diff]), Input::Paths(paths) => { if paths.is_empty() { - cli_warn!( + return sh_warn!( "Nothing to format.\n\ HINT: If you are working outside of the project, \ try providing paths to your source files: `forge fmt `" - ); - return Ok(()) + ) } paths .par_iter() diff --git a/crates/forge/bin/cmd/fourbyte.rs b/crates/forge/bin/cmd/fourbyte.rs index bf58b79a77425..8a152d28ad585 100644 --- a/crates/forge/bin/cmd/fourbyte.rs +++ b/crates/forge/bin/cmd/fourbyte.rs @@ -6,7 +6,7 @@ use foundry_cli::{ utils::FoundryPathExt, }; use foundry_common::{ - compile, + compile::ProjectCompiler, selectors::{import_selectors, SelectorImportData}, }; @@ -42,9 +42,9 @@ impl UploadSelectorsArgs { }; let project = build_args.project()?; - let outcome = compile::suppress_compile(&project)?; + let output = ProjectCompiler::new().quiet(true).compile(&project)?; let artifacts = if all { - outcome + output .into_artifacts_with_files() .filter(|(file, _, _)| { let is_sources_path = @@ -57,7 +57,7 @@ impl UploadSelectorsArgs { .collect() } else { let contract = contract.unwrap(); - let found_artifact = outcome.find_first(&contract); + let found_artifact = output.find_first(&contract); let artifact = found_artifact .ok_or_else(|| { eyre::eyre!("Could not find artifact `{contract}` in the compiled artifacts") diff --git a/crates/forge/bin/cmd/inspect.rs b/crates/forge/bin/cmd/inspect.rs index 1359067578b90..edbf779e4f306 100644 --- a/crates/forge/bin/cmd/inspect.rs +++ b/crates/forge/bin/cmd/inspect.rs @@ -16,7 +16,7 @@ use ethers::{ }; use eyre::Result; use foundry_cli::opts::{CompilerArgs, CoreBuildArgs}; -use foundry_common::compile; +use foundry_common::compile::ProjectCompiler; use serde_json::{to_value, Value}; use std::fmt; use tracing::trace; @@ -67,16 +67,16 @@ impl InspectArgs { // Build the project let project = modified_build_args.project()?; - let outcome = if let Some(ref mut contract_path) = contract.path { + let mut compiler = ProjectCompiler::new().quiet(true); + if let Some(contract_path) = &mut contract.path { let target_path = canonicalize(&*contract_path)?; *contract_path = target_path.to_string_lossy().to_string(); - compile::compile_files(&project, vec![target_path], true) - } else { - compile::suppress_compile(&project) - }?; + compiler = compiler.files([target_path]); + } + let output = compiler.compile(&project)?; // Find the artifact - let found_artifact = outcome.find_contract(&contract); + let found_artifact = output.find_contract(&contract); trace!(target: "forge", artifact=?found_artifact, input=?contract, "Found contract"); diff --git a/crates/forge/bin/cmd/script/build.rs b/crates/forge/bin/cmd/script/build.rs index c905662af0b1a..4be71ea31efc4 100644 --- a/crates/forge/bin/cmd/script/build.rs +++ b/crates/forge/bin/cmd/script/build.rs @@ -13,7 +13,7 @@ use ethers::{ }; use eyre::{Context, ContextCompat, Result}; use foundry_cli::utils::get_cached_entry_by_name; -use foundry_common::compile; +use foundry_common::compile::{self, ProjectCompiler}; use foundry_utils::{PostLinkInput, ResolvedDependency}; use std::{collections::BTreeMap, fs, str::FromStr}; use tracing::{trace, warn}; @@ -213,7 +213,6 @@ impl ScriptArgs { let output = compile::compile_target_with_filter( &target_contract, &project, - self.opts.args.silent, self.verify, filters, )?; @@ -231,23 +230,14 @@ impl ScriptArgs { if let Some(path) = contract.path { let path = dunce::canonicalize(path).wrap_err("Could not canonicalize the target path")?; - let output = compile::compile_target_with_filter( - &path, - &project, - self.opts.args.silent, - self.verify, - filters, - )?; + let output = + compile::compile_target_with_filter(&path, &project, self.verify, filters)?; self.path = path.to_string_lossy().to_string(); return Ok((project, output)) } // We received `contract_name`, and need to find its file path. - let output = if self.opts.args.silent { - compile::suppress_compile(&project) - } else { - compile::compile(&project, false, false) - }?; + let output = ProjectCompiler::new().compile(&project)?; let cache = SolFilesCache::read_joined(&project.paths).wrap_err("Could not open compiler cache")?; diff --git a/crates/forge/bin/cmd/selectors.rs b/crates/forge/bin/cmd/selectors.rs index b594081a72d11..3ca48cc93225d 100644 --- a/crates/forge/bin/cmd/selectors.rs +++ b/crates/forge/bin/cmd/selectors.rs @@ -7,7 +7,7 @@ use foundry_cli::{ utils::FoundryPathExt, }; use foundry_common::{ - compile, + compile::ProjectCompiler, selectors::{import_selectors, SelectorImportData}, }; use std::fs::canonicalize; @@ -18,21 +18,14 @@ pub enum SelectorsSubcommands { /// Check for selector collisions between contracts #[clap(visible_alias = "co")] Collision { - /// First contract - #[clap( - help = "The first of the two contracts for which to look selector collisions for, in the form `(:)?`", - value_name = "FIRST_CONTRACT" - )] + /// The first of the two contracts for which to look selector collisions for, in the form + /// `(:)?`. first_contract: ContractInfo, - /// Second contract - #[clap( - help = "The second of the two contracts for which to look selector collisions for, in the form `(:)?`", - value_name = "SECOND_CONTRACT" - )] + /// The second of the two contracts for which to look selector collisions for, in the form + /// `(:)?`. second_contract: ContractInfo, - /// Support build args #[clap(flatten)] build: Box, }, @@ -67,9 +60,9 @@ impl SelectorsSubcommands { }; let project = build_args.project()?; - let outcome = compile::suppress_compile(&project)?; + let output = ProjectCompiler::new().quiet(true).compile(&project)?; let artifacts = if all { - outcome + output .into_artifacts_with_files() .filter(|(file, _, _)| { let is_sources_path = file @@ -82,7 +75,7 @@ impl SelectorsSubcommands { .collect() } else { let contract = contract.unwrap(); - let found_artifact = outcome.find_first(&contract); + let found_artifact = output.find_first(&contract); let artifact = found_artifact .ok_or_else(|| { eyre::eyre!( @@ -114,41 +107,34 @@ impl SelectorsSubcommands { } } SelectorsSubcommands::Collision { mut first_contract, mut second_contract, build } => { - // Build first project - let first_project = build.project()?; - let first_outcome = if let Some(ref mut contract_path) = first_contract.path { + // Compile the project with the two contracts included + let project = build.project()?; + let mut compiler = ProjectCompiler::new().quiet(true); + + if let Some(contract_path) = &mut first_contract.path { let target_path = canonicalize(&*contract_path)?; *contract_path = target_path.to_string_lossy().to_string(); - compile::compile_files(&first_project, vec![target_path], true) - } else { - compile::suppress_compile(&first_project) - }?; - - // Build second project - let second_project = build.project()?; - let second_outcome = if let Some(ref mut contract_path) = second_contract.path { + compiler = compiler.files([target_path]); + } + if let Some(contract_path) = &mut second_contract.path { let target_path = canonicalize(&*contract_path)?; *contract_path = target_path.to_string_lossy().to_string(); - compile::compile_files(&second_project, vec![target_path], true) - } else { - compile::suppress_compile(&second_project) - }?; - - // Find the artifacts - let first_found_artifact = first_outcome.find_contract(&first_contract); - let second_found_artifact = second_outcome.find_contract(&second_contract); + compiler = compiler.files([target_path]); + } - // Unwrap inner artifacts - let first_artifact = first_found_artifact.ok_or_else(|| { - eyre::eyre!("Failed to extract first artifact bytecode as a string") - })?; - let second_artifact = second_found_artifact.ok_or_else(|| { - eyre::eyre!("Failed to extract second artifact bytecode as a string") - })?; + let output = compiler.compile(&project)?; // Check method selectors for collisions - let first_method_map = first_artifact.method_identifiers.as_ref().unwrap(); - let second_method_map = second_artifact.method_identifiers.as_ref().unwrap(); + let methods = |contract: &ContractInfo| -> eyre::Result<_> { + let artifact = output + .find_contract(contract) + .ok_or_else(|| eyre::eyre!("Could not find artifact for {contract}"))?; + artifact.method_identifiers.as_ref().ok_or_else(|| { + eyre::eyre!("Could not find method identifiers for {contract}") + }) + }; + let first_method_map = methods(&first_contract)?; + let second_method_map = methods(&second_contract)?; let colliding_methods: Vec<(&String, &String, &String)> = first_method_map .iter() diff --git a/crates/forge/bin/cmd/test/mod.rs b/crates/forge/bin/cmd/test/mod.rs index 7e755db937fcd..c2df07b36f845 100644 --- a/crates/forge/bin/cmd/test/mod.rs +++ b/crates/forge/bin/cmd/test/mod.rs @@ -17,11 +17,7 @@ use foundry_cli::{ opts::CoreBuildArgs, utils::{self, LoadConfig}, }; -use foundry_common::{ - compile::{self, ProjectCompiler}, - evm::EvmArgs, - get_contract_name, get_file_name, -}; +use foundry_common::{compile::ProjectCompiler, evm::EvmArgs, get_contract_name, get_file_name}; use foundry_config::{ figment, figment::{ @@ -144,14 +140,10 @@ impl TestArgs { project = config.project()?; } - let compiler = ProjectCompiler::default(); - let output = if config.sparse_mode { - compiler.compile_sparse(&project, filter.clone()) - } else if self.opts.silent { - compile::suppress_compile(&project) - } else { - compiler.compile(&project) - }?; + let output = ProjectCompiler::new() + .sparse(config.sparse_mode) + // .filters(filter) // TODO: sparse filter + .compile(&project)?; // Create test options from general project settings // and compiler output @@ -213,15 +205,13 @@ impl TestArgs { }; // Run the debugger - let mut opts = self.opts.clone(); - opts.silent = true; let debugger = DebugArgs { path: PathBuf::from(runner.source_paths.get(&id).unwrap()), target_contract: Some(get_contract_name(&id).to_string()), sig, args: Vec::new(), debug: true, - opts, + opts: self.opts.clone(), // TODO: silent? evm_opts: self.evm_opts, }; debugger.debug(breakpoints).await?; From d63f99dee6564e5646f42098d32bf8befd657b13 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Sun, 27 Aug 2023 14:47:29 +0200 Subject: [PATCH 05/14] chore: move io module to common --- Cargo.lock | 7 +++---- crates/cast/bin/cmd/access_list.rs | 1 - crates/cast/bin/cmd/call.rs | 1 - crates/cast/bin/cmd/wallet/mod.rs | 5 +---- crates/cast/bin/main.rs | 7 ++++--- crates/cli/Cargo.toml | 6 ------ crates/cli/src/lib.rs | 6 +++--- crates/cli/src/opts/shell.rs | 2 +- crates/cli/src/opts/wallet/mod.rs | 6 +----- crates/cli/src/opts/wallet/multi_wallet.rs | 10 ++-------- crates/cli/src/utils/cmd.rs | 1 - crates/cli/src/utils/mod.rs | 4 ++-- crates/common/Cargo.toml | 7 ++++++- crates/common/src/clap_helpers.rs | 6 ------ crates/{cli => common}/src/io/macros.rs | 0 crates/{cli => common}/src/io/mod.rs | 2 ++ crates/{cli => common}/src/io/shell.rs | 13 ++++++++++++- crates/{cli => common}/src/io/stdin.rs | 1 + crates/common/src/lib.rs | 4 +++- crates/forge/bin/cmd/install.rs | 3 +-- crates/forge/bin/cmd/script/mod.rs | 9 ++------- crates/forge/bin/cmd/test/mod.rs | 2 +- crates/forge/bin/main.rs | 4 ++-- 23 files changed, 47 insertions(+), 60 deletions(-) delete mode 100644 crates/common/src/clap_helpers.rs rename crates/{cli => common}/src/io/macros.rs (100%) rename crates/{cli => common}/src/io/mod.rs (55%) rename crates/{cli => common}/src/io/shell.rs (96%) rename crates/{cli => common}/src/io/stdin.rs (98%) diff --git a/Cargo.lock b/Cargo.lock index ffd0fc8839dc6..d3d4336fc44e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2445,7 +2445,6 @@ dependencies = [ "foundry-common", "foundry-config", "foundry-evm", - "fwdansi", "indicatif", "itertools 0.11.0", "once_cell", @@ -2454,12 +2453,9 @@ dependencies = [ "rusoto_core", "rusoto_kms", "serde", - "serde_json", "strsim", "strum 0.25.0", "tempfile", - "termcolor", - "terminal_size", "thiserror", "tokio", "tracing", @@ -2486,6 +2482,7 @@ dependencies = [ "eyre", "foundry-config", "foundry-macros", + "fwdansi", "globset", "once_cell", "regex", @@ -2494,6 +2491,8 @@ dependencies = [ "serde", "serde_json", "tempfile", + "termcolor", + "terminal_size", "thiserror", "tokio", "tracing", diff --git a/crates/cast/bin/cmd/access_list.rs b/crates/cast/bin/cmd/access_list.rs index 261975a14f7cd..0b098da5fa42e 100644 --- a/crates/cast/bin/cmd/access_list.rs +++ b/crates/cast/bin/cmd/access_list.rs @@ -34,7 +34,6 @@ pub struct AccessListArgs { #[clap( long, value_name = "DATA", - value_parser = foundry_common::clap_helpers::strip_0x_prefix, conflicts_with_all = &["sig", "args"] )] data: Option, diff --git a/crates/cast/bin/cmd/call.rs b/crates/cast/bin/cmd/call.rs index 3659bcf2c098a..571334b5db6e8 100644 --- a/crates/cast/bin/cmd/call.rs +++ b/crates/cast/bin/cmd/call.rs @@ -32,7 +32,6 @@ pub struct CallArgs { /// Data for the transaction. #[clap( long, - value_parser = foundry_common::clap_helpers::strip_0x_prefix, conflicts_with_all = &["sig", "args"] )] data: Option, diff --git a/crates/cast/bin/cmd/wallet/mod.rs b/crates/cast/bin/cmd/wallet/mod.rs index 1fdaa923e25ea..acfdadd83a86e 100644 --- a/crates/cast/bin/cmd/wallet/mod.rs +++ b/crates/cast/bin/cmd/wallet/mod.rs @@ -45,10 +45,7 @@ pub enum WalletSubcommands { #[clap(visible_aliases = &["a", "addr"])] Address { /// If provided, the address will be derived from the specified private key. - #[clap( - value_name = "PRIVATE_KEY", - value_parser = foundry_common::clap_helpers::strip_0x_prefix, - )] + #[clap(value_name = "PRIVATE_KEY")] private_key_override: Option, #[clap(flatten)] diff --git a/crates/cast/bin/main.rs b/crates/cast/bin/main.rs index 6d40b84c5f88f..7f8284c34c971 100644 --- a/crates/cast/bin/main.rs +++ b/crates/cast/bin/main.rs @@ -8,7 +8,7 @@ use ethers::{ utils::keccak256, }; use eyre::Result; -use foundry_cli::{handler, prompt, stdin, utils}; +use foundry_cli::{handler, utils}; use foundry_common::{ abi::{format_tokens, get_event}, fs, @@ -16,12 +16,13 @@ use foundry_common::{ decode_calldata, decode_event_topic, decode_function_selector, import_selectors, parse_signatures, pretty_calldata, ParsedSignatures, SelectorImportData, }, + stdin, }; use foundry_config::Config; use std::time::Instant; #[macro_use] -extern crate foundry_cli; +extern crate foundry_common; pub mod cmd; pub mod opts; @@ -31,7 +32,7 @@ use opts::{Opts, Subcommands, ToBaseArgs}; #[tokio::main] async fn main() { if let Err(err) = run().await { - let _ = foundry_cli::Shell::get().error(&err); + let _ = foundry_common::Shell::get().error(&err); std::process::exit(1); } } diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index bd106efc5ea92..07734dd122ac6 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -35,11 +35,8 @@ once_cell = "1" regex = { version = "1", default-features = false } rpassword = "7" serde = { version = "1", features = ["derive"] } -serde_json = "1" strsim = "0.10" strum = { version = "0.25", features = ["derive"] } -termcolor = "1" -terminal_size = "0.2" thiserror = "1" tokio = { version = "1", features = ["macros"] } tracing = "0.1" @@ -47,9 +44,6 @@ tracing-error = "0.2" tracing-subscriber = { version = "0.3", features = ["registry", "env-filter", "fmt"] } yansi = "0.5" -[target.'cfg(windows)'.dependencies] -fwdansi = "1" - [dev-dependencies] tempfile = "3.7" diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index a6b2f36fb74d4..783f79da1d2a0 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -1,8 +1,8 @@ #![warn(unused_crate_dependencies)] +#[macro_use] +extern crate foundry_common; + pub mod handler; pub mod opts; pub mod utils; - -mod io; -pub use io::{shell, stdin, Shell}; diff --git a/crates/cli/src/opts/shell.rs b/crates/cli/src/opts/shell.rs index b60a955e406b0..42ff4aaa223b1 100644 --- a/crates/cli/src/opts/shell.rs +++ b/crates/cli/src/opts/shell.rs @@ -1,5 +1,5 @@ -use crate::shell::{ColorChoice, Shell, Verbosity}; use clap::Parser; +use foundry_common::shell::{ColorChoice, Shell, Verbosity}; /// Global shell options. #[derive(Clone, Copy, Debug, Parser)] diff --git a/crates/cli/src/opts/wallet/mod.rs b/crates/cli/src/opts/wallet/mod.rs index a85b4e7c77026..640f906a621ca 100644 --- a/crates/cli/src/opts/wallet/mod.rs +++ b/crates/cli/src/opts/wallet/mod.rs @@ -45,11 +45,7 @@ pub struct RawWallet { pub interactive: bool, /// Use the provided private key. - #[clap( - long, - value_name = "RAW_PRIVATE_KEY", - value_parser = foundry_common::clap_helpers::strip_0x_prefix - )] + #[clap(long, value_name = "RAW_PRIVATE_KEY")] pub private_key: Option, /// Use the mnemonic phrase of mnemonic file at the specified path. diff --git a/crates/cli/src/opts/wallet/multi_wallet.rs b/crates/cli/src/opts/wallet/multi_wallet.rs index 92091e2263d6c..2158a8c1b8f23 100644 --- a/crates/cli/src/opts/wallet/multi_wallet.rs +++ b/crates/cli/src/opts/wallet/multi_wallet.rs @@ -98,12 +98,7 @@ pub struct MultiWallet { pub interactives: u32, /// Use the provided private keys. - #[clap( - long, - help_heading = "Wallet options - raw", - value_name = "RAW_PRIVATE_KEYS", - value_parser = foundry_common::clap_helpers::strip_0x_prefix, - )] + #[clap(long, help_heading = "Wallet options - raw", value_name = "RAW_PRIVATE_KEYS")] pub private_keys: Option>, /// Use the provided private key. @@ -111,8 +106,7 @@ pub struct MultiWallet { long, help_heading = "Wallet options - raw", conflicts_with = "private_keys", - value_name = "RAW_PRIVATE_KEY", - value_parser = foundry_common::clap_helpers::strip_0x_prefix, + value_name = "RAW_PRIVATE_KEY" )] pub private_key: Option, diff --git a/crates/cli/src/utils/cmd.rs b/crates/cli/src/utils/cmd.rs index 20738fcef69d6..5134fe6cc1a4c 100644 --- a/crates/cli/src/utils/cmd.rs +++ b/crates/cli/src/utils/cmd.rs @@ -1,4 +1,3 @@ -use crate::sh_warn; use ethers::{ abi::Abi, core::types::Chain, diff --git a/crates/cli/src/utils/mod.rs b/crates/cli/src/utils/mod.rs index c26665d23b2e4..10fa3ef010d58 100644 --- a/crates/cli/src/utils/mod.rs +++ b/crates/cli/src/utils/mod.rs @@ -202,7 +202,7 @@ pub fn enable_paint() { pub fn print_receipt(chain: Chain, receipt: &TransactionReceipt) { let gas_used = receipt.gas_used.unwrap_or_default(); let gas_price = receipt.effective_gas_price.unwrap_or_default(); - crate::sh_println!( + sh_println!( "\n##### {chain}\n{status}Hash: {tx_hash:?}{caddr}\nBlock: {bn}\n{gas}\n", status = if receipt.status.map_or(true, |s| s.is_zero()) { "❌ [Failed]" @@ -496,7 +496,7 @@ https://github.com/foundry-rs/foundry/issues/new/choose" // don't set this in cmd() because it's not wanted for all commands fn stderr() -> Stdio { - if crate::Shell::get().verbosity().is_verbose() { + if foundry_common::Shell::get().verbosity().is_verbose() { Stdio::inherit() } else { Stdio::piped() diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index e945b45ce0947..811b6d3b83d32 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -27,9 +27,11 @@ reqwest = { version = "0.11", default-features = false } # cli clap = { version = "4", features = ["derive", "env", "unicode", "wrap_help"] } comfy-table = "6" +tempfile = "3" +termcolor = "1" +terminal_size = "0.2" tracing = "0.1" yansi = "0.5" -tempfile = "3" # misc auto_impl = "1.1.0" @@ -46,5 +48,8 @@ globset = "0.4" # Using const-hex instead of hex for speed hex.workspace = true +[target.'cfg(windows)'.dependencies] +fwdansi = "1" + [dev-dependencies] tokio = { version = "1", features = ["rt-multi-thread", "macros"] } diff --git a/crates/common/src/clap_helpers.rs b/crates/common/src/clap_helpers.rs deleted file mode 100644 index f26455b764148..0000000000000 --- a/crates/common/src/clap_helpers.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! Additional utils for clap - -/// A `clap` `value_parser` that removes a `0x` prefix if it exists -pub fn strip_0x_prefix(s: &str) -> Result { - Ok(s.strip_prefix("0x").unwrap_or(s).to_string()) -} diff --git a/crates/cli/src/io/macros.rs b/crates/common/src/io/macros.rs similarity index 100% rename from crates/cli/src/io/macros.rs rename to crates/common/src/io/macros.rs diff --git a/crates/cli/src/io/mod.rs b/crates/common/src/io/mod.rs similarity index 55% rename from crates/cli/src/io/mod.rs rename to crates/common/src/io/mod.rs index bc5f0c05a1c8e..09c66824a6989 100644 --- a/crates/cli/src/io/mod.rs +++ b/crates/common/src/io/mod.rs @@ -1,3 +1,5 @@ +//! Utilities for working with standard input, output, and error. + mod macros; pub mod shell; diff --git a/crates/cli/src/io/shell.rs b/crates/common/src/io/shell.rs similarity index 96% rename from crates/cli/src/io/shell.rs rename to crates/common/src/io/shell.rs index 171f0dc4842db..e452a70e467ed 100644 --- a/crates/cli/src/io/shell.rs +++ b/crates/common/src/io/shell.rs @@ -18,13 +18,18 @@ use termcolor::{ static GLOBAL_SHELL: Lazy> = Lazy::new(|| Mutex::new(Shell::new())); +/// Terminal width. pub enum TtyWidth { + /// Not a terminal, or could not determine size. NoTty, + /// A known width. Known(usize), + /// A guess at the width. Guess(usize), } impl TtyWidth { + /// Returns the width of the terminal from the environment, if known. pub fn get() -> Self { // use stderr #[cfg(unix)] @@ -141,6 +146,7 @@ impl Shell { Self::new_with(ColorChoice::Auto, Verbosity::Verbose) } + /// Creates a new shell with the given color choice and verbosity. pub fn new_with(color: ColorChoice, verbosity: Verbosity) -> Self { Self { output: ShellOut::Stream { @@ -257,6 +263,7 @@ impl Shell { self.print(&status, Some(&message), Green, true) } + /// Shortcut to right-align and color cyan a status without a message. pub fn status_header(&mut self, status: T) -> Result<()> where T: fmt::Display, @@ -358,7 +365,7 @@ impl Shell { } } - /// Whether the shell supports color. + /// Whether `stderr` supports color. pub fn err_supports_color(&self) -> bool { match &self.output { ShellOut::Write(_) => false, @@ -366,6 +373,7 @@ impl Shell { } } + /// Whether `stdout` supports color. pub fn out_supports_color(&self) -> bool { match &self.output { ShellOut::Write(_) => false, @@ -380,6 +388,7 @@ impl Shell { self.output.write_stdout(fragment, color) } + /// Write a styled fragment with the default color. pub fn print_out(&mut self, fragment: impl fmt::Display) -> Result<()> { self.write_stdout(fragment, &ColorSpec::new()) } @@ -391,6 +400,7 @@ impl Shell { self.output.write_stderr(fragment, color) } + /// Write a styled fragment with the default color. pub fn print_err(&mut self, fragment: impl fmt::Display) -> Result<()> { self.write_stderr(fragment, &ColorSpec::new()) } @@ -423,6 +433,7 @@ impl Shell { Ok(()) } + /// Serializes an object to JSON and prints it to `stdout`. pub fn print_json(&mut self, obj: &T) -> Result<()> { // Path may fail to serialize to JSON ... let encoded = serde_json::to_string(&obj)?; diff --git a/crates/cli/src/io/stdin.rs b/crates/common/src/io/stdin.rs similarity index 98% rename from crates/cli/src/io/stdin.rs rename to crates/common/src/io/stdin.rs index 3f78c5e495999..17b40a2cff1fe 100644 --- a/crates/cli/src/io/stdin.rs +++ b/crates/common/src/io/stdin.rs @@ -19,6 +19,7 @@ where } } +/// Shortcut for `(unwrap(a), unwrap(b))`. #[inline] pub fn unwrap2(a: Option, b: Option) -> Result<(A, B)> where diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 5cba49c267a6a..f68be64de99ed 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -4,7 +4,6 @@ pub mod abi; pub mod calc; -pub mod clap_helpers; pub mod compile; pub mod constants; pub mod contracts; @@ -13,6 +12,7 @@ pub mod evm; pub mod fmt; pub mod fs; pub mod glob; +pub mod io; pub mod provider; pub mod selectors; pub mod term; @@ -24,3 +24,5 @@ pub use contracts::*; pub use provider::*; pub use traits::*; pub use transactions::*; + +pub use io::{shell, stdin, Shell}; diff --git a/crates/forge/bin/cmd/install.rs b/crates/forge/bin/cmd/install.rs index 28848188da2ac..090292819a9e3 100644 --- a/crates/forge/bin/cmd/install.rs +++ b/crates/forge/bin/cmd/install.rs @@ -2,7 +2,6 @@ use clap::{Parser, ValueHint}; use eyre::{Context, Result}; use foundry_cli::{ opts::Dependency, - prompt, utils::{CommandUtils, Git, LoadConfig}, }; use foundry_common::fs; @@ -176,7 +175,7 @@ impl DependencyInstallOpts { } // TODO: make this a shell note - if !foundry_cli::Shell::get().verbosity().is_quiet() { + if !foundry_common::Shell::get().verbosity().is_quiet() { let mut msg = format!(" {} {}", Paint::green("Installed"), dep.name); if let Some(tag) = dep.tag.or(installed_tag) { msg.push(' '); diff --git a/crates/forge/bin/cmd/script/mod.rs b/crates/forge/bin/cmd/script/mod.rs index df9cab3a3c48a..bd44f72811426 100644 --- a/crates/forge/bin/cmd/script/mod.rs +++ b/crates/forge/bin/cmd/script/mod.rs @@ -110,12 +110,7 @@ pub struct ScriptArgs { pub target_contract: Option, /// The signature of the function you want to call in the contract, or raw calldata. - #[clap( - long, - short, - default_value = "run()", - value_parser = foundry_common::clap_helpers::strip_0x_prefix - )] + #[clap(long, short, default_value = "run()")] pub sig: String, /// Max priority fee per gas for EIP1559 transactions. @@ -382,7 +377,7 @@ impl ScriptArgs { let console_logs = decode_console_logs(&result.logs); let output = JsonResult { logs: console_logs, gas_used: result.gas_used, returns }; - foundry_cli::Shell::get().print_json(&output) + foundry_common::Shell::get().print_json(&output) } /// It finds the deployer from the running script and uses it to predeploy libraries. diff --git a/crates/forge/bin/cmd/test/mod.rs b/crates/forge/bin/cmd/test/mod.rs index c2df07b36f845..cc38694b4c135 100644 --- a/crates/forge/bin/cmd/test/mod.rs +++ b/crates/forge/bin/cmd/test/mod.rs @@ -364,7 +364,7 @@ impl TestOutcome { return Ok(()) } - if foundry_cli::Shell::get().verbosity().is_quiet() { + if foundry_common::Shell::get().verbosity().is_quiet() { // skip printing and exit early std::process::exit(1); } diff --git a/crates/forge/bin/main.rs b/crates/forge/bin/main.rs index 2f9c3f77236f5..bf4b151829433 100644 --- a/crates/forge/bin/main.rs +++ b/crates/forge/bin/main.rs @@ -4,7 +4,7 @@ use eyre::Result; use foundry_cli::{handler, utils}; #[macro_use] -extern crate foundry_cli; +extern crate foundry_common; mod cmd; mod opts; @@ -14,7 +14,7 @@ use opts::{Opts, Subcommands}; fn main() { if let Err(err) = run() { - let _ = foundry_cli::Shell::get().error(&err); + let _ = foundry_common::Shell::get().error(&err); std::process::exit(1); } } From dc0398f21ebf8206f0669176db15d7c242416a2a Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Sun, 27 Aug 2023 14:50:14 +0200 Subject: [PATCH 06/14] fix TODOs --- crates/common/src/compile.rs | 5 ++--- crates/common/src/term.rs | 5 +++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/common/src/compile.rs b/crates/common/src/compile.rs index 17735d74f18c6..fb549e7e2aeb4 100644 --- a/crates/common/src/compile.rs +++ b/crates/common/src/compile.rs @@ -65,7 +65,7 @@ impl ProjectCompiler { verify: None, print_names: None, print_sizes: None, - quiet: None, // TODO: set quiet from Shell + quiet: Some(crate::Shell::get().verbosity().is_quiet()), filters: Vec::new(), files: Vec::new(), } @@ -187,8 +187,7 @@ impl ProjectCompiler { let reporter = if quiet { Report::new(NoReporter::default()) } else { - // TODO: stderr.is_terminal() - if true { + if crate::Shell::get().is_err_tty() { Report::new(SpinnerReporter::spawn()) } else { Report::new(BasicStdoutReporter::default()) diff --git a/crates/common/src/term.rs b/crates/common/src/term.rs index 8b3063f45ad13..3c8009a94d13c 100644 --- a/crates/common/src/term.rs +++ b/crates/common/src/term.rs @@ -71,8 +71,9 @@ impl Spinner { let indicator = Paint::green(self.indicator[self.idx % self.indicator.len()]); let indicator = Paint::new(format!("[{indicator}]")).bold(); - print!("\r\x33[2K\r{indicator} {}", self.message); - io::stdout().flush().unwrap(); + let mut stderr = io::stderr().lock(); + write!(stderr, "\r\x33[2K\r{indicator} {}", self.message); + stderr.flush().unwrap(); self.idx = self.idx.wrapping_add(1); } From 99dec64f8585d8a79047f23f0342325e87d57187 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 28 Aug 2023 13:18:52 +0200 Subject: [PATCH 07/14] wip --- crates/cast/bin/cmd/storage.rs | 7 ++--- crates/common/src/io/macros.rs | 2 ++ crates/common/src/io/shell.rs | 2 +- crates/forge/bin/cmd/fmt.rs | 22 +++++++------ crates/forge/bin/cmd/geiger/mod.rs | 5 ++- crates/forge/bin/cmd/install.rs | 26 ++++++++-------- crates/forge/bin/cmd/script/mod.rs | 19 +++--------- crates/forge/bin/cmd/script/sequence.rs | 4 +-- crates/forge/bin/cmd/snapshot.rs | 6 ++-- crates/forge/bin/cmd/test/mod.rs | 41 ++++++++++++++----------- crates/forge/src/multi_runner.rs | 10 +++--- 11 files changed, 73 insertions(+), 71 deletions(-) diff --git a/crates/cast/bin/cmd/storage.rs b/crates/cast/bin/cmd/storage.rs index ff6d385f05ce7..9c455c7b90807 100644 --- a/crates/cast/bin/cmd/storage.rs +++ b/crates/cast/bin/cmd/storage.rs @@ -118,7 +118,7 @@ impl StorageArgs { // Not a forge project or artifact not found // Get code from Etherscan - eprintln!("No matching artifacts found, fetching source code from Etherscan..."); + sh_note!("No matching artifacts found, fetching source code from Etherscan...")?; let chain = utils::get_chain(config.chain_id, &provider).await?; let api_key = config.get_etherscan_api_key(Some(chain)).unwrap_or_default(); @@ -150,7 +150,7 @@ impl StorageArgs { if is_storage_layout_empty(&artifact.storage_layout) && auto_detect { // try recompiling with the minimum version - eprintln!("The requested contract was compiled with {version} while the minimum version for storage layouts is {MIN_SOLC} and as a result the output may be empty."); + sh_warn!("The requested contract was compiled with {version} while the minimum version for storage layouts is {MIN_SOLC} and as a result the output may be empty.")?; let solc = Solc::find_or_install_svm_version(MIN_SOLC.to_string())?; project.solc = solc; project.auto_detect = false; @@ -181,8 +181,7 @@ async fn fetch_and_print_storage( pretty: bool, ) -> Result<()> { if is_storage_layout_empty(&artifact.storage_layout) { - eprintln!("Storage layout is empty."); - Ok(()) + sh_note!("Storage layout is empty.") } else { let layout = artifact.storage_layout.as_ref().unwrap().clone(); let values = fetch_storage_values(provider, address, &layout).await?; diff --git a/crates/common/src/io/macros.rs b/crates/common/src/io/macros.rs index 654795ed33095..cedd3c260d877 100644 --- a/crates/common/src/io/macros.rs +++ b/crates/common/src/io/macros.rs @@ -111,12 +111,14 @@ mod tests { sh_print!("print -").unwrap(); sh_print!("print {} -", "arg").unwrap(); + sh_println!().unwrap(); sh_println!("println").unwrap(); sh_println!("println {}", "arg").unwrap(); sh_eprint!("eprint -").unwrap(); sh_eprint!("eprint {} -", "arg").unwrap(); + sh_eprintln!().unwrap(); sh_eprintln!("eprintln").unwrap(); sh_eprintln!("eprintln {}", "arg").unwrap(); } diff --git a/crates/common/src/io/shell.rs b/crates/common/src/io/shell.rs index e452a70e467ed..89e95096131b5 100644 --- a/crates/common/src/io/shell.rs +++ b/crates/common/src/io/shell.rs @@ -434,7 +434,7 @@ impl Shell { } /// Serializes an object to JSON and prints it to `stdout`. - pub fn print_json(&mut self, obj: &T) -> Result<()> { + pub fn print_json(&mut self, obj: &T) -> Result<()> { // Path may fail to serialize to JSON ... let encoded = serde_json::to_string(&obj)?; // ... but don't fail due to a closed pipe. diff --git a/crates/forge/bin/cmd/fmt.rs b/crates/forge/bin/cmd/fmt.rs index cb9d5b011a0d2..686ffbeaf2d0f 100644 --- a/crates/forge/bin/cmd/fmt.rs +++ b/crates/forge/bin/cmd/fmt.rs @@ -99,7 +99,7 @@ impl FmtArgs { Some(path) => { path.strip_prefix(&config.__root.0).unwrap_or(path).display().to_string() } - None => "stdin".to_string(), + None => "".to_string(), }; let parsed = parse(&source).map_err(|diagnostics| { @@ -112,17 +112,18 @@ impl FmtArgs { let mut lines = source[..loc.start().min(source.len())].split('\n'); let col = lines.next_back().unwrap().len() + 1; let row = lines.count() + 1; - sh_warn!("[{name}:{row}:{col}] {warning}")?; + sh_warn!("[{name}:{row}:{col}]: {warning}")?; } } let mut output = String::new(); format(&mut output, parsed, config.fmt.clone()).unwrap(); - solang_parser::parse(&output, 0).map_err(|diags| { + // validate + let _ = solang_parser::parse(&output, 0).map_err(|diags| { + tracing::debug!(?diags); eyre::eyre!( - "Failed to construct valid Solidity code for {name}. Leaving source unchanged.\n\ - Debug info: {diags:?}" + "Failed to construct valid Solidity code for {name}. Leaving source unchanged." ) })?; @@ -145,11 +146,12 @@ impl FmtArgs { Input::Stdin(source) => format(source, None).map(|diff| vec![diff]), Input::Paths(paths) => { if paths.is_empty() { - return sh_warn!( - "Nothing to format.\n\ - HINT: If you are working outside of the project, \ - try providing paths to your source files: `forge fmt `" - ) + sh_eprintln!("Nothing to format")?; + sh_note!( + "if you are working outside of the project, \ + try providing paths to your source files: `forge fmt ...`" + )?; + return Ok(()) } paths .par_iter() diff --git a/crates/forge/bin/cmd/geiger/mod.rs b/crates/forge/bin/cmd/geiger/mod.rs index 6756a5921a939..1584353de91db 100644 --- a/crates/forge/bin/cmd/geiger/mod.rs +++ b/crates/forge/bin/cmd/geiger/mod.rs @@ -6,7 +6,6 @@ use foundry_config::{impl_figment_convert_basic, Config}; use itertools::Itertools; use rayon::prelude::*; use std::path::PathBuf; -use yansi::Paint; mod error; @@ -91,7 +90,7 @@ impl GeigerArgs { let sources = self.sources(&config).wrap_err("Failed to resolve files")?; if config.ffi { - eprintln!("{}\n", Paint::red("ffi enabled")); + sh_note!("Enabled FFI.")?; } let root = config.__root.0; @@ -108,7 +107,7 @@ impl GeigerArgs { len } Err(err) => { - eprintln!("{err}"); + let _ = sh_err!("{err}"); 0 } }) diff --git a/crates/forge/bin/cmd/install.rs b/crates/forge/bin/cmd/install.rs index 090292819a9e3..fa6dbfe0a74eb 100644 --- a/crates/forge/bin/cmd/install.rs +++ b/crates/forge/bin/cmd/install.rs @@ -15,7 +15,6 @@ use std::{ str, }; use tracing::{trace, warn}; -use yansi::Paint; static DEPENDENCY_VERSION_TAG_REGEX: Lazy = Lazy::new(|| Regex::new(r"^v?\d+(\.\d+)*$").unwrap()); @@ -126,12 +125,15 @@ impl DependencyInstallOpts { let rel_path = path .strip_prefix(git.root) .wrap_err("Library directory is not relative to the repository root")?; - sh_eprintln!( - "Installing {} in {} (url: {:?}, tag: {:?})", - dep.name, - path.display(), - dep.url, - dep.tag + foundry_common::Shell::get().status( + "Installing", + format!( + "Installing {} in {} (url: {:?}, tag: {:?})", + dep.name, + path.display(), + dep.url, + dep.tag + ), )?; // this tracks the actual installed tag @@ -174,14 +176,14 @@ impl DependencyInstallOpts { } } - // TODO: make this a shell note - if !foundry_common::Shell::get().verbosity().is_quiet() { - let mut msg = format!(" {} {}", Paint::green("Installed"), dep.name); + let mut shell = foundry_common::Shell::get(); + if !shell.verbosity().is_quiet() { + let mut msg = dep.name; if let Some(tag) = dep.tag.or(installed_tag) { msg.push(' '); - msg.push_str(tag.as_str()); + msg.push_str(&tag); } - sh_eprintln!("{msg}")?; + shell.status("Installed", msg)?; } } diff --git a/crates/forge/bin/cmd/script/mod.rs b/crates/forge/bin/cmd/script/mod.rs index bd44f72811426..11a4fce00a702 100644 --- a/crates/forge/bin/cmd/script/mod.rs +++ b/crates/forge/bin/cmd/script/mod.rs @@ -282,9 +282,7 @@ impl ScriptArgs { ); } } - Err(_) => { - sh_eprintln!("{:x?}", (&returned))?; - } + Err(_) => sh_eprintln!("{returned:x?}")?, } Ok(returns) @@ -308,7 +306,7 @@ impl ScriptArgs { for (kind, trace) in &mut result.traces { let should_include = match kind { TraceKind::Setup => verbosity >= 5, - TraceKind::Execution => verbosity > 3, + TraceKind::Execution => verbosity >= 4, _ => false, } || !result.success; @@ -347,9 +345,7 @@ impl ScriptArgs { )?; } } - Err(_) => { - sh_eprintln!("{:x?}", (&result.returned))?; - } + Err(_) => sh_eprintln!("{:x?}", result.returned)?, } } @@ -609,12 +605,7 @@ impl ScriptArgs { if deployment_size > max_size { prompt_user = self.broadcast; - sh_eprintln!( - "{}", - Paint::red(format!( - "`{name}` is above the contract size limit ({deployment_size} > {max_size})." - )) - )?; + sh_warn!("`{name}` is above the contract size limit ({deployment_size} > {max_size})")?; } } } @@ -755,7 +746,7 @@ impl ScriptConfig { .collect::>() .join(", "); sh_warn!( - " + "\ EIP-3855 is not supported in one or more of the RPCs used. Unsupported Chain IDs: {ids}. Contracts deployed with a Solidity version equal or higher than 0.8.20 might not work properly. diff --git a/crates/forge/bin/cmd/script/sequence.rs b/crates/forge/bin/cmd/script/sequence.rs index 531216c8b3591..1ff11aa9758e1 100644 --- a/crates/forge/bin/cmd/script/sequence.rs +++ b/crates/forge/bin/cmd/script/sequence.rs @@ -174,8 +174,8 @@ impl ScriptSequence { //../run-[timestamp].json fs::copy(&self.sensitive_path, self.sensitive_path.with_file_name(&ts_name))?; - sh_eprintln!("\nTransactions saved to: {}\n", self.path.display())?; - sh_eprintln!("Sensitive values saved to: {}\n", self.sensitive_path.display())?; + sh_note!("Transactions saved to: {}", self.path.display())?; + sh_note!("Sensitive values saved to: {}", self.sensitive_path.display())?; Ok(()) } diff --git a/crates/forge/bin/cmd/snapshot.rs b/crates/forge/bin/cmd/snapshot.rs index ebc1f242cec74..14623c14005e3 100644 --- a/crates/forge/bin/cmd/snapshot.rs +++ b/crates/forge/bin/cmd/snapshot.rs @@ -330,8 +330,8 @@ fn check(tests: Vec, snaps: Vec, tolerance: Option) -> { let source_gas = test.result.kind.report(); if !within_tolerance(source_gas.gas(), target_gas.gas(), tolerance) { - eprintln!( - "Diff in \"{}::{}\": consumed \"{}\" gas, expected \"{}\" gas ", + let _ = sh_eprintln!( + "Diff in \"{}::{}\": consumed \"{}\" gas, expected \"{}\" gas", test.contract_name(), test.signature, source_gas, @@ -340,7 +340,7 @@ fn check(tests: Vec, snaps: Vec, tolerance: Option) -> has_diff = true; } } else { - eprintln!( + let _ = sh_eprintln!( "No matching snapshot entry found for \"{}::{}\" in snapshot file", test.contract_name(), test.signature diff --git a/crates/forge/bin/cmd/test/mod.rs b/crates/forge/bin/cmd/test/mod.rs index cc38694b4c135..0a699db35055f 100644 --- a/crates/forge/bin/cmd/test/mod.rs +++ b/crates/forge/bin/cmd/test/mod.rs @@ -316,6 +316,7 @@ impl Test { } /// Represents the bundled results of all tests +#[derive(Default)] pub struct TestOutcome { /// Whether failures are allowed pub allow_failure: bool, @@ -500,29 +501,29 @@ async fn test( fail_fast: bool, ) -> Result { trace!(target: "forge::test", "running all tests"); + if runner.count_filtered_tests(&filter) == 0 { let filter_str = filter.to_string(); if filter_str.is_empty() { - println!( - "\nNo tests found in project! Forge looks for functions that starts with `test`." - ); + sh_eprintln!("No tests found in project.")?; + sh_note!("Forge looks for functions that start with `test`.")?; } else { - println!("\nNo tests match the provided pattern:"); - println!("{filter_str}"); + sh_eprintln!("No tests match the provided pattern:\n{filter_str}")?; // Try to suggest a test when there's no match - if let Some(ref test_pattern) = filter.args().test_pattern { + if let Some(test_pattern) = &filter.args().test_pattern { let test_name = test_pattern.as_str(); let candidates = runner.get_tests(&filter); - if let Some(suggestion) = utils::did_you_mean(test_name, candidates).pop() { - println!("\nDid you mean `{suggestion}`?"); + if let Some(suggestion) = utils::did_you_mean(test_name, candidates).last() { + sh_note!("\nDid you mean `{suggestion}`?")?; } } } + return Ok(Default::default()) } if json { let results = runner.test(filter, None, test_options).await; - println!("{}", serde_json::to_string(&results)?); + foundry_common::Shell::get().print_json(&results)?; return Ok(TestOutcome::new(results, allow_failure)) } @@ -548,17 +549,19 @@ async fn test( let mut total_failed = 0; let mut total_skipped = 0; + let mut shell = foundry_common::Shell::get(); 'outer: for (contract_name, suite_result) in rx { results.insert(contract_name.clone(), suite_result.clone()); let mut tests = suite_result.test_results.clone(); - println!(); + let _ = shell.print_out("\n"); for warning in suite_result.warnings.iter() { - eprintln!("{} {warning}", Paint::yellow("Warning:").bold()); + let _ = shell.warn(warning); } if !tests.is_empty() { - let term = if tests.len() > 1 { "tests" } else { "test" }; - println!("Running {} {term} for {contract_name}", tests.len()); + let n_tests = tests.len(); + let term = if n_tests > 1 { "tests" } else { "test" }; + let _ = shell.print_out(format!("Running {n_tests} {term} for {contract_name}")); } for (name, result) in &mut tests { short_test_result(name, result); @@ -648,16 +651,18 @@ async fn test( } if gas_reporting { - println!("{}", gas_report.finalize()); + let _ = shell.print_out(gas_report.finalize()); } let num_test_suites = results.len(); if num_test_suites > 0 { - println!( - "{}", - format_aggregated_summary(num_test_suites, total_passed, total_failed, total_skipped) - ); + let _ = shell.print_out(format_aggregated_summary( + num_test_suites, + total_passed, + total_failed, + total_skipped, + )); } // reattach the thread diff --git a/crates/forge/src/multi_runner.rs b/crates/forge/src/multi_runner.rs index 24b06d63f088d..8a9f92e0f974c 100644 --- a/crates/forge/src/multi_runner.rs +++ b/crates/forge/src/multi_runner.rs @@ -70,17 +70,19 @@ impl MultiContractRunner { .count() } - // Get all tests of matching path and contract - pub fn get_tests(&self, filter: &impl TestFilter) -> Vec { + // Returns an iterator over all test function signatures that match the given `filter`. + pub fn get_tests<'a>( + &'a self, + filter: &'a impl TestFilter, + ) -> impl Iterator + 'a { self.contracts .iter() .filter(|(id, _)| { filter.matches_path(id.source.to_string_lossy()) && filter.matches_contract(&id.name) }) - .flat_map(|(_, (abi, _, _))| abi.functions().map(|func| func.name.clone())) + .flat_map(|(_, (abi, _, _))| abi.functions().map(|func| &func.name[..])) .filter(|sig| sig.is_test()) - .collect() } /// Returns all matching tests grouped by contract grouped by file (file -> (contract -> tests)) From 5ed14fbc202ee71b4281ea7324228b3291aeb7e5 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Tue, 29 Aug 2023 21:49:30 +0200 Subject: [PATCH 08/14] replace in forge --- crates/cast/bin/main.rs | 9 +- crates/cli/src/handler.rs | 18 +- crates/cli/src/opts/dependency.rs | 15 +- crates/cli/src/opts/shell.rs | 6 +- crates/common/src/io/macros.rs | 92 +++++- crates/common/src/io/shell.rs | 310 ++++++++++-------- crates/common/src/selectors.rs | 34 +- crates/forge/bin/cmd/bind.rs | 20 +- crates/forge/bin/cmd/config.rs | 3 +- crates/forge/bin/cmd/create.rs | 12 +- crates/forge/bin/cmd/flatten.rs | 13 +- crates/forge/bin/cmd/fourbyte.rs | 6 +- crates/forge/bin/cmd/generate/mod.rs | 3 +- crates/forge/bin/cmd/inspect.rs | 173 ++++------ crates/forge/bin/cmd/install.rs | 35 +- crates/forge/bin/cmd/remappings.rs | 25 +- crates/forge/bin/cmd/remove.rs | 4 +- crates/forge/bin/cmd/script/executor.rs | 8 +- crates/forge/bin/cmd/script/multi.rs | 4 +- crates/forge/bin/cmd/script/receipts.rs | 2 +- crates/forge/bin/cmd/script/runner.rs | 2 +- crates/forge/bin/cmd/script/sequence.rs | 22 +- crates/forge/bin/cmd/selectors.rs | 15 +- crates/forge/bin/cmd/snapshot.rs | 9 +- crates/forge/bin/cmd/test/mod.rs | 74 +++-- .../forge/bin/cmd/verify/etherscan/flatten.rs | 10 +- crates/forge/bin/cmd/verify/etherscan/mod.rs | 65 ++-- .../bin/cmd/verify/etherscan/standard_json.rs | 2 +- crates/forge/bin/cmd/verify/mod.rs | 12 +- crates/forge/bin/cmd/verify/sourcify.rs | 56 ++-- crates/forge/bin/main.rs | 5 +- crates/forge/src/coverage.rs | 29 +- crates/forge/src/gas_report.rs | 4 +- crates/forge/src/lib.rs | 2 + 34 files changed, 555 insertions(+), 544 deletions(-) diff --git a/crates/cast/bin/main.rs b/crates/cast/bin/main.rs index 7f8284c34c971..ba9caa12965d7 100644 --- a/crates/cast/bin/main.rs +++ b/crates/cast/bin/main.rs @@ -39,12 +39,13 @@ async fn main() { async fn run() -> Result<()> { utils::load_dotenv(); - handler::install()?; + handler::install(); utils::subscriber(); utils::enable_paint(); let opts = Opts::parse(); - opts.shell.set_global_shell(); + // SAFETY: See [foundry_common::Shell::set]. + unsafe { opts.shell.shell().set() }; match opts.sub { // Constants Subcommands::MaxInt { r#type } => { @@ -396,10 +397,10 @@ async fn run() -> Result<()> { let signatures = stdin::unwrap_vec(signatures)?; let ParsedSignatures { signatures, abis } = parse_signatures(signatures); if !abis.is_empty() { - import_selectors(SelectorImportData::Abi(abis)).await?.describe(); + import_selectors(SelectorImportData::Abi(abis)).await?.describe()?; } if !signatures.is_empty() { - import_selectors(SelectorImportData::Raw(signatures)).await?.describe(); + import_selectors(SelectorImportData::Raw(signatures)).await?.describe()?; } } diff --git a/crates/cli/src/handler.rs b/crates/cli/src/handler.rs index c1ee8fe29a95f..69063734e54ff 100644 --- a/crates/cli/src/handler.rs +++ b/crates/cli/src/handler.rs @@ -1,4 +1,4 @@ -use eyre::{EyreHandler, Result}; +use eyre::EyreHandler; use std::error::Error; use tracing::error; use yansi::Paint; @@ -49,11 +49,10 @@ impl EyreHandler for Handler { /// /// Panics are always caught by the more debug-centric handler. #[cfg_attr(windows, inline(never))] -pub fn install() -> Result<()> { +pub fn install() { let debug_enabled = std::env::var("FOUNDRY_DEBUG").is_ok(); - if debug_enabled { - color_eyre::install()?; + let _ = color_eyre::install(); } else { let (panic_hook, _) = color_eyre::config::HookBuilder::default() .panic_section( @@ -61,15 +60,8 @@ pub fn install() -> Result<()> { ) .into_hooks(); panic_hook.install(); - // see - if cfg!(windows) { - if let Err(err) = eyre::set_hook(Box::new(move |_| Box::new(Handler))) { - error!(?err, "failed to install panic hook"); - } - } else { - eyre::set_hook(Box::new(move |_| Box::new(Handler)))?; + if let Err(err) = eyre::set_hook(Box::new(move |_| Box::new(Handler))) { + error!(?err, "failed to install panic hook"); } } - - Ok(()) } diff --git a/crates/cli/src/opts/dependency.rs b/crates/cli/src/opts/dependency.rs index ee6b0ee99021f..4f3bbfce558f1 100644 --- a/crates/cli/src/opts/dependency.rs +++ b/crates/cli/src/opts/dependency.rs @@ -3,7 +3,7 @@ use eyre::Result; use once_cell::sync::Lazy; use regex::Regex; -use std::str::FromStr; +use std::{fmt, str::FromStr}; static GH_REPO_REGEX: Lazy = Lazy::new(|| Regex::new(r"[\w-]+/[\w.-]+").unwrap()); @@ -46,6 +46,19 @@ pub struct Dependency { pub alias: Option, } +impl fmt::Display for Dependency { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.name())?; + if let Some(tag) = &self.tag { + write!(f, "{VERSION_SEPARATOR}{tag}")?; + } + if let Some(url) = &self.url { + write!(f, " ({url})")?; + } + Ok(()) + } +} + impl FromStr for Dependency { type Err = eyre::Error; fn from_str(dependency: &str) -> Result { diff --git a/crates/cli/src/opts/shell.rs b/crates/cli/src/opts/shell.rs index 42ff4aaa223b1..8c0c0a10e8683 100644 --- a/crates/cli/src/opts/shell.rs +++ b/crates/cli/src/opts/shell.rs @@ -5,7 +5,7 @@ use foundry_common::shell::{ColorChoice, Shell, Verbosity}; #[derive(Clone, Copy, Debug, Parser)] pub struct ShellOptions { /// Use verbose output. - #[clap(long, short, global = true, conflicts_with = "quiet")] + #[clap(long, global = true, conflicts_with = "quiet")] pub verbose: bool, /// Do not print log messages. @@ -27,8 +27,4 @@ impl ShellOptions { }; Shell::new_with(self.color.unwrap_or_default(), verbosity) } - - pub fn set_global_shell(self) { - self.shell().set(); - } } diff --git a/crates/common/src/io/macros.rs b/crates/common/src/io/macros.rs index cedd3c260d877..f1cfb59b38499 100644 --- a/crates/common/src/io/macros.rs +++ b/crates/common/src/io/macros.rs @@ -31,7 +31,7 @@ macro_rules! prompt { #[macro_export] macro_rules! sh_err { ($($args:tt)*) => { - $crate::Shell::error(&mut *$crate::Shell::get(), ::core::format_args!($($args)*)) + $crate::__sh_dispatch!(error $($args)*) }; } @@ -39,7 +39,7 @@ macro_rules! sh_err { #[macro_export] macro_rules! sh_warn { ($($args:tt)*) => { - $crate::Shell::warn(&mut *$crate::Shell::get(), ::core::format_args!($($args)*)) + $crate::__sh_dispatch!(warn $($args)*) }; } @@ -47,58 +47,114 @@ macro_rules! sh_warn { #[macro_export] macro_rules! sh_note { ($($args:tt)*) => { - $crate::Shell::note(&mut *$crate::Shell::get(), ::core::format_args!($($args)*)) + $crate::__sh_dispatch!(note $($args)*) }; } /// Prints a raw formatted message to stdout. +/// +/// **Note**: This macro is **not** affected by the `--quiet` flag. #[macro_export] macro_rules! sh_print { ($($args:tt)*) => { - $crate::Shell::print_out(&mut *$crate::Shell::get(), ::core::format_args!($($args)*)) + $crate::__sh_dispatch!(print_out $($args)*) }; } /// Prints a raw formatted message to stderr. +/// +/// **Note**: This macro **is** affected by the `--quiet` flag. #[macro_export] macro_rules! sh_eprint { ($($args:tt)*) => { - $crate::Shell::print_err(&mut *$crate::Shell::get(), ::core::format_args!($($args)*)) + $crate::__sh_dispatch!(print_err $($args)*) }; } /// Prints a raw formatted message to stdout, with a trailing newline. +/// +/// **Note**: This macro is **not** affected by the `--quiet` flag. #[macro_export] macro_rules! sh_println { () => { $crate::sh_print!("\n") }; - ($($t:tt)*) => { - $crate::sh_print!("{}\n", ::core::format_args!($($t)*)) + ($fmt:literal $($args:tt)*) => { + $crate::sh_print!("{}\n", ::core::format_args!($fmt $($args)*)) + }; + + ($shell:expr $(,)?) => { + $crate::sh_print!($shell, "\n") + }; + + ($shell:expr, $($args:tt)*) => { + $crate::sh_print!($shell, "{}\n", ::core::format_args!($($args)*)) + }; + + ($($args:tt)*) => { + $crate::sh_print!("{}\n", ::core::format_args!($($args)*)) }; } /// Prints a raw formatted message to stderr, with a trailing newline. +/// +/// **Note**: This macro **is** affected by the `--quiet` flag. #[macro_export] macro_rules! sh_eprintln { () => { $crate::sh_eprint!("\n") }; - ($($t:tt)+) => { - $crate::sh_eprint!("{}\n", ::core::format_args!($($t)+)) + ($fmt:literal $($args:tt)*) => { + $crate::sh_eprint!("{}\n", ::core::format_args!($fmt $($args)*)) + }; + + ($shell:expr $(,)?) => { + $crate::sh_eprint!($shell, "\n") + }; + + ($shell:expr, $($args:tt)*) => { + $crate::sh_eprint!($shell, "{}\n", ::core::format_args!($($args)*)) + }; + + ($($args:tt)*) => { + $crate::sh_eprint!("{}\n", ::core::format_args!($($args)*)) + }; +} + +/// Prints a justified status header with an optional message. +#[macro_export] +macro_rules! sh_status { + ($header:expr) => { + $crate::Shell::status_header(&mut *$crate::Shell::get(), $header) + }; + + ($header:expr => $($args:tt)*) => { + $crate::Shell::status(&mut *$crate::Shell::get(), $header, ::core::format_args!($($args)*)) + }; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! __sh_dispatch { + ($f:ident $fmt:literal $($args:tt)*) => { + $crate::Shell::$f(&mut *$crate::Shell::get(), ::core::format_args!($fmt $($args)*)) + }; + + ($f:ident $shell:expr, $($args:tt)*) => { + $crate::Shell::$f($shell, ::core::format_args!($($args)*)) + }; + + ($f:ident $($args:tt)*) => { + $crate::Shell::$f(&mut *$crate::Shell::get(), ::core::format_args!($($args)*)) }; } #[cfg(test)] mod tests { - use crate::Shell; - #[test] fn macros() { - Shell::new().set(); - sh_err!("err").unwrap(); sh_err!("err {}", "arg").unwrap(); @@ -122,4 +178,14 @@ mod tests { sh_eprintln!("eprintln").unwrap(); sh_eprintln!("eprintln {}", "arg").unwrap(); } + + #[test] + fn macros_with_shell() { + let shell = &mut crate::Shell::new(); + sh_eprintln!(shell).unwrap(); + sh_eprintln!(shell,).unwrap(); + sh_eprintln!(shell, "shelled eprintln").unwrap(); + sh_eprintln!(shell, "shelled eprintln {}", "arg").unwrap(); + sh_eprintln!(&mut crate::Shell::new(), "shelled eprintln {}", "arg").unwrap(); + } } diff --git a/crates/common/src/io/shell.rs b/crates/common/src/io/shell.rs index 89e95096131b5..1b7b98a9bec6a 100644 --- a/crates/common/src/io/shell.rs +++ b/crates/common/src/io/shell.rs @@ -4,19 +4,31 @@ use clap::ValueEnum; use eyre::Result; -use once_cell::sync::Lazy; use std::{ fmt, io::{prelude::*, IsTerminal}, ops::DerefMut, - sync::Mutex, + sync::atomic::{AtomicBool, Ordering}, }; use termcolor::{ Color::{self, Cyan, Green, Red, Yellow}, ColorSpec, StandardStream, WriteColor, }; -static GLOBAL_SHELL: Lazy> = Lazy::new(|| Mutex::new(Shell::new())); +/// The global shell instance. +/// +/// # Safety +/// +/// This instance is only ever initialized in `main`, and its fields are as follows: +/// - `output` +/// - `Stream`'s fields are not modified, and the underlying streams can only be the standard ones +/// which lock on write +/// - `Write` is not thread safe, but it's only used in tests (as of writing, not even there) +/// - `verbosity` cannot modified after initialization +/// - `needs_clear` is an atomic boolean +/// +/// In general this is probably fine. +static mut GLOBAL_SHELL: Option = None; /// Terminal width. pub enum TtyWidth { @@ -89,11 +101,13 @@ pub struct Shell { /// Wrapper around stdout/stderr. This helps with supporting sending /// output to a memory buffer which is useful for tests. output: ShellOut, + /// How verbose messages should be. verbosity: Verbosity, + /// Flag that indicates the current line needs to be cleared before /// printing. Used when a progress bar is currently displayed. - needs_clear: bool, + needs_clear: AtomicBool, } impl fmt::Debug for Shell { @@ -109,8 +123,6 @@ impl fmt::Debug for Shell { /// A `Write`able object, either with or without color support. enum ShellOut { - /// A plain write object without color support. - Write(Box), /// Color-enabled stdio, with information on whether color should be used. Stream { stdout: StandardStream, @@ -118,18 +130,20 @@ enum ShellOut { stderr_tty: bool, color_choice: ColorChoice, }, + /// A plain write object without color support. + Write(Box), } /// Whether messages should use color output. #[derive(Debug, Default, PartialEq, Clone, Copy, ValueEnum)] pub enum ColorChoice { + /// Intelligently guess whether to use color output (default). + #[default] + Auto, /// Force color output. Always, /// Force disable color output. Never, - /// Intelligently guess whether to use color output (default). - #[default] - Auto, } impl Default for Shell { @@ -142,11 +156,13 @@ impl Default for Shell { impl Shell { /// Creates a new shell (color choice and verbosity), defaulting to 'auto' color and verbose /// output. + #[inline] pub fn new() -> Self { Self::new_with(ColorChoice::Auto, Verbosity::Verbose) } /// Creates a new shell with the given color choice and verbosity. + #[inline] pub fn new_with(color: ColorChoice, verbosity: Verbosity) -> Self { Self { output: ShellOut::Stream { @@ -156,60 +172,74 @@ impl Shell { stderr_tty: std::io::stderr().is_terminal(), }, verbosity, - needs_clear: false, + needs_clear: AtomicBool::new(false), } } /// Creates a shell from a plain writable object, with no color, and max verbosity. + /// + /// Not thread safe, so not exposed outside of tests. + #[inline] pub fn from_write(out: Box) -> Self { - Self { output: ShellOut::Write(out), verbosity: Verbosity::Verbose, needs_clear: false } + let needs_clear = AtomicBool::new(false); + Self { output: ShellOut::Write(out), verbosity: Verbosity::Verbose, needs_clear } } /// Get a static reference to the global shell. #[inline] #[track_caller] pub fn get() -> impl DerefMut + 'static { - GLOBAL_SHELL.lock().unwrap() + // SAFETY: See [GLOBAL_SHELL] + match unsafe { &mut GLOBAL_SHELL } { + Some(shell) => shell, + // This shouldn't happen outside of tests + none => { + if cfg!(test) { + none.insert(Self::new()) + } else { + // use `expect` to get `#[cold]` + none.as_mut().expect("attempted to get global shell before it was set") + } + } + } } /// Set the global shell. + /// + /// # Safety + /// + /// See [GLOBAL_SHELL]. #[inline] #[track_caller] - pub fn set(self) { - *GLOBAL_SHELL.lock().unwrap() = self; + pub unsafe fn set(self) { + let shell = unsafe { &mut GLOBAL_SHELL }; + if shell.is_none() { + *shell = Some(self); + } else { + panic!("attempted to set global shell twice"); + } } - /// Prints a message, where the status will have `color` color, and can be justified. The - /// messages follows without color. - fn print( - &mut self, - status: &dyn fmt::Display, - message: Option<&dyn fmt::Display>, - color: Color, - justified: bool, - ) -> Result<()> { - match self.verbosity { - Verbosity::Quiet => Ok(()), - _ => { - if self.needs_clear { - self.err_erase_line(); - } - self.output.message_stderr(status, message, color, justified) - } - } + /// Sets whether the next print should clear the current line and returns the previous value. + #[inline] + pub fn set_needs_clear(&mut self, needs_clear: bool) -> bool { + self.needs_clear.swap(needs_clear, Ordering::Relaxed) } - /// Sets whether the next print should clear the current line. - pub fn set_needs_clear(&mut self, needs_clear: bool) { - self.needs_clear = needs_clear; + /// Returns `true` if the `needs_clear` flag is set. + #[inline] + pub fn needs_clear(&self) -> bool { + self.needs_clear.load(Ordering::Relaxed) } /// Returns `true` if the `needs_clear` flag is unset. + #[inline] pub fn is_cleared(&self) -> bool { - !self.needs_clear + !self.needs_clear() } /// Returns the width of the terminal in spaces, if any. + #[inline] pub fn err_width(&self) -> TtyWidth { match self.output { ShellOut::Stream { stderr_tty: true, .. } => TtyWidth::get(), @@ -217,44 +247,78 @@ impl Shell { } } + /// Gets the verbosity of the shell. + #[inline] + pub fn verbosity(&self) -> Verbosity { + self.verbosity + } + + /// Gets the current color choice. + /// + /// If we are not using a color stream, this will always return `Never`, even if the color + /// choice has been set to something else. + #[inline] + pub fn color_choice(&self) -> ColorChoice { + match self.output { + ShellOut::Stream { color_choice, .. } => color_choice, + ShellOut::Write(_) => ColorChoice::Never, + } + } + /// Returns `true` if stderr is a tty. + #[inline] pub fn is_err_tty(&self) -> bool { match self.output { ShellOut::Stream { stderr_tty, .. } => stderr_tty, - _ => false, + ShellOut::Write(_) => false, + } + } + + /// Whether `stderr` supports color. + #[inline] + pub fn err_supports_color(&self) -> bool { + match &self.output { + ShellOut::Stream { stderr, .. } => stderr.supports_color(), + ShellOut::Write(_) => false, + } + } + + /// Whether `stdout` supports color. + #[inline] + pub fn out_supports_color(&self) -> bool { + match &self.output { + ShellOut::Stream { stdout, .. } => stdout.supports_color(), + ShellOut::Write(_) => false, } } /// Gets a reference to the underlying stdout writer. #[inline] pub fn out(&mut self) -> &mut dyn Write { - if self.needs_clear { - self.err_erase_line(); - } + self.maybe_err_erase_line(); self.output.stdout() } /// Gets a reference to the underlying stderr writer. #[inline] pub fn err(&mut self) -> &mut dyn Write { - if self.needs_clear { - self.err_erase_line(); - } + self.maybe_err_erase_line(); self.output.stderr() } - /// Erase from cursor to end of line. - pub fn err_erase_line(&mut self) { - if self.err_supports_color() { + /// Erase from cursor to end of line if needed. + #[inline] + pub fn maybe_err_erase_line(&mut self) { + if self.err_supports_color() && self.set_needs_clear(false) { // This is the "EL - Erase in Line" sequence. It clears from the cursor // to the end of line. // https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences let _ = self.output.stderr().write_all(b"\x1B[K"); - self.set_needs_clear(false); } } /// Shortcut to right-align and color green a status message. + #[inline] pub fn status(&mut self, status: T, message: U) -> Result<()> where T: fmt::Display, @@ -264,14 +328,13 @@ impl Shell { } /// Shortcut to right-align and color cyan a status without a message. - pub fn status_header(&mut self, status: T) -> Result<()> - where - T: fmt::Display, - { + #[inline] + pub fn status_header(&mut self, status: impl fmt::Display) -> Result<()> { self.print(&status, None, Cyan, true) } /// Shortcut to right-align a status message. + #[inline] pub fn status_with_color(&mut self, status: T, message: U, color: Color) -> Result<()> where T: fmt::Display, @@ -281,10 +344,8 @@ impl Shell { } /// Runs the callback only if we are in verbose mode. - pub fn verbose(&mut self, mut callback: F) -> Result<()> - where - F: FnMut(&mut Shell) -> Result<()>, - { + #[inline] + pub fn verbose(&mut self, mut callback: impl FnMut(&mut Shell) -> Result<()>) -> Result<()> { match self.verbosity { Verbosity::Verbose => callback(self), _ => Ok(()), @@ -292,103 +353,48 @@ impl Shell { } /// Runs the callback if we are not in verbose mode. - pub fn concise(&mut self, mut callback: F) -> Result<()> - where - F: FnMut(&mut Shell) -> Result<()>, - { + #[inline] + pub fn concise(&mut self, mut callback: impl FnMut(&mut Shell) -> Result<()>) -> Result<()> { match self.verbosity { Verbosity::Verbose => Ok(()), _ => callback(self), } } - /// Prints a red 'error' message. - pub fn error(&mut self, message: T) -> Result<()> { - if self.needs_clear { - self.err_erase_line(); - } + /// Prints a red 'error' message. Use the [`sh_err!`] macro instead. + #[inline] + pub fn error(&mut self, message: impl fmt::Display) -> Result<()> { + self.maybe_err_erase_line(); self.output.message_stderr(&"error", Some(&message), Red, false) } - /// Prints an amber 'warning' message. - pub fn warn(&mut self, message: T) -> Result<()> { + /// Prints an amber 'warning' message. Use the [`sh_warn!`] macro instead. + #[inline] + pub fn warn(&mut self, message: impl fmt::Display) -> Result<()> { match self.verbosity { Verbosity::Quiet => Ok(()), _ => self.print(&"warning", Some(&message), Yellow, false), } } - /// Prints a cyan 'note' message. - pub fn note(&mut self, message: T) -> Result<()> { + /// Prints a cyan 'note' message. Use the [`sh_note!`] macro instead. + #[inline] + pub fn note(&mut self, message: impl fmt::Display) -> Result<()> { self.print(&"note", Some(&message), Cyan, false) } - /// Updates the verbosity of the shell. - pub fn set_verbosity(&mut self, verbosity: Verbosity) { - self.verbosity = verbosity; - } - - /// Gets the verbosity of the shell. - pub fn verbosity(&self) -> Verbosity { - self.verbosity - } - - /// Updates the color choice (always, never, or auto) from a string.. - pub fn set_color_choice(&mut self, color: Option<&str>) -> Result<()> { - if let ShellOut::Stream { stdout, stderr, color_choice, .. } = &mut self.output { - let cfg = match color { - Some("always") => ColorChoice::Always, - Some("never") => ColorChoice::Never, - - Some("auto") | None => ColorChoice::Auto, - - Some(arg) => eyre::bail!( - "argument for --color must be auto, always, or \ - never, but found `{arg}`", - ), - }; - *color_choice = cfg; - *stdout = StandardStream::stdout(cfg.to_termcolor_color_choice(Stream::Stdout)); - *stderr = StandardStream::stderr(cfg.to_termcolor_color_choice(Stream::Stderr)); - } - Ok(()) - } - - /// Gets the current color choice. - /// - /// If we are not using a color stream, this will always return `Never`, even if the color - /// choice has been set to something else. - pub fn color_choice(&self) -> ColorChoice { - match self.output { - ShellOut::Stream { color_choice, .. } => color_choice, - ShellOut::Write(_) => ColorChoice::Never, - } - } - - /// Whether `stderr` supports color. - pub fn err_supports_color(&self) -> bool { - match &self.output { - ShellOut::Write(_) => false, - ShellOut::Stream { stderr, .. } => stderr.supports_color(), - } - } - - /// Whether `stdout` supports color. - pub fn out_supports_color(&self) -> bool { - match &self.output { - ShellOut::Write(_) => false, - ShellOut::Stream { stdout, .. } => stdout.supports_color(), - } - } - - /// Write a styled fragment + /// Write a styled fragment. /// /// Caller is responsible for deciding whether [`Shell::verbosity`] is affects output. + #[inline] pub fn write_stdout(&mut self, fragment: impl fmt::Display, color: &ColorSpec) -> Result<()> { self.output.write_stdout(fragment, color) } - /// Write a styled fragment with the default color. + /// Write a styled fragment with the default color. Use the [`sh_print!`] macro instead. + /// + /// **Note**: `verbosity` is ignored. + #[inline] pub fn print_out(&mut self, fragment: impl fmt::Display) -> Result<()> { self.write_stdout(fragment, &ColorSpec::new()) } @@ -396,20 +402,27 @@ impl Shell { /// Write a styled fragment /// /// Caller is responsible for deciding whether [`Shell::verbosity`] is affects output. + #[inline] pub fn write_stderr(&mut self, fragment: impl fmt::Display, color: &ColorSpec) -> Result<()> { self.output.write_stderr(fragment, color) } - /// Write a styled fragment with the default color. + /// Write a styled fragment with the default color. Use the [`sh_eprint!`] macro instead. + /// + /// **Note**: if `verbosity` is set to `Quiet`, this is a no-op. + #[inline] pub fn print_err(&mut self, fragment: impl fmt::Display) -> Result<()> { - self.write_stderr(fragment, &ColorSpec::new()) + if self.verbosity == Verbosity::Quiet { + Ok(()) + } else { + self.write_stderr(fragment, &ColorSpec::new()) + } } /// Prints a message to stderr and translates ANSI escape code into console colors. + #[inline] pub fn print_ansi_stderr(&mut self, message: &[u8]) -> Result<()> { - if self.needs_clear { - self.err_erase_line(); - } + self.maybe_err_erase_line(); #[cfg(windows)] if let ShellOut::Stream { stderr, .. } = &self.output { ::fwdansi::write_ansi(stderr, message)?; @@ -420,10 +433,9 @@ impl Shell { } /// Prints a message to stdout and translates ANSI escape code into console colors. + #[inline] pub fn print_ansi_stdout(&mut self, message: &[u8]) -> Result<()> { - if self.needs_clear { - self.err_erase_line(); - } + self.maybe_err_erase_line(); #[cfg(windows)] if let ShellOut::Stream { stdout, .. } = &self.output { ::fwdansi::write_ansi(stdout, message)?; @@ -434,13 +446,32 @@ impl Shell { } /// Serializes an object to JSON and prints it to `stdout`. - pub fn print_json(&mut self, obj: &T) -> Result<()> { + #[inline] + pub fn print_json(&mut self, obj: &impl serde::Serialize) -> Result<()> { // Path may fail to serialize to JSON ... let encoded = serde_json::to_string(&obj)?; // ... but don't fail due to a closed pipe. let _ = writeln!(self.out(), "{encoded}"); Ok(()) } + + /// Prints a message, where the status will have `color` color, and can be justified. The + /// messages follows without color. + fn print( + &mut self, + status: &dyn fmt::Display, + message: Option<&dyn fmt::Display>, + color: Color, + justified: bool, + ) -> Result<()> { + match self.verbosity { + Verbosity::Quiet => Ok(()), + _ => { + self.maybe_err_erase_line(); + self.output.message_stderr(status, message, color, justified) + } + } + } } impl ShellOut { @@ -472,6 +503,7 @@ impl ShellOut { writeln!(stderr, "{message}")?; } } + Self::Write(w) => { if justified { write!(w, "{status:>12}") } else { write!(w, "{status}:") }?; w.write_all(b" ")?; @@ -492,6 +524,7 @@ impl ShellOut { write!(stdout, "{fragment}")?; stdout.reset()?; } + Self::Write(w) => { write!(w, "{fragment}")?; } @@ -508,6 +541,7 @@ impl ShellOut { write!(stderr, "{fragment}")?; stderr.reset()?; } + Self::Write(w) => { write!(w, "{fragment}")?; } @@ -520,6 +554,7 @@ impl ShellOut { fn stdout(&mut self) -> &mut dyn Write { match self { Self::Stream { stdout, .. } => stdout, + Self::Write(w) => w, } } @@ -529,6 +564,7 @@ impl ShellOut { fn stderr(&mut self) -> &mut dyn Write { match self { Self::Stream { stderr, .. } => stderr, + Self::Write(w) => w, } } diff --git a/crates/common/src/selectors.rs b/crates/common/src/selectors.rs index e87c55148b5e0..0804953b51411 100644 --- a/crates/common/src/selectors.rs +++ b/crates/common/src/selectors.rs @@ -1,6 +1,6 @@ #![allow(missing_docs)] //! Support for handling/identifying selectors -use crate::abi::abi_decode; +use crate::{abi::abi_decode, sh_eprintln, sh_status}; use ethers_solc::artifacts::LosslessAbi; use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; @@ -450,25 +450,21 @@ pub struct SelectorImportResponse { impl SelectorImportResponse { /// Print info about the functions which were uploaded or already known - pub fn describe(&self) { - self.result - .function - .imported - .iter() - .for_each(|(k, v)| println!("Imported: Function {k}: {v}")); - self.result.event.imported.iter().for_each(|(k, v)| println!("Imported: Event {k}: {v}")); - self.result - .function - .duplicated - .iter() - .for_each(|(k, v)| println!("Duplicated: Function {k}: {v}")); - self.result - .event - .duplicated - .iter() - .for_each(|(k, v)| println!("Duplicated: Event {k}: {v}")); + pub fn describe(&self) -> eyre::Result<()> { + for (k, v) in &self.result.function.imported { + sh_status!("Imported" => "Function {k}: {v}")?; + } + for (k, v) in &self.result.event.imported { + sh_status!("Imported" => "Event {k}: {v}")?; + } + for (k, v) in &self.result.function.duplicated { + sh_status!("Duplicated" => "Function {k}: {v}")?; + } + for (k, v) in &self.result.event.duplicated { + sh_status!("Duplicated" => "Event {k}: {v}")?; + } - println!("Selectors successfully uploaded to https://api.openchain.xyz"); + sh_eprintln!("Selectors successfully uploaded to https://api.openchain.xyz") } } diff --git a/crates/forge/bin/cmd/bind.rs b/crates/forge/bin/cmd/bind.rs index 7c0a8748c9172..b1c640ae29e1a 100644 --- a/crates/forge/bin/cmd/bind.rs +++ b/crates/forge/bin/cmd/bind.rs @@ -147,7 +147,7 @@ No contract artifacts found. Hint: Have you built your contracts yet? `forge bin /// Check that the existing bindings match the expected abigen output fn check_existing_bindings(&self, artifacts: impl AsRef) -> Result<()> { let bindings = self.get_multi(&artifacts)?.build()?; - println!("Checking bindings for {} contracts.", bindings.len()); + sh_eprintln!("Checking bindings for {} contracts.", bindings.len())?; if !self.module { bindings.ensure_consistent_crate( &self.crate_name, @@ -159,25 +159,23 @@ No contract artifacts found. Hint: Have you built your contracts yet? `forge bin } else { bindings.ensure_consistent_module(self.bindings_root(&artifacts), self.single_file)?; } - println!("OK."); - Ok(()) + sh_eprintln!("OK.") } /// Generate the bindings fn generate_bindings(&self, artifacts: impl AsRef) -> Result<()> { let bindings = self.get_multi(&artifacts)?.build()?; - println!("Generating bindings for {} contracts", bindings.len()); + sh_println!("Generating bindings for {} contracts", bindings.len())?; if !self.module { bindings.write_to_crate( &self.crate_name, &self.crate_version, self.bindings_root(&artifacts), self.single_file, - )?; + ) } else { - bindings.write_to_module(self.bindings_root(&artifacts), self.single_file)?; + bindings.write_to_module(self.bindings_root(&artifacts), self.single_file) } - Ok(()) } pub fn run(self) -> Result<()> { @@ -190,7 +188,7 @@ No contract artifacts found. Hint: Have you built your contracts yet? `forge bin let artifacts = self.try_load_config_emit_warnings()?.out; if !self.overwrite && self.bindings_exist(&artifacts) { - println!("Bindings found. Checking for consistency."); + sh_eprintln!("Bindings found. Checking for consistency.")?; return self.check_existing_bindings(&artifacts) } @@ -200,10 +198,6 @@ No contract artifacts found. Hint: Have you built your contracts yet? `forge bin self.generate_bindings(&artifacts)?; - println!( - "Bindings have been output to {}", - self.bindings_root(&artifacts).to_str().unwrap() - ); - Ok(()) + sh_eprintln!("Bindings have been written to {}", self.bindings_root(&artifacts).display()) } } diff --git a/crates/forge/bin/cmd/config.rs b/crates/forge/bin/cmd/config.rs index 7cd7ae2b10ec1..c0babae913911 100644 --- a/crates/forge/bin/cmd/config.rs +++ b/crates/forge/bin/cmd/config.rs @@ -54,7 +54,6 @@ impl ConfigArgs { config.to_string_pretty()? }; - println!("{s}"); - Ok(()) + sh_println!("{s}") } } diff --git a/crates/forge/bin/cmd/create.rs b/crates/forge/bin/cmd/create.rs index 8d992b6b72ec4..d5131a25ddf3d 100644 --- a/crates/forge/bin/cmd/create.rs +++ b/crates/forge/bin/cmd/create.rs @@ -270,18 +270,18 @@ impl CreateArgs { "deployedTo": to_checksum(&address, None), "transactionHash": receipt.transaction_hash }); - println!("{output}"); + sh_println!("{output}")?; } else { - println!("Deployer: {}", to_checksum(&deployer_address, None)); - println!("Deployed to: {}", to_checksum(&address, None)); - println!("Transaction hash: {:?}", receipt.transaction_hash); + sh_println!("Deployer: {}", to_checksum(&deployer_address, None))?; + sh_println!("Deployed to: {}", to_checksum(&address, None))?; + sh_println!("Transaction hash: {:?}", receipt.transaction_hash)?; }; if !self.verify { return Ok(()) } - println!("Starting contract verification..."); + sh_println!("Starting contract verification...")?; let num_of_optimizations = if self.opts.compiler.optimize { self.opts.compiler.optimizer_runs } else { None }; @@ -302,7 +302,7 @@ impl CreateArgs { verifier: self.verifier, show_standard_json_input: false, }; - println!("Waiting for {} to detect contract deployment...", verify.verifier.verifier); + sh_println!("Waiting for {} to detect contract deployment...", verify.verifier.verifier)?; verify.run().await } diff --git a/crates/forge/bin/cmd/flatten.rs b/crates/forge/bin/cmd/flatten.rs index 45386bdf2c7a5..81e3af021a3e2 100644 --- a/crates/forge/bin/cmd/flatten.rs +++ b/crates/forge/bin/cmd/flatten.rs @@ -55,19 +55,16 @@ impl FlattenArgs { let paths = config.project_paths(); let target_path = dunce::canonicalize(target_path)?; - let flattened = paths - .flatten(&target_path) - .map_err(|err| eyre::Error::msg(format!("Failed to flatten the file: {err}")))?; + let flattened = + paths.flatten(&target_path).map_err(|err| eyre::eyre!("Failed to flatten: {err}"))?; match output { Some(output) => { fs::create_dir_all(output.parent().unwrap())?; fs::write(&output, flattened)?; - println!("Flattened file written at {}", output.display()); + sh_note!("Wrote flattened file to {}", output.display()) } - None => println!("{flattened}"), - }; - - Ok(()) + None => sh_println!("{flattened}"), + } } } diff --git a/crates/forge/bin/cmd/fourbyte.rs b/crates/forge/bin/cmd/fourbyte.rs index 8a152d28ad585..0bb0b28b534f8 100644 --- a/crates/forge/bin/cmd/fourbyte.rs +++ b/crates/forge/bin/cmd/fourbyte.rs @@ -76,13 +76,13 @@ impl UploadSelectorsArgs { continue } - println!("Uploading selectors for {contract}..."); + sh_status!("Uploading" => "{contract}")?; // upload abi to selector database - import_selectors(SelectorImportData::Abi(vec![abi])).await?.describe(); + import_selectors(SelectorImportData::Abi(vec![abi])).await?.describe()?; if artifacts.peek().is_some() { - println!() + sh_eprintln!()?; } } diff --git a/crates/forge/bin/cmd/generate/mod.rs b/crates/forge/bin/cmd/generate/mod.rs index 9e25d6532a808..24deb890f2894 100644 --- a/crates/forge/bin/cmd/generate/mod.rs +++ b/crates/forge/bin/cmd/generate/mod.rs @@ -44,8 +44,7 @@ impl GenerateTestArgs { // Write the test content to the test file. fs::write(&test_file_path, test_content)?; - println!("{} test file: {}", Paint::green("Generated"), test_file_path.to_str().unwrap()); - Ok(()) + sh_println!("{} test file: {}", Paint::green("Generated"), test_file_path.display()) } } diff --git a/crates/forge/bin/cmd/inspect.rs b/crates/forge/bin/cmd/inspect.rs index edbf779e4f306..d511a4d2d80b0 100644 --- a/crates/forge/bin/cmd/inspect.rs +++ b/crates/forge/bin/cmd/inspect.rs @@ -1,7 +1,7 @@ use clap::Parser; use comfy_table::{presets::ASCII_MARKDOWN, Table}; use ethers::{ - abi::RawAbi, + abi::{ErrorExt, EventExt, RawAbi}, prelude::{ artifacts::output_selection::{ BytecodeOutputSelection, ContractOutputSelection, DeployedBytecodeOutputSelection, @@ -16,9 +16,8 @@ use ethers::{ }; use eyre::Result; use foundry_cli::opts::{CompilerArgs, CoreBuildArgs}; -use foundry_common::compile::ProjectCompiler; -use serde_json::{to_value, Value}; -use std::fmt; +use foundry_common::{compile::ProjectCompiler, Shell}; +use std::{collections::BTreeMap, fmt}; use tracing::trace; /// CLI arguments for `forge inspect`. @@ -76,126 +75,81 @@ impl InspectArgs { let output = compiler.compile(&project)?; // Find the artifact - let found_artifact = output.find_contract(&contract); - - trace!(target: "forge", artifact=?found_artifact, input=?contract, "Found contract"); - - // Unwrap the inner artifact - let artifact = found_artifact.ok_or_else(|| { + let artifact = output.find_contract(&contract).ok_or_else(|| { eyre::eyre!("Could not find artifact `{contract}` in the compiled artifacts") })?; - // Match on ContractArtifactFields and Pretty Print + // Match on ContractArtifactFields and pretty-print match field { ContractArtifactField::Abi => { let abi = artifact .abi .as_ref() .ok_or_else(|| eyre::eyre!("Failed to fetch lossless ABI"))?; - print_abi(abi, pretty)?; + let abi_json = &abi.abi_value; + if pretty { + let abi_json: RawAbi = serde_json::from_value(abi_json.clone())?; + let source: String = foundry_utils::abi::abi_to_solidity(&abi_json, "")?; + Shell::get().write_stdout(source, &Default::default()) + } else { + Shell::get().print_json(abi_json) + }?; } ContractArtifactField::Bytecode => { - let tval: Value = to_value(&artifact.bytecode)?; - println!( - "{}", - tval.get("object").unwrap_or(&tval).as_str().ok_or_else(|| eyre::eyre!( - "Failed to extract artifact bytecode as a string" - ))? - ); + print_json_str(&artifact.bytecode, Some("object"))?; } ContractArtifactField::DeployedBytecode => { - let tval: Value = to_value(&artifact.deployed_bytecode)?; - println!( - "{}", - tval.get("object").unwrap_or(&tval).as_str().ok_or_else(|| eyre::eyre!( - "Failed to extract artifact deployed bytecode as a string" - ))? - ); + print_json_str(&artifact.deployed_bytecode, Some("object"))?; } ContractArtifactField::Assembly | ContractArtifactField::AssemblyOptimized => { - println!( - "{}", - to_value(&artifact.assembly)?.as_str().ok_or_else(|| eyre::eyre!( - "Failed to extract artifact assembly as a string" - ))? - ); + print_json(&artifact.assembly)?; } ContractArtifactField::MethodIdentifiers => { - println!( - "{}", - serde_json::to_string_pretty(&to_value(&artifact.method_identifiers)?)? - ); + print_json(&artifact.method_identifiers)?; } ContractArtifactField::GasEstimates => { - println!("{}", serde_json::to_string_pretty(&to_value(&artifact.gas_estimates)?)?); + print_json(&artifact.gas_estimates)?; } ContractArtifactField::StorageLayout => { print_storage_layout(&artifact.storage_layout, pretty)?; } ContractArtifactField::DevDoc => { - println!("{}", serde_json::to_string_pretty(&to_value(&artifact.devdoc)?)?); + print_json(&artifact.devdoc)?; } ContractArtifactField::Ir => { - println!( - "{}", - to_value(&artifact.ir)? - .as_str() - .ok_or_else(|| eyre::eyre!("Failed to extract artifact ir as a string"))? - ); + print_json(&artifact.ir)?; } ContractArtifactField::IrOptimized => { - println!( - "{}", - to_value(&artifact.ir_optimized)?.as_str().ok_or_else(|| eyre::eyre!( - "Failed to extract artifact optimized ir as a string" - ))? - ); + print_json_str(&artifact.ir_optimized, None)?; } ContractArtifactField::Metadata => { - println!("{}", serde_json::to_string_pretty(&to_value(&artifact.metadata)?)?); + print_json(&artifact.metadata)?; } ContractArtifactField::UserDoc => { - println!("{}", serde_json::to_string_pretty(&to_value(&artifact.userdoc)?)?); + print_json(&artifact.userdoc)?; } ContractArtifactField::Ewasm => { - println!( - "{}", - to_value(&artifact.ewasm)?.as_str().ok_or_else(|| eyre::eyre!( - "Failed to extract artifact ewasm as a string" - ))? - ); + print_json_str(&artifact.ewasm, None)?; } ContractArtifactField::Errors => { - let mut out = serde_json::Map::new(); - if let Some(LosslessAbi { abi, .. }) = &artifact.abi { - // Print the signature of all errors - for er in abi.errors.iter().flat_map(|(_, errors)| errors) { - let types = - er.inputs.iter().map(|p| p.kind.to_string()).collect::>(); - let sig = format!("{:x}", er.signature()); - let sig_trimmed = &sig[0..8]; - out.insert( - format!("{}({})", er.name, types.join(",")), - sig_trimmed.to_string().into(), - ); - } - } - println!("{}", serde_json::to_string_pretty(&out)?); + let Some(LosslessAbi { abi, .. }) = &artifact.abi else { + return sh_println!("{{}}") + }; + let map = abi + .errors() + .map(|error| (error.abi_signature(), hex::encode(error.selector()))) + .collect::>(); + print_json(&map)?; } ContractArtifactField::Events => { - let mut out = serde_json::Map::new(); - if let Some(LosslessAbi { abi, .. }) = &artifact.abi { - // print the signature of all events including anonymous - for ev in abi.events.iter().flat_map(|(_, events)| events) { - let types = - ev.inputs.iter().map(|p| p.kind.to_string()).collect::>(); - out.insert( - format!("{}({})", ev.name, types.join(",")), - format!("{:?}", ev.signature()).into(), - ); - } - } - println!("{}", serde_json::to_string_pretty(&out)?); + let Some(LosslessAbi { abi, .. }) = &artifact.abi else { + return sh_println!("{{}}") + }; + let map = abi + .events() + .map(|event| (event.abi_signature(), hex::encode(event.signature()))) + .collect::>(); + print_json(&map)?; } }; @@ -203,30 +157,13 @@ impl InspectArgs { } } -pub fn print_abi(abi: &LosslessAbi, pretty: bool) -> Result<()> { - let abi_json = to_value(abi)?; - if !pretty { - println!("{}", serde_json::to_string_pretty(&abi_json)?); - return Ok(()) - } - - let abi_json: RawAbi = serde_json::from_value(abi_json)?; - let source = foundry_utils::abi::abi_to_solidity(&abi_json, "")?; - println!("{}", source); - - Ok(()) -} - pub fn print_storage_layout(storage_layout: &Option, pretty: bool) -> Result<()> { - if storage_layout.is_none() { + let Some(storage_layout) = storage_layout.as_ref() else { eyre::bail!("Could not get storage layout") - } - - let storage_layout = storage_layout.as_ref().unwrap(); + }; if !pretty { - println!("{}", serde_json::to_string_pretty(&to_value(storage_layout)?)?); - return Ok(()) + return print_json(&storage_layout) } let mut table = Table::new(); @@ -237,17 +174,15 @@ pub fn print_storage_layout(storage_layout: &Option, pretty: bool let storage_type = storage_layout.types.get(&slot.storage_type); table.add_row(vec![ slot.label.clone(), - storage_type.as_ref().map_or("?".to_string(), |t| t.label.clone()), + storage_type.as_ref().map_or_else(|| "?".into(), |t| t.label.clone()), slot.slot.clone(), slot.offset.to_string(), - storage_type.as_ref().map_or("?".to_string(), |t| t.number_of_bytes.clone()), + storage_type.as_ref().map_or_else(|| "?".into(), |t| t.number_of_bytes.clone()), slot.contract.clone(), ]); } - println!("{table}"); - - Ok(()) + Shell::get().write_stdout(table, &Default::default()) } /// Contract level output selection @@ -420,6 +355,22 @@ impl ContractArtifactField { } } +fn print_json(obj: &impl serde::Serialize) -> Result<()> { + Shell::get().print_json(obj) +} + +fn print_json_str(obj: &impl serde::Serialize, key: Option<&str>) -> Result<()> { + let value = serde_json::to_value(obj)?; + let mut value_ref = &value; + if let Some(key) = key { + if let Some(value2) = value.get(key) { + value_ref = value2; + } + } + let s = value_ref.as_str().ok_or_else(|| eyre::eyre!("not a string: {value}"))?; + sh_println!("{s}") +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/forge/bin/cmd/install.rs b/crates/forge/bin/cmd/install.rs index fa6dbfe0a74eb..65267f75a8788 100644 --- a/crates/forge/bin/cmd/install.rs +++ b/crates/forge/bin/cmd/install.rs @@ -125,16 +125,7 @@ impl DependencyInstallOpts { let rel_path = path .strip_prefix(git.root) .wrap_err("Library directory is not relative to the repository root")?; - foundry_common::Shell::get().status( - "Installing", - format!( - "Installing {} in {} (url: {:?}, tag: {:?})", - dep.name, - path.display(), - dep.url, - dep.tag - ), - )?; + sh_status!("Installing" => "{dep} ({})", path.display())?; // this tracks the actual installed tag let installed_tag; @@ -176,15 +167,7 @@ impl DependencyInstallOpts { } } - let mut shell = foundry_common::Shell::get(); - if !shell.verbosity().is_quiet() { - let mut msg = dep.name; - if let Some(tag) = dep.tag.or(installed_tag) { - msg.push(' '); - msg.push_str(&tag); - } - shell.status("Installed", msg)?; - } + let _ = installed_tag; } // update `libs` in config if not included yet @@ -383,9 +366,9 @@ impl Installer<'_> { // multiple candidates, ask the user to choose one or skip candidates.insert(0, String::from("SKIP AND USE ORIGINAL TAG")); - println!("There are multiple matching tags:"); + sh_println!("There are multiple matching tags:")?; for (i, candidate) in candidates.iter().enumerate() { - println!("[{i}] {candidate}"); + sh_println!("[{i}] {candidate}")?; } let n_candidates = candidates.len(); @@ -400,7 +383,7 @@ impl Installer<'_> { Ok(0) => return Ok(tag.into()), Ok(i) if (1..=n_candidates).contains(&i) => { let c = &candidates[i]; - println!("[{i}] {c} selected"); + sh_println!("[{i}] {c} selected")?; return Ok(c.clone()) } _ => continue, @@ -445,9 +428,9 @@ impl Installer<'_> { // multiple candidates, ask the user to choose one or skip candidates.insert(0, format!("{tag} (original branch)")); - println!("There are multiple matching branches:"); + sh_println!("There are multiple matching branches:")?; for (i, candidate) in candidates.iter().enumerate() { - println!("[{i}] {candidate}"); + sh_println!("[{i}] {candidate}")?; } let n_candidates = candidates.len(); @@ -459,7 +442,7 @@ impl Installer<'_> { // default selection, return None if input.is_empty() { - println!("Canceled branch matching"); + sh_println!("Canceled branch matching")?; return Ok(None) } @@ -468,7 +451,7 @@ impl Installer<'_> { Ok(0) => Ok(Some(tag.into())), Ok(i) if (1..=n_candidates).contains(&i) => { let c = &candidates[i]; - println!("[{i}] {c} selected"); + sh_println!("[{i}] {c} selected")?; Ok(Some(c.clone())) } _ => Ok(None), diff --git a/crates/forge/bin/cmd/remappings.rs b/crates/forge/bin/cmd/remappings.rs index c3d30cf73526e..d83be2f03f8dc 100644 --- a/crates/forge/bin/cmd/remappings.rs +++ b/crates/forge/bin/cmd/remappings.rs @@ -1,5 +1,4 @@ use clap::{Parser, ValueHint}; -use ethers::solc::remappings::RelativeRemapping; use eyre::Result; use foundry_cli::utils::LoadConfig; use foundry_config::impl_figment_convert_basic; @@ -22,34 +21,30 @@ pub struct RemappingArgs { impl_figment_convert_basic!(RemappingArgs); impl RemappingArgs { - // TODO: Do people use `forge remappings >> file`? pub fn run(self) -> Result<()> { let config = self.try_load_config_emit_warnings()?; if self.pretty { - let groups = config.remappings.into_iter().fold( - HashMap::new(), - |mut groups: HashMap, Vec>, remapping| { - groups.entry(remapping.context.clone()).or_default().push(remapping); - groups - }, - ); - for (group, remappings) in groups.into_iter() { + let mut groups = HashMap::<_, Vec<_>>::with_capacity(config.remappings.len()); + for remapping in config.remappings { + groups.entry(remapping.context.clone()).or_default().push(remapping); + } + for (group, remappings) in groups { if let Some(group) = group { - println!("Context: {group}"); + sh_println!("Context: {group}")?; } else { - println!("Global:"); + sh_println!("Global:")?; } for mut remapping in remappings.into_iter() { remapping.context = None; // avoid writing context twice - println!("- {remapping}"); + sh_println!("- {remapping}")?; } - println!(); + sh_println!()?; } } else { for remapping in config.remappings.into_iter() { - println!("{remapping}"); + sh_println!("{remapping}")?; } } diff --git a/crates/forge/bin/cmd/remove.rs b/crates/forge/bin/cmd/remove.rs index f5deb00b2e1fa..0d865e5ef3244 100644 --- a/crates/forge/bin/cmd/remove.rs +++ b/crates/forge/bin/cmd/remove.rs @@ -36,8 +36,8 @@ impl RemoveArgs { Git::new(&root).rm(self.force, &paths)?; // remove all the dependencies from .git/modules - for (Dependency { name, url, tag, .. }, path) in self.dependencies.iter().zip(&paths) { - println!("Removing '{name}' in {}, (url: {url:?}, tag: {tag:?})", path.display()); + for (dep, path) in self.dependencies.iter().zip(&paths) { + sh_status!("Removing" => "{dep} ({})", path.display())?; std::fs::remove_dir_all(git_modules.join(path))?; } diff --git a/crates/forge/bin/cmd/script/executor.rs b/crates/forge/bin/cmd/script/executor.rs index 745cdf8a20ed6..cc25c45102c45 100644 --- a/crates/forge/bin/cmd/script/executor.rs +++ b/crates/forge/bin/cmd/script/executor.rs @@ -108,8 +108,8 @@ impl ScriptArgs { ); if script_config.evm_opts.verbosity > 3 { - println!("=========================="); - println!("Simulated On-chain Traces:\n"); + sh_println!("==========================")?; + sh_println!("Simulated On-chain Traces:\n")?; } let address_to_abi: BTreeMap = decoder @@ -188,7 +188,7 @@ impl ScriptArgs { tx.gas = Some(U256::from(result.gas_used * self.gas_estimate_multiplier / 100)); } else { - println!("Gas limit was set in script to {:}", tx.gas.unwrap()); + sh_println!("Gas limit was set in script to {:}", tx.gas.unwrap())?; } let tx = TransactionWithMetadata::new( @@ -226,7 +226,7 @@ impl ScriptArgs { for (_kind, trace) in &mut traces { decoder.decode(trace).await; - println!("{trace}"); + sh_println!("{trace}")?; } } diff --git a/crates/forge/bin/cmd/script/multi.rs b/crates/forge/bin/cmd/script/multi.rs index 04862b8389481..cb339737259e9 100644 --- a/crates/forge/bin/cmd/script/multi.rs +++ b/crates/forge/bin/cmd/script/multi.rs @@ -102,9 +102,7 @@ impl MultiChainSequence { fs::create_dir_all(file.parent().unwrap())?; fs::copy(&self.path, &file)?; - println!("\nTransactions saved to: {}\n", self.path.display()); - - Ok(()) + sh_println!("\nTransactions saved to: {}\n", self.path.display()) } } diff --git a/crates/forge/bin/cmd/script/receipts.rs b/crates/forge/bin/cmd/script/receipts.rs index 57b531caf9f94..6a9e19490deb9 100644 --- a/crates/forge/bin/cmd/script/receipts.rs +++ b/crates/forge/bin/cmd/script/receipts.rs @@ -38,7 +38,7 @@ pub async fn wait_for_pending( if deployment_sequence.pending.is_empty() { return Ok(()) } - println!("##\nChecking previously pending transactions."); + sh_println!("##\nChecking previously pending transactions.")?; clear_pendings(provider, deployment_sequence, None).await } diff --git a/crates/forge/bin/cmd/script/runner.rs b/crates/forge/bin/cmd/script/runner.rs index 8a50bb21eaf32..7fd4fb931f97b 100644 --- a/crates/forge/bin/cmd/script/runner.rs +++ b/crates/forge/bin/cmd/script/runner.rs @@ -216,7 +216,7 @@ impl ScriptRunner { } Err(EvmError::Execution(err)) => { let ExecutionErr { reason, traces, gas_used, logs, debug, .. } = *err; - println!("{}", Paint::red(format!("\nFailed with `{reason}`:\n"))); + sh_println!("{}", Paint::red(format!("\nFailed with `{reason}`:\n")))?; (Address::zero(), gas_used, logs, traces, debug) } diff --git a/crates/forge/bin/cmd/script/sequence.rs b/crates/forge/bin/cmd/script/sequence.rs index 1ff11aa9758e1..411dbe0cf28f4 100644 --- a/crates/forge/bin/cmd/script/sequence.rs +++ b/crates/forge/bin/cmd/script/sequence.rs @@ -23,7 +23,6 @@ use std::{ path::{Path, PathBuf}, }; use tracing::trace; -use yansi::Paint; pub const DRY_RUN_DIR: &str = "dry-run"; @@ -303,12 +302,12 @@ impl ScriptSequence { self.check_unverified(unverifiable_contracts, verify); let num_verifications = future_verifications.len(); - println!("##\nStart verification for ({num_verifications}) contracts",); + sh_println!("##\nStart verification for ({num_verifications}) contracts")?; for verification in future_verifications { verification.await?; } - println!("All ({num_verifications}) contracts were verified!"); + sh_println!("All ({num_verifications}) contracts were verified!")?; } Ok(()) @@ -318,14 +317,10 @@ impl ScriptSequence { /// hints on potential causes. fn check_unverified(&self, unverifiable_contracts: Vec
, verify: VerifyBundle) { if !unverifiable_contracts.is_empty() { - println!( - "\n{}", - Paint::yellow(format!( - "We haven't found any matching bytecode for the following contracts: {:?}.\n\n{}", - unverifiable_contracts, - "This may occur when resuming a verification, but the underlying source code or compiler version has changed." - )) - .bold(), + let _ = sh_warn!( + "We haven't found any matching bytecode for the following contracts: {unverifiable_contracts:?}.\n\ + This may occur when resuming a verification, \ + but the underlying source code or compiler version has changed.", ); if let Some(commit) = &self.commit { @@ -336,7 +331,10 @@ impl ScriptSequence { .unwrap_or_default(); if ¤t_commit != commit { - println!("\tScript was broadcasted on commit `{commit}`, but we are at `{current_commit}`."); + let _ = sh_println!( + "\tScript was broadcasted on commit `{commit}`, \ + but we are at `{current_commit}`." + ); } } } diff --git a/crates/forge/bin/cmd/selectors.rs b/crates/forge/bin/cmd/selectors.rs index 3ca48cc93225d..3df4a789599b8 100644 --- a/crates/forge/bin/cmd/selectors.rs +++ b/crates/forge/bin/cmd/selectors.rs @@ -96,13 +96,13 @@ impl SelectorsSubcommands { continue } - println!("Uploading selectors for {contract}..."); + sh_status!("Uploading" => "{contract}")?; // upload abi to selector database - import_selectors(SelectorImportData::Abi(vec![abi])).await?.describe(); + import_selectors(SelectorImportData::Abi(vec![abi])).await?.describe()?; if artifacts.peek().is_some() { - println!() + sh_eprintln!()?; } } } @@ -147,7 +147,7 @@ impl SelectorsSubcommands { .collect(); if colliding_methods.is_empty() { - println!("No colliding method selectors between the two contracts."); + sh_println!("No colliding method selectors between the two contracts.")?; } else { let mut table = Table::new(); table.set_header(vec![ @@ -155,11 +155,10 @@ impl SelectorsSubcommands { first_contract.name, second_contract.name, ]); - colliding_methods.iter().for_each(|t| { + for &t in &colliding_methods { table.add_row(vec![t.0, t.1, t.2]); - }); - println!("{} collisions found:", colliding_methods.len()); - println!("{table}"); + } + sh_println!("{} collisions found:\n{table}", colliding_methods.len())?; } } } diff --git a/crates/forge/bin/cmd/snapshot.rs b/crates/forge/bin/cmd/snapshot.rs index 14623c14005e3..f7662a2cf31cb 100644 --- a/crates/forge/bin/cmd/snapshot.rs +++ b/crates/forge/bin/cmd/snapshot.rs @@ -381,21 +381,20 @@ fn diff(tests: Vec, snaps: Vec) -> Result<()> { overall_gas_change += gas_change; overall_gas_used += diff.target_gas_used.gas() as i128; let gas_diff = diff.gas_diff(); - println!( + sh_println!( "{} (gas: {} ({})) ", diff.signature, fmt_change(gas_change), fmt_pct_change(gas_diff) - ); + )?; } let overall_gas_diff = overall_gas_change as f64 / overall_gas_used as f64; - println!( + sh_println!( "Overall gas change: {} ({})", fmt_change(overall_gas_change), fmt_pct_change(overall_gas_diff) - ); - Ok(()) + ) } fn fmt_pct_change(change: f64) -> String { diff --git a/crates/forge/bin/cmd/test/mod.rs b/crates/forge/bin/cmd/test/mod.rs index 0a699db35055f..d3271979450c7 100644 --- a/crates/forge/bin/cmd/test/mod.rs +++ b/crates/forge/bin/cmd/test/mod.rs @@ -329,26 +329,27 @@ impl TestOutcome { Self { results, allow_failure } } - /// Iterator over all succeeding tests and their names + /// Returns an iterator over all succeeding tests and their names. pub fn successes(&self) -> impl Iterator { self.tests().filter(|(_, t)| t.status == TestStatus::Success) } - /// Iterator over all failing tests and their names + /// Returns an iterator over all failing tests and their names. pub fn failures(&self) -> impl Iterator { self.tests().filter(|(_, t)| t.status == TestStatus::Failure) } + /// Returns an iterator over all skipped tests and their names. pub fn skips(&self) -> impl Iterator { self.tests().filter(|(_, t)| t.status == TestStatus::Skipped) } - /// Iterator over all tests and their names + /// Returns an iterator over all tests and their names. pub fn tests(&self) -> impl Iterator { self.results.values().flat_map(|suite| suite.tests()) } - /// Returns an iterator over all `Test` + /// Returns an iterator over all `Test`s. pub fn into_tests(self) -> impl Iterator { self.results .into_iter() @@ -370,8 +371,7 @@ impl TestOutcome { std::process::exit(1); } - println!(); - println!("Failing tests:"); + sh_println!("\nFailing tests:")?; for (suite_name, suite) in self.results.iter() { let failures = suite.failures().count(); if failures == 0 { @@ -379,19 +379,19 @@ impl TestOutcome { } let term = if failures > 1 { "tests" } else { "test" }; - println!("Encountered {failures} failing {term} in {suite_name}"); + sh_println!("Encountered {failures} failing {term} in {suite_name}")?; for (name, result) in suite.failures() { short_test_result(name, result); } - println!(); + sh_println!()?; } let successes = self.successes().count(); - println!( + sh_println!( "Encountered a total of {} failing tests, {} tests succeeded", Paint::red(failures.to_string()), Paint::green(successes.to_string()) - ); + )?; std::process::exit(1); } @@ -446,7 +446,7 @@ fn short_test_result(name: &str, result: &TestResult) { Paint::red(format!("[FAIL. {reason}{counterexample}")) }; - println!("{status} {name} {}", result.kind.report()); + let _ = sh_println!("{status} {name} {}", result.kind.report()); } /** @@ -474,13 +474,13 @@ fn list( let results = runner.list(&filter); if json { - println!("{}", serde_json::to_string(&results)?); + foundry_common::Shell::get().print_json(&results)?; } else { - for (file, contracts) in results.iter() { - println!("{file}"); + for (file, contracts) in &results { + sh_println!("{file}")?; for (contract, tests) in contracts.iter() { - println!(" {contract}"); - println!(" {}\n", tests.join("\n ")); + sh_println!(" {contract}")?; + sh_println!(" {}\n", tests.join("\n "))?; } } } @@ -505,10 +505,10 @@ async fn test( if runner.count_filtered_tests(&filter) == 0 { let filter_str = filter.to_string(); if filter_str.is_empty() { - sh_eprintln!("No tests found in project.")?; + sh_println!("No tests found in project.")?; sh_note!("Forge looks for functions that start with `test`.")?; } else { - sh_eprintln!("No tests match the provided pattern:\n{filter_str}")?; + sh_println!("No tests match the provided pattern:\n{filter_str}")?; // Try to suggest a test when there's no match if let Some(test_pattern) = &filter.args().test_pattern { let test_name = test_pattern.as_str(); @@ -549,19 +549,23 @@ async fn test( let mut total_failed = 0; let mut total_skipped = 0; - let mut shell = foundry_common::Shell::get(); + // skip printing test results if verbosity is quiet + if foundry_common::Shell::get().verbosity().is_quiet() { + return Ok(TestOutcome::new(handle.await?, allow_failure)) + } + 'outer: for (contract_name, suite_result) in rx { results.insert(contract_name.clone(), suite_result.clone()); let mut tests = suite_result.test_results.clone(); - let _ = shell.print_out("\n"); + sh_println!()?; for warning in suite_result.warnings.iter() { - let _ = shell.warn(warning); + sh_warn!("{warning}")?; } if !tests.is_empty() { let n_tests = tests.len(); let term = if n_tests > 1 { "tests" } else { "test" }; - let _ = shell.print_out(format!("Running {n_tests} {term} for {contract_name}")); + sh_println!("Running {n_tests} {term} for {contract_name}")?; } for (name, result) in &mut tests { short_test_result(name, result); @@ -576,11 +580,11 @@ async fn test( // We only decode logs from Hardhat and DS-style console events let console_logs = decode_console_logs(&result.logs); if !console_logs.is_empty() { - println!("Logs:"); + sh_println!("Logs:")?; for log in console_logs { - println!(" {log}"); + sh_println!(" {log}")?; } - println!(); + sh_println!()?; } } @@ -633,8 +637,10 @@ async fn test( } if !decoded_traces.is_empty() { - println!("Traces:"); - decoded_traces.into_iter().for_each(|trace| println!("{trace}")); + sh_println!("Traces:")?; + for trace in decoded_traces { + sh_println!(" {trace}")?; + } } if gas_reporting { @@ -647,22 +653,20 @@ async fn test( total_failed += block_outcome.failures().count(); total_skipped += block_outcome.skips().count(); - println!("{}", block_outcome.summary()); + sh_println!("{}", block_outcome.summary())?; } if gas_reporting { - let _ = shell.print_out(gas_report.finalize()); + sh_println!("{}", gas_report.finalize())?; } let num_test_suites = results.len(); if num_test_suites > 0 { - let _ = shell.print_out(format_aggregated_summary( - num_test_suites, - total_passed, - total_failed, - total_skipped, - )); + sh_println!( + "{}", + format_aggregated_summary(num_test_suites, total_passed, total_failed, total_skipped,) + )?; } // reattach the thread diff --git a/crates/forge/bin/cmd/verify/etherscan/flatten.rs b/crates/forge/bin/cmd/verify/etherscan/flatten.rs index fb32057924b0b..2af3219765688 100644 --- a/crates/forge/bin/cmd/verify/etherscan/flatten.rs +++ b/crates/forge/bin/cmd/verify/etherscan/flatten.rs @@ -82,16 +82,16 @@ impl EtherscanFlattenedSource { if out.has_error() { let mut o = AggregatedCompilerOutput::default(); o.extend(version, out); - eprintln!("{}", o.diagnostics(&[], Default::default())); + let diags = o.diagnostics(&[], Default::default()); - eprintln!( - r#"Failed to compile the flattened code locally. + eyre::bail!( + "\ +Failed to compile the flattened code locally. This could be a bug, please inspect the output of `forge flatten {}` and report an issue. To skip this solc dry, pass `--force`. -"#, +Diagnostics: {diags}", contract_path.display() ); - std::process::exit(1) } Ok(()) diff --git a/crates/forge/bin/cmd/verify/etherscan/mod.rs b/crates/forge/bin/cmd/verify/etherscan/mod.rs index b0d5edbe4e16e..a0a163902deb7 100644 --- a/crates/forge/bin/cmd/verify/etherscan/mod.rs +++ b/crates/forge/bin/cmd/verify/etherscan/mod.rs @@ -11,7 +11,7 @@ use ethers::{ solc::{artifacts::CompactContract, cache::CacheEntry, Project, Solc}, utils::to_checksum, }; -use eyre::{eyre, Context, Result}; +use eyre::{bail, eyre, Context, Result}; use foundry_cli::utils::{get_cached_entry_by_name, read_constructor_args_file, LoadConfig}; use foundry_common::abi::encode_args; use foundry_config::{Chain, Config, SolcReq}; @@ -63,49 +63,50 @@ impl VerificationProvider for EtherscanVerificationProvider { let (etherscan, verify_args) = self.prepare_request(&args).await?; if self.is_contract_verified(ðerscan, &verify_args).await? { - println!( + return sh_println!( "\nContract [{}] {:?} is already verified. Skipping verification.", verify_args.contract_name, to_checksum(&verify_args.address, None) - ); - - return Ok(()) + ) } - trace!(target : "forge::verify", ?verify_args, "submitting verification request"); + trace!(target: "forge::verify", ?verify_args, "submitting verification request"); let retry: Retry = args.retry.into(); let resp = retry.run_async(|| { async { - println!("\nSubmitting verification for [{}] {:?}.", verify_args.contract_name, to_checksum(&verify_args.address, None)); + sh_println!( + "\nSubmitting verification for [{}] {:?}.", + verify_args.contract_name, + to_checksum(&verify_args.address, None) + )?; let resp = etherscan .submit_contract_verification(&verify_args) .await .wrap_err_with(|| { // valid json let args = serde_json::to_string(&verify_args).unwrap(); - error!(target : "forge::verify", ?args, "Failed to submit verification"); + error!(target: "forge::verify", ?args, "Failed to submit verification"); format!("Failed to submit contract verification, payload:\n{args}") })?; - trace!(target : "forge::verify", ?resp, "Received verification response"); + trace!(target: "forge::verify", ?resp, "Received verification response"); if resp.status == "0" { + tracing::debug!("{resp:?}"); + if resp.result == "Contract source code already verified" { return Ok(None) } if resp.result.starts_with("Unable to locate ContractCode at") { - warn!("{}", resp.result); - return Err(eyre!("Etherscan could not detect the deployment.")) + bail!("Etherscan could not detect the deployment.") } - warn!("Failed verify submission: {:?}", resp); - eprintln!( + bail!( "Encountered an error verifying this contract:\nResponse: `{}`\nDetails: `{}`", resp.message, resp.result ); - std::process::exit(1); } Ok(Some(resp)) @@ -114,13 +115,12 @@ impl VerificationProvider for EtherscanVerificationProvider { }).await?; if let Some(resp) = resp { - println!( - "Submitted contract for verification:\n\tResponse: `{}`\n\tGUID: `{}`\n\tURL: - {}", + sh_println!( + "Submitted contract for verification:\n\tResponse: `{}`\n\tGUID: `{}`\n\tURL: {}", resp.message, resp.result, etherscan.address_url(args.address) - ); + )?; if args.watch { let check_args = VerifyCheckArgs { @@ -133,7 +133,7 @@ impl VerificationProvider for EtherscanVerificationProvider { return self.check(check_args).await } } else { - println!("Contract source code already verified"); + sh_println!("Contract source code already verified")?; } Ok(()) @@ -157,33 +157,32 @@ impl VerificationProvider for EtherscanVerificationProvider { .await .wrap_err("Failed to request verification status")?; - trace!(target : "forge::verify", ?resp, "Received verification response"); + trace!(target: "forge::verify", ?resp, "Received verification response"); - eprintln!( + sh_println!( "Contract verification status:\nResponse: `{}`\nDetails: `{}`", - resp.message, resp.result - ); + resp.message, + resp.result + )?; + + if resp.status == "0" { + bail!("Contract failed to verify.") + } if resp.result == "Pending in queue" { - return Err(eyre!("Verification is still pending...",)) + bail!("Verification is still pending...") } if resp.result == "Unable to verify" { - return Err(eyre!("Unable to verify.",)) + bail!("Unable to verify.") } if resp.result == "Already Verified" { - println!("Contract source code already verified"); - return Ok(()) - } - - if resp.status == "0" { - println!("Contract failed to verify."); - std::process::exit(1); + return sh_println!("Contract source code already verified") } if resp.result == "Pass - Verified" { - println!("Contract successfully verified"); + return sh_println!("Contract successfully verified") } Ok(()) diff --git a/crates/forge/bin/cmd/verify/etherscan/standard_json.rs b/crates/forge/bin/cmd/verify/etherscan/standard_json.rs index f79c76461abfd..056a431ae6752 100644 --- a/crates/forge/bin/cmd/verify/etherscan/standard_json.rs +++ b/crates/forge/bin/cmd/verify/etherscan/standard_json.rs @@ -36,7 +36,7 @@ impl EtherscanSourceProvider for EtherscanStandardJsonSource { let source = serde_json::to_string(&input).wrap_err("Failed to parse standard json input")?; - trace!(target : "forge::verify", standard_json = source, "determined standard json input"); + trace!(target: "forge::verify", standard_json = source, "determined standard json input"); let name = format!( "{}:{}", diff --git a/crates/forge/bin/cmd/verify/mod.rs b/crates/forge/bin/cmd/verify/mod.rs index bd17d1418b9b7..1ef0a62983b0c 100644 --- a/crates/forge/bin/cmd/verify/mod.rs +++ b/crates/forge/bin/cmd/verify/mod.rs @@ -134,19 +134,18 @@ impl VerifyArgs { if self.show_standard_json_input { let args = EtherscanVerificationProvider::default().create_verify_request(&self, None).await?; - println!("{}", args.source); - return Ok(()) + return sh_println!("{}", args.source) } let verifier_url = self.verifier.verifier_url.clone(); - println!("Start verifying contract `{:?}` deployed on {chain}", self.address); + sh_println!("Start verifying contract `{:?}` deployed on {chain}", self.address)?; self.verifier.verifier.client(&self.etherscan.key)?.verify(self).await.map_err(|err| { if let Some(verifier_url) = verifier_url { match Url::parse(&verifier_url) { Ok(url) => { if is_host_only(&url) { return err.wrap_err(format!( - "Provided URL `{verifier_url}` is host only.\n Did you mean to use the API endpoint`{verifier_url}/api` ?" + "Provided URL `{verifier_url}` is host only.\nDid you mean to use the API endpoint`{verifier_url}/api`?" )) } } @@ -193,7 +192,10 @@ impl_figment_convert_cast!(VerifyCheckArgs); impl VerifyCheckArgs { /// Run the verify command to submit the contract's source code for verification on etherscan pub async fn run(self) -> Result<()> { - println!("Checking verification status on {}", self.etherscan.chain.unwrap_or_default()); + sh_println!( + "Checking verification status on {}", + self.etherscan.chain.unwrap_or_default() + )?; self.verifier.verifier.client(&self.etherscan.key)?.check(self).await } } diff --git a/crates/forge/bin/cmd/verify/sourcify.rs b/crates/forge/bin/cmd/verify/sourcify.rs index 30a5139ba61ad..3bad9502e68a9 100644 --- a/crates/forge/bin/cmd/verify/sourcify.rs +++ b/crates/forge/bin/cmd/verify/sourcify.rs @@ -8,7 +8,7 @@ use foundry_utils::Retry; use futures::FutureExt; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, path::PathBuf}; -use tracing::{trace, warn}; +use tracing::trace; pub static SOURCIFY_URL: &str = "https://sourcify.dev/server/"; @@ -35,11 +35,11 @@ impl VerificationProvider for SourcifyVerificationProvider { let resp = retry .run_async(|| { async { - println!( + sh_println!( "\nSubmitting verification for [{}] {:?}.", args.contract.name, to_checksum(&args.address, None) - ); + )?; let response = client .post(args.verifier.verifier_url.as_deref().unwrap_or(SOURCIFY_URL)) .header("Content-Type", "application/json") @@ -50,14 +50,10 @@ impl VerificationProvider for SourcifyVerificationProvider { let status = response.status(); if !status.is_success() { let error: serde_json::Value = response.json().await?; - eprintln!( - "Sourcify verification request for address ({}) failed with status code {}\nDetails: {:#}", - format_args!("{:?}", args.address), - status, - error + eyre::bail!( + "Sourcify verification request for address ({:?}) failed with status code {status}\nDetails: {error:#}", + args.address, ); - warn!("Failed verify submission: {:?}", error); - std::process::exit(1); } let text = response.text().await?; @@ -67,8 +63,7 @@ impl VerificationProvider for SourcifyVerificationProvider { }) .await?; - self.process_sourcify_response(resp.map(|r| r.result)); - Ok(()) + self.process_sourcify_response(resp.map(|r| r.result)) } async fn check(&self, args: VerifyCheckArgs) -> Result<()> { @@ -85,11 +80,10 @@ impl VerificationProvider for SourcifyVerificationProvider { let response = reqwest::get(url).await?; if !response.status().is_success() { - eprintln!( + eyre::bail!( "Failed to request verification status with status code {}", response.status() ); - std::process::exit(1); }; Ok(Some(response.json::>().await?)) @@ -98,8 +92,7 @@ impl VerificationProvider for SourcifyVerificationProvider { }) .await?; - self.process_sourcify_response(resp); - Ok(()) + self.process_sourcify_response(resp) } } @@ -167,21 +160,24 @@ metadata output can be enabled via `extra_output = ["metadata"]` in `foundry.tom Ok(req) } - fn process_sourcify_response(&self, response: Option>) { - let response = response.unwrap().remove(0); - if response.status == "perfect" { - if let Some(ts) = response.storage_timestamp { - println!("Contract source code already verified. Storage Timestamp: {ts}"); - } else { - println!("Contract successfully verified") + fn process_sourcify_response( + &self, + response: Option>, + ) -> Result<()> { + let Some([response, ..]) = response.as_deref() else { return Ok(()) }; + match response.status.as_str() { + "perfect" => { + if let Some(ts) = &response.storage_timestamp { + sh_println!("Contract source code already verified. Storage Timestamp: {ts}") + } else { + sh_println!("Contract successfully verified") + } } - } else if response.status == "partial" { - println!("The recompiled contract partially matches the deployed version") - } else if response.status == "false" { - println!("Contract source code is not verified") - } else { - eprintln!("Unknown status from sourcify. Status: {}", response.status); - std::process::exit(1); + "partial" => { + sh_println!("The recompiled contract partially matches the deployed version") + } + "false" => sh_println!("Contract source code is not verified"), + s => Err(eyre::eyre!("Unknown status from sourcify. Status: {s:?}")), } } } diff --git a/crates/forge/bin/main.rs b/crates/forge/bin/main.rs index bf4b151829433..12cc3f0959183 100644 --- a/crates/forge/bin/main.rs +++ b/crates/forge/bin/main.rs @@ -21,12 +21,13 @@ fn main() { fn run() -> Result<()> { utils::load_dotenv(); - handler::install()?; + handler::install(); utils::subscriber(); utils::enable_paint(); let opts = Opts::parse(); - opts.shell.set_global_shell(); + // SAFETY: See [foundry_common::Shell::set]. + unsafe { opts.shell.shell().set() }; match opts.sub { Subcommands::Test(cmd) => { if cmd.is_watch() { diff --git a/crates/forge/src/coverage.rs b/crates/forge/src/coverage.rs index d9cfabaff900b..240125e9dc37d 100644 --- a/crates/forge/src/coverage.rs +++ b/crates/forge/src/coverage.rs @@ -45,8 +45,7 @@ impl CoverageReporter for SummaryReporter { } self.add_row("Total", self.total.clone()); - println!("{}", self.table); - Ok(()) + sh_println!("{}", self.table) } } @@ -128,9 +127,7 @@ impl<'a> CoverageReporter for LcovReporter<'a> { writeln!(self.destination, "end_of_record")?; } - println!("Wrote LCOV report."); - - Ok(()) + sh_println!("Wrote LCOV report.") } } @@ -140,29 +137,27 @@ pub struct DebugReporter; impl CoverageReporter for DebugReporter { fn report(self, report: &CoverageReport) -> eyre::Result<()> { for (path, items) in report.items_by_source() { - println!("Uncovered for {path}:"); - items.iter().for_each(|item| { + sh_println!("\nUncovered for {path}:")?; + for item in items { if item.hits == 0 { - println!("- {item}"); + sh_println!("- {item}")?; } - }); - println!(); + } } for (contract_id, anchors) in &report.anchors { - println!("Anchors for {contract_id}:"); - anchors.iter().for_each(|anchor| { - println!("- {anchor}"); - println!( + sh_println!("\nAnchors for {contract_id}:")?; + for anchor in anchors { + sh_println!("- {anchor}")?; + sh_println!( " - Refers to item: {}", report .items .get(&contract_id.version) .and_then(|items| items.get(anchor.item_id)) .map_or("None".to_owned(), |item| item.to_string()) - ); - }); - println!(); + )?; + } } Ok(()) diff --git a/crates/forge/src/gas_report.rs b/crates/forge/src/gas_report.rs index d610a19190a68..6497cc9e42219 100644 --- a/crates/forge/src/gas_report.rs +++ b/crates/forge/src/gas_report.rs @@ -51,14 +51,14 @@ impl GasReport { } if let Some(name) = &trace.contract { - let contract_name = name.rsplit(':').next().unwrap_or(name.as_str()).to_string(); + let contract_name = name.rsplit(':').next().unwrap().to_string(); // If the user listed the contract in 'gas_reports' (the foundry.toml field) a // report for the contract is generated even if it's listed in the ignore // list. This is addressed this way because getting a report you don't expect is // preferable than not getting one you expect. A warning is printed to stderr // indicating the "double listing". if self.report_for.contains(&contract_name) && self.ignore.contains(&contract_name) { - eprintln!( + let _ = sh_eprintln!( "{}: {} is listed in both 'gas_reports' and 'gas_reports_ignore'.", yansi::Paint::yellow("warning").bold(), contract_name diff --git a/crates/forge/src/lib.rs b/crates/forge/src/lib.rs index 99af3a7e9841c..cacf0e49fc261 100644 --- a/crates/forge/src/lib.rs +++ b/crates/forge/src/lib.rs @@ -7,6 +7,8 @@ use foundry_config::{ use proptest::test_runner::{RngAlgorithm, TestRng, TestRunner}; use std::path::Path; +#[macro_use] +extern crate foundry_common; #[macro_use] extern crate tracing; From b25951b61c6401f36f418302b7329931a33825c2 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Tue, 29 Aug 2023 22:01:14 +0200 Subject: [PATCH 09/14] fix verbosity --- crates/cli/src/utils/mod.rs | 6 +++--- crates/forge/bin/cmd/install.rs | 2 +- crates/forge/bin/cmd/remove.rs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/cli/src/utils/mod.rs b/crates/cli/src/utils/mod.rs index 10fa3ef010d58..47bace0bddc5c 100644 --- a/crates/cli/src/utils/mod.rs +++ b/crates/cli/src/utils/mod.rs @@ -496,10 +496,10 @@ https://github.com/foundry-rs/foundry/issues/new/choose" // don't set this in cmd() because it's not wanted for all commands fn stderr() -> Stdio { - if foundry_common::Shell::get().verbosity().is_verbose() { - Stdio::inherit() - } else { + if foundry_common::Shell::get().verbosity().is_quiet() { Stdio::piped() + } else { + Stdio::inherit() } } } diff --git a/crates/forge/bin/cmd/install.rs b/crates/forge/bin/cmd/install.rs index 65267f75a8788..5142e0d9b24ae 100644 --- a/crates/forge/bin/cmd/install.rs +++ b/crates/forge/bin/cmd/install.rs @@ -125,7 +125,7 @@ impl DependencyInstallOpts { let rel_path = path .strip_prefix(git.root) .wrap_err("Library directory is not relative to the repository root")?; - sh_status!("Installing" => "{dep} ({})", path.display())?; + sh_status!("Installing" => "{dep} to {}", path.display())?; // this tracks the actual installed tag let installed_tag; diff --git a/crates/forge/bin/cmd/remove.rs b/crates/forge/bin/cmd/remove.rs index 0d865e5ef3244..3b4dd772523ac 100644 --- a/crates/forge/bin/cmd/remove.rs +++ b/crates/forge/bin/cmd/remove.rs @@ -37,7 +37,7 @@ impl RemoveArgs { // remove all the dependencies from .git/modules for (dep, path) in self.dependencies.iter().zip(&paths) { - sh_status!("Removing" => "{dep} ({})", path.display())?; + sh_status!("Removing" => "{dep} from {}", path.display())?; std::fs::remove_dir_all(git_modules.join(path))?; } From 772ab977f4004751d05c5530875652a1a7f48d67 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Tue, 29 Aug 2023 22:52:50 +0200 Subject: [PATCH 10/14] replace everywhere except cast --- Cargo.lock | 1 + crates/abi/build.rs | 2 ++ crates/anvil/core/src/lib.rs | 2 ++ crates/anvil/rpc/src/lib.rs | 2 ++ crates/anvil/server/src/lib.rs | 1 + crates/anvil/src/lib.rs | 2 ++ crates/binder/src/lib.rs | 2 ++ crates/binder/src/utils.rs | 5 ++- crates/cast/bin/main.rs | 3 ++ crates/chisel/bin/main.rs | 2 ++ crates/chisel/src/lib.rs | 6 ++-- crates/cli/src/utils/cmd.rs | 22 ++++++------ crates/common/src/abi.rs | 4 +-- crates/common/src/compile.rs | 21 ++++++----- crates/common/src/io/mod.rs | 1 + crates/common/src/lib.rs | 4 ++- crates/common/src/selectors.rs | 24 +++++++------ crates/common/src/term.rs | 6 +++- crates/config/src/lib.rs | 1 + crates/doc/Cargo.toml | 1 + crates/doc/src/builder.rs | 3 +- crates/doc/src/lib.rs | 9 +++-- crates/doc/src/server.rs | 2 +- crates/evm/src/executor/inspector/printer.rs | 6 ++-- crates/evm/src/lib.rs | 2 ++ crates/evm/src/trace/identifier/etherscan.rs | 16 ++++----- crates/test-utils/src/lib.rs | 1 + crates/ui/src/lib.rs | 37 ++++++++++++++++---- 28 files changed, 120 insertions(+), 68 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d3d4336fc44e5..3d673233fe307 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2361,6 +2361,7 @@ dependencies = [ "ethers-solc", "eyre", "forge-fmt", + "foundry-common", "foundry-config", "foundry-utils", "futures-util", diff --git a/crates/abi/build.rs b/crates/abi/build.rs index 9817d2deb30fe..d0e6aaea27cf4 100644 --- a/crates/abi/build.rs +++ b/crates/abi/build.rs @@ -1,3 +1,5 @@ +#![allow(clippy::disallowed_macros)] + use ethers_contract_abigen::MultiAbigen; /// Includes a JSON ABI as a string literal. diff --git a/crates/anvil/core/src/lib.rs b/crates/anvil/core/src/lib.rs index a09e7cd684321..46dfe2f4517d8 100644 --- a/crates/anvil/core/src/lib.rs +++ b/crates/anvil/core/src/lib.rs @@ -1,3 +1,5 @@ +#![allow(clippy::disallowed_macros)] + /// Various Ethereum types pub mod eth; diff --git a/crates/anvil/rpc/src/lib.rs b/crates/anvil/rpc/src/lib.rs index 939fbff1690b1..190be422bea00 100644 --- a/crates/anvil/rpc/src/lib.rs +++ b/crates/anvil/rpc/src/lib.rs @@ -1,3 +1,5 @@ +#![allow(clippy::disallowed_macros)] + /// JSON-RPC request bindings pub mod request; diff --git a/crates/anvil/server/src/lib.rs b/crates/anvil/server/src/lib.rs index 3cd044c94b375..48fc965742065 100644 --- a/crates/anvil/server/src/lib.rs +++ b/crates/anvil/server/src/lib.rs @@ -1,5 +1,6 @@ //! Bootstrap [axum] RPC servers +#![allow(clippy::disallowed_macros)] #![deny(missing_docs, unsafe_code, unused_crate_dependencies)] use anvil_rpc::{ diff --git a/crates/anvil/src/lib.rs b/crates/anvil/src/lib.rs index 17005bb598e06..d07aeec0d53f0 100644 --- a/crates/anvil/src/lib.rs +++ b/crates/anvil/src/lib.rs @@ -1,3 +1,5 @@ +#![allow(clippy::disallowed_macros)] + use crate::{ eth::{ backend::{info::StorageInfo, mem}, diff --git a/crates/binder/src/lib.rs b/crates/binder/src/lib.rs index e57d8c95a7cf9..b02c1c56829ec 100644 --- a/crates/binder/src/lib.rs +++ b/crates/binder/src/lib.rs @@ -1,5 +1,7 @@ //! Generate [ethers-rs]("https://github.com/gakonst/ethers-rs") bindings for solidity projects in a build script. +#![allow(clippy::disallowed_macros)] + use crate::utils::{GitReference, GitRemote}; use ethers_contract::MultiAbigen; pub use foundry_config::Config; diff --git a/crates/binder/src/utils.rs b/crates/binder/src/utils.rs index 1d73e942b56c3..89d0dff665a2a 100644 --- a/crates/binder/src/utils.rs +++ b/crates/binder/src/utils.rs @@ -396,13 +396,12 @@ impl Retry { pub fn r#try(&mut self, f: impl FnOnce() -> eyre::Result) -> eyre::Result> { match f() { - Err(ref e) if maybe_spurious(e) && self.remaining > 0 => { - let msg = format!( + Err(e) if maybe_spurious(&e) && self.remaining > 0 => { + println!( "spurious network error ({} tries remaining): {}", self.remaining, e.root_cause(), ); - println!("{msg}"); self.remaining -= 1; Ok(None) } diff --git a/crates/cast/bin/main.rs b/crates/cast/bin/main.rs index ba9caa12965d7..68e266921cc7b 100644 --- a/crates/cast/bin/main.rs +++ b/crates/cast/bin/main.rs @@ -1,3 +1,6 @@ +// TODO +#![allow(clippy::disallowed_macros)] + use cast::{Cast, SimpleCast}; use clap::{CommandFactory, Parser}; use clap_complete::generate; diff --git a/crates/chisel/bin/main.rs b/crates/chisel/bin/main.rs index 55de1f0649edb..b8bfd846dcefa 100644 --- a/crates/chisel/bin/main.rs +++ b/crates/chisel/bin/main.rs @@ -3,6 +3,8 @@ //! This module contains the core readline loop for the Chisel CLI as well as the //! executable's `main` function. +#![allow(clippy::disallowed_macros)] + use chisel::{ history::chisel_history_file, prelude::{ChiselCommand, ChiselDispatcher, DispatchResult, SolidityHelper}, diff --git a/crates/chisel/src/lib.rs b/crates/chisel/src/lib.rs index c5668909ff750..328461b9a2029 100644 --- a/crates/chisel/src/lib.rs +++ b/crates/chisel/src/lib.rs @@ -1,8 +1,6 @@ #![doc = include_str!("../README.md")] -#![warn(missing_docs)] -#![warn(unused_extern_crates)] -#![forbid(unsafe_code)] -#![forbid(where_clauses_object_safety)] +#![allow(clippy::disallowed_macros)] +#![warn(missing_docs, unused_extern_crates)] /// REPL input dispatcher module pub mod dispatcher; diff --git a/crates/cli/src/utils/cmd.rs b/crates/cli/src/utils/cmd.rs index 5134fe6cc1a4c..35c3f7ef1ed6b 100644 --- a/crates/cli/src/utils/cmd.rs +++ b/crates/cli/src/utils/cmd.rs @@ -124,9 +124,8 @@ pub fn needs_setup(abi: &Abi) -> bool { for setup_fn in setup_fns.iter() { if setup_fn.name != "setUp" { - println!( - "{} Found invalid setup function \"{}\" did you mean \"setUp()\"?", - Paint::yellow("Warning:").bold(), + let _ = sh_warn!( + "Found invalid setup function \"{}\" did you mean \"setUp()\"?", setup_fn.signature() ); } @@ -415,25 +414,24 @@ pub async fn print_traces( panic!("No traces found") } - println!("Traces:"); + sh_println!("Traces:")?; for (_, trace) in &mut result.traces { decoder.decode(trace).await; - if !verbose { - println!("{trace}"); + if verbose { + sh_println!("{trace:#}")?; } else { - println!("{trace:#}"); + sh_println!("{trace}")?; } } - println!(); + sh_println!()?; if result.success { - println!("{}", Paint::green("Transaction successfully executed.")); + sh_println!("{}", Paint::green("Transaction successfully executed."))?; } else { - println!("{}", Paint::red("Transaction failed.")); + sh_println!("{}", Paint::red("Transaction failed."))?; } - println!("Gas used: {}", result.gas_used); - Ok(()) + sh_println!("Gas used: {}", result.gas_used) } pub fn run_debugger( diff --git a/crates/common/src/abi.rs b/crates/common/src/abi.rs index d934d816c521d..26ad448451ecb 100644 --- a/crates/common/src/abi.rs +++ b/crates/common/src/abi.rs @@ -340,9 +340,9 @@ pub fn find_source( Ok(source) } else { let implementation = metadata.implementation.unwrap(); - println!( + sh_note!( "Contract at {address} is a proxy, trying to fetch source at {implementation:?}..." - ); + )?; match find_source(client, implementation).await { impl_source @ Ok(_) => impl_source, Err(e) => { diff --git a/crates/common/src/compile.rs b/crates/common/src/compile.rs index fb549e7e2aeb4..df125c5028e0d 100644 --- a/crates/common/src/compile.rs +++ b/crates/common/src/compile.rs @@ -177,8 +177,9 @@ impl ProjectCompiler { where F: FnOnce() -> Result, { + // TODO: Avoid process::exit if !project.paths.has_input_files() { - println!("Nothing to compile"); + sh_eprintln!("Nothing to compile")?; // nothing to do here std::process::exit(0); } @@ -211,10 +212,10 @@ impl ProjectCompiler { if !quiet { if output.is_unchanged() { - println!("No files changed, compilation skipped"); + sh_println!("No files changed, compilation skipped")?; } else { // print the compiler output / warnings - println!("{output}"); + sh_println!("{output}")?; } self.handle_output(&output); @@ -235,12 +236,14 @@ impl ProjectCompiler { artifacts.entry(version).or_default().push(name); } for (version, names) in artifacts { - println!( + let _ = sh_println!( " compiler version: {}.{}.{}", - version.major, version.minor, version.patch + version.major, + version.minor, + version.patch ); for name in names { - println!(" - {name}"); + let _ = sh_println!(" - {name}"); } } } @@ -248,8 +251,9 @@ impl ProjectCompiler { if print_sizes { // add extra newline if names were already printed if print_names { - println!(); + let _ = sh_println!(); } + let mut size_report = SizeReport { contracts: BTreeMap::new() }; let artifacts: BTreeMap<_, _> = output.artifacts().collect(); for (name, artifact) in artifacts { @@ -269,8 +273,9 @@ impl ProjectCompiler { size_report.contracts.insert(name, ContractInfo { size, is_dev_contract }); } - println!("{size_report}"); + let _ = sh_println!("{size_report}"); + // TODO: avoid process::exit // exit with error if any contract exceeds the size limit, excluding test contracts. if size_report.exceeds_size_limit() { std::process::exit(1); diff --git a/crates/common/src/io/mod.rs b/crates/common/src/io/mod.rs index 09c66824a6989..de81f70cef746 100644 --- a/crates/common/src/io/mod.rs +++ b/crates/common/src/io/mod.rs @@ -1,5 +1,6 @@ //! Utilities for working with standard input, output, and error. +#[macro_use] mod macros; pub mod shell; diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index f68be64de99ed..6124c61e9be87 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -2,6 +2,9 @@ #![warn(missing_docs, unused_crate_dependencies)] +#[macro_use] +pub mod io; + pub mod abi; pub mod calc; pub mod compile; @@ -12,7 +15,6 @@ pub mod evm; pub mod fmt; pub mod fs; pub mod glob; -pub mod io; pub mod provider; pub mod selectors; pub mod term; diff --git a/crates/common/src/selectors.rs b/crates/common/src/selectors.rs index 0804953b51411..3511b280291e0 100644 --- a/crates/common/src/selectors.rs +++ b/crates/common/src/selectors.rs @@ -1,6 +1,7 @@ -#![allow(missing_docs)] //! Support for handling/identifying selectors -use crate::{abi::abi_decode, sh_eprintln, sh_status}; +#![allow(missing_docs)] + +use crate::abi::abi_decode; use ethers_solc::artifacts::LosslessAbi; use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; @@ -240,13 +241,14 @@ impl SignEthClient { /// Pretty print calldata and if available, fetch possible function signatures /// - /// ```no_run - /// + /// ``` /// use foundry_common::selectors::SignEthClient; /// /// # async fn foo() -> eyre::Result<()> { - /// let pretty_data = SignEthClient::new()?.pretty_calldata("0x70a08231000000000000000000000000d0074f4e6490ae3f888d1d4f7e3e43326bd3f0f5".to_string(), false).await?; - /// println!("{}",pretty_data); + /// SignEthClient::new()?.pretty_calldata( + /// "0x70a08231000000000000000000000000d0074f4e6490ae3f888d1d4f7e3e43326bd3f0f5", + /// false, + /// ).await?; /// # Ok(()) /// # } /// ``` @@ -387,13 +389,14 @@ pub async fn decode_event_topic(topic: &str) -> eyre::Result> { /// Pretty print calldata and if available, fetch possible function signatures /// -/// ```no_run -/// +/// ``` /// use foundry_common::selectors::pretty_calldata; /// /// # async fn foo() -> eyre::Result<()> { -/// let pretty_data = pretty_calldata("0x70a08231000000000000000000000000d0074f4e6490ae3f888d1d4f7e3e43326bd3f0f5".to_string(), false).await?; -/// println!("{}",pretty_data); +/// pretty_calldata( +/// "0x70a08231000000000000000000000000d0074f4e6490ae3f888d1d4f7e3e43326bd3f0f5", +/// false, +/// ).await?; /// # Ok(()) /// # } /// ``` @@ -579,7 +582,6 @@ mod tests { let abi: LosslessAbi = serde_json::from_str(r#"[{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function", "methodIdentifiers": {"transfer(address,uint256)(uint256)": "0xa9059cbb"}}]"#).unwrap(); let result = import_selectors(SelectorImportData::Abi(vec![abi])).await; - println!("{:?}", result); assert_eq!( result.unwrap().result.function.duplicated.get("transfer(address,uint256)").unwrap(), "0xa9059cbb" diff --git a/crates/common/src/term.rs b/crates/common/src/term.rs index 3c8009a94d13c..a334b322e9384 100644 --- a/crates/common/src/term.rs +++ b/crates/common/src/term.rs @@ -1,4 +1,8 @@ -//! terminal utils +//! Terminal utils. + +// TODO +#![allow(clippy::disallowed_macros)] + use ethers_solc::{ remappings::Remapping, report::{self, BasicStdoutReporter, Reporter, SolcCompilerIoReporter}, diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index a3cac6b3e18bf..70ed478124ffd 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -1,5 +1,6 @@ //! Foundry configuration. +#![allow(clippy::disallowed_macros)] #![warn(missing_docs, unused_crate_dependencies)] use crate::cache::StorageCachingConfig; diff --git a/crates/doc/Cargo.toml b/crates/doc/Cargo.toml index b189f3732f69e..76052b5b42919 100644 --- a/crates/doc/Cargo.toml +++ b/crates/doc/Cargo.toml @@ -13,6 +13,7 @@ repository.workspace = true [dependencies] # foundry internal forge-fmt.workspace = true +foundry-common.workspace = true foundry-config.workspace = true foundry-utils.workspace = true diff --git a/crates/doc/src/builder.rs b/crates/doc/src/builder.rs index d015c21d379b9..a1658f8360342 100644 --- a/crates/doc/src/builder.rs +++ b/crates/doc/src/builder.rs @@ -95,8 +95,7 @@ impl DocBuilder { .collect::>(); if sources.is_empty() { - println!("No sources detected at {}", self.sources.display()); - return Ok(()) + return sh_println!("No sources detected at {}", self.sources.display()) } let documents = sources diff --git a/crates/doc/src/lib.rs b/crates/doc/src/lib.rs index d629283b55723..e1947ff2a5d69 100644 --- a/crates/doc/src/lib.rs +++ b/crates/doc/src/lib.rs @@ -1,3 +1,7 @@ +//! The module for generating Solidity documentation. +//! +//! See [DocBuilder] + #![warn(missing_debug_implementations, missing_docs, unreachable_pub, unused_crate_dependencies)] #![deny(unused_must_use, rust_2018_idioms)] #![doc(test( @@ -5,9 +9,8 @@ attr(deny(warnings, rust_2018_idioms), allow(dead_code, unused_variables)) ))] -//! The module for generating Solidity documentation. -//! -//! See [DocBuilder] +#[macro_use] +extern crate foundry_common; mod builder; mod document; diff --git a/crates/doc/src/server.rs b/crates/doc/src/server.rs index bd2cc801a927b..4f845d706d0f6 100644 --- a/crates/doc/src/server.rs +++ b/crates/doc/src/server.rs @@ -74,7 +74,7 @@ impl Server { // A channel used to broadcast to any websockets to reload when a file changes. let (tx, _rx) = tokio::sync::broadcast::channel::(100); - println!("Serving on: http://{address}"); + sh_println!("Serving on: http://{address}")?; serve(build_dir, sockaddr, tx, &file_404); Ok(()) } diff --git a/crates/evm/src/executor/inspector/printer.rs b/crates/evm/src/executor/inspector/printer.rs index 0f8d9127bf87b..38e6161b3ae1a 100644 --- a/crates/evm/src/executor/inspector/printer.rs +++ b/crates/evm/src/executor/inspector/printer.rs @@ -16,7 +16,7 @@ impl Inspector for TracePrinter { let opcode = interp.current_opcode(); let opcode_str = opcode::OPCODE_JUMPMAP[opcode as usize]; let gas_remaining = interp.gas.remaining(); - println!( + let _ = sh_println!( "depth:{}, PC:{}, gas:{:#x}({}), OPCODE: {:?}({:?}) refund:{:#x}({}) Stack:{:?}, Data size:{}, Data: 0x{}", data.journaled_state.depth(), interp.program_counter(), @@ -39,7 +39,7 @@ impl Inspector for TracePrinter { _data: &mut EVMData<'_, DB>, inputs: &mut CallInputs, ) -> (InstructionResult, Gas, Bytes) { - println!( + let _ = sh_println!( "SM CALL: {:?},context:{:?}, is_static:{:?}, transfer:{:?}, input_size:{:?}", inputs.contract, inputs.context, @@ -55,7 +55,7 @@ impl Inspector for TracePrinter { _data: &mut EVMData<'_, DB>, inputs: &mut CreateInputs, ) -> (InstructionResult, Option, Gas, Bytes) { - println!( + let _ = sh_println!( "CREATE CALL: caller:{:?}, scheme:{:?}, value:{:?}, init_code:{:?}, gas:{:?}", inputs.caller, inputs.scheme, diff --git a/crates/evm/src/lib.rs b/crates/evm/src/lib.rs index 1d4d04ce89ad1..1d745a4049680 100644 --- a/crates/evm/src/lib.rs +++ b/crates/evm/src/lib.rs @@ -1,5 +1,7 @@ #![warn(unused_crate_dependencies)] +#[macro_use] +extern crate foundry_common; #[macro_use] extern crate tracing; diff --git a/crates/evm/src/trace/identifier/etherscan.rs b/crates/evm/src/trace/identifier/etherscan.rs index b9bdf43feb100..0e8e420ddd102 100644 --- a/crates/evm/src/trace/identifier/etherscan.rs +++ b/crates/evm/src/trace/identifier/etherscan.rs @@ -72,17 +72,13 @@ impl EtherscanIdentifier { // filter out vyper files .filter(|(_, metadata)| !metadata.is_vyper()); - let outputs_fut = contracts_iter - .clone() - .map(|(address, metadata)| { - println!("Compiling: {} {address:?}", metadata.contract_name); - let err_msg = format!( - "Failed to compile contract {} from {address:?}", - metadata.contract_name - ); - compile::compile_from_source(metadata).map_err(move |err| err.wrap_err(err_msg)) + let outputs_fut = contracts_iter.clone().map(|(address, metadata)| { + let name = &metadata.contract_name; + let _ = sh_status!("Compiling" => "{name} ({address:?})"); + compile::compile_from_source(metadata).map_err(move |e| { + e.wrap_err(format!("Failed to compile contract {name} from {address:?}")) }) - .collect::>(); + }); // poll all the futures concurrently let artifacts = join_all(outputs_fut).await; diff --git a/crates/test-utils/src/lib.rs b/crates/test-utils/src/lib.rs index 1cdbe23c96df6..1ca818ff408a5 100644 --- a/crates/test-utils/src/lib.rs +++ b/crates/test-utils/src/lib.rs @@ -1,3 +1,4 @@ +#![allow(clippy::disallowed_macros)] #![warn(unused_crate_dependencies)] // Macros useful for testing. diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index ca13218fa726f..e477d1ec73f40 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -29,12 +29,15 @@ use revm::{interpreter::opcode, primitives::SpecId}; use std::{ cmp::{max, min}, collections::{BTreeMap, HashMap, VecDeque}, - io, + io, panic, sync::mpsc, thread, time::{Duration, Instant}, }; +#[macro_use] +extern crate foundry_common; + /// Trait for starting the UI pub trait Ui { /// Start the agent that will now take over @@ -987,12 +990,8 @@ impl Ui for Tui { fn start(mut self) -> Result { // If something panics inside here, we should do everything we can to // not corrupt the user's terminal. - std::panic::set_hook(Box::new(|e| { - disable_raw_mode().expect("Unable to disable raw mode"); - execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture) - .expect("unable to execute disable mouse capture"); - println!("{e}"); - })); + let _hook = PanicHook::new(); + // This is the recommend tick rate from tui-rs, based on their examples let tick_rate = Duration::from_millis(200); @@ -1307,6 +1306,30 @@ impl Ui for Tui { } } +struct PanicHook { + previous: Option) + 'static + Sync + Send>>, +} + +impl PanicHook { + fn new() -> Self { + let previous = panic::take_hook(); + panic::set_hook(Box::new(|e| { + let _ = disable_raw_mode(); + let _ = execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture); + let _ = sh_println!("{e}"); + })); + Self { previous: Some(previous) } + } +} + +impl Drop for PanicHook { + fn drop(&mut self) { + if let Some(previous) = self.previous.take() { + panic::set_hook(previous); + } + } +} + /// Why did we wake up drawing thread? enum Interrupt { KeyPressed(KeyEvent), From 83b7c60fc37b0b597044aa6519b6e6114a295da2 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Tue, 29 Aug 2023 23:14:06 +0200 Subject: [PATCH 11/14] chore: disallow print macros --- clippy.toml | 8 ++++++++ crates/anvil/tests/it/main.rs | 2 ++ crates/binder/src/lib.rs | 3 ++- crates/cast/benches/vanity.rs | 1 + crates/cast/src/rlp_converter.rs | 2 -- crates/cast/tests/cli/main.rs | 2 ++ crates/chisel/bin/main.rs | 4 ++-- crates/cli/src/opts/wallet/multi_wallet.rs | 2 +- crates/common/src/compile.rs | 1 + crates/common/src/io/macros.rs | 2 +- crates/common/src/io/shell.rs | 6 ++++-- crates/forge/bin/cmd/cache.rs | 3 +-- crates/forge/bin/cmd/fmt.rs | 2 +- crates/forge/bin/cmd/geiger/mod.rs | 6 ++++-- crates/forge/tests/cli/main.rs | 2 ++ crates/forge/tests/it/main.rs | 2 ++ crates/ui/src/lib.rs | 1 + 17 files changed, 35 insertions(+), 14 deletions(-) diff --git a/clippy.toml b/clippy.toml index ebba0354acd0b..97df7b37156ed 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1 +1,9 @@ msrv = "1.72" + +disallowed-macros = [ + # See `foundry_common::shell` + { path = "std::print", reason = "use `sh_print` or similar macros instead" }, + { path = "std::eprint", reason = "use `sh_eprint` or similar macros instead" }, + { path = "std::println", reason = "use `sh_println` or similar macros instead" }, + { path = "std::eprintln", reason = "use `sh_eprintln` or similar macros instead" }, +] diff --git a/crates/anvil/tests/it/main.rs b/crates/anvil/tests/it/main.rs index cd99a9f15a011..041a85ecfd64b 100644 --- a/crates/anvil/tests/it/main.rs +++ b/crates/anvil/tests/it/main.rs @@ -1,3 +1,5 @@ +#![allow(clippy::disallowed_macros)] + mod abi; mod anvil; mod anvil_api; diff --git a/crates/binder/src/lib.rs b/crates/binder/src/lib.rs index b02c1c56829ec..abbedd9a08a80 100644 --- a/crates/binder/src/lib.rs +++ b/crates/binder/src/lib.rs @@ -1,4 +1,5 @@ -//! Generate [ethers-rs]("https://github.com/gakonst/ethers-rs") bindings for solidity projects in a build script. +//! Generate [ethers-rs](https://github.com/gakonst/ethers-rs) bindings for solidity projects in a +//! build script. #![allow(clippy::disallowed_macros)] diff --git a/crates/cast/benches/vanity.rs b/crates/cast/benches/vanity.rs index 4311b5283a74e..43b57411c6ae8 100644 --- a/crates/cast/benches/vanity.rs +++ b/crates/cast/benches/vanity.rs @@ -4,6 +4,7 @@ use std::{hint::black_box, time::Duration}; #[path = "../bin/cmd/wallet/mod.rs"] #[allow(unused)] +#[allow(clippy::all)] mod wallet; use wallet::vanity::*; diff --git a/crates/cast/src/rlp_converter.rs b/crates/cast/src/rlp_converter.rs index 0ca8597d4e23e..6223b3fda224a 100644 --- a/crates/cast/src/rlp_converter.rs +++ b/crates/cast/src/rlp_converter.rs @@ -134,7 +134,6 @@ mod test { assert_eq!(rlp::decode::(&encoded)?, params.2); let decoded = rlp::decode::(¶ms.1); assert_eq!(rlp::encode::(&decoded?), params.1); - println!("case {} validated", params.0) } Ok(()) @@ -164,7 +163,6 @@ mod test { let val = serde_json::from_str(params.1)?; let item = Item::value_to_item(&val).unwrap(); assert_eq!(item, params.2); - println!("case {} validated", params.0); } Ok(()) diff --git a/crates/cast/tests/cli/main.rs b/crates/cast/tests/cli/main.rs index 9eff595b69695..59554e24a2690 100644 --- a/crates/cast/tests/cli/main.rs +++ b/crates/cast/tests/cli/main.rs @@ -1,5 +1,7 @@ //! Contains various tests for checking cast commands +#![allow(clippy::disallowed_macros)] + use foundry_test_utils::{ casttest, util::{OutputExt, TestCommand, TestProject}, diff --git a/crates/chisel/bin/main.rs b/crates/chisel/bin/main.rs index 62ca6712ca0a4..a5cfbd53a64f4 100644 --- a/crates/chisel/bin/main.rs +++ b/crates/chisel/bin/main.rs @@ -1,7 +1,7 @@ //! Chisel CLI //! -//! This module contains the core readline loop for the Chisel CLI as well as the -//! executable's `main` function. +//! This module contains the core readline loop for the Chisel CLI as well as the executable's +//! `main` function. #![allow(clippy::disallowed_macros)] diff --git a/crates/cli/src/opts/wallet/multi_wallet.rs b/crates/cli/src/opts/wallet/multi_wallet.rs index 2158a8c1b8f23..5e0266cce6c0a 100644 --- a/crates/cli/src/opts/wallet/multi_wallet.rs +++ b/crates/cli/src/opts/wallet/multi_wallet.rs @@ -213,7 +213,7 @@ impl MultiWallet { mut addresses: HashSet
, script_wallets: &[LocalWallet], ) -> Result> { - println!("\n###\nFinding wallets for all the necessary addresses..."); + sh_println!("\n###\nFinding wallets for all the necessary addresses...")?; let chain = provider.get_chainid().await?.as_u64(); let mut local_wallets = HashMap::new(); diff --git a/crates/common/src/compile.rs b/crates/common/src/compile.rs index df125c5028e0d..0ef4ba50d6448 100644 --- a/crates/common/src/compile.rs +++ b/crates/common/src/compile.rs @@ -185,6 +185,7 @@ impl ProjectCompiler { } let quiet = self.quiet.unwrap_or(false); + #[allow(clippy::collapsible_else_if)] let reporter = if quiet { Report::new(NoReporter::default()) } else { diff --git a/crates/common/src/io/macros.rs b/crates/common/src/io/macros.rs index f1cfb59b38499..8dd9acdaf7cb5 100644 --- a/crates/common/src/io/macros.rs +++ b/crates/common/src/io/macros.rs @@ -19,7 +19,7 @@ macro_rules! prompt { }; ($($tt:tt)+) => {{ - ::std::print!($($tt)+); + let _ = $crate::sh_print!($($tt)+); match ::std::io::Write::flush(&mut ::std::io::stdout()) { ::core::result::Result::Ok(()) => $crate::prompt!(), ::core::result::Result::Err(e) => ::core::result::Result::Err(::eyre::eyre!("Could not flush stdout: {e}")) diff --git a/crates/common/src/io/shell.rs b/crates/common/src/io/shell.rs index 1b7b98a9bec6a..753a52aeb7310 100644 --- a/crates/common/src/io/shell.rs +++ b/crates/common/src/io/shell.rs @@ -45,6 +45,7 @@ impl TtyWidth { pub fn get() -> Self { // use stderr #[cfg(unix)] + #[allow(clippy::useless_conversion)] let opt = terminal_size::terminal_size_using_fd(2.into()); #[cfg(not(unix))] let opt = terminal_size::terminal_size(); @@ -520,7 +521,7 @@ impl ShellOut { match self { Self::Stream { stdout, .. } => { stdout.reset()?; - stdout.set_color(&color)?; + stdout.set_color(color)?; write!(stdout, "{fragment}")?; stdout.reset()?; } @@ -537,7 +538,7 @@ impl ShellOut { match self { Self::Stream { stderr, .. } => { stderr.reset()?; - stderr.set_color(&color)?; + stderr.set_color(color)?; write!(stderr, "{fragment}")?; stderr.reset()?; } @@ -587,6 +588,7 @@ impl ColorChoice { } } +#[derive(Clone, Copy)] enum Stream { Stdout, Stderr, diff --git a/crates/forge/bin/cmd/cache.rs b/crates/forge/bin/cmd/cache.rs index e70a2c66f2c83..19508c318d8f9 100644 --- a/crates/forge/bin/cmd/cache.rs +++ b/crates/forge/bin/cmd/cache.rs @@ -101,8 +101,7 @@ impl LsArgs { ChainOrAll::All => cache = Config::list_foundry_cache()?, } } - print!("{cache}"); - Ok(()) + sh_print!("{cache}") } } diff --git a/crates/forge/bin/cmd/fmt.rs b/crates/forge/bin/cmd/fmt.rs index 686ffbeaf2d0f..1a2565a125fe9 100644 --- a/crates/forge/bin/cmd/fmt.rs +++ b/crates/forge/bin/cmd/fmt.rs @@ -129,7 +129,7 @@ impl FmtArgs { if self.check || path.is_none() { if self.raw { - print!("{output}"); + sh_print!("{output}")?; } let diff = TextDiff::from_lines(&source, &output); diff --git a/crates/forge/bin/cmd/geiger/mod.rs b/crates/forge/bin/cmd/geiger/mod.rs index 1584353de91db..817c0464106e6 100644 --- a/crates/forge/bin/cmd/geiger/mod.rs +++ b/crates/forge/bin/cmd/geiger/mod.rs @@ -100,9 +100,11 @@ impl GeigerArgs { .map(|file| match find_cheatcodes_in_file(file) { Ok(metrics) => { let len = metrics.cheatcodes.len(); - let printer = SolFileMetricsPrinter { metrics: &metrics, root: &root }; if self.full || len == 0 { - eprint!("{printer}"); + let _ = sh_eprint!( + "{}", + SolFileMetricsPrinter { metrics: &metrics, root: &root } + ); } len } diff --git a/crates/forge/tests/cli/main.rs b/crates/forge/tests/cli/main.rs index 69868c93a7c8e..b53b835779a88 100644 --- a/crates/forge/tests/cli/main.rs +++ b/crates/forge/tests/cli/main.rs @@ -1,3 +1,5 @@ +#![allow(clippy::disallowed_macros)] + pub mod constants; pub mod utils; diff --git a/crates/forge/tests/it/main.rs b/crates/forge/tests/it/main.rs index f8197c99d42a5..ec8dafd0707e6 100644 --- a/crates/forge/tests/it/main.rs +++ b/crates/forge/tests/it/main.rs @@ -1,3 +1,5 @@ +#![allow(clippy::disallowed_macros)] + mod cheats; pub mod config; mod core; diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index e477d1ec73f40..c0e9e3881b0e5 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -1306,6 +1306,7 @@ impl Ui for Tui { } } +#[allow(clippy::type_complexity)] // standard panic handler type struct PanicHook { previous: Option) + 'static + Sync + Send>>, } From 62286323fa15ffea20228dc9bcc3520408e4dc38 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Wed, 30 Aug 2023 01:06:10 +0200 Subject: [PATCH 12/14] fixes --- Cargo.lock | 16 ++++++++-------- crates/cli/src/opts/shell.rs | 4 +++- crates/common/src/io/macros.rs | 3 ++- crates/common/src/selectors.rs | 9 ++++----- crates/forge/bin/cmd/init.rs | 30 ++++++++++++++++++------------ crates/forge/bin/cmd/install.rs | 2 +- 6 files changed, 36 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f660182b8bb21..a1c72563adeca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,9 +62,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6748e8def348ed4d14996fa801f4122cd763fff530258cdc03f64b25f89d3a5a" +checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783" dependencies = [ "memchr", ] @@ -669,9 +669,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05" +checksum = "8042c26c77e5bd6897a7358e0abb3ec412ed126d826988135653fc669263899d" dependencies = [ "memchr", "regex-automata 0.3.7", @@ -1449,9 +1449,9 @@ dependencies = [ [[package]] name = "dashmap" -version = "5.5.2" +version = "5.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b101bb8960ab42ada6ae98eb82afcea4452294294c45b681295af26610d6d28" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", "hashbrown 0.14.0", @@ -3972,9 +3972,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76fc44e2588d5b436dbc3c6cf62aef290f90dab6235744a93dfe1cc18f451e2c" +checksum = "f478948fd84d9f8e86967bf432640e46adfb5a4bd4f14ef7e864ab38220534ae" [[package]] name = "memmap2" diff --git a/crates/cli/src/opts/shell.rs b/crates/cli/src/opts/shell.rs index 8c0c0a10e8683..3854d66b7c721 100644 --- a/crates/cli/src/opts/shell.rs +++ b/crates/cli/src/opts/shell.rs @@ -1,6 +1,8 @@ use clap::Parser; use foundry_common::shell::{ColorChoice, Shell, Verbosity}; +// note: `verbose` and `quiet` cannot have `short` because of conflicts with multiple commands. + /// Global shell options. #[derive(Clone, Copy, Debug, Parser)] pub struct ShellOptions { @@ -9,7 +11,7 @@ pub struct ShellOptions { pub verbose: bool, /// Do not print log messages. - #[clap(long, short, global = true, alias = "silent", conflicts_with = "verbose")] + #[clap(long, global = true, alias = "silent", conflicts_with = "verbose")] pub quiet: bool, /// Log messages coloring. diff --git a/crates/common/src/io/macros.rs b/crates/common/src/io/macros.rs index 8dd9acdaf7cb5..e024debd3db47 100644 --- a/crates/common/src/io/macros.rs +++ b/crates/common/src/io/macros.rs @@ -5,7 +5,8 @@ /// # Examples /// /// ```no_run -/// # use foundry_cli::prompt; +/// use foundry_common::prompt; +/// /// let response: String = prompt!("Would you like to continue? [y/N] ")?; /// if !matches!(response.as_str(), "y" | "Y") { /// return Ok(()) diff --git a/crates/common/src/selectors.rs b/crates/common/src/selectors.rs index 3511b280291e0..7de2e76803d60 100644 --- a/crates/common/src/selectors.rs +++ b/crates/common/src/selectors.rs @@ -400,7 +400,6 @@ pub async fn decode_event_topic(topic: &str) -> eyre::Result> { /// # Ok(()) /// # } /// ``` - pub async fn pretty_calldata( calldata: impl AsRef, offline: bool, @@ -455,16 +454,16 @@ impl SelectorImportResponse { /// Print info about the functions which were uploaded or already known pub fn describe(&self) -> eyre::Result<()> { for (k, v) in &self.result.function.imported { - sh_status!("Imported" => "Function {k}: {v}")?; + sh_status!("Imported" => "function {k}: {v}")?; } for (k, v) in &self.result.event.imported { - sh_status!("Imported" => "Event {k}: {v}")?; + sh_status!("Imported" => "event {k}: {v}")?; } for (k, v) in &self.result.function.duplicated { - sh_status!("Duplicated" => "Function {k}: {v}")?; + sh_status!("Duplicated" => "function {k}: {v}")?; } for (k, v) in &self.result.event.duplicated { - sh_status!("Duplicated" => "Event {k}: {v}")?; + sh_status!("Duplicated" => "event {k}: {v}")?; } sh_eprintln!("Selectors successfully uploaded to https://api.openchain.xyz") diff --git a/crates/forge/bin/cmd/init.rs b/crates/forge/bin/cmd/init.rs index 6d9bf1d1aba15..62e97041bb5d6 100644 --- a/crates/forge/bin/cmd/init.rs +++ b/crates/forge/bin/cmd/init.rs @@ -5,8 +5,10 @@ use eyre::Result; use foundry_cli::utils::Git; use foundry_common::fs; use foundry_config::Config; -use std::path::{Path, PathBuf}; -use yansi::Paint; +use std::{ + fmt::Write, + path::{Path, PathBuf}, +}; /// CLI arguments for `forge init`. #[derive(Debug, Clone, Parser)] @@ -50,19 +52,19 @@ impl InitArgs { if !root.exists() { fs::create_dir_all(&root)?; } - let root = dunce::canonicalize(root)?; + let root_rel = &root; + let root = dunce::canonicalize(&root)?; let git = Git::new(&root).shallow(shallow); // if a template is provided, then this command clones the template repo, removes the .git // folder, and initializes a new git repo—-this ensures there is no history from the // template and the template is not set as a remote. - if let Some(template) = template { + if let Some(template) = &template { let template = if template.contains("://") { - template + template.clone() } else { "https://github.com/".to_string() + &template }; - sh_eprintln!("Initializing {} from {}...", root.display(), template)?; if let Some(branch) = branch { Git::clone_with_branch(shallow, &template, branch, Some(&root))?; @@ -87,7 +89,7 @@ impl InitArgs { ); } - sh_eprintln!("Target directory is not empty, but `--force` was specified")?; + sh_note!("Target directory is not empty, but `--force` was specified")?; } // ensure git status is clean before generating anything @@ -95,8 +97,6 @@ impl InitArgs { git.ensure_clean()?; } - sh_eprintln!("Initializing {}...", root.display())?; - // make the dirs let src = root.join("src"); fs::create_dir_all(&src)?; @@ -136,7 +136,7 @@ impl InitArgs { // install forge-std if !offline { if root.join("lib/forge-std").exists() { - sh_eprintln!("\"lib/forge-std\" already exists, skipping install....")?; + sh_status!("Skipping" => "forge-std install")?; self.opts.install(&mut config, vec![])?; } else { let dep = "https://github.com/foundry-rs/forge-std".parse()?; @@ -150,8 +150,14 @@ impl InitArgs { } } - sh_eprintln!(" {} forge project", Paint::green("Initialized"))?; - Ok(()) + let mut msg = "Foundry project".to_string(); + if let Some(template) = &template { + write!(msg, " from {template}").unwrap(); + } + if root_rel != Path::new(".") { + write!(msg, " in {}", root_rel.display()).unwrap(); + } + sh_status!("Created" => "{msg}") } } diff --git a/crates/forge/bin/cmd/install.rs b/crates/forge/bin/cmd/install.rs index 5142e0d9b24ae..bf19e82a3ee54 100644 --- a/crates/forge/bin/cmd/install.rs +++ b/crates/forge/bin/cmd/install.rs @@ -114,7 +114,7 @@ impl DependencyInstallOpts { let libs = git.root.join(install_lib_dir); if dependencies.is_empty() && !self.no_git { - sh_eprintln!("Updating dependencies in {}", libs.display())?; + sh_status!("Updating" => "dependencies in {}", libs.display())?; git.submodule_update(false, false, Some(&libs))?; } fs::create_dir_all(&libs)?; From a66c98e7fe27147f7dc310434ce64cca72489e7b Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Fri, 8 Sep 2023 02:52:51 +0200 Subject: [PATCH 13/14] fix doctests --- crates/common/src/compile.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/common/src/compile.rs b/crates/common/src/compile.rs index 0ef4ba50d6448..4be919a31c213 100644 --- a/crates/common/src/compile.rs +++ b/crates/common/src/compile.rs @@ -169,8 +169,9 @@ impl ProjectCompiler { /// ```no_run /// use foundry_common::compile::ProjectCompiler; /// let config = foundry_config::Config::load(); + /// let prj = config.project().unwrap(); /// ProjectCompiler::default() - /// .compile_with(&config.project().unwrap(), |prj| Ok(prj.compile()?)).unwrap(); + /// .compile_with(&prj, || Ok(prj.compile()?)).unwrap(); /// ``` #[tracing::instrument(target = "forge::compile", skip_all)] pub fn compile_with(self, project: &Project, f: F) -> Result From f5a2b204fbb5568c8fc0ef2f645f5e716f58386f Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Fri, 8 Sep 2023 02:56:31 +0200 Subject: [PATCH 14/14] sync deps --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6c7853711045e..9a4806ecca07c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -668,9 +668,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.6.1" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8042c26c77e5bd6897a7358e0abb3ec412ed126d826988135653fc669263899d" +checksum = "4c2f7349907b712260e64b0afe2f84692af14a454be26187d9df565c7f69266a" dependencies = [ "memchr", "regex-automata 0.3.8", @@ -3971,9 +3971,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.6.1" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f478948fd84d9f8e86967bf432640e46adfb5a4bd4f14ef7e864ab38220534ae" +checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" [[package]] name = "memmap2"