From c1ff6e71735f57fe9c57d60a22a460dd38215f0a Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Thu, 4 Jan 2024 01:22:26 +0100 Subject: [PATCH 1/6] refactor: rewrite compile functions to a common builder --- crates/cast/bin/cmd/access_list.rs | 1 - crates/cast/bin/cmd/call.rs | 1 - crates/cast/bin/cmd/storage.rs | 15 +- crates/cast/bin/cmd/wallet/mod.rs | 5 +- crates/cast/bin/main.rs | 3 +- crates/cast/bin/opts.rs | 21 +- crates/cast/src/base.rs | 10 +- crates/chisel/bin/main.rs | 2 +- crates/cli/src/handler.rs | 7 +- crates/cli/src/opts/wallet/mod.rs | 6 +- crates/cli/src/opts/wallet/multi_wallet.rs | 10 +- crates/common/src/clap_helpers.rs | 6 - crates/common/src/compile.rs | 390 ++++++++---------- crates/common/src/lib.rs | 1 - crates/common/src/selectors.rs | 5 +- crates/config/src/warning.rs | 45 +- crates/forge/bin/cmd/bind.rs | 9 +- crates/forge/bin/cmd/build.rs | 29 +- crates/forge/bin/cmd/create.rs | 10 +- crates/forge/bin/cmd/flatten.rs | 5 +- crates/forge/bin/cmd/inspect.rs | 124 ++---- crates/forge/bin/cmd/remappings.rs | 15 +- crates/forge/bin/cmd/script/build.rs | 18 +- crates/forge/bin/cmd/script/mod.rs | 7 +- crates/forge/bin/cmd/selectors.rs | 74 ++-- crates/forge/bin/cmd/test/mod.rs | 20 +- .../forge/bin/cmd/verify/etherscan/flatten.rs | 10 +- crates/forge/bin/cmd/verify/sourcify.rs | 50 ++- crates/forge/bin/main.rs | 3 +- crates/forge/bin/opts.rs | 20 +- 30 files changed, 391 insertions(+), 531 deletions(-) delete mode 100644 crates/common/src/clap_helpers.rs diff --git a/crates/cast/bin/cmd/access_list.rs b/crates/cast/bin/cmd/access_list.rs index 28be54ee04fa..fa31380ff14d 100644 --- a/crates/cast/bin/cmd/access_list.rs +++ b/crates/cast/bin/cmd/access_list.rs @@ -33,7 +33,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 b5cc9e2b8a4b..8217eeb9e1ed 100644 --- a/crates/cast/bin/cmd/call.rs +++ b/crates/cast/bin/cmd/call.rs @@ -34,7 +34,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/storage.rs b/crates/cast/bin/cmd/storage.rs index 293833fb42be..7285f9397719 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}, types::{ToAlloy, ToEthers}, RetryProvider, }; @@ -100,7 +100,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()?; @@ -115,7 +115,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...")?; if self.etherscan.key.is_none() { eyre::bail!("You must provide an Etherscan API key if you're fetching a remote contract's storage."); @@ -146,7 +146,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() @@ -155,11 +155,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() @@ -217,8 +217,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_slots(provider, address, &layout).await?; diff --git a/crates/cast/bin/cmd/wallet/mod.rs b/crates/cast/bin/cmd/wallet/mod.rs index b4cec44e0879..9a1253d42dee 100644 --- a/crates/cast/bin/cmd/wallet/mod.rs +++ b/crates/cast/bin/cmd/wallet/mod.rs @@ -72,10 +72,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 5ceea192a0b5..4e707a2ee70a 100644 --- a/crates/cast/bin/main.rs +++ b/crates/cast/bin/main.rs @@ -28,8 +28,7 @@ pub mod opts; use opts::{Opts, Subcommands, ToBaseArgs}; #[tokio::main] -async fn main() -> Result<()> { - handler::install()?; + handler::install(); utils::load_dotenv(); utils::subscriber(); utils::enable_paint(); diff --git a/crates/cast/bin/opts.rs b/crates/cast/bin/opts.rs index dcd557d310fa..b1c1e1184760 100644 --- a/crates/cast/bin/opts.rs +++ b/crates/cast/bin/opts.rs @@ -7,7 +7,7 @@ use alloy_primitives::{Address, B256, U256}; use clap::{Parser, Subcommand, ValueHint}; use ethers_core::types::{BlockId, NameOrAddress}; use eyre::Result; -use foundry_cli::opts::{EtherscanOpts, RpcOpts}; +use foundry_cli::opts::{EtherscanOpts, RpcOpts, ShellOptions}; use std::{path::PathBuf, str::FromStr}; const VERSION_MESSAGE: &str = concat!( @@ -19,19 +19,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 80d2f6047fc4..0ef91c6ddb07 100644 --- a/crates/cast/src/base.rs +++ b/crates/cast/src/base.rs @@ -41,12 +41,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/chisel/bin/main.rs b/crates/chisel/bin/main.rs index 6b94eecb498f..69ae708405c5 100644 --- a/crates/chisel/bin/main.rs +++ b/crates/chisel/bin/main.rs @@ -84,7 +84,7 @@ pub enum ChiselParserSub { #[tokio::main] async fn main() -> eyre::Result<()> { - handler::install()?; + handler::install(); utils::subscriber(); #[cfg(windows)] if !Paint::enable_windows_ascii() { diff --git a/crates/cli/src/handler.rs b/crates/cli/src/handler.rs index f4778c43960d..61d3246f4738 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 yansi::Paint; @@ -47,9 +47,8 @@ impl EyreHandler for Handler { /// verbose debug-centric handler is installed. /// /// Panics are always caught by the more debug-centric handler. -pub fn install() -> Result<()> { +pub fn install() { let debug_enabled = std::env::var("FOUNDRY_DEBUG").is_ok(); - if debug_enabled { if let Err(e) = color_eyre::install() { debug!("failed to install color eyre error hook: {e}"); @@ -65,6 +64,4 @@ pub fn install() -> Result<()> { debug!("failed to install eyre error hook: {e}"); } } - - Ok(()) } diff --git a/crates/cli/src/opts/wallet/mod.rs b/crates/cli/src/opts/wallet/mod.rs index 66523e92ec5c..49f2fbc8ce39 100644 --- a/crates/cli/src/opts/wallet/mod.rs +++ b/crates/cli/src/opts/wallet/mod.rs @@ -42,11 +42,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 0ca13005ef0c..71baf28f529d 100644 --- a/crates/cli/src/opts/wallet/multi_wallet.rs +++ b/crates/cli/src/opts/wallet/multi_wallet.rs @@ -97,12 +97,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. @@ -110,8 +105,7 @@ pub struct MultiWallet { long, help_heading = "Wallet options - raw", conflicts_with = "private_keys", - value_name = "RAW_PRIVATE_KEY", - value_parser = foundry_common::clap_helpers::strip_0x_prefix, + value_name = "RAW_PRIVATE_KEY" )] pub private_key: Option, diff --git a/crates/common/src/clap_helpers.rs b/crates/common/src/clap_helpers.rs deleted file mode 100644 index f26455b76414..000000000000 --- 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 3d1db499d407..cef1440f4929 100644 --- a/crates/common/src/compile.rs +++ b/crates/common/src/compile.rs @@ -1,12 +1,13 @@ //! Support for compiling [foundry_compilers::Project] -use crate::{compact_to_contract, glob::GlobMatcher, term, TestFunctionExt}; -use comfy_table::{presets::ASCII_MARKDOWN, *}; + +use crate::{compact_to_contract, glob::GlobMatcher, term::SpinnerReporter, TestFunctionExt}; +use comfy_table::{presets::ASCII_MARKDOWN, Attribute, Cell, Color, Table}; use eyre::Result; use foundry_block_explorers::contract::Metadata; use foundry_compilers::{ 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(Clone, Debug, 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::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 } - /// 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 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 + } + + /// 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,38 +169,55 @@ impl ProjectCompiler { /// ```no_run /// use foundry_common::compile::ProjectCompiler; /// let config = foundry_config::Config::load(); - /// ProjectCompiler::default() - /// .compile_with(&config.project().unwrap(), |prj| Ok(prj.compile()?)) - /// .unwrap(); + /// let prj = config.project().unwrap(); + /// ProjectCompiler::new().compile_with(&prj, || Ok(prj.compile()?)).unwrap(); /// ``` #[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(); - 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 = term::with_spinner_reporter(|| f(project))?; + let output = foundry_compilers::report::with_scoped(&reporter, || { + tracing::debug!("compiling project"); - let elapsed = now.elapsed(); - trace!(?elapsed, "finished compiling"); + let timer = std::time::Instant::now(); + let r = f(); + let elapsed = timer.elapsed(); + + tracing::debug!("finished compiling in {:.3}s", elapsed.as_secs_f64()); + r + })?; if output.has_compiler_errors() { - 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); } @@ -122,27 +227,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 +273,9 @@ impl ProjectCompiler { size_report.contracts.insert(name, ContractInfo { size, is_dev_contract }); } - println!("{size_report}"); + let _ = sh_println!("{size_report}"); + // TODO: avoid process::exit // exit with error if any contract exceeds the size limit, excluding test contracts. if size_report.exceeds_size_limit() { std::process::exit(1); @@ -180,7 +293,7 @@ const CONTRACT_SIZE_LIMIT: usize = 24576; /// Contracts with info about their size pub struct SizeReport { - /// `:info>` + /// `contract name -> info` pub contracts: BTreeMap, } @@ -261,175 +374,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`] and does not throw if there's any compiler error -/// Doesn't print anything to stdout, thus is "suppressed". -pub fn try_suppress_compile(project: &Project) -> Result { - Ok(foundry_compilers::report::with_scoped( - &foundry_compilers::report::Report::new(NoReporter::default()), - || project.compile(), - )?) -} - -/// 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 = try_suppress_compile(project)?; - - 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`] and throw if there's any compiler error -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)) - } -} - -/// Depending on whether the `skip` is empty this will [`suppress_compile_sparse`] or -/// [`suppress_compile`] and does not throw if there's any compiler error -pub fn suppress_compile_with_filter_json( - project: &Project, - skip: Vec, -) -> Result { - if skip.is_empty() { - try_suppress_compile(project) - } else { - try_suppress_compile_sparse(project, SkipBuildFilters(skip)) - } -} - -/// Compiles the provided [`Project`], -/// Doesn't print anything to stdout, thus is "suppressed". -/// -/// See [`Project::compile_sparse`] -pub fn try_suppress_compile_sparse( - project: &Project, - filter: F, -) -> Result { - Ok(foundry_compilers::report::with_scoped( - &foundry_compilers::report::Report::new(NoReporter::default()), - || project.compile_sparse(filter), - )?) -} - -/// 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 = try_suppress_compile_sparse(project, 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 { - foundry_compilers::report::with_scoped( - &foundry_compilers::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) } /// Compiles an Etherscan source from metadata by creating a project. @@ -444,7 +410,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, file_id, contract) = project_output diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 02d11bb43590..3e7e6ecbd775 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -9,7 +9,6 @@ extern crate tracing; pub mod abi; pub mod calc; -pub mod clap_helpers; pub mod compile; pub mod constants; pub mod contracts; diff --git a/crates/common/src/selectors.rs b/crates/common/src/selectors.rs index 682b31f2f8d7..576af2b9f119 100644 --- a/crates/common/src/selectors.rs +++ b/crates/common/src/selectors.rs @@ -1,5 +1,7 @@ +//! Support for handling/identifying selectors. + #![allow(missing_docs)] -//! Support for handling/identifying selectors + use crate::abi::abi_decode_calldata; use alloy_json_abi::JsonAbi; use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; @@ -410,7 +412,6 @@ pub async fn decode_event_topic(topic: &str) -> eyre::Result> { /// # Ok(()) /// # } /// ``` - pub async fn pretty_calldata( calldata: impl AsRef, offline: bool, diff --git a/crates/config/src/warning.rs b/crates/config/src/warning.rs index 027554ff1047..a19104eaf7b4 100644 --- a/crates/config/src/warning.rs +++ b/crates/config/src/warning.rs @@ -53,30 +53,37 @@ impl fmt::Display for Warning { match self { Self::UnknownSection { unknown_section, source } => { let source = source.as_ref().map(|src| format!(" in {src}")).unwrap_or_default(); - f.write_fmt(format_args!("Unknown section [{unknown_section}] found{source}. This notation for profiles has been deprecated and may result in the profile not being registered in future versions. Please use [profile.{unknown_section}] instead or run `forge config --fix`.")) - } - Self::NoLocalToml(tried) => { - let path = tried.display(); - f.write_fmt(format_args!("No local TOML found to fix at {path}. Change the current directory to a project path or set the foundry.toml path with the FOUNDRY_CONFIG environment variable")) + write!( + f, + "Found unknown config section{source}: [{unknown_section}]\n\ + This notation for profiles has been deprecated and may result in the profile \ + not being registered in future versions.\n\ + Please use [profile.{unknown_section}] instead or run `forge config --fix`." + ) } + Self::NoLocalToml(path) => write!( + f, + "No local TOML found to fix at {}.\n\ + Change the current directory to a project path or set the foundry.toml path with \ + the `FOUNDRY_CONFIG` environment variable", + path.display() + ), + Self::CouldNotReadToml { path, err } => { - f.write_fmt(format_args!("Could not read TOML at {}: {err}", path.display())) + write!(f, "Could not read TOML at {}: {err}", path.display()) } Self::CouldNotWriteToml { path, err } => { - f.write_fmt(format_args!("Could not write TOML to {}: {err}", path.display())) + write!(f, "Could not write TOML to {}: {err}", path.display()) + } + Self::CouldNotFixProfile { path, profile, err } => { + write!(f, "Could not fix [{profile}] in TOML at {}: {err}", path.display()) + } + Self::DeprecatedKey { old, new } if new.is_empty() => { + write!(f, "Key `{old}` is being deprecated and will be removed in future versions.") + } + Self::DeprecatedKey { old, new } => { + write!(f, "Key `{old}` is being deprecated in favor of `{new}`. It will be removed in future versions.") } - Self::CouldNotFixProfile { path, profile, err } => f.write_fmt(format_args!( - "Could not fix [{}] in TOML at {}: {}", - profile, - path.display(), - err - )), - Self::DeprecatedKey { old, new } if new.is_empty() => f.write_fmt(format_args!( - "Key `{old}` is being deprecated and will be removed in future versions.", - )), - Self::DeprecatedKey { old, new } => f.write_fmt(format_args!( - "Key `{old}` is being deprecated in favor of `{new}`. It will be removed in future versions.", - )), } } } diff --git a/crates/forge/bin/cmd/bind.rs b/crates/forge/bin/cmd/bind.rs index 2f0a53fcbfec..c4fb92bd6e08 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, WrapErr}; 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, @@ -86,7 +86,7 @@ impl BindArgs { if !self.skip_build { // run `forge build` let project = self.build_args.project()?; - compile::compile(&project, false, false)?; + let _ = ProjectCompiler::new().compile(&project)?; } let artifacts = self.try_load_config_emit_warnings()?.out; @@ -216,11 +216,10 @@ No contract artifacts found. Hint: Have you built your contracts yet? `forge bin &self.crate_version, self.bindings_root(&artifacts), self.single_file, - )?; + ) } else { trace!(single_file = self.single_file, "generating module"); - bindings.write_to_module(self.bindings_root(&artifacts), self.single_file)?; + bindings.write_to_module(self.bindings_root(&artifacts), self.single_file) } - Ok(()) } } diff --git a/crates/forge/bin/cmd/build.rs b/crates/forge/bin/cmd/build.rs index 46ebb02041bb..666b13d2d29f 100644 --- a/crates/forge/bin/cmd/build.rs +++ b/crates/forge/bin/cmd/build.rs @@ -2,10 +2,7 @@ use super::{install, watch::WatchArgs}; use clap::Parser; 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_compilers::{Project, ProjectCompileOutput}; use foundry_config::{ figment::{ @@ -72,7 +69,7 @@ pub struct BuildArgs { /// Output the compilation errors in the json format. /// This is useful when you want to use the output in other tools. - #[clap(long, conflicts_with = "silent")] + #[clap(long, conflicts_with = "quiet")] #[serde(skip)] pub format_json: bool, } @@ -82,27 +79,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.format_json { - let output = compile::suppress_compile_with_filter_json(&project, filters)?; - let json = serde_json::to_string_pretty(&output.clone().output())?; - println!("{}", json); - Ok(output) - } else 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/create.rs b/crates/forge/bin/cmd/create.rs index 5f22cc146cab..f9203123aa88 100644 --- a/crates/forge/bin/cmd/create.rs +++ b/crates/forge/bin/cmd/create.rs @@ -19,7 +19,8 @@ use foundry_cli::{ utils::{self, read_constructor_args_file, remove_contract, LoadConfig}, }; use foundry_common::{ - compile, estimate_eip1559_fees, + compile::ProjectCompiler, + estimate_eip1559_fees, fmt::parse_tokens, types::{ToAlloy, ToEthers}, }; @@ -90,12 +91,7 @@ impl CreateArgs { pub async fn run(mut self) -> Result<()> { // Find Project & Compile let project = self.opts.project()?; - let mut output = if self.json || self.opts.silent { - // Suppress compile stdout messages when printing json output or when silent - compile::suppress_compile(&project) - } else { - compile::compile(&project, false, false) - }?; + let mut output = ProjectCompiler::new().quiet_if(self.json).compile(&project)?; if let Some(ref mut path) = self.contract.path { // paths are absolute in the project's output diff --git a/crates/forge/bin/cmd/flatten.rs b/crates/forge/bin/cmd/flatten.rs index 03aa7cf6d8f4..0716af47267a 100644 --- a/crates/forge/bin/cmd/flatten.rs +++ b/crates/forge/bin/cmd/flatten.rs @@ -40,9 +40,8 @@ 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) => { diff --git a/crates/forge/bin/cmd/inspect.rs b/crates/forge/bin/cmd/inspect.rs index a05dbb3c1ee7..33153c32740f 100644 --- a/crates/forge/bin/cmd/inspect.rs +++ b/crates/forge/bin/cmd/inspect.rs @@ -1,9 +1,8 @@ -use alloy_json_abi::JsonAbi; use clap::Parser; use comfy_table::{presets::ASCII_MARKDOWN, Table}; use eyre::Result; use foundry_cli::opts::{CompilerArgs, CoreBuildArgs}; -use foundry_common::compile; +use foundry_common::{compile::ProjectCompiler, Shell}; use foundry_compilers::{ artifacts::{ output_selection::{ @@ -15,7 +14,6 @@ use foundry_compilers::{ info::ContractInfo, utils::canonicalize, }; -use serde_json::{to_value, Value}; use std::fmt; /// CLI arguments for `forge inspect`. @@ -64,103 +62,68 @@ 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)?; + if pretty { + let source = foundry_cli::utils::abi_to_solidity(abi, "")?; + Shell::get().write_stdout(source, &Default::default()) + } else { + Shell::get().print_json(abi) + }?; } 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.as_ref(), 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(); @@ -176,7 +139,7 @@ impl InspectArgs { ); } } - println!("{}", serde_json::to_string_pretty(&out)?); + Shell::get().print_json(&out)?; } ContractArtifactField::Events => { let mut out = serde_json::Map::new(); @@ -190,7 +153,7 @@ impl InspectArgs { ); } } - println!("{}", serde_json::to_string_pretty(&out)?); + Shell::get().print_json(&out)?; } }; @@ -198,24 +161,13 @@ impl InspectArgs { } } -pub fn print_abi(abi: &JsonAbi, pretty: bool) -> Result<()> { - let s = if pretty { - foundry_cli::utils::abi_to_solidity(abi, "")? - } else { - serde_json::to_string_pretty(&abi)? - }; - println!("{s}"); - Ok(()) -} - pub fn print_storage_layout(storage_layout: Option<&StorageLayout>, pretty: bool) -> Result<()> { let Some(storage_layout) = storage_layout else { eyre::bail!("Could not get storage layout"); }; if !pretty { - println!("{}", serde_json::to_string_pretty(&to_value(storage_layout)?)?); - return Ok(()) + return print_json(&storage_layout) } let mut table = Table::new(); @@ -234,9 +186,7 @@ pub fn print_storage_layout(storage_layout: Option<&StorageLayout>, pretty: bool ]); } - println!("{table}"); - - Ok(()) + Shell::get().write_stdout(table, &Default::default()) } /// Contract level output selection @@ -409,6 +359,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/remappings.rs b/crates/forge/bin/cmd/remappings.rs index d01243b35d3a..2a0379af2f0e 100644 --- a/crates/forge/bin/cmd/remappings.rs +++ b/crates/forge/bin/cmd/remappings.rs @@ -1,7 +1,6 @@ use clap::{Parser, ValueHint}; use eyre::Result; use foundry_cli::utils::LoadConfig; -use foundry_compilers::remappings::RelativeRemapping; use foundry_config::impl_figment_convert_basic; use foundry_evm::hashbrown::HashMap; use std::path::PathBuf; @@ -22,19 +21,15 @@ 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}"); } else { diff --git a/crates/forge/bin/cmd/script/build.rs b/crates/forge/bin/cmd/script/build.rs index 784cd5cd7bd8..6a5a52e09bd4 100644 --- a/crates/forge/bin/cmd/script/build.rs +++ b/crates/forge/bin/cmd/script/build.rs @@ -5,7 +5,7 @@ use forge::link::{link_with_nonce_or_address, PostLinkInput, ResolvedDependency} use foundry_cli::utils::get_cached_entry_by_name; use foundry_common::{ compact_to_contract, - compile::{self, ContractSources}, + compile::{self, ContractSources, ProjectCompiler}, fs, }; use foundry_compilers::{ @@ -225,7 +225,6 @@ impl ScriptArgs { let output = compile::compile_target_with_filter( &target_contract, &project, - self.opts.args.silent, self.verify, filters, )?; @@ -243,23 +242,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/mod.rs b/crates/forge/bin/cmd/script/mod.rs index e7dab664b2eb..c3ab3caa7116 100644 --- a/crates/forge/bin/cmd/script/mod.rs +++ b/crates/forge/bin/cmd/script/mod.rs @@ -91,12 +91,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. diff --git a/crates/forge/bin/cmd/selectors.rs b/crates/forge/bin/cmd/selectors.rs index 6bcb7dde34dd..7318fa04fc2e 100644 --- a/crates/forge/bin/cmd/selectors.rs +++ b/crates/forge/bin/cmd/selectors.rs @@ -6,7 +6,7 @@ use foundry_cli::{ utils::FoundryPathExt, }; use foundry_common::{ - compile, + compile::ProjectCompiler, selectors::{import_selectors, SelectorImportData}, }; use foundry_compilers::{artifacts::output_selection::ContractOutputSelection, info::ContractInfo}; @@ -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, }, @@ -78,9 +71,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 @@ -93,7 +86,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!( @@ -122,41 +115,34 @@ impl SelectorsSubcommands { } } SelectorsSubcommands::Collision { mut first_contract, mut second_contract, build } => { - // Build first project - let first_project = build.project()?; - let first_outcome = if let Some(ref mut contract_path) = first_contract.path { + // Compile the project with the two contracts included + let project = build.project()?; + let mut compiler = ProjectCompiler::new().quiet(true); + + if let Some(contract_path) = &mut first_contract.path { let target_path = canonicalize(&*contract_path)?; *contract_path = target_path.to_string_lossy().to_string(); - compile::compile_files(&first_project, vec![target_path], true) - } else { - compile::suppress_compile(&first_project) - }?; - - // Build second project - let second_project = build.project()?; - let second_outcome = if let Some(ref mut contract_path) = second_contract.path { + compiler = compiler.files([target_path]); + } + if let Some(contract_path) = &mut second_contract.path { let target_path = canonicalize(&*contract_path)?; *contract_path = target_path.to_string_lossy().to_string(); - compile::compile_files(&second_project, vec![target_path], true) - } else { - compile::suppress_compile(&second_project) - }?; - - // Find the artifacts - let first_found_artifact = first_outcome.find_contract(&first_contract); - let second_found_artifact = second_outcome.find_contract(&second_contract); + compiler = compiler.files([target_path]); + } - // Unwrap inner artifacts - let first_artifact = first_found_artifact.ok_or_else(|| { - eyre::eyre!("Failed to extract first artifact bytecode as a string") - })?; - let second_artifact = second_found_artifact.ok_or_else(|| { - eyre::eyre!("Failed to extract second artifact bytecode as a string") - })?; + let output = compiler.compile(&project)?; // Check method selectors for collisions - let first_method_map = first_artifact.method_identifiers.as_ref().unwrap(); - let second_method_map = second_artifact.method_identifiers.as_ref().unwrap(); + let methods = |contract: &ContractInfo| -> eyre::Result<_> { + let artifact = output + .find_contract(contract) + .ok_or_else(|| eyre::eyre!("Could not find artifact for {contract}"))?; + artifact.method_identifiers.as_ref().ok_or_else(|| { + eyre::eyre!("Could not find method identifiers for {contract}") + }) + }; + let first_method_map = methods(&first_contract)?; + let second_method_map = methods(&second_contract)?; let colliding_methods: Vec<(&String, &String, &String)> = first_method_map .iter() @@ -197,7 +183,7 @@ impl SelectorsSubcommands { // compile the project to get the artifacts/abis let project = build_args.project()?; - let outcome = compile::suppress_compile(&project)?; + let outcome = ProjectCompiler::new().quiet(true).compile(&project)?; let artifacts = if let Some(contract) = contract { let found_artifact = outcome.find_first(&contract); let artifact = found_artifact diff --git a/crates/forge/bin/cmd/test/mod.rs b/crates/forge/bin/cmd/test/mod.rs index 09d92fb41230..1027ae6e4cfb 100644 --- a/crates/forge/bin/cmd/test/mod.rs +++ b/crates/forge/bin/cmd/test/mod.rs @@ -158,13 +158,10 @@ impl TestArgs { project = config.project()?; } - let compiler = ProjectCompiler::default(); - let output = match (config.sparse_mode, self.opts.silent | self.json) { - (false, false) => compiler.compile(&project), - (true, false) => compiler.compile_sparse(&project, filter.clone()), - (false, true) => compile::suppress_compile(&project), - (true, true) => compile::suppress_compile_sparse(&project, filter.clone()), - }?; + let output = ProjectCompiler::new() + .quiet_if(self.json) + .sparse(config.sparse_mode) + .compile_sparse(&project, filter.clone())?; // Create test options from general project settings // and compiler output let project_root = &project.paths.root; @@ -623,26 +620,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() diff --git a/crates/forge/bin/cmd/verify/etherscan/flatten.rs b/crates/forge/bin/cmd/verify/etherscan/flatten.rs index a9ed47bc2370..e58b784f15d9 100644 --- a/crates/forge/bin/cmd/verify/etherscan/flatten.rs +++ b/crates/forge/bin/cmd/verify/etherscan/flatten.rs @@ -80,16 +80,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/sourcify.rs b/crates/forge/bin/cmd/verify/sourcify.rs index ef9f0635018e..5299b686f13d 100644 --- a/crates/forge/bin/cmd/verify/sourcify.rs +++ b/crates/forge/bin/cmd/verify/sourcify.rs @@ -48,14 +48,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?; @@ -65,8 +61,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<()> { @@ -83,11 +78,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?)) @@ -96,8 +90,7 @@ impl VerificationProvider for SourcifyVerificationProvider { }) .await?; - self.process_sourcify_response(resp); - Ok(()) + self.process_sourcify_response(resp) } } @@ -165,21 +158,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 f2d8e99f8fdc..fbd723e7be6c 100644 --- a/crates/forge/bin/main.rs +++ b/crates/forge/bin/main.rs @@ -12,8 +12,7 @@ mod opts; use cmd::{cache::CacheSubcommands, generate::GenerateSubcommands, watch}; use opts::{Opts, Subcommands}; -fn main() -> Result<()> { - handler::install()?; + handler::install(); utils::load_dotenv(); utils::subscriber(); utils::enable_paint(); diff --git a/crates/forge/bin/opts.rs b/crates/forge/bin/opts.rs index 28b5039811ce..3fa9ece0b7f5 100644 --- a/crates/forge/bin/opts.rs +++ b/crates/forge/bin/opts.rs @@ -20,6 +20,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!( @@ -31,19 +32,22 @@ const VERSION_MESSAGE: &str = concat!( ")" ); -#[derive(Debug, Parser)] -#[clap(name = "forge", version = VERSION_MESSAGE)] +/// Build, test, fuzz, debug and deploy Solidity contracts. +#[derive(Parser)] +#[clap( + name = "forge", + version = VERSION_MESSAGE, + after_help = "Find more information in the book: http://book.getfoundry.sh/reference/forge/forge.html", + next_display_order = None, +)] pub struct Opts { #[clap(subcommand)] pub sub: Subcommands, + #[clap(flatten)] + pub shell: ShellOptions, } -#[derive(Debug, Subcommand)] -#[clap( - about = "Build, test, fuzz, debug and deploy Solidity contracts.", - after_help = "Find more information in the book: http://book.getfoundry.sh/reference/forge/forge.html", - next_display_order = None -)] +#[derive(Subcommand)] #[allow(clippy::large_enum_variant)] pub enum Subcommands { /// Run the project's tests. From f8dbc716fa9946dbf090564126154077dc76438f Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Thu, 4 Jan 2024 01:24:34 +0100 Subject: [PATCH 2/6] fixes --- crates/cast/bin/cmd/storage.rs | 7 ++++--- crates/cast/bin/main.rs | 1 + crates/cast/bin/opts.rs | 4 +--- crates/common/src/compile.rs | 22 ++++++++++------------ crates/forge/bin/cmd/build.rs | 4 +++- crates/forge/bin/cmd/inspect.rs | 21 ++++++++++++--------- crates/forge/bin/cmd/test/mod.rs | 2 +- crates/forge/bin/cmd/verify/sourcify.rs | 11 ++++++----- crates/forge/bin/main.rs | 1 + crates/forge/bin/opts.rs | 3 --- 10 files changed, 39 insertions(+), 37 deletions(-) diff --git a/crates/cast/bin/cmd/storage.rs b/crates/cast/bin/cmd/storage.rs index 7285f9397719..da9451ccc8ef 100644 --- a/crates/cast/bin/cmd/storage.rs +++ b/crates/cast/bin/cmd/storage.rs @@ -115,7 +115,7 @@ impl StorageArgs { // Not a forge project or artifact not found // Get code from Etherscan - sh_note!("No matching artifacts found, fetching source code from Etherscan...")?; + eprintln!("No matching artifacts found, fetching source code from Etherscan..."); if self.etherscan.key.is_none() { eyre::bail!("You must provide an Etherscan API key if you're fetching a remote contract's storage."); @@ -155,7 +155,7 @@ impl StorageArgs { if is_storage_layout_empty(&artifact.storage_layout) && auto_detect { // try recompiling with the minimum version - 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.")?; + 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."); let solc = Solc::find_or_install_svm_version(MIN_SOLC.to_string())?; project.solc = solc; project.auto_detect = false; @@ -217,7 +217,8 @@ async fn fetch_and_print_storage( pretty: bool, ) -> Result<()> { if is_storage_layout_empty(&artifact.storage_layout) { - sh_note!("Storage layout is empty.") + eprintln!("Storage layout is empty."); + Ok(()) } else { let layout = artifact.storage_layout.as_ref().unwrap().clone(); let values = fetch_storage_slots(provider, address, &layout).await?; diff --git a/crates/cast/bin/main.rs b/crates/cast/bin/main.rs index 4e707a2ee70a..b80d4f9285c2 100644 --- a/crates/cast/bin/main.rs +++ b/crates/cast/bin/main.rs @@ -28,6 +28,7 @@ pub mod opts; use opts::{Opts, Subcommands, ToBaseArgs}; #[tokio::main] +async fn main() -> Result<()> { handler::install(); utils::load_dotenv(); utils::subscriber(); diff --git a/crates/cast/bin/opts.rs b/crates/cast/bin/opts.rs index b1c1e1184760..df71300138be 100644 --- a/crates/cast/bin/opts.rs +++ b/crates/cast/bin/opts.rs @@ -7,7 +7,7 @@ use alloy_primitives::{Address, B256, U256}; use clap::{Parser, Subcommand, ValueHint}; use ethers_core::types::{BlockId, NameOrAddress}; use eyre::Result; -use foundry_cli::opts::{EtherscanOpts, RpcOpts, ShellOptions}; +use foundry_cli::opts::{EtherscanOpts, RpcOpts}; use std::{path::PathBuf, str::FromStr}; const VERSION_MESSAGE: &str = concat!( @@ -30,8 +30,6 @@ const VERSION_MESSAGE: &str = concat!( pub struct Opts { #[clap(subcommand)] pub sub: Subcommands, - #[clap(flatten)] - pub shell: ShellOptions, } #[derive(Subcommand)] diff --git a/crates/common/src/compile.rs b/crates/common/src/compile.rs index cef1440f4929..d136c8767692 100644 --- a/crates/common/src/compile.rs +++ b/crates/common/src/compile.rs @@ -65,7 +65,7 @@ impl ProjectCompiler { verify: None, print_names: None, print_sizes: None, - quiet: Some(crate::shell::verbosity().is_quiet()), + quiet: Some(crate::shell::verbosity().is_silent()), filters: Vec::new(), files: Vec::new(), } @@ -179,7 +179,7 @@ impl ProjectCompiler { { // TODO: Avoid process::exit if !project.paths.has_input_files() { - sh_eprintln!("Nothing to compile")?; + println!("Nothing to compile"); // nothing to do here std::process::exit(0); } @@ -189,7 +189,7 @@ impl ProjectCompiler { let reporter = if quiet { Report::new(NoReporter::default()) } else { - if crate::Shell::get().is_err_tty() { + if false { Report::new(SpinnerReporter::spawn()) } else { Report::new(BasicStdoutReporter::default()) @@ -213,10 +213,10 @@ impl ProjectCompiler { if !quiet { if output.is_unchanged() { - sh_println!("No files changed, compilation skipped")?; + println!("No files changed, compilation skipped"); } else { // print the compiler output / warnings - sh_println!("{output}")?; + println!("{output}"); } self.handle_output(&output); @@ -237,14 +237,12 @@ impl ProjectCompiler { artifacts.entry(version).or_default().push(name); } for (version, names) in artifacts { - let _ = sh_println!( + println!( " compiler version: {}.{}.{}", - version.major, - version.minor, - version.patch + version.major, version.minor, version.patch ); for name in names { - let _ = sh_println!(" - {name}"); + println!(" - {name}"); } } } @@ -252,7 +250,7 @@ impl ProjectCompiler { if print_sizes { // add extra newline if names were already printed if print_names { - let _ = sh_println!(); + println!(); } let mut size_report = SizeReport { contracts: BTreeMap::new() }; @@ -273,7 +271,7 @@ impl ProjectCompiler { size_report.contracts.insert(name, ContractInfo { size, is_dev_contract }); } - let _ = sh_println!("{size_report}"); + println!("{size_report}"); // TODO: avoid process::exit // exit with error if any contract exceeds the size limit, excluding test contracts. diff --git a/crates/forge/bin/cmd/build.rs b/crates/forge/bin/cmd/build.rs index 666b13d2d29f..20ea791a50c3 100644 --- a/crates/forge/bin/cmd/build.rs +++ b/crates/forge/bin/cmd/build.rs @@ -79,7 +79,9 @@ impl BuildArgs { let mut config = self.try_load_config_emit_warnings()?; let mut project = config.project()?; - if install::install_missing_dependencies(&mut config) && config.auto_detect_remappings { + if install::install_missing_dependencies(&mut config, self.args.silent) && + config.auto_detect_remappings + { // need to re-configure here to also catch additional remappings config = self.load_config(); project = config.project()?; diff --git a/crates/forge/bin/cmd/inspect.rs b/crates/forge/bin/cmd/inspect.rs index 33153c32740f..73322df51368 100644 --- a/crates/forge/bin/cmd/inspect.rs +++ b/crates/forge/bin/cmd/inspect.rs @@ -2,7 +2,7 @@ use clap::Parser; use comfy_table::{presets::ASCII_MARKDOWN, Table}; use eyre::Result; use foundry_cli::opts::{CompilerArgs, CoreBuildArgs}; -use foundry_common::{compile::ProjectCompiler, Shell}; +use foundry_common::compile::ProjectCompiler; use foundry_compilers::{ artifacts::{ output_selection::{ @@ -84,10 +84,10 @@ impl InspectArgs { .ok_or_else(|| eyre::eyre!("Failed to fetch lossless ABI"))?; if pretty { let source = foundry_cli::utils::abi_to_solidity(abi, "")?; - Shell::get().write_stdout(source, &Default::default()) + println!("{source}"); } else { - Shell::get().print_json(abi) - }?; + print_json(abi)?; + } } ContractArtifactField::Bytecode => { print_json_str(&artifact.bytecode, Some("object"))?; @@ -139,7 +139,7 @@ impl InspectArgs { ); } } - Shell::get().print_json(&out)?; + print_json(&out)?; } ContractArtifactField::Events => { let mut out = serde_json::Map::new(); @@ -153,7 +153,7 @@ impl InspectArgs { ); } } - Shell::get().print_json(&out)?; + print_json(&out)?; } }; @@ -186,7 +186,8 @@ pub fn print_storage_layout(storage_layout: Option<&StorageLayout>, pretty: bool ]); } - Shell::get().write_stdout(table, &Default::default()) + println!("{table}"); + Ok(()) } /// Contract level output selection @@ -360,7 +361,8 @@ impl ContractArtifactField { } fn print_json(obj: &impl serde::Serialize) -> Result<()> { - Shell::get().print_json(obj) + println!("{}", serde_json::to_string(obj)?); + Ok(()) } fn print_json_str(obj: &impl serde::Serialize, key: Option<&str>) -> Result<()> { @@ -372,7 +374,8 @@ fn print_json_str(obj: &impl serde::Serialize, key: Option<&str>) -> Result<()> } } let s = value_ref.as_str().ok_or_else(|| eyre::eyre!("not a string: {value}"))?; - sh_println!("{s}") + println!("{s}"); + Ok(()) } #[cfg(test)] diff --git a/crates/forge/bin/cmd/test/mod.rs b/crates/forge/bin/cmd/test/mod.rs index 1027ae6e4cfb..5c2c78f4eeaa 100644 --- a/crates/forge/bin/cmd/test/mod.rs +++ b/crates/forge/bin/cmd/test/mod.rs @@ -19,7 +19,7 @@ use foundry_cli::{ }; use foundry_common::{ compact_to_contract, - compile::{self, ContractSources, ProjectCompiler}, + compile::{ContractSources, ProjectCompiler}, evm::EvmArgs, get_contract_name, get_file_name, shell, }; diff --git a/crates/forge/bin/cmd/verify/sourcify.rs b/crates/forge/bin/cmd/verify/sourcify.rs index 5299b686f13d..6c17d3f1293d 100644 --- a/crates/forge/bin/cmd/verify/sourcify.rs +++ b/crates/forge/bin/cmd/verify/sourcify.rs @@ -166,17 +166,18 @@ metadata output can be enabled via `extra_output = ["metadata"]` in `foundry.tom match response.status.as_str() { "perfect" => { if let Some(ts) = &response.storage_timestamp { - sh_println!("Contract source code already verified. Storage Timestamp: {ts}") + println!("Contract source code already verified. Storage Timestamp: {ts}"); } else { - sh_println!("Contract successfully verified") + println!("Contract successfully verified"); } } "partial" => { - sh_println!("The recompiled contract partially matches the deployed version") + 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:?}")), + "false" => println!("Contract source code is not verified"), + s => eyre::bail!("Unknown status from sourcify. Status: {s:?}"), } + Ok(()) } } diff --git a/crates/forge/bin/main.rs b/crates/forge/bin/main.rs index fbd723e7be6c..819c82f430ce 100644 --- a/crates/forge/bin/main.rs +++ b/crates/forge/bin/main.rs @@ -12,6 +12,7 @@ mod opts; use cmd::{cache::CacheSubcommands, generate::GenerateSubcommands, watch}; use opts::{Opts, Subcommands}; +fn main() -> Result<()> { handler::install(); utils::load_dotenv(); utils::subscriber(); diff --git a/crates/forge/bin/opts.rs b/crates/forge/bin/opts.rs index 3fa9ece0b7f5..3b9c48680e29 100644 --- a/crates/forge/bin/opts.rs +++ b/crates/forge/bin/opts.rs @@ -20,7 +20,6 @@ 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!( @@ -43,8 +42,6 @@ const VERSION_MESSAGE: &str = concat!( pub struct Opts { #[clap(subcommand)] pub sub: Subcommands, - #[clap(flatten)] - pub shell: ShellOptions, } #[derive(Subcommand)] From f7743d7d79ecc4a79fac0b6a50c3883e5739614c Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Thu, 4 Jan 2024 01:49:45 +0100 Subject: [PATCH 3/6] fixes2 --- crates/common/src/compile.rs | 6 ++++-- crates/forge/bin/cmd/build.rs | 2 +- crates/forge/bin/cmd/create.rs | 3 ++- crates/forge/bin/cmd/script/build.rs | 10 ++++++++-- crates/forge/bin/cmd/test/mod.rs | 2 +- 5 files changed, 16 insertions(+), 7 deletions(-) diff --git a/crates/common/src/compile.rs b/crates/common/src/compile.rs index d136c8767692..14c7821ac0e1 100644 --- a/crates/common/src/compile.rs +++ b/crates/common/src/compile.rs @@ -15,6 +15,7 @@ use std::{ collections::{BTreeMap, HashMap}, convert::Infallible, fmt::Display, + io::IsTerminal, path::{Path, PathBuf}, result, str::FromStr, @@ -189,7 +190,7 @@ impl ProjectCompiler { let reporter = if quiet { Report::new(NoReporter::default()) } else { - if false { + if std::io::stdout().is_terminal() { Report::new(SpinnerReporter::spawn()) } else { Report::new(BasicStdoutReporter::default()) @@ -381,12 +382,13 @@ pub fn compile_target_with_filter( target_path: &Path, project: &Project, verify: bool, + quiet: bool, skip: Vec, ) -> Result { let graph = Graph::resolve(&project.paths)?; // Checking if it's a standalone script, or part of a project. - let mut compiler = ProjectCompiler::new().filters(skip); + let mut compiler = ProjectCompiler::new().filters(skip).quiet(quiet); if !graph.files().contains_key(target_path) { if verify { eyre::bail!("You can only verify deployments from inside a project! Make sure it exists with `forge tree`."); diff --git a/crates/forge/bin/cmd/build.rs b/crates/forge/bin/cmd/build.rs index 20ea791a50c3..0f26bcb0932e 100644 --- a/crates/forge/bin/cmd/build.rs +++ b/crates/forge/bin/cmd/build.rs @@ -69,7 +69,7 @@ pub struct BuildArgs { /// Output the compilation errors in the json format. /// This is useful when you want to use the output in other tools. - #[clap(long, conflicts_with = "quiet")] + #[clap(long, conflicts_with = "silent")] #[serde(skip)] pub format_json: bool, } diff --git a/crates/forge/bin/cmd/create.rs b/crates/forge/bin/cmd/create.rs index f9203123aa88..93ae99d8cd97 100644 --- a/crates/forge/bin/cmd/create.rs +++ b/crates/forge/bin/cmd/create.rs @@ -91,7 +91,8 @@ impl CreateArgs { pub async fn run(mut self) -> Result<()> { // Find Project & Compile let project = self.opts.project()?; - let mut output = ProjectCompiler::new().quiet_if(self.json).compile(&project)?; + let mut output = + ProjectCompiler::new().quiet_if(self.json || self.opts.silent).compile(&project)?; if let Some(ref mut path) = self.contract.path { // paths are absolute in the project's output diff --git a/crates/forge/bin/cmd/script/build.rs b/crates/forge/bin/cmd/script/build.rs index 6a5a52e09bd4..1402da19678f 100644 --- a/crates/forge/bin/cmd/script/build.rs +++ b/crates/forge/bin/cmd/script/build.rs @@ -226,6 +226,7 @@ impl ScriptArgs { &target_contract, &project, self.verify, + self.opts.args.silent, filters, )?; return Ok((project, output)) @@ -242,8 +243,13 @@ 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.verify, filters)?; + let output = compile::compile_target_with_filter( + &path, + &project, + self.verify, + self.opts.args.silent, + filters, + )?; self.path = path.to_string_lossy().to_string(); return Ok((project, output)) } diff --git a/crates/forge/bin/cmd/test/mod.rs b/crates/forge/bin/cmd/test/mod.rs index 5c2c78f4eeaa..2e8da0b89bf5 100644 --- a/crates/forge/bin/cmd/test/mod.rs +++ b/crates/forge/bin/cmd/test/mod.rs @@ -159,7 +159,7 @@ impl TestArgs { } let output = ProjectCompiler::new() - .quiet_if(self.json) + .quiet_if(self.json || self.opts.silent) .sparse(config.sparse_mode) .compile_sparse(&project, filter.clone())?; // Create test options from general project settings From db6e8b18580f873849300a6ee563f7d61d11b01d Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Thu, 4 Jan 2024 01:54:28 +0100 Subject: [PATCH 4/6] fix: format_json --- crates/common/src/compile.rs | 16 +++++++++++++++- crates/forge/bin/cmd/build.rs | 10 ++++++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/crates/common/src/compile.rs b/crates/common/src/compile.rs index 14c7821ac0e1..4dc304c8f630 100644 --- a/crates/common/src/compile.rs +++ b/crates/common/src/compile.rs @@ -43,6 +43,9 @@ pub struct ProjectCompiler { /// Whether to print anything at all. Overrides other `print` options. quiet: Option, + /// Whether to bail on compiler errors. + bail: Option, + /// Files to exclude. filters: Vec, @@ -67,6 +70,7 @@ impl ProjectCompiler { print_names: None, print_sizes: None, quiet: Some(crate::shell::verbosity().is_silent()), + bail: None, filters: Vec::new(), files: Vec::new(), } @@ -117,6 +121,13 @@ impl ProjectCompiler { self } + /// Sets whether to bail on compiler errors. + #[inline] + pub fn bail(mut self, yes: bool) -> Self { + self.bail = Some(yes); + self + } + /// Sets files to exclude. #[inline] pub fn filters(mut self, filters: impl IntoIterator) -> Self { @@ -186,6 +197,7 @@ impl ProjectCompiler { } let quiet = self.quiet.unwrap_or(false); + let bail = self.bail.unwrap_or(true); #[allow(clippy::collapsible_else_if)] let reporter = if quiet { Report::new(NoReporter::default()) @@ -208,7 +220,7 @@ impl ProjectCompiler { r })?; - if output.has_compiler_errors() { + if bail && output.has_compiler_errors() { eyre::bail!("{output}") } @@ -377,6 +389,8 @@ pub struct ContractInfo { /// /// If `verify` and it's a standalone script, throw error. Only allowed for projects. /// +/// If `quiet` no solc related output will be emitted to stdout. +/// /// **Note:** this expects the `target_path` to be absolute pub fn compile_target_with_filter( target_path: &Path, diff --git a/crates/forge/bin/cmd/build.rs b/crates/forge/bin/cmd/build.rs index 0f26bcb0932e..bd1158a9aead 100644 --- a/crates/forge/bin/cmd/build.rs +++ b/crates/forge/bin/cmd/build.rs @@ -87,11 +87,17 @@ impl BuildArgs { project = config.project()?; } - ProjectCompiler::new() + let output = ProjectCompiler::new() .print_names(self.names) .print_sizes(self.sizes) + .quiet(self.format_json) + .bail(!self.format_json) .filters(self.skip.unwrap_or_default()) - .compile(&project) + .compile(&project)?; + if self.format_json { + println!("{}", serde_json::to_string_pretty(&output.clone().output())?); + } + Ok(output) } /// Returns the `Project` for the current workspace From 83cfc0ba4c7f4360cb7720136ebcf5c6c1c25cff Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Thu, 4 Jan 2024 02:43:07 +0100 Subject: [PATCH 5/6] fix: remaining tests --- crates/common/src/compile.rs | 55 +++++++--------------------- crates/forge/bin/cmd/build.rs | 4 +- crates/forge/bin/cmd/inspect.rs | 12 +++--- crates/forge/bin/cmd/script/build.rs | 4 +- crates/forge/bin/cmd/test/mod.rs | 24 ++++++------ crates/forge/tests/cli/config.rs | 6 +-- 6 files changed, 40 insertions(+), 65 deletions(-) diff --git a/crates/common/src/compile.rs b/crates/common/src/compile.rs index 4dc304c8f630..b03e7aa9304c 100644 --- a/crates/common/src/compile.rs +++ b/crates/common/src/compile.rs @@ -25,12 +25,8 @@ use std::{ /// /// 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 compile in sparse mode. - sparse: Option, - /// Whether we are going to verify the contracts after compilation. verify: Option, @@ -47,7 +43,7 @@ pub struct ProjectCompiler { bail: Option, /// Files to exclude. - filters: Vec, + filter: Option>, /// Extra files to include, that are not necessarily in the project's source dir. files: Vec, @@ -65,24 +61,16 @@ impl ProjectCompiler { #[inline] pub fn new() -> Self { Self { - sparse: None, verify: None, print_names: None, print_sizes: None, quiet: Some(crate::shell::verbosity().is_silent()), bail: None, - filters: Vec::new(), + filter: None, files: Vec::new(), } } - /// 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 { @@ -128,10 +116,10 @@ impl ProjectCompiler { self } - /// Sets files to exclude. + /// Sets the filter to use. #[inline] - pub fn filters(mut self, filters: impl IntoIterator) -> Self { - self.filters.extend(filters); + pub fn filter(mut self, filter: Box) -> Self { + self.filter = Some(filter); self } @@ -142,16 +130,16 @@ impl ProjectCompiler { self } - /// Compiles the project with [`Project::compile()`]. + /// Compiles the project. 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 filter = std::mem::take(&mut self.filter); 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 if let Some(filter) = filter { + project.compile_sparse(move |file: &_| filter.is_match(file)) } else { project.compile() } @@ -159,21 +147,6 @@ impl ProjectCompiler { }) } - /// 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, || project.compile_sparse(filter).map_err(Into::into)) - } - /// Compiles the project with the given closure /// /// # Example @@ -185,7 +158,7 @@ impl ProjectCompiler { /// ProjectCompiler::new().compile_with(&prj, || Ok(prj.compile()?)).unwrap(); /// ``` #[instrument(target = "forge::compile", skip_all)] - pub fn compile_with(self, project: &Project, f: F) -> Result + fn compile_with(self, project: &Project, f: F) -> Result where F: FnOnce() -> Result, { @@ -387,22 +360,22 @@ pub struct ContractInfo { /// Compiles target file path. /// -/// If `verify` and it's a standalone script, throw error. Only allowed for projects. -/// /// If `quiet` 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_with_filter( target_path: &Path, project: &Project, - verify: bool, quiet: bool, + verify: bool, skip: Vec, ) -> Result { let graph = Graph::resolve(&project.paths)?; // Checking if it's a standalone script, or part of a project. - let mut compiler = ProjectCompiler::new().filters(skip).quiet(quiet); + let mut compiler = ProjectCompiler::new().filter(Box::new(SkipBuildFilters(skip))).quiet(quiet); if !graph.files().contains_key(target_path) { if verify { eyre::bail!("You can only verify deployments from inside a project! Make sure it exists with `forge tree`."); diff --git a/crates/forge/bin/cmd/build.rs b/crates/forge/bin/cmd/build.rs index bd1158a9aead..839f5a504f19 100644 --- a/crates/forge/bin/cmd/build.rs +++ b/crates/forge/bin/cmd/build.rs @@ -2,7 +2,7 @@ use super::{install, watch::WatchArgs}; use clap::Parser; use eyre::Result; use foundry_cli::{opts::CoreBuildArgs, utils::LoadConfig}; -use foundry_common::compile::{ProjectCompiler, SkipBuildFilter}; +use foundry_common::compile::{ProjectCompiler, SkipBuildFilter, SkipBuildFilters}; use foundry_compilers::{Project, ProjectCompileOutput}; use foundry_config::{ figment::{ @@ -92,7 +92,7 @@ impl BuildArgs { .print_sizes(self.sizes) .quiet(self.format_json) .bail(!self.format_json) - .filters(self.skip.unwrap_or_default()) + .filter(Box::new(SkipBuildFilters(self.skip.unwrap_or_default()))) .compile(&project)?; if self.format_json { println!("{}", serde_json::to_string_pretty(&output.clone().output())?); diff --git a/crates/forge/bin/cmd/inspect.rs b/crates/forge/bin/cmd/inspect.rs index 73322df51368..0d038c436ffc 100644 --- a/crates/forge/bin/cmd/inspect.rs +++ b/crates/forge/bin/cmd/inspect.rs @@ -96,7 +96,7 @@ impl InspectArgs { print_json_str(&artifact.deployed_bytecode, Some("object"))?; } ContractArtifactField::Assembly | ContractArtifactField::AssemblyOptimized => { - print_json(&artifact.assembly)?; + print_json_str(&artifact.assembly, None)?; } ContractArtifactField::MethodIdentifiers => { print_json(&artifact.method_identifiers)?; @@ -111,7 +111,7 @@ impl InspectArgs { print_json(&artifact.devdoc)?; } ContractArtifactField::Ir => { - print_json(&artifact.ir)?; + print_json_str(&artifact.ir, None)?; } ContractArtifactField::IrOptimized => { print_json_str(&artifact.ir_optimized, None)?; @@ -361,7 +361,7 @@ impl ContractArtifactField { } fn print_json(obj: &impl serde::Serialize) -> Result<()> { - println!("{}", serde_json::to_string(obj)?); + println!("{}", serde_json::to_string_pretty(obj)?); Ok(()) } @@ -373,8 +373,10 @@ fn print_json_str(obj: &impl serde::Serialize, key: Option<&str>) -> Result<()> value_ref = value2; } } - let s = value_ref.as_str().ok_or_else(|| eyre::eyre!("not a string: {value}"))?; - println!("{s}"); + match value_ref.as_str() { + Some(s) => println!("{s}"), + None => println!("{value_ref:#}"), + } Ok(()) } diff --git a/crates/forge/bin/cmd/script/build.rs b/crates/forge/bin/cmd/script/build.rs index 1402da19678f..27d691eeb745 100644 --- a/crates/forge/bin/cmd/script/build.rs +++ b/crates/forge/bin/cmd/script/build.rs @@ -225,8 +225,8 @@ impl ScriptArgs { let output = compile::compile_target_with_filter( &target_contract, &project, - self.verify, self.opts.args.silent, + self.verify, filters, )?; return Ok((project, output)) @@ -246,8 +246,8 @@ impl ScriptArgs { let output = compile::compile_target_with_filter( &path, &project, - self.verify, self.opts.args.silent, + self.verify, filters, )?; self.path = path.to_string_lossy().to_string(); diff --git a/crates/forge/bin/cmd/test/mod.rs b/crates/forge/bin/cmd/test/mod.rs index 2e8da0b89bf5..1f413696f8cf 100644 --- a/crates/forge/bin/cmd/test/mod.rs +++ b/crates/forge/bin/cmd/test/mod.rs @@ -142,14 +142,10 @@ impl TestArgs { // Merge all configs let (mut config, mut evm_opts) = self.load_config_and_evm_opts_emit_warnings()?; - let mut filter = self.filter(&config); - - trace!(target: "forge::test", ?filter, "using filter"); - - // Set up the project + // Set up the project. let mut project = config.project()?; - // install missing dependencies + // Install missing dependencies. if install::install_missing_dependencies(&mut config, self.build_args().silent) && config.auto_detect_remappings { @@ -158,12 +154,16 @@ impl TestArgs { project = config.project()?; } - let output = ProjectCompiler::new() - .quiet_if(self.json || self.opts.silent) - .sparse(config.sparse_mode) - .compile_sparse(&project, filter.clone())?; - // Create test options from general project settings - // and compiler output + let mut filter = self.filter(&config); + trace!(target: "forge::test", ?filter, "using filter"); + + let mut compiler = ProjectCompiler::new().quiet_if(self.json || self.opts.silent); + if config.sparse_mode { + compiler = compiler.filter(Box::new(filter.clone())); + } + let output = compiler.compile(&project)?; + + // Create test options from general project settings and compiler output. let project_root = &project.paths.root; let toml = config.get_config_path(); let profiles = get_available_profiles(toml)?; diff --git a/crates/forge/tests/cli/config.rs b/crates/forge/tests/cli/config.rs index 27001b732172..6761bb022ce0 100644 --- a/crates/forge/tests/cli/config.rs +++ b/crates/forge/tests/cli/config.rs @@ -603,10 +603,10 @@ forgetest!(config_emit_warnings, |prj, cmd| { assert_eq!( String::from_utf8_lossy(&output.stderr) .lines() - .filter(|line| { line.contains("Unknown section [default]") }) + .filter(|line| line.contains("unknown config section") && line.contains("[default]")) .count(), - 1 - ) + 1, + ); }); forgetest_init!(can_skip_remappings_auto_detection, |prj, cmd| { From 2f5e0f1f95071d3d28e808706d51d881c645efab Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Thu, 4 Jan 2024 03:14:20 +0100 Subject: [PATCH 6/6] fix: ignore private doctest --- crates/common/src/compile.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/common/src/compile.rs b/crates/common/src/compile.rs index b03e7aa9304c..7690ddbc277d 100644 --- a/crates/common/src/compile.rs +++ b/crates/common/src/compile.rs @@ -151,7 +151,7 @@ impl ProjectCompiler { /// /// # Example /// - /// ```no_run + /// ```ignore /// use foundry_common::compile::ProjectCompiler; /// let config = foundry_config::Config::load(); /// let prj = config.project().unwrap();