From afaf3b4c2cfc99ee167bdbf9a08559d1a3fc26ed Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Thu, 4 Jan 2024 21:04:25 +0100 Subject: [PATCH] refactor: rewrite compile functions to a common builder (#6702) * refactor: rewrite compile functions to a common builder * fixes * fixes2 * fix: format_json * fix: remaining tests * fix: ignore private doctest --- crates/cast/bin/cmd/access_list.rs | 1 - crates/cast/bin/cmd/call.rs | 1 - crates/cast/bin/cmd/storage.rs | 8 +- crates/cast/bin/cmd/wallet/mod.rs | 5 +- crates/cast/bin/main.rs | 2 +- crates/cast/bin/opts.rs | 17 +- 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 | 393 ++++++++---------- 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 | 25 +- crates/forge/bin/cmd/create.rs | 11 +- crates/forge/bin/cmd/flatten.rs | 5 +- crates/forge/bin/cmd/inspect.rs | 125 +++--- crates/forge/bin/cmd/remappings.rs | 15 +- crates/forge/bin/cmd/script/build.rs | 8 +- crates/forge/bin/cmd/script/mod.rs | 7 +- crates/forge/bin/cmd/selectors.rs | 74 ++-- crates/forge/bin/cmd/test/mod.rs | 38 +- .../forge/bin/cmd/verify/etherscan/flatten.rs | 10 +- crates/forge/bin/cmd/verify/sourcify.rs | 51 ++- crates/forge/bin/main.rs | 2 +- crates/forge/bin/opts.rs | 17 +- crates/forge/tests/cli/config.rs | 6 +- 31 files changed, 394 insertions(+), 528 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..da9451ccc8ef 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()?; @@ -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() @@ -159,7 +159,7 @@ impl StorageArgs { let solc = Solc::find_or_install_svm_version(MIN_SOLC.to_string())?; project.solc = solc; project.auto_detect = false; - if let Ok(output) = suppress_compile(&project) { + if let Ok(output) = ProjectCompiler::new().quiet(true).compile(&project) { out = output; let (_, new_artifact) = out .artifacts() diff --git a/crates/cast/bin/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..b80d4f9285c2 100644 --- a/crates/cast/bin/main.rs +++ b/crates/cast/bin/main.rs @@ -29,7 +29,7 @@ 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..df71300138be 100644 --- a/crates/cast/bin/opts.rs +++ b/crates/cast/bin/opts.rs @@ -19,19 +19,20 @@ 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, } -/// 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..7690ddbc277d 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, }; @@ -14,105 +15,195 @@ use std::{ collections::{BTreeMap, HashMap}, convert::Infallible, fmt::Display, + io::IsTerminal, path::{Path, PathBuf}, result, 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. +#[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 - filters: Vec, + /// 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, + + /// Whether to bail on compiler errors. + bail: Option, + + /// Files to exclude. + filter: Option>, + + /// 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 { + verify: None, + print_names: None, + print_sizes: None, + quiet: Some(crate::shell::verbosity().is_silent()), + bail: None, + filter: None, + 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 we are going to verify the contracts after compilation. + #[inline] + pub fn verify(mut self, yes: bool) -> Self { + self.verify = 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() - } else { - prj.compile_sparse(SkipBuildFilters(filters)) - }?; - Ok(output) - }) + /// Sets whether to print contract names. + #[inline] + pub fn print_names(mut self, yes: bool) -> Self { + self.print_names = Some(yes); + self } - /// Compiles the project with [`Project::compile_parse()`] 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. - pub fn compile_sparse( - self, - project: &Project, - filter: F, - ) -> Result { - self.compile_with(project, |prj| Ok(prj.compile_sparse(filter)?)) + /// 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 whether to bail on compiler errors. + #[inline] + pub fn bail(mut self, yes: bool) -> Self { + self.bail = Some(yes); + self + } + + /// Sets the filter to use. + #[inline] + pub fn filter(mut self, filter: Box) -> Self { + self.filter = Some(filter); + 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. + pub fn compile(mut self, project: &Project) -> Result { + // Taking is fine since we don't need these in `compile_with`. + 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 let Some(filter) = filter { + project.compile_sparse(move |file: &_| filter.is_match(file)) + } else { + project.compile() + } + .map_err(Into::into) + }) } /// Compiles the project with the given closure /// /// # Example /// - /// ```no_run + /// ```ignore /// 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 + 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"); // 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); + let bail = self.bail.unwrap_or(true); + #[allow(clippy::collapsible_else_if)] + let reporter = if quiet { + Report::new(NoReporter::default()) + } else { + if std::io::stdout().is_terminal() { + Report::new(SpinnerReporter::spawn()) + } else { + Report::new(BasicStdoutReporter::default()) + } + }; + + let output = foundry_compilers::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(); - trace!(?elapsed, "finished compiling"); + 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}"); + if bail && output.has_compiler_errors() { + eyre::bail!("{output}") + } + + if !quiet { + if output.is_unchanged() { + println!("No files changed, compilation skipped"); + } else { + // print the compiler output / warnings + println!("{output}"); + } self.handle_output(&output); } @@ -122,8 +213,11 @@ impl ProjectCompiler { /// If configured, this will print sizes or names fn handle_output(&self, output: &ProjectCompileOutput) { + let print_names = self.print_names.unwrap_or(false); + let print_sizes = self.print_sizes.unwrap_or(false); + // print any sizes or names - if self.print_names { + if print_names { let mut artifacts: BTreeMap<_, Vec<_>> = BTreeMap::new(); for (name, (_, version)) in output.versioned_artifacts() { artifacts.entry(version).or_default().push(name); @@ -138,11 +232,13 @@ impl ProjectCompiler { } } } - if self.print_sizes { + + if print_sizes { // add extra newline if names were already printed - if self.print_names { + if print_names { println!(); } + let mut size_report = SizeReport { contracts: BTreeMap::new() }; let artifacts: BTreeMap<_, _> = output.artifacts().collect(); for (name, artifact) in artifacts { @@ -163,6 +259,7 @@ impl ProjectCompiler { 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 +277,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 +358,31 @@ 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 `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( - 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, + 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. - if graph.files().get(target_path).is_none() { + 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`."); } - 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 +397,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..839f5a504f19 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, SkipBuildFilters}; use foundry_compilers::{Project, ProjectCompileOutput}; use foundry_config::{ figment::{ @@ -90,19 +87,17 @@ impl BuildArgs { project = config.project()?; } - let filters = self.skip.unwrap_or_default(); - + let output = ProjectCompiler::new() + .print_names(self.names) + .print_sizes(self.sizes) + .quiet(self.format_json) + .bail(!self.format_json) + .filter(Box::new(SkipBuildFilters(self.skip.unwrap_or_default()))) + .compile(&project)?; 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) + println!("{}", serde_json::to_string_pretty(&output.clone().output())?); } + Ok(output) } /// 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..93ae99d8cd97 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,8 @@ 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 || 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/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..0d038c436ffc 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; 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, "")?; + println!("{source}"); + } else { + 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_str(&artifact.assembly, None)?; } 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_str(&artifact.ir, None)?; } 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)?); + 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)?); + 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(); @@ -235,7 +187,6 @@ pub fn print_storage_layout(storage_layout: Option<&StorageLayout>, pretty: bool } println!("{table}"); - Ok(()) } @@ -409,6 +360,26 @@ impl ContractArtifactField { } } +fn print_json(obj: &impl serde::Serialize) -> Result<()> { + println!("{}", serde_json::to_string_pretty(obj)?); + Ok(()) +} + +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; + } + } + match value_ref.as_str() { + Some(s) => println!("{s}"), + None => println!("{value_ref:#}"), + } + Ok(()) +} + #[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..27d691eeb745 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::{ @@ -255,11 +255,7 @@ impl ScriptArgs { } // 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..1f413696f8cf 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, }; @@ -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,15 +154,16 @@ 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()), - }?; - // 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)?; @@ -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..6c17d3f1293d 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,22 +158,26 @@ 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 { + println!("Contract source code already verified. Storage Timestamp: {ts}"); + } else { + 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" => { + println!("The recompiled contract partially matches the deployed version"); + } + "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 f2d8e99f8fdc..819c82f430ce 100644 --- a/crates/forge/bin/main.rs +++ b/crates/forge/bin/main.rs @@ -13,7 +13,7 @@ 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..3b9c48680e29 100644 --- a/crates/forge/bin/opts.rs +++ b/crates/forge/bin/opts.rs @@ -31,19 +31,20 @@ 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, } -#[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/tests/cli/config.rs b/crates/forge/tests/cli/config.rs index d54f1af54885..c6c04bf5d756 100644 --- a/crates/forge/tests/cli/config.rs +++ b/crates/forge/tests/cli/config.rs @@ -605,10 +605,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| {