diff --git a/Cargo.lock b/Cargo.lock index 81da6b2be3294..9a4806ecca07c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2340,6 +2340,7 @@ dependencies = [ "ethers-solc", "eyre", "forge-fmt", + "foundry-common", "foundry-config", "foundry-utils", "futures-util", @@ -2462,6 +2463,7 @@ dependencies = [ "eyre", "foundry-config", "foundry-macros", + "fwdansi", "globset", "once_cell", "regex", @@ -2470,6 +2472,8 @@ dependencies = [ "serde", "serde_json", "tempfile", + "termcolor", + "terminal_size", "thiserror", "tokio", "tracing", @@ -2755,6 +2759,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/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/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 134de3db3d0d0..8c7f5f328d7dc 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/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 e57d8c95a7cf9..abbedd9a08a80 100644 --- a/crates/binder/src/lib.rs +++ b/crates/binder/src/lib.rs @@ -1,4 +1,7 @@ -//! 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)] use crate::utils::{GitReference, GitRemote}; use ethers_contract::MultiAbigen; 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/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/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 ec89196590131..b78de85bada23 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/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..9c455c7b90807 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()?; @@ -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(); @@ -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() @@ -150,11 +150,11 @@ 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; - if let Ok(output) = suppress_compile(&project) { + if let Ok(output) = ProjectCompiler::new().quiet(true).compile(&project) { out = output; let (_, new_artifact) = out .artifacts() @@ -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/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 c50c5a77cc9f4..62bffa49843b7 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; @@ -8,7 +11,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,10 +19,14 @@ 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_common; + pub mod cmd; pub mod opts; @@ -30,13 +37,22 @@ use opts::{Opts, Subcommands, ToBaseArgs}; static ALLOC: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; #[tokio::main] -async fn main() -> Result<()> { +async fn main() { + if let Err(err) = run().await { + let _ = foundry_common::Shell::get().error(&err); + std::process::exit(1); + } +} + +async fn run() -> Result<()> { utils::load_dotenv(); - handler::install()?; + handler::install(); utils::subscriber(); utils::enable_paint(); let opts = Opts::parse(); + // SAFETY: See [foundry_common::Shell::set]. + unsafe { opts.shell.shell().set() }; match opts.sub { // Constants Subcommands::MaxInt { r#type } => { @@ -388,10 +404,10 @@ async fn main() -> 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/cast/bin/opts.rs b/crates/cast/bin/opts.rs index 379d537ff3495..002773ecd1f1d 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/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/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 cbc999079262a..5cb951169b50f 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 3b7f26a9c4cac..a5cfbd53a64f4 100644 --- a/crates/chisel/bin/main.rs +++ b/crates/chisel/bin/main.rs @@ -1,7 +1,9 @@ //! 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)] use chisel::{ history::chisel_history_file, 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/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/lib.rs b/crates/cli/src/lib.rs index 39f4e95765dd2..783f79da1d2a0 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -1,6 +1,8 @@ #![warn(unused_crate_dependencies)] +#[macro_use] +extern crate foundry_common; + pub mod handler; pub mod opts; -pub mod stdin; pub mod utils; 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/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/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..3854d66b7c721 --- /dev/null +++ b/crates/cli/src/opts/shell.rs @@ -0,0 +1,32 @@ +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 { + /// Use verbose output. + #[clap(long, global = true, conflicts_with = "quiet")] + pub verbose: bool, + + /// Do not print log messages. + #[clap(long, global = true, alias = "silent", conflicts_with = "verbose")] + pub quiet: bool, + + /// Log messages coloring. + #[clap(long, global = true, value_enum)] + pub color: Option, +} + +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.unwrap_or_default(), verbosity) + } +} 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..5e0266cce6c0a 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, @@ -219,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/cli/src/utils/cmd.rs b/crates/cli/src/utils/cmd.rs index 4323ddb2c6463..35c3f7ef1ed6b 100644 --- a/crates/cli/src/utils/cmd.rs +++ b/crates/cli/src/utils/cmd.rs @@ -10,7 +10,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, @@ -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() ); } @@ -256,35 +255,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() { @@ -409,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/cli/src/utils/mod.rs b/crates/cli/src/utils/mod.rs index d36b1de2b1c07..029411cc795e2 100644 --- a/crates/cli/src/utils/mod.rs +++ b/crates/cli/src/utils/mod.rs @@ -162,23 +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)*) - } - }} -} - /// 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 @@ -219,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!( + 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]" @@ -244,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"); } @@ -309,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] @@ -372,11 +354,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 { @@ -498,7 +475,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")) @@ -514,7 +491,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")) @@ -537,8 +514,8 @@ 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 { + fn stderr() -> Stdio { + if foundry_common::Shell::get().verbosity().is_quiet() { Stdio::piped() } else { Stdio::inherit() diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 24183a8ef2721..b7d4656b3e703 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" @@ -49,6 +51,8 @@ url = "2" # 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/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/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/common/src/compile.rs b/crates/common/src/compile.rs index 37d765ae2b747..4be919a31c213 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: Some(crate::Shell::get().verbosity().is_quiet()), + 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 @@ -81,37 +169,56 @@ 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 where - F: FnOnce(&Project) -> Result, + 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); } - let now = std::time::Instant::now(); - tracing::trace!("start compiling project"); + let quiet = self.quiet.unwrap_or(false); + #[allow(clippy::collapsible_else_if)] + let reporter = if quiet { + Report::new(NoReporter::default()) + } else { + if crate::Shell::get().is_err_tty() { + 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() { + sh_println!("No files changed, compilation skipped")?; + } else { + // print the compiler output / warnings + sh_println!("{output}")?; + } self.handle_output(&output); } @@ -121,27 +228,34 @@ 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); } 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}"); } } } - if self.print_sizes { + + if print_sizes { // add extra newline if names were already printed - if self.print_names { - println!(); + if print_names { + let _ = sh_println!(); } + let mut size_report = SizeReport { contracts: BTreeMap::new() }; let artifacts: BTreeMap<_, _> = output.artifacts().collect(); for (name, artifact) in artifacts { @@ -161,8 +275,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); @@ -176,7 +291,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 +372,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. - if graph.files().get(target_path).is_none() { + 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 +407,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/io/macros.rs b/crates/common/src/io/macros.rs new file mode 100644 index 0000000000000..e024debd3db47 --- /dev/null +++ b/crates/common/src/io/macros.rs @@ -0,0 +1,192 @@ +/// 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_common::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)+) => {{ + 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}")) + } + }}; +} + +/// Prints a formatted error to stderr. +#[macro_export] +macro_rules! sh_err { + ($($args:tt)*) => { + $crate::__sh_dispatch!(error $($args)*) + }; +} + +/// Prints a formatted warning to stderr. +#[macro_export] +macro_rules! sh_warn { + ($($args:tt)*) => { + $crate::__sh_dispatch!(warn $($args)*) + }; +} + +/// Prints a formatted note to stderr. +#[macro_export] +macro_rules! sh_note { + ($($args:tt)*) => { + $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::__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::__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") + }; + + ($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") + }; + + ($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 { + #[test] + fn macros() { + 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!().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(); + } + + #[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/mod.rs b/crates/common/src/io/mod.rs new file mode 100644 index 0000000000000..de81f70cef746 --- /dev/null +++ b/crates/common/src/io/mod.rs @@ -0,0 +1,10 @@ +//! Utilities for working with standard input, output, and error. + +#[macro_use] +mod macros; + +pub mod shell; +pub mod stdin; + +#[doc(no_inline)] +pub use shell::Shell; diff --git a/crates/common/src/io/shell.rs b/crates/common/src/io/shell.rs new file mode 100644 index 0000000000000..753a52aeb7310 --- /dev/null +++ b/crates/common/src/io/shell.rs @@ -0,0 +1,604 @@ +//! 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 std::{ + fmt, + io::{prelude::*, IsTerminal}, + ops::DerefMut, + sync::atomic::{AtomicBool, Ordering}, +}; +use termcolor::{ + Color::{self, Cyan, Green, Red, Yellow}, + ColorSpec, StandardStream, WriteColor, +}; + +/// 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 { + /// 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)] + #[allow(clippy::useless_conversion)] + 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, + } + } + + /// 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, Default, Clone, Copy, PartialEq)] +pub enum Verbosity { + /// All output + Verbose, + /// Default output + #[default] + Normal, + /// No output + 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 { + /// 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: AtomicBool, +} + +impl fmt::Debug for Shell { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + 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. +enum ShellOut { + /// Color-enabled stdio, with information on whether color should be used. + Stream { + stdout: StandardStream, + stderr: StandardStream, + 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, +} + +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. + #[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 { + 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, + 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 { + 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 { + // 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 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"); + } + } + + /// 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) + } + + /// 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() + } + + /// 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(), + _ => TtyWidth::NoTty, + } + } + + /// 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, + 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 { + 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 { + self.maybe_err_erase_line(); + self.output.stderr() + } + + /// 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"); + } + } + + /// 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, + U: fmt::Display, + { + self.print(&status, Some(&message), Green, true) + } + + /// Shortcut to right-align and color cyan a status without a message. + #[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, + U: fmt::Display, + { + self.print(&status, Some(&message), color, true) + } + + /// Runs the callback only if we are in verbose mode. + #[inline] + pub fn verbose(&mut self, mut callback: impl FnMut(&mut Shell) -> Result<()>) -> Result<()> { + match self.verbosity { + Verbosity::Verbose => callback(self), + _ => Ok(()), + } + } + + /// Runs the callback if we are not in verbose mode. + #[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. 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. 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. Use the [`sh_note!`] macro instead. + #[inline] + pub fn note(&mut self, message: impl fmt::Display) -> Result<()> { + self.print(&"note", Some(&message), Cyan, false) + } + + /// 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. 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()) + } + + /// 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. 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<()> { + 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<()> { + self.maybe_err_erase_line(); + #[cfg(windows)] + 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. + #[inline] + pub fn print_ansi_stdout(&mut self, message: &[u8]) -> Result<()> { + self.maybe_err_erase_line(); + #[cfg(windows)] + if let ShellOut::Stream { stdout, .. } = &self.output { + ::fwdansi::write_ansi(stdout, message)?; + return Ok(()) + } + self.out().write_all(message)?; + Ok(()) + } + + /// Serializes an object to JSON and prints it to `stdout`. + #[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 { + /// 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, + ) -> Result<()> { + match self { + Self::Stream { stderr, .. } => { + stderr.reset()?; + stderr.set_color(ColorSpec::new().set_bold(true).set_fg(Some(color)))?; + if justified { + write!(stderr, "{status:>12}") + } else { + write!(stderr, "{status}")?; + stderr.set_color(ColorSpec::new().set_bold(true))?; + write!(stderr, ":") + }?; + stderr.reset()?; + + stderr.write_all(b" ")?; + if let Some(message) = message { + writeln!(stderr, "{message}")?; + } + } + + 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}")?; + } + } + } + Ok(()) + } + + /// Write a styled fragment + 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}")?; + stdout.reset()?; + } + + Self::Write(w) => { + write!(w, "{fragment}")?; + } + } + Ok(()) + } + + /// Write a styled fragment + 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}")?; + stderr.reset()?; + } + + Self::Write(w) => { + write!(w, "{fragment}")?; + } + } + Ok(()) + } + + /// Gets stdout as a `io::Write`. + #[inline] + fn stdout(&mut self) -> &mut dyn Write { + 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 { + Self::Stream { stderr, .. } => stderr, + + Self::Write(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::Auto => { + if stream.is_terminal() { + termcolor::ColorChoice::Auto + } else { + termcolor::ColorChoice::Never + } + } + } + } +} + +#[derive(Clone, Copy)] +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(), + } + } +} diff --git a/crates/cli/src/stdin.rs b/crates/common/src/io/stdin.rs similarity index 76% rename from crates/cli/src/stdin.rs rename to crates/common/src/io/stdin.rs index 8242cc8057724..17b40a2cff1fe 100644 --- a/crates/cli/src/stdin.rs +++ b/crates/common/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 @@ -50,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 bd7162e7c8dd4..0ddf1abcdf2b1 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -2,9 +2,11 @@ #![warn(missing_docs, unused_crate_dependencies)] +#[macro_use] +pub mod io; + pub mod abi; pub mod calc; -pub mod clap_helpers; pub mod compile; pub mod constants; pub mod contracts; @@ -16,7 +18,6 @@ pub mod glob; pub mod provider; pub mod runtime_client; pub mod selectors; -pub mod shell; pub mod term; pub mod traits; pub mod transactions; @@ -26,3 +27,5 @@ pub use contracts::*; pub use provider::*; pub use traits::*; pub use transactions::*; + +pub use io::{shell, stdin, Shell}; diff --git a/crates/common/src/selectors.rs b/crates/common/src/selectors.rs index 870c6d3c77828..36c46bb73048b 100644 --- a/crates/common/src/selectors.rs +++ b/crates/common/src/selectors.rs @@ -1,5 +1,6 @@ -#![allow(missing_docs)] //! Support for handling/identifying selectors +#![allow(missing_docs)] + use crate::abi::abi_decode; use ethers_solc::artifacts::LosslessAbi; use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; @@ -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,17 +389,17 @@ 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(()) /// # } /// ``` - pub async fn pretty_calldata( calldata: impl AsRef, offline: bool, @@ -450,25 +452,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") } } @@ -583,7 +581,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/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/common/src/term.rs b/crates/common/src/term.rs index 82dc5b29bf738..4dcaf0fb625d0 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}, @@ -71,8 +75,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); } @@ -214,21 +219,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/lib.rs b/crates/config/src/lib.rs index 9be852fb7826e..7be8e3623503c 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/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/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/forge/bin/cmd/bind.rs b/crates/forge/bin/cmd/bind.rs index 237af98b87534..b1c640ae29e1a 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, @@ -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,38 +159,36 @@ 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<()> { 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; 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/build.rs b/crates/forge/bin/cmd/build.rs index 7c4e8fce2ca33..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, @@ -76,22 +73,17 @@ 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()?; } - 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/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/config.rs b/crates/forge/bin/cmd/config.rs index a8e33cdba38ca..c0babae913911 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(()) } @@ -54,7 +54,6 @@ impl ConfigArgs { config.to_string_pretty()? }; - println!("{s}"); - Ok(()) + sh_println!("{s}") } } diff --git a/crates/forge/bin/cmd/coverage.rs b/crates/forge/bin/cmd/coverage.rs index 0ce7fdbc40f1a..d78d0c1ba7bc1 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}; @@ -30,7 +29,6 @@ use foundry_config::{Config, SolcReq}; 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; @@ -69,9 +67,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(); } @@ -80,10 +76,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 } @@ -106,15 +102,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/create.rs b/crates/forge/bin/cmd/create.rs index ad82163aa72f6..d5131a25ddf3d 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 @@ -275,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 }; @@ -307,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 2f750877694ea..81e3af021a3e2 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, }; @@ -56,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/fmt.rs b/crates/forge/bin/cmd/fmt.rs index bf8334d70b9fb..1a2565a125fe9 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::*; @@ -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,23 +112,24 @@ 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}")?; } } 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." ) })?; if self.check || path.is_none() { if self.raw { - print!("{output}"); + sh_print!("{output}")?; } let diff = TextDiff::from_lines(&source, &output); @@ -145,11 +146,11 @@ impl FmtArgs { Input::Stdin(source) => format(source, None).map(|diff| vec![diff]), Input::Paths(paths) => { if paths.is_empty() { - cli_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 diff --git a/crates/forge/bin/cmd/fourbyte.rs b/crates/forge/bin/cmd/fourbyte.rs index 86a889d6fe635..0bb0b28b534f8 100644 --- a/crates/forge/bin/cmd/fourbyte.rs +++ b/crates/forge/bin/cmd/fourbyte.rs @@ -6,11 +6,9 @@ use foundry_cli::{ utils::FoundryPathExt, }; use foundry_common::{ - compile, + compile::ProjectCompiler, 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; @@ -44,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 = @@ -59,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") @@ -78,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/geiger/mod.rs b/crates/forge/bin/cmd/geiger/mod.rs index 6756a5921a939..817c0464106e6 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; @@ -101,14 +100,16 @@ 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 } Err(err) => { - eprintln!("{err}"); + let _ = sh_err!("{err}"); 0 } }) 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/init.rs b/crates/forge/bin/cmd/init.rs index a1b2f2c78c210..62e97041bb5d6 100644 --- a/crates/forge/bin/cmd/init.rs +++ b/crates/forge/bin/cmd/init.rs @@ -2,11 +2,13 @@ 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}; -use yansi::Paint; +use std::{ + fmt::Write, + path::{Path, PathBuf}, +}; /// CLI arguments for `forge init`. #[derive(Debug, Clone, Parser)] @@ -44,25 +46,25 @@ pub struct InitArgs { impl InitArgs { pub fn run(self) -> Result<()> { let InitArgs { root, template, branch, 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 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 }; - p_println!(!quiet => "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 { ); } - p_println!(!quiet => "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()?; } - p_println!(!quiet => "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() { - p_println!(!quiet => "\"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 { } } - p_println!(!quiet => " {} 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/inspect.rs b/crates/forge/bin/cmd/inspect.rs index 1359067578b90..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; -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`. @@ -67,135 +66,90 @@ 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); - - 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 5dcf3960adf17..bf19e82a3ee54 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, - p_println, prompt, utils::{CommandUtils, Git, LoadConfig}, }; use foundry_common::fs; @@ -16,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()); @@ -78,15 +76,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 +89,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 +106,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 +114,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_status!("Updating" => "dependencies in {}", libs.display())?; git.submodule_update(false, false, Some(&libs))?; } fs::create_dir_all(&libs)?; @@ -136,7 +125,7 @@ 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_status!("Installing" => "{dep} to {}", path.display())?; // this tracks the actual installed tag let installed_tag; @@ -178,14 +167,7 @@ impl DependencyInstallOpts { } } - if !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}"); - } + let _ = installed_tag; } // update `libs` in config if not included yet @@ -197,8 +179,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)] @@ -384,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(); @@ -401,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, @@ -446,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(); @@ -460,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) } @@ -469,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..3b4dd772523ac 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} from {}", path.display())?; std::fs::remove_dir_all(git_modules.join(path))?; } 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/build.rs b/crates/forge/bin/cmd/script/build.rs index e80a4ec922494..780175e111787 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}; @@ -215,7 +215,6 @@ impl ScriptArgs { let output = compile::compile_target_with_filter( &target_contract, &project, - self.opts.args.silent, self.verify, filters, )?; @@ -233,23 +232,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/script/executor.rs b/crates/forge/bin/cmd/script/executor.rs index bc31d78bfe916..9ab60e746126a 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 futures::future::join_all; use parking_lot::RwLock; use std::{collections::VecDeque, sync::Arc}; @@ -107,8 +107,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 @@ -187,7 +187,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( @@ -225,7 +225,7 @@ impl ScriptArgs { for (_kind, trace) in &mut traces { decoder.decode(trace).await; - println!("{trace}"); + sh_println!("{trace}")?; } } @@ -247,9 +247,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..11a4fce00a702 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, @@ -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. @@ -287,9 +282,7 @@ impl ScriptArgs { ); } } - Err(_) => { - shell::println(format!("{:x?}", (&returned)))?; - } + Err(_) => sh_eprintln!("{returned:x?}")?, } Ok(returns) @@ -309,32 +302,32 @@ 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, - TraceKind::Execution => verbosity > 3, + TraceKind::Execution => verbosity >= 4, _ => false, } || !result.success; 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 +338,22 @@ 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)))?; - } + Err(_) => 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 +373,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_common::Shell::get().print_json(&output) } /// It finds the deployer from the running script and uses it to predeploy libraries. @@ -410,7 +398,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 +605,7 @@ impl ScriptArgs { if deployment_size > max_size { prompt_user = self.broadcast; - shell::println(format!( - "{}", - 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})")?; } } } @@ -719,12 +702,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 +739,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/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 0c9d0ad20bde3..411dbe0cf28f4 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::{ @@ -23,7 +23,6 @@ use std::{ path::{Path, PathBuf}, }; use tracing::trace; -use yansi::Paint; pub const DRY_RUN_DIR: &str = "dry-run"; @@ -174,8 +173,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_note!("Transactions saved to: {}", self.path.display())?; + sh_note!("Sensitive values saved to: {}", self.sensitive_path.display())?; Ok(()) } @@ -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 b594081a72d11..3df4a789599b8 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!( @@ -103,52 +96,45 @@ 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!()?; } } } 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() @@ -161,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![ @@ -169,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 ebc1f242cec74..f7662a2cf31cb 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 @@ -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 d2ab5c0de80ff..66143981290ee 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, shell, -}; +use foundry_common::{compile::ProjectCompiler, evm::EvmArgs, get_contract_name, get_file_name}; use foundry_config::{ figment, figment::{ @@ -117,7 +113,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,22 +134,16 @@ 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()?; } - 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 @@ -214,15 +203,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?; @@ -327,6 +314,7 @@ impl Test { } /// Represents the bundled results of all tests +#[derive(Default)] pub struct TestOutcome { /// Whether failures are allowed pub allow_failure: bool, @@ -339,26 +327,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() @@ -375,13 +364,12 @@ impl TestOutcome { return Ok(()) } - if !shell::verbosity().is_normal() { + if foundry_common::Shell::get().verbosity().is_quiet() { // skip printing and exit early 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 { @@ -389,18 +377,18 @@ 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); } @@ -456,7 +444,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()); } /** @@ -488,13 +476,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 "))?; } } } @@ -515,29 +503,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_println!("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_println!("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)) } @@ -563,17 +551,23 @@ async fn test( let mut total_failed = 0; let mut total_skipped = 0; + // 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(); - println!(); + sh_println!()?; for warning in suite_result.warnings.iter() { - eprintln!("{} {warning}", Paint::yellow("Warning:").bold()); + sh_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" }; + sh_println!("Running {n_tests} {term} for {contract_name}")?; } for (name, result) in &mut tests { short_test_result(name, result); @@ -588,11 +582,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!()?; } } @@ -645,8 +639,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 { @@ -659,20 +655,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 { - println!("{}", gas_report.finalize()); + sh_println!("{}", gas_report.finalize())?; } let num_test_suites = results.len(); if num_test_suites > 0 { - println!( + sh_println!( "{}", - format_aggregated_summary(num_test_suites, total_passed, total_failed, total_skipped) - ); + 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 d311007f8134a..fb82830318595 100644 --- a/crates/forge/bin/main.rs +++ b/crates/forge/bin/main.rs @@ -3,6 +3,9 @@ use clap_complete::generate; use eyre::Result; use foundry_cli::{handler, utils}; +#[macro_use] +extern crate foundry_common; + mod cmd; mod opts; @@ -13,13 +16,22 @@ use opts::{Opts, Subcommands}; #[global_allocator] static ALLOC: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; -fn main() -> Result<()> { +fn main() { + if let Err(err) = run() { + let _ = foundry_common::Shell::get().error(&err); + std::process::exit(1); + } +} + +fn run() -> Result<()> { utils::load_dotenv(); - handler::install()?; + handler::install(); utils::subscriber(); utils::enable_paint(); let opts = Opts::parse(); + // SAFETY: See [foundry_common::Shell::set]. + unsafe { opts.shell.shell().set() }; match opts.sub { Subcommands::Test(cmd) => { if cmd.is_watch() { @@ -29,14 +41,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. 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; diff --git a/crates/forge/src/multi_runner.rs b/crates/forge/src/multi_runner.rs index 55b2c34e13646..61c90b31e74b9 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)) 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/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..c0e9e3881b0e5 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,31 @@ impl Ui for Tui { } } +#[allow(clippy::type_complexity)] // standard panic handler type +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),