diff --git a/Cargo.lock b/Cargo.lock index 5c4b9c57f186..941618fb411c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3183,7 +3183,6 @@ dependencies = [ "derive_more", "eyre", "forge-fmt", - "foundry-common", "foundry-compilers", "foundry-config", "itertools 0.13.0", @@ -3436,8 +3435,6 @@ dependencies = [ "foundry-config", "foundry-linking", "foundry-macros", - "glob", - "globset", "num-format", "once_cell", "reqwest", @@ -3458,16 +3455,18 @@ dependencies = [ [[package]] name = "foundry-compilers" -version = "0.6.0" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe70a3860ec9f1861e5d82cbd4ffc55756975c0826dacf8ae4d6d696df8f7f53" +checksum = "f9a92aa3e4d0aa91fda44c1840c68d634fc126bdd06099516eb2b81035e5e6d0" dependencies = [ "alloy-json-abi", "alloy-primitives", "auto_impl", "cfg-if", + "derivative", "dirs 5.0.1", "dunce", + "dyn-clone", "fs_extra", "futures-util", "home", @@ -3508,6 +3507,7 @@ dependencies = [ "figment", "foundry-block-explorers", "foundry-compilers", + "glob", "globset", "number_prefix", "once_cell", diff --git a/Cargo.toml b/Cargo.toml index d3809d541136..4b7373c7659a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -152,7 +152,7 @@ foundry-linking = { path = "crates/linking" } # solc & compilation utilities foundry-block-explorers = { version = "0.3.0", default-features = false } -foundry-compilers = { version = "0.6.0", default-features = false } +foundry-compilers = { version = "0.6.2", default-features = false } ## revm # no default features to avoid c-kzg @@ -242,4 +242,4 @@ tower-http = "0.5" revm = { git = "https://github.com/bluealloy/revm.git", rev = "a28a543" } revm-interpreter = { git = "https://github.com/bluealloy/revm.git", rev = "a28a543" } revm-precompile = { git = "https://github.com/bluealloy/revm.git", rev = "a28a543" } -revm-primitives = { git = "https://github.com/bluealloy/revm.git", rev = "a28a543" } +revm-primitives = { git = "https://github.com/bluealloy/revm.git", rev = "a28a543" } \ No newline at end of file diff --git a/crates/cli/src/opts/build/core.rs b/crates/cli/src/opts/build/core.rs index 3d45de22577d..bf0fd019f694 100644 --- a/crates/cli/src/opts/build/core.rs +++ b/crates/cli/src/opts/build/core.rs @@ -13,6 +13,7 @@ use foundry_config::{ value::{Dict, Map, Value}, Figment, Metadata, Profile, Provider, }, + filter::SkipBuildFilter, providers::remappings::Remappings, Config, }; @@ -118,6 +119,13 @@ pub struct CoreBuildArgs { #[serde(skip_serializing_if = "Option::is_none")] pub build_info_path: Option, + /// Skip building files whose names contain the given filter. + /// + /// `test` and `script` are aliases for `.t.sol` and `.s.sol`. + #[arg(long, num_args(1..))] + #[serde(skip)] + pub skip: Option>, + #[command(flatten)] #[serde(flatten)] pub compiler: CompilerArgs, @@ -148,7 +156,7 @@ impl CoreBuildArgs { // Loads project's figment and merges the build cli arguments into it impl<'a> From<&'a CoreBuildArgs> for Figment { fn from(args: &'a CoreBuildArgs) -> Self { - let figment = if let Some(ref config_path) = args.project_paths.config_path { + let mut figment = if let Some(ref config_path) = args.project_paths.config_path { if !config_path.exists() { panic!("error: config-path `{}` does not exist", config_path.display()) } @@ -165,7 +173,15 @@ impl<'a> From<&'a CoreBuildArgs> for Figment { let mut remappings = Remappings::new_with_remappings(args.project_paths.get_remappings()); remappings .extend(figment.extract_inner::>("remappings").unwrap_or_default()); - figment.merge(("remappings", remappings.into_inner())).merge(args) + figment = figment.merge(("remappings", remappings.into_inner())).merge(args); + + if let Some(skip) = &args.skip { + let mut skip = skip.iter().map(|s| s.file_pattern().to_string()).collect::>(); + skip.extend(figment.extract_inner::>("skip").unwrap_or_default()); + figment = figment.merge(("skip", skip)); + }; + + figment } } diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 5b11a1be90a0..1a48e80acd65 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -42,8 +42,6 @@ clap = { version = "4", features = ["derive", "env", "unicode", "wrap_help"] } comfy-table = "7" dunce = "1" eyre.workspace = true -glob = "0.3" -globset = "0.4" once_cell = "1" reqwest.workspace = true semver = "1" diff --git a/crates/common/src/compile.rs b/crates/common/src/compile.rs index 13fb79be06b9..4ab4b170d9d6 100644 --- a/crates/common/src/compile.rs +++ b/crates/common/src/compile.rs @@ -1,28 +1,25 @@ //! Support for compiling [foundry_compilers::Project] -use crate::{compact_to_contract, glob::GlobMatcher, term::SpinnerReporter, TestFunctionExt}; +use crate::{compact_to_contract, term::SpinnerReporter, TestFunctionExt}; use comfy_table::{presets::ASCII_MARKDOWN, Attribute, Cell, CellAlignment, Color, Table}; use eyre::{Context, Result}; use foundry_block_explorers::contract::Metadata; use foundry_compilers::{ - artifacts::{BytecodeObject, ContractBytecodeSome, Libraries}, - compilers::{solc::SolcCompiler, Compiler}, + artifacts::{BytecodeObject, ContractBytecodeSome, Libraries, Source}, + compilers::{solc::SolcCompiler, CompilationError, Compiler}, remappings::Remapping, report::{BasicStdoutReporter, NoReporter, Report}, - Artifact, ArtifactId, FileFilter, Project, ProjectBuilder, ProjectCompileOutput, - ProjectPathsConfig, Solc, SolcConfig, SparseOutputFileFilter, + Artifact, ArtifactId, Project, ProjectBuilder, ProjectCompileOutput, ProjectPathsConfig, Solc, + SolcConfig, }; use foundry_linking::Linker; use num_format::{Locale, ToFormattedString}; use rustc_hash::FxHashMap; use std::{ collections::{BTreeMap, HashMap}, - convert::Infallible, fmt::Display, io::IsTerminal, path::{Path, PathBuf}, - result, - str::FromStr, time::Instant, }; @@ -31,7 +28,7 @@ use std::{ /// This is merely a wrapper for [`Project::compile()`] which also prints to stdout depending on its /// settings. #[must_use = "ProjectCompiler does nothing unless you call a `compile*` method"] -pub struct ProjectCompiler { +pub struct ProjectCompiler { /// Whether we are going to verify the contracts after compilation. verify: Option, @@ -47,21 +44,18 @@ pub struct ProjectCompiler { /// 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 { +impl Default for ProjectCompiler { #[inline] fn default() -> Self { Self::new() } } -impl ProjectCompiler { +impl ProjectCompiler { /// Create a new builder with the default settings. #[inline] pub fn new() -> Self { @@ -71,7 +65,6 @@ impl ProjectCompiler { print_sizes: None, quiet: Some(crate::shell::verbosity().is_silent()), bail: None, - filter: None, files: Vec::new(), } } @@ -121,13 +114,6 @@ impl ProjectCompiler { 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 { @@ -136,7 +122,7 @@ impl ProjectCompiler { } /// Compiles the project. - pub fn compile( + pub fn compile( mut self, project: &Project, ) -> Result> { @@ -148,17 +134,17 @@ impl ProjectCompiler { } // 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(|| { - if !files.is_empty() { - project.compile_files(files) - } else if let Some(filter) = filter { - project.compile_sparse(filter) + let sources = if !files.is_empty() { + Source::read_all(files)? } else { - project.compile() - } - .map_err(Into::into) + project.paths.read_input_files()? + }; + + foundry_compilers::project::ProjectCompiler::with_sources(project, sources)? + .compile() + .map_err(Into::into) }) } @@ -173,9 +159,9 @@ impl ProjectCompiler { /// ProjectCompiler::new().compile_with(|| Ok(prj.compile()?)).unwrap(); /// ``` #[instrument(target = "forge::compile", skip_all)] - fn compile_with(self, f: F) -> Result> + fn compile_with(self, f: F) -> Result> where - F: FnOnce() -> Result>, + F: FnOnce() -> Result>, { let quiet = self.quiet.unwrap_or(false); let bail = self.bail.unwrap_or(true); @@ -223,7 +209,7 @@ impl ProjectCompiler { } /// If configured, this will print sizes or names - fn handle_output(&self, output: &ProjectCompileOutput) { + fn handle_output(&self, output: &ProjectCompileOutput) { let print_names = self.print_names.unwrap_or(false); let print_sizes = self.print_sizes.unwrap_or(false); @@ -479,7 +465,7 @@ pub fn compile_target( project: &Project, quiet: bool, ) -> Result> { - ProjectCompiler::::new().quiet(quiet).files([target_path.into()]).compile(project) + ProjectCompiler::new().quiet(quiet).files([target_path.into()]).compile(project) } /// Compiles an Etherscan source from metadata by creating a project. @@ -563,125 +549,3 @@ pub fn etherscan_project( .no_artifacts() .build(compiler)?) } - -/// Bundles multiple `SkipBuildFilter` into a single `FileFilter` -#[derive(Clone, Debug)] -pub struct SkipBuildFilters { - /// All provided filters. - pub matchers: Vec, - /// Root of the project. - pub project_root: PathBuf, -} - -impl FileFilter for SkipBuildFilters { - /// Only returns a match if _no_ exclusion filter matches - fn is_match(&self, file: &Path) -> bool { - self.matchers.iter().all(|matcher| { - if !is_match_exclude(matcher, file) { - false - } else { - file.strip_prefix(&self.project_root) - .map_or(true, |stripped| is_match_exclude(matcher, stripped)) - } - }) - } -} - -impl FileFilter for &SkipBuildFilters { - fn is_match(&self, file: &Path) -> bool { - (*self).is_match(file) - } -} - -impl SkipBuildFilters { - /// Creates a new `SkipBuildFilters` from multiple `SkipBuildFilter`. - pub fn new( - filters: impl IntoIterator, - project_root: PathBuf, - ) -> Result { - let matchers = filters.into_iter().map(|m| m.compile()).collect::>(); - matchers.map(|filters| Self { matchers: filters, project_root }) - } -} - -/// A filter that excludes matching contracts from the build -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum SkipBuildFilter { - /// Exclude all `.t.sol` contracts - Tests, - /// Exclude all `.s.sol` contracts - Scripts, - /// Exclude if the file matches - Custom(String), -} - -impl SkipBuildFilter { - fn new(s: &str) -> Self { - match s { - "test" | "tests" => Self::Tests, - "script" | "scripts" => Self::Scripts, - s => Self::Custom(s.to_string()), - } - } - - /// Returns the pattern to match against a file - fn file_pattern(&self) -> &str { - match self { - Self::Tests => ".t.sol", - Self::Scripts => ".s.sol", - Self::Custom(s) => s.as_str(), - } - } - - fn compile(&self) -> Result { - self.file_pattern().parse().map_err(Into::into) - } -} - -impl FromStr for SkipBuildFilter { - type Err = Infallible; - - fn from_str(s: &str) -> result::Result { - Ok(Self::new(s)) - } -} - -/// Matches file only if the filter does not apply. -/// -/// This returns the inverse of `file.name.contains(pattern) || matcher.is_match(file)`. -fn is_match_exclude(matcher: &GlobMatcher, path: &Path) -> bool { - fn is_match(matcher: &GlobMatcher, path: &Path) -> Option { - let file_name = path.file_name()?.to_str()?; - Some(file_name.contains(matcher.as_str()) || matcher.is_match(path)) - } - - !is_match(matcher, path).unwrap_or_default() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_build_filter() { - let tests = SkipBuildFilter::Tests.compile().unwrap(); - let scripts = SkipBuildFilter::Scripts.compile().unwrap(); - let custom = |s: &str| SkipBuildFilter::Custom(s.to_string()).compile().unwrap(); - - let file = Path::new("A.t.sol"); - assert!(!is_match_exclude(&tests, file)); - assert!(is_match_exclude(&scripts, file)); - assert!(!is_match_exclude(&custom("A.t"), file)); - - let file = Path::new("A.s.sol"); - assert!(is_match_exclude(&tests, file)); - assert!(!is_match_exclude(&scripts, file)); - assert!(!is_match_exclude(&custom("A.s"), file)); - - let file = Path::new("/home/test/Foo.sol"); - assert!(!is_match_exclude(&custom("*/test/**"), file)); - - let file = Path::new("/home/script/Contract.sol"); - assert!(!is_match_exclude(&custom("*/script/**"), file)); - } -} diff --git a/crates/common/src/glob.rs b/crates/common/src/glob.rs deleted file mode 100644 index 070f703675fd..000000000000 --- a/crates/common/src/glob.rs +++ /dev/null @@ -1,94 +0,0 @@ -//! Contains `globset::Glob` wrapper functions used for filtering. - -use std::{ - fmt, - path::{Path, PathBuf}, - str::FromStr, -}; - -/// Expand globs with a root path. -pub fn expand_globs( - root: &Path, - patterns: impl IntoIterator>, -) -> eyre::Result> { - let mut expanded = Vec::new(); - for pattern in patterns { - for paths in glob::glob(&root.join(pattern.as_ref()).display().to_string())? { - expanded.push(paths?); - } - } - Ok(expanded) -} - -/// A `globset::Glob` that creates its `globset::GlobMatcher` when its created, so it doesn't need -/// to be compiled when the filter functions `TestFilter` functions are called. -#[derive(Clone, Debug)] -pub struct GlobMatcher { - /// The compiled glob - pub matcher: globset::GlobMatcher, -} - -impl GlobMatcher { - /// Creates a new `GlobMatcher` from a `globset::Glob`. - pub fn new(glob: globset::Glob) -> Self { - Self { matcher: glob.compile_matcher() } - } - - /// Tests whether the given path matches this pattern or not. - /// - /// The glob `./test/*` won't match absolute paths like `test/Contract.sol`, which is common - /// format here, so we also handle this case here - pub fn is_match(&self, path: &Path) -> bool { - if self.matcher.is_match(path) { - return true; - } - - if !path.starts_with("./") && self.as_str().starts_with("./") { - return self.matcher.is_match(format!("./{}", path.display())); - } - - false - } - - /// Returns the `globset::Glob`. - pub fn glob(&self) -> &globset::Glob { - self.matcher.glob() - } - - /// Returns the `Glob` string used to compile this matcher. - pub fn as_str(&self) -> &str { - self.glob().glob() - } -} - -impl fmt::Display for GlobMatcher { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.glob().fmt(f) - } -} - -impl FromStr for GlobMatcher { - type Err = globset::Error; - - fn from_str(s: &str) -> Result { - s.parse::().map(Self::new) - } -} - -impl From for GlobMatcher { - fn from(glob: globset::Glob) -> Self { - Self::new(glob) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn can_match_glob_paths() { - let matcher: GlobMatcher = "./test/*".parse().unwrap(); - assert!(matcher.is_match(Path::new("test/Contract.sol"))); - assert!(matcher.is_match(Path::new("./test/Contract.sol"))); - } -} diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index fa1cecbbe85d..7b1c0ff76afc 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -21,7 +21,6 @@ pub mod errors; pub mod evm; pub mod fmt; pub mod fs; -pub mod glob; pub mod provider; pub mod retry; pub mod selectors; diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml index d5a1ddda6fa9..c487c78c688e 100644 --- a/crates/config/Cargo.toml +++ b/crates/config/Cargo.toml @@ -25,6 +25,7 @@ dunce = "1" eyre.workspace = true figment = { version = "0.10", features = ["toml", "env"] } globset = "0.4" +glob = "0.3" Inflector = "0.11" number_prefix = "0.4" once_cell = "1" diff --git a/crates/config/src/filter.rs b/crates/config/src/filter.rs new file mode 100644 index 000000000000..385b442256aa --- /dev/null +++ b/crates/config/src/filter.rs @@ -0,0 +1,204 @@ +//! Helpers for constructing and using [FileFilter]s. + +use core::fmt; +use foundry_compilers::FileFilter; +use std::{ + convert::Infallible, + path::{Path, PathBuf}, + str::FromStr, +}; + +/// Expand globs with a root path. +pub fn expand_globs( + root: &Path, + patterns: impl IntoIterator>, +) -> eyre::Result> { + let mut expanded = Vec::new(); + for pattern in patterns { + for paths in glob::glob(&root.join(pattern.as_ref()).display().to_string())? { + expanded.push(paths?); + } + } + Ok(expanded) +} + +/// A `globset::Glob` that creates its `globset::GlobMatcher` when its created, so it doesn't need +/// to be compiled when the filter functions `TestFilter` functions are called. +#[derive(Clone, Debug)] +pub struct GlobMatcher { + /// The compiled glob + pub matcher: globset::GlobMatcher, +} + +impl GlobMatcher { + /// Creates a new `GlobMatcher` from a `globset::Glob`. + pub fn new(glob: globset::Glob) -> Self { + Self { matcher: glob.compile_matcher() } + } + + /// Tests whether the given path matches this pattern or not. + /// + /// The glob `./test/*` won't match absolute paths like `test/Contract.sol`, which is common + /// format here, so we also handle this case here + pub fn is_match(&self, path: &Path) -> bool { + if self.matcher.is_match(path) { + return true; + } + + if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) { + if file_name.contains(self.as_str()) { + return true; + } + } + + if !path.starts_with("./") && self.as_str().starts_with("./") { + return self.matcher.is_match(format!("./{}", path.display())); + } + + false + } + + /// Matches file only if the filter does not apply. + /// + /// This returns the inverse of `self.is_match(file)`. + fn is_match_exclude(&self, path: &Path) -> bool { + !self.is_match(path) + } + + /// Returns the `globset::Glob`. + pub fn glob(&self) -> &globset::Glob { + self.matcher.glob() + } + + /// Returns the `Glob` string used to compile this matcher. + pub fn as_str(&self) -> &str { + self.glob().glob() + } +} + +impl fmt::Display for GlobMatcher { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.glob().fmt(f) + } +} + +impl FromStr for GlobMatcher { + type Err = globset::Error; + + fn from_str(s: &str) -> Result { + s.parse::().map(Self::new) + } +} + +impl From for GlobMatcher { + fn from(glob: globset::Glob) -> Self { + Self::new(glob) + } +} + +/// Bundles multiple `SkipBuildFilter` into a single `FileFilter` +#[derive(Clone, Debug)] +pub struct SkipBuildFilters { + /// All provided filters. + pub matchers: Vec, + /// Root of the project. + pub project_root: PathBuf, +} + +impl FileFilter for SkipBuildFilters { + /// Only returns a match if _no_ exclusion filter matches + fn is_match(&self, file: &Path) -> bool { + self.matchers.iter().all(|matcher| { + if !matcher.is_match_exclude(file) { + false + } else { + file.strip_prefix(&self.project_root) + .map_or(true, |stripped| matcher.is_match_exclude(stripped)) + } + }) + } +} + +impl SkipBuildFilters { + /// Creates a new `SkipBuildFilters` from multiple `SkipBuildFilter`. + pub fn new>( + filters: impl IntoIterator, + project_root: PathBuf, + ) -> Self { + let matchers = filters.into_iter().map(|m| m.into()).collect(); + Self { matchers, project_root } + } +} + +/// A filter that excludes matching contracts from the build +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum SkipBuildFilter { + /// Exclude all `.t.sol` contracts + Tests, + /// Exclude all `.s.sol` contracts + Scripts, + /// Exclude if the file matches + Custom(String), +} + +impl SkipBuildFilter { + fn new(s: &str) -> Self { + match s { + "test" | "tests" => SkipBuildFilter::Tests, + "script" | "scripts" => SkipBuildFilter::Scripts, + s => SkipBuildFilter::Custom(s.to_string()), + } + } + + /// Returns the pattern to match against a file + pub fn file_pattern(&self) -> &str { + match self { + SkipBuildFilter::Tests => ".t.sol", + SkipBuildFilter::Scripts => ".s.sol", + SkipBuildFilter::Custom(s) => s.as_str(), + } + } +} + +impl FromStr for SkipBuildFilter { + type Err = Infallible; + + fn from_str(s: &str) -> Result { + Ok(Self::new(s)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build_filter() { + let tests = GlobMatcher::from_str(SkipBuildFilter::Tests.file_pattern()).unwrap(); + let scripts = GlobMatcher::from_str(SkipBuildFilter::Scripts.file_pattern()).unwrap(); + let custom = |s| GlobMatcher::from_str(s).unwrap(); + + let file = Path::new("A.t.sol"); + assert!(!tests.is_match_exclude(file)); + assert!(scripts.is_match_exclude(file)); + assert!(!custom("A.t").is_match_exclude(file)); + + let file = Path::new("A.s.sol"); + assert!(tests.is_match_exclude(file)); + assert!(!scripts.is_match_exclude(file)); + assert!(!custom("A.s").is_match_exclude(file)); + + let file = Path::new("/home/test/Foo.sol"); + assert!(!custom("*/test/**").is_match_exclude(file)); + + let file = Path::new("/home/script/Contract.sol"); + assert!(!custom("*/script/**").is_match_exclude(file)); + } + + #[test] + fn can_match_glob_paths() { + let matcher: GlobMatcher = "./test/*".parse().unwrap(); + assert!(matcher.is_match(Path::new("test/Contract.sol"))); + assert!(matcher.is_match(Path::new("./test/Contract.sol"))); + } +} diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 2d658894b3e3..781e6b574243 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -80,6 +80,9 @@ pub use error::SolidityErrorCode; pub mod doc; pub use doc::DocConfig; +pub mod filter; +pub use filter::SkipBuildFilters; + mod warning; pub use warning::*; @@ -171,6 +174,9 @@ pub struct Config { pub allow_paths: Vec, /// additional solc include paths for `--include-path` pub include_paths: Vec, + /// glob patterns to skip + #[serde(with = "from_vec_glob")] + pub skip: Vec, /// whether to force a `project.clean()` pub force: bool, /// evm version to use @@ -773,7 +779,7 @@ impl Config { /// Creates a [Project] with the given `cached` and `no_artifacts` flags pub fn create_project(&self, cached: bool, no_artifacts: bool) -> Result { - let project = Project::builder() + let mut builder = Project::builder() .artifacts(self.configured_artifacts_handler()) .paths(self.project_paths()) .settings(self.compiler_settings()?) @@ -787,8 +793,14 @@ impl Config { .set_offline(self.offline) .set_cached(cached && !self.build_info) .set_build_info(!no_artifacts && self.build_info) - .set_no_artifacts(no_artifacts) - .build(self.compiler()?)?; + .set_no_artifacts(no_artifacts); + + if !self.skip.is_empty() { + let filter = SkipBuildFilters::new(self.skip.clone(), self.__root.0.clone()); + builder = builder.sparse_output(filter); + } + + let project = builder.build(self.compiler()?)?; if self.force { self.cleanup(&project)?; @@ -1901,6 +1913,30 @@ pub(crate) mod from_opt_glob { } } +/// Ser/de `globset::Glob` explicitly to handle `Option` properly +pub(crate) mod from_vec_glob { + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + pub fn serialize(value: &[globset::Glob], serializer: S) -> Result + where + S: Serializer, + { + let value = value.iter().map(|g| g.glob()).collect::>(); + value.serialize(serializer) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let s: Vec = Vec::deserialize(deserializer)?; + s.into_iter() + .map(|s| globset::Glob::new(&s)) + .collect::, _>>() + .map_err(serde::de::Error::custom) + } +} + /// A helper wrapper around the root path used during Config detection #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[serde(transparent)] @@ -2065,6 +2101,7 @@ impl Default for Config { unchecked_cheatcode_artifacts: false, create2_library_salt: Config::DEFAULT_CREATE2_LIBRARY_SALT, lang: Language::Solidity, + skip: vec![], __non_exhaustive: (), __warnings: vec![], } diff --git a/crates/doc/Cargo.toml b/crates/doc/Cargo.toml index baf914347cd2..36b107a9ee76 100644 --- a/crates/doc/Cargo.toml +++ b/crates/doc/Cargo.toml @@ -15,7 +15,6 @@ workspace = true [dependencies] forge-fmt.workspace = true -foundry-common.workspace = true foundry-compilers.workspace = true foundry-config.workspace = true diff --git a/crates/doc/src/builder.rs b/crates/doc/src/builder.rs index ae51e982cf45..3c8270a3a21c 100644 --- a/crates/doc/src/builder.rs +++ b/crates/doc/src/builder.rs @@ -3,9 +3,8 @@ use crate::{ ParseSource, Parser, Preprocessor, }; use forge_fmt::{FormatterConfig, Visitable}; -use foundry_common::glob::expand_globs; use foundry_compilers::{utils::source_files_iter, SOLC_EXTENSIONS}; -use foundry_config::DocConfig; +use foundry_config::{filter::expand_globs, DocConfig}; use itertools::Itertools; use mdbook::MDBook; use rayon::prelude::*; diff --git a/crates/forge/bin/cmd/bind.rs b/crates/forge/bin/cmd/bind.rs index b4cd5ede89e6..dcf76fc2f424 100644 --- a/crates/forge/bin/cmd/bind.rs +++ b/crates/forge/bin/cmd/bind.rs @@ -6,6 +6,7 @@ use eyre::{Result, WrapErr}; use foundry_cli::{opts::CoreBuildArgs, utils::LoadConfig}; use foundry_common::{compile::ProjectCompiler, fs::json_files}; use foundry_config::impl_figment_convert; +use regex::Regex; use std::{ fs, path::{Path, PathBuf}, @@ -32,10 +33,6 @@ pub struct BindArgs { #[arg(long)] pub select: Vec, - /// Create bindings only for contracts whose names do not match the specified filter(s) - #[arg(long, conflicts_with = "select")] - pub skip: Vec, - /// Explicitly generate bindings for all contracts /// /// By default all contracts ending with `Test` or `Script` are excluded. @@ -133,18 +130,25 @@ impl BindArgs { } /// Returns the filter to use for `MultiAbigen` - fn get_filter(&self) -> ContractFilter { + fn get_filter(&self) -> Result { if self.select_all { - return ContractFilter::All + return Ok(ContractFilter::All) } if !self.select.is_empty() { - return SelectContracts::default().extend_regex(self.select.clone()).into() + return Ok(SelectContracts::default().extend_regex(self.select.clone()).into()) } - if !self.skip.is_empty() { - return ExcludeContracts::default().extend_regex(self.skip.clone()).into() + if let Some(skip) = self.build_args.skip.as_ref().filter(|s| !s.is_empty()) { + return Ok(ExcludeContracts::default() + .extend_regex( + skip.clone() + .into_iter() + .map(|s| Regex::new(s.file_pattern())) + .collect::, _>>()?, + ) + .into()) } // This excludes all Test/Script and forge-std contracts - ExcludeContracts::default() + Ok(ExcludeContracts::default() .extend_pattern([ ".*Test.*", ".*Script", @@ -155,13 +159,13 @@ impl BindArgs { "[Vv]m.*", ]) .extend_names(["IMulticall3"]) - .into() + .into()) } /// Returns an iterator over the JSON files and the contract name in the `artifacts` directory. - fn get_json_files(&self, artifacts: &Path) -> impl Iterator { - let filter = self.get_filter(); - json_files(artifacts) + fn get_json_files(&self, artifacts: &Path) -> Result> { + let filter = self.get_filter()?; + Ok(json_files(artifacts) .filter_map(|path| { // Ignore the build info JSON. if path.to_str()?.contains("/build-info/") { @@ -181,13 +185,13 @@ impl BindArgs { Some((name, path)) }) - .filter(move |(name, _path)| filter.is_match(name)) + .filter(move |(name, _path)| filter.is_match(name))) } /// Instantiate the multi-abigen fn get_multi(&self, artifacts: &Path) -> Result { let abigens = self - .get_json_files(artifacts) + .get_json_files(artifacts)? .map(|(name, path)| { trace!(?path, "parsing Abigen from file"); let abi = Abigen::new(name, path.to_str().unwrap()) diff --git a/crates/forge/bin/cmd/build.rs b/crates/forge/bin/cmd/build.rs index c2fc8088f469..e78ac283a994 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, SkipBuildFilters}; +use foundry_common::compile::ProjectCompiler; use foundry_compilers::{Project, ProjectCompileOutput}; use foundry_config::{ figment::{ @@ -52,13 +52,6 @@ pub struct BuildArgs { #[serde(skip)] pub sizes: bool, - /// Skip building files whose names contain the given filter. - /// - /// `test` and `script` are aliases for `.t.sol` and `.s.sol`. - #[arg(long, num_args(1..))] - #[serde(skip)] - pub skip: Option>, - #[command(flatten)] #[serde(flatten)] pub args: CoreBuildArgs, @@ -87,19 +80,12 @@ impl BuildArgs { let project = config.project()?; - let mut compiler = ProjectCompiler::new() + let compiler = ProjectCompiler::new() .print_names(self.names) .print_sizes(self.sizes) .quiet(self.format_json) .bail(!self.format_json); - if let Some(ref skip) = self.skip { - if !skip.is_empty() { - let filter = SkipBuildFilters::new(skip.clone(), project.root().clone())?; - compiler = compiler.filter(Box::new(filter)); - } - }; - let output = compiler.compile(&project)?; if self.format_json { @@ -160,21 +146,22 @@ impl Provider for BuildArgs { #[cfg(test)] mod tests { use super::*; + use foundry_config::filter::SkipBuildFilter; #[test] fn can_parse_build_filters() { let args: BuildArgs = BuildArgs::parse_from(["foundry-cli", "--skip", "tests"]); - assert_eq!(args.skip, Some(vec![SkipBuildFilter::Tests])); + assert_eq!(args.args.skip, Some(vec![SkipBuildFilter::Tests])); let args: BuildArgs = BuildArgs::parse_from(["foundry-cli", "--skip", "scripts"]); - assert_eq!(args.skip, Some(vec![SkipBuildFilter::Scripts])); + assert_eq!(args.args.skip, Some(vec![SkipBuildFilter::Scripts])); let args: BuildArgs = BuildArgs::parse_from(["foundry-cli", "--skip", "tests", "--skip", "scripts"]); - assert_eq!(args.skip, Some(vec![SkipBuildFilter::Tests, SkipBuildFilter::Scripts])); + assert_eq!(args.args.skip, Some(vec![SkipBuildFilter::Tests, SkipBuildFilter::Scripts])); let args: BuildArgs = BuildArgs::parse_from(["foundry-cli", "--skip", "tests", "scripts"]); - assert_eq!(args.skip, Some(vec![SkipBuildFilter::Tests, SkipBuildFilter::Scripts])); + assert_eq!(args.args.skip, Some(vec![SkipBuildFilter::Tests, SkipBuildFilter::Scripts])); } #[test] diff --git a/crates/forge/bin/cmd/fmt.rs b/crates/forge/bin/cmd/fmt.rs index 34df8dbcf5e1..b62f6a7eb56d 100644 --- a/crates/forge/bin/cmd/fmt.rs +++ b/crates/forge/bin/cmd/fmt.rs @@ -2,9 +2,9 @@ use clap::{Parser, ValueHint}; use eyre::Result; use forge_fmt::{format_to, parse, print_diagnostics_report}; use foundry_cli::utils::{FoundryPathExt, LoadConfig}; -use foundry_common::{fs, glob::expand_globs, term::cli_warn}; +use foundry_common::{fs, term::cli_warn}; use foundry_compilers::{compilers::solc::SolcLanguage, SOLC_EXTENSIONS}; -use foundry_config::impl_figment_convert_basic; +use foundry_config::{filter::expand_globs, impl_figment_convert_basic}; use rayon::prelude::*; use similar::{ChangeTag, TextDiff}; use std::{ diff --git a/crates/forge/bin/cmd/test/filter.rs b/crates/forge/bin/cmd/test/filter.rs index 1a7f355d7e75..7ececa7d4ae3 100644 --- a/crates/forge/bin/cmd/test/filter.rs +++ b/crates/forge/bin/cmd/test/filter.rs @@ -1,8 +1,7 @@ use clap::Parser; use forge::TestFilter; -use foundry_common::glob::GlobMatcher; use foundry_compilers::{FileFilter, ProjectPathsConfig}; -use foundry_config::Config; +use foundry_config::{filter::GlobMatcher, Config}; use std::{fmt, path::Path}; /// The filter to use during testing. diff --git a/crates/forge/bin/cmd/test/mod.rs b/crates/forge/bin/cmd/test/mod.rs index 165a07374766..3f54e01e531b 100644 --- a/crates/forge/bin/cmd/test/mod.rs +++ b/crates/forge/bin/cmd/test/mod.rs @@ -162,7 +162,7 @@ impl TestArgs { *selection = OutputSelection::common_output_selection(["abi".to_string()]); }); - let output = project.compile_sparse(Box::new(filter.clone()))?; + let output = project.compile()?; if output.has_compiler_errors() { println!("{output}"); diff --git a/crates/forge/tests/cli/build.rs b/crates/forge/tests/cli/build.rs index 668e4be5e1de..e2dca7e0aaa2 100644 --- a/crates/forge/tests/cli/build.rs +++ b/crates/forge/tests/cli/build.rs @@ -1,4 +1,6 @@ +use foundry_config::Config; use foundry_test_utils::{forgetest, util::OutputExt}; +use globset::Glob; use std::path::PathBuf; // tests that json is printed when --json is passed @@ -40,3 +42,30 @@ forgetest_init!(build_sizes_no_forge_std, |prj, cmd| { assert!(!stdout.contains("std"), "\n{stdout}"); assert!(stdout.contains("Counter"), "\n{stdout}"); }); + +// tests that skip key in config can be used to skip non-compilable contract +forgetest_init!(test_can_skip_contract, |prj, cmd| { + prj.add_source( + "InvalidContract", + r" +contract InvalidContract { + some_invalid_syntax +} +", + ) + .unwrap(); + + prj.add_source( + "ValidContract", + r" +contract ValidContract {} +", + ) + .unwrap(); + + let config = + Config { skip: vec![Glob::new("src/InvalidContract.sol").unwrap()], ..Default::default() }; + prj.write_config(config); + + cmd.args(["build"]).assert_success(); +}); diff --git a/crates/forge/tests/cli/config.rs b/crates/forge/tests/cli/config.rs index 31a9ec1fae01..f26f8243ea20 100644 --- a/crates/forge/tests/cli/config.rs +++ b/crates/forge/tests/cli/config.rs @@ -137,6 +137,7 @@ forgetest!(can_extract_config_values, |prj, cmd| { unchecked_cheatcode_artifacts: false, create2_library_salt: Config::DEFAULT_CREATE2_LIBRARY_SALT, lang: Language::Solidity, + skip: vec![], __non_exhaustive: (), __warnings: vec![], }; diff --git a/crates/script/src/lib.rs b/crates/script/src/lib.rs index 3cea29f20279..087834dce9d5 100644 --- a/crates/script/src/lib.rs +++ b/crates/script/src/lib.rs @@ -22,7 +22,6 @@ use forge_verify::RetryArgs; use foundry_cli::{opts::CoreBuildArgs, utils::LoadConfig}; use foundry_common::{ abi::{encode_function_args, get_func}, - compile::SkipBuildFilter, evm::{Breakpoints, EvmArgs}, shell, ContractsByArtifact, CONTRACT_MAX_SIZE, SELECTOR_LEN, }; @@ -180,12 +179,6 @@ pub struct ScriptArgs { )] pub with_gas_price: Option, - /// Skip building files whose names contain the given filter. - /// - /// `test` and `script` are aliases for `.t.sol` and `.s.sol`. - #[arg(long, num_args(1..))] - pub skip: Option>, - #[command(flatten)] pub opts: CoreBuildArgs, diff --git a/crates/verify/src/bytecode.rs b/crates/verify/src/bytecode.rs index b49af60063eb..b39bad21cec9 100644 --- a/crates/verify/src/bytecode.rs +++ b/crates/verify/src/bytecode.rs @@ -8,16 +8,13 @@ use foundry_cli::{ opts::EtherscanOpts, utils::{self, read_constructor_args_file, LoadConfig}, }; -use foundry_common::{ - compile::{ProjectCompiler, SkipBuildFilter, SkipBuildFilters}, - provider::ProviderBuilder, -}; +use foundry_common::{compile::ProjectCompiler, provider::ProviderBuilder}; use foundry_compilers::{ artifacts::{BytecodeHash, BytecodeObject, CompactContractBytecode}, info::ContractInfo, Artifact, EvmVersion, }; -use foundry_config::{figment, impl_figment_convert, Chain, Config}; +use foundry_config::{figment, filter::SkipBuildFilter, impl_figment_convert, Chain, Config}; use foundry_evm::{ constants::DEFAULT_CREATE2_DEPLOYER, executors::TracingExecutor, utils::configure_tx_env, }; @@ -375,14 +372,8 @@ impl VerifyBytecodeArgs { fn build_project(&self, config: &Config) -> Result { let project = config.project()?; - let mut compiler = ProjectCompiler::new(); + let compiler = ProjectCompiler::new(); - if let Some(skip) = &self.skip { - if !skip.is_empty() { - let filter = SkipBuildFilters::new(skip.to_owned(), project.root().to_path_buf())?; - compiler = compiler.filter(Box::new(filter)); - } - } let output = compiler.compile(&project)?; let artifact = output