From 7f3666126e265c67a2664728a150a78527ce48ec Mon Sep 17 00:00:00 2001 From: lazymio Date: Wed, 21 May 2025 17:07:55 +0800 Subject: [PATCH 01/57] Support `transient` in `StorageLocation` (#269) Trivial patch. The new variant `transient` seems missing. --- crates/artifacts/solc/src/ast/misc.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/artifacts/solc/src/ast/misc.rs b/crates/artifacts/solc/src/ast/misc.rs index 7144ddc..dc765d4 100644 --- a/crates/artifacts/solc/src/ast/misc.rs +++ b/crates/artifacts/solc/src/ast/misc.rs @@ -92,6 +92,7 @@ pub enum StorageLocation { Default, Memory, Storage, + Transient, } /// Visibility specifier. From 507cd414fa374b50bb3d58f7501f5e477b73a9c8 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Wed, 21 May 2025 11:08:52 +0200 Subject: [PATCH 02/57] chore: release 0.16.2 --- CHANGELOG.md | 10 ++++++++++ Cargo.toml | 12 ++++++------ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94f65b9..c478eeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,22 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.16.2](https://github.com/foundry-rs/compilers/releases/tag/v0.16.2) - 2025-05-21 + +### Other + +- Support `transient` in `StorageLocation` ([#269](https://github.com/foundry-rs/compilers/issues/269)) + ## [0.16.1](https://github.com/foundry-rs/compilers/releases/tag/v0.16.1) - 2025-05-16 ### Bug Fixes - Is_dirty to use additional_files ([#268](https://github.com/foundry-rs/compilers/issues/268)) +### Miscellaneous Tasks + +- Release 0.16.1 + ## [0.16.0](https://github.com/foundry-rs/compilers/releases/tag/v0.16.0) - 2025-05-12 ### Dependencies diff --git a/Cargo.toml b/Cargo.toml index ec5da8f..562f6d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ resolver = "2" [workspace.package] authors = ["Foundry Maintainers"] -version = "0.16.1" +version = "0.16.2" rust-version = "1.86" readme = "README.md" license = "MIT OR Apache-2.0" @@ -36,11 +36,11 @@ redundant-lifetimes = "warn" all = "warn" [workspace.dependencies] -foundry-compilers = { path = "crates/compilers", version = "0.16.1" } -foundry-compilers-artifacts = { path = "crates/artifacts/artifacts", version = "0.16.1" } -foundry-compilers-artifacts-solc = { path = "crates/artifacts/solc", version = "0.16.1" } -foundry-compilers-artifacts-vyper = { path = "crates/artifacts/vyper", version = "0.16.1" } -foundry-compilers-core = { path = "crates/core", version = "0.16.1" } +foundry-compilers = { path = "crates/compilers", version = "0.16.2" } +foundry-compilers-artifacts = { path = "crates/artifacts/artifacts", version = "0.16.2" } +foundry-compilers-artifacts-solc = { path = "crates/artifacts/solc", version = "0.16.2" } +foundry-compilers-artifacts-vyper = { path = "crates/artifacts/vyper", version = "0.16.2" } +foundry-compilers-core = { path = "crates/core", version = "0.16.2" } alloy-json-abi = { version = "1.0", features = ["serde_json"] } alloy-primitives = { version = "1.0", features = ["serde", "rand"] } From 61ef8a311b7cdf20bc7523213c1439b67394a8b7 Mon Sep 17 00:00:00 2001 From: VolodymyrBg Date: Wed, 21 May 2025 16:08:35 +0300 Subject: [PATCH 03/57] fix: update Tera documentation link in cliff.toml (#270) Replaced the outdated or broken link to the Tera template engine documentation in cliff.toml with the current one: https://keats.github.io/tera/docs/#introduction This change helps users quickly find up-to-date documentation for the template syntax used in the changelog configuration. --- cliff.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cliff.toml b/cliff.toml index aa3c5c0..3f2f805 100644 --- a/cliff.toml +++ b/cliff.toml @@ -10,7 +10,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n """ -# https://tera.netlify.app/docs/#introduction +# https://keats.github.io/tera/docs/#introduction body = """ {% if version %}\ ## [{{ version | trim_start_matches(pat="v") }}](https://github.com/foundry-rs/compilers/releases/tag/v{{ version | trim_start_matches(pat="v") }}) - {{ timestamp | date(format="%Y-%m-%d") }} From 2c90332e7e906c9ec75f2d965aa95548435e7fad Mon Sep 17 00:00:00 2001 From: lazymio Date: Fri, 23 May 2025 19:13:34 +0800 Subject: [PATCH 04/57] Some fields are optional during `"stopAfter":"parsing"` (#271) Trivial patch. Reproduce: ```bash echo '{"language":"Solidity","sources":{"test.sol":{"content":"contract TT {\\n function main() public {}\\n}"}},"settings":{"stopAfter":"parsing","optimizer":{"enabled":false,"runs":200},"outputSelection":{"*":{"":["ast"],"*":[]}},"viaIR":true,"libraries":{}}}' | solc --standard-json ``` Since there is no semantics analysis when stopping after parsing, `scope` along with a few other fields are not available and this PR marks all `scope` to be optional. --- crates/artifacts/solc/src/ast/mod.rs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/crates/artifacts/solc/src/ast/mod.rs b/crates/artifacts/solc/src/ast/mod.rs index 92e9959..90cf1e6 100644 --- a/crates/artifacts/solc/src/ast/mod.rs +++ b/crates/artifacts/solc/src/ast/mod.rs @@ -175,10 +175,14 @@ ast_node!( #[serde(rename = "contractKind")] kind: ContractKind, documentation: Option, - fully_implemented: bool, + // Not available when "stopAfter": "parsing" is specified. + fully_implemented: Option, + // Not available when "stopAfter": "parsing" is specified. + #[serde(default)] linearized_base_contracts: Vec, nodes: Vec, - scope: usize, + // Not available when "stopAfter": "parsing" is specified. + scope: Option, #[serde(default, deserialize_with = "serde_helpers::default_for_null")] used_errors: Vec, #[serde(default, deserialize_with = "serde_helpers::default_for_null")] @@ -536,7 +540,8 @@ ast_node!( #[serde(default)] mutability: Option, overrides: Option, - scope: usize, + // Not available when "stopAfter": "parsing" is specified. + scope: Option, storage_location: StorageLocation, type_descriptions: TypeDescriptions, type_name: Option, @@ -713,7 +718,8 @@ ast_node!( overrides: Option, parameters: ParameterList, return_parameters: ParameterList, - scope: usize, + // Not available when "stopAfter": "parsing" is specified. + scope: Option, visibility: Visibility, /// The kind of function this node defines. Only valid for Solidity versions 0.5.x and /// above. @@ -1028,7 +1034,8 @@ ast_node!( name_location: Option, canonical_name: String, members: Vec, - scope: usize, + // Not available when "stopAfter": "parsing" is specified. + scope: Option, visibility: Visibility, } ); @@ -1082,7 +1089,8 @@ ast_node!( file: String, #[serde(default, with = "serde_helpers::display_from_str_opt")] name_location: Option, - scope: usize, + // Not available when "stopAfter": "parsing" is specified. + scope: Option, source_unit: usize, symbol_aliases: Vec, unit_alias: String, From 64cf128b01c7406b26f6953443cb69822d963878 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Tue, 27 May 2025 14:30:47 +0200 Subject: [PATCH 05/57] chore: clean up error! calls (#273) I got a `ERROR` log when using `forge flatten` with no extra message, and the error itself gets ignored with a fallback --------- Co-authored-by: Matthias Seitz --- crates/compilers/src/artifact_output/mod.rs | 25 +++++++++---------- .../compilers/src/compilers/solc/compiler.rs | 2 +- crates/compilers/src/resolver/mod.rs | 10 +++++--- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/crates/compilers/src/artifact_output/mod.rs b/crates/compilers/src/artifact_output/mod.rs index ec2a7d0..ccd079a 100644 --- a/crates/compilers/src/artifact_output/mod.rs +++ b/crates/compilers/src/artifact_output/mod.rs @@ -625,10 +625,8 @@ pub trait ArtifactOutput { ) -> Result> { let mut artifacts = self.output_to_artifacts(contracts, sources, ctx, layout, primary_profiles); - fs::create_dir_all(&layout.artifacts).map_err(|err| { - error!(dir=?layout.artifacts, "Failed to create artifacts folder"); - SolcIoError::new(err, &layout.artifacts) - })?; + fs::create_dir_all(&layout.artifacts) + .map_err(|err| SolcIoError::new(err, &layout.artifacts))?; artifacts.join_all(&layout.artifacts); artifacts.write_all()?; @@ -1140,16 +1138,17 @@ impl ArtifactOutput for MinimalCombinedArtifactsHardhatFallback { } fn read_cached_artifact(path: &Path) -> Result { - let content = fs::read_to_string(path).map_err(|err| SolcError::io(err, path))?; - if let Ok(a) = serde_json::from_str(&content) { - Ok(a) - } else { - error!("Failed to deserialize compact artifact"); - trace!("Fallback to hardhat artifact deserialization"); - let artifact = serde_json::from_str::(&content)?; - trace!("successfully deserialized hardhat artifact"); - Ok(artifact.into_contract_bytecode()) + #[derive(Deserialize)] + #[serde(untagged)] + enum Artifact { + Compact(CompactContractBytecode), + Hardhat(HardhatArtifact), } + + Ok(match utils::read_json_file::(path)? { + Artifact::Compact(c) => c, + Artifact::Hardhat(h) => h.into_contract_bytecode(), + }) } fn contract_to_artifact( diff --git a/crates/compilers/src/compilers/solc/compiler.rs b/crates/compilers/src/compilers/solc/compiler.rs index 194b37f..cd4d755 100644 --- a/crates/compilers/src/compilers/solc/compiler.rs +++ b/crates/compilers/src/compilers/solc/compiler.rs @@ -54,7 +54,7 @@ pub static RELEASES: std::sync::LazyLock<(svm::Releases, Vec, bool)> = (releases, sorted_versions, true) } Err(err) => { - error!("{:?}", err); + error!("failed to deserialize SVM static RELEASES JSON: {err}"); Default::default() } } diff --git a/crates/compilers/src/resolver/mod.rs b/crates/compilers/src/resolver/mod.rs index 4b0c722..d91592e 100644 --- a/crates/compilers/src/resolver/mod.rs +++ b/crates/compilers/src/resolver/mod.rs @@ -917,8 +917,9 @@ impl> Graph { .collect(), ); } else { - error!("failed to resolve versions"); - return Err(SolcError::msg(errors.join("\n"))); + let s = errors.join("\n"); + debug!("failed to resolve versions: {s}"); + return Err(SolcError::msg(s)); } } @@ -960,8 +961,9 @@ impl> Graph { if errors.is_empty() { Ok(resulted_sources) } else { - error!("failed to resolve settings"); - Err(SolcError::msg(errors.join("\n"))) + let s = errors.join("\n"); + debug!("failed to resolve settings: {s}"); + Err(SolcError::msg(s)) } } From 628b40d6eb5b8e44d364b4de3ebafa47dda033d0 Mon Sep 17 00:00:00 2001 From: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Date: Wed, 28 May 2025 18:26:07 +0200 Subject: [PATCH 06/57] chore: switch to `Prague` hardfork by default (#272) Related PR: https://github.com/foundry-rs/foundry/pull/10565 --- crates/artifacts/solc/src/lib.rs | 4 ++-- crates/compilers/tests/project.rs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/artifacts/solc/src/lib.rs b/crates/artifacts/solc/src/lib.rs index 3f3a741..83f9c4b 100644 --- a/crates/artifacts/solc/src/lib.rs +++ b/crates/artifacts/solc/src/lib.rs @@ -793,7 +793,7 @@ impl YulDetails { /// EVM versions. /// -/// Default is `Cancun`, since 0.8.25 +/// Default is `Prague`, since 0.8.30 /// /// Kept in sync with: // When adding new EVM versions (see a previous attempt at https://github.com/foundry-rs/compilers/pull/51): @@ -816,8 +816,8 @@ pub enum EvmVersion { London, Paris, Shanghai, - #[default] Cancun, + #[default] Prague, Osaka, } diff --git a/crates/compilers/tests/project.rs b/crates/compilers/tests/project.rs index d8c0852..334d438 100644 --- a/crates/compilers/tests/project.rs +++ b/crates/compilers/tests/project.rs @@ -2678,7 +2678,7 @@ fn can_create_standard_json_input_with_external_file() { ] ); - let solc = Solc::find_or_install(&Version::new(0, 8, 24)).unwrap(); + let solc = Solc::find_or_install(&Version::new(0, 8, 27)).unwrap(); // can compile using the created json let compiler_errors = solc @@ -2703,7 +2703,7 @@ fn can_compile_std_json_input() { assert!(input.sources.contains_key(Path::new("lib/ds-test/src/test.sol"))); // should be installed - if let Ok(solc) = Solc::find_or_install(&Version::new(0, 8, 24)) { + if let Ok(solc) = Solc::find_or_install(&Version::new(0, 8, 28)) { let out = solc.compile(&input).unwrap(); assert!(out.errors.is_empty()); assert!(out.sources.contains_key(Path::new("lib/ds-test/src/test.sol"))); @@ -2767,7 +2767,7 @@ fn can_create_standard_json_input_with_symlink() { ] ); - let solc = Solc::find_or_install(&Version::new(0, 8, 24)).unwrap(); + let solc = Solc::find_or_install(&Version::new(0, 8, 28)).unwrap(); // can compile using the created json let compiler_errors = solc @@ -2936,7 +2936,7 @@ async fn can_install_solc_and_compile_std_json_input_async() { tmp.assert_no_errors(); let source = tmp.list_source_files().into_iter().find(|p| p.ends_with("Dapp.t.sol")).unwrap(); let input = tmp.project().standard_json_input(&source).unwrap(); - let solc = Solc::find_or_install(&Version::new(0, 8, 24)).unwrap(); + let solc = Solc::find_or_install(&Version::new(0, 8, 27)).unwrap(); assert!(input.settings.remappings.contains(&"ds-test/=lib/ds-test/src/".parse().unwrap())); let input: SolcInput = input.into(); From e6cb9563b5cf1ece8c73b90da47bfec0d86321f1 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Wed, 28 May 2025 19:47:47 +0200 Subject: [PATCH 07/57] chore: release 0.16.3 --- CHANGELOG.md | 19 +++++++++++++++++++ Cargo.toml | 12 ++++++------ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c478eeb..547780f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,27 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.16.3](https://github.com/foundry-rs/compilers/releases/tag/v0.16.3) - 2025-05-28 + +### Bug Fixes + +- Update Tera documentation link in cliff.toml ([#270](https://github.com/foundry-rs/compilers/issues/270)) + +### Miscellaneous Tasks + +- Switch to `Prague` hardfork by default ([#272](https://github.com/foundry-rs/compilers/issues/272)) +- Clean up error! calls ([#273](https://github.com/foundry-rs/compilers/issues/273)) + +### Other + +- Some fields are optional during `"stopAfter":"parsing"` ([#271](https://github.com/foundry-rs/compilers/issues/271)) + ## [0.16.2](https://github.com/foundry-rs/compilers/releases/tag/v0.16.2) - 2025-05-21 +### Miscellaneous Tasks + +- Release 0.16.2 + ### Other - Support `transient` in `StorageLocation` ([#269](https://github.com/foundry-rs/compilers/issues/269)) diff --git a/Cargo.toml b/Cargo.toml index 562f6d9..c8c3eef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ resolver = "2" [workspace.package] authors = ["Foundry Maintainers"] -version = "0.16.2" +version = "0.16.3" rust-version = "1.86" readme = "README.md" license = "MIT OR Apache-2.0" @@ -36,11 +36,11 @@ redundant-lifetimes = "warn" all = "warn" [workspace.dependencies] -foundry-compilers = { path = "crates/compilers", version = "0.16.2" } -foundry-compilers-artifacts = { path = "crates/artifacts/artifacts", version = "0.16.2" } -foundry-compilers-artifacts-solc = { path = "crates/artifacts/solc", version = "0.16.2" } -foundry-compilers-artifacts-vyper = { path = "crates/artifacts/vyper", version = "0.16.2" } -foundry-compilers-core = { path = "crates/core", version = "0.16.2" } +foundry-compilers = { path = "crates/compilers", version = "0.16.3" } +foundry-compilers-artifacts = { path = "crates/artifacts/artifacts", version = "0.16.3" } +foundry-compilers-artifacts-solc = { path = "crates/artifacts/solc", version = "0.16.3" } +foundry-compilers-artifacts-vyper = { path = "crates/artifacts/vyper", version = "0.16.3" } +foundry-compilers-core = { path = "crates/core", version = "0.16.3" } alloy-json-abi = { version = "1.0", features = ["serde_json"] } alloy-primitives = { version = "1.0", features = ["serde", "rand"] } From 72690449bff0434beeaff470adc641e56c0c41f0 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> Date: Thu, 29 May 2025 18:44:04 +0200 Subject: [PATCH 08/57] chore: bump solar v0.1.4 (#275) --- .github/workflows/ci.yml | 4 ++-- Cargo.toml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0da36e8..3e05f11 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,11 +22,11 @@ jobs: fail-fast: false matrix: os: ["ubuntu-latest", "macos-latest", "windows-latest"] - rust: ["stable", "1.86"] + rust: ["stable", "1.87"] flags: ["", "--all-features"] exclude: # Skip because some features have higher MSRV. - - rust: "1.86" # MSRV + - rust: "1.87" # MSRV flags: "--all-features" steps: - uses: actions/checkout@v4 diff --git a/Cargo.toml b/Cargo.toml index c8c3eef..a210048 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ resolver = "2" [workspace.package] authors = ["Foundry Maintainers"] version = "0.16.3" -rust-version = "1.86" +rust-version = "1.87" readme = "README.md" license = "MIT OR Apache-2.0" repository = "https://github.com/foundry-rs/compilers" @@ -54,8 +54,8 @@ semver = { version = "1.0", features = ["serde"] } serde = { version = "1", features = ["derive", "rc"] } serde_json = "1.0" similar-asserts = "1" -solar-parse = { version = "=0.1.3", default-features = false } -solar-sema = { version = "=0.1.3", default-features = false } +solar-parse = { version = "=0.1.4", default-features = false } +solar-sema = { version = "=0.1.4", default-features = false } svm = { package = "svm-rs", version = "0.5", default-features = false } tempfile = "3.9" thiserror = "2" From dda285e0579b2a96e450bd214e33454a8e479941 Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Thu, 29 May 2025 21:06:36 +0400 Subject: [PATCH 09/57] chore: release 0.16.4 --- CHANGELOG.md | 230 +++++++++++++-------------------------------------- Cargo.toml | 12 +-- 2 files changed, 62 insertions(+), 180 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 547780f..dcfda9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,164 +5,80 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.16.3](https://github.com/foundry-rs/compilers/releases/tag/v0.16.3) - 2025-05-28 +## [0.16.4](https://github.com/foundry-rs/compilers/releases/tag/v0.16.4) - 2025-05-29 ### Bug Fixes - Update Tera documentation link in cliff.toml ([#270](https://github.com/foundry-rs/compilers/issues/270)) - -### Miscellaneous Tasks - -- Switch to `Prague` hardfork by default ([#272](https://github.com/foundry-rs/compilers/issues/272)) -- Clean up error! calls ([#273](https://github.com/foundry-rs/compilers/issues/273)) - -### Other - -- Some fields are optional during `"stopAfter":"parsing"` ([#271](https://github.com/foundry-rs/compilers/issues/271)) - -## [0.16.2](https://github.com/foundry-rs/compilers/releases/tag/v0.16.2) - 2025-05-21 - -### Miscellaneous Tasks - -- Release 0.16.2 - -### Other - -- Support `transient` in `StorageLocation` ([#269](https://github.com/foundry-rs/compilers/issues/269)) - -## [0.16.1](https://github.com/foundry-rs/compilers/releases/tag/v0.16.1) - 2025-05-16 - -### Bug Fixes - - Is_dirty to use additional_files ([#268](https://github.com/foundry-rs/compilers/issues/268)) - -### Miscellaneous Tasks - -- Release 0.16.1 - -## [0.16.0](https://github.com/foundry-rs/compilers/releases/tag/v0.16.0) - 2025-05-12 +- Fix Update CONTRIBUTING.md ([#261](https://github.com/foundry-rs/compilers/issues/261)) +- Missing check for normalization ([#257](https://github.com/foundry-rs/compilers/issues/257)) +- Update normalization ([#256](https://github.com/foundry-rs/compilers/issues/256)) +- Allow top level event declarations ([#251](https://github.com/foundry-rs/compilers/issues/251)) +- Ordering for flattener ([#247](https://github.com/foundry-rs/compilers/issues/247)) +- Handle displaying multiline errors correctly ([#245](https://github.com/foundry-rs/compilers/issues/245)) ### Dependencies +- Bump solar v0.1.4 ([#275](https://github.com/foundry-rs/compilers/issues/275)) - Bump solar version ([#264](https://github.com/foundry-rs/compilers/issues/264)) - -### Miscellaneous Tasks - -- Release 0.16.0 - -## [0.15.0](https://github.com/foundry-rs/compilers/releases/tag/v0.15.0) - 2025-05-07 - -### Dependencies - - [deps] Bump alloy 1.0 ([#263](https://github.com/foundry-rs/compilers/issues/263)) +- [deps] Bump dirs ([#243](https://github.com/foundry-rs/compilers/issues/243)) ### Documentation - Update CHANGELOG.md -### Miscellaneous Tasks - -- Release 0.15.0 - -## [0.14.1](https://github.com/foundry-rs/compilers/releases/tag/v0.14.1) - 2025-04-19 - -### Bug Fixes - -- Fix Update CONTRIBUTING.md ([#261](https://github.com/foundry-rs/compilers/issues/261)) - -### Miscellaneous Tasks - -- Release 0.14.1 - -### Performance - -- Switch md5 to xxhash ([#262](https://github.com/foundry-rs/compilers/issues/262)) - -## [0.14.0](https://github.com/foundry-rs/compilers/releases/tag/v0.14.0) - 2025-04-07 - ### Features - Add support for preprocessing sources ([#252](https://github.com/foundry-rs/compilers/issues/252)) +- Add osaka evm version ([#254](https://github.com/foundry-rs/compilers/issues/254)) +- Impl `.path(&self)` for `ContractInfo` ([#250](https://github.com/foundry-rs/compilers/issues/250)) ### Miscellaneous Tasks +- Release 0.16.3 +- Switch to `Prague` hardfork by default ([#272](https://github.com/foundry-rs/compilers/issues/272)) +- Clean up error! calls ([#273](https://github.com/foundry-rs/compilers/issues/273)) +- Release 0.16.2 +- Release 0.16.1 +- Release 0.16.0 +- Release 0.15.0 +- Release 0.14.1 - Release 0.14.0 - Simplify pragma parsing ([#260](https://github.com/foundry-rs/compilers/issues/260)) - -### Styling - -- Update file extension for compatibility ([#258](https://github.com/foundry-rs/compilers/issues/258)) - -## [0.13.5](https://github.com/foundry-rs/compilers/releases/tag/v0.13.5) - 2025-03-14 - -### Bug Fixes - -- Missing check for normalization ([#257](https://github.com/foundry-rs/compilers/issues/257)) - -### Miscellaneous Tasks - - Release 0.13.5 - -## [0.13.4](https://github.com/foundry-rs/compilers/releases/tag/v0.13.4) - 2025-03-14 - -### Bug Fixes - -- Update normalization ([#256](https://github.com/foundry-rs/compilers/issues/256)) - -### Features - -- Add osaka evm version ([#254](https://github.com/foundry-rs/compilers/issues/254)) - -### Miscellaneous Tasks - - Release 0.13.4 +- Release 0.13.3 +- Release 0.13.2 +- Fix spelling issues ([#248](https://github.com/foundry-rs/compilers/issues/248)) +- Release 0.13.1 +- Clippy + winnow 0.7 ([#244](https://github.com/foundry-rs/compilers/issues/244)) +- Call shrink_to_fit afte parsing source maps ([#242](https://github.com/foundry-rs/compilers/issues/242)) ### Other +- Some fields are optional during `"stopAfter":"parsing"` ([#271](https://github.com/foundry-rs/compilers/issues/271)) +- Support `transient` in `StorageLocation` ([#269](https://github.com/foundry-rs/compilers/issues/269)) - Allow unmaintained paste ([#255](https://github.com/foundry-rs/compilers/issues/255)) -## [0.13.3](https://github.com/foundry-rs/compilers/releases/tag/v0.13.3) - 2025-02-14 - -### Bug Fixes - -- Allow top level event declarations ([#251](https://github.com/foundry-rs/compilers/issues/251)) - -### Features - -- Impl `.path(&self)` for `ContractInfo` ([#250](https://github.com/foundry-rs/compilers/issues/250)) - -### Miscellaneous Tasks - -- Release 0.13.3 - -## [0.13.2](https://github.com/foundry-rs/compilers/releases/tag/v0.13.2) - 2025-02-06 - -### Bug Fixes +### Performance -- Ordering for flattener ([#247](https://github.com/foundry-rs/compilers/issues/247)) +- Switch md5 to xxhash ([#262](https://github.com/foundry-rs/compilers/issues/262)) -### Miscellaneous Tasks +### Styling -- Release 0.13.2 -- Fix spelling issues ([#248](https://github.com/foundry-rs/compilers/issues/248)) +- Update file extension for compatibility ([#258](https://github.com/foundry-rs/compilers/issues/258)) -## [0.13.1](https://github.com/foundry-rs/compilers/releases/tag/v0.13.1) - 2025-02-02 +## [0.13.0](https://github.com/foundry-rs/compilers/releases/tag/v0.13.0) - 2025-01-21 ### Bug Fixes -- Handle displaying multiline errors correctly ([#245](https://github.com/foundry-rs/compilers/issues/245)) +- EvmVersion `from_str` ([#235](https://github.com/foundry-rs/compilers/issues/235)) ### Dependencies -- [deps] Bump dirs ([#243](https://github.com/foundry-rs/compilers/issues/243)) - -### Miscellaneous Tasks - -- Release 0.13.1 -- Clippy + winnow 0.7 ([#244](https://github.com/foundry-rs/compilers/issues/244)) -- Call shrink_to_fit afte parsing source maps ([#242](https://github.com/foundry-rs/compilers/issues/242)) - -## [0.13.0](https://github.com/foundry-rs/compilers/releases/tag/v0.13.0) - 2025-01-21 +- [deps] Bump solar 0.1.1 ([#237](https://github.com/foundry-rs/compilers/issues/237)) ### Features @@ -173,19 +89,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Release 0.13.0 - More lints ([#238](https://github.com/foundry-rs/compilers/issues/238)) - -## [0.12.9](https://github.com/foundry-rs/compilers/releases/tag/v0.12.9) - 2025-01-05 - -### Bug Fixes - -- EvmVersion `from_str` ([#235](https://github.com/foundry-rs/compilers/issues/235)) - -### Dependencies - -- [deps] Bump solar 0.1.1 ([#237](https://github.com/foundry-rs/compilers/issues/237)) - -### Miscellaneous Tasks - - Release 0.12.9 - Clippy ([#236](https://github.com/foundry-rs/compilers/issues/236)) @@ -194,65 +97,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Bug Fixes - Correctly merge restrictions ([#234](https://github.com/foundry-rs/compilers/issues/234)) +- Vyper version comparison typo ([#232](https://github.com/foundry-rs/compilers/issues/232)) +- Add fallback parser for contract names ([#229](https://github.com/foundry-rs/compilers/issues/229)) +- Fix minor grammatical issue in project documentation ([#226](https://github.com/foundry-rs/compilers/issues/226)) -### Miscellaneous Tasks - -- Release 0.12.8 - -### Other - -- Move deny to ci ([#233](https://github.com/foundry-rs/compilers/issues/233)) - -## [0.12.7](https://github.com/foundry-rs/compilers/releases/tag/v0.12.7) - 2024-12-05 - -### Bug Fixes +### Dependencies -- Vyper version comparison typo ([#232](https://github.com/foundry-rs/compilers/issues/232)) +- Bump MSRV to 1.83 ([#230](https://github.com/foundry-rs/compilers/issues/230)) ### Miscellaneous Tasks +- Release 0.12.8 - Release 0.12.7 +- Release 0.12.6 +- Release 0.12.5 +- Release 0.12.5 +- Release 0.12.4 -## [0.12.6](https://github.com/foundry-rs/compilers/releases/tag/v0.12.6) - 2024-12-04 - -### Miscellaneous Tasks +### Other -- Release 0.12.6 +- Move deny to ci ([#233](https://github.com/foundry-rs/compilers/issues/233)) +- Add note about grammar,spelling prs ([#228](https://github.com/foundry-rs/compilers/issues/228)) ### Performance - Don't request unnecessary output ([#231](https://github.com/foundry-rs/compilers/issues/231)) -## [0.12.5](https://github.com/foundry-rs/compilers/releases/tag/v0.12.5) - 2024-12-04 - -### Miscellaneous Tasks - -- Release 0.12.5 -- Release 0.12.5 - ### Refactor - Make Contract generic for Compiler and add metadata to CompilerOutput ([#224](https://github.com/foundry-rs/compilers/issues/224)) -## [0.12.4](https://github.com/foundry-rs/compilers/releases/tag/v0.12.4) - 2024-12-02 - -### Bug Fixes - -- Add fallback parser for contract names ([#229](https://github.com/foundry-rs/compilers/issues/229)) -- Fix minor grammatical issue in project documentation ([#226](https://github.com/foundry-rs/compilers/issues/226)) - -### Dependencies - -- Bump MSRV to 1.83 ([#230](https://github.com/foundry-rs/compilers/issues/230)) - -### Miscellaneous Tasks - -- Release 0.12.4 - -### Other - -- Add note about grammar,spelling prs ([#228](https://github.com/foundry-rs/compilers/issues/228)) - ## [0.12.3](https://github.com/foundry-rs/compilers/releases/tag/v0.12.3) - 2024-11-20 ### Bug Fixes @@ -291,7 +165,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Sanitize `settings.optimizer.details.inliner` ([#216](https://github.com/foundry-rs/compilers/issues/216)) - [tests] Always try installing pinned solc ([#217](https://github.com/foundry-rs/compilers/issues/217)) - Outdated merge build error -- Correctly handle b as pre-release in Vyper version ([#213](https://github.com/foundry-rs/compilers/issues/213)) ### Features @@ -304,6 +177,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Remove outdated `ref` patterns ([#218](https://github.com/foundry-rs/compilers/issues/218)) - Inline constants in Settings::sanitize ([#219](https://github.com/foundry-rs/compilers/issues/219)) - Use Version::new over .parse ([#220](https://github.com/foundry-rs/compilers/issues/220)) + +## [0.11.6](https://github.com/foundry-rs/compilers/releases/tag/v0.11.6) - 2024-10-16 + +### Bug Fixes + +- Correctly handle b as pre-release in Vyper version ([#213](https://github.com/foundry-rs/compilers/issues/213)) + +### Miscellaneous Tasks + - Release 0.11.6 ## [0.11.5](https://github.com/foundry-rs/compilers/releases/tag/v0.11.5) - 2024-10-14 diff --git a/Cargo.toml b/Cargo.toml index a210048..4f15139 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ resolver = "2" [workspace.package] authors = ["Foundry Maintainers"] -version = "0.16.3" +version = "0.16.4" rust-version = "1.87" readme = "README.md" license = "MIT OR Apache-2.0" @@ -36,11 +36,11 @@ redundant-lifetimes = "warn" all = "warn" [workspace.dependencies] -foundry-compilers = { path = "crates/compilers", version = "0.16.3" } -foundry-compilers-artifacts = { path = "crates/artifacts/artifacts", version = "0.16.3" } -foundry-compilers-artifacts-solc = { path = "crates/artifacts/solc", version = "0.16.3" } -foundry-compilers-artifacts-vyper = { path = "crates/artifacts/vyper", version = "0.16.3" } -foundry-compilers-core = { path = "crates/core", version = "0.16.3" } +foundry-compilers = { path = "crates/compilers", version = "0.16.4" } +foundry-compilers-artifacts = { path = "crates/artifacts/artifacts", version = "0.16.4" } +foundry-compilers-artifacts-solc = { path = "crates/artifacts/solc", version = "0.16.4" } +foundry-compilers-artifacts-vyper = { path = "crates/artifacts/vyper", version = "0.16.4" } +foundry-compilers-core = { path = "crates/core", version = "0.16.4" } alloy-json-abi = { version = "1.0", features = ["serde_json"] } alloy-primitives = { version = "1.0", features = ["serde", "rand"] } From 4226be4b0fa56a67bde8c43caee6c11ab1dfd0cc Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Thu, 29 May 2025 21:33:57 +0400 Subject: [PATCH 10/57] chore: release 0.17.0 --- CHANGELOG.md | 1 + Cargo.toml | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dcfda9e..1402d0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Miscellaneous Tasks +- Release 0.16.4 - Release 0.16.3 - Switch to `Prague` hardfork by default ([#272](https://github.com/foundry-rs/compilers/issues/272)) - Clean up error! calls ([#273](https://github.com/foundry-rs/compilers/issues/273)) diff --git a/Cargo.toml b/Cargo.toml index 4f15139..79fa09e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ resolver = "2" [workspace.package] authors = ["Foundry Maintainers"] -version = "0.16.4" +version = "0.17.0" rust-version = "1.87" readme = "README.md" license = "MIT OR Apache-2.0" @@ -36,11 +36,11 @@ redundant-lifetimes = "warn" all = "warn" [workspace.dependencies] -foundry-compilers = { path = "crates/compilers", version = "0.16.4" } -foundry-compilers-artifacts = { path = "crates/artifacts/artifacts", version = "0.16.4" } -foundry-compilers-artifacts-solc = { path = "crates/artifacts/solc", version = "0.16.4" } -foundry-compilers-artifacts-vyper = { path = "crates/artifacts/vyper", version = "0.16.4" } -foundry-compilers-core = { path = "crates/core", version = "0.16.4" } +foundry-compilers = { path = "crates/compilers", version = "0.17.0" } +foundry-compilers-artifacts = { path = "crates/artifacts/artifacts", version = "0.17.0" } +foundry-compilers-artifacts-solc = { path = "crates/artifacts/solc", version = "0.17.0" } +foundry-compilers-artifacts-vyper = { path = "crates/artifacts/vyper", version = "0.17.0" } +foundry-compilers-core = { path = "crates/core", version = "0.17.0" } alloy-json-abi = { version = "1.0", features = ["serde_json"] } alloy-primitives = { version = "1.0", features = ["serde", "rand"] } From 8352a621f8f5da02473fb18dcb2d66d63bdfc8b8 Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Thu, 29 May 2025 21:41:55 +0400 Subject: [PATCH 11/57] chore: release 0.17.0 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1402d0d..63b485a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.17.0](https://github.com/foundry-rs/compilers/releases/tag/v0.17.0) - 2025-05-29 + +### Miscellaneous Tasks + +- Release 0.17.0 + ## [0.16.4](https://github.com/foundry-rs/compilers/releases/tag/v0.16.4) - 2025-05-29 ### Bug Fixes From 283899fc4d83e594f7612003065a88d7bcff52c7 Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Thu, 29 May 2025 21:43:16 +0400 Subject: [PATCH 12/57] chore: release 0.17.0 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63b485a..7d0e273 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Miscellaneous Tasks +- Release 0.17.0 - Release 0.17.0 ## [0.16.4](https://github.com/foundry-rs/compilers/releases/tag/v0.16.4) - 2025-05-29 From 3e04a0e51dcac3099f44dab8a54d8adc0fa70d64 Mon Sep 17 00:00:00 2001 From: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Date: Mon, 2 Jun 2025 13:55:04 +0200 Subject: [PATCH 13/57] chore: add language matcher on `MultiCompilerLanguage` (#276) --- crates/compilers/src/compilers/multi.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/compilers/src/compilers/multi.rs b/crates/compilers/src/compilers/multi.rs index d206076..f729d50 100644 --- a/crates/compilers/src/compilers/multi.rs +++ b/crates/compilers/src/compilers/multi.rs @@ -66,6 +66,16 @@ pub enum MultiCompilerLanguage { Vyper(VyperLanguage), } +impl MultiCompilerLanguage { + pub fn is_vyper(&self) -> bool { + matches!(self, Self::Vyper(_)) + } + + pub fn is_solc(&self) -> bool { + matches!(self, Self::Solc(_)) + } +} + impl From for MultiCompilerLanguage { fn from(language: SolcLanguage) -> Self { Self::Solc(language) From e035bbcede0d8f7b0bba20a8385581bc0d811c88 Mon Sep 17 00:00:00 2001 From: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Date: Mon, 2 Jun 2025 15:19:28 +0200 Subject: [PATCH 14/57] chore: update MSRV policy, bump to `1.87` in `clippy.toml` in line with `CI` and `Cargo.toml` (#277) Closes: #266 --- README.md | 7 +++---- clippy.toml | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 4ad5fae..7ed2475 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Foundry Compilers + | [Docs](https://docs.rs/foundry-compilers/latest/foundry_compilers/) | Originally part of [`ethers-rs`] as `ethers-solc`, Foundry Compilers is the compilation backend for [Foundry](https://github.com/foundry-rs/foundry). @@ -24,9 +25,7 @@ When updating this, also update: - .github/workflows/ci.yml --> -Foundry Compilers will keep a rolling MSRV (minimum supported rust version) policy of **at -least** 6 months. When increasing the MSRV, the new Rust version must have been -released at least six months ago. The current MSRV is 1.86.0. +The current MSRV (minimum supported rust version) is 1.87. Note that the MSRV is not increased automatically, and only as part of a minor release. @@ -46,7 +45,7 @@ To install, simply add `foundry-compilers` to your cargo dependencies. ```toml [dependencies] -foundry-compilers = "0.10.1" +foundry-compilers = "" ``` Example usage: diff --git a/clippy.toml b/clippy.toml index 80fcb9d..d946dae 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1 +1 @@ -msrv = "1.86" +msrv = "1.87" From 9cadfd252ba7dd4ff65435cf91dca6e459b67f8c Mon Sep 17 00:00:00 2001 From: zerosnacks Date: Mon, 2 Jun 2025 16:07:00 +0200 Subject: [PATCH 15/57] bump to latest version in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7ed2475..2b636be 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ To install, simply add `foundry-compilers` to your cargo dependencies. ```toml [dependencies] -foundry-compilers = "" +foundry-compilers = "0.17.1" ``` Example usage: From dabbb0a80d391322072e84aa40c596410f5fe9bc Mon Sep 17 00:00:00 2001 From: zerosnacks Date: Mon, 2 Jun 2025 16:21:58 +0200 Subject: [PATCH 16/57] chore: release 0.17.1 --- CHANGELOG.md | 252 +++++++++++++++++++++++++++++++++++++++------------ Cargo.toml | 12 +-- 2 files changed, 202 insertions(+), 62 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d0e273..2bfc74b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,88 +5,194 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.17.1](https://github.com/foundry-rs/compilers/releases/tag/v0.17.1) - 2025-06-02 + +### Dependencies + +- Bump to latest version in README +- Update MSRV policy, bump to `1.87` in `clippy.toml` in line with `CI` and `Cargo.toml` ([#277](https://github.com/foundry-rs/compilers/issues/277)) + +### Miscellaneous Tasks + +- Add language matcher on `MultiCompilerLanguage` ([#276](https://github.com/foundry-rs/compilers/issues/276)) + ## [0.17.0](https://github.com/foundry-rs/compilers/releases/tag/v0.17.0) - 2025-05-29 ### Miscellaneous Tasks +- Release 0.17.0 - Release 0.17.0 - Release 0.17.0 ## [0.16.4](https://github.com/foundry-rs/compilers/releases/tag/v0.16.4) - 2025-05-29 +### Dependencies + +- Bump solar v0.1.4 ([#275](https://github.com/foundry-rs/compilers/issues/275)) + +### Miscellaneous Tasks + +- Release 0.16.4 + +## [0.16.3](https://github.com/foundry-rs/compilers/releases/tag/v0.16.3) - 2025-05-28 + ### Bug Fixes - Update Tera documentation link in cliff.toml ([#270](https://github.com/foundry-rs/compilers/issues/270)) + +### Miscellaneous Tasks + +- Release 0.16.3 +- Switch to `Prague` hardfork by default ([#272](https://github.com/foundry-rs/compilers/issues/272)) +- Clean up error! calls ([#273](https://github.com/foundry-rs/compilers/issues/273)) + +### Other + +- Some fields are optional during `"stopAfter":"parsing"` ([#271](https://github.com/foundry-rs/compilers/issues/271)) + +## [0.16.2](https://github.com/foundry-rs/compilers/releases/tag/v0.16.2) - 2025-05-21 + +### Miscellaneous Tasks + +- Release 0.16.2 + +### Other + +- Support `transient` in `StorageLocation` ([#269](https://github.com/foundry-rs/compilers/issues/269)) + +## [0.16.1](https://github.com/foundry-rs/compilers/releases/tag/v0.16.1) - 2025-05-16 + +### Bug Fixes + - Is_dirty to use additional_files ([#268](https://github.com/foundry-rs/compilers/issues/268)) -- Fix Update CONTRIBUTING.md ([#261](https://github.com/foundry-rs/compilers/issues/261)) -- Missing check for normalization ([#257](https://github.com/foundry-rs/compilers/issues/257)) -- Update normalization ([#256](https://github.com/foundry-rs/compilers/issues/256)) -- Allow top level event declarations ([#251](https://github.com/foundry-rs/compilers/issues/251)) -- Ordering for flattener ([#247](https://github.com/foundry-rs/compilers/issues/247)) -- Handle displaying multiline errors correctly ([#245](https://github.com/foundry-rs/compilers/issues/245)) + +### Miscellaneous Tasks + +- Release 0.16.1 + +## [0.16.0](https://github.com/foundry-rs/compilers/releases/tag/v0.16.0) - 2025-05-12 ### Dependencies -- Bump solar v0.1.4 ([#275](https://github.com/foundry-rs/compilers/issues/275)) - Bump solar version ([#264](https://github.com/foundry-rs/compilers/issues/264)) + +### Miscellaneous Tasks + +- Release 0.16.0 + +## [0.15.0](https://github.com/foundry-rs/compilers/releases/tag/v0.15.0) - 2025-05-07 + +### Dependencies + - [deps] Bump alloy 1.0 ([#263](https://github.com/foundry-rs/compilers/issues/263)) -- [deps] Bump dirs ([#243](https://github.com/foundry-rs/compilers/issues/243)) ### Documentation - Update CHANGELOG.md +### Miscellaneous Tasks + +- Release 0.15.0 + +## [0.14.1](https://github.com/foundry-rs/compilers/releases/tag/v0.14.1) - 2025-04-19 + +### Bug Fixes + +- Fix Update CONTRIBUTING.md ([#261](https://github.com/foundry-rs/compilers/issues/261)) + +### Miscellaneous Tasks + +- Release 0.14.1 + +### Performance + +- Switch md5 to xxhash ([#262](https://github.com/foundry-rs/compilers/issues/262)) + +## [0.14.0](https://github.com/foundry-rs/compilers/releases/tag/v0.14.0) - 2025-04-07 + ### Features - Add support for preprocessing sources ([#252](https://github.com/foundry-rs/compilers/issues/252)) -- Add osaka evm version ([#254](https://github.com/foundry-rs/compilers/issues/254)) -- Impl `.path(&self)` for `ContractInfo` ([#250](https://github.com/foundry-rs/compilers/issues/250)) ### Miscellaneous Tasks -- Release 0.16.4 -- Release 0.16.3 -- Switch to `Prague` hardfork by default ([#272](https://github.com/foundry-rs/compilers/issues/272)) -- Clean up error! calls ([#273](https://github.com/foundry-rs/compilers/issues/273)) -- Release 0.16.2 -- Release 0.16.1 -- Release 0.16.0 -- Release 0.15.0 -- Release 0.14.1 - Release 0.14.0 - Simplify pragma parsing ([#260](https://github.com/foundry-rs/compilers/issues/260)) + +### Styling + +- Update file extension for compatibility ([#258](https://github.com/foundry-rs/compilers/issues/258)) + +## [0.13.5](https://github.com/foundry-rs/compilers/releases/tag/v0.13.5) - 2025-03-14 + +### Bug Fixes + +- Missing check for normalization ([#257](https://github.com/foundry-rs/compilers/issues/257)) + +### Miscellaneous Tasks + - Release 0.13.5 + +## [0.13.4](https://github.com/foundry-rs/compilers/releases/tag/v0.13.4) - 2025-03-14 + +### Bug Fixes + +- Update normalization ([#256](https://github.com/foundry-rs/compilers/issues/256)) + +### Features + +- Add osaka evm version ([#254](https://github.com/foundry-rs/compilers/issues/254)) + +### Miscellaneous Tasks + - Release 0.13.4 -- Release 0.13.3 -- Release 0.13.2 -- Fix spelling issues ([#248](https://github.com/foundry-rs/compilers/issues/248)) -- Release 0.13.1 -- Clippy + winnow 0.7 ([#244](https://github.com/foundry-rs/compilers/issues/244)) -- Call shrink_to_fit afte parsing source maps ([#242](https://github.com/foundry-rs/compilers/issues/242)) ### Other -- Some fields are optional during `"stopAfter":"parsing"` ([#271](https://github.com/foundry-rs/compilers/issues/271)) -- Support `transient` in `StorageLocation` ([#269](https://github.com/foundry-rs/compilers/issues/269)) - Allow unmaintained paste ([#255](https://github.com/foundry-rs/compilers/issues/255)) -### Performance +## [0.13.3](https://github.com/foundry-rs/compilers/releases/tag/v0.13.3) - 2025-02-14 -- Switch md5 to xxhash ([#262](https://github.com/foundry-rs/compilers/issues/262)) +### Bug Fixes -### Styling +- Allow top level event declarations ([#251](https://github.com/foundry-rs/compilers/issues/251)) -- Update file extension for compatibility ([#258](https://github.com/foundry-rs/compilers/issues/258)) +### Features -## [0.13.0](https://github.com/foundry-rs/compilers/releases/tag/v0.13.0) - 2025-01-21 +- Impl `.path(&self)` for `ContractInfo` ([#250](https://github.com/foundry-rs/compilers/issues/250)) + +### Miscellaneous Tasks + +- Release 0.13.3 + +## [0.13.2](https://github.com/foundry-rs/compilers/releases/tag/v0.13.2) - 2025-02-06 ### Bug Fixes -- EvmVersion `from_str` ([#235](https://github.com/foundry-rs/compilers/issues/235)) +- Ordering for flattener ([#247](https://github.com/foundry-rs/compilers/issues/247)) + +### Miscellaneous Tasks + +- Release 0.13.2 +- Fix spelling issues ([#248](https://github.com/foundry-rs/compilers/issues/248)) + +## [0.13.1](https://github.com/foundry-rs/compilers/releases/tag/v0.13.1) - 2025-02-02 + +### Bug Fixes + +- Handle displaying multiline errors correctly ([#245](https://github.com/foundry-rs/compilers/issues/245)) ### Dependencies -- [deps] Bump solar 0.1.1 ([#237](https://github.com/foundry-rs/compilers/issues/237)) +- [deps] Bump dirs ([#243](https://github.com/foundry-rs/compilers/issues/243)) + +### Miscellaneous Tasks + +- Release 0.13.1 +- Clippy + winnow 0.7 ([#244](https://github.com/foundry-rs/compilers/issues/244)) +- Call shrink_to_fit afte parsing source maps ([#242](https://github.com/foundry-rs/compilers/issues/242)) + +## [0.13.0](https://github.com/foundry-rs/compilers/releases/tag/v0.13.0) - 2025-01-21 ### Features @@ -97,6 +203,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Release 0.13.0 - More lints ([#238](https://github.com/foundry-rs/compilers/issues/238)) + +## [0.12.9](https://github.com/foundry-rs/compilers/releases/tag/v0.12.9) - 2025-01-05 + +### Bug Fixes + +- EvmVersion `from_str` ([#235](https://github.com/foundry-rs/compilers/issues/235)) + +### Dependencies + +- [deps] Bump solar 0.1.1 ([#237](https://github.com/foundry-rs/compilers/issues/237)) + +### Miscellaneous Tasks + - Release 0.12.9 - Clippy ([#236](https://github.com/foundry-rs/compilers/issues/236)) @@ -105,36 +224,65 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Bug Fixes - Correctly merge restrictions ([#234](https://github.com/foundry-rs/compilers/issues/234)) -- Vyper version comparison typo ([#232](https://github.com/foundry-rs/compilers/issues/232)) -- Add fallback parser for contract names ([#229](https://github.com/foundry-rs/compilers/issues/229)) -- Fix minor grammatical issue in project documentation ([#226](https://github.com/foundry-rs/compilers/issues/226)) - -### Dependencies - -- Bump MSRV to 1.83 ([#230](https://github.com/foundry-rs/compilers/issues/230)) ### Miscellaneous Tasks - Release 0.12.8 -- Release 0.12.7 -- Release 0.12.6 -- Release 0.12.5 -- Release 0.12.5 -- Release 0.12.4 ### Other - Move deny to ci ([#233](https://github.com/foundry-rs/compilers/issues/233)) -- Add note about grammar,spelling prs ([#228](https://github.com/foundry-rs/compilers/issues/228)) + +## [0.12.7](https://github.com/foundry-rs/compilers/releases/tag/v0.12.7) - 2024-12-05 + +### Bug Fixes + +- Vyper version comparison typo ([#232](https://github.com/foundry-rs/compilers/issues/232)) + +### Miscellaneous Tasks + +- Release 0.12.7 + +## [0.12.6](https://github.com/foundry-rs/compilers/releases/tag/v0.12.6) - 2024-12-04 + +### Miscellaneous Tasks + +- Release 0.12.6 ### Performance - Don't request unnecessary output ([#231](https://github.com/foundry-rs/compilers/issues/231)) +## [0.12.5](https://github.com/foundry-rs/compilers/releases/tag/v0.12.5) - 2024-12-04 + +### Miscellaneous Tasks + +- Release 0.12.5 +- Release 0.12.5 + ### Refactor - Make Contract generic for Compiler and add metadata to CompilerOutput ([#224](https://github.com/foundry-rs/compilers/issues/224)) +## [0.12.4](https://github.com/foundry-rs/compilers/releases/tag/v0.12.4) - 2024-12-02 + +### Bug Fixes + +- Add fallback parser for contract names ([#229](https://github.com/foundry-rs/compilers/issues/229)) +- Fix minor grammatical issue in project documentation ([#226](https://github.com/foundry-rs/compilers/issues/226)) + +### Dependencies + +- Bump MSRV to 1.83 ([#230](https://github.com/foundry-rs/compilers/issues/230)) + +### Miscellaneous Tasks + +- Release 0.12.4 + +### Other + +- Add note about grammar,spelling prs ([#228](https://github.com/foundry-rs/compilers/issues/228)) + ## [0.12.3](https://github.com/foundry-rs/compilers/releases/tag/v0.12.3) - 2024-11-20 ### Bug Fixes @@ -173,6 +321,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Sanitize `settings.optimizer.details.inliner` ([#216](https://github.com/foundry-rs/compilers/issues/216)) - [tests] Always try installing pinned solc ([#217](https://github.com/foundry-rs/compilers/issues/217)) - Outdated merge build error +- Correctly handle b as pre-release in Vyper version ([#213](https://github.com/foundry-rs/compilers/issues/213)) ### Features @@ -185,15 +334,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Remove outdated `ref` patterns ([#218](https://github.com/foundry-rs/compilers/issues/218)) - Inline constants in Settings::sanitize ([#219](https://github.com/foundry-rs/compilers/issues/219)) - Use Version::new over .parse ([#220](https://github.com/foundry-rs/compilers/issues/220)) - -## [0.11.6](https://github.com/foundry-rs/compilers/releases/tag/v0.11.6) - 2024-10-16 - -### Bug Fixes - -- Correctly handle b as pre-release in Vyper version ([#213](https://github.com/foundry-rs/compilers/issues/213)) - -### Miscellaneous Tasks - - Release 0.11.6 ## [0.11.5](https://github.com/foundry-rs/compilers/releases/tag/v0.11.5) - 2024-10-14 diff --git a/Cargo.toml b/Cargo.toml index 79fa09e..9878291 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ resolver = "2" [workspace.package] authors = ["Foundry Maintainers"] -version = "0.17.0" +version = "0.17.1" rust-version = "1.87" readme = "README.md" license = "MIT OR Apache-2.0" @@ -36,11 +36,11 @@ redundant-lifetimes = "warn" all = "warn" [workspace.dependencies] -foundry-compilers = { path = "crates/compilers", version = "0.17.0" } -foundry-compilers-artifacts = { path = "crates/artifacts/artifacts", version = "0.17.0" } -foundry-compilers-artifacts-solc = { path = "crates/artifacts/solc", version = "0.17.0" } -foundry-compilers-artifacts-vyper = { path = "crates/artifacts/vyper", version = "0.17.0" } -foundry-compilers-core = { path = "crates/core", version = "0.17.0" } +foundry-compilers = { path = "crates/compilers", version = "0.17.1" } +foundry-compilers-artifacts = { path = "crates/artifacts/artifacts", version = "0.17.1" } +foundry-compilers-artifacts-solc = { path = "crates/artifacts/solc", version = "0.17.1" } +foundry-compilers-artifacts-vyper = { path = "crates/artifacts/vyper", version = "0.17.1" } +foundry-compilers-core = { path = "crates/core", version = "0.17.1" } alloy-json-abi = { version = "1.0", features = ["serde_json"] } alloy-primitives = { version = "1.0", features = ["serde", "rand"] } From 33ff5ea40ec4962c465655a2d97abc844f68c974 Mon Sep 17 00:00:00 2001 From: zerosnacks Date: Mon, 2 Jun 2025 16:32:35 +0200 Subject: [PATCH 17/57] chore: release 0.17.1 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bfc74b..58c0ddf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Miscellaneous Tasks +- Release 0.17.1 - Add language matcher on `MultiCompilerLanguage` ([#276](https://github.com/foundry-rs/compilers/issues/276)) ## [0.17.0](https://github.com/foundry-rs/compilers/releases/tag/v0.17.0) - 2025-05-29 From e4a9b049d08e43899f9eebd2be56edaac04fef07 Mon Sep 17 00:00:00 2001 From: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Date: Thu, 5 Jun 2025 19:11:51 +0200 Subject: [PATCH 18/57] debt: remove EOF version field (#279) EOF has been deprecated in Foundry Remove unused `serde_repr` dependency --- crates/artifacts/solc/Cargo.toml | 1 - crates/artifacts/solc/src/lib.rs | 12 ------------ crates/compilers/src/compilers/solc/mod.rs | 2 -- 3 files changed, 15 deletions(-) diff --git a/crates/artifacts/solc/Cargo.toml b/crates/artifacts/solc/Cargo.toml index d4e1b95..807b2f7 100644 --- a/crates/artifacts/solc/Cargo.toml +++ b/crates/artifacts/solc/Cargo.toml @@ -21,7 +21,6 @@ alloy-json-abi.workspace = true alloy-primitives.workspace = true semver.workspace = true serde_json.workspace = true -serde_repr = "0.1" serde.workspace = true thiserror.workspace = true tracing.workspace = true diff --git a/crates/artifacts/solc/src/lib.rs b/crates/artifacts/solc/src/lib.rs index 83f9c4b..a5e2b27 100644 --- a/crates/artifacts/solc/src/lib.rs +++ b/crates/artifacts/solc/src/lib.rs @@ -9,7 +9,6 @@ extern crate tracing; use semver::Version; use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer}; -use serde_repr::{Deserialize_repr, Serialize_repr}; use std::{ collections::{BTreeMap, HashSet}, fmt, @@ -272,16 +271,6 @@ pub struct Settings { /// If this key is an empty string, that refers to a global level. #[serde(default)] pub libraries: Libraries, - /// Specify EOF version to produce. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub eof_version: Option, -} - -/// Available EOF versions. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize_repr, Deserialize_repr)] -#[repr(u8)] -pub enum EofVersion { - V1 = 1, } impl Settings { @@ -562,7 +551,6 @@ impl Default for Settings { libraries: Default::default(), remappings: Default::default(), model_checker: None, - eof_version: None, } .with_ast() } diff --git a/crates/compilers/src/compilers/solc/mod.rs b/crates/compilers/src/compilers/solc/mod.rs index e4381aa..7a2f166 100644 --- a/crates/compilers/src/compilers/solc/mod.rs +++ b/crates/compilers/src/compilers/solc/mod.rs @@ -297,7 +297,6 @@ impl CompilerSettings for SolcSettings { via_ir, debug, libraries, - eof_version, }, .. } = self; @@ -311,7 +310,6 @@ impl CompilerSettings for SolcSettings { && *via_ir == other.settings.via_ir && *debug == other.settings.debug && *libraries == other.settings.libraries - && *eof_version == other.settings.eof_version && output_selection.is_subset_of(&other.settings.output_selection) } From 22662dd2b374f0903a2ef29afa172b4c7bb24cce Mon Sep 17 00:00:00 2001 From: Ferran Borreguero Date: Tue, 10 Jun 2025 22:12:18 +0100 Subject: [PATCH 19/57] Add missing node types (#282) This PR adds a few missing NodeTypes for the node AST. --- crates/artifacts/solc/src/ast/lowfidelity.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/artifacts/solc/src/ast/lowfidelity.rs b/crates/artifacts/solc/src/ast/lowfidelity.rs index 1d4fc75..33df93a 100644 --- a/crates/artifacts/solc/src/ast/lowfidelity.rs +++ b/crates/artifacts/solc/src/ast/lowfidelity.rs @@ -202,6 +202,9 @@ pub enum NodeType { ParameterList, TryCatchClause, ModifierInvocation, + UserDefinedTypeName, + ArrayTypeName, + Mapping, /// An unknown AST node type. Other(String), From 9920c749f30f39f27f605ce25d69bf9483495f1f Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Tue, 10 Jun 2025 13:06:30 +0200 Subject: [PATCH 20/57] fix: implement proper serde handling for unknown AST node types (#280) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem The `NodeType` enum had an `Other(String)` variant intended to catch unknown AST node types, but it wasn't working correctly. The issue was that serde's default enum serialization uses tagged format (e.g., `{"Other": "UnknownType"}`), but Solidity AST serializes node types as simple strings (e.g., `"UnknownType"`). When encountering an unknown node type like `"SomeNewNodeType"`, the deserializer would fail instead of falling back to the `Other` variant. ## Solution Implemented custom `Serialize` and `Deserialize` traits for `NodeType`: - **Custom serialization**: All variants serialize as simple strings - **Custom deserialization**: Known strings map to specific variants, unknown strings map to `Other(String)` - **Backwards compatibility**: All existing functionality remains unchanged ## Testing Added comprehensive tests covering: - Unknown node type deserialization → `Other` variant - Known node type deserialization (unchanged behavior) - Roundtrip serialization for all node types - Complete AST parsing with unknown node types - Mixed known/unknown node structures This ensures the library can handle future Solidity compiler versions that introduce new AST node types without breaking existing code. Closes #280 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- crates/artifacts/solc/src/ast/lowfidelity.rs | 258 ++++++++++++++++++- 1 file changed, 257 insertions(+), 1 deletion(-) diff --git a/crates/artifacts/solc/src/ast/lowfidelity.rs b/crates/artifacts/solc/src/ast/lowfidelity.rs index 33df93a..e2a817e 100644 --- a/crates/artifacts/solc/src/ast/lowfidelity.rs +++ b/crates/artifacts/solc/src/ast/lowfidelity.rs @@ -118,7 +118,7 @@ impl fmt::Display for SourceLocation { } } -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum NodeType { // Expressions Assignment, @@ -210,6 +210,169 @@ pub enum NodeType { Other(String), } +impl serde::Serialize for NodeType { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + NodeType::Assignment => serializer.serialize_str("Assignment"), + NodeType::BinaryOperation => serializer.serialize_str("BinaryOperation"), + NodeType::Conditional => serializer.serialize_str("Conditional"), + NodeType::ElementaryTypeNameExpression => { + serializer.serialize_str("ElementaryTypeNameExpression") + } + NodeType::FunctionCall => serializer.serialize_str("FunctionCall"), + NodeType::FunctionCallOptions => serializer.serialize_str("FunctionCallOptions"), + NodeType::Identifier => serializer.serialize_str("Identifier"), + NodeType::IndexAccess => serializer.serialize_str("IndexAccess"), + NodeType::IndexRangeAccess => serializer.serialize_str("IndexRangeAccess"), + NodeType::Literal => serializer.serialize_str("Literal"), + NodeType::MemberAccess => serializer.serialize_str("MemberAccess"), + NodeType::NewExpression => serializer.serialize_str("NewExpression"), + NodeType::TupleExpression => serializer.serialize_str("TupleExpression"), + NodeType::UnaryOperation => serializer.serialize_str("UnaryOperation"), + NodeType::Block => serializer.serialize_str("Block"), + NodeType::Break => serializer.serialize_str("Break"), + NodeType::Continue => serializer.serialize_str("Continue"), + NodeType::DoWhileStatement => serializer.serialize_str("DoWhileStatement"), + NodeType::EmitStatement => serializer.serialize_str("EmitStatement"), + NodeType::ExpressionStatement => serializer.serialize_str("ExpressionStatement"), + NodeType::ForStatement => serializer.serialize_str("ForStatement"), + NodeType::IfStatement => serializer.serialize_str("IfStatement"), + NodeType::InlineAssembly => serializer.serialize_str("InlineAssembly"), + NodeType::PlaceholderStatement => serializer.serialize_str("PlaceholderStatement"), + NodeType::Return => serializer.serialize_str("Return"), + NodeType::RevertStatement => serializer.serialize_str("RevertStatement"), + NodeType::TryStatement => serializer.serialize_str("TryStatement"), + NodeType::UncheckedBlock => serializer.serialize_str("UncheckedBlock"), + NodeType::VariableDeclarationStatement => { + serializer.serialize_str("VariableDeclarationStatement") + } + NodeType::VariableDeclaration => serializer.serialize_str("VariableDeclaration"), + NodeType::WhileStatement => serializer.serialize_str("WhileStatement"), + NodeType::YulAssignment => serializer.serialize_str("YulAssignment"), + NodeType::YulBlock => serializer.serialize_str("YulBlock"), + NodeType::YulBreak => serializer.serialize_str("YulBreak"), + NodeType::YulCase => serializer.serialize_str("YulCase"), + NodeType::YulContinue => serializer.serialize_str("YulContinue"), + NodeType::YulExpressionStatement => serializer.serialize_str("YulExpressionStatement"), + NodeType::YulLeave => serializer.serialize_str("YulLeave"), + NodeType::YulForLoop => serializer.serialize_str("YulForLoop"), + NodeType::YulFunctionDefinition => serializer.serialize_str("YulFunctionDefinition"), + NodeType::YulIf => serializer.serialize_str("YulIf"), + NodeType::YulSwitch => serializer.serialize_str("YulSwitch"), + NodeType::YulVariableDeclaration => serializer.serialize_str("YulVariableDeclaration"), + NodeType::YulFunctionCall => serializer.serialize_str("YulFunctionCall"), + NodeType::YulIdentifier => serializer.serialize_str("YulIdentifier"), + NodeType::YulLiteral => serializer.serialize_str("YulLiteral"), + NodeType::YulLiteralValue => serializer.serialize_str("YulLiteralValue"), + NodeType::YulHexValue => serializer.serialize_str("YulHexValue"), + NodeType::YulTypedName => serializer.serialize_str("YulTypedName"), + NodeType::ContractDefinition => serializer.serialize_str("ContractDefinition"), + NodeType::FunctionDefinition => serializer.serialize_str("FunctionDefinition"), + NodeType::EventDefinition => serializer.serialize_str("EventDefinition"), + NodeType::ErrorDefinition => serializer.serialize_str("ErrorDefinition"), + NodeType::ModifierDefinition => serializer.serialize_str("ModifierDefinition"), + NodeType::StructDefinition => serializer.serialize_str("StructDefinition"), + NodeType::EnumDefinition => serializer.serialize_str("EnumDefinition"), + NodeType::UserDefinedValueTypeDefinition => { + serializer.serialize_str("UserDefinedValueTypeDefinition") + } + NodeType::PragmaDirective => serializer.serialize_str("PragmaDirective"), + NodeType::ImportDirective => serializer.serialize_str("ImportDirective"), + NodeType::UsingForDirective => serializer.serialize_str("UsingForDirective"), + NodeType::SourceUnit => serializer.serialize_str("SourceUnit"), + NodeType::InheritanceSpecifier => serializer.serialize_str("InheritanceSpecifier"), + NodeType::ElementaryTypeName => serializer.serialize_str("ElementaryTypeName"), + NodeType::FunctionTypeName => serializer.serialize_str("FunctionTypeName"), + NodeType::ParameterList => serializer.serialize_str("ParameterList"), + NodeType::TryCatchClause => serializer.serialize_str("TryCatchClause"), + NodeType::ModifierInvocation => serializer.serialize_str("ModifierInvocation"), + NodeType::Other(s) => serializer.serialize_str(s), + } + } +} + +impl<'de> serde::Deserialize<'de> for NodeType { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Ok(match s.as_str() { + "Assignment" => NodeType::Assignment, + "BinaryOperation" => NodeType::BinaryOperation, + "Conditional" => NodeType::Conditional, + "ElementaryTypeNameExpression" => NodeType::ElementaryTypeNameExpression, + "FunctionCall" => NodeType::FunctionCall, + "FunctionCallOptions" => NodeType::FunctionCallOptions, + "Identifier" => NodeType::Identifier, + "IndexAccess" => NodeType::IndexAccess, + "IndexRangeAccess" => NodeType::IndexRangeAccess, + "Literal" => NodeType::Literal, + "MemberAccess" => NodeType::MemberAccess, + "NewExpression" => NodeType::NewExpression, + "TupleExpression" => NodeType::TupleExpression, + "UnaryOperation" => NodeType::UnaryOperation, + "Block" => NodeType::Block, + "Break" => NodeType::Break, + "Continue" => NodeType::Continue, + "DoWhileStatement" => NodeType::DoWhileStatement, + "EmitStatement" => NodeType::EmitStatement, + "ExpressionStatement" => NodeType::ExpressionStatement, + "ForStatement" => NodeType::ForStatement, + "IfStatement" => NodeType::IfStatement, + "InlineAssembly" => NodeType::InlineAssembly, + "PlaceholderStatement" => NodeType::PlaceholderStatement, + "Return" => NodeType::Return, + "RevertStatement" => NodeType::RevertStatement, + "TryStatement" => NodeType::TryStatement, + "UncheckedBlock" => NodeType::UncheckedBlock, + "VariableDeclarationStatement" => NodeType::VariableDeclarationStatement, + "VariableDeclaration" => NodeType::VariableDeclaration, + "WhileStatement" => NodeType::WhileStatement, + "YulAssignment" => NodeType::YulAssignment, + "YulBlock" => NodeType::YulBlock, + "YulBreak" => NodeType::YulBreak, + "YulCase" => NodeType::YulCase, + "YulContinue" => NodeType::YulContinue, + "YulExpressionStatement" => NodeType::YulExpressionStatement, + "YulLeave" => NodeType::YulLeave, + "YulForLoop" => NodeType::YulForLoop, + "YulFunctionDefinition" => NodeType::YulFunctionDefinition, + "YulIf" => NodeType::YulIf, + "YulSwitch" => NodeType::YulSwitch, + "YulVariableDeclaration" => NodeType::YulVariableDeclaration, + "YulFunctionCall" => NodeType::YulFunctionCall, + "YulIdentifier" => NodeType::YulIdentifier, + "YulLiteral" => NodeType::YulLiteral, + "YulLiteralValue" => NodeType::YulLiteralValue, + "YulHexValue" => NodeType::YulHexValue, + "YulTypedName" => NodeType::YulTypedName, + "ContractDefinition" => NodeType::ContractDefinition, + "FunctionDefinition" => NodeType::FunctionDefinition, + "EventDefinition" => NodeType::EventDefinition, + "ErrorDefinition" => NodeType::ErrorDefinition, + "ModifierDefinition" => NodeType::ModifierDefinition, + "StructDefinition" => NodeType::StructDefinition, + "EnumDefinition" => NodeType::EnumDefinition, + "UserDefinedValueTypeDefinition" => NodeType::UserDefinedValueTypeDefinition, + "PragmaDirective" => NodeType::PragmaDirective, + "ImportDirective" => NodeType::ImportDirective, + "UsingForDirective" => NodeType::UsingForDirective, + "SourceUnit" => NodeType::SourceUnit, + "InheritanceSpecifier" => NodeType::InheritanceSpecifier, + "ElementaryTypeName" => NodeType::ElementaryTypeName, + "FunctionTypeName" => NodeType::FunctionTypeName, + "ParameterList" => NodeType::ParameterList, + "TryCatchClause" => NodeType::TryCatchClause, + "ModifierInvocation" => NodeType::ModifierInvocation, + _ => NodeType::Other(s), + }) + } +} + #[cfg(test)] mod tests { use super::*; @@ -219,4 +382,97 @@ mod tests { let ast = include_str!("../../../../../test-data/ast/ast-erc4626.json"); let _ast: Ast = serde_json::from_str(ast).unwrap(); } + + #[test] + fn test_unknown_node_type_deserialization() { + // Test that unknown node types are properly handled with the Other variant + let json = r#"{"nodeType": "SomeUnknownNodeType"}"#; + let parsed: serde_json::Value = serde_json::from_str(json).unwrap(); + let node_type: NodeType = serde_json::from_value(parsed["nodeType"].clone()).unwrap(); + assert_eq!(node_type, NodeType::Other("SomeUnknownNodeType".to_string())); + } + + #[test] + fn test_known_node_type_deserialization() { + // Test that known node types still work properly + let json = r#"{"nodeType": "Assignment"}"#; + let parsed: serde_json::Value = serde_json::from_str(json).unwrap(); + let node_type: NodeType = serde_json::from_value(parsed["nodeType"].clone()).unwrap(); + assert_eq!(node_type, NodeType::Assignment); + } + + #[test] + fn test_node_type_serialization() { + // Test that serialization works correctly for both known and unknown node types + let known = NodeType::Assignment; + let unknown = NodeType::Other("SomeUnknownNodeType".to_string()); + + let known_json = serde_json::to_string(&known).unwrap(); + let unknown_json = serde_json::to_string(&unknown).unwrap(); + + assert_eq!(known_json, r#""Assignment""#); + assert_eq!(unknown_json, r#""SomeUnknownNodeType""#); + } + + #[test] + fn test_node_type_roundtrip_serialization() { + // Test roundtrip serialization for all known node types to ensure nothing is broken + let test_cases = [ + NodeType::Assignment, + NodeType::BinaryOperation, + NodeType::FunctionCall, + NodeType::ContractDefinition, + NodeType::YulAssignment, + NodeType::Other("CustomNodeType".to_string()), + ]; + + for original in test_cases { + let serialized = serde_json::to_string(&original).unwrap(); + let deserialized: NodeType = serde_json::from_str(&serialized).unwrap(); + assert_eq!(original, deserialized); + } + } + + #[test] + fn test_ast_node_with_unknown_type() { + // Test that a complete Node with unknown nodeType can be parsed + let json = r#"{ + "id": 1, + "nodeType": "NewFancyNodeType", + "src": "0:0:0" + }"#; + + let node: Node = serde_json::from_str(json).unwrap(); + assert_eq!(node.node_type, NodeType::Other("NewFancyNodeType".to_string())); + assert_eq!(node.id, Some(1)); + } + + #[test] + fn test_mixed_known_unknown_nodes() { + // Test parsing a JSON structure with both known and unknown node types + let json = r#"{ + "absolutePath": "/test/path.sol", + "id": 0, + "nodeType": "SourceUnit", + "src": "0:100:0", + "nodes": [ + { + "id": 1, + "nodeType": "Assignment", + "src": "10:20:0" + }, + { + "id": 2, + "nodeType": "FutureNodeType", + "src": "30:40:0" + } + ] + }"#; + + let ast: Ast = serde_json::from_str(json).unwrap(); + assert_eq!(ast.node_type, NodeType::SourceUnit); + assert_eq!(ast.nodes.len(), 2); + assert_eq!(ast.nodes[0].node_type, NodeType::Assignment); + assert_eq!(ast.nodes[1].node_type, NodeType::Other("FutureNodeType".to_string())); + } } From d4dde106ea9118ce9620111d950e91290d1a762a Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Tue, 10 Jun 2025 23:13:07 +0200 Subject: [PATCH 21/57] chore: release 0.17.2 --- CHANGELOG.md | 12 ++++++++++++ Cargo.toml | 12 ++++++------ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58c0ddf..302cb38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.17.2](https://github.com/foundry-rs/compilers/releases/tag/v0.17.2) - 2025-06-10 + +### Bug Fixes + +- Implement proper serde handling for unknown AST node types ([#280](https://github.com/foundry-rs/compilers/issues/280)) + +### Other + +- Add missing node types ([#282](https://github.com/foundry-rs/compilers/issues/282)) +- Remove EOF version field ([#279](https://github.com/foundry-rs/compilers/issues/279)) + ## [0.17.1](https://github.com/foundry-rs/compilers/releases/tag/v0.17.1) - 2025-06-02 ### Dependencies @@ -14,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Miscellaneous Tasks +- Release 0.17.1 - Release 0.17.1 - Add language matcher on `MultiCompilerLanguage` ([#276](https://github.com/foundry-rs/compilers/issues/276)) diff --git a/Cargo.toml b/Cargo.toml index 9878291..c671472 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ resolver = "2" [workspace.package] authors = ["Foundry Maintainers"] -version = "0.17.1" +version = "0.17.2" rust-version = "1.87" readme = "README.md" license = "MIT OR Apache-2.0" @@ -36,11 +36,11 @@ redundant-lifetimes = "warn" all = "warn" [workspace.dependencies] -foundry-compilers = { path = "crates/compilers", version = "0.17.1" } -foundry-compilers-artifacts = { path = "crates/artifacts/artifacts", version = "0.17.1" } -foundry-compilers-artifacts-solc = { path = "crates/artifacts/solc", version = "0.17.1" } -foundry-compilers-artifacts-vyper = { path = "crates/artifacts/vyper", version = "0.17.1" } -foundry-compilers-core = { path = "crates/core", version = "0.17.1" } +foundry-compilers = { path = "crates/compilers", version = "0.17.2" } +foundry-compilers-artifacts = { path = "crates/artifacts/artifacts", version = "0.17.2" } +foundry-compilers-artifacts-solc = { path = "crates/artifacts/solc", version = "0.17.2" } +foundry-compilers-artifacts-vyper = { path = "crates/artifacts/vyper", version = "0.17.2" } +foundry-compilers-core = { path = "crates/core", version = "0.17.2" } alloy-json-abi = { version = "1.0", features = ["serde_json"] } alloy-primitives = { version = "1.0", features = ["serde", "rand"] } From daa3ce144055ac7fff0ebf2026f51e9df7af8c1a Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Sat, 14 Jun 2025 20:36:32 +0200 Subject: [PATCH 22/57] =?UTF-8?q?Revert=20"fix:=20implement=20proper=20ser?= =?UTF-8?q?de=20handling=20for=20unknown=20AST=20node=20typ=E2=80=A6=20(#2?= =?UTF-8?q?84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …es (#280)" This reverts commit 9920c749f30f39f27f605ce25d69bf9483495f1f. This was accidentally committed to main, supersedes #283 --- crates/artifacts/solc/src/ast/lowfidelity.rs | 258 +------------------ 1 file changed, 1 insertion(+), 257 deletions(-) diff --git a/crates/artifacts/solc/src/ast/lowfidelity.rs b/crates/artifacts/solc/src/ast/lowfidelity.rs index e2a817e..33df93a 100644 --- a/crates/artifacts/solc/src/ast/lowfidelity.rs +++ b/crates/artifacts/solc/src/ast/lowfidelity.rs @@ -118,7 +118,7 @@ impl fmt::Display for SourceLocation { } } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum NodeType { // Expressions Assignment, @@ -210,169 +210,6 @@ pub enum NodeType { Other(String), } -impl serde::Serialize for NodeType { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - match self { - NodeType::Assignment => serializer.serialize_str("Assignment"), - NodeType::BinaryOperation => serializer.serialize_str("BinaryOperation"), - NodeType::Conditional => serializer.serialize_str("Conditional"), - NodeType::ElementaryTypeNameExpression => { - serializer.serialize_str("ElementaryTypeNameExpression") - } - NodeType::FunctionCall => serializer.serialize_str("FunctionCall"), - NodeType::FunctionCallOptions => serializer.serialize_str("FunctionCallOptions"), - NodeType::Identifier => serializer.serialize_str("Identifier"), - NodeType::IndexAccess => serializer.serialize_str("IndexAccess"), - NodeType::IndexRangeAccess => serializer.serialize_str("IndexRangeAccess"), - NodeType::Literal => serializer.serialize_str("Literal"), - NodeType::MemberAccess => serializer.serialize_str("MemberAccess"), - NodeType::NewExpression => serializer.serialize_str("NewExpression"), - NodeType::TupleExpression => serializer.serialize_str("TupleExpression"), - NodeType::UnaryOperation => serializer.serialize_str("UnaryOperation"), - NodeType::Block => serializer.serialize_str("Block"), - NodeType::Break => serializer.serialize_str("Break"), - NodeType::Continue => serializer.serialize_str("Continue"), - NodeType::DoWhileStatement => serializer.serialize_str("DoWhileStatement"), - NodeType::EmitStatement => serializer.serialize_str("EmitStatement"), - NodeType::ExpressionStatement => serializer.serialize_str("ExpressionStatement"), - NodeType::ForStatement => serializer.serialize_str("ForStatement"), - NodeType::IfStatement => serializer.serialize_str("IfStatement"), - NodeType::InlineAssembly => serializer.serialize_str("InlineAssembly"), - NodeType::PlaceholderStatement => serializer.serialize_str("PlaceholderStatement"), - NodeType::Return => serializer.serialize_str("Return"), - NodeType::RevertStatement => serializer.serialize_str("RevertStatement"), - NodeType::TryStatement => serializer.serialize_str("TryStatement"), - NodeType::UncheckedBlock => serializer.serialize_str("UncheckedBlock"), - NodeType::VariableDeclarationStatement => { - serializer.serialize_str("VariableDeclarationStatement") - } - NodeType::VariableDeclaration => serializer.serialize_str("VariableDeclaration"), - NodeType::WhileStatement => serializer.serialize_str("WhileStatement"), - NodeType::YulAssignment => serializer.serialize_str("YulAssignment"), - NodeType::YulBlock => serializer.serialize_str("YulBlock"), - NodeType::YulBreak => serializer.serialize_str("YulBreak"), - NodeType::YulCase => serializer.serialize_str("YulCase"), - NodeType::YulContinue => serializer.serialize_str("YulContinue"), - NodeType::YulExpressionStatement => serializer.serialize_str("YulExpressionStatement"), - NodeType::YulLeave => serializer.serialize_str("YulLeave"), - NodeType::YulForLoop => serializer.serialize_str("YulForLoop"), - NodeType::YulFunctionDefinition => serializer.serialize_str("YulFunctionDefinition"), - NodeType::YulIf => serializer.serialize_str("YulIf"), - NodeType::YulSwitch => serializer.serialize_str("YulSwitch"), - NodeType::YulVariableDeclaration => serializer.serialize_str("YulVariableDeclaration"), - NodeType::YulFunctionCall => serializer.serialize_str("YulFunctionCall"), - NodeType::YulIdentifier => serializer.serialize_str("YulIdentifier"), - NodeType::YulLiteral => serializer.serialize_str("YulLiteral"), - NodeType::YulLiteralValue => serializer.serialize_str("YulLiteralValue"), - NodeType::YulHexValue => serializer.serialize_str("YulHexValue"), - NodeType::YulTypedName => serializer.serialize_str("YulTypedName"), - NodeType::ContractDefinition => serializer.serialize_str("ContractDefinition"), - NodeType::FunctionDefinition => serializer.serialize_str("FunctionDefinition"), - NodeType::EventDefinition => serializer.serialize_str("EventDefinition"), - NodeType::ErrorDefinition => serializer.serialize_str("ErrorDefinition"), - NodeType::ModifierDefinition => serializer.serialize_str("ModifierDefinition"), - NodeType::StructDefinition => serializer.serialize_str("StructDefinition"), - NodeType::EnumDefinition => serializer.serialize_str("EnumDefinition"), - NodeType::UserDefinedValueTypeDefinition => { - serializer.serialize_str("UserDefinedValueTypeDefinition") - } - NodeType::PragmaDirective => serializer.serialize_str("PragmaDirective"), - NodeType::ImportDirective => serializer.serialize_str("ImportDirective"), - NodeType::UsingForDirective => serializer.serialize_str("UsingForDirective"), - NodeType::SourceUnit => serializer.serialize_str("SourceUnit"), - NodeType::InheritanceSpecifier => serializer.serialize_str("InheritanceSpecifier"), - NodeType::ElementaryTypeName => serializer.serialize_str("ElementaryTypeName"), - NodeType::FunctionTypeName => serializer.serialize_str("FunctionTypeName"), - NodeType::ParameterList => serializer.serialize_str("ParameterList"), - NodeType::TryCatchClause => serializer.serialize_str("TryCatchClause"), - NodeType::ModifierInvocation => serializer.serialize_str("ModifierInvocation"), - NodeType::Other(s) => serializer.serialize_str(s), - } - } -} - -impl<'de> serde::Deserialize<'de> for NodeType { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - Ok(match s.as_str() { - "Assignment" => NodeType::Assignment, - "BinaryOperation" => NodeType::BinaryOperation, - "Conditional" => NodeType::Conditional, - "ElementaryTypeNameExpression" => NodeType::ElementaryTypeNameExpression, - "FunctionCall" => NodeType::FunctionCall, - "FunctionCallOptions" => NodeType::FunctionCallOptions, - "Identifier" => NodeType::Identifier, - "IndexAccess" => NodeType::IndexAccess, - "IndexRangeAccess" => NodeType::IndexRangeAccess, - "Literal" => NodeType::Literal, - "MemberAccess" => NodeType::MemberAccess, - "NewExpression" => NodeType::NewExpression, - "TupleExpression" => NodeType::TupleExpression, - "UnaryOperation" => NodeType::UnaryOperation, - "Block" => NodeType::Block, - "Break" => NodeType::Break, - "Continue" => NodeType::Continue, - "DoWhileStatement" => NodeType::DoWhileStatement, - "EmitStatement" => NodeType::EmitStatement, - "ExpressionStatement" => NodeType::ExpressionStatement, - "ForStatement" => NodeType::ForStatement, - "IfStatement" => NodeType::IfStatement, - "InlineAssembly" => NodeType::InlineAssembly, - "PlaceholderStatement" => NodeType::PlaceholderStatement, - "Return" => NodeType::Return, - "RevertStatement" => NodeType::RevertStatement, - "TryStatement" => NodeType::TryStatement, - "UncheckedBlock" => NodeType::UncheckedBlock, - "VariableDeclarationStatement" => NodeType::VariableDeclarationStatement, - "VariableDeclaration" => NodeType::VariableDeclaration, - "WhileStatement" => NodeType::WhileStatement, - "YulAssignment" => NodeType::YulAssignment, - "YulBlock" => NodeType::YulBlock, - "YulBreak" => NodeType::YulBreak, - "YulCase" => NodeType::YulCase, - "YulContinue" => NodeType::YulContinue, - "YulExpressionStatement" => NodeType::YulExpressionStatement, - "YulLeave" => NodeType::YulLeave, - "YulForLoop" => NodeType::YulForLoop, - "YulFunctionDefinition" => NodeType::YulFunctionDefinition, - "YulIf" => NodeType::YulIf, - "YulSwitch" => NodeType::YulSwitch, - "YulVariableDeclaration" => NodeType::YulVariableDeclaration, - "YulFunctionCall" => NodeType::YulFunctionCall, - "YulIdentifier" => NodeType::YulIdentifier, - "YulLiteral" => NodeType::YulLiteral, - "YulLiteralValue" => NodeType::YulLiteralValue, - "YulHexValue" => NodeType::YulHexValue, - "YulTypedName" => NodeType::YulTypedName, - "ContractDefinition" => NodeType::ContractDefinition, - "FunctionDefinition" => NodeType::FunctionDefinition, - "EventDefinition" => NodeType::EventDefinition, - "ErrorDefinition" => NodeType::ErrorDefinition, - "ModifierDefinition" => NodeType::ModifierDefinition, - "StructDefinition" => NodeType::StructDefinition, - "EnumDefinition" => NodeType::EnumDefinition, - "UserDefinedValueTypeDefinition" => NodeType::UserDefinedValueTypeDefinition, - "PragmaDirective" => NodeType::PragmaDirective, - "ImportDirective" => NodeType::ImportDirective, - "UsingForDirective" => NodeType::UsingForDirective, - "SourceUnit" => NodeType::SourceUnit, - "InheritanceSpecifier" => NodeType::InheritanceSpecifier, - "ElementaryTypeName" => NodeType::ElementaryTypeName, - "FunctionTypeName" => NodeType::FunctionTypeName, - "ParameterList" => NodeType::ParameterList, - "TryCatchClause" => NodeType::TryCatchClause, - "ModifierInvocation" => NodeType::ModifierInvocation, - _ => NodeType::Other(s), - }) - } -} - #[cfg(test)] mod tests { use super::*; @@ -382,97 +219,4 @@ mod tests { let ast = include_str!("../../../../../test-data/ast/ast-erc4626.json"); let _ast: Ast = serde_json::from_str(ast).unwrap(); } - - #[test] - fn test_unknown_node_type_deserialization() { - // Test that unknown node types are properly handled with the Other variant - let json = r#"{"nodeType": "SomeUnknownNodeType"}"#; - let parsed: serde_json::Value = serde_json::from_str(json).unwrap(); - let node_type: NodeType = serde_json::from_value(parsed["nodeType"].clone()).unwrap(); - assert_eq!(node_type, NodeType::Other("SomeUnknownNodeType".to_string())); - } - - #[test] - fn test_known_node_type_deserialization() { - // Test that known node types still work properly - let json = r#"{"nodeType": "Assignment"}"#; - let parsed: serde_json::Value = serde_json::from_str(json).unwrap(); - let node_type: NodeType = serde_json::from_value(parsed["nodeType"].clone()).unwrap(); - assert_eq!(node_type, NodeType::Assignment); - } - - #[test] - fn test_node_type_serialization() { - // Test that serialization works correctly for both known and unknown node types - let known = NodeType::Assignment; - let unknown = NodeType::Other("SomeUnknownNodeType".to_string()); - - let known_json = serde_json::to_string(&known).unwrap(); - let unknown_json = serde_json::to_string(&unknown).unwrap(); - - assert_eq!(known_json, r#""Assignment""#); - assert_eq!(unknown_json, r#""SomeUnknownNodeType""#); - } - - #[test] - fn test_node_type_roundtrip_serialization() { - // Test roundtrip serialization for all known node types to ensure nothing is broken - let test_cases = [ - NodeType::Assignment, - NodeType::BinaryOperation, - NodeType::FunctionCall, - NodeType::ContractDefinition, - NodeType::YulAssignment, - NodeType::Other("CustomNodeType".to_string()), - ]; - - for original in test_cases { - let serialized = serde_json::to_string(&original).unwrap(); - let deserialized: NodeType = serde_json::from_str(&serialized).unwrap(); - assert_eq!(original, deserialized); - } - } - - #[test] - fn test_ast_node_with_unknown_type() { - // Test that a complete Node with unknown nodeType can be parsed - let json = r#"{ - "id": 1, - "nodeType": "NewFancyNodeType", - "src": "0:0:0" - }"#; - - let node: Node = serde_json::from_str(json).unwrap(); - assert_eq!(node.node_type, NodeType::Other("NewFancyNodeType".to_string())); - assert_eq!(node.id, Some(1)); - } - - #[test] - fn test_mixed_known_unknown_nodes() { - // Test parsing a JSON structure with both known and unknown node types - let json = r#"{ - "absolutePath": "/test/path.sol", - "id": 0, - "nodeType": "SourceUnit", - "src": "0:100:0", - "nodes": [ - { - "id": 1, - "nodeType": "Assignment", - "src": "10:20:0" - }, - { - "id": 2, - "nodeType": "FutureNodeType", - "src": "30:40:0" - } - ] - }"#; - - let ast: Ast = serde_json::from_str(json).unwrap(); - assert_eq!(ast.node_type, NodeType::SourceUnit); - assert_eq!(ast.nodes.len(), 2); - assert_eq!(ast.nodes[0].node_type, NodeType::Assignment); - assert_eq!(ast.nodes[1].node_type, NodeType::Other("FutureNodeType".to_string())); - } } From a15a469519caea315ea5fd88538202652107fd83 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Sat, 14 Jun 2025 20:38:11 +0200 Subject: [PATCH 23/57] chore: release 0.17.3 --- CHANGELOG.md | 10 ++++++++++ Cargo.toml | 12 ++++++------ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 302cb38..4f73847 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,22 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.17.3](https://github.com/foundry-rs/compilers/releases/tag/v0.17.3) - 2025-06-14 + +### Other + +- Revert "fix: implement proper serde handling for unknown AST node typ… ([#284](https://github.com/foundry-rs/compilers/issues/284)) + ## [0.17.2](https://github.com/foundry-rs/compilers/releases/tag/v0.17.2) - 2025-06-10 ### Bug Fixes - Implement proper serde handling for unknown AST node types ([#280](https://github.com/foundry-rs/compilers/issues/280)) +### Miscellaneous Tasks + +- Release 0.17.2 + ### Other - Add missing node types ([#282](https://github.com/foundry-rs/compilers/issues/282)) diff --git a/Cargo.toml b/Cargo.toml index c671472..d2277f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ resolver = "2" [workspace.package] authors = ["Foundry Maintainers"] -version = "0.17.2" +version = "0.17.3" rust-version = "1.87" readme = "README.md" license = "MIT OR Apache-2.0" @@ -36,11 +36,11 @@ redundant-lifetimes = "warn" all = "warn" [workspace.dependencies] -foundry-compilers = { path = "crates/compilers", version = "0.17.2" } -foundry-compilers-artifacts = { path = "crates/artifacts/artifacts", version = "0.17.2" } -foundry-compilers-artifacts-solc = { path = "crates/artifacts/solc", version = "0.17.2" } -foundry-compilers-artifacts-vyper = { path = "crates/artifacts/vyper", version = "0.17.2" } -foundry-compilers-core = { path = "crates/core", version = "0.17.2" } +foundry-compilers = { path = "crates/compilers", version = "0.17.3" } +foundry-compilers-artifacts = { path = "crates/artifacts/artifacts", version = "0.17.3" } +foundry-compilers-artifacts-solc = { path = "crates/artifacts/solc", version = "0.17.3" } +foundry-compilers-artifacts-vyper = { path = "crates/artifacts/vyper", version = "0.17.3" } +foundry-compilers-core = { path = "crates/core", version = "0.17.3" } alloy-json-abi = { version = "1.0", features = ["serde_json"] } alloy-primitives = { version = "1.0", features = ["serde", "rand"] } From 24dfb58e6ef4fa3d4e8859caef4ae400e0eba871 Mon Sep 17 00:00:00 2001 From: Soubhik Singha Mahapatra <160333583+Soubhik-10@users.noreply.github.com> Date: Thu, 26 Jun 2025 21:17:24 +0530 Subject: [PATCH 24/57] chore: upstreamed `strip_bytecode_placeholders` from foundry (#287) ref [#10596](https://github.com/foundry-rs/foundry/pull/10596#discussion_r2161689091) --- crates/artifacts/solc/Cargo.toml | 1 + crates/artifacts/solc/src/bytecode.rs | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/crates/artifacts/solc/Cargo.toml b/crates/artifacts/solc/Cargo.toml index 807b2f7..15f0f12 100644 --- a/crates/artifacts/solc/Cargo.toml +++ b/crates/artifacts/solc/Cargo.toml @@ -25,6 +25,7 @@ serde.workspace = true thiserror.workspace = true tracing.workspace = true yansi.workspace = true +regex.workspace = true # async tokio = { workspace = true, optional = true, features = ["fs"] } diff --git a/crates/artifacts/solc/src/bytecode.rs b/crates/artifacts/solc/src/bytecode.rs index eb6985f..b88121d 100644 --- a/crates/artifacts/solc/src/bytecode.rs +++ b/crates/artifacts/solc/src/bytecode.rs @@ -353,6 +353,21 @@ impl BytecodeObject { pub fn contains_placeholder(&self, file: &str, library: &str) -> bool { self.contains_fully_qualified_placeholder(&format!("{file}:{library}")) } + + /// Strips all __$xxx$__ placeholders from the bytecode if it's an unlinked bytecode. + /// by replacing them with 20 zero bytes. + /// This is useful for matching bytecodes to a contract source, and for the source map, + /// in which the actual address of the placeholder isn't important. + pub fn strip_bytecode_placeholders(&self) -> Option { + match &self { + Self::Bytecode(bytes) => Some(bytes.clone()), + Self::Unlinked(s) => { + // Replace all __$xxx$__ placeholders with 32 zero bytes + let bytes = replace_placeholders_and_decode(s).ok()?; + Some(bytes.into()) + } + } + } } // Returns an empty bytecode object @@ -388,6 +403,13 @@ where } } +// Replace all __$xxx$__ placeholders with 32 zero bytes +pub fn replace_placeholders_and_decode(s: &str) -> Result, hex::FromHexError> { + let re = regex::Regex::new(r"_\$.{34}\$_").expect("invalid regex"); + let s = re.replace_all(s, "00".repeat(40)); + hex::decode(s.as_bytes()) +} + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct DeployedBytecode { #[serde(flatten)] From 6b44ff8b62bfd4898f0e88344edab63928467fb7 Mon Sep 17 00:00:00 2001 From: GarmashAlex Date: Thu, 26 Jun 2025 19:54:55 +0300 Subject: [PATCH 25/57] Fix typos in comments and variable names across solc-related modules (#286) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fixed typos in comments: - "identfiers" → "identifiers" in `output_selection.rs` - "blocking_intsall" → "blocking_install" in `compiler.rs` - "depeending" → "depending" in `compiler.rs` - Corrected a variable name typo: - "output_selecttion_specific" → "output_selection_specific` in `output_selection.rs` tests --- crates/artifacts/solc/src/output_selection.rs | 6 +++--- crates/compilers/src/compilers/solc/compiler.rs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/artifacts/solc/src/output_selection.rs b/crates/artifacts/solc/src/output_selection.rs index c56c59e..693c973 100644 --- a/crates/artifacts/solc/src/output_selection.rs +++ b/crates/artifacts/solc/src/output_selection.rs @@ -201,7 +201,7 @@ impl ContractOutputSelection { /// that solc emits. /// /// These correspond to the fields in `CompactBytecode`, `CompactDeployedBytecode`, ABI, and - /// method identfiers. + /// method identifiers. pub fn basic() -> Vec { // We don't include all the `bytecode` fields because `generatedSources` is a massive JSON // object and is not used by Foundry. @@ -610,7 +610,7 @@ mod tests { assert!(!output_selection.is_subset_of(&output_selection_empty)); assert!(!output_selection_abi.is_subset_of(&output_selection_empty)); - let output_selecttion_specific = OutputSelection::from(BTreeMap::from([( + let output_selection_specific = OutputSelection::from(BTreeMap::from([( "Contract.sol".to_string(), BTreeMap::from([( "Contract".to_string(), @@ -622,7 +622,7 @@ mod tests { )]), )])); - assert!(!output_selecttion_specific.is_subset_of(&output_selection)); + assert!(!output_selection_specific.is_subset_of(&output_selection)); } #[test] diff --git a/crates/compilers/src/compilers/solc/compiler.rs b/crates/compilers/src/compilers/solc/compiler.rs index cd4d755..c74785a 100644 --- a/crates/compilers/src/compilers/solc/compiler.rs +++ b/crates/compilers/src/compilers/solc/compiler.rs @@ -293,7 +293,7 @@ impl Solc { trace!("blocking installing solc version \"{}\"", version); crate::report::solc_installation_start(&version); - // The async version `svm::install` is used instead of `svm::blocking_intsall` + // The async version `svm::install` is used instead of `svm::blocking_install` // because the underlying `reqwest::blocking::Client` does not behave well // inside of a Tokio runtime. See: https://github.com/seanmonstar/reqwest/issues/1017 match RuntimeOrHandle::new().block_on(svm::install(&version)) { @@ -475,7 +475,7 @@ impl Solc { move |err| SolcError::io(err, &self.solc) } - /// Configures [Command] object depeending on settings and solc version used. + /// Configures [Command] object depending on settings and solc version used. /// Some features are only supported by newer versions of solc, so we have to disable them for /// older ones. pub fn configure_cmd(&self) -> Command { From 98ede42fdfc673175eaa747c433b110ace4516ae Mon Sep 17 00:00:00 2001 From: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Date: Mon, 30 Jun 2025 23:35:52 +0200 Subject: [PATCH 26/57] chore: bump vyper to 0.4.3 which adds support for `prague` (#285) Adds support for Vyper 0.4.3 which now defaults to `prague` --- crates/artifacts/vyper/src/settings.rs | 5 ++++- crates/compilers/tests/project.rs | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/artifacts/vyper/src/settings.rs b/crates/artifacts/vyper/src/settings.rs index 3653b0a..f162d28 100644 --- a/crates/artifacts/vyper/src/settings.rs +++ b/crates/artifacts/vyper/src/settings.rs @@ -13,6 +13,7 @@ pub const VYPER_BERLIN: Version = Version::new(0, 3, 0); pub const VYPER_PARIS: Version = Version::new(0, 3, 7); pub const VYPER_SHANGHAI: Version = Version::new(0, 3, 8); pub const VYPER_CANCUN: Version = Version::new(0, 3, 8); +pub const VYPER_PRAGUE: Version = Version::new(0, 4, 3); const VYPER_0_4: Version = Version::new(0, 4, 0); @@ -126,7 +127,9 @@ impl VyperSettings { /// Adjusts the EVM version based on the compiler version. pub fn normalize_evm_version(&mut self, version: &Version) { if let Some(evm_version) = &mut self.evm_version { - *evm_version = if *evm_version >= EvmVersion::Cancun && *version >= VYPER_CANCUN { + *evm_version = if *evm_version >= EvmVersion::Prague && *version >= VYPER_PRAGUE { + EvmVersion::Prague + } else if *evm_version >= EvmVersion::Cancun && *version >= VYPER_CANCUN { EvmVersion::Cancun } else if *evm_version >= EvmVersion::Shanghai && *version >= VYPER_SHANGHAI { EvmVersion::Shanghai diff --git a/crates/compilers/tests/project.rs b/crates/compilers/tests/project.rs index 334d438..fcae495 100644 --- a/crates/compilers/tests/project.rs +++ b/crates/compilers/tests/project.rs @@ -60,7 +60,7 @@ pub static VYPER: LazyLock = LazyLock::new(|| { return Vyper::new(&path).unwrap(); } - let base = "https://github.com/vyperlang/vyper/releases/download/v0.4.0/vyper.0.4.0+commit.e9db8d9f"; + let base = "https://github.com/vyperlang/vyper/releases/download/v0.4.3/vyper.0.4.3+commit.bff19ea2"; let url = format!( "{base}.{}", match platform() { From 5846d3ca5181ecf4515997eecdeac52fdda22d29 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Tue, 1 Jul 2025 00:14:51 +0200 Subject: [PATCH 27/57] chore: release 0.17.4 --- CHANGELOG.md | 18 ++++++++++++++++++ Cargo.toml | 12 ++++++------ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f73847..90fded9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,26 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.17.4](https://github.com/foundry-rs/compilers/releases/tag/v0.17.4) - 2025-06-30 + +### Bug Fixes + +- Fix typos in comments and variable names across solc-related modules ([#286](https://github.com/foundry-rs/compilers/issues/286)) + +### Dependencies + +- Bump vyper to 0.4.3 which adds support for `prague` ([#285](https://github.com/foundry-rs/compilers/issues/285)) + +### Miscellaneous Tasks + +- Upstreamed `strip_bytecode_placeholders` from foundry ([#287](https://github.com/foundry-rs/compilers/issues/287)) + ## [0.17.3](https://github.com/foundry-rs/compilers/releases/tag/v0.17.3) - 2025-06-14 +### Miscellaneous Tasks + +- Release 0.17.3 + ### Other - Revert "fix: implement proper serde handling for unknown AST node typ… ([#284](https://github.com/foundry-rs/compilers/issues/284)) diff --git a/Cargo.toml b/Cargo.toml index d2277f8..9b595ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ resolver = "2" [workspace.package] authors = ["Foundry Maintainers"] -version = "0.17.3" +version = "0.17.4" rust-version = "1.87" readme = "README.md" license = "MIT OR Apache-2.0" @@ -36,11 +36,11 @@ redundant-lifetimes = "warn" all = "warn" [workspace.dependencies] -foundry-compilers = { path = "crates/compilers", version = "0.17.3" } -foundry-compilers-artifacts = { path = "crates/artifacts/artifacts", version = "0.17.3" } -foundry-compilers-artifacts-solc = { path = "crates/artifacts/solc", version = "0.17.3" } -foundry-compilers-artifacts-vyper = { path = "crates/artifacts/vyper", version = "0.17.3" } -foundry-compilers-core = { path = "crates/core", version = "0.17.3" } +foundry-compilers = { path = "crates/compilers", version = "0.17.4" } +foundry-compilers-artifacts = { path = "crates/artifacts/artifacts", version = "0.17.4" } +foundry-compilers-artifacts-solc = { path = "crates/artifacts/solc", version = "0.17.4" } +foundry-compilers-artifacts-vyper = { path = "crates/artifacts/vyper", version = "0.17.4" } +foundry-compilers-core = { path = "crates/core", version = "0.17.4" } alloy-json-abi = { version = "1.0", features = ["serde_json"] } alloy-primitives = { version = "1.0", features = ["serde", "rand"] } From ca26835e922ae449430a6376532dab7bbdbd23b5 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> Date: Mon, 14 Jul 2025 09:48:08 +0200 Subject: [PATCH 28/57] chore: bump solar + MSRV (#289) bumps solar to v0.1.5 and MSRV to 1.88 --- .github/workflows/ci.yml | 4 ++-- Cargo.toml | 12 +++++++++--- README.md | 2 +- clippy.toml | 2 +- crates/compilers/src/cache/iface.rs | 2 +- crates/compilers/src/resolver/parse.rs | 2 +- 6 files changed, 15 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e05f11..2a4e68c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,11 +22,11 @@ jobs: fail-fast: false matrix: os: ["ubuntu-latest", "macos-latest", "windows-latest"] - rust: ["stable", "1.87"] + rust: ["stable", "1.88"] flags: ["", "--all-features"] exclude: # Skip because some features have higher MSRV. - - rust: "1.87" # MSRV + - rust: "1.88" # MSRV flags: "--all-features" steps: - uses: actions/checkout@v4 diff --git a/Cargo.toml b/Cargo.toml index 9b595ad..3862c6c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ resolver = "2" [workspace.package] authors = ["Foundry Maintainers"] version = "0.17.4" -rust-version = "1.87" +rust-version = "1.88" readme = "README.md" license = "MIT OR Apache-2.0" repository = "https://github.com/foundry-rs/compilers" @@ -54,8 +54,8 @@ semver = { version = "1.0", features = ["serde"] } serde = { version = "1", features = ["derive", "rc"] } serde_json = "1.0" similar-asserts = "1" -solar-parse = { version = "=0.1.4", default-features = false } -solar-sema = { version = "=0.1.4", default-features = false } +solar-parse = { version = "=0.1.5", default-features = false } +solar-sema = { version = "=0.1.5", default-features = false } svm = { package = "svm-rs", version = "0.5", default-features = false } tempfile = "3.9" thiserror = "2" @@ -68,3 +68,9 @@ futures-util = "0.3" tokio = { version = "1.35", features = ["rt-multi-thread"] } snapbox = "0.6.9" + +# [patch.crates-io] +# solar-parse = { git = "https://github.com/paradigmxyz/solar", branch = "main" } +# solar-sema = { git = "https://github.com/paradigmxyz/solar", branch = "main" } +# solar-ast = { git = "https://github.com/paradigmxyz/solar", branch = "main" } +# solar-interface = { git = "https://github.com/paradigmxyz/solar", branch = "main" } diff --git a/README.md b/README.md index 2b636be..80b3851 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ When updating this, also update: - .github/workflows/ci.yml --> -The current MSRV (minimum supported rust version) is 1.87. +The current MSRV (minimum supported rust version) is 1.88. Note that the MSRV is not increased automatically, and only as part of a minor release. diff --git a/clippy.toml b/clippy.toml index d946dae..f3322b5 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1 +1 @@ -msrv = "1.87" +msrv = "1.88" diff --git a/crates/compilers/src/cache/iface.rs b/crates/compilers/src/cache/iface.rs index 8f006ef..ff29be1 100644 --- a/crates/compilers/src/cache/iface.rs +++ b/crates/compilers/src/cache/iface.rs @@ -38,7 +38,7 @@ pub(crate) fn interface_representation_ast( let is_exposed = match function.kind { // Function with external or public visibility ast::FunctionKind::Function => { - function.header.visibility >= Some(ast::Visibility::Public) + function.header.visibility.map(|v| *v) >= Some(ast::Visibility::Public) } ast::FunctionKind::Constructor | ast::FunctionKind::Fallback diff --git a/crates/compilers/src/resolver/parse.rs b/crates/compilers/src/resolver/parse.rs index 0627bb0..06c21f9 100644 --- a/crates/compilers/src/resolver/parse.rs +++ b/crates/compilers/src/resolver/parse.rs @@ -272,7 +272,7 @@ fn library_is_inlined(contract: &ast::ItemContract<'_>) -> bool { }) .all(|f| { !matches!( - f.header.visibility, + f.header.visibility.map(|v| *v), Some(ast::Visibility::Public | ast::Visibility::External) ) }) From df39e4cfa1194123cfd31a91948bd6b79e02dd50 Mon Sep 17 00:00:00 2001 From: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Date: Mon, 14 Jul 2025 11:06:37 +0200 Subject: [PATCH 29/57] chore: update deps (#290) Bump outdated dependencies as preparation for next release --- Cargo.toml | 14 +++++++------- crates/compilers/Cargo.toml | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3862c6c..66d3da8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,14 +42,14 @@ foundry-compilers-artifacts-solc = { path = "crates/artifacts/solc", version = " foundry-compilers-artifacts-vyper = { path = "crates/artifacts/vyper", version = "0.17.4" } foundry-compilers-core = { path = "crates/core", version = "0.17.4" } -alloy-json-abi = { version = "1.0", features = ["serde_json"] } -alloy-primitives = { version = "1.0", features = ["serde", "rand"] } +alloy-json-abi = { version = "1.2", features = ["serde_json"] } +alloy-primitives = { version = "1.2", features = ["serde", "rand"] } cfg-if = "1.0" dunce = "1.0" memmap2 = "0.9" path-slash = "0.2" -rayon = "1.8" -regex = "1.10" +rayon = "1.10" +regex = "1.11" semver = { version = "1.0", features = ["serde"] } serde = { version = "1", features = ["derive", "rc"] } serde_json = "1.0" @@ -57,7 +57,7 @@ similar-asserts = "1" solar-parse = { version = "=0.1.5", default-features = false } solar-sema = { version = "=0.1.5", default-features = false } svm = { package = "svm-rs", version = "0.5", default-features = false } -tempfile = "3.9" +tempfile = "3.20" thiserror = "2" tracing = "0.1" walkdir = "2.5" @@ -65,9 +65,9 @@ yansi = "1.0" # async futures-util = "0.3" -tokio = { version = "1.35", features = ["rt-multi-thread"] } +tokio = { version = "1.46", features = ["rt-multi-thread"] } -snapbox = "0.6.9" +snapbox = "0.6.21" # [patch.crates-io] # solar-parse = { git = "https://github.com/paradigmxyz/solar", branch = "main" } diff --git a/crates/compilers/Cargo.toml b/crates/compilers/Cargo.toml index 73808a0..3f7e121 100644 --- a/crates/compilers/Cargo.toml +++ b/crates/compilers/Cargo.toml @@ -45,7 +45,7 @@ dirs = "6.0" itertools = ">=0.13, <=0.14" # project-util -tempfile = { version = "3.9", optional = true } +tempfile = { version = "3.20", optional = true } fs_extra = { version = "1.3", optional = true } rand = { version = "0.8", optional = true } @@ -60,10 +60,10 @@ tracing-subscriber = { version = "0.3", default-features = false, features = [ "fmt", ] } similar-asserts.workspace = true -fd-lock = "4.0.0" -tokio = { version = "1.35", features = ["rt-multi-thread", "macros"] } +fd-lock = "4.0.4" +tokio = { version = "1.46", features = ["rt-multi-thread", "macros"] } reqwest = "0.12" -tempfile = "3.9" +tempfile = "3.20" snapbox.workspace = true foundry-compilers-core = { workspace = true, features = ["test-utils"] } From a58445c38d5c9aa31fd41b5d017a5cd7de2be302 Mon Sep 17 00:00:00 2001 From: zerosnacks Date: Mon, 14 Jul 2025 11:08:41 +0200 Subject: [PATCH 30/57] bump to 0.18.0 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 80b3851..6f2cd24 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ To install, simply add `foundry-compilers` to your cargo dependencies. ```toml [dependencies] -foundry-compilers = "0.17.1" +foundry-compilers = "0.18.0" ``` Example usage: From 50b9f0ffaf5dcc2b5e5f8c38db13f2a98763cdb4 Mon Sep 17 00:00:00 2001 From: zerosnacks Date: Mon, 14 Jul 2025 11:09:23 +0200 Subject: [PATCH 31/57] chore: release 0.18.0 --- CHANGELOG.md | 9 +++++++++ Cargo.toml | 12 ++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90fded9..3af30f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.18.0](https://github.com/foundry-rs/compilers/releases/tag/v0.18.0) - 2025-07-14 + +### Dependencies + +- Bump to 0.18.0 +- Update deps ([#290](https://github.com/foundry-rs/compilers/issues/290)) +- Bump solar + MSRV ([#289](https://github.com/foundry-rs/compilers/issues/289)) + ## [0.17.4](https://github.com/foundry-rs/compilers/releases/tag/v0.17.4) - 2025-06-30 ### Bug Fixes @@ -17,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Miscellaneous Tasks +- Release 0.17.4 - Upstreamed `strip_bytecode_placeholders` from foundry ([#287](https://github.com/foundry-rs/compilers/issues/287)) ## [0.17.3](https://github.com/foundry-rs/compilers/releases/tag/v0.17.3) - 2025-06-14 diff --git a/Cargo.toml b/Cargo.toml index 66d3da8..5ef69a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ resolver = "2" [workspace.package] authors = ["Foundry Maintainers"] -version = "0.17.4" +version = "0.18.0" rust-version = "1.88" readme = "README.md" license = "MIT OR Apache-2.0" @@ -36,11 +36,11 @@ redundant-lifetimes = "warn" all = "warn" [workspace.dependencies] -foundry-compilers = { path = "crates/compilers", version = "0.17.4" } -foundry-compilers-artifacts = { path = "crates/artifacts/artifacts", version = "0.17.4" } -foundry-compilers-artifacts-solc = { path = "crates/artifacts/solc", version = "0.17.4" } -foundry-compilers-artifacts-vyper = { path = "crates/artifacts/vyper", version = "0.17.4" } -foundry-compilers-core = { path = "crates/core", version = "0.17.4" } +foundry-compilers = { path = "crates/compilers", version = "0.18.0" } +foundry-compilers-artifacts = { path = "crates/artifacts/artifacts", version = "0.18.0" } +foundry-compilers-artifacts-solc = { path = "crates/artifacts/solc", version = "0.18.0" } +foundry-compilers-artifacts-vyper = { path = "crates/artifacts/vyper", version = "0.18.0" } +foundry-compilers-core = { path = "crates/core", version = "0.18.0" } alloy-json-abi = { version = "1.2", features = ["serde_json"] } alloy-primitives = { version = "1.2", features = ["serde", "rand"] } From 5fc80c983ee12819f301ce13753f1367cb8b7de3 Mon Sep 17 00:00:00 2001 From: zerosnacks Date: Mon, 14 Jul 2025 11:10:22 +0200 Subject: [PATCH 32/57] chore: release 0.18.0 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3af30f7..4943e33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Update deps ([#290](https://github.com/foundry-rs/compilers/issues/290)) - Bump solar + MSRV ([#289](https://github.com/foundry-rs/compilers/issues/289)) +### Miscellaneous Tasks + +- Release 0.18.0 + ## [0.17.4](https://github.com/foundry-rs/compilers/releases/tag/v0.17.4) - 2025-06-30 ### Bug Fixes From e8a2e0279b873e4eb5f2b19936e90fb4330b8f5b Mon Sep 17 00:00:00 2001 From: Galoretka Date: Wed, 23 Jul 2025 22:12:16 +0300 Subject: [PATCH 33/57] Remove duplicate assembly check in is_dirty (#292) This commit removes a redundant check for assembly in the is_dirty method, leaving only a single, correct check for assembly. No other logic was changed. --- crates/compilers/src/artifact_output/configurable.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/crates/compilers/src/artifact_output/configurable.rs b/crates/compilers/src/artifact_output/configurable.rs index c661a4e..7f24081 100644 --- a/crates/compilers/src/artifact_output/configurable.rs +++ b/crates/compilers/src/artifact_output/configurable.rs @@ -377,9 +377,6 @@ impl ArtifactOutput for ConfigurableArtifacts { if assembly && artifact.assembly.is_none() { return Ok(true); } - if assembly && artifact.assembly.is_none() { - return Ok(true); - } if legacy_assembly && artifact.legacy_assembly.is_none() { return Ok(true); } From 4e454b4c47d19124f24e87d7b8cd054e520344ae Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Wed, 30 Jul 2025 20:32:55 +0200 Subject: [PATCH 34/57] chore: add more instrumentation (#293) `#[instrument]` and clean up. No functional changes intended. --- crates/artifacts/solc/src/sources.rs | 5 ++-- crates/compilers/src/cache.rs | 20 ++++++++++++- crates/compilers/src/compile/output/mod.rs | 1 + crates/compilers/src/compile/project.rs | 8 +++++- crates/compilers/src/compilers/mod.rs | 28 +++++++++++-------- .../compilers/src/compilers/solc/compiler.rs | 10 +++++-- crates/compilers/src/compilers/vyper/mod.rs | 4 +-- .../compilers/src/compilers/vyper/parser.rs | 1 + crates/compilers/src/resolver/mod.rs | 1 + crates/compilers/src/resolver/parse.rs | 1 + 10 files changed, 58 insertions(+), 21 deletions(-) diff --git a/crates/artifacts/solc/src/sources.rs b/crates/artifacts/solc/src/sources.rs index b7e103d..c101918 100644 --- a/crates/artifacts/solc/src/sources.rs +++ b/crates/artifacts/solc/src/sources.rs @@ -124,7 +124,7 @@ impl Source { } /// Reads the file's content - #[instrument(name = "read_source", level = "debug", skip_all, err)] + #[instrument(name = "Source::read", skip_all, err)] pub fn read(file: &Path) -> Result { trace!(file=%file.display()); let mut content = fs::read_to_string(file).map_err(|err| SolcIoError::new(err, file))?; @@ -162,6 +162,7 @@ impl Source { } /// Reads all files + #[instrument(name = "Source::read_all", skip_all)] pub fn read_all(files: I) -> Result where I: IntoIterator, @@ -211,7 +212,7 @@ impl Source { #[cfg(feature = "async")] impl Source { /// async version of `Self::read` - #[instrument(name = "async_read_source", level = "debug", skip_all, err)] + #[instrument(name = "Source::async_read", skip_all, err)] pub async fn async_read(file: &Path) -> Result { let mut content = tokio::fs::read_to_string(file).await.map_err(|err| SolcIoError::new(err, file))?; diff --git a/crates/compilers/src/cache.rs b/crates/compilers/src/cache.rs index e22c538..07313a3 100644 --- a/crates/compilers/src/cache.rs +++ b/crates/compilers/src/cache.rs @@ -118,7 +118,7 @@ impl CompilerCache { /// cache.join_artifacts_files(project.artifacts_path()); /// # Ok::<_, Box>(()) /// ``` - #[instrument(skip_all, name = "sol-files-cache::read")] + #[instrument(name = "CompilerCache::read", skip_all)] pub fn read(path: &Path) -> Result { trace!("reading solfiles cache at {}", path.display()); let cache: Self = utils::read_json_file(path)?; @@ -149,6 +149,7 @@ impl CompilerCache { } /// Write the cache as json file to the given path + #[instrument(name = "CompilerCache::write", skip_all)] pub fn write(&self, path: &Path) -> Result<()> { trace!("writing cache with {} entries to json file: \"{}\"", self.len(), path.display()); utils::create_parent_dir_all(path)?; @@ -158,6 +159,7 @@ impl CompilerCache { } /// Removes build infos which don't have any artifacts linked to them. + #[instrument(skip_all)] pub fn remove_outdated_builds(&mut self) { let mut outdated = Vec::new(); for build_id in &self.builds { @@ -180,6 +182,7 @@ impl CompilerCache { } /// Sets the `CacheEntry`'s file paths to `root` adjoined to `self.file`. + #[instrument(skip_all)] pub fn join_entries(&mut self, root: &Path) -> &mut Self { self.files = std::mem::take(&mut self.files) .into_iter() @@ -189,6 +192,7 @@ impl CompilerCache { } /// Removes `base` from all `CacheEntry` paths + #[instrument(skip_all)] pub fn strip_entries_prefix(&mut self, base: &Path) -> &mut Self { self.files = std::mem::take(&mut self.files) .into_iter() @@ -198,12 +202,14 @@ impl CompilerCache { } /// Sets the artifact files location to `base` adjoined to the `CachEntries` artifacts. + #[instrument(skip_all)] pub fn join_artifacts_files(&mut self, base: &Path) -> &mut Self { self.files.values_mut().for_each(|entry| entry.join_artifacts_files(base)); self } /// Removes `base` from all artifact file paths + #[instrument(skip_all)] pub fn strip_artifact_files_prefixes(&mut self, base: &Path) -> &mut Self { self.files.values_mut().for_each(|entry| entry.strip_artifact_files_prefixes(base)); self @@ -212,6 +218,7 @@ impl CompilerCache { /// Removes all `CacheEntry` which source files don't exist on disk /// /// **NOTE:** this assumes the `files` are absolute + #[instrument(skip_all)] pub fn remove_missing_files(&mut self) { trace!("remove non existing files from cache"); self.files.retain(|file, _| { @@ -292,6 +299,7 @@ impl CompilerCache { /// /// **NOTE**: unless the cache's `files` keys were modified `contract_file` is expected to be /// absolute. + #[instrument(skip_all)] pub fn read_artifact( &self, contract_file: &Path, @@ -318,6 +326,7 @@ impl CompilerCache { /// let artifacts = cache.read_artifacts::()?; /// # Ok::<_, Box>(()) /// ``` + #[instrument(skip_all)] pub fn read_artifacts( &self, ) -> Result> { @@ -335,6 +344,7 @@ impl CompilerCache { /// objects, so we are basically just partially deserializing build infos here. /// /// [BuildContext]: crate::buildinfo::BuildContext + #[instrument(skip_all)] pub fn read_builds(&self, build_info_dir: &Path) -> Result> { use rayon::prelude::*; @@ -491,6 +501,7 @@ impl CacheEntry { /// Reads all artifact files associated with the `CacheEntry` /// /// **Note:** all artifact file paths should be absolute. + #[instrument(skip_all)] fn read_artifact_files( &self, ) -> Result>>> { @@ -514,6 +525,7 @@ impl CacheEntry { Ok(artifacts) } + #[instrument(skip_all)] pub(crate) fn merge_artifacts<'a, A, I, T: 'a>(&mut self, artifacts: I) where I: IntoIterator, @@ -1017,6 +1029,7 @@ impl<'a, T: ArtifactOutput, C: Compiler> ArtifactsCache<'a, T, C> { /// Create a new cache instance with the given files + #[instrument(name = "ArtifactsCache::new", skip(project, edges))] pub fn new( project: &'a Project, edges: GraphEdges, @@ -1042,6 +1055,8 @@ impl<'a, T: ArtifactOutput, C: Compiler> } } + trace!(invalidate_cache, "cache invalidated"); + // new empty cache CompilerCache::new(Default::default(), paths, preprocessed) } @@ -1135,6 +1150,7 @@ impl<'a, T: ArtifactOutput, C: Compiler> } /// Adds the file's hashes to the set if not set yet + #[instrument(skip_all)] pub fn remove_dirty_sources(&mut self) { match self { ArtifactsCache::Ephemeral(..) => {} @@ -1161,6 +1177,7 @@ impl<'a, T: ArtifactOutput, C: Compiler> } /// Filters out those sources that don't need to be compiled + #[instrument(name = "ArtifactsCache::filter", skip_all)] pub fn filter(&mut self, sources: &mut Sources, version: &Version, profile: &str) { match self { ArtifactsCache::Ephemeral(..) => {} @@ -1173,6 +1190,7 @@ impl<'a, T: ArtifactOutput, C: Compiler> /// compiled and written to disk `written_artifacts`. /// /// Returns all the _cached_ artifacts. + #[instrument(name = "ArtifactsCache::consume", skip_all)] pub fn consume( self, written_artifacts: &Artifacts, diff --git a/crates/compilers/src/compile/output/mod.rs b/crates/compilers/src/compile/output/mod.rs index 23f2745..fcc0148 100644 --- a/crates/compilers/src/compile/output/mod.rs +++ b/crates/compilers/src/compile/output/mod.rs @@ -87,6 +87,7 @@ impl, C: Compiler> ProjectCompileOutput { /// Converts all `\\` separators in _all_ paths to `/` + #[instrument(skip_all)] pub fn slash_paths(&mut self) { self.compiler_output.slash_paths(); self.compiled_artifacts.slash_paths(); diff --git a/crates/compilers/src/compile/project.rs b/crates/compilers/src/compile/project.rs index 6ffc26f..32859e7 100644 --- a/crates/compilers/src/compile/project.rs +++ b/crates/compilers/src/compile/project.rs @@ -171,6 +171,7 @@ impl<'a, T: ArtifactOutput, C: Compiler> /// /// Multiple (`Solc` -> `Sources`) pairs can be compiled in parallel if the `Project` allows /// multiple `jobs`, see [`crate::Project::set_solc_jobs()`]. + #[instrument(name = "ProjectCompiler::new", skip_all)] pub fn with_sources(project: &'a Project, mut sources: Sources) -> Result { if let Some(filter) = &project.sparse_output { sources.retain(|f, _| filter.is_match(f)) @@ -209,6 +210,7 @@ impl<'a, T: ArtifactOutput, C: Compiler> /// let output = project.compile()?; /// # Ok::<(), Box>(()) /// ``` + #[instrument(name = "compile_project", skip_all)] pub fn compile(self) -> Result> { let slash_paths = self.project.slash_paths; @@ -226,6 +228,7 @@ impl<'a, T: ArtifactOutput, C: Compiler> /// Does basic preprocessing /// - sets proper source unit names /// - check cache + #[instrument(skip_all)] fn preprocess(self) -> Result> { trace!("preprocessing"); let Self { edges, project, mut sources, primary_profiles, preprocessor } = self; @@ -265,6 +268,7 @@ impl<'a, T: ArtifactOutput, C: Compiler> PreprocessedState<'a, T, C> { /// advance to the next state by compiling all sources + #[instrument(skip_all)] fn compile(self) -> Result> { trace!("compiling"); let PreprocessedState { sources, mut cache, primary_profiles, preprocessor } = self; @@ -297,7 +301,7 @@ impl<'a, T: ArtifactOutput, C: Compiler> /// /// Writes all output contracts to disk if enabled in the `Project` and if the build was /// successful - #[instrument(skip_all, name = "write-artifacts")] + #[instrument(skip_all)] fn write_artifacts(self) -> Result> { let CompiledState { output, cache, primary_profiles } = self; @@ -365,6 +369,7 @@ impl, C: Compiler> /// Writes the cache file /// /// this concludes the [`Project::compile()`] statemachine + #[instrument(skip_all)] fn write_cache(self) -> Result> { let ArtifactsState { output, cache, compiled_artifacts } = self; let project = cache.project(); @@ -436,6 +441,7 @@ impl CompilerSources<'_, L, S> { } /// Filters out all sources that don't need to be compiled, see [`ArtifactsCache::filter`] + #[instrument(name = "CompilerSources::filter", skip_all)] fn filter< T: ArtifactOutput, C: Compiler, diff --git a/crates/compilers/src/compilers/mod.rs b/crates/compilers/src/compilers/mod.rs index 5abb74b..0081051 100644 --- a/crates/compilers/src/compilers/mod.rs +++ b/crates/compilers/src/compilers/mod.rs @@ -13,7 +13,7 @@ use semver::{Version, VersionReq}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::{ borrow::Cow, - collections::{BTreeMap, BTreeSet, HashMap, HashSet}, + collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap, HashSet}, fmt::{Debug, Display}, hash::Hash, path::{Path, PathBuf}, @@ -356,20 +356,24 @@ pub(crate) fn cache_version( f: impl FnOnce(&Path) -> Result, ) -> Result { #[allow(clippy::complexity)] - static VERSION_CACHE: OnceLock, Version>>>> = + static VERSION_CACHE: OnceLock), Version>>> = OnceLock::new(); - let mut lock = VERSION_CACHE - .get_or_init(|| Mutex::new(HashMap::new())) + + let mut cache = VERSION_CACHE + .get_or_init(Default::default) .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); - if let Some(version) = lock.get(&path).and_then(|versions| versions.get(args)) { - return Ok(version.clone()); + match cache.entry((path, args.to_vec())) { + Entry::Occupied(entry) => Ok(entry.get().clone()), + Entry::Vacant(entry) => { + let path = &entry.key().0; + let _guard = + debug_span!("get_version", path = %path.file_name().map(|n| n.to_string_lossy()).unwrap_or_else(|| path.to_string_lossy())) + .entered(); + let version = f(path)?; + entry.insert(version.clone()); + Ok(version) + } } - - let version = f(&path)?; - - lock.entry(path).or_default().insert(args.to_vec(), version.clone()); - - Ok(version) } diff --git a/crates/compilers/src/compilers/solc/compiler.rs b/crates/compilers/src/compilers/solc/compiler.rs index c74785a..118aa1f 100644 --- a/crates/compilers/src/compilers/solc/compiler.rs +++ b/crates/compilers/src/compilers/solc/compiler.rs @@ -89,6 +89,7 @@ impl Solc { /// A new instance which points to `solc`. Invokes `solc --version` to determine the version. /// /// Returns error if `solc` is not found in the system or if the version cannot be retrieved. + #[instrument(name = "Solc::new", skip_all)] pub fn new(path: impl Into) -> Result { let path = path.into(); let version = Self::version(path.clone())?; @@ -200,6 +201,7 @@ impl Solc { /// /// Ok::<_, Box>(()) /// ``` + #[instrument(skip_all)] pub fn find_svm_installed_version(version: &Version) -> Result> { let version = format!("{}.{}.{}", version.major, version.minor, version.patch); let solc = Self::svm_home() @@ -266,6 +268,7 @@ impl Solc { /// # } /// ``` #[cfg(feature = "svm-solc")] + #[instrument(name = "Solc::install", skip_all)] pub async fn install(version: &Version) -> std::result::Result { trace!("installing solc version \"{}\"", version); crate::report::solc_installation_start(version); @@ -283,6 +286,7 @@ impl Solc { /// Blocking version of `Self::install` #[cfg(feature = "svm-solc")] + #[instrument(name = "Solc::blocking_install", skip_all)] pub fn blocking_install(version: &Version) -> std::result::Result { use foundry_compilers_core::utils::RuntimeOrHandle; @@ -311,6 +315,7 @@ impl Solc { /// Verify that the checksum for this version of solc is correct. We check against the SHA256 /// checksum from the build information published by [binaries.soliditylang.org](https://binaries.soliditylang.org/) #[cfg(feature = "svm-solc")] + #[instrument(name = "Solc::verify_checksum", skip_all)] pub fn verify_checksum(&self) -> Result<()> { let version = self.version_short(); let mut version_path = svm::version_path(version.to_string().as_str()); @@ -407,6 +412,7 @@ impl Solc { } /// Compiles with `--standard-json` and deserializes the output as the given `D`. + #[instrument(name = "Solc::compile", skip_all)] pub fn compile_as(&self, input: &T) -> Result { let output = self.compile_output(input)?; @@ -417,7 +423,7 @@ impl Solc { } /// Compiles with `--standard-json` and returns the raw `stdout` output. - #[instrument(name = "compile", level = "debug", skip_all)] + #[instrument(name = "Solc::compile_raw", skip_all)] pub fn compile_output(&self, input: &T) -> Result> { let mut cmd = self.configure_cmd(); @@ -447,13 +453,11 @@ impl Solc { } /// Invokes `solc --version` and parses the output as a SemVer [`Version`]. - #[instrument(level = "debug", skip_all)] pub fn version(solc: impl Into) -> Result { Self::version_with_args(solc, &[]) } /// Invokes `solc --version` and parses the output as a SemVer [`Version`]. - #[instrument(level = "debug", skip_all)] pub fn version_with_args(solc: impl Into, args: &[String]) -> Result { crate::cache_version(solc.into(), args, |solc| { let mut cmd = Command::new(solc); diff --git a/crates/compilers/src/compilers/vyper/mod.rs b/crates/compilers/src/compilers/vyper/mod.rs index 8304b76..6d85c49 100644 --- a/crates/compilers/src/compilers/vyper/mod.rs +++ b/crates/compilers/src/compilers/vyper/mod.rs @@ -127,6 +127,7 @@ impl Vyper { } /// Compiles with `--standard-json` and deserializes the output as the given `D`. + #[instrument(name = "Vyper::compile", skip_all)] pub fn compile_as(&self, input: &T) -> Result { let output = self.compile_output(input)?; @@ -139,7 +140,7 @@ impl Vyper { } /// Compiles with `--standard-json` and returns the raw `stdout` output. - #[instrument(name = "compile", level = "debug", skip_all)] + #[instrument(name = "Vyper::compile_raw", skip_all)] pub fn compile_output(&self, input: &T) -> Result> { let mut cmd = Command::new(&self.path); cmd.arg("--standard-json") @@ -171,7 +172,6 @@ impl Vyper { } /// Invokes `vyper --version` and parses the output as a SemVer [`Version`]. - #[instrument(level = "debug", skip_all)] pub fn version(vyper: impl Into) -> Result { crate::cache_version(vyper.into(), &[], |vyper| { let mut cmd = Command::new(vyper); diff --git a/crates/compilers/src/compilers/vyper/parser.rs b/crates/compilers/src/compilers/vyper/parser.rs index 2037b01..2f524ff 100644 --- a/crates/compilers/src/compilers/vyper/parser.rs +++ b/crates/compilers/src/compilers/vyper/parser.rs @@ -36,6 +36,7 @@ pub struct VyperParsedSource { impl ParsedSource for VyperParsedSource { type Language = VyperLanguage; + #[instrument(name = "VyperParsedSource::parse", skip_all)] fn parse(content: &str, file: &Path) -> Result { let version_req = capture_outer_and_inner(content, &RE_VYPER_VERSION, &["version"]) .first() diff --git a/crates/compilers/src/resolver/mod.rs b/crates/compilers/src/resolver/mod.rs index d91592e..339773a 100644 --- a/crates/compilers/src/resolver/mod.rs +++ b/crates/compilers/src/resolver/mod.rs @@ -332,6 +332,7 @@ impl> Graph { } /// Resolves a number of sources within the given config + #[instrument(name = "Graph::resolve_sources", skip_all)] pub fn resolve_sources( paths: &ProjectPathsConfig, sources: Sources, diff --git a/crates/compilers/src/resolver/parse.rs b/crates/compilers/src/resolver/parse.rs index 06c21f9..8126c85 100644 --- a/crates/compilers/src/resolver/parse.rs +++ b/crates/compilers/src/resolver/parse.rs @@ -42,6 +42,7 @@ impl SolData { /// /// This will attempt to parse the solidity AST and extract the imports and version pragma. If /// parsing fails, we'll fall back to extract that info via regex + #[instrument(name = "SolData::parse", skip_all)] pub fn parse(content: &str, file: &Path) -> Self { let is_yul = file.extension().is_some_and(|ext| ext == "yul"); let mut version = None; From 8552ca99690eb4f1bf5f1851a90f96b00598b8ad Mon Sep 17 00:00:00 2001 From: grandizzy <38490174+grandizzy@users.noreply.github.com> Date: Thu, 31 Jul 2025 11:40:03 +0300 Subject: [PATCH 35/57] fix: consistent handle of unresolved imports (#294) --- crates/compilers/src/resolver/mod.rs | 33 ++++++---------------------- 1 file changed, 7 insertions(+), 26 deletions(-) diff --git a/crates/compilers/src/resolver/mod.rs b/crates/compilers/src/resolver/mod.rs index 339773a..52157d5 100644 --- a/crates/compilers/src/resolver/mod.rs +++ b/crates/compilers/src/resolver/mod.rs @@ -401,38 +401,19 @@ impl> Graph { }; for import_path in node.data.resolve_imports(paths, &mut resolved_solc_include_paths)? { - match paths.resolve_import_and_include_paths( + if let Some(err) = match paths.resolve_import_and_include_paths( cwd, &import_path, &mut resolved_solc_include_paths, ) { Ok(import) => { - add_node(&mut unresolved, &mut index, &mut resolved_imports, import) - .map_err(|err| { - match err { - SolcError::ResolveCaseSensitiveFileName { .. } - | SolcError::Resolve(_) => { - // make the error more helpful by providing additional - // context - SolcError::FailedResolveImport( - Box::new(err), - node.path.clone(), - import_path.clone(), - ) - } - _ => err, - } - })? + add_node(&mut unresolved, &mut index, &mut resolved_imports, import).err() } - Err(err) => { - unresolved_imports.insert((import_path.to_path_buf(), node.path.clone())); - trace!( - "failed to resolve import component \"{:?}\" for {:?}", - err, - node.path - ) - } - }; + Err(err) => Some(err), + } { + unresolved_imports.insert((import_path.to_path_buf(), node.path.clone())); + trace!("failed to resolve import component \"{:?}\" for {:?}", err, node.path) + } } nodes.push(node); From 910af38eca7dfa455b2bb412d9f70d22c0ae974e Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Thu, 31 Jul 2025 11:46:33 +0200 Subject: [PATCH 36/57] chore: release 0.18.1 --- CHANGELOG.md | 15 +++++++++++++++ Cargo.toml | 12 ++++++------ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4943e33..35483cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.18.1](https://github.com/foundry-rs/compilers/releases/tag/v0.18.1) - 2025-07-31 + +### Bug Fixes + +- Consistent handle of unresolved imports ([#294](https://github.com/foundry-rs/compilers/issues/294)) + +### Miscellaneous Tasks + +- Add more instrumentation ([#293](https://github.com/foundry-rs/compilers/issues/293)) + +### Other + +- Remove duplicate assembly check in is_dirty ([#292](https://github.com/foundry-rs/compilers/issues/292)) + ## [0.18.0](https://github.com/foundry-rs/compilers/releases/tag/v0.18.0) - 2025-07-14 ### Dependencies @@ -15,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Miscellaneous Tasks +- Release 0.18.0 - Release 0.18.0 ## [0.17.4](https://github.com/foundry-rs/compilers/releases/tag/v0.17.4) - 2025-06-30 diff --git a/Cargo.toml b/Cargo.toml index 5ef69a3..52b800d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ resolver = "2" [workspace.package] authors = ["Foundry Maintainers"] -version = "0.18.0" +version = "0.18.1" rust-version = "1.88" readme = "README.md" license = "MIT OR Apache-2.0" @@ -36,11 +36,11 @@ redundant-lifetimes = "warn" all = "warn" [workspace.dependencies] -foundry-compilers = { path = "crates/compilers", version = "0.18.0" } -foundry-compilers-artifacts = { path = "crates/artifacts/artifacts", version = "0.18.0" } -foundry-compilers-artifacts-solc = { path = "crates/artifacts/solc", version = "0.18.0" } -foundry-compilers-artifacts-vyper = { path = "crates/artifacts/vyper", version = "0.18.0" } -foundry-compilers-core = { path = "crates/core", version = "0.18.0" } +foundry-compilers = { path = "crates/compilers", version = "0.18.1" } +foundry-compilers-artifacts = { path = "crates/artifacts/artifacts", version = "0.18.1" } +foundry-compilers-artifacts-solc = { path = "crates/artifacts/solc", version = "0.18.1" } +foundry-compilers-artifacts-vyper = { path = "crates/artifacts/vyper", version = "0.18.1" } +foundry-compilers-core = { path = "crates/core", version = "0.18.1" } alloy-json-abi = { version = "1.2", features = ["serde_json"] } alloy-primitives = { version = "1.2", features = ["serde", "rand"] } From f6e4e6a441f6ff7d45c8f0ddcf8b786252433c41 Mon Sep 17 00:00:00 2001 From: grandizzy <38490174+grandizzy@users.noreply.github.com> Date: Fri, 1 Aug 2025 19:26:35 +0300 Subject: [PATCH 37/57] fix: allow single sol file remappings (#295) ref https://github.com/foundry-rs/foundry/issues/6706 tested and confirm solves https://github.com/foundry-rs/foundry/issues/8499 too - do not add trailing `/` to remappings if single .sol file remapped - https://github.com/foundry-rs/foundry/issues/6706#issuecomment-3141270852 - if `stripped_import` is empty then do not join when resolving library import as will append `/` - https://github.com/foundry-rs/foundry/issues/6706#issuecomment-1879229300 - fix display so `forge remappings` properly display remappings `@ens/utils/BytesUtils.sol=src/BytesUtils.sol` --- crates/artifacts/solc/src/remappings/mod.rs | 41 +++++++++++++++++--- crates/compilers/src/config.rs | 42 ++++++++++++++++++++- 2 files changed, 76 insertions(+), 7 deletions(-) diff --git a/crates/artifacts/solc/src/remappings/mod.rs b/crates/artifacts/solc/src/remappings/mod.rs index 5899d9b..78fb5f8 100644 --- a/crates/artifacts/solc/src/remappings/mod.rs +++ b/crates/artifacts/solc/src/remappings/mod.rs @@ -138,8 +138,11 @@ impl fmt::Display for Remapping { } s.push(':'); } - let name = - if !self.name.ends_with('/') { format!("{}/", self.name) } else { self.name.clone() }; + let name = if needs_trailing_slash(&self.name) { + format!("{}/", self.name) + } else { + self.name.clone() + }; s.push_str(&{ #[cfg(target_os = "windows")] { @@ -153,7 +156,7 @@ impl fmt::Display for Remapping { } }); - if !s.ends_with('/') { + if needs_trailing_slash(&s) { s.push('/'); } f.write_str(&s) @@ -241,7 +244,7 @@ impl fmt::Display for RelativeRemapping { } }); - if !s.ends_with('/') { + if needs_trailing_slash(&s) { s.push('/'); } f.write_str(&s) @@ -252,10 +255,10 @@ impl From for Remapping { fn from(r: RelativeRemapping) -> Self { let RelativeRemapping { context, mut name, path } = r; let mut path = path.relative().display().to_string(); - if !path.ends_with('/') { + if needs_trailing_slash(&path) { path.push('/'); } - if !name.ends_with('/') { + if needs_trailing_slash(&name) { name.push('/'); } Self { context, name, path } @@ -341,6 +344,15 @@ impl<'de> Deserialize<'de> for RelativeRemapping { } } +/// Helper to determine if name or path of a remapping needs trailing slash. +/// Returns false if it already ends with a slash or if remapping is a solidity file. +/// Used to preserve name and path of single file remapping, see +/// +/// +fn needs_trailing_slash(name_or_path: &str) -> bool { + !name_or_path.ends_with('/') && !name_or_path.ends_with(".sol") +} + #[cfg(test)] mod tests { pub use super::*; @@ -423,4 +435,21 @@ mod tests { ); assert_eq!(remapping.to_string(), "oz/=a/b/c/d/".to_string()); } + + // + #[test] + fn can_preserve_single_sol_file_remapping() { + let remapping = "@my-lib/B.sol=lib/my-lib/B.sol"; + let remapping = Remapping::from_str(remapping).unwrap(); + + assert_eq!( + remapping, + Remapping { + context: None, + name: "@my-lib/B.sol".to_string(), + path: "lib/my-lib/B.sol".to_string() + } + ); + assert_eq!(remapping.to_string(), "@my-lib/B.sol=lib/my-lib/B.sol".to_string()); + } } diff --git a/crates/compilers/src/config.rs b/crates/compilers/src/config.rs index 356d80b..45dfd8d 100644 --- a/crates/compilers/src/config.rs +++ b/crates/compilers/src/config.rs @@ -524,7 +524,12 @@ impl ProjectPathsConfig { }) .find_map(|r| { import.strip_prefix(&r.name).ok().map(|stripped_import| { - let lib_path = Path::new(&r.path).join(stripped_import); + let lib_path = + if stripped_import.as_os_str().is_empty() && r.path.ends_with(".sol") { + r.path.clone().into() + } else { + Path::new(&r.path).join(stripped_import) + }; // we handle the edge case where the path of a remapping ends with "contracts" // (`/=.../contracts`) and the stripped import also starts with @@ -1196,4 +1201,39 @@ mod tests { dependency.join("A.sol") ); } + + #[test] + fn can_resolve_single_file_mapped_import() { + let dir = tempfile::tempdir().unwrap(); + let mut config = ProjectPathsConfig::builder().root(dir.path()).build::<()>().unwrap(); + config.create_all().unwrap(); + + fs::write( + config.sources.join("A.sol"), + r#"pragma solidity ^0.8.0; import "@my-lib/B.sol"; contract A is B {}"#, + ) + .unwrap(); + + let dependency = config.root.join("my-lib"); + fs::create_dir(&dependency).unwrap(); + fs::write(dependency.join("B.sol"), r"pragma solidity ^0.8.0; contract B {}").unwrap(); + + config.remappings.push(Remapping { + context: None, + name: "@my-lib/B.sol".into(), + path: "my-lib/B.sol".into(), + }); + + // Test that single file import / remapping resolves to file. + assert!(config + .resolve_import_and_include_paths( + &config.sources, + Path::new("@my-lib/B.sol"), + &mut Default::default(), + ) + .unwrap() + .to_str() + .unwrap() + .ends_with("my-lib/B.sol")); + } } From fea243084391c6975e01077af332106bbac354dd Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Fri, 1 Aug 2025 18:51:17 +0200 Subject: [PATCH 38/57] chore: release 0.18.2 --- CHANGELOG.md | 7 +++++++ Cargo.toml | 12 ++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35483cf..cb53799 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.18.2](https://github.com/foundry-rs/compilers/releases/tag/v0.18.2) - 2025-08-01 + +### Bug Fixes + +- Allow single sol file remappings ([#295](https://github.com/foundry-rs/compilers/issues/295)) + ## [0.18.1](https://github.com/foundry-rs/compilers/releases/tag/v0.18.1) - 2025-07-31 ### Bug Fixes @@ -13,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Miscellaneous Tasks +- Release 0.18.1 - Add more instrumentation ([#293](https://github.com/foundry-rs/compilers/issues/293)) ### Other diff --git a/Cargo.toml b/Cargo.toml index 52b800d..4e2cb8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ resolver = "2" [workspace.package] authors = ["Foundry Maintainers"] -version = "0.18.1" +version = "0.18.2" rust-version = "1.88" readme = "README.md" license = "MIT OR Apache-2.0" @@ -36,11 +36,11 @@ redundant-lifetimes = "warn" all = "warn" [workspace.dependencies] -foundry-compilers = { path = "crates/compilers", version = "0.18.1" } -foundry-compilers-artifacts = { path = "crates/artifacts/artifacts", version = "0.18.1" } -foundry-compilers-artifacts-solc = { path = "crates/artifacts/solc", version = "0.18.1" } -foundry-compilers-artifacts-vyper = { path = "crates/artifacts/vyper", version = "0.18.1" } -foundry-compilers-core = { path = "crates/core", version = "0.18.1" } +foundry-compilers = { path = "crates/compilers", version = "0.18.2" } +foundry-compilers-artifacts = { path = "crates/artifacts/artifacts", version = "0.18.2" } +foundry-compilers-artifacts-solc = { path = "crates/artifacts/solc", version = "0.18.2" } +foundry-compilers-artifacts-vyper = { path = "crates/artifacts/vyper", version = "0.18.2" } +foundry-compilers-core = { path = "crates/core", version = "0.18.2" } alloy-json-abi = { version = "1.2", features = ["serde_json"] } alloy-primitives = { version = "1.2", features = ["serde", "rand"] } From 076c333fbbf0e3654a1341cfff2c68cdb612d998 Mon Sep 17 00:00:00 2001 From: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Date: Tue, 19 Aug 2025 03:32:52 -0700 Subject: [PATCH 39/57] chore: update `CODEOWNERS` to improve visibility (#298) --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9f19e67..b30255c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @danipopes @klkvr @mattsse +* @danipopes @klkvr @mattsse @grandizzy @yash-atreya @zerosnacks @onbjerg From ab573a0e80043eee4f9a2f6fbdee7db93802c7e2 Mon Sep 17 00:00:00 2001 From: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Date: Tue, 19 Aug 2025 03:45:36 -0700 Subject: [PATCH 40/57] chore: add @0xrusowsky to `CODEOWNERS` (#299) --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b30255c..aefd74a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @danipopes @klkvr @mattsse @grandizzy @yash-atreya @zerosnacks @onbjerg +* @danipopes @klkvr @mattsse @grandizzy @yash-atreya @zerosnacks @onbjerg @0xrusowsky From 5af6f8ae71484e44c23712bae1021304923941d9 Mon Sep 17 00:00:00 2001 From: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Date: Tue, 19 Aug 2025 04:50:51 -0700 Subject: [PATCH 41/57] chore: update deps + fix clippy (#297) Apply cargo update / upgrade + clippy / breaking change fixes --- Cargo.toml | 8 +-- benches/read_all.rs | 4 +- crates/compilers/Cargo.toml | 6 +-- crates/compilers/src/project_util/mock.rs | 65 ++++++++++++----------- 4 files changed, 43 insertions(+), 40 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4e2cb8d..e0569f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,13 +42,13 @@ foundry-compilers-artifacts-solc = { path = "crates/artifacts/solc", version = " foundry-compilers-artifacts-vyper = { path = "crates/artifacts/vyper", version = "0.18.2" } foundry-compilers-core = { path = "crates/core", version = "0.18.2" } -alloy-json-abi = { version = "1.2", features = ["serde_json"] } -alloy-primitives = { version = "1.2", features = ["serde", "rand"] } +alloy-json-abi = { version = "1.3", features = ["serde_json"] } +alloy-primitives = { version = "1.3", features = ["serde", "rand"] } cfg-if = "1.0" dunce = "1.0" memmap2 = "0.9" path-slash = "0.2" -rayon = "1.10" +rayon = "1.11" regex = "1.11" semver = { version = "1.0", features = ["serde"] } serde = { version = "1", features = ["derive", "rc"] } @@ -65,7 +65,7 @@ yansi = "1.0" # async futures-util = "0.3" -tokio = { version = "1.46", features = ["rt-multi-thread"] } +tokio = { version = "1.47", features = ["rt-multi-thread"] } snapbox = "0.6.21" diff --git a/benches/read_all.rs b/benches/read_all.rs index cdc448f..6281630 100644 --- a/benches/read_all.rs +++ b/benches/read_all.rs @@ -53,10 +53,10 @@ fn prepare_contracts(root: &Path, num: usize) -> Vec { let f = File::create(&path).unwrap(); let mut writer = BufWriter::new(f); - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); // let's assume a solidity file is between 2kb and 16kb - let n: usize = rng.gen_range(4..17); + let n: usize = rng.random_range(4..17); let s: String = rng.sample_iter(&Alphanumeric).take(n * 1024).map(char::from).collect(); writer.write_all(s.as_bytes()).unwrap(); writer.flush().unwrap(); diff --git a/crates/compilers/Cargo.toml b/crates/compilers/Cargo.toml index 3f7e121..cfd0e3e 100644 --- a/crates/compilers/Cargo.toml +++ b/crates/compilers/Cargo.toml @@ -39,7 +39,7 @@ tokio = { workspace = true, optional = true } auto_impl = "1" winnow = "0.7" dyn-clone = "1" -derive_more = { version = "1", features = ["debug"] } +derive_more = { version = "2", features = ["debug"] } home = "0.5" dirs = "6.0" itertools = ">=0.13, <=0.14" @@ -47,7 +47,7 @@ itertools = ">=0.13, <=0.14" # project-util tempfile = { version = "3.20", optional = true } fs_extra = { version = "1.3", optional = true } -rand = { version = "0.8", optional = true } +rand = { version = "0.9", optional = true } # svm svm = { workspace = true, optional = true } @@ -61,7 +61,7 @@ tracing-subscriber = { version = "0.3", default-features = false, features = [ ] } similar-asserts.workspace = true fd-lock = "4.0.4" -tokio = { version = "1.46", features = ["rt-multi-thread", "macros"] } +tokio = { version = "1.47", features = ["rt-multi-thread", "macros"] } reqwest = "0.12" tempfile = "3.20" snapbox.workspace = true diff --git a/crates/compilers/src/project_util/mock.rs b/crates/compilers/src/project_util/mock.rs index 4d47f91..0e9b843 100644 --- a/crates/compilers/src/project_util/mock.rs +++ b/crates/compilers/src/project_util/mock.rs @@ -2,11 +2,7 @@ use foundry_compilers_artifacts::Remapping; use foundry_compilers_core::error::{Result, SolcError}; -use rand::{ - distributions::{Distribution, Uniform}, - seq::SliceRandom, - Rng, -}; +use rand::{seq::SliceRandom, Rng}; use serde::{Deserialize, Serialize}; use std::{ collections::{BTreeSet, HashMap, HashSet, VecDeque}, @@ -70,11 +66,11 @@ impl MockProjectGenerator { } let graph = Graph::::resolve(paths)?; - let mut gen = Self::default(); + let mut generated = Self::default(); let (_, edges) = graph.into_sources(); // add all files as source files - gen.add_sources(edges.files().count()); + generated.add_sources(edges.files().count()); // stores libs and their files let libs = get_libs( @@ -85,25 +81,25 @@ impl MockProjectGenerator { // mark all files as libs for (lib_id, lib_files) in libs.into_values().enumerate() { - let lib_name = gen.name_strategy.new_lib_name(lib_id); - let offset = gen.inner.files.len(); + let lib_name = generated.name_strategy.new_lib_name(lib_id); + let offset = generated.inner.files.len(); let lib = MockLib { name: lib_name, id: lib_id, num_files: lib_files.len(), offset }; for lib_file in lib_files { - let file = &mut gen.inner.files[lib_file]; + let file = &mut generated.inner.files[lib_file]; file.lib_id = Some(lib_id); - file.name = gen.name_strategy.new_lib_name(file.id); + file.name = generated.name_strategy.new_lib_name(file.id); } - gen.inner.libraries.push(lib); + generated.inner.libraries.push(lib); } for id in edges.files() { for import in edges.imported_nodes(id).iter().copied() { - let import = gen.get_import(import); - gen.inner.files[id].imports.insert(import); + let import = generated.get_import(import); + generated.inner.files[id].imports.insert(import); } } - Ok(gen) + Ok(generated) } /// Consumes the type and returns the underlying skeleton @@ -243,34 +239,36 @@ impl MockProjectGenerator { self } - /// randomly assign empty file status so that mocked files don't emit artifacts + /// Randomly assign empty file status so that mocked files don't emit artifacts. pub fn assign_empty_files(&mut self) -> &mut Self { - let mut rng = rand::thread_rng(); - let die = Uniform::from(0..self.inner.files.len()); + let mut rng = rand::rng(); + let n = self.inner.files.len(); + for file in self.inner.files.iter_mut() { - let throw = die.sample(&mut rng); + let throw = rng.random_range(0..n); if throw == 0 { - // give it a 1 in num(files) chance that the file will be empty + // 1 in n chance that the file will be empty file.emit_artifacts = false; } } + self } /// Populates the imports of the project pub fn populate_imports(&mut self, settings: &MockProjectSettings) -> &mut Self { - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); // populate imports for id in 0..self.inner.files.len() { let imports = if let Some(lib) = self.inner.files[id].lib_id { let num_imports = rng - .gen_range(settings.min_imports..=settings.max_imports) + .random_range(settings.min_imports..=settings.max_imports) .min(self.inner.libraries[lib].num_files.saturating_sub(1)); self.unique_imports_for_lib(&mut rng, lib, id, num_imports) } else { let num_imports = rng - .gen_range(settings.min_imports..=settings.max_imports) + .random_range(settings.min_imports..=settings.max_imports) .min(self.inner.files.len().saturating_sub(1)); self.unique_imports_for_source(&mut rng, id, num_imports) }; @@ -437,11 +435,16 @@ impl MockFile { pub fn target_path( &self, - gen: &MockProjectGenerator, + generated: &MockProjectGenerator, paths: &ProjectPathsConfig, ) -> PathBuf { let mut target = if let Some(lib) = self.lib_id { - paths.root.join("lib").join(&gen.inner.libraries[lib].name).join("src").join(&self.name) + paths + .root + .join("lib") + .join(&generated.inner.libraries[lib].name) + .join("src") + .join(&self.name) } else { paths.sources.join(&self.name) }; @@ -552,14 +555,14 @@ pub struct MockProjectSettings { impl MockProjectSettings { /// Generates a new instance with random settings within an arbitrary range pub fn random() -> Self { - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); // arbitrary thresholds Self { - num_sources: rng.gen_range(2..25), - num_libs: rng.gen_range(0..5), - num_lib_files: rng.gen_range(1..10), - min_imports: rng.gen_range(0..3), - max_imports: rng.gen_range(4..10), + num_sources: rng.random_range(2..25), + num_libs: rng.random_range(0..5), + num_lib_files: rng.random_range(1..10), + min_imports: rng.random_range(0..3), + max_imports: rng.random_range(4..10), allow_no_artifacts_files: true, } } From a30af456a9df36e617f7083bb3d3d25215dd256f Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 25 Aug 2025 10:35:15 +0200 Subject: [PATCH 42/57] chore: use svm instead of manual svm dir logic (#301) I don't really know why these re-implement svm functions Closes https://github.com/foundry-rs/compilers/pull/296 --- .github/workflows/ci.yml | 12 +- crates/compilers/Cargo.toml | 2 - .../compilers/src/compilers/solc/compiler.rs | 107 +++++++++--------- 3 files changed, 61 insertions(+), 60 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a4e68c..6dd698b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: - rust: "1.88" # MSRV flags: "--all-features" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.rust }} @@ -49,7 +49,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 with: @@ -60,7 +60,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable - uses: taiki-e/install-action@cargo-hack - uses: Swatinem/rust-cache@v2 @@ -73,7 +73,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable with: components: clippy @@ -88,7 +88,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@nightly - uses: Swatinem/rust-cache@v2 with: @@ -101,7 +101,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@nightly with: components: rustfmt diff --git a/crates/compilers/Cargo.toml b/crates/compilers/Cargo.toml index cfd0e3e..dca0c15 100644 --- a/crates/compilers/Cargo.toml +++ b/crates/compilers/Cargo.toml @@ -40,8 +40,6 @@ auto_impl = "1" winnow = "0.7" dyn-clone = "1" derive_more = { version = "2", features = ["debug"] } -home = "0.5" -dirs = "6.0" itertools = ">=0.13, <=0.14" # project-util diff --git a/crates/compilers/src/compilers/solc/compiler.rs b/crates/compilers/src/compilers/solc/compiler.rs index 118aa1f..ca1ab71 100644 --- a/crates/compilers/src/compilers/solc/compiler.rs +++ b/crates/compilers/src/compilers/solc/compiler.rs @@ -2,7 +2,7 @@ use crate::resolver::parse::SolData; use foundry_compilers_artifacts::{sources::Source, CompilerOutput, SolcInput}; use foundry_compilers_core::{ error::{Result, SolcError}, - utils::{self, SUPPORTS_BASE_PATH, SUPPORTS_INCLUDE_PATH}, + utils::{SUPPORTS_BASE_PATH, SUPPORTS_INCLUDE_PATH}, }; use itertools::Itertools; use semver::{Version, VersionReq}; @@ -91,9 +91,7 @@ impl Solc { /// Returns error if `solc` is not found in the system or if the version cannot be retrieved. #[instrument(name = "Solc::new", skip_all)] pub fn new(path: impl Into) -> Result { - let path = path.into(); - let version = Self::version(path.clone())?; - Ok(Self::new_with_version(path, version)) + Self::new_with_args(path, Vec::::new()) } /// A new instance which points to `solc` with additional cli arguments. Invokes `solc @@ -104,25 +102,39 @@ impl Solc { path: impl Into, extra_args: impl IntoIterator>, ) -> Result { - let args = extra_args.into_iter().map(Into::into).collect::>(); let path = path.into(); - let version = Self::version_with_args(path.clone(), &args)?; - - let mut solc = Self::new_with_version(path, version); - solc.extra_args = args; - - Ok(solc) + let extra_args = extra_args.into_iter().map(Into::into).collect::>(); + let version = Self::version_with_args(path.clone(), &extra_args)?; + Ok(Self::_new(path, version, extra_args)) } /// A new instance which points to `solc` with the given version pub fn new_with_version(path: impl Into, version: Version) -> Self { - Self { - solc: path.into(), + Self::_new(path.into(), version, Default::default()) + } + + fn _new(path: PathBuf, version: Version, extra_args: Vec) -> Self { + let this = Self { + solc: path, version, base_path: None, allow_paths: Default::default(), include_paths: Default::default(), - extra_args: Default::default(), + extra_args, + }; + this.debug_assert(); + this + } + + fn debug_assert(&self) { + if !cfg!(debug_assertions) { + return; + } + assert_eq!(self.version_short(), self.version); + if let Ok(v) = Self::version_with_args(&self.solc, &self.extra_args) { + assert_eq!(v.major, self.version.major); + assert_eq!(v.minor, self.version.minor); + assert_eq!(v.patch, self.version.patch); } } @@ -202,17 +214,14 @@ impl Solc { /// Ok::<_, Box>(()) /// ``` #[instrument(skip_all)] + #[cfg(feature = "svm-solc")] pub fn find_svm_installed_version(version: &Version) -> Result> { - let version = format!("{}.{}.{}", version.major, version.minor, version.patch); - let solc = Self::svm_home() - .ok_or_else(|| SolcError::msg("svm home dir not found"))? - .join(&version) - .join(format!("solc-{version}")); - + let version = Version::new(version.major, version.minor, version.patch); + let solc = svm::version_binary(&version.to_string()); if !solc.is_file() { return Ok(None); } - Self::new(&solc).map(Some) + Ok(Some(Self::new_with_version(&solc, version))) } /// Returns the directory in which [svm](https://github.com/roynalnaruto/svm-rs) stores all versions @@ -220,14 +229,9 @@ impl Solc { /// This will be: /// - `~/.svm` on unix, if it exists /// - $XDG_DATA_HOME (~/.local/share/svm) if the svm folder does not exist. + #[cfg(feature = "svm-solc")] pub fn svm_home() -> Option { - if let Some(home_dir) = home::home_dir() { - let home_dot_svm = home_dir.join(".svm"); - if home_dot_svm.exists() { - return Some(home_dot_svm); - } - } - dirs::data_dir().map(|dir| dir.join("svm")) + Some(svm::data_dir().to_path_buf()) } /// Returns the `semver::Version` [svm](https://github.com/roynalnaruto/svm-rs)'s `.global_version` is currently set to. @@ -235,23 +239,21 @@ impl Solc { /// /// This will read the version string (eg: "0.8.9") that the `~/.svm/.global_version` file /// contains + #[cfg(feature = "svm-solc")] pub fn svm_global_version() -> Option { - let home = Self::svm_home()?; - let version = std::fs::read_to_string(home.join(".global_version")).ok()?; - Version::parse(&version).ok() + svm::get_global_version().ok().flatten() } /// Returns the list of all solc instances installed at `SVM_HOME` + #[cfg(feature = "svm-solc")] pub fn installed_versions() -> Vec { - Self::svm_home() - .map(|home| utils::installed_versions(&home).unwrap_or_default()) - .unwrap_or_default() + svm::installed_versions().unwrap_or_default() } /// Returns the list of all versions that are available to download #[cfg(feature = "svm-solc")] pub fn released_versions() -> Vec { - RELEASES.1.clone().into_iter().collect() + RELEASES.1.clone() } /// Installs the provided version of Solc in the machine under the svm dir and returns the @@ -446,8 +448,7 @@ impl Solc { compile_output(output) } - /// Invokes `solc --version` and parses the output as a SemVer [`Version`], stripping the - /// pre-release and build metadata. + /// Returns the SemVer [`Version`], stripping the pre-release and build metadata. pub fn version_short(&self) -> Version { Version::new(self.version.major, self.version.minor, self.version.patch) } @@ -459,20 +460,22 @@ impl Solc { /// Invokes `solc --version` and parses the output as a SemVer [`Version`]. pub fn version_with_args(solc: impl Into, args: &[String]) -> Result { - crate::cache_version(solc.into(), args, |solc| { - let mut cmd = Command::new(solc); - cmd.args(args) - .arg("--version") - .stdin(Stdio::piped()) - .stderr(Stdio::piped()) - .stdout(Stdio::piped()); - debug!(?cmd, "getting Solc version"); - let output = cmd.output().map_err(|e| SolcError::io(e, solc))?; - trace!(?output); - let version = version_from_output(output)?; - debug!(%version); - Ok(version) - }) + crate::cache_version(solc.into(), args, |solc| Self::version_impl(solc, args)) + } + + fn version_impl(solc: &Path, args: &[String]) -> Result { + let mut cmd = Command::new(solc); + cmd.args(args) + .arg("--version") + .stdin(Stdio::piped()) + .stderr(Stdio::piped()) + .stdout(Stdio::piped()); + debug!(?cmd, "getting Solc version"); + let output = cmd.output().map_err(|e| SolcError::io(e, solc))?; + trace!(?output); + let version = version_from_output(output)?; + debug!(%version); + Ok(version) } fn map_io_err(&self) -> impl FnOnce(std::io::Error) -> SolcError + '_ { @@ -759,7 +762,7 @@ mod tests { // This test does not take the lock by default, so we need to manually add it here. take_solc_installer_lock!(_lock); let version = Version::new(0, 8, 6); - if utils::installed_versions(svm::data_dir()) + if svm::installed_versions() .map(|versions| !versions.contains(&version)) .unwrap_or_default() { From a4cec2c218f0db54d6ade111724d5fc613a717c8 Mon Sep 17 00:00:00 2001 From: grandizzy <38490174+grandizzy@users.noreply.github.com> Date: Mon, 25 Aug 2025 21:31:16 +0300 Subject: [PATCH 43/57] fix(flatten): sort by loc path and loc start (#302) ref https://github.com/foundry-rs/foundry/issues/11417 for ```Solidity // SPDX-License-Identifier: MIT pragma solidity >=0.8.19; ... function convert(UD60x18 x) pure returns (uint256 result) { ... } function convert(uint256 x) pure returns (UD60x18 result) { ... } ``` we have items as ``` [ (8378, ItemLocation { path: "lib/prb-math/src/ud60x18/Conversions.sol", start: 462, end: 469 }), (8409, ItemLocation { path: "lib/prb-math/src/ud60x18/Conversions.sol", start: 833, end: 840 }) ] ``` but we sort only by path, hence order could be reversed. Sort also by loc.start to make sure order is preserved --- crates/compilers/src/flatten.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/compilers/src/flatten.rs b/crates/compilers/src/flatten.rs index de629cc..3d5860c 100644 --- a/crates/compilers/src/flatten.rs +++ b/crates/compilers/src/flatten.rs @@ -317,10 +317,10 @@ impl Flattener { // `loc.path` is expected to be different for each id because there can't be 2 // top-level declarations with the same name in the same file. // - // Sorting by index loc.path in sorted files to make the renaming process - // deterministic. + // Sorting by index loc.path and loc.start in sorted files to make the renaming + // process deterministic. ids.sort_by_key(|(_, loc)| { - self.ordered_sources.iter().position(|p| p == &loc.path).unwrap() + (self.ordered_sources.iter().position(|p| p == &loc.path).unwrap(), loc.start) }); } for (i, (id, loc)) in ids.iter().enumerate() { From 87ad8eba36d36a71dac1eada0e8bbb042f87dc08 Mon Sep 17 00:00:00 2001 From: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Date: Mon, 25 Aug 2025 12:02:54 -0700 Subject: [PATCH 44/57] chore(deps): bump to 0.18.3 (#303) pushing to main directly is disabled --- CHANGELOG.md | 22 ++++++++++++++++++++++ Cargo.toml | 12 ++++++------ README.md | 2 +- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb53799..113b6e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,34 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.18.3](https://github.com/foundry-rs/compilers/releases/tag/v0.18.3) - 2025-08-25 + +### Bug Fixes + +- [flatten] Sort by loc path and loc start ([#302](https://github.com/foundry-rs/compilers/issues/302)) + +### Dependencies + +- Bump +- Update deps + fix clippy ([#297](https://github.com/foundry-rs/compilers/issues/297)) + +### Miscellaneous Tasks + +- Release 0.18.3 +- Use svm instead of manual svm dir logic ([#301](https://github.com/foundry-rs/compilers/issues/301)) +- Add @0xrusowsky to `CODEOWNERS` ([#299](https://github.com/foundry-rs/compilers/issues/299)) +- Update `CODEOWNERS` to improve visibility ([#298](https://github.com/foundry-rs/compilers/issues/298)) + ## [0.18.2](https://github.com/foundry-rs/compilers/releases/tag/v0.18.2) - 2025-08-01 ### Bug Fixes - Allow single sol file remappings ([#295](https://github.com/foundry-rs/compilers/issues/295)) +### Miscellaneous Tasks + +- Release 0.18.2 + ## [0.18.1](https://github.com/foundry-rs/compilers/releases/tag/v0.18.1) - 2025-07-31 ### Bug Fixes diff --git a/Cargo.toml b/Cargo.toml index e0569f7..5ca69f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ resolver = "2" [workspace.package] authors = ["Foundry Maintainers"] -version = "0.18.2" +version = "0.18.3" rust-version = "1.88" readme = "README.md" license = "MIT OR Apache-2.0" @@ -36,11 +36,11 @@ redundant-lifetimes = "warn" all = "warn" [workspace.dependencies] -foundry-compilers = { path = "crates/compilers", version = "0.18.2" } -foundry-compilers-artifacts = { path = "crates/artifacts/artifacts", version = "0.18.2" } -foundry-compilers-artifacts-solc = { path = "crates/artifacts/solc", version = "0.18.2" } -foundry-compilers-artifacts-vyper = { path = "crates/artifacts/vyper", version = "0.18.2" } -foundry-compilers-core = { path = "crates/core", version = "0.18.2" } +foundry-compilers = { path = "crates/compilers", version = "0.18.3" } +foundry-compilers-artifacts = { path = "crates/artifacts/artifacts", version = "0.18.3" } +foundry-compilers-artifacts-solc = { path = "crates/artifacts/solc", version = "0.18.3" } +foundry-compilers-artifacts-vyper = { path = "crates/artifacts/vyper", version = "0.18.3" } +foundry-compilers-core = { path = "crates/core", version = "0.18.3" } alloy-json-abi = { version = "1.3", features = ["serde_json"] } alloy-primitives = { version = "1.3", features = ["serde", "rand"] } diff --git a/README.md b/README.md index 6f2cd24..ae7584b 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ To install, simply add `foundry-compilers` to your cargo dependencies. ```toml [dependencies] -foundry-compilers = "0.18.0" +foundry-compilers = "0.18.3" ``` Example usage: From 55bb2b27bf6f620c3d54079df30b2a50f5711a9f Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 25 Aug 2025 22:22:18 +0200 Subject: [PATCH 45/57] fix: remove superfluous assertion (#304) https://github.com/foundry-rs/foundry/pull/11421#pullrequestreview-3152774547 --- crates/compilers/src/compilers/solc/compiler.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/compilers/src/compilers/solc/compiler.rs b/crates/compilers/src/compilers/solc/compiler.rs index ca1ab71..8e67209 100644 --- a/crates/compilers/src/compilers/solc/compiler.rs +++ b/crates/compilers/src/compilers/solc/compiler.rs @@ -130,7 +130,6 @@ impl Solc { if !cfg!(debug_assertions) { return; } - assert_eq!(self.version_short(), self.version); if let Ok(v) = Self::version_with_args(&self.solc, &self.extra_args) { assert_eq!(v.major, self.version.major); assert_eq!(v.minor, self.version.minor); From c3ec28c136c7c7e150a15d29c8180003a430da6f Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 25 Aug 2025 22:58:44 +0200 Subject: [PATCH 46/57] chore: release 0.18.4 (#305) --- CHANGELOG.md | 6 +++--- Cargo.toml | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 113b6e8..9712f80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,20 +5,20 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.18.3](https://github.com/foundry-rs/compilers/releases/tag/v0.18.3) - 2025-08-25 +## [0.18.4](https://github.com/foundry-rs/compilers/releases/tag/v0.18.4) - 2025-08-25 ### Bug Fixes +- Remove superfluous assertion ([#304](https://github.com/foundry-rs/compilers/issues/304)) - [flatten] Sort by loc path and loc start ([#302](https://github.com/foundry-rs/compilers/issues/302)) ### Dependencies -- Bump +- [deps] Bump to 0.18.3 ([#303](https://github.com/foundry-rs/compilers/issues/303)) - Update deps + fix clippy ([#297](https://github.com/foundry-rs/compilers/issues/297)) ### Miscellaneous Tasks -- Release 0.18.3 - Use svm instead of manual svm dir logic ([#301](https://github.com/foundry-rs/compilers/issues/301)) - Add @0xrusowsky to `CODEOWNERS` ([#299](https://github.com/foundry-rs/compilers/issues/299)) - Update `CODEOWNERS` to improve visibility ([#298](https://github.com/foundry-rs/compilers/issues/298)) diff --git a/Cargo.toml b/Cargo.toml index 5ca69f6..1cced97 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ resolver = "2" [workspace.package] authors = ["Foundry Maintainers"] -version = "0.18.3" +version = "0.18.4" rust-version = "1.88" readme = "README.md" license = "MIT OR Apache-2.0" @@ -36,11 +36,11 @@ redundant-lifetimes = "warn" all = "warn" [workspace.dependencies] -foundry-compilers = { path = "crates/compilers", version = "0.18.3" } -foundry-compilers-artifacts = { path = "crates/artifacts/artifacts", version = "0.18.3" } -foundry-compilers-artifacts-solc = { path = "crates/artifacts/solc", version = "0.18.3" } -foundry-compilers-artifacts-vyper = { path = "crates/artifacts/vyper", version = "0.18.3" } -foundry-compilers-core = { path = "crates/core", version = "0.18.3" } +foundry-compilers = { path = "crates/compilers", version = "0.18.4" } +foundry-compilers-artifacts = { path = "crates/artifacts/artifacts", version = "0.18.4" } +foundry-compilers-artifacts-solc = { path = "crates/artifacts/solc", version = "0.18.4" } +foundry-compilers-artifacts-vyper = { path = "crates/artifacts/vyper", version = "0.18.4" } +foundry-compilers-core = { path = "crates/core", version = "0.18.4" } alloy-json-abi = { version = "1.3", features = ["serde_json"] } alloy-primitives = { version = "1.3", features = ["serde", "rand"] } From 4d5e52c0344ff8189a0ed0d7508a85dcafec59f3 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 25 Aug 2025 23:07:25 +0200 Subject: [PATCH 47/57] feat: add `SourceParser` (#300) Allows persisting a shared [`solar_sema::Compiler`](https://github.com/paradigmxyz/solar/pull/397) throughout compilation and storing it inside of the output. --------- Co-authored-by: onbjerg --- Cargo.toml | 6 +- crates/artifacts/solc/src/sources.rs | 26 +- crates/compilers/src/cache.rs | 35 +- crates/compilers/src/cache/iface.rs | 11 +- crates/compilers/src/compile/output/mod.rs | 20 +- crates/compilers/src/compile/project.rs | 5 +- crates/compilers/src/compilers/mod.rs | 35 +- crates/compilers/src/compilers/multi.rs | 128 ++++++- crates/compilers/src/compilers/solc/mod.rs | 102 +++++- crates/compilers/src/compilers/vyper/mod.rs | 7 +- .../compilers/src/compilers/vyper/parser.rs | 15 +- crates/compilers/src/config.rs | 40 +-- crates/compilers/src/filter.rs | 6 +- crates/compilers/src/flatten.rs | 18 +- crates/compilers/src/lib.rs | 19 +- crates/compilers/src/project_util/mock.rs | 13 +- crates/compilers/src/resolver/mod.rs | 233 +++++++----- crates/compilers/src/resolver/parse.rs | 336 ++++++++++++------ crates/compilers/src/resolver/tree.rs | 14 +- crates/compilers/tests/project.rs | 17 +- 20 files changed, 763 insertions(+), 323 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1cced97..ebc76d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,8 +54,8 @@ semver = { version = "1.0", features = ["serde"] } serde = { version = "1", features = ["derive", "rc"] } serde_json = "1.0" similar-asserts = "1" -solar-parse = { version = "=0.1.5", default-features = false } -solar-sema = { version = "=0.1.5", default-features = false } +solar-parse = { version = "=0.1.6", default-features = false } +solar-sema = { version = "=0.1.6", default-features = false } svm = { package = "svm-rs", version = "0.5", default-features = false } tempfile = "3.20" thiserror = "2" @@ -69,7 +69,7 @@ tokio = { version = "1.47", features = ["rt-multi-thread"] } snapbox = "0.6.21" -# [patch.crates-io] +[patch.crates-io] # solar-parse = { git = "https://github.com/paradigmxyz/solar", branch = "main" } # solar-sema = { git = "https://github.com/paradigmxyz/solar", branch = "main" } # solar-ast = { git = "https://github.com/paradigmxyz/solar", branch = "main" } diff --git a/crates/artifacts/solc/src/sources.rs b/crates/artifacts/solc/src/sources.rs index c101918..b8ac1c8 100644 --- a/crates/artifacts/solc/src/sources.rs +++ b/crates/artifacts/solc/src/sources.rs @@ -1,4 +1,4 @@ -use foundry_compilers_core::error::SolcIoError; +use foundry_compilers_core::error::{SolcError, SolcIoError}; use serde::{Deserialize, Serialize}; use std::{ collections::BTreeMap, @@ -137,6 +137,30 @@ impl Source { Ok(Self::new(content)) } + /// [`read`](Self::read) + mapping error to [`SolcError`]. + pub fn read_(file: &Path) -> Result { + Self::read(file).map_err(|err| { + let exists = err.path().exists(); + if !exists && err.path().is_symlink() { + return SolcError::ResolveBadSymlink(err); + } + + // This is an additional check useful on OS that have case-sensitive paths, + // see also + // check if there exists a file with different case + #[cfg(feature = "walkdir")] + if !exists { + if let Some(existing_file) = + foundry_compilers_core::utils::find_case_sensitive_existing_file(file) + { + return SolcError::ResolveCaseSensitiveFileName { error: err, existing_file }; + } + } + + SolcError::Resolve(err) + }) + } + /// Returns `true` if the source should be compiled with full output selection. pub fn is_dirty(&self) -> bool { self.kind.is_dirty() diff --git a/crates/compilers/src/cache.rs b/crates/compilers/src/cache.rs index 07313a3..90e50d8 100644 --- a/crates/compilers/src/cache.rs +++ b/crates/compilers/src/cache.rs @@ -6,7 +6,7 @@ use crate::{ output::Builds, resolver::GraphEdges, ArtifactFile, ArtifactOutput, Artifacts, ArtifactsMap, Graph, OutputContext, Project, - ProjectPaths, ProjectPathsConfig, SourceCompilationKind, + ProjectPaths, ProjectPathsConfig, SourceCompilationKind, SourceParser, }; use foundry_compilers_artifacts::{ sources::{Source, Sources}, @@ -658,7 +658,7 @@ pub(crate) struct ArtifactsCacheInner< pub cached_builds: Builds, /// Relationship between all the files. - pub edges: GraphEdges, + pub edges: GraphEdges, /// The project. pub project: &'a Project, @@ -723,6 +723,7 @@ impl, C: Compiler> /// Gets or calculates the interface representation hash for the given source file. fn interface_repr_hash(&mut self, source: &Source, file: &Path) -> &str { self.interface_repr_hashes.entry(file.to_path_buf()).or_insert_with(|| { + // TODO: use `interface_representation_ast` directly with `edges.parser()`. if let Some(r) = interface_repr_hash(&source.content, file) { return r; } @@ -823,10 +824,10 @@ impl, C: Compiler> // Walks over all cache entries, detects dirty files and removes them from cache. fn find_and_remove_dirty(&mut self) { - fn populate_dirty_files( + fn populate_dirty_files( file: &Path, dirty_files: &mut HashSet, - edges: &GraphEdges, + edges: &GraphEdges

, ) { for file in edges.importers(file) { // If file is marked as dirty we either have already visited it or it was marked as @@ -890,7 +891,7 @@ impl, C: Compiler> // Build a temporary graph for walking imports. We need this because `self.edges` // only contains graph data for in-scope sources but we are operating on cache entries. - if let Ok(graph) = Graph::::resolve_sources(&self.project.paths, sources) { + if let Ok(graph) = Graph::::resolve_sources(&self.project.paths, sources) { let (sources, edges) = graph.into_sources(); // Calculate content hashes for later comparison. @@ -1020,7 +1021,7 @@ pub(crate) enum ArtifactsCache< C: Compiler, > { /// Cache nothing on disk - Ephemeral(GraphEdges, &'a Project), + Ephemeral(GraphEdges, &'a Project), /// Handles the actual cached artifacts, detects artifacts that can be reused Cached(ArtifactsCacheInner<'a, T, C>), } @@ -1032,7 +1033,7 @@ impl<'a, T: ArtifactOutput, C: Compiler> #[instrument(name = "ArtifactsCache::new", skip(project, edges))] pub fn new( project: &'a Project, - edges: GraphEdges, + edges: GraphEdges, preprocessed: bool, ) -> Result { /// Returns the [CompilerCache] to use @@ -1117,7 +1118,7 @@ impl<'a, T: ArtifactOutput, C: Compiler> } /// Returns the graph data for this project - pub fn graph(&self) -> &GraphEdges { + pub fn graph(&self) -> &GraphEdges { match self { ArtifactsCache::Ephemeral(graph, _) => graph, ArtifactsCache::Cached(inner) => &inner.edges, @@ -1191,18 +1192,22 @@ impl<'a, T: ArtifactOutput, C: Compiler> /// /// Returns all the _cached_ artifacts. #[instrument(name = "ArtifactsCache::consume", skip_all)] + #[allow(clippy::type_complexity)] pub fn consume( self, written_artifacts: &Artifacts, written_build_infos: &Vec>, write_to_disk: bool, - ) -> Result<(Artifacts, Builds)> + ) -> Result<(Artifacts, Builds, GraphEdges)> where T: ArtifactOutput, { - let ArtifactsCache::Cached(cache) = self else { - trace!("no cache configured, ephemeral"); - return Ok(Default::default()); + let cache = match self { + ArtifactsCache::Ephemeral(edges, _project) => { + trace!("no cache configured, ephemeral"); + return Ok((Default::default(), Default::default(), edges)); + } + ArtifactsCache::Cached(cache) => cache, }; let ArtifactsCacheInner { @@ -1212,7 +1217,9 @@ impl<'a, T: ArtifactOutput, C: Compiler> dirty_sources, sources_in_scope, project, - .. + edges, + content_hashes: _, + interface_repr_hashes: _, } = cache; // Remove cached artifacts which are out of scope, dirty or appear in `written_artifacts`. @@ -1264,7 +1271,7 @@ impl<'a, T: ArtifactOutput, C: Compiler> cache.write(project.cache_path())?; } - Ok((cached_artifacts, cached_builds)) + Ok((cached_artifacts, cached_builds, edges)) } /// Marks the cached entry as seen by the compiler, if it's cached. diff --git a/crates/compilers/src/cache/iface.rs b/crates/compilers/src/cache/iface.rs index ff29be1..aea2e43 100644 --- a/crates/compilers/src/cache/iface.rs +++ b/crates/compilers/src/cache/iface.rs @@ -1,5 +1,5 @@ use crate::{parse_one_source, replace_source_content}; -use solar_sema::{ +use solar_parse::{ ast::{self, Span}, interface::diagnostics::EmittedDiagnostics, }; @@ -11,7 +11,7 @@ pub(crate) fn interface_repr_hash(content: &str, path: &Path) -> Option } pub(crate) fn interface_repr(content: &str, path: &Path) -> Result { - parse_one_source(content, path, |ast| interface_representation_ast(content, &ast)) + parse_one_source(content, path, |sess, ast| interface_representation_ast(content, sess, ast)) } /// Helper function to remove parts of the contract which do not alter its interface: @@ -21,6 +21,7 @@ pub(crate) fn interface_repr(content: &str, path: &Path) -> Result, ) -> String { let mut spans_to_remove: Vec = Vec::new(); @@ -57,9 +58,9 @@ pub(crate) fn interface_representation_ast( } } } - let content = - replace_source_content(content, spans_to_remove.iter().map(|span| (span.to_range(), ""))) - .replace("\n", ""); + let updates = + spans_to_remove.iter().map(|&span| (sess.source_map().span_to_source(span).unwrap().1, "")); + let content = replace_source_content(content, updates).replace("\n", ""); crate::utils::RE_TWO_OR_MORE_SPACES.replace_all(&content, "").into_owned() } diff --git a/crates/compilers/src/compile/output/mod.rs b/crates/compilers/src/compile/output/mod.rs index fcc0148..d20681b 100644 --- a/crates/compilers/src/compile/output/mod.rs +++ b/crates/compilers/src/compile/output/mod.rs @@ -19,6 +19,7 @@ use crate::{ compilers::{ multi::MultiCompiler, CompilationError, Compiler, CompilerContract, CompilerOutput, }, + resolver::GraphEdges, Artifact, ArtifactId, ArtifactOutput, Artifacts, ConfigurableArtifacts, }; @@ -62,7 +63,7 @@ impl IntoIterator for Builds { /// Contains a mixture of already compiled/cached artifacts and the input set of sources that still /// need to be compiled. -#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[derive(Clone, Debug, Default)] pub struct ProjectCompileOutput< C: Compiler = MultiCompiler, T: ArtifactOutput = ConfigurableArtifacts, @@ -81,11 +82,23 @@ pub struct ProjectCompileOutput< pub(crate) compiler_severity_filter: Severity, /// all build infos that were just compiled pub(crate) builds: Builds, + /// The relationship between the source files and their imports + pub(crate) edges: GraphEdges, } impl, C: Compiler> ProjectCompileOutput { + /// Returns the parser used to parse the sources. + pub fn parser(&self) -> &C::Parser { + self.edges.parser() + } + + /// Returns the parser used to parse the sources. + pub fn parser_mut(&mut self) -> &mut C::Parser { + self.edges.parser_mut() + } + /// Converts all `\\` separators in _all_ paths to `/` #[instrument(skip_all)] pub fn slash_paths(&mut self) { @@ -460,6 +473,11 @@ impl, C: Compiler> pub fn builds(&self) -> impl Iterator)> { self.builds.iter() } + + /// Returns the source graph of the project. + pub fn graph(&self) -> &GraphEdges { + &self.edges + } } impl> diff --git a/crates/compilers/src/compile/project.rs b/crates/compilers/src/compile/project.rs index 32859e7..cbfbf70 100644 --- a/crates/compilers/src/compile/project.rs +++ b/crates/compilers/src/compile/project.rs @@ -146,7 +146,7 @@ pub struct ProjectCompiler< C: Compiler, > { /// Contains the relationship of the source files and their imports - edges: GraphEdges, + edges: GraphEdges, project: &'a Project, /// A mapping from a source file path to the primary profile name selected for it. primary_profiles: HashMap, @@ -381,7 +381,7 @@ impl, C: Compiler> let skip_write_to_disk = project.no_artifacts || has_error; trace!(has_error, project.no_artifacts, skip_write_to_disk, cache_path=?project.cache_path(),"prepare writing cache file"); - let (cached_artifacts, cached_builds) = + let (cached_artifacts, cached_builds, edges) = cache.consume(&compiled_artifacts, &output.build_infos, !skip_write_to_disk)?; project.artifacts_handler().handle_cached_artifacts(&cached_artifacts)?; @@ -404,6 +404,7 @@ impl, C: Compiler> ignored_file_paths, compiler_severity_filter, builds, + edges, }) } } diff --git a/crates/compilers/src/compilers/mod.rs b/crates/compilers/src/compilers/mod.rs index 0081051..565ffbf 100644 --- a/crates/compilers/src/compilers/mod.rs +++ b/crates/compilers/src/compilers/mod.rs @@ -1,4 +1,4 @@ -use crate::ProjectPathsConfig; +use crate::{resolver::Node, ProjectPathsConfig}; use alloy_json_abi::JsonAbi; use core::fmt; use foundry_compilers_artifacts::{ @@ -9,6 +9,7 @@ use foundry_compilers_artifacts::{ BytecodeObject, CompactContractRef, Contract, FileToContractsMap, Severity, SourceFile, }; use foundry_compilers_core::error::Result; +use rayon::prelude::*; use semver::{Version, VersionReq}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::{ @@ -139,12 +140,40 @@ pub trait CompilerInput: Serialize + Send + Sync + Sized + Debug { fn strip_prefix(&mut self, base: &Path); } +/// [`ParsedSource`] parser. +pub trait SourceParser: Clone + Debug + Send + Sync { + type ParsedSource: ParsedSource; + + /// Creates a new parser for the given config. + fn new(config: &ProjectPathsConfig) -> Self; + + /// Reads and parses the source file at the given path. + fn read(&mut self, path: &Path) -> Result> { + Node::read(path) + } + + /// Parses the sources in the given sources map. + fn parse_sources( + &mut self, + sources: &mut Sources, + ) -> Result)>> { + sources + .0 + .par_iter() + .map(|(path, source)| { + let data = Self::ParsedSource::parse(source.as_ref(), path)?; + Ok((path.clone(), Node::new(path.clone(), source.clone(), data))) + }) + .collect::>() + } +} + /// Parser of the source files which is used to identify imports and version requirements of the /// given source. /// /// Used by path resolver to resolve imports or determine compiler versions needed to compiler given /// sources. -pub trait ParsedSource: Debug + Sized + Send + Clone { +pub trait ParsedSource: Clone + Debug + Sized + Send { type Language: Language; /// Parses the content of the source file. @@ -331,7 +360,7 @@ pub trait Compiler: Send + Sync + Clone { /// Output data for each contract type CompilerContract: CompilerContract; /// Source parser used for resolving imports and version requirements. - type ParsedSource: ParsedSource; + type Parser: SourceParser>; /// Compiler settings. type Settings: CompilerSettings; /// Enum of languages supported by the compiler. diff --git a/crates/compilers/src/compilers/multi.rs b/crates/compilers/src/compilers/multi.rs index f729d50..3f5ef53 100644 --- a/crates/compilers/src/compilers/multi.rs +++ b/crates/compilers/src/compilers/multi.rs @@ -10,9 +10,11 @@ use super::{ }; use crate::{ artifacts::vyper::{VyperCompilationError, VyperSettings}, - resolver::parse::SolData, + parser::VyperParser, + resolver::parse::{SolData, SolParser}, settings::VyperRestrictions, solc::SolcRestrictions, + SourceParser, }; use foundry_compilers_artifacts::{ error::SourceLocation, @@ -66,6 +68,12 @@ pub enum MultiCompilerLanguage { Vyper(VyperLanguage), } +impl Default for MultiCompilerLanguage { + fn default() -> Self { + Self::Solc(SolcLanguage::Solidity) + } +} + impl MultiCompilerLanguage { pub fn is_vyper(&self) -> bool { matches!(self, Self::Vyper(_)) @@ -101,6 +109,35 @@ impl fmt::Display for MultiCompilerLanguage { } } +/// Source parser for the [`MultiCompiler`]. Recognizes Solc and Vyper sources. +#[derive(Clone, Debug)] +pub struct MultiCompilerParser { + solc: SolParser, + vyper: VyperParser, +} + +impl MultiCompilerParser { + /// Returns the parser used to parse Solc sources. + pub fn solc(&self) -> &SolParser { + &self.solc + } + + /// Returns the parser used to parse Solc sources. + pub fn solc_mut(&mut self) -> &mut SolParser { + &mut self.solc + } + + /// Returns the parser used to parse Vyper sources. + pub fn vyper(&self) -> &VyperParser { + &self.vyper + } + + /// Returns the parser used to parse Vyper sources. + pub fn vyper_mut(&mut self) -> &mut VyperParser { + &mut self.vyper + } +} + /// Source parser for the [MultiCompiler]. Recognizes Solc and Vyper sources. #[derive(Clone, Debug)] pub enum MultiCompilerParsedSource { @@ -287,7 +324,7 @@ impl CompilerInput for MultiCompilerInput { impl Compiler for MultiCompiler { type Input = MultiCompilerInput; type CompilationError = MultiCompilerError; - type ParsedSource = MultiCompilerParsedSource; + type Parser = MultiCompilerParser; type Settings = MultiCompilerSettings; type Language = MultiCompilerLanguage; type CompilerContract = Contract; @@ -327,20 +364,67 @@ impl Compiler for MultiCompiler { } } +impl SourceParser for MultiCompilerParser { + type ParsedSource = MultiCompilerParsedSource; + + fn new(config: &crate::ProjectPathsConfig) -> Self { + Self { solc: SolParser::new(config), vyper: VyperParser::new(config) } + } + + fn read(&mut self, path: &Path) -> Result> { + Ok(match guess_lang(path)? { + MultiCompilerLanguage::Solc(_) => { + self.solc.read(path)?.map_data(MultiCompilerParsedSource::Solc) + } + MultiCompilerLanguage::Vyper(_) => { + self.vyper.read(path)?.map_data(MultiCompilerParsedSource::Vyper) + } + }) + } + + fn parse_sources( + &mut self, + sources: &mut Sources, + ) -> Result)>> { + let mut vyper = Sources::new(); + sources.retain(|path, source| { + if let Ok(lang) = guess_lang(path) { + match lang { + MultiCompilerLanguage::Solc(_) => {} + MultiCompilerLanguage::Vyper(_) => { + vyper.insert(path.clone(), source.clone()); + return false; + } + } + } + true + }); + + let solc_nodes = self.solc.parse_sources(sources)?; + let vyper_nodes = self.vyper.parse_sources(&mut vyper)?; + Ok(solc_nodes + .into_iter() + .map(|(k, v)| (k, v.map_data(MultiCompilerParsedSource::Solc))) + .chain( + vyper_nodes + .into_iter() + .map(|(k, v)| (k, v.map_data(MultiCompilerParsedSource::Vyper))), + ) + .collect()) + } +} + impl ParsedSource for MultiCompilerParsedSource { type Language = MultiCompilerLanguage; - fn parse(content: &str, file: &std::path::Path) -> Result { - let Some(extension) = file.extension().and_then(|e| e.to_str()) else { - return Err(SolcError::msg("failed to resolve file extension")); - }; - - if SOLC_EXTENSIONS.contains(&extension) { - ::parse(content, file).map(Self::Solc) - } else if VYPER_EXTENSIONS.contains(&extension) { - VyperParsedSource::parse(content, file).map(Self::Vyper) - } else { - Err(SolcError::msg("unexpected file extension")) + fn parse(content: &str, file: &Path) -> Result { + match guess_lang(file)? { + MultiCompilerLanguage::Solc(_) => { + ::parse(content, file).map(Self::Solc) + } + MultiCompilerLanguage::Vyper(_) => { + VyperParsedSource::parse(content, file).map(Self::Vyper) + } } } @@ -399,6 +483,24 @@ impl ParsedSource for MultiCompilerParsedSource { } } +fn guess_lang(path: &Path) -> Result { + let extension = path + .extension() + .and_then(|e| e.to_str()) + .ok_or_else(|| SolcError::msg("failed to resolve file extension"))?; + if SOLC_EXTENSIONS.contains(&extension) { + Ok(MultiCompilerLanguage::Solc(match extension { + "sol" => SolcLanguage::Solidity, + "yul" => SolcLanguage::Yul, + _ => unreachable!(), + })) + } else if VYPER_EXTENSIONS.contains(&extension) { + Ok(MultiCompilerLanguage::Vyper(VyperLanguage::default())) + } else { + Err(SolcError::msg("unexpected file extension")) + } +} + impl CompilationError for MultiCompilerError { fn is_warning(&self) -> bool { match self { diff --git a/crates/compilers/src/compilers/solc/mod.rs b/crates/compilers/src/compilers/solc/mod.rs index 7a2f166..ee31cf3 100644 --- a/crates/compilers/src/compilers/solc/mod.rs +++ b/crates/compilers/src/compilers/solc/mod.rs @@ -2,8 +2,13 @@ use super::{ restrictions::CompilerSettingsRestrictions, CompilationError, Compiler, CompilerInput, CompilerOutput, CompilerSettings, CompilerVersion, Language, ParsedSource, }; -use crate::resolver::parse::SolData; -pub use foundry_compilers_artifacts::SolcLanguage; +use crate::{ + resolver::{ + parse::{SolData, SolParser}, + Node, + }, + SourceParser, +}; use foundry_compilers_artifacts::{ error::SourceLocation, output_selection::OutputSelection, @@ -11,7 +16,8 @@ use foundry_compilers_artifacts::{ sources::{Source, Sources}, BytecodeHash, Contract, Error, EvmVersion, Settings, Severity, SolcInput, }; -use foundry_compilers_core::error::Result; +use foundry_compilers_core::error::{Result, SolcError, SolcIoError}; +use rayon::prelude::*; use semver::Version; use serde::{Deserialize, Serialize}; use std::{ @@ -20,6 +26,9 @@ use std::{ ops::{Deref, DerefMut}, path::{Path, PathBuf}, }; + +pub use foundry_compilers_artifacts::SolcLanguage; + mod compiler; pub use compiler::{Solc, SOLC_EXTENSIONS}; @@ -40,7 +49,7 @@ impl Language for SolcLanguage { impl Compiler for SolcCompiler { type Input = SolcVersionedInput; type CompilationError = Error; - type ParsedSource = SolData; + type Parser = SolParser; type Settings = SolcSettings; type Language = SolcLanguage; type CompilerContract = Contract; @@ -355,6 +364,91 @@ impl CompilerSettings for SolcSettings { } } +impl SourceParser for SolParser { + type ParsedSource = SolData; + + fn new(config: &crate::ProjectPathsConfig) -> Self { + Self { + compiler: solar_sema::Compiler::new(Self::session_with_opts( + solar_sema::interface::config::Opts { + include_paths: config.include_paths.iter().cloned().collect(), + base_path: Some(config.root.clone()), + import_remappings: config + .remappings + .iter() + .map(|r| solar_sema::interface::config::ImportRemapping { + context: r.context.clone().unwrap_or_default(), + prefix: r.name.clone(), + path: r.path.clone(), + }) + .collect(), + ..Default::default() + }, + )), + } + } + + fn read(&mut self, path: &Path) -> Result> { + let mut sources = Sources::from_iter([(path.to_path_buf(), Source::read_(path)?)]); + let nodes = self.parse_sources(&mut sources)?; + debug_assert_eq!(nodes.len(), 1, "{nodes:#?}"); + Ok(nodes.into_iter().next().unwrap().1) + } + + fn parse_sources( + &mut self, + sources: &mut Sources, + ) -> Result)>> { + self.compiler_mut().enter_mut(|compiler| { + let mut pcx = compiler.parse(); + let files = sources + .par_iter() + .map(|(path, source)| { + pcx.sess + .source_map() + .new_source_file(path.clone(), source.content.as_str()) + .map_err(|e| SolcError::Io(SolcIoError::new(e, path))) + }) + .collect::>>()?; + pcx.add_files(files); + pcx.parse(); + + let parsed = sources.par_iter().map(|(path, source)| { + let sf = compiler.sess().source_map().get_file(path).unwrap(); + let (_, s) = compiler.gcx().sources.get_file(&sf).unwrap(); + let node = Node::new( + path.clone(), + source.clone(), + SolData::parse_from(compiler.gcx().sess, s), + ); + (path.clone(), node) + }); + let mut parsed = parsed.collect::>(); + + // Set error on the first successful source, if any. This doesn't really have to be + // exact, as long as at least one source has an error set it should be enough. + if let Some(Err(diag)) = compiler.gcx().sess.emitted_errors() { + if let Some(idx) = parsed + .iter() + .position(|(_, node)| node.data.parse_result.is_ok()) + .or_else(|| parsed.first().map(|_| 0)) + { + let (_, node) = &mut parsed[idx]; + node.data.parse_result = Err(diag.to_string()); + } + } + + for (path, node) in &parsed { + if let Err(e) = &node.data.parse_result { + debug!("failed parsing {}: {e}", path.display()); + } + } + + Ok(parsed) + }) + } +} + impl ParsedSource for SolData { type Language = SolcLanguage; diff --git a/crates/compilers/src/compilers/vyper/mod.rs b/crates/compilers/src/compilers/vyper/mod.rs index 6d85c49..88034cb 100644 --- a/crates/compilers/src/compilers/vyper/mod.rs +++ b/crates/compilers/src/compilers/vyper/mod.rs @@ -1,6 +1,7 @@ -use self::{input::VyperVersionedInput, parser::VyperParsedSource}; +use self::input::VyperVersionedInput; use super::{Compiler, CompilerOutput, Language}; pub use crate::artifacts::vyper::{VyperCompilationError, VyperInput, VyperOutput, VyperSettings}; +use crate::parser::VyperParser; use core::fmt; use foundry_compilers_artifacts::{sources::Source, Contract}; use foundry_compilers_core::error::{Result, SolcError}; @@ -26,7 +27,7 @@ pub const VYPER_EXTENSIONS: &[&str] = &["vy", "vyi"]; pub const VYPER_INTERFACE_EXTENSION: &str = "vyi"; /// Vyper language, used as [Compiler::Language] for the Vyper compiler. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] #[non_exhaustive] pub struct VyperLanguage; @@ -201,7 +202,7 @@ impl Vyper { impl Compiler for Vyper { type Settings = VyperSettings; type CompilationError = VyperCompilationError; - type ParsedSource = VyperParsedSource; + type Parser = VyperParser; type Input = VyperVersionedInput; type Language = VyperLanguage; type CompilerContract = Contract; diff --git a/crates/compilers/src/compilers/vyper/parser.rs b/crates/compilers/src/compilers/vyper/parser.rs index 2f524ff..d3602b0 100644 --- a/crates/compilers/src/compilers/vyper/parser.rs +++ b/crates/compilers/src/compilers/vyper/parser.rs @@ -1,7 +1,7 @@ use super::VyperLanguage; use crate::{ compilers::{vyper::VYPER_EXTENSIONS, ParsedSource}, - ProjectPathsConfig, + ProjectPathsConfig, SourceParser, }; use foundry_compilers_core::{ error::{Result, SolcError}, @@ -26,6 +26,19 @@ pub struct VyperImport { pub final_part: Option, } +#[derive(Clone, Debug, Default)] +pub struct VyperParser { + _inner: (), +} + +impl SourceParser for VyperParser { + type ParsedSource = VyperParsedSource; + + fn new(_config: &ProjectPathsConfig) -> Self { + Self { _inner: () } + } +} + #[derive(Clone, Debug)] pub struct VyperParsedSource { path: PathBuf, diff --git a/crates/compilers/src/config.rs b/crates/compilers/src/config.rs index 45dfd8d..3f3b5f4 100644 --- a/crates/compilers/src/config.rs +++ b/crates/compilers/src/config.rs @@ -2,7 +2,7 @@ use crate::{ cache::SOLIDITY_FILES_CACHE_FILENAME, compilers::{multi::MultiCompilerLanguage, Language}, flatten::{collect_ordered_deps, combine_version_pragmas}, - resolver::{parse::SolData, SolImportAlias}, + resolver::{parse::SolParser, SolImportAlias}, Graph, }; use foundry_compilers_artifacts::{ @@ -110,7 +110,7 @@ impl ProjectPathsConfig { } let sources = Source::read_all_files(input_files)?; - let graph = Graph::::resolve_sources(self, sources)?; + let graph = Graph::::resolve_sources(self, sources)?; let ordered_deps = collect_ordered_deps(&flatten_target, self, &graph)?; #[cfg(windows)] @@ -549,36 +549,14 @@ impl ProjectPathsConfig { } } - pub fn with_language(self) -> ProjectPathsConfig { - let Self { - root, - cache, - artifacts, - build_infos, - sources, - tests, - scripts, - libraries, - remappings, - include_paths, - allowed_paths, - _l, - } = self; + pub fn with_language_ref(&self) -> &ProjectPathsConfig { + // SAFETY: `Lang` is `PhantomData`. + unsafe { std::mem::transmute(self) } + } - ProjectPathsConfig { - root, - cache, - artifacts, - build_infos, - sources, - tests, - scripts, - libraries, - remappings, - include_paths, - allowed_paths, - _l: PhantomData, - } + pub fn with_language(self) -> ProjectPathsConfig { + // SAFETY: `Lang` is `PhantomData`. + unsafe { std::mem::transmute(self) } } pub fn apply_lib_remappings(&self, mut libraries: Libraries) -> Libraries { diff --git a/crates/compilers/src/filter.rs b/crates/compilers/src/filter.rs index 979b1e5..aa25914 100644 --- a/crates/compilers/src/filter.rs +++ b/crates/compilers/src/filter.rs @@ -3,7 +3,7 @@ use crate::{ compilers::{multi::MultiCompilerParsedSource, CompilerSettings, ParsedSource}, resolver::{parse::SolData, GraphEdges}, - Sources, + SourceParser, Sources, }; use foundry_compilers_artifacts::output_selection::OutputSelection; use std::{ @@ -101,11 +101,11 @@ impl<'a> SparseOutputFilter<'a> { /// /// This also takes the project's graph as input, this allows us to check if the files the /// filter matches depend on libraries that need to be linked - pub fn sparse_sources( + pub fn sparse_sources( &self, sources: &Sources, settings: &mut S, - graph: &GraphEdges, + graph: &GraphEdges

, ) -> Vec { let mut full_compilation: HashSet = sources .dirty_files() diff --git a/crates/compilers/src/flatten.rs b/crates/compilers/src/flatten.rs index 3d5860c..ce34243 100644 --- a/crates/compilers/src/flatten.rs +++ b/crates/compilers/src/flatten.rs @@ -3,7 +3,7 @@ use crate::{ compilers::{Compiler, ParsedSource}, filter::MaybeSolData, resolver::parse::SolData, - ArtifactOutput, CompilerSettings, Graph, Project, ProjectPathsConfig, Updates, + ArtifactOutput, CompilerSettings, Graph, Project, ProjectPathsConfig, SourceParser, Updates, }; use foundry_compilers_artifacts::{ ast::{visitor::Visitor, *}, @@ -192,7 +192,7 @@ impl Flattener { target: &Path, ) -> std::result::Result where - C::ParsedSource: MaybeSolData, + C::Parser: SourceParser, { // Configure project to compile the target file and only request AST for target file. project.cached = false; @@ -210,7 +210,7 @@ impl Flattener { let output = output.compiler_output; let sources = Source::read_all_files(vec![target.to_path_buf()])?; - let graph = Graph::::resolve_sources(&project.paths, sources)?; + let graph = Graph::::resolve_sources(&project.paths, sources)?; let ordered_sources = collect_ordered_deps(target, &project.paths, &graph)?; @@ -794,10 +794,10 @@ impl Flattener { } /// Performs DFS to collect all dependencies of a target -fn collect_deps( +fn collect_deps>( path: &Path, - paths: &ProjectPathsConfig, - graph: &Graph, + paths: &ProjectPathsConfig<::Language>, + graph: &Graph

, deps: &mut HashSet, ) -> Result<()> { if deps.insert(path.to_path_buf()) { @@ -830,10 +830,10 @@ fn collect_deps( /// Instead, we sort files by the number of their dependencies (imports of any depth) in ascending /// order. If files have the same number of dependencies, we sort them alphabetically. /// Target file is always placed last. -pub fn collect_ordered_deps( +pub fn collect_ordered_deps>( path: &Path, - paths: &ProjectPathsConfig, - graph: &Graph, + paths: &ProjectPathsConfig<::Language>, + graph: &Graph

, ) -> Result> { let mut deps = HashSet::new(); collect_deps(path, paths, graph, &mut deps)?; diff --git a/crates/compilers/src/lib.rs b/crates/compilers/src/lib.rs index bd4bc98..ad0ee4b 100644 --- a/crates/compilers/src/lib.rs +++ b/crates/compilers/src/lib.rs @@ -64,8 +64,10 @@ use foundry_compilers_core::error::{Result, SolcError, SolcIoError}; use output::sources::{VersionedSourceFile, VersionedSourceFiles}; use project::ProjectCompiler; use semver::Version; -use solar_parse::Parser; -use solar_sema::interface::{diagnostics::EmittedDiagnostics, source_map::FileName, Session}; +use solar_parse::{ + interface::{diagnostics::EmittedDiagnostics, source_map::FileName, Session}, + Parser, +}; use solc::SolcSettings; use std::{ collections::{BTreeMap, BTreeSet, HashMap, HashSet}, @@ -173,7 +175,7 @@ where /// Returns standard-json-input to compile the target contract pub fn standard_json_input(&self, target: &Path) -> Result { trace!(?target, "Building standard-json-input"); - let graph = Graph::::resolve(&self.paths)?; + let graph = Graph::::resolve(&self.paths)?; let target_index = graph.files().get(target).ok_or_else(|| { SolcError::msg(format!("cannot resolve file at {:?}", target.display())) })?; @@ -389,7 +391,7 @@ impl, C: Compiler> Pro T: Clone, C: Clone, { - let graph = Graph::::resolve(&self.paths)?; + let graph = Graph::::resolve(&self.paths)?; let mut contracts: HashMap> = HashMap::new(); if !graph.is_empty() { for node in &graph.nodes { @@ -923,15 +925,15 @@ pub fn replace_source_content( pub(crate) fn parse_one_source( content: &str, path: &Path, - f: impl FnOnce(solar_sema::ast::SourceUnit<'_>) -> R, + f: impl FnOnce(&Session, &solar_parse::ast::SourceUnit<'_>) -> R, ) -> Result { let sess = Session::builder().with_buffer_emitter(Default::default()).build(); - let res = sess.enter(|| -> solar_parse::interface::Result<_> { + let res = sess.enter_sequential(|| -> solar_parse::interface::Result<_> { let arena = solar_parse::ast::Arena::new(); let filename = FileName::Real(path.to_path_buf()); let mut parser = Parser::from_source_code(&sess, &arena, filename, content.to_string())?; let ast = parser.parse_file().map_err(|e| e.emit())?; - Ok(f(ast)) + Ok(f(&sess, &ast)) }); // Return if any diagnostics emitted during content parsing. @@ -946,11 +948,10 @@ pub(crate) fn parse_one_source( #[cfg(test)] #[cfg(feature = "svm-solc")] mod tests { + use super::*; use foundry_compilers_artifacts::Remapping; use foundry_compilers_core::utils::{self, mkdir_or_touch, tempdir}; - use super::*; - #[test] #[cfg_attr(windows, ignore = "<0.7 solc is flaky")] fn test_build_all_versions() { diff --git a/crates/compilers/src/project_util/mock.rs b/crates/compilers/src/project_util/mock.rs index 0e9b843..0964cb7 100644 --- a/crates/compilers/src/project_util/mock.rs +++ b/crates/compilers/src/project_util/mock.rs @@ -10,9 +10,8 @@ use std::{ }; use crate::{ - compilers::{multi::MultiCompilerParsedSource, Language, ParsedSource}, - resolver::GraphEdges, - Graph, ProjectPathsConfig, + compilers::Language, multi::MultiCompilerParser, resolver::GraphEdges, Graph, + ProjectPathsConfig, SourceParser, }; /// Represents the layout of a project @@ -51,9 +50,9 @@ impl MockProjectGenerator { } /// Create a skeleton of a real project - pub fn create(paths: &ProjectPathsConfig) -> Result { - fn get_libs( - edges: &GraphEdges, + pub fn create(paths: &ProjectPathsConfig) -> Result { + fn get_libs( + edges: &GraphEdges

, lib_folder: &Path, ) -> Option>> { let mut libs: HashMap<_, Vec<_>> = HashMap::new(); @@ -65,7 +64,7 @@ impl MockProjectGenerator { Some(libs) } - let graph = Graph::::resolve(paths)?; + let graph = Graph::::resolve(paths)?; let mut generated = Self::default(); let (_, edges) = graph.into_sources(); diff --git a/crates/compilers/src/resolver/mod.rs b/crates/compilers/src/resolver/mod.rs index 52157d5..feb4abc 100644 --- a/crates/compilers/src/resolver/mod.rs +++ b/crates/compilers/src/resolver/mod.rs @@ -46,18 +46,17 @@ //! which is defined on a per source file basis. use crate::{ - compilers::{Compiler, CompilerVersion, Language, ParsedSource}, + compilers::{Compiler, CompilerVersion, ParsedSource}, project::VersionedSources, - ArtifactOutput, CompilerSettings, Project, ProjectPathsConfig, + resolver::parse::SolParser, + ArtifactOutput, CompilerSettings, Project, ProjectPathsConfig, SourceParser, }; use core::fmt; use foundry_compilers_artifacts::sources::{Source, Sources}; use foundry_compilers_core::{ error::{Result, SolcError}, - utils::{self, find_case_sensitive_existing_file}, + utils, }; -use parse::SolData; -use rayon::prelude::*; use semver::{Version, VersionReq}; use std::{ collections::{BTreeSet, HashMap, HashSet, VecDeque}, @@ -89,15 +88,15 @@ pub struct ResolvedSources<'a, C: Compiler> { /// a profile suffix. pub primary_profiles: HashMap, /// Graph edges. - pub edges: GraphEdges, + pub edges: GraphEdges, } /// The underlying edges of the graph which only contains the raw relationship data. /// /// This is kept separate from the `Graph` as the `Node`s get consumed when the `Solc` to `Sources` /// set is determined. -#[derive(Debug)] -pub struct GraphEdges { +#[derive(Clone, Debug)] +pub struct GraphEdges { /// The indices of `edges` correspond to the `nodes`. That is, `edges[0]` /// is the set of outgoing edges for `nodes[0]`. edges: Vec>, @@ -109,8 +108,10 @@ pub struct GraphEdges { rev_indices: HashMap, /// the identified version requirement of a file versions: HashMap>, - /// the extracted data from the source file - data: HashMap, + /// the extracted data from the source files + data: Vec, + /// The parser which parsed `data`. + parser: Option

, /// with how many input files we started with, corresponds to `let input_files = /// nodes[..num_input_files]`. /// @@ -127,7 +128,34 @@ pub struct GraphEdges { resolved_solc_include_paths: BTreeSet, } -impl GraphEdges { +impl Default for GraphEdges

{ + fn default() -> Self { + Self { + edges: Default::default(), + rev_edges: Default::default(), + indices: Default::default(), + rev_indices: Default::default(), + versions: Default::default(), + data: Default::default(), + parser: Default::default(), + num_input_files: Default::default(), + unresolved_imports: Default::default(), + resolved_solc_include_paths: Default::default(), + } + } +} + +impl GraphEdges

{ + /// Returns the parser used to parse the sources. + pub fn parser(&self) -> &P { + self.parser.as_ref().unwrap() + } + + /// Returns the parser used to parse the sources. + pub fn parser_mut(&mut self) -> &mut P { + self.parser.as_mut().unwrap() + } + /// How many files are source files pub fn num_source_files(&self) -> usize { self.num_input_files @@ -212,8 +240,11 @@ impl GraphEdges { } /// Returns the parsed source data for the given file - pub fn get_parsed_source(&self, file: &Path) -> Option<&D> { - self.indices.get(file).and_then(|idx| self.data.get(idx)) + pub fn get_parsed_source(&self, file: &Path) -> Option<&P::ParsedSource> + where + P: SourceParser, + { + self.indices.get(file).and_then(|idx| self.data.get(*idx)) } } @@ -223,16 +254,23 @@ impl GraphEdges { /// /// See also #[derive(Debug)] -pub struct Graph { +pub struct Graph { /// all nodes in the project, a `Node` represents a single file - pub nodes: Vec>, + pub nodes: Vec>, /// relationship of the nodes - edges: GraphEdges, + edges: GraphEdges

, /// the root of the project this graph represents root: PathBuf, } -impl> Graph { +type L

= <

::ParsedSource as ParsedSource>::Language; + +impl Graph

{ + /// Returns the parser used to parse the sources. + pub fn parser(&self) -> &P { + self.edges.parser() + } + /// Print the graph to `StdOut` pub fn print(&self) { self.print_with_options(Default::default()) @@ -275,11 +313,11 @@ impl> Graph { /// # Panics /// /// if the `index` node id is not included in the graph - pub fn node(&self, index: usize) -> &Node { + pub fn node(&self, index: usize) -> &Node { &self.nodes[index] } - pub(crate) fn display_node(&self, index: usize) -> DisplayNode<'_, D> { + pub(crate) fn display_node(&self, index: usize) -> DisplayNode<'_, P::ParsedSource> { DisplayNode { node: self.node(index), root: &self.root } } @@ -294,11 +332,11 @@ impl> Graph { } /// Same as `Self::node_ids` but returns the actual `Node` - pub fn nodes(&self, start: usize) -> impl Iterator> + '_ { + pub fn nodes(&self, start: usize) -> impl Iterator> + '_ { self.node_ids(start).map(move |idx| self.node(idx)) } - fn split(self) -> (Vec<(PathBuf, Source)>, GraphEdges) { + fn split(self) -> (Vec<(PathBuf, Source)>, GraphEdges

) { let Self { nodes, mut edges, .. } = self; // need to move the extracted data to the edges, essentially splitting the node so we have // access to the data at a later stage in the compile pipeline @@ -306,7 +344,9 @@ impl> Graph { for (idx, node) in nodes.into_iter().enumerate() { let Node { path, source, data } = node; sources.push((path, source)); - edges.data.insert(idx, data); + let idx2 = edges.data.len(); + edges.data.push(data); + assert_eq!(idx, idx2); } (sources, edges) @@ -314,7 +354,7 @@ impl> Graph { /// Consumes the `Graph`, effectively splitting the `nodes` and the `GraphEdges` off and /// returning the `nodes` converted to `Sources` - pub fn into_sources(self) -> (Sources, GraphEdges) { + pub fn into_sources(self) -> (Sources, GraphEdges

) { let (sources, edges) = self.split(); (sources.into_iter().collect(), edges) } @@ -322,7 +362,7 @@ impl> Graph { /// Returns an iterator that yields only those nodes that represent input files. /// See `Self::resolve_sources` /// This won't yield any resolved library nodes - pub fn input_nodes(&self) -> impl Iterator> { + pub fn input_nodes(&self) -> impl Iterator> { self.nodes.iter().take(self.edges.num_input_files) } @@ -334,14 +374,15 @@ impl> Graph { /// Resolves a number of sources within the given config #[instrument(name = "Graph::resolve_sources", skip_all)] pub fn resolve_sources( - paths: &ProjectPathsConfig, - sources: Sources, + paths: &ProjectPathsConfig<::Language>, + mut sources: Sources, ) -> Result { /// checks if the given target path was already resolved, if so it adds its id to the list /// of resolved imports. If it hasn't been resolved yet, it queues in the file for /// processing - fn add_node( - unresolved: &mut VecDeque<(PathBuf, Node)>, + fn add_node( + parser: &mut P, + unresolved: &mut VecDeque<(PathBuf, Node)>, index: &mut HashMap, resolved_imports: &mut Vec, target: PathBuf, @@ -350,7 +391,7 @@ impl> Graph { resolved_imports.push(idx); } else { // imported file is not part of the input files - let node = Node::read(&target)?; + let node = parser.read(&target)?; unresolved.push_back((target.clone(), node)); let idx = index.len(); index.insert(target, idx); @@ -359,16 +400,11 @@ impl> Graph { Ok(()) } + let mut parser = P::new(paths.with_language_ref()); + // we start off by reading all input files, which includes all solidity files from the // source and test folder - let mut unresolved: VecDeque<_> = sources - .0 - .into_par_iter() - .map(|(path, source)| { - let data = D::parse(source.as_ref(), &path)?; - Ok((path.clone(), Node { path, source, data })) - }) - .collect::>()?; + let mut unresolved: VecDeque<_> = parser.parse_sources(&mut sources)?.into(); // identifiers of all resolved files let mut index: HashMap<_, _> = @@ -406,9 +442,14 @@ impl> Graph { &import_path, &mut resolved_solc_include_paths, ) { - Ok(import) => { - add_node(&mut unresolved, &mut index, &mut resolved_imports, import).err() - } + Ok(import) => add_node( + &mut parser, + &mut unresolved, + &mut index, + &mut resolved_imports, + import, + ) + .err(), Err(err) => Some(err), } { unresolved_imports.insert((import_path.to_path_buf(), node.path.clone())); @@ -452,6 +493,7 @@ impl> Graph { .map(|(idx, node)| (idx, node.data.version_req().cloned())) .collect(), data: Default::default(), + parser: Some(parser), unresolved_imports, resolved_solc_include_paths, }; @@ -459,12 +501,12 @@ impl> Graph { } /// Resolves the dependencies of a project's source contracts - pub fn resolve(paths: &ProjectPathsConfig) -> Result { + pub fn resolve( + paths: &ProjectPathsConfig<::Language>, + ) -> Result { Self::resolve_sources(paths, paths.read_input_files()?) } -} -impl> Graph { /// Consumes the nodes of the graph and returns all input files together with their appropriate /// version and the edges of the graph /// @@ -476,7 +518,7 @@ impl> Graph { ) -> Result> where T: ArtifactOutput, - C: Compiler, + C: Compiler::Language>, { /// insert the imports of the given node into the sources map /// There can be following graph: @@ -788,7 +830,7 @@ impl> Graph { Err(msg) } - fn input_nodes_by_language(&self) -> HashMap> { + fn input_nodes_by_language(&self) -> HashMap, Vec> { let mut nodes = HashMap::new(); for idx in 0..self.edges.num_input_files { @@ -808,13 +850,14 @@ impl> Graph { /// /// This also attempts to prefer local installations over remote available. /// If `offline` is set to `true` then only already installed. + #[allow(clippy::type_complexity)] fn get_input_node_versions< - C: Compiler, + C: Compiler>, T: ArtifactOutput, >( &self, project: &Project, - ) -> Result>>> { + ) -> Result, HashMap>>> { trace!("resolving input node versions"); let mut resulted_nodes = HashMap::new(); @@ -910,13 +953,13 @@ impl> Graph { #[allow(clippy::complexity)] fn resolve_settings< - C: Compiler, + C: Compiler>, T: ArtifactOutput, >( &self, project: &Project, - input_nodes_versions: HashMap>>, - ) -> Result>>>> { + input_nodes_versions: HashMap, HashMap>>, + ) -> Result, HashMap>>>> { let mut resulted_sources = HashMap::new(); let mut errors = Vec::new(); for (language, versions) in input_nodes_versions { @@ -1034,20 +1077,20 @@ impl> Graph { /// An iterator over a node and its dependencies #[derive(Debug)] -pub struct NodesIter<'a, D> { +pub struct NodesIter<'a, P: SourceParser> { /// stack of nodes stack: VecDeque, visited: HashSet, - graph: &'a GraphEdges, + graph: &'a GraphEdges

, } -impl<'a, D> NodesIter<'a, D> { - fn new(start: usize, graph: &'a GraphEdges) -> Self { +impl<'a, P: SourceParser> NodesIter<'a, P> { + fn new(start: usize, graph: &'a GraphEdges

) -> Self { Self { stack: VecDeque::from([start]), visited: HashSet::new(), graph } } } -impl Iterator for NodesIter<'_, D> { +impl Iterator for NodesIter<'_, P> { type Item = usize; fn next(&mut self) -> Option { let node = self.stack.pop_front()?; @@ -1061,38 +1104,35 @@ impl Iterator for NodesIter<'_, D> { } #[derive(Debug)] -pub struct Node { +pub struct Node { /// path of the solidity file path: PathBuf, /// content of the solidity file source: Source, /// parsed data - pub data: D, + pub data: S, } -impl Node { +impl Node { + pub fn new(path: PathBuf, source: Source, data: S) -> Self { + Self { path, source, data } + } + + pub fn map_data(self, f: impl FnOnce(S) -> T) -> Node { + Node::new(self.path, self.source, f(self.data)) + } +} + +impl Node { /// Reads the content of the file and returns a [Node] containing relevant information pub fn read(file: &Path) -> Result { - let source = Source::read(file).map_err(|err| { - let exists = err.path().exists(); - if !exists && err.path().is_symlink() { - SolcError::ResolveBadSymlink(err) - } else { - // This is an additional check useful on OS that have case-sensitive paths, See also - if !exists { - // check if there exists a file with different case - if let Some(existing_file) = find_case_sensitive_existing_file(file) { - SolcError::ResolveCaseSensitiveFileName { error: err, existing_file } - } else { - SolcError::Resolve(err) - } - } else { - SolcError::Resolve(err) - } - } - })?; - let data = D::parse(source.as_ref(), file)?; - Ok(Self { path: file.to_path_buf(), source, data }) + let source = Source::read_(file)?; + Self::parse(file, source) + } + + pub fn parse(file: &Path, source: Source) -> Result { + let data = S::parse(source.as_ref(), file)?; + Ok(Self::new(file.to_path_buf(), source, data)) } /// Returns the path of the file. @@ -1111,12 +1151,12 @@ impl Node { } /// Helper type for formatting a node -pub(crate) struct DisplayNode<'a, D> { - node: &'a Node, +pub(crate) struct DisplayNode<'a, S> { + node: &'a Node, root: &'a PathBuf, } -impl fmt::Display for DisplayNode<'_, D> { +impl fmt::Display for DisplayNode<'_, S> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let path = utils::source_name(&self.node.path, self.root); write!(f, "{}", path.display())?; @@ -1148,7 +1188,7 @@ mod tests { let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data/hardhat-sample"); let paths = ProjectPathsConfig::hardhat(&root).unwrap(); - let graph = Graph::::resolve(&paths).unwrap(); + let graph = Graph::::resolve(&paths).unwrap(); assert_eq!(graph.edges.num_input_files, 1); assert_eq!(graph.files().len(), 2); @@ -1167,7 +1207,7 @@ mod tests { let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data/dapp-sample"); let paths = ProjectPathsConfig::dapptools(&root).unwrap(); - let graph = Graph::::resolve(&paths).unwrap(); + let graph = Graph::::resolve(&paths).unwrap(); assert_eq!(graph.edges.num_input_files, 2); assert_eq!(graph.files().len(), 3); @@ -1190,26 +1230,31 @@ mod tests { } #[test] - #[cfg(not(target_os = "windows"))] fn can_print_dapp_sample_graph() { let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data/dapp-sample"); let paths = ProjectPathsConfig::dapptools(&root).unwrap(); - let graph = Graph::::resolve(&paths).unwrap(); + let graph = Graph::::resolve(&paths).unwrap(); let mut out = Vec::::new(); tree::print(&graph, &Default::default(), &mut out).unwrap(); - assert_eq!( - " + if !cfg!(windows) { + assert_eq!( + " src/Dapp.sol >=0.6.6 src/Dapp.t.sol >=0.6.6 ├── lib/ds-test/src/test.sol >=0.4.23 └── src/Dapp.sol >=0.6.6 " - .trim_start() - .as_bytes() - .to_vec(), - out - ); + .trim_start() + .as_bytes() + .to_vec(), + out + ); + } + + graph.edges.parser().compiler.enter(|c| { + assert_eq!(c.gcx().sources.len(), 3); + }); } #[test] @@ -1217,7 +1262,7 @@ src/Dapp.t.sol >=0.6.6 fn can_print_hardhat_sample_graph() { let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data/hardhat-sample"); let paths = ProjectPathsConfig::hardhat(&root).unwrap(); - let graph = Graph::::resolve(&paths).unwrap(); + let graph = Graph::::resolve(&paths).unwrap(); let mut out = Vec::::new(); tree::print(&graph, &Default::default(), &mut out).unwrap(); assert_eq!( @@ -1236,7 +1281,7 @@ src/Dapp.t.sol >=0.6.6 let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data/incompatible-pragmas"); let paths = ProjectPathsConfig::dapptools(&root).unwrap(); - let graph = Graph::::resolve(&paths).unwrap(); + let graph = Graph::::resolve(&paths).unwrap(); let Err(SolcError::Message(err)) = graph.get_input_node_versions( &ProjectBuilder::::default() .paths(paths) diff --git a/crates/compilers/src/resolver/parse.rs b/crates/compilers/src/resolver/parse.rs index 8126c85..26baf15 100644 --- a/crates/compilers/src/resolver/parse.rs +++ b/crates/compilers/src/resolver/parse.rs @@ -1,11 +1,91 @@ use foundry_compilers_core::utils; use semver::VersionReq; use solar_parse::{ast, interface::sym}; +use solar_sema::interface; use std::{ ops::Range, path::{Path, PathBuf}, }; +/// Solidity parser. +/// +/// Holds a [`solar_sema::Compiler`] that is used to parse sources incrementally. +/// After project compilation ([`Graph::resolve`]), this will contain all sources parsed by +/// [`Graph`]. +/// +/// This state is currently lost on `Clone`. +/// +/// [`Graph`]: crate::Graph +/// [`Graph::resolve`]: crate::Graph::resolve +#[derive(derive_more::Debug)] +pub struct SolParser { + #[debug(ignore)] + pub(crate) compiler: solar_sema::Compiler, +} + +impl Clone for SolParser { + fn clone(&self) -> Self { + Self { + compiler: solar_sema::Compiler::new(Self::session_with_opts( + self.compiler.sess().opts.clone(), + )), + } + } +} + +impl SolParser { + /// Returns a reference to the compiler. + pub fn compiler(&self) -> &solar_sema::Compiler { + &self.compiler + } + + /// Returns a mutable reference to the compiler. + pub fn compiler_mut(&mut self) -> &mut solar_sema::Compiler { + &mut self.compiler + } + + /// Consumes the parser and returns the compiler. + pub fn into_compiler(self) -> solar_sema::Compiler { + self.compiler + } + + pub(crate) fn session_with_opts( + opts: solar_sema::interface::config::Opts, + ) -> solar_sema::interface::Session { + let sess = solar_sema::interface::Session::builder() + .with_buffer_emitter(Default::default()) + .opts(opts) + .build(); + sess.source_map().set_file_loader(FileLoader); + sess + } +} + +struct FileLoader; +impl interface::source_map::FileLoader for FileLoader { + fn canonicalize_path(&self, path: &Path) -> std::io::Result { + interface::source_map::RealFileLoader.canonicalize_path(path) + } + + fn load_stdin(&self) -> std::io::Result { + interface::source_map::RealFileLoader.load_stdin() + } + + fn load_file(&self, path: &Path) -> std::io::Result { + interface::source_map::RealFileLoader.load_file(path).map(|s| { + if s.contains('\r') { + s.replace('\r', "") + } else { + s + } + }) + } + + fn load_binary_file(&self, path: &Path) -> std::io::Result> { + interface::source_map::RealFileLoader.load_binary_file(path) + } +} + /// Represents various information about a Solidity file. #[derive(Clone, Debug)] #[non_exhaustive] @@ -44,111 +124,26 @@ impl SolData { /// parsing fails, we'll fall back to extract that info via regex #[instrument(name = "SolData::parse", skip_all)] pub fn parse(content: &str, file: &Path) -> Self { - let is_yul = file.extension().is_some_and(|ext| ext == "yul"); - let mut version = None; - let mut experimental = None; - let mut imports = Vec::>::new(); - let mut libraries = Vec::new(); - let mut contract_names = Vec::new(); - let mut parse_result = Ok(()); - - let result = crate::parse_one_source(content, file, |ast| { - for item in ast.items.iter() { - let loc = item.span.lo().to_usize()..item.span.hi().to_usize(); - match &item.kind { - ast::ItemKind::Pragma(pragma) => match &pragma.tokens { - ast::PragmaTokens::Version(name, req) if name.name == sym::solidity => { - version = Some(Spanned::new(req.to_string(), loc)); - } - ast::PragmaTokens::Custom(name, value) - if name.as_str() == "experimental" => - { - let value = - value.as_ref().map(|v| v.as_str().to_string()).unwrap_or_default(); - experimental = Some(Spanned::new(value, loc)); - } - _ => {} - }, - - ast::ItemKind::Import(import) => { - let path = import.path.value.to_string(); - let aliases = match &import.items { - ast::ImportItems::Plain(None) => &[][..], - ast::ImportItems::Plain(Some(alias)) - | ast::ImportItems::Glob(alias) => &[(*alias, None)][..], - ast::ImportItems::Aliases(aliases) => aliases, - }; - let sol_import = SolImport::new(PathBuf::from(path)).set_aliases( - aliases - .iter() - .map(|(id, alias)| match alias { - Some(al) => SolImportAlias::Contract( - al.name.to_string(), - id.name.to_string(), - ), - None => SolImportAlias::File(id.name.to_string()), - }) - .collect(), - ); - imports.push(Spanned::new(sol_import, loc)); - } - - ast::ItemKind::Contract(contract) => { - if contract.kind.is_library() { - libraries.push(SolLibrary { is_inlined: library_is_inlined(contract) }); - } - contract_names.push(contract.name.to_string()); - } - - _ => {} - } - } - }); - if let Err(e) = result { - let e = e.to_string(); - trace!("failed parsing {file:?}: {e}"); - parse_result = Err(e); - - if version.is_none() { - version = utils::capture_outer_and_inner( - content, - &utils::RE_SOL_PRAGMA_VERSION, - &["version"], - ) - .first() - .map(|(cap, name)| Spanned::new(name.as_str().to_owned(), cap.range())); - } - if imports.is_empty() { - imports = capture_imports(content); - } - if contract_names.is_empty() { - utils::RE_CONTRACT_NAMES.captures_iter(content).for_each(|cap| { - contract_names.push(cap[1].to_owned()); - }); + match crate::parse_one_source(content, file, |sess, ast| { + SolDataBuilder::parse(content, file, Ok((sess, ast))) + }) { + Ok(data) => data, + Err(e) => { + let e = e.to_string(); + trace!("failed parsing {file:?}: {e}"); + SolDataBuilder::parse(content, file, Err(Some(e))) } } - let license = content.lines().next().and_then(|line| { - utils::capture_outer_and_inner( - line, - &utils::RE_SOL_SDPX_LICENSE_IDENTIFIER, - &["license"], - ) - .first() - .map(|(cap, l)| Spanned::new(l.as_str().to_owned(), cap.range())) - }); - let version_req = version.as_ref().and_then(|v| Self::parse_version_req(v.data()).ok()); + } - Self { - version_req, - version, - experimental, - imports, - license, - libraries, - contract_names, - is_yul, - parse_result, - } + pub(crate) fn parse_from( + sess: &solar_sema::interface::Session, + s: &solar_sema::Source<'_>, + ) -> Self { + let content = s.file.src.as_str(); + let file = s.file.name.as_real().unwrap(); + let ast = s.ast.as_ref().map(|ast| (sess, ast)).ok_or(None); + SolDataBuilder::parse(content, file, ast) } /// Parses the version pragma and returns the corresponding SemVer version requirement. @@ -169,7 +164,7 @@ impl SolData { // Somehow, Solidity semver without an operator is considered to be "exact", // but lack of operator automatically marks the operator as Caret, so we need // to manually patch it? :shrug: - let exact = !matches!(&version[0..1], "*" | "^" | "=" | ">" | "<" | "~"); + let exact = !matches!(version.get(..1), Some("*" | "^" | "=" | ">" | "<" | "~")); let mut version = VersionReq::parse(&version)?; if exact { version.comparators[0].op = semver::Op::Exact; @@ -179,6 +174,141 @@ impl SolData { } } +#[derive(Default)] +struct SolDataBuilder { + version: Option>, + experimental: Option>, + imports: Vec>, + libraries: Vec, + contract_names: Vec, + parse_err: Option, +} + +impl SolDataBuilder { + fn parse( + content: &str, + file: &Path, + ast: Result< + (&solar_sema::interface::Session, &solar_parse::ast::SourceUnit<'_>), + Option, + >, + ) -> SolData { + let mut builder = Self::default(); + match ast { + Ok((sess, ast)) => builder.parse_from_ast(sess, ast), + Err(err) => { + builder.parse_from_regex(content); + if let Some(err) = err { + builder.parse_err = Some(err); + } + } + } + builder.build(content, file) + } + + fn parse_from_ast( + &mut self, + sess: &solar_sema::interface::Session, + ast: &solar_parse::ast::SourceUnit<'_>, + ) { + for item in ast.items.iter() { + let loc = sess.source_map().span_to_source(item.span).unwrap().1; + match &item.kind { + ast::ItemKind::Pragma(pragma) => match &pragma.tokens { + ast::PragmaTokens::Version(name, req) if name.name == sym::solidity => { + self.version = Some(Spanned::new(req.to_string(), loc)); + } + ast::PragmaTokens::Custom(name, value) if name.as_str() == "experimental" => { + let value = + value.as_ref().map(|v| v.as_str().to_string()).unwrap_or_default(); + self.experimental = Some(Spanned::new(value, loc)); + } + _ => {} + }, + + ast::ItemKind::Import(import) => { + let path = import.path.value.to_string(); + let aliases = match &import.items { + ast::ImportItems::Plain(None) => &[][..], + ast::ImportItems::Plain(Some(alias)) | ast::ImportItems::Glob(alias) => { + &[(*alias, None)][..] + } + ast::ImportItems::Aliases(aliases) => aliases, + }; + let sol_import = SolImport::new(PathBuf::from(path)).set_aliases( + aliases + .iter() + .map(|(id, alias)| match alias { + Some(al) => SolImportAlias::Contract( + al.name.to_string(), + id.name.to_string(), + ), + None => SolImportAlias::File(id.name.to_string()), + }) + .collect(), + ); + self.imports.push(Spanned::new(sol_import, loc)); + } + + ast::ItemKind::Contract(contract) => { + if contract.kind.is_library() { + self.libraries + .push(SolLibrary { is_inlined: library_is_inlined(contract) }); + } + self.contract_names.push(contract.name.to_string()); + } + + _ => {} + } + } + } + + fn parse_from_regex(&mut self, content: &str) { + if self.version.is_none() { + self.version = utils::capture_outer_and_inner( + content, + &utils::RE_SOL_PRAGMA_VERSION, + &["version"], + ) + .first() + .map(|(cap, name)| Spanned::new(name.as_str().to_owned(), cap.range())); + } + if self.imports.is_empty() { + self.imports = capture_imports(content); + } + if self.contract_names.is_empty() { + utils::RE_CONTRACT_NAMES.captures_iter(content).for_each(|cap| { + self.contract_names.push(cap[1].to_owned()); + }); + } + } + + fn build(self, content: &str, file: &Path) -> SolData { + let Self { version, experimental, imports, libraries, contract_names, parse_err } = self; + let license = content.lines().next().and_then(|line| { + utils::capture_outer_and_inner( + line, + &utils::RE_SOL_SDPX_LICENSE_IDENTIFIER, + &["license"], + ) + .first() + .map(|(cap, l)| Spanned::new(l.as_str().to_owned(), cap.range())) + }); + let version_req = version.as_ref().and_then(|v| SolData::parse_version_req(v.data()).ok()); + SolData { + license, + version, + experimental, + imports, + version_req, + libraries, + contract_names, + is_yul: file.extension().is_some_and(|ext| ext == "yul"), + parse_result: parse_err.map(Err).unwrap_or(Ok(())), + } + } +} + #[derive(Clone, Debug)] pub struct SolImport { path: PathBuf, diff --git a/crates/compilers/src/resolver/tree.rs b/crates/compilers/src/resolver/tree.rs index ca73098..6ffdf1f 100644 --- a/crates/compilers/src/resolver/tree.rs +++ b/crates/compilers/src/resolver/tree.rs @@ -1,4 +1,4 @@ -use crate::{compilers::ParsedSource, Graph}; +use crate::{Graph, SourceParser}; use std::{collections::HashSet, io, io::Write, str::FromStr}; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] @@ -46,8 +46,8 @@ static UTF8_SYMBOLS: Symbols = Symbols { down: "│", tee: "├", ell: "└", ri static ASCII_SYMBOLS: Symbols = Symbols { down: "|", tee: "|", ell: "`", right: "-" }; -pub fn print( - graph: &Graph, +pub fn print( + graph: &Graph

, opts: &TreeOptions, out: &mut dyn Write, ) -> io::Result<()> { @@ -83,8 +83,8 @@ pub fn print( } #[allow(clippy::too_many_arguments)] -fn print_node( - graph: &Graph, +fn print_node( + graph: &Graph

, node_index: usize, symbols: &Symbols, no_dedupe: bool, @@ -137,8 +137,8 @@ fn print_node( /// Prints all the imports of a node #[allow(clippy::too_many_arguments)] -fn print_imports( - graph: &Graph, +fn print_imports( + graph: &Graph, node_index: usize, symbols: &Symbols, no_dedupe: bool, diff --git a/crates/compilers/tests/project.rs b/crates/compilers/tests/project.rs index fcae495..e5c28b1 100644 --- a/crates/compilers/tests/project.rs +++ b/crates/compilers/tests/project.rs @@ -5,16 +5,14 @@ use foundry_compilers::{ buildinfo::BuildInfo, cache::{CompilerCache, SOLIDITY_FILES_CACHE_FILENAME}, compilers::{ - multi::{ - MultiCompiler, MultiCompilerLanguage, MultiCompilerParsedSource, MultiCompilerSettings, - }, + multi::{MultiCompiler, MultiCompilerLanguage, MultiCompilerSettings}, solc::{Solc, SolcCompiler, SolcLanguage}, vyper::{Vyper, VyperLanguage, VyperSettings}, CompilerOutput, }, flatten::Flattener, info::ContractInfo, - multi::{MultiCompilerInput, MultiCompilerRestrictions}, + multi::{MultiCompilerInput, MultiCompilerParser, MultiCompilerRestrictions}, project::{Preprocessor, ProjectCompiler}, project_util::*, solc::{Restriction, SolcRestrictions, SolcSettings}, @@ -264,7 +262,7 @@ fn can_compile_dapp_detect_changes_in_libs() { ) .unwrap(); - let graph = Graph::::resolve(project.paths()).unwrap(); + let graph = Graph::::resolve(project.paths()).unwrap(); assert_eq!(graph.files().len(), 2); assert_eq!(graph.files().clone(), HashMap::from([(src, 0), (lib, 1),])); @@ -294,7 +292,7 @@ fn can_compile_dapp_detect_changes_in_libs() { ) .unwrap(); - let graph = Graph::::resolve(project.paths()).unwrap(); + let graph = Graph::::resolve(project.paths()).unwrap(); assert_eq!(graph.files().len(), 2); let compiled = project.compile().unwrap(); @@ -336,7 +334,7 @@ fn can_compile_dapp_detect_changes_in_sources() { ) .unwrap(); - let graph = Graph::::resolve(project.paths()).unwrap(); + let graph = Graph::::resolve(project.paths()).unwrap(); assert_eq!(graph.files().len(), 2); assert_eq!(graph.files().clone(), HashMap::from([(base, 0), (src, 1),])); assert_eq!(graph.imported_nodes(1).to_vec(), vec![0]); @@ -373,7 +371,7 @@ fn can_compile_dapp_detect_changes_in_sources() { ", ) .unwrap(); - let graph = Graph::::resolve(project.paths()).unwrap(); + let graph = Graph::::resolve(project.paths()).unwrap(); assert_eq!(graph.files().len(), 2); let compiled = project.compile().unwrap(); @@ -768,7 +766,6 @@ contract Contract { .unwrap(); let result = project.paths().clone().with_language::().flatten(target.as_path()); - assert!(result.is_err()); println!("{}", result.unwrap_err()); } @@ -3679,7 +3676,7 @@ fn can_add_basic_contract_and_library() { let lib = project.add_basic_source("Bar", "^0.8.0").unwrap(); - let graph = Graph::::resolve(project.paths()).unwrap(); + let graph = Graph::::resolve(project.paths()).unwrap(); assert_eq!(graph.files().len(), 2); assert!(graph.files().contains_key(&src)); assert!(graph.files().contains_key(&lib)); From b28f2f3836a42527ee65aab7f24d65c18e0fa250 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 25 Aug 2025 23:17:53 +0200 Subject: [PATCH 48/57] chore: release 0.19.0 (#306) --- CHANGELOG.md | 7 ++++++- Cargo.toml | 12 ++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9712f80..514e5a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.18.4](https://github.com/foundry-rs/compilers/releases/tag/v0.18.4) - 2025-08-25 +## [0.19.0](https://github.com/foundry-rs/compilers/releases/tag/v0.19.0) - 2025-08-25 ### Bug Fixes @@ -17,8 +17,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [deps] Bump to 0.18.3 ([#303](https://github.com/foundry-rs/compilers/issues/303)) - Update deps + fix clippy ([#297](https://github.com/foundry-rs/compilers/issues/297)) +### Features + +- Add `SourceParser` ([#300](https://github.com/foundry-rs/compilers/issues/300)) + ### Miscellaneous Tasks +- Release 0.18.4 ([#305](https://github.com/foundry-rs/compilers/issues/305)) - Use svm instead of manual svm dir logic ([#301](https://github.com/foundry-rs/compilers/issues/301)) - Add @0xrusowsky to `CODEOWNERS` ([#299](https://github.com/foundry-rs/compilers/issues/299)) - Update `CODEOWNERS` to improve visibility ([#298](https://github.com/foundry-rs/compilers/issues/298)) diff --git a/Cargo.toml b/Cargo.toml index ebc76d0..0b7582a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ resolver = "2" [workspace.package] authors = ["Foundry Maintainers"] -version = "0.18.4" +version = "0.19.0" rust-version = "1.88" readme = "README.md" license = "MIT OR Apache-2.0" @@ -36,11 +36,11 @@ redundant-lifetimes = "warn" all = "warn" [workspace.dependencies] -foundry-compilers = { path = "crates/compilers", version = "0.18.4" } -foundry-compilers-artifacts = { path = "crates/artifacts/artifacts", version = "0.18.4" } -foundry-compilers-artifacts-solc = { path = "crates/artifacts/solc", version = "0.18.4" } -foundry-compilers-artifacts-vyper = { path = "crates/artifacts/vyper", version = "0.18.4" } -foundry-compilers-core = { path = "crates/core", version = "0.18.4" } +foundry-compilers = { path = "crates/compilers", version = "0.19.0" } +foundry-compilers-artifacts = { path = "crates/artifacts/artifacts", version = "0.19.0" } +foundry-compilers-artifacts-solc = { path = "crates/artifacts/solc", version = "0.19.0" } +foundry-compilers-artifacts-vyper = { path = "crates/artifacts/vyper", version = "0.19.0" } +foundry-compilers-core = { path = "crates/core", version = "0.19.0" } alloy-json-abi = { version = "1.3", features = ["serde_json"] } alloy-primitives = { version = "1.3", features = ["serde", "rand"] } From 7638a8da4ebb3dd103b2c4907a1a49c50e06b5e1 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Tue, 26 Aug 2025 09:18:17 +0200 Subject: [PATCH 49/57] chore(deps): switch to solar meta crate (#307) --- Cargo.toml | 8 ++--- crates/compilers/Cargo.toml | 3 +- crates/compilers/src/cache/iface.rs | 6 ++-- crates/compilers/src/compilers/solc/mod.rs | 6 ++-- crates/compilers/src/lib.rs | 8 ++--- crates/compilers/src/resolver/parse.rs | 34 ++++++++++++---------- 6 files changed, 31 insertions(+), 34 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0b7582a..7c20dae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,8 +54,7 @@ semver = { version = "1.0", features = ["serde"] } serde = { version = "1", features = ["derive", "rc"] } serde_json = "1.0" similar-asserts = "1" -solar-parse = { version = "=0.1.6", default-features = false } -solar-sema = { version = "=0.1.6", default-features = false } +solar = { package = "solar-compiler", version = "=0.1.6", default-features = false } svm = { package = "svm-rs", version = "0.5", default-features = false } tempfile = "3.20" thiserror = "2" @@ -70,7 +69,4 @@ tokio = { version = "1.47", features = ["rt-multi-thread"] } snapbox = "0.6.21" [patch.crates-io] -# solar-parse = { git = "https://github.com/paradigmxyz/solar", branch = "main" } -# solar-sema = { git = "https://github.com/paradigmxyz/solar", branch = "main" } -# solar-ast = { git = "https://github.com/paradigmxyz/solar", branch = "main" } -# solar-interface = { git = "https://github.com/paradigmxyz/solar", branch = "main" } +# solar = { package = "solar-compiler", git = "https://github.com/paradigmxyz/solar", branch = "main" } diff --git a/crates/compilers/Cargo.toml b/crates/compilers/Cargo.toml index dca0c15..62ef396 100644 --- a/crates/compilers/Cargo.toml +++ b/crates/compilers/Cargo.toml @@ -31,8 +31,7 @@ rayon.workspace = true thiserror.workspace = true path-slash.workspace = true yansi.workspace = true -solar-parse.workspace = true -solar-sema.workspace = true +solar.workspace = true futures-util = { workspace = true, optional = true } tokio = { workspace = true, optional = true } diff --git a/crates/compilers/src/cache/iface.rs b/crates/compilers/src/cache/iface.rs index aea2e43..1c49540 100644 --- a/crates/compilers/src/cache/iface.rs +++ b/crates/compilers/src/cache/iface.rs @@ -1,5 +1,5 @@ use crate::{parse_one_source, replace_source_content}; -use solar_parse::{ +use solar::parse::{ ast::{self, Span}, interface::diagnostics::EmittedDiagnostics, }; @@ -21,8 +21,8 @@ pub(crate) fn interface_repr(content: &str, path: &Path) -> Result, + sess: &solar::sema::interface::Session, + ast: &solar::parse::ast::SourceUnit<'_>, ) -> String { let mut spans_to_remove: Vec = Vec::new(); for item in ast.items.iter() { diff --git a/crates/compilers/src/compilers/solc/mod.rs b/crates/compilers/src/compilers/solc/mod.rs index ee31cf3..6d6db9e 100644 --- a/crates/compilers/src/compilers/solc/mod.rs +++ b/crates/compilers/src/compilers/solc/mod.rs @@ -369,14 +369,14 @@ impl SourceParser for SolParser { fn new(config: &crate::ProjectPathsConfig) -> Self { Self { - compiler: solar_sema::Compiler::new(Self::session_with_opts( - solar_sema::interface::config::Opts { + compiler: solar::sema::Compiler::new(Self::session_with_opts( + solar::sema::interface::config::Opts { include_paths: config.include_paths.iter().cloned().collect(), base_path: Some(config.root.clone()), import_remappings: config .remappings .iter() - .map(|r| solar_sema::interface::config::ImportRemapping { + .map(|r| solar::sema::interface::config::ImportRemapping { context: r.context.clone().unwrap_or_default(), prefix: r.name.clone(), path: r.path.clone(), diff --git a/crates/compilers/src/lib.rs b/crates/compilers/src/lib.rs index ad0ee4b..bac57fa 100644 --- a/crates/compilers/src/lib.rs +++ b/crates/compilers/src/lib.rs @@ -64,7 +64,7 @@ use foundry_compilers_core::error::{Result, SolcError, SolcIoError}; use output::sources::{VersionedSourceFile, VersionedSourceFiles}; use project::ProjectCompiler; use semver::Version; -use solar_parse::{ +use solar::parse::{ interface::{diagnostics::EmittedDiagnostics, source_map::FileName, Session}, Parser, }; @@ -925,11 +925,11 @@ pub fn replace_source_content( pub(crate) fn parse_one_source( content: &str, path: &Path, - f: impl FnOnce(&Session, &solar_parse::ast::SourceUnit<'_>) -> R, + f: impl FnOnce(&Session, &solar::parse::ast::SourceUnit<'_>) -> R, ) -> Result { let sess = Session::builder().with_buffer_emitter(Default::default()).build(); - let res = sess.enter_sequential(|| -> solar_parse::interface::Result<_> { - let arena = solar_parse::ast::Arena::new(); + let res = sess.enter_sequential(|| -> solar::parse::interface::Result<_> { + let arena = solar::parse::ast::Arena::new(); let filename = FileName::Real(path.to_path_buf()); let mut parser = Parser::from_source_code(&sess, &arena, filename, content.to_string())?; let ast = parser.parse_file().map_err(|e| e.emit())?; diff --git a/crates/compilers/src/resolver/parse.rs b/crates/compilers/src/resolver/parse.rs index 26baf15..2bb0189 100644 --- a/crates/compilers/src/resolver/parse.rs +++ b/crates/compilers/src/resolver/parse.rs @@ -1,7 +1,9 @@ use foundry_compilers_core::utils; use semver::VersionReq; -use solar_parse::{ast, interface::sym}; -use solar_sema::interface; +use solar::{ + parse::{ast, interface::sym}, + sema::interface, +}; use std::{ ops::Range, path::{Path, PathBuf}, @@ -9,7 +11,7 @@ use std::{ /// Solidity parser. /// -/// Holds a [`solar_sema::Compiler`] that is used to parse sources incrementally. +/// Holds a [`solar::sema::Compiler`] that is used to parse sources incrementally. /// After project compilation ([`Graph::resolve`]), this will contain all sources parsed by /// [`Graph`]. /// @@ -20,13 +22,13 @@ use std::{ #[derive(derive_more::Debug)] pub struct SolParser { #[debug(ignore)] - pub(crate) compiler: solar_sema::Compiler, + pub(crate) compiler: solar::sema::Compiler, } impl Clone for SolParser { fn clone(&self) -> Self { Self { - compiler: solar_sema::Compiler::new(Self::session_with_opts( + compiler: solar::sema::Compiler::new(Self::session_with_opts( self.compiler.sess().opts.clone(), )), } @@ -35,24 +37,24 @@ impl Clone for SolParser { impl SolParser { /// Returns a reference to the compiler. - pub fn compiler(&self) -> &solar_sema::Compiler { + pub fn compiler(&self) -> &solar::sema::Compiler { &self.compiler } /// Returns a mutable reference to the compiler. - pub fn compiler_mut(&mut self) -> &mut solar_sema::Compiler { + pub fn compiler_mut(&mut self) -> &mut solar::sema::Compiler { &mut self.compiler } /// Consumes the parser and returns the compiler. - pub fn into_compiler(self) -> solar_sema::Compiler { + pub fn into_compiler(self) -> solar::sema::Compiler { self.compiler } pub(crate) fn session_with_opts( - opts: solar_sema::interface::config::Opts, - ) -> solar_sema::interface::Session { - let sess = solar_sema::interface::Session::builder() + opts: solar::sema::interface::config::Opts, + ) -> solar::sema::interface::Session { + let sess = solar::sema::interface::Session::builder() .with_buffer_emitter(Default::default()) .opts(opts) .build(); @@ -137,8 +139,8 @@ impl SolData { } pub(crate) fn parse_from( - sess: &solar_sema::interface::Session, - s: &solar_sema::Source<'_>, + sess: &solar::sema::interface::Session, + s: &solar::sema::Source<'_>, ) -> Self { let content = s.file.src.as_str(); let file = s.file.name.as_real().unwrap(); @@ -189,7 +191,7 @@ impl SolDataBuilder { content: &str, file: &Path, ast: Result< - (&solar_sema::interface::Session, &solar_parse::ast::SourceUnit<'_>), + (&solar::sema::interface::Session, &solar::parse::ast::SourceUnit<'_>), Option, >, ) -> SolData { @@ -208,8 +210,8 @@ impl SolDataBuilder { fn parse_from_ast( &mut self, - sess: &solar_sema::interface::Session, - ast: &solar_parse::ast::SourceUnit<'_>, + sess: &solar::sema::interface::Session, + ast: &solar::parse::ast::SourceUnit<'_>, ) { for item in ast.items.iter() { let loc = sess.source_map().span_to_source(item.span).unwrap().1; From bd9b7c9ded4f3f324c1dd216cbb1e1a139ec5894 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Fri, 29 Aug 2025 16:22:19 +0200 Subject: [PATCH 50/57] fix: sanitize `stopAfter` (#309) Introduced in 0.7.3: https://github.com/ethereum/solidity/releases/tag/v0.7.3 --- crates/artifacts/solc/src/lib.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/artifacts/solc/src/lib.rs b/crates/artifacts/solc/src/lib.rs index a5e2b27..d632f15 100644 --- a/crates/artifacts/solc/src/lib.rs +++ b/crates/artifacts/solc/src/lib.rs @@ -230,7 +230,8 @@ impl From for StandardJsonCompilerInput { #[serde(rename_all = "camelCase")] pub struct Settings { /// Stop compilation after the given stage. - /// since 0.8.11: only "parsing" is valid here + /// + /// Since 0.7.3. Only "parsing" is valid here. #[serde(default, skip_serializing_if = "Option::is_none")] pub stop_after: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] @@ -297,6 +298,11 @@ impl Settings { self.debug = None; } + if *version < Version::new(0, 7, 3) { + // introduced in 0.7.3 + self.stop_after = None; + } + if *version < Version::new(0, 7, 5) { // introduced in 0.7.5 self.via_ir = None; From e07c391ee00c99652c2cc5efc12c5ecc9ba1f1e4 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Fri, 29 Aug 2025 17:25:25 +0200 Subject: [PATCH 51/57] chore: use FnMut instead of FnOnce + Copy (#310) FnOnce + Copy doesn't really make sense. --- crates/compilers/src/compilers/mod.rs | 2 +- crates/compilers/src/compilers/multi.rs | 4 ++-- crates/compilers/src/compilers/solc/mod.rs | 4 ++-- crates/compilers/src/compilers/vyper/settings.rs | 4 ++-- crates/compilers/src/lib.rs | 6 +++--- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/crates/compilers/src/compilers/mod.rs b/crates/compilers/src/compilers/mod.rs index 565ffbf..29e1ca7 100644 --- a/crates/compilers/src/compilers/mod.rs +++ b/crates/compilers/src/compilers/mod.rs @@ -75,7 +75,7 @@ pub trait CompilerSettings: type Restrictions: CompilerSettingsRestrictions; /// Executes given fn with mutable reference to configured [OutputSelection]. - fn update_output_selection(&mut self, f: impl FnOnce(&mut OutputSelection) + Copy); + fn update_output_selection(&mut self, f: impl FnMut(&mut OutputSelection)); /// Returns true if artifacts compiled with given `other` config are compatible with this /// config and if compilation can be skipped. diff --git a/crates/compilers/src/compilers/multi.rs b/crates/compilers/src/compilers/multi.rs index 3f5ef53..0953349 100644 --- a/crates/compilers/src/compilers/multi.rs +++ b/crates/compilers/src/compilers/multi.rs @@ -204,8 +204,8 @@ impl CompilerSettings for MultiCompilerSettings { self.solc.can_use_cached(&other.solc) && self.vyper.can_use_cached(&other.vyper) } - fn update_output_selection(&mut self, f: impl FnOnce(&mut OutputSelection) + Copy) { - self.solc.update_output_selection(f); + fn update_output_selection(&mut self, mut f: impl FnMut(&mut OutputSelection)) { + self.solc.update_output_selection(&mut f); self.vyper.update_output_selection(f); } diff --git a/crates/compilers/src/compilers/solc/mod.rs b/crates/compilers/src/compilers/solc/mod.rs index 6d6db9e..bf7792e 100644 --- a/crates/compilers/src/compilers/solc/mod.rs +++ b/crates/compilers/src/compilers/solc/mod.rs @@ -288,8 +288,8 @@ impl CompilerSettingsRestrictions for SolcRestrictions { impl CompilerSettings for SolcSettings { type Restrictions = SolcRestrictions; - fn update_output_selection(&mut self, f: impl FnOnce(&mut OutputSelection) + Copy) { - f(&mut self.settings.output_selection) + fn update_output_selection(&mut self, mut f: impl FnMut(&mut OutputSelection)) { + f(&mut self.settings.output_selection); } fn can_use_cached(&self, other: &Self) -> bool { diff --git a/crates/compilers/src/compilers/vyper/settings.rs b/crates/compilers/src/compilers/vyper/settings.rs index 2a815d6..0a4b0f5 100644 --- a/crates/compilers/src/compilers/vyper/settings.rs +++ b/crates/compilers/src/compilers/vyper/settings.rs @@ -21,8 +21,8 @@ impl CompilerSettingsRestrictions for VyperRestrictions { impl CompilerSettings for VyperSettings { type Restrictions = VyperRestrictions; - fn update_output_selection(&mut self, f: impl FnOnce(&mut OutputSelection)) { - f(&mut self.output_selection) + fn update_output_selection(&mut self, mut f: impl FnMut(&mut OutputSelection)) { + f(&mut self.output_selection); } fn can_use_cached(&self, other: &Self) -> bool { diff --git a/crates/compilers/src/lib.rs b/crates/compilers/src/lib.rs index bac57fa..ae7d970 100644 --- a/crates/compilers/src/lib.rs +++ b/crates/compilers/src/lib.rs @@ -430,10 +430,10 @@ impl, C: Compiler> Pro /// Invokes [CompilerSettings::update_output_selection] on the project's settings and all /// additional settings profiles. - pub fn update_output_selection(&mut self, f: impl FnOnce(&mut OutputSelection) + Copy) { - self.settings.update_output_selection(f); + pub fn update_output_selection(&mut self, mut f: impl FnMut(&mut OutputSelection)) { + self.settings.update_output_selection(&mut f); self.additional_settings.iter_mut().for_each(|(_, s)| { - s.update_output_selection(f); + s.update_output_selection(&mut f); }); } } From 0073f15878c2658ffd483d6b3089002200168613 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Fri, 29 Aug 2025 18:12:00 +0200 Subject: [PATCH 52/57] refactor: cache/is_dirty impls (#311) Making more readable and improving tracing. No functional changes. --- crates/compilers/src/cache.rs | 156 ++++++++++++++++++---------------- 1 file changed, 85 insertions(+), 71 deletions(-) diff --git a/crates/compilers/src/cache.rs b/crates/compilers/src/cache.rs index 90e50d8..5cd8411 100644 --- a/crates/compilers/src/cache.rs +++ b/crates/compilers/src/cache.rs @@ -53,6 +53,7 @@ pub struct CompilerCache { } impl CompilerCache { + /// Creates a new empty cache. pub fn new(format: String, paths: ProjectPaths, preprocessed: bool) -> Self { Self { format, @@ -118,11 +119,10 @@ impl CompilerCache { /// cache.join_artifacts_files(project.artifacts_path()); /// # Ok::<_, Box>(()) /// ``` - #[instrument(name = "CompilerCache::read", skip_all)] + #[instrument(name = "CompilerCache::read", err)] pub fn read(path: &Path) -> Result { - trace!("reading solfiles cache at {}", path.display()); let cache: Self = utils::read_json_file(path)?; - trace!("read cache \"{}\" with {} entries", cache.format, cache.files.len()); + trace!(cache.format, cache.files = cache.files.len(), "read cache"); Ok(cache) } @@ -787,43 +787,51 @@ impl, C: Compiler> } /// Returns whether we are missing artifacts for the given file and version. - #[instrument(level = "trace", skip(self))] fn is_missing_artifacts(&self, file: &Path, version: &Version, profile: &str) -> bool { + self.is_missing_artifacts_impl(file, version, profile).is_err() + } + + /// Returns whether we are missing artifacts for the given file and version. + #[instrument(level = "trace", name = "is_missing_artifacts", skip(self), ret)] + fn is_missing_artifacts_impl( + &self, + file: &Path, + version: &Version, + profile: &str, + ) -> Result<(), &'static str> { let Some(entry) = self.cache.entry(file) else { - trace!("missing cache entry"); - return true; + return Err("missing cache entry"); }; // only check artifact's existence if the file generated artifacts. // e.g. a solidity file consisting only of import statements (like interfaces that // re-export) do not create artifacts if entry.seen_by_compiler && entry.artifacts.is_empty() { - trace!("no artifacts"); - return false; + return Ok(()); } if !entry.contains(version, profile) { - trace!("missing linked artifacts"); - return true; + return Err("missing linked artifacts"); } - if entry.artifacts_for_version(version).any(|artifact| { - let missing_artifact = !self.cached_artifacts.has_artifact(&artifact.path); - if missing_artifact { - trace!("missing artifact \"{}\"", artifact.path.display()); - } - missing_artifact - }) { - return true; + if entry + .artifacts_for_version(version) + .any(|artifact| !self.cached_artifacts.has_artifact(&artifact.path)) + { + return Err("missing artifact"); } // If any requested extra files are missing for any artifact, mark source as dirty to // generate them - self.missing_extra_files() + if self.missing_extra_files() { + return Err("missing extra files"); + } + + Ok(()) } // Walks over all cache entries, detects dirty files and removes them from cache. - fn find_and_remove_dirty(&mut self) { + fn remove_dirty_sources(&mut self) { fn populate_dirty_files( file: &Path, dirty_files: &mut HashSet, @@ -839,41 +847,7 @@ impl, C: Compiler> } } - let existing_profiles = self.project.settings_profiles().collect::>(); - - let mut dirty_profiles = HashSet::new(); - for (profile, settings) in &self.cache.profiles { - if !existing_profiles.get(profile.as_str()).is_some_and(|p| p.can_use_cached(settings)) - { - trace!("dirty profile: {}", profile); - dirty_profiles.insert(profile.clone()); - } - } - - for profile in &dirty_profiles { - self.cache.profiles.remove(profile); - } - - self.cache.files.retain(|_, entry| { - // keep entries which already had no artifacts - if entry.artifacts.is_empty() { - return true; - } - entry.artifacts.retain(|_, artifacts| { - artifacts.retain(|_, artifacts| { - artifacts.retain(|profile, _| !dirty_profiles.contains(profile)); - !artifacts.is_empty() - }); - !artifacts.is_empty() - }); - !entry.artifacts.is_empty() - }); - - for (profile, settings) in existing_profiles { - if !self.cache.profiles.contains_key(profile) { - self.cache.profiles.insert(profile.to_string(), settings.clone()); - } - } + self.update_profiles(); // Iterate over existing cache entries. let files = self.cache.files.keys().cloned().collect::>(); @@ -899,7 +873,7 @@ impl, C: Compiler> // Pre-add all sources that are guaranteed to be dirty for file in sources.keys() { - if self.is_dirty_impl(file, false) { + if self.is_dirty(file, false) { self.dirty_sources.insert(file.clone()); } } @@ -928,7 +902,7 @@ impl, C: Compiler> } else if !is_src && self.dirty_sources.contains(import) && (!self.is_source_file(import) - || self.is_dirty_impl(import, true) + || self.is_dirty(import, true) || self.cache.mocks.contains(file)) { if self.cache.mocks.contains(file) { @@ -953,36 +927,76 @@ impl, C: Compiler> } } - fn is_dirty_impl(&self, file: &Path, use_interface_repr: bool) -> bool { + /// Updates the profiles in the cache, removing those which are dirty alongside their artifacts. + fn update_profiles(&mut self) { + let existing_profiles = self.project.settings_profiles().collect::>(); + + let mut dirty_profiles = HashSet::new(); + for (profile, settings) in &self.cache.profiles { + if !existing_profiles.get(profile.as_str()).is_some_and(|p| p.can_use_cached(settings)) + { + dirty_profiles.insert(profile.clone()); + } + } + + for profile in &dirty_profiles { + trace!(profile, "removing dirty profile and artifacts"); + self.cache.profiles.remove(profile); + } + + for (profile, settings) in existing_profiles { + if !self.cache.profiles.contains_key(profile) { + trace!(profile, "adding new profile"); + self.cache.profiles.insert(profile.to_string(), settings.clone()); + } + } + + self.cache.files.retain(|_, entry| { + // keep entries which already had no artifacts + if entry.artifacts.is_empty() { + return true; + } + entry.artifacts.retain(|_, artifacts| { + artifacts.retain(|_, artifacts| { + artifacts.retain(|profile, _| !dirty_profiles.contains(profile)); + !artifacts.is_empty() + }); + !artifacts.is_empty() + }); + !entry.artifacts.is_empty() + }); + } + + fn is_dirty(&self, file: &Path, use_interface_repr: bool) -> bool { + self.is_dirty_impl(file, use_interface_repr).is_err() + } + + #[instrument(level = "trace", name = "is_dirty", skip(self), ret)] + fn is_dirty_impl(&self, file: &Path, use_interface_repr: bool) -> Result<(), &'static str> { let Some(entry) = self.cache.entry(file) else { - trace!("missing cache entry"); - return true; + return Err("missing cache entry"); }; if use_interface_repr && self.cache.preprocessed { let Some(interface_hash) = self.interface_repr_hashes.get(file) else { - trace!("missing interface hash"); - return true; + return Err("missing interface hash"); }; if entry.interface_repr_hash.as_ref() != Some(interface_hash) { - trace!("interface hash changed"); - return true; - }; + return Err("interface hash changed"); + } } else { let Some(content_hash) = self.content_hashes.get(file) else { - trace!("missing content hash"); - return true; + return Err("missing content hash"); }; if entry.content_hash != *content_hash { - trace!("content hash changed"); - return true; + return Err("content hash changed"); } } // all things match, can be reused - false + Ok(()) } /// Adds the file's hashes to the set if not set yet @@ -1155,7 +1169,7 @@ impl<'a, T: ArtifactOutput, C: Compiler> pub fn remove_dirty_sources(&mut self) { match self { ArtifactsCache::Ephemeral(..) => {} - ArtifactsCache::Cached(cache) => cache.find_and_remove_dirty(), + ArtifactsCache::Cached(cache) => cache.remove_dirty_sources(), } } From 353e85ace5b3e4149734d252789ce7303b6a3bd0 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Fri, 29 Aug 2025 18:42:37 +0200 Subject: [PATCH 53/57] fix: make sources' paths absolute (#312) Cache entries are keyed by absolute path relative to project root, but it's possible to construct and pass in Sources from a relative path, such as CLI argument in `forge lint`/`forge eip712`... In these cases the path will be relative and will always be a cache miss. This includes the path's imports, which get resolved as relative to the initial path rather than root now with Solar. Make all paths in Sources absolute when constructing Graph so that this change propagates through the entire compilation process. I couldn't find a reproducer in today's stable/nightly Foundry, however making `config.solar_project()` use `project()` instead of `ephemeral_project()` will reproduce the behavior explained above. With this change, `solar_project` will almost never call to solc. --- crates/artifacts/solc/src/sources.rs | 16 +++++++++++----- crates/compilers/src/config.rs | 2 +- crates/compilers/src/flatten.rs | 2 +- crates/compilers/src/resolver/mod.rs | 9 ++++++--- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/crates/artifacts/solc/src/sources.rs b/crates/artifacts/solc/src/sources.rs index b8ac1c8..2b929ad 100644 --- a/crates/artifacts/solc/src/sources.rs +++ b/crates/artifacts/solc/src/sources.rs @@ -22,6 +22,14 @@ impl Sources { Self::default() } + /// Joins all paths relative to `root`. + pub fn make_absolute(&mut self, root: &Path) { + self.0 = std::mem::take(&mut self.0) + .into_iter() + .map(|(path, source)| (root.join(path), source)) + .collect(); + } + /// Returns `true` if no sources should have optimized output selection. pub fn all_dirty(&self) -> bool { self.0.values().all(|s| s.is_dirty()) @@ -169,7 +177,7 @@ impl Source { /// Recursively finds all source files under the given dir path and reads them all #[cfg(feature = "walkdir")] pub fn read_all_from(dir: &Path, extensions: &[&str]) -> Result { - Self::read_all_files(utils::source_files(dir, extensions)) + Self::read_all(utils::source_files_iter(dir, extensions)) } /// Recursively finds all solidity and yul files under the given dir path and reads them all @@ -178,14 +186,12 @@ impl Source { Self::read_all_from(dir, utils::SOLC_EXTENSIONS) } - /// Reads all source files of the given vec - /// - /// Depending on the len of the vec it will try to read the files in parallel + /// Reads all source files of the given list. pub fn read_all_files(files: Vec) -> Result { Self::read_all(files) } - /// Reads all files + /// Reads all of the given files. #[instrument(name = "Source::read_all", skip_all)] pub fn read_all(files: I) -> Result where diff --git a/crates/compilers/src/config.rs b/crates/compilers/src/config.rs index 3f3b5f4..326bd32 100644 --- a/crates/compilers/src/config.rs +++ b/crates/compilers/src/config.rs @@ -616,7 +616,7 @@ impl ProjectPathsConfig { /// Returns the combined set of `Self::read_sources` + `Self::read_tests` + `Self::read_scripts` pub fn read_input_files(&self) -> Result { - Ok(Source::read_all_files(self.input_files())?) + Ok(Source::read_all(self.input_files_iter())?) } } diff --git a/crates/compilers/src/flatten.rs b/crates/compilers/src/flatten.rs index ce34243..5ec755d 100644 --- a/crates/compilers/src/flatten.rs +++ b/crates/compilers/src/flatten.rs @@ -209,7 +209,7 @@ impl Flattener { let output = output.compiler_output; - let sources = Source::read_all_files(vec![target.to_path_buf()])?; + let sources = Source::read_all([target.to_path_buf()])?; let graph = Graph::::resolve_sources(&project.paths, sources)?; let ordered_sources = collect_ordered_deps(target, &project.paths, &graph)?; diff --git a/crates/compilers/src/resolver/mod.rs b/crates/compilers/src/resolver/mod.rs index feb4abc..4a9cc85 100644 --- a/crates/compilers/src/resolver/mod.rs +++ b/crates/compilers/src/resolver/mod.rs @@ -400,6 +400,9 @@ impl Graph

{ Ok(()) } + // The cache relies on the absolute paths relative to the project root as cache keys. + sources.make_absolute(&paths.root); + let mut parser = P::new(paths.with_language_ref()); // we start off by reading all input files, which includes all solidity files from the @@ -566,14 +569,14 @@ impl Graph

{ let mut versioned_sources = Vec::with_capacity(versioned_nodes.len()); for (version, profile_to_nodes) in versioned_nodes { - for (profile_idx, input_node_indixies) in profile_to_nodes { + for (profile_idx, input_node_indexes) in profile_to_nodes { let mut sources = Sources::new(); // all input nodes will be processed - let mut processed_sources = input_node_indixies.iter().copied().collect(); + let mut processed_sources = input_node_indexes.iter().copied().collect(); // we only process input nodes (from sources, tests for example) - for idx in input_node_indixies { + for idx in input_node_indexes { // insert the input node in the sources set and remove it from the available // set let (path, source) = From 0a1c4958f81bebbc2895df8038ae38aefff50649 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Fri, 29 Aug 2025 19:05:56 +0200 Subject: [PATCH 54/57] chore: release 0.19.1 (#313) --- CHANGELOG.md | 11 ++++++++++- Cargo.toml | 12 ++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 514e5a6..7a4bc58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,15 +5,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.19.0](https://github.com/foundry-rs/compilers/releases/tag/v0.19.0) - 2025-08-25 +## [0.19.1](https://github.com/foundry-rs/compilers/releases/tag/v0.19.1) - 2025-08-29 ### Bug Fixes +- Make sources' paths absolute ([#312](https://github.com/foundry-rs/compilers/issues/312)) +- Sanitize `stopAfter` ([#309](https://github.com/foundry-rs/compilers/issues/309)) - Remove superfluous assertion ([#304](https://github.com/foundry-rs/compilers/issues/304)) - [flatten] Sort by loc path and loc start ([#302](https://github.com/foundry-rs/compilers/issues/302)) ### Dependencies +- [deps] Switch to solar meta crate ([#307](https://github.com/foundry-rs/compilers/issues/307)) - [deps] Bump to 0.18.3 ([#303](https://github.com/foundry-rs/compilers/issues/303)) - Update deps + fix clippy ([#297](https://github.com/foundry-rs/compilers/issues/297)) @@ -23,11 +26,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Miscellaneous Tasks +- Use FnMut instead of FnOnce + Copy ([#310](https://github.com/foundry-rs/compilers/issues/310)) +- Release 0.19.0 ([#306](https://github.com/foundry-rs/compilers/issues/306)) - Release 0.18.4 ([#305](https://github.com/foundry-rs/compilers/issues/305)) - Use svm instead of manual svm dir logic ([#301](https://github.com/foundry-rs/compilers/issues/301)) - Add @0xrusowsky to `CODEOWNERS` ([#299](https://github.com/foundry-rs/compilers/issues/299)) - Update `CODEOWNERS` to improve visibility ([#298](https://github.com/foundry-rs/compilers/issues/298)) +### Refactor + +- Cache/is_dirty impls ([#311](https://github.com/foundry-rs/compilers/issues/311)) + ## [0.18.2](https://github.com/foundry-rs/compilers/releases/tag/v0.18.2) - 2025-08-01 ### Bug Fixes diff --git a/Cargo.toml b/Cargo.toml index 7c20dae..93b0121 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ resolver = "2" [workspace.package] authors = ["Foundry Maintainers"] -version = "0.19.0" +version = "0.19.1" rust-version = "1.88" readme = "README.md" license = "MIT OR Apache-2.0" @@ -36,11 +36,11 @@ redundant-lifetimes = "warn" all = "warn" [workspace.dependencies] -foundry-compilers = { path = "crates/compilers", version = "0.19.0" } -foundry-compilers-artifacts = { path = "crates/artifacts/artifacts", version = "0.19.0" } -foundry-compilers-artifacts-solc = { path = "crates/artifacts/solc", version = "0.19.0" } -foundry-compilers-artifacts-vyper = { path = "crates/artifacts/vyper", version = "0.19.0" } -foundry-compilers-core = { path = "crates/core", version = "0.19.0" } +foundry-compilers = { path = "crates/compilers", version = "0.19.1" } +foundry-compilers-artifacts = { path = "crates/artifacts/artifacts", version = "0.19.1" } +foundry-compilers-artifacts-solc = { path = "crates/artifacts/solc", version = "0.19.1" } +foundry-compilers-artifacts-vyper = { path = "crates/artifacts/vyper", version = "0.19.1" } +foundry-compilers-core = { path = "crates/core", version = "0.19.1" } alloy-json-abi = { version = "1.3", features = ["serde_json"] } alloy-primitives = { version = "1.3", features = ["serde", "rand"] } From 51ca0d286e517cdb536a4cdde00e8358f0ba8cad Mon Sep 17 00:00:00 2001 From: cdrappi Date: Fri, 12 Sep 2025 12:27:21 -0400 Subject: [PATCH 55/57] upstream merge through commit 0a1c495 (0.19.1) --- .github/CODEOWNERS | 4 + .github/workflows/ci.yml | 16 +- CHANGELOG.md | 171 +++++++++ Cargo.toml | 39 +- README.md | 9 +- benches/read_all.rs | 4 +- cliff.toml | 2 +- clippy.toml | 2 +- crates/artifacts/solc/Cargo.toml | 2 +- crates/artifacts/solc/src/ast/lowfidelity.rs | 3 + crates/artifacts/solc/src/ast/misc.rs | 1 + crates/artifacts/solc/src/ast/mod.rs | 20 +- crates/artifacts/solc/src/bytecode.rs | 22 ++ crates/artifacts/solc/src/lib.rs | 22 +- crates/artifacts/solc/src/output_selection.rs | 6 +- crates/artifacts/solc/src/remappings/mod.rs | 41 ++- crates/artifacts/solc/src/sources.rs | 47 ++- crates/artifacts/vyper/src/settings.rs | 5 +- crates/compilers/Cargo.toml | 17 +- .../src/artifact_output/configurable.rs | 3 - crates/compilers/src/artifact_output/mod.rs | 25 +- crates/compilers/src/cache.rs | 209 ++++++----- crates/compilers/src/cache/iface.rs | 15 +- crates/compilers/src/compile/output/mod.rs | 21 +- crates/compilers/src/compile/project.rs | 13 +- crates/compilers/src/compilers/mod.rs | 65 +++- crates/compilers/src/compilers/multi.rs | 142 +++++++- .../compilers/src/compilers/solc/compiler.rs | 122 ++++--- crates/compilers/src/compilers/solc/mod.rs | 108 +++++- crates/compilers/src/compilers/vyper/mod.rs | 11 +- .../compilers/src/compilers/vyper/parser.rs | 16 +- .../compilers/src/compilers/vyper/settings.rs | 4 +- crates/compilers/src/config.rs | 84 +++-- crates/compilers/src/filter.rs | 6 +- crates/compilers/src/flatten.rs | 26 +- crates/compilers/src/lib.rs | 27 +- crates/compilers/src/project_util/mock.rs | 78 ++-- crates/compilers/src/resolver/mod.rs | 284 ++++++++------- crates/compilers/src/resolver/parse.rs | 343 ++++++++++++------ crates/compilers/src/resolver/tree.rs | 14 +- crates/compilers/tests/project.rs | 27 +- 41 files changed, 1417 insertions(+), 659 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0bdaff5..a489dd4 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1,5 @@ +<<<<<<< HEAD * @sfyll +======= +* @danipopes @klkvr @mattsse @grandizzy @yash-atreya @zerosnacks @onbjerg @0xrusowsky +>>>>>>> 0a1c4958f81bebbc2895df8038ae38aefff50649 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0da36e8..6dd698b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,14 +22,14 @@ jobs: fail-fast: false matrix: os: ["ubuntu-latest", "macos-latest", "windows-latest"] - rust: ["stable", "1.86"] + rust: ["stable", "1.88"] flags: ["", "--all-features"] exclude: # Skip because some features have higher MSRV. - - rust: "1.86" # MSRV + - rust: "1.88" # MSRV flags: "--all-features" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.rust }} @@ -49,7 +49,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 with: @@ -60,7 +60,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable - uses: taiki-e/install-action@cargo-hack - uses: Swatinem/rust-cache@v2 @@ -73,7 +73,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable with: components: clippy @@ -88,7 +88,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@nightly - uses: Swatinem/rust-cache@v2 with: @@ -101,7 +101,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@nightly with: components: rustfmt diff --git a/CHANGELOG.md b/CHANGELOG.md index 94f65b9..7a4bc58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,183 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.19.1](https://github.com/foundry-rs/compilers/releases/tag/v0.19.1) - 2025-08-29 + +### Bug Fixes + +- Make sources' paths absolute ([#312](https://github.com/foundry-rs/compilers/issues/312)) +- Sanitize `stopAfter` ([#309](https://github.com/foundry-rs/compilers/issues/309)) +- Remove superfluous assertion ([#304](https://github.com/foundry-rs/compilers/issues/304)) +- [flatten] Sort by loc path and loc start ([#302](https://github.com/foundry-rs/compilers/issues/302)) + +### Dependencies + +- [deps] Switch to solar meta crate ([#307](https://github.com/foundry-rs/compilers/issues/307)) +- [deps] Bump to 0.18.3 ([#303](https://github.com/foundry-rs/compilers/issues/303)) +- Update deps + fix clippy ([#297](https://github.com/foundry-rs/compilers/issues/297)) + +### Features + +- Add `SourceParser` ([#300](https://github.com/foundry-rs/compilers/issues/300)) + +### Miscellaneous Tasks + +- Use FnMut instead of FnOnce + Copy ([#310](https://github.com/foundry-rs/compilers/issues/310)) +- Release 0.19.0 ([#306](https://github.com/foundry-rs/compilers/issues/306)) +- Release 0.18.4 ([#305](https://github.com/foundry-rs/compilers/issues/305)) +- Use svm instead of manual svm dir logic ([#301](https://github.com/foundry-rs/compilers/issues/301)) +- Add @0xrusowsky to `CODEOWNERS` ([#299](https://github.com/foundry-rs/compilers/issues/299)) +- Update `CODEOWNERS` to improve visibility ([#298](https://github.com/foundry-rs/compilers/issues/298)) + +### Refactor + +- Cache/is_dirty impls ([#311](https://github.com/foundry-rs/compilers/issues/311)) + +## [0.18.2](https://github.com/foundry-rs/compilers/releases/tag/v0.18.2) - 2025-08-01 + +### Bug Fixes + +- Allow single sol file remappings ([#295](https://github.com/foundry-rs/compilers/issues/295)) + +### Miscellaneous Tasks + +- Release 0.18.2 + +## [0.18.1](https://github.com/foundry-rs/compilers/releases/tag/v0.18.1) - 2025-07-31 + +### Bug Fixes + +- Consistent handle of unresolved imports ([#294](https://github.com/foundry-rs/compilers/issues/294)) + +### Miscellaneous Tasks + +- Release 0.18.1 +- Add more instrumentation ([#293](https://github.com/foundry-rs/compilers/issues/293)) + +### Other + +- Remove duplicate assembly check in is_dirty ([#292](https://github.com/foundry-rs/compilers/issues/292)) + +## [0.18.0](https://github.com/foundry-rs/compilers/releases/tag/v0.18.0) - 2025-07-14 + +### Dependencies + +- Bump to 0.18.0 +- Update deps ([#290](https://github.com/foundry-rs/compilers/issues/290)) +- Bump solar + MSRV ([#289](https://github.com/foundry-rs/compilers/issues/289)) + +### Miscellaneous Tasks + +- Release 0.18.0 +- Release 0.18.0 + +## [0.17.4](https://github.com/foundry-rs/compilers/releases/tag/v0.17.4) - 2025-06-30 + +### Bug Fixes + +- Fix typos in comments and variable names across solc-related modules ([#286](https://github.com/foundry-rs/compilers/issues/286)) + +### Dependencies + +- Bump vyper to 0.4.3 which adds support for `prague` ([#285](https://github.com/foundry-rs/compilers/issues/285)) + +### Miscellaneous Tasks + +- Release 0.17.4 +- Upstreamed `strip_bytecode_placeholders` from foundry ([#287](https://github.com/foundry-rs/compilers/issues/287)) + +## [0.17.3](https://github.com/foundry-rs/compilers/releases/tag/v0.17.3) - 2025-06-14 + +### Miscellaneous Tasks + +- Release 0.17.3 + +### Other + +- Revert "fix: implement proper serde handling for unknown AST node typ… ([#284](https://github.com/foundry-rs/compilers/issues/284)) + +## [0.17.2](https://github.com/foundry-rs/compilers/releases/tag/v0.17.2) - 2025-06-10 + +### Bug Fixes + +- Implement proper serde handling for unknown AST node types ([#280](https://github.com/foundry-rs/compilers/issues/280)) + +### Miscellaneous Tasks + +- Release 0.17.2 + +### Other + +- Add missing node types ([#282](https://github.com/foundry-rs/compilers/issues/282)) +- Remove EOF version field ([#279](https://github.com/foundry-rs/compilers/issues/279)) + +## [0.17.1](https://github.com/foundry-rs/compilers/releases/tag/v0.17.1) - 2025-06-02 + +### Dependencies + +- Bump to latest version in README +- Update MSRV policy, bump to `1.87` in `clippy.toml` in line with `CI` and `Cargo.toml` ([#277](https://github.com/foundry-rs/compilers/issues/277)) + +### Miscellaneous Tasks + +- Release 0.17.1 +- Release 0.17.1 +- Add language matcher on `MultiCompilerLanguage` ([#276](https://github.com/foundry-rs/compilers/issues/276)) + +## [0.17.0](https://github.com/foundry-rs/compilers/releases/tag/v0.17.0) - 2025-05-29 + +### Miscellaneous Tasks + +- Release 0.17.0 +- Release 0.17.0 +- Release 0.17.0 + +## [0.16.4](https://github.com/foundry-rs/compilers/releases/tag/v0.16.4) - 2025-05-29 + +### Dependencies + +- Bump solar v0.1.4 ([#275](https://github.com/foundry-rs/compilers/issues/275)) + +### Miscellaneous Tasks + +- Release 0.16.4 + +## [0.16.3](https://github.com/foundry-rs/compilers/releases/tag/v0.16.3) - 2025-05-28 + +### Bug Fixes + +- Update Tera documentation link in cliff.toml ([#270](https://github.com/foundry-rs/compilers/issues/270)) + +### Miscellaneous Tasks + +- Release 0.16.3 +- Switch to `Prague` hardfork by default ([#272](https://github.com/foundry-rs/compilers/issues/272)) +- Clean up error! calls ([#273](https://github.com/foundry-rs/compilers/issues/273)) + +### Other + +- Some fields are optional during `"stopAfter":"parsing"` ([#271](https://github.com/foundry-rs/compilers/issues/271)) + +## [0.16.2](https://github.com/foundry-rs/compilers/releases/tag/v0.16.2) - 2025-05-21 + +### Miscellaneous Tasks + +- Release 0.16.2 + +### Other + +- Support `transient` in `StorageLocation` ([#269](https://github.com/foundry-rs/compilers/issues/269)) + ## [0.16.1](https://github.com/foundry-rs/compilers/releases/tag/v0.16.1) - 2025-05-16 ### Bug Fixes - Is_dirty to use additional_files ([#268](https://github.com/foundry-rs/compilers/issues/268)) +### Miscellaneous Tasks + +- Release 0.16.1 + ## [0.16.0](https://github.com/foundry-rs/compilers/releases/tag/v0.16.0) - 2025-05-12 ### Dependencies diff --git a/Cargo.toml b/Cargo.toml index 170d899..e3dfebe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,8 +4,8 @@ resolver = "2" [workspace.package] authors = ["Foundry Maintainers", "Seismic Systems"] -version = "0.16.1" -rust-version = "1.86" +version = "0.19.1" +rust-version = "1.88" readme = "README.md" license = "MIT OR Apache-2.0" repository = "https://github.com/SeismicSystems/seismic-compilers" @@ -36,28 +36,27 @@ redundant-lifetimes = "warn" all = "warn" [workspace.dependencies] -foundry-compilers = { path = "crates/compilers", version = "0.16.1" } -foundry-compilers-artifacts = { path = "crates/artifacts/artifacts", version = "0.16.1" } -foundry-compilers-artifacts-solc = { path = "crates/artifacts/solc", version = "0.16.1" } -foundry-compilers-artifacts-vyper = { path = "crates/artifacts/vyper", version = "0.16.1" } -foundry-compilers-core = { path = "crates/core", version = "0.16.1" } +foundry-compilers = { path = "crates/compilers", version = "0.19.1" } +foundry-compilers-artifacts = { path = "crates/artifacts/artifacts", version = "0.19.1" } +foundry-compilers-artifacts-solc = { path = "crates/artifacts/solc", version = "0.19.1" } +foundry-compilers-artifacts-vyper = { path = "crates/artifacts/vyper", version = "0.19.1" } +foundry-compilers-core = { path = "crates/core", version = "0.19.1" } -alloy-json-abi = { version = "1.0", features = ["serde_json"] } -alloy-primitives = { version = "1.0", features = ["serde", "rand"] } +alloy-json-abi = { version = "1.3", features = ["serde_json"] } +alloy-primitives = { version = "1.3", features = ["serde", "rand"] } cfg-if = "1.0" dunce = "1.0" memmap2 = "0.9" path-slash = "0.2" -rayon = "1.8" -regex = "1.10" +rayon = "1.11" +regex = "1.11" semver = { version = "1.0", features = ["serde"] } serde = { version = "1", features = ["derive", "rc"] } serde_json = "1.0" similar-asserts = "1" -solar-parse = { version = "=0.1.3", default-features = false } -solar-sema = { version = "=0.1.3", default-features = false } +solar = { package = "solar-compiler", version = "=0.1.6", default-features = false } svm = { package = "svm-rs", version = "0.5", default-features = false } -tempfile = "3.9" +tempfile = "3.20" thiserror = "2" tracing = "0.1" walkdir = "2.5" @@ -65,15 +64,9 @@ yansi = "1.0" # async futures-util = "0.3" -tokio = { version = "1.35", features = ["rt-multi-thread"] } +tokio = { version = "1.47", features = ["rt-multi-thread"] } -snapbox = "0.6.9" +snapbox = "0.6.21" [patch.crates-io] - solar-sema = { git = "https://github.com/paradigmxyz/solar.git", rev = "1887ab5" } - solar-parse = { git = "https://github.com/paradigmxyz/solar.git", rev = "1887ab5" } - solar-ast = { git = "https://github.com/paradigmxyz/solar.git", rev = "1887ab5" } - solar-data-structures = { git = "https://github.com/paradigmxyz/solar.git", rev = "1887ab5" } - solar-interface = { git = "https://github.com/paradigmxyz/solar.git", rev = "1887ab5" } - solar-config = { git = "https://github.com/paradigmxyz/solar.git", rev = "1887ab5" } - solar-macros = { git = "https://github.com/paradigmxyz/solar.git", rev = "1887ab5" } \ No newline at end of file +# solar = { package = "solar-compiler", git = "https://github.com/paradigmxyz/solar", branch = "main" } diff --git a/README.md b/README.md index 8f43a2c..163a40b 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,8 @@ Seismic's forks of the [reth](https://github.com/paradigmxyz/reth) stack all hav - `main` or `master`: this branch only consists of commits from the upstream repository. However it will rarely be up-to-date with upstream. The latest commit from this branch reflects how recently Seismic has merged in upstream commits to the seismic branch - `seismic`: the default and production branch for these repositories. This includes all Seismic-specific code essential to make our network run -## Overview + +# Foundry Compilers | [Docs](https://docs.rs/foundry-compilers/latest/foundry_compilers/) | @@ -43,9 +44,7 @@ When updating this, also update: - .github/workflows/ci.yml --> -Foundry Compilers will keep a rolling MSRV (minimum supported rust version) policy of **at -least** 6 months. When increasing the MSRV, the new Rust version must have been -released at least six months ago. The current MSRV is 1.86.0. +The current MSRV (minimum supported rust version) is 1.88. Note that the MSRV is not increased automatically, and only as part of a minor release. @@ -65,7 +64,7 @@ To install, simply add `foundry-compilers` to your cargo dependencies. ```toml [dependencies] -foundry-compilers = "0.10.1" +foundry-compilers = "0.18.3" ``` Example usage: diff --git a/benches/read_all.rs b/benches/read_all.rs index cdc448f..6281630 100644 --- a/benches/read_all.rs +++ b/benches/read_all.rs @@ -53,10 +53,10 @@ fn prepare_contracts(root: &Path, num: usize) -> Vec { let f = File::create(&path).unwrap(); let mut writer = BufWriter::new(f); - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); // let's assume a solidity file is between 2kb and 16kb - let n: usize = rng.gen_range(4..17); + let n: usize = rng.random_range(4..17); let s: String = rng.sample_iter(&Alphanumeric).take(n * 1024).map(char::from).collect(); writer.write_all(s.as_bytes()).unwrap(); writer.flush().unwrap(); diff --git a/cliff.toml b/cliff.toml index aa3c5c0..3f2f805 100644 --- a/cliff.toml +++ b/cliff.toml @@ -10,7 +10,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n """ -# https://tera.netlify.app/docs/#introduction +# https://keats.github.io/tera/docs/#introduction body = """ {% if version %}\ ## [{{ version | trim_start_matches(pat="v") }}](https://github.com/foundry-rs/compilers/releases/tag/v{{ version | trim_start_matches(pat="v") }}) - {{ timestamp | date(format="%Y-%m-%d") }} diff --git a/clippy.toml b/clippy.toml index 80fcb9d..f3322b5 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1 +1 @@ -msrv = "1.86" +msrv = "1.88" diff --git a/crates/artifacts/solc/Cargo.toml b/crates/artifacts/solc/Cargo.toml index d4e1b95..15f0f12 100644 --- a/crates/artifacts/solc/Cargo.toml +++ b/crates/artifacts/solc/Cargo.toml @@ -21,11 +21,11 @@ alloy-json-abi.workspace = true alloy-primitives.workspace = true semver.workspace = true serde_json.workspace = true -serde_repr = "0.1" serde.workspace = true thiserror.workspace = true tracing.workspace = true yansi.workspace = true +regex.workspace = true # async tokio = { workspace = true, optional = true, features = ["fs"] } diff --git a/crates/artifacts/solc/src/ast/lowfidelity.rs b/crates/artifacts/solc/src/ast/lowfidelity.rs index 1d4fc75..33df93a 100644 --- a/crates/artifacts/solc/src/ast/lowfidelity.rs +++ b/crates/artifacts/solc/src/ast/lowfidelity.rs @@ -202,6 +202,9 @@ pub enum NodeType { ParameterList, TryCatchClause, ModifierInvocation, + UserDefinedTypeName, + ArrayTypeName, + Mapping, /// An unknown AST node type. Other(String), diff --git a/crates/artifacts/solc/src/ast/misc.rs b/crates/artifacts/solc/src/ast/misc.rs index 7144ddc..dc765d4 100644 --- a/crates/artifacts/solc/src/ast/misc.rs +++ b/crates/artifacts/solc/src/ast/misc.rs @@ -92,6 +92,7 @@ pub enum StorageLocation { Default, Memory, Storage, + Transient, } /// Visibility specifier. diff --git a/crates/artifacts/solc/src/ast/mod.rs b/crates/artifacts/solc/src/ast/mod.rs index 92e9959..90cf1e6 100644 --- a/crates/artifacts/solc/src/ast/mod.rs +++ b/crates/artifacts/solc/src/ast/mod.rs @@ -175,10 +175,14 @@ ast_node!( #[serde(rename = "contractKind")] kind: ContractKind, documentation: Option, - fully_implemented: bool, + // Not available when "stopAfter": "parsing" is specified. + fully_implemented: Option, + // Not available when "stopAfter": "parsing" is specified. + #[serde(default)] linearized_base_contracts: Vec, nodes: Vec, - scope: usize, + // Not available when "stopAfter": "parsing" is specified. + scope: Option, #[serde(default, deserialize_with = "serde_helpers::default_for_null")] used_errors: Vec, #[serde(default, deserialize_with = "serde_helpers::default_for_null")] @@ -536,7 +540,8 @@ ast_node!( #[serde(default)] mutability: Option, overrides: Option, - scope: usize, + // Not available when "stopAfter": "parsing" is specified. + scope: Option, storage_location: StorageLocation, type_descriptions: TypeDescriptions, type_name: Option, @@ -713,7 +718,8 @@ ast_node!( overrides: Option, parameters: ParameterList, return_parameters: ParameterList, - scope: usize, + // Not available when "stopAfter": "parsing" is specified. + scope: Option, visibility: Visibility, /// The kind of function this node defines. Only valid for Solidity versions 0.5.x and /// above. @@ -1028,7 +1034,8 @@ ast_node!( name_location: Option, canonical_name: String, members: Vec, - scope: usize, + // Not available when "stopAfter": "parsing" is specified. + scope: Option, visibility: Visibility, } ); @@ -1082,7 +1089,8 @@ ast_node!( file: String, #[serde(default, with = "serde_helpers::display_from_str_opt")] name_location: Option, - scope: usize, + // Not available when "stopAfter": "parsing" is specified. + scope: Option, source_unit: usize, symbol_aliases: Vec, unit_alias: String, diff --git a/crates/artifacts/solc/src/bytecode.rs b/crates/artifacts/solc/src/bytecode.rs index eb6985f..b88121d 100644 --- a/crates/artifacts/solc/src/bytecode.rs +++ b/crates/artifacts/solc/src/bytecode.rs @@ -353,6 +353,21 @@ impl BytecodeObject { pub fn contains_placeholder(&self, file: &str, library: &str) -> bool { self.contains_fully_qualified_placeholder(&format!("{file}:{library}")) } + + /// Strips all __$xxx$__ placeholders from the bytecode if it's an unlinked bytecode. + /// by replacing them with 20 zero bytes. + /// This is useful for matching bytecodes to a contract source, and for the source map, + /// in which the actual address of the placeholder isn't important. + pub fn strip_bytecode_placeholders(&self) -> Option { + match &self { + Self::Bytecode(bytes) => Some(bytes.clone()), + Self::Unlinked(s) => { + // Replace all __$xxx$__ placeholders with 32 zero bytes + let bytes = replace_placeholders_and_decode(s).ok()?; + Some(bytes.into()) + } + } + } } // Returns an empty bytecode object @@ -388,6 +403,13 @@ where } } +// Replace all __$xxx$__ placeholders with 32 zero bytes +pub fn replace_placeholders_and_decode(s: &str) -> Result, hex::FromHexError> { + let re = regex::Regex::new(r"_\$.{34}\$_").expect("invalid regex"); + let s = re.replace_all(s, "00".repeat(40)); + hex::decode(s.as_bytes()) +} + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct DeployedBytecode { #[serde(flatten)] diff --git a/crates/artifacts/solc/src/lib.rs b/crates/artifacts/solc/src/lib.rs index d680c81..426143e 100644 --- a/crates/artifacts/solc/src/lib.rs +++ b/crates/artifacts/solc/src/lib.rs @@ -9,7 +9,6 @@ extern crate tracing; use semver::Version; use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer}; -use serde_repr::{Deserialize_repr, Serialize_repr}; use std::{ collections::{BTreeMap, HashSet}, fmt, @@ -231,7 +230,8 @@ impl From for StandardJsonCompilerInput { #[serde(rename_all = "camelCase")] pub struct Settings { /// Stop compilation after the given stage. - /// since 0.8.11: only "parsing" is valid here + /// + /// Since 0.7.3. Only "parsing" is valid here. #[serde(default, skip_serializing_if = "Option::is_none")] pub stop_after: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] @@ -272,16 +272,6 @@ pub struct Settings { /// If this key is an empty string, that refers to a global level. #[serde(default)] pub libraries: Libraries, - /// Specify EOF version to produce. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub eof_version: Option, -} - -/// Available EOF versions. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize_repr, Deserialize_repr)] -#[repr(u8)] -pub enum EofVersion { - V1 = 1, } impl Settings { @@ -308,6 +298,11 @@ impl Settings { self.debug = None; } + if *version < Version::new(0, 7, 3) { + // introduced in 0.7.3 + self.stop_after = None; + } + if *version < Version::new(0, 7, 5) { // introduced in 0.7.5 self.via_ir = None; @@ -562,7 +557,6 @@ impl Default for Settings { libraries: Default::default(), remappings: Default::default(), model_checker: None, - eof_version: None, } .with_ast() } @@ -793,7 +787,7 @@ impl YulDetails { /// EVM versions. /// -/// Default is `Cancun`, since 0.8.25 +/// Default is `Prague`, since 0.8.30 /// /// Kept in sync with: // When adding new EVM versions (see a previous attempt at https://github.com/foundry-rs/compilers/pull/51): diff --git a/crates/artifacts/solc/src/output_selection.rs b/crates/artifacts/solc/src/output_selection.rs index c56c59e..693c973 100644 --- a/crates/artifacts/solc/src/output_selection.rs +++ b/crates/artifacts/solc/src/output_selection.rs @@ -201,7 +201,7 @@ impl ContractOutputSelection { /// that solc emits. /// /// These correspond to the fields in `CompactBytecode`, `CompactDeployedBytecode`, ABI, and - /// method identfiers. + /// method identifiers. pub fn basic() -> Vec { // We don't include all the `bytecode` fields because `generatedSources` is a massive JSON // object and is not used by Foundry. @@ -610,7 +610,7 @@ mod tests { assert!(!output_selection.is_subset_of(&output_selection_empty)); assert!(!output_selection_abi.is_subset_of(&output_selection_empty)); - let output_selecttion_specific = OutputSelection::from(BTreeMap::from([( + let output_selection_specific = OutputSelection::from(BTreeMap::from([( "Contract.sol".to_string(), BTreeMap::from([( "Contract".to_string(), @@ -622,7 +622,7 @@ mod tests { )]), )])); - assert!(!output_selecttion_specific.is_subset_of(&output_selection)); + assert!(!output_selection_specific.is_subset_of(&output_selection)); } #[test] diff --git a/crates/artifacts/solc/src/remappings/mod.rs b/crates/artifacts/solc/src/remappings/mod.rs index 5899d9b..78fb5f8 100644 --- a/crates/artifacts/solc/src/remappings/mod.rs +++ b/crates/artifacts/solc/src/remappings/mod.rs @@ -138,8 +138,11 @@ impl fmt::Display for Remapping { } s.push(':'); } - let name = - if !self.name.ends_with('/') { format!("{}/", self.name) } else { self.name.clone() }; + let name = if needs_trailing_slash(&self.name) { + format!("{}/", self.name) + } else { + self.name.clone() + }; s.push_str(&{ #[cfg(target_os = "windows")] { @@ -153,7 +156,7 @@ impl fmt::Display for Remapping { } }); - if !s.ends_with('/') { + if needs_trailing_slash(&s) { s.push('/'); } f.write_str(&s) @@ -241,7 +244,7 @@ impl fmt::Display for RelativeRemapping { } }); - if !s.ends_with('/') { + if needs_trailing_slash(&s) { s.push('/'); } f.write_str(&s) @@ -252,10 +255,10 @@ impl From for Remapping { fn from(r: RelativeRemapping) -> Self { let RelativeRemapping { context, mut name, path } = r; let mut path = path.relative().display().to_string(); - if !path.ends_with('/') { + if needs_trailing_slash(&path) { path.push('/'); } - if !name.ends_with('/') { + if needs_trailing_slash(&name) { name.push('/'); } Self { context, name, path } @@ -341,6 +344,15 @@ impl<'de> Deserialize<'de> for RelativeRemapping { } } +/// Helper to determine if name or path of a remapping needs trailing slash. +/// Returns false if it already ends with a slash or if remapping is a solidity file. +/// Used to preserve name and path of single file remapping, see +/// +/// +fn needs_trailing_slash(name_or_path: &str) -> bool { + !name_or_path.ends_with('/') && !name_or_path.ends_with(".sol") +} + #[cfg(test)] mod tests { pub use super::*; @@ -423,4 +435,21 @@ mod tests { ); assert_eq!(remapping.to_string(), "oz/=a/b/c/d/".to_string()); } + + // + #[test] + fn can_preserve_single_sol_file_remapping() { + let remapping = "@my-lib/B.sol=lib/my-lib/B.sol"; + let remapping = Remapping::from_str(remapping).unwrap(); + + assert_eq!( + remapping, + Remapping { + context: None, + name: "@my-lib/B.sol".to_string(), + path: "lib/my-lib/B.sol".to_string() + } + ); + assert_eq!(remapping.to_string(), "@my-lib/B.sol=lib/my-lib/B.sol".to_string()); + } } diff --git a/crates/artifacts/solc/src/sources.rs b/crates/artifacts/solc/src/sources.rs index b7e103d..2b929ad 100644 --- a/crates/artifacts/solc/src/sources.rs +++ b/crates/artifacts/solc/src/sources.rs @@ -1,4 +1,4 @@ -use foundry_compilers_core::error::SolcIoError; +use foundry_compilers_core::error::{SolcError, SolcIoError}; use serde::{Deserialize, Serialize}; use std::{ collections::BTreeMap, @@ -22,6 +22,14 @@ impl Sources { Self::default() } + /// Joins all paths relative to `root`. + pub fn make_absolute(&mut self, root: &Path) { + self.0 = std::mem::take(&mut self.0) + .into_iter() + .map(|(path, source)| (root.join(path), source)) + .collect(); + } + /// Returns `true` if no sources should have optimized output selection. pub fn all_dirty(&self) -> bool { self.0.values().all(|s| s.is_dirty()) @@ -124,7 +132,7 @@ impl Source { } /// Reads the file's content - #[instrument(name = "read_source", level = "debug", skip_all, err)] + #[instrument(name = "Source::read", skip_all, err)] pub fn read(file: &Path) -> Result { trace!(file=%file.display()); let mut content = fs::read_to_string(file).map_err(|err| SolcIoError::new(err, file))?; @@ -137,6 +145,30 @@ impl Source { Ok(Self::new(content)) } + /// [`read`](Self::read) + mapping error to [`SolcError`]. + pub fn read_(file: &Path) -> Result { + Self::read(file).map_err(|err| { + let exists = err.path().exists(); + if !exists && err.path().is_symlink() { + return SolcError::ResolveBadSymlink(err); + } + + // This is an additional check useful on OS that have case-sensitive paths, + // see also + // check if there exists a file with different case + #[cfg(feature = "walkdir")] + if !exists { + if let Some(existing_file) = + foundry_compilers_core::utils::find_case_sensitive_existing_file(file) + { + return SolcError::ResolveCaseSensitiveFileName { error: err, existing_file }; + } + } + + SolcError::Resolve(err) + }) + } + /// Returns `true` if the source should be compiled with full output selection. pub fn is_dirty(&self) -> bool { self.kind.is_dirty() @@ -145,7 +177,7 @@ impl Source { /// Recursively finds all source files under the given dir path and reads them all #[cfg(feature = "walkdir")] pub fn read_all_from(dir: &Path, extensions: &[&str]) -> Result { - Self::read_all_files(utils::source_files(dir, extensions)) + Self::read_all(utils::source_files_iter(dir, extensions)) } /// Recursively finds all solidity and yul files under the given dir path and reads them all @@ -154,14 +186,13 @@ impl Source { Self::read_all_from(dir, utils::SOLC_EXTENSIONS) } - /// Reads all source files of the given vec - /// - /// Depending on the len of the vec it will try to read the files in parallel + /// Reads all source files of the given list. pub fn read_all_files(files: Vec) -> Result { Self::read_all(files) } - /// Reads all files + /// Reads all of the given files. + #[instrument(name = "Source::read_all", skip_all)] pub fn read_all(files: I) -> Result where I: IntoIterator, @@ -211,7 +242,7 @@ impl Source { #[cfg(feature = "async")] impl Source { /// async version of `Self::read` - #[instrument(name = "async_read_source", level = "debug", skip_all, err)] + #[instrument(name = "Source::async_read", skip_all, err)] pub async fn async_read(file: &Path) -> Result { let mut content = tokio::fs::read_to_string(file).await.map_err(|err| SolcIoError::new(err, file))?; diff --git a/crates/artifacts/vyper/src/settings.rs b/crates/artifacts/vyper/src/settings.rs index 3653b0a..f162d28 100644 --- a/crates/artifacts/vyper/src/settings.rs +++ b/crates/artifacts/vyper/src/settings.rs @@ -13,6 +13,7 @@ pub const VYPER_BERLIN: Version = Version::new(0, 3, 0); pub const VYPER_PARIS: Version = Version::new(0, 3, 7); pub const VYPER_SHANGHAI: Version = Version::new(0, 3, 8); pub const VYPER_CANCUN: Version = Version::new(0, 3, 8); +pub const VYPER_PRAGUE: Version = Version::new(0, 4, 3); const VYPER_0_4: Version = Version::new(0, 4, 0); @@ -126,7 +127,9 @@ impl VyperSettings { /// Adjusts the EVM version based on the compiler version. pub fn normalize_evm_version(&mut self, version: &Version) { if let Some(evm_version) = &mut self.evm_version { - *evm_version = if *evm_version >= EvmVersion::Cancun && *version >= VYPER_CANCUN { + *evm_version = if *evm_version >= EvmVersion::Prague && *version >= VYPER_PRAGUE { + EvmVersion::Prague + } else if *evm_version >= EvmVersion::Cancun && *version >= VYPER_CANCUN { EvmVersion::Cancun } else if *evm_version >= EvmVersion::Shanghai && *version >= VYPER_SHANGHAI { EvmVersion::Shanghai diff --git a/crates/compilers/Cargo.toml b/crates/compilers/Cargo.toml index 73808a0..62ef396 100644 --- a/crates/compilers/Cargo.toml +++ b/crates/compilers/Cargo.toml @@ -31,23 +31,20 @@ rayon.workspace = true thiserror.workspace = true path-slash.workspace = true yansi.workspace = true -solar-parse.workspace = true -solar-sema.workspace = true +solar.workspace = true futures-util = { workspace = true, optional = true } tokio = { workspace = true, optional = true } auto_impl = "1" winnow = "0.7" dyn-clone = "1" -derive_more = { version = "1", features = ["debug"] } -home = "0.5" -dirs = "6.0" +derive_more = { version = "2", features = ["debug"] } itertools = ">=0.13, <=0.14" # project-util -tempfile = { version = "3.9", optional = true } +tempfile = { version = "3.20", optional = true } fs_extra = { version = "1.3", optional = true } -rand = { version = "0.8", optional = true } +rand = { version = "0.9", optional = true } # svm svm = { workspace = true, optional = true } @@ -60,10 +57,10 @@ tracing-subscriber = { version = "0.3", default-features = false, features = [ "fmt", ] } similar-asserts.workspace = true -fd-lock = "4.0.0" -tokio = { version = "1.35", features = ["rt-multi-thread", "macros"] } +fd-lock = "4.0.4" +tokio = { version = "1.47", features = ["rt-multi-thread", "macros"] } reqwest = "0.12" -tempfile = "3.9" +tempfile = "3.20" snapbox.workspace = true foundry-compilers-core = { workspace = true, features = ["test-utils"] } diff --git a/crates/compilers/src/artifact_output/configurable.rs b/crates/compilers/src/artifact_output/configurable.rs index c661a4e..7f24081 100644 --- a/crates/compilers/src/artifact_output/configurable.rs +++ b/crates/compilers/src/artifact_output/configurable.rs @@ -377,9 +377,6 @@ impl ArtifactOutput for ConfigurableArtifacts { if assembly && artifact.assembly.is_none() { return Ok(true); } - if assembly && artifact.assembly.is_none() { - return Ok(true); - } if legacy_assembly && artifact.legacy_assembly.is_none() { return Ok(true); } diff --git a/crates/compilers/src/artifact_output/mod.rs b/crates/compilers/src/artifact_output/mod.rs index ec2a7d0..ccd079a 100644 --- a/crates/compilers/src/artifact_output/mod.rs +++ b/crates/compilers/src/artifact_output/mod.rs @@ -625,10 +625,8 @@ pub trait ArtifactOutput { ) -> Result> { let mut artifacts = self.output_to_artifacts(contracts, sources, ctx, layout, primary_profiles); - fs::create_dir_all(&layout.artifacts).map_err(|err| { - error!(dir=?layout.artifacts, "Failed to create artifacts folder"); - SolcIoError::new(err, &layout.artifacts) - })?; + fs::create_dir_all(&layout.artifacts) + .map_err(|err| SolcIoError::new(err, &layout.artifacts))?; artifacts.join_all(&layout.artifacts); artifacts.write_all()?; @@ -1140,16 +1138,17 @@ impl ArtifactOutput for MinimalCombinedArtifactsHardhatFallback { } fn read_cached_artifact(path: &Path) -> Result { - let content = fs::read_to_string(path).map_err(|err| SolcError::io(err, path))?; - if let Ok(a) = serde_json::from_str(&content) { - Ok(a) - } else { - error!("Failed to deserialize compact artifact"); - trace!("Fallback to hardhat artifact deserialization"); - let artifact = serde_json::from_str::(&content)?; - trace!("successfully deserialized hardhat artifact"); - Ok(artifact.into_contract_bytecode()) + #[derive(Deserialize)] + #[serde(untagged)] + enum Artifact { + Compact(CompactContractBytecode), + Hardhat(HardhatArtifact), } + + Ok(match utils::read_json_file::(path)? { + Artifact::Compact(c) => c, + Artifact::Hardhat(h) => h.into_contract_bytecode(), + }) } fn contract_to_artifact( diff --git a/crates/compilers/src/cache.rs b/crates/compilers/src/cache.rs index e22c538..5cd8411 100644 --- a/crates/compilers/src/cache.rs +++ b/crates/compilers/src/cache.rs @@ -6,7 +6,7 @@ use crate::{ output::Builds, resolver::GraphEdges, ArtifactFile, ArtifactOutput, Artifacts, ArtifactsMap, Graph, OutputContext, Project, - ProjectPaths, ProjectPathsConfig, SourceCompilationKind, + ProjectPaths, ProjectPathsConfig, SourceCompilationKind, SourceParser, }; use foundry_compilers_artifacts::{ sources::{Source, Sources}, @@ -53,6 +53,7 @@ pub struct CompilerCache { } impl CompilerCache { + /// Creates a new empty cache. pub fn new(format: String, paths: ProjectPaths, preprocessed: bool) -> Self { Self { format, @@ -118,11 +119,10 @@ impl CompilerCache { /// cache.join_artifacts_files(project.artifacts_path()); /// # Ok::<_, Box>(()) /// ``` - #[instrument(skip_all, name = "sol-files-cache::read")] + #[instrument(name = "CompilerCache::read", err)] pub fn read(path: &Path) -> Result { - trace!("reading solfiles cache at {}", path.display()); let cache: Self = utils::read_json_file(path)?; - trace!("read cache \"{}\" with {} entries", cache.format, cache.files.len()); + trace!(cache.format, cache.files = cache.files.len(), "read cache"); Ok(cache) } @@ -149,6 +149,7 @@ impl CompilerCache { } /// Write the cache as json file to the given path + #[instrument(name = "CompilerCache::write", skip_all)] pub fn write(&self, path: &Path) -> Result<()> { trace!("writing cache with {} entries to json file: \"{}\"", self.len(), path.display()); utils::create_parent_dir_all(path)?; @@ -158,6 +159,7 @@ impl CompilerCache { } /// Removes build infos which don't have any artifacts linked to them. + #[instrument(skip_all)] pub fn remove_outdated_builds(&mut self) { let mut outdated = Vec::new(); for build_id in &self.builds { @@ -180,6 +182,7 @@ impl CompilerCache { } /// Sets the `CacheEntry`'s file paths to `root` adjoined to `self.file`. + #[instrument(skip_all)] pub fn join_entries(&mut self, root: &Path) -> &mut Self { self.files = std::mem::take(&mut self.files) .into_iter() @@ -189,6 +192,7 @@ impl CompilerCache { } /// Removes `base` from all `CacheEntry` paths + #[instrument(skip_all)] pub fn strip_entries_prefix(&mut self, base: &Path) -> &mut Self { self.files = std::mem::take(&mut self.files) .into_iter() @@ -198,12 +202,14 @@ impl CompilerCache { } /// Sets the artifact files location to `base` adjoined to the `CachEntries` artifacts. + #[instrument(skip_all)] pub fn join_artifacts_files(&mut self, base: &Path) -> &mut Self { self.files.values_mut().for_each(|entry| entry.join_artifacts_files(base)); self } /// Removes `base` from all artifact file paths + #[instrument(skip_all)] pub fn strip_artifact_files_prefixes(&mut self, base: &Path) -> &mut Self { self.files.values_mut().for_each(|entry| entry.strip_artifact_files_prefixes(base)); self @@ -212,6 +218,7 @@ impl CompilerCache { /// Removes all `CacheEntry` which source files don't exist on disk /// /// **NOTE:** this assumes the `files` are absolute + #[instrument(skip_all)] pub fn remove_missing_files(&mut self) { trace!("remove non existing files from cache"); self.files.retain(|file, _| { @@ -292,6 +299,7 @@ impl CompilerCache { /// /// **NOTE**: unless the cache's `files` keys were modified `contract_file` is expected to be /// absolute. + #[instrument(skip_all)] pub fn read_artifact( &self, contract_file: &Path, @@ -318,6 +326,7 @@ impl CompilerCache { /// let artifacts = cache.read_artifacts::()?; /// # Ok::<_, Box>(()) /// ``` + #[instrument(skip_all)] pub fn read_artifacts( &self, ) -> Result> { @@ -335,6 +344,7 @@ impl CompilerCache { /// objects, so we are basically just partially deserializing build infos here. /// /// [BuildContext]: crate::buildinfo::BuildContext + #[instrument(skip_all)] pub fn read_builds(&self, build_info_dir: &Path) -> Result> { use rayon::prelude::*; @@ -491,6 +501,7 @@ impl CacheEntry { /// Reads all artifact files associated with the `CacheEntry` /// /// **Note:** all artifact file paths should be absolute. + #[instrument(skip_all)] fn read_artifact_files( &self, ) -> Result>>> { @@ -514,6 +525,7 @@ impl CacheEntry { Ok(artifacts) } + #[instrument(skip_all)] pub(crate) fn merge_artifacts<'a, A, I, T: 'a>(&mut self, artifacts: I) where I: IntoIterator, @@ -646,7 +658,7 @@ pub(crate) struct ArtifactsCacheInner< pub cached_builds: Builds, /// Relationship between all the files. - pub edges: GraphEdges, + pub edges: GraphEdges, /// The project. pub project: &'a Project, @@ -711,6 +723,7 @@ impl, C: Compiler> /// Gets or calculates the interface representation hash for the given source file. fn interface_repr_hash(&mut self, source: &Source, file: &Path) -> &str { self.interface_repr_hashes.entry(file.to_path_buf()).or_insert_with(|| { + // TODO: use `interface_representation_ast` directly with `edges.parser()`. if let Some(r) = interface_repr_hash(&source.content, file) { return r; } @@ -774,47 +787,55 @@ impl, C: Compiler> } /// Returns whether we are missing artifacts for the given file and version. - #[instrument(level = "trace", skip(self))] fn is_missing_artifacts(&self, file: &Path, version: &Version, profile: &str) -> bool { + self.is_missing_artifacts_impl(file, version, profile).is_err() + } + + /// Returns whether we are missing artifacts for the given file and version. + #[instrument(level = "trace", name = "is_missing_artifacts", skip(self), ret)] + fn is_missing_artifacts_impl( + &self, + file: &Path, + version: &Version, + profile: &str, + ) -> Result<(), &'static str> { let Some(entry) = self.cache.entry(file) else { - trace!("missing cache entry"); - return true; + return Err("missing cache entry"); }; // only check artifact's existence if the file generated artifacts. // e.g. a solidity file consisting only of import statements (like interfaces that // re-export) do not create artifacts if entry.seen_by_compiler && entry.artifacts.is_empty() { - trace!("no artifacts"); - return false; + return Ok(()); } if !entry.contains(version, profile) { - trace!("missing linked artifacts"); - return true; + return Err("missing linked artifacts"); } - if entry.artifacts_for_version(version).any(|artifact| { - let missing_artifact = !self.cached_artifacts.has_artifact(&artifact.path); - if missing_artifact { - trace!("missing artifact \"{}\"", artifact.path.display()); - } - missing_artifact - }) { - return true; + if entry + .artifacts_for_version(version) + .any(|artifact| !self.cached_artifacts.has_artifact(&artifact.path)) + { + return Err("missing artifact"); } // If any requested extra files are missing for any artifact, mark source as dirty to // generate them - self.missing_extra_files() + if self.missing_extra_files() { + return Err("missing extra files"); + } + + Ok(()) } // Walks over all cache entries, detects dirty files and removes them from cache. - fn find_and_remove_dirty(&mut self) { - fn populate_dirty_files( + fn remove_dirty_sources(&mut self) { + fn populate_dirty_files( file: &Path, dirty_files: &mut HashSet, - edges: &GraphEdges, + edges: &GraphEdges

, ) { for file in edges.importers(file) { // If file is marked as dirty we either have already visited it or it was marked as @@ -826,41 +847,7 @@ impl, C: Compiler> } } - let existing_profiles = self.project.settings_profiles().collect::>(); - - let mut dirty_profiles = HashSet::new(); - for (profile, settings) in &self.cache.profiles { - if !existing_profiles.get(profile.as_str()).is_some_and(|p| p.can_use_cached(settings)) - { - trace!("dirty profile: {}", profile); - dirty_profiles.insert(profile.clone()); - } - } - - for profile in &dirty_profiles { - self.cache.profiles.remove(profile); - } - - self.cache.files.retain(|_, entry| { - // keep entries which already had no artifacts - if entry.artifacts.is_empty() { - return true; - } - entry.artifacts.retain(|_, artifacts| { - artifacts.retain(|_, artifacts| { - artifacts.retain(|profile, _| !dirty_profiles.contains(profile)); - !artifacts.is_empty() - }); - !artifacts.is_empty() - }); - !entry.artifacts.is_empty() - }); - - for (profile, settings) in existing_profiles { - if !self.cache.profiles.contains_key(profile) { - self.cache.profiles.insert(profile.to_string(), settings.clone()); - } - } + self.update_profiles(); // Iterate over existing cache entries. let files = self.cache.files.keys().cloned().collect::>(); @@ -878,7 +865,7 @@ impl, C: Compiler> // Build a temporary graph for walking imports. We need this because `self.edges` // only contains graph data for in-scope sources but we are operating on cache entries. - if let Ok(graph) = Graph::::resolve_sources(&self.project.paths, sources) { + if let Ok(graph) = Graph::::resolve_sources(&self.project.paths, sources) { let (sources, edges) = graph.into_sources(); // Calculate content hashes for later comparison. @@ -886,7 +873,7 @@ impl, C: Compiler> // Pre-add all sources that are guaranteed to be dirty for file in sources.keys() { - if self.is_dirty_impl(file, false) { + if self.is_dirty(file, false) { self.dirty_sources.insert(file.clone()); } } @@ -915,7 +902,7 @@ impl, C: Compiler> } else if !is_src && self.dirty_sources.contains(import) && (!self.is_source_file(import) - || self.is_dirty_impl(import, true) + || self.is_dirty(import, true) || self.cache.mocks.contains(file)) { if self.cache.mocks.contains(file) { @@ -940,36 +927,76 @@ impl, C: Compiler> } } - fn is_dirty_impl(&self, file: &Path, use_interface_repr: bool) -> bool { + /// Updates the profiles in the cache, removing those which are dirty alongside their artifacts. + fn update_profiles(&mut self) { + let existing_profiles = self.project.settings_profiles().collect::>(); + + let mut dirty_profiles = HashSet::new(); + for (profile, settings) in &self.cache.profiles { + if !existing_profiles.get(profile.as_str()).is_some_and(|p| p.can_use_cached(settings)) + { + dirty_profiles.insert(profile.clone()); + } + } + + for profile in &dirty_profiles { + trace!(profile, "removing dirty profile and artifacts"); + self.cache.profiles.remove(profile); + } + + for (profile, settings) in existing_profiles { + if !self.cache.profiles.contains_key(profile) { + trace!(profile, "adding new profile"); + self.cache.profiles.insert(profile.to_string(), settings.clone()); + } + } + + self.cache.files.retain(|_, entry| { + // keep entries which already had no artifacts + if entry.artifacts.is_empty() { + return true; + } + entry.artifacts.retain(|_, artifacts| { + artifacts.retain(|_, artifacts| { + artifacts.retain(|profile, _| !dirty_profiles.contains(profile)); + !artifacts.is_empty() + }); + !artifacts.is_empty() + }); + !entry.artifacts.is_empty() + }); + } + + fn is_dirty(&self, file: &Path, use_interface_repr: bool) -> bool { + self.is_dirty_impl(file, use_interface_repr).is_err() + } + + #[instrument(level = "trace", name = "is_dirty", skip(self), ret)] + fn is_dirty_impl(&self, file: &Path, use_interface_repr: bool) -> Result<(), &'static str> { let Some(entry) = self.cache.entry(file) else { - trace!("missing cache entry"); - return true; + return Err("missing cache entry"); }; if use_interface_repr && self.cache.preprocessed { let Some(interface_hash) = self.interface_repr_hashes.get(file) else { - trace!("missing interface hash"); - return true; + return Err("missing interface hash"); }; if entry.interface_repr_hash.as_ref() != Some(interface_hash) { - trace!("interface hash changed"); - return true; - }; + return Err("interface hash changed"); + } } else { let Some(content_hash) = self.content_hashes.get(file) else { - trace!("missing content hash"); - return true; + return Err("missing content hash"); }; if entry.content_hash != *content_hash { - trace!("content hash changed"); - return true; + return Err("content hash changed"); } } // all things match, can be reused - false + Ok(()) } /// Adds the file's hashes to the set if not set yet @@ -1008,7 +1035,7 @@ pub(crate) enum ArtifactsCache< C: Compiler, > { /// Cache nothing on disk - Ephemeral(GraphEdges, &'a Project), + Ephemeral(GraphEdges, &'a Project), /// Handles the actual cached artifacts, detects artifacts that can be reused Cached(ArtifactsCacheInner<'a, T, C>), } @@ -1017,9 +1044,10 @@ impl<'a, T: ArtifactOutput, C: Compiler> ArtifactsCache<'a, T, C> { /// Create a new cache instance with the given files + #[instrument(name = "ArtifactsCache::new", skip(project, edges))] pub fn new( project: &'a Project, - edges: GraphEdges, + edges: GraphEdges, preprocessed: bool, ) -> Result { /// Returns the [CompilerCache] to use @@ -1042,6 +1070,8 @@ impl<'a, T: ArtifactOutput, C: Compiler> } } + trace!(invalidate_cache, "cache invalidated"); + // new empty cache CompilerCache::new(Default::default(), paths, preprocessed) } @@ -1102,7 +1132,7 @@ impl<'a, T: ArtifactOutput, C: Compiler> } /// Returns the graph data for this project - pub fn graph(&self) -> &GraphEdges { + pub fn graph(&self) -> &GraphEdges { match self { ArtifactsCache::Ephemeral(graph, _) => graph, ArtifactsCache::Cached(inner) => &inner.edges, @@ -1135,10 +1165,11 @@ impl<'a, T: ArtifactOutput, C: Compiler> } /// Adds the file's hashes to the set if not set yet + #[instrument(skip_all)] pub fn remove_dirty_sources(&mut self) { match self { ArtifactsCache::Ephemeral(..) => {} - ArtifactsCache::Cached(cache) => cache.find_and_remove_dirty(), + ArtifactsCache::Cached(cache) => cache.remove_dirty_sources(), } } @@ -1161,6 +1192,7 @@ impl<'a, T: ArtifactOutput, C: Compiler> } /// Filters out those sources that don't need to be compiled + #[instrument(name = "ArtifactsCache::filter", skip_all)] pub fn filter(&mut self, sources: &mut Sources, version: &Version, profile: &str) { match self { ArtifactsCache::Ephemeral(..) => {} @@ -1173,18 +1205,23 @@ impl<'a, T: ArtifactOutput, C: Compiler> /// compiled and written to disk `written_artifacts`. /// /// Returns all the _cached_ artifacts. + #[instrument(name = "ArtifactsCache::consume", skip_all)] + #[allow(clippy::type_complexity)] pub fn consume( self, written_artifacts: &Artifacts, written_build_infos: &Vec>, write_to_disk: bool, - ) -> Result<(Artifacts, Builds)> + ) -> Result<(Artifacts, Builds, GraphEdges)> where T: ArtifactOutput, { - let ArtifactsCache::Cached(cache) = self else { - trace!("no cache configured, ephemeral"); - return Ok(Default::default()); + let cache = match self { + ArtifactsCache::Ephemeral(edges, _project) => { + trace!("no cache configured, ephemeral"); + return Ok((Default::default(), Default::default(), edges)); + } + ArtifactsCache::Cached(cache) => cache, }; let ArtifactsCacheInner { @@ -1194,7 +1231,9 @@ impl<'a, T: ArtifactOutput, C: Compiler> dirty_sources, sources_in_scope, project, - .. + edges, + content_hashes: _, + interface_repr_hashes: _, } = cache; // Remove cached artifacts which are out of scope, dirty or appear in `written_artifacts`. @@ -1246,7 +1285,7 @@ impl<'a, T: ArtifactOutput, C: Compiler> cache.write(project.cache_path())?; } - Ok((cached_artifacts, cached_builds)) + Ok((cached_artifacts, cached_builds, edges)) } /// Marks the cached entry as seen by the compiler, if it's cached. diff --git a/crates/compilers/src/cache/iface.rs b/crates/compilers/src/cache/iface.rs index 8f006ef..18cfd6c 100644 --- a/crates/compilers/src/cache/iface.rs +++ b/crates/compilers/src/cache/iface.rs @@ -1,5 +1,5 @@ use crate::{parse_one_source, replace_source_content}; -use solar_sema::{ +use solar::parse::{ ast::{self, Span}, interface::diagnostics::EmittedDiagnostics, }; @@ -11,7 +11,7 @@ pub(crate) fn interface_repr_hash(content: &str, path: &Path) -> Option } pub(crate) fn interface_repr(content: &str, path: &Path) -> Result { - parse_one_source(content, path, |ast| interface_representation_ast(content, &ast)) + parse_one_source(content, path, |sess, ast| interface_representation_ast(content, sess, ast)) } /// Helper function to remove parts of the contract which do not alter its interface: @@ -21,7 +21,8 @@ pub(crate) fn interface_repr(content: &str, path: &Path) -> Result, + sess: &solar::sema::interface::Session, + ast: &solar::parse::ast::SourceUnit<'_>, ) -> String { let mut spans_to_remove: Vec = Vec::new(); for item in ast.items.iter() { @@ -38,7 +39,7 @@ pub(crate) fn interface_representation_ast( let is_exposed = match function.kind { // Function with external or public visibility ast::FunctionKind::Function => { - function.header.visibility >= Some(ast::Visibility::Public) + function.header.visibility.map(|v| *v) >= Some(ast::Visibility::Public) } ast::FunctionKind::Constructor | ast::FunctionKind::Fallback @@ -57,9 +58,9 @@ pub(crate) fn interface_representation_ast( } } } - let content = - replace_source_content(content, spans_to_remove.iter().map(|span| (span.to_range(), ""))) - .replace("\n", ""); + let updates = + spans_to_remove.iter().map(|&span| (sess.source_map().span_to_source(span).unwrap().data, "")); + let content = replace_source_content(content, updates).replace("\n", ""); crate::utils::RE_TWO_OR_MORE_SPACES.replace_all(&content, "").into_owned() } diff --git a/crates/compilers/src/compile/output/mod.rs b/crates/compilers/src/compile/output/mod.rs index 23f2745..d20681b 100644 --- a/crates/compilers/src/compile/output/mod.rs +++ b/crates/compilers/src/compile/output/mod.rs @@ -19,6 +19,7 @@ use crate::{ compilers::{ multi::MultiCompiler, CompilationError, Compiler, CompilerContract, CompilerOutput, }, + resolver::GraphEdges, Artifact, ArtifactId, ArtifactOutput, Artifacts, ConfigurableArtifacts, }; @@ -62,7 +63,7 @@ impl IntoIterator for Builds { /// Contains a mixture of already compiled/cached artifacts and the input set of sources that still /// need to be compiled. -#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[derive(Clone, Debug, Default)] pub struct ProjectCompileOutput< C: Compiler = MultiCompiler, T: ArtifactOutput = ConfigurableArtifacts, @@ -81,12 +82,25 @@ pub struct ProjectCompileOutput< pub(crate) compiler_severity_filter: Severity, /// all build infos that were just compiled pub(crate) builds: Builds, + /// The relationship between the source files and their imports + pub(crate) edges: GraphEdges, } impl, C: Compiler> ProjectCompileOutput { + /// Returns the parser used to parse the sources. + pub fn parser(&self) -> &C::Parser { + self.edges.parser() + } + + /// Returns the parser used to parse the sources. + pub fn parser_mut(&mut self) -> &mut C::Parser { + self.edges.parser_mut() + } + /// Converts all `\\` separators in _all_ paths to `/` + #[instrument(skip_all)] pub fn slash_paths(&mut self) { self.compiler_output.slash_paths(); self.compiled_artifacts.slash_paths(); @@ -459,6 +473,11 @@ impl, C: Compiler> pub fn builds(&self) -> impl Iterator)> { self.builds.iter() } + + /// Returns the source graph of the project. + pub fn graph(&self) -> &GraphEdges { + &self.edges + } } impl> diff --git a/crates/compilers/src/compile/project.rs b/crates/compilers/src/compile/project.rs index 6ffc26f..cbfbf70 100644 --- a/crates/compilers/src/compile/project.rs +++ b/crates/compilers/src/compile/project.rs @@ -146,7 +146,7 @@ pub struct ProjectCompiler< C: Compiler, > { /// Contains the relationship of the source files and their imports - edges: GraphEdges, + edges: GraphEdges, project: &'a Project, /// A mapping from a source file path to the primary profile name selected for it. primary_profiles: HashMap, @@ -171,6 +171,7 @@ impl<'a, T: ArtifactOutput, C: Compiler> /// /// Multiple (`Solc` -> `Sources`) pairs can be compiled in parallel if the `Project` allows /// multiple `jobs`, see [`crate::Project::set_solc_jobs()`]. + #[instrument(name = "ProjectCompiler::new", skip_all)] pub fn with_sources(project: &'a Project, mut sources: Sources) -> Result { if let Some(filter) = &project.sparse_output { sources.retain(|f, _| filter.is_match(f)) @@ -209,6 +210,7 @@ impl<'a, T: ArtifactOutput, C: Compiler> /// let output = project.compile()?; /// # Ok::<(), Box>(()) /// ``` + #[instrument(name = "compile_project", skip_all)] pub fn compile(self) -> Result> { let slash_paths = self.project.slash_paths; @@ -226,6 +228,7 @@ impl<'a, T: ArtifactOutput, C: Compiler> /// Does basic preprocessing /// - sets proper source unit names /// - check cache + #[instrument(skip_all)] fn preprocess(self) -> Result> { trace!("preprocessing"); let Self { edges, project, mut sources, primary_profiles, preprocessor } = self; @@ -265,6 +268,7 @@ impl<'a, T: ArtifactOutput, C: Compiler> PreprocessedState<'a, T, C> { /// advance to the next state by compiling all sources + #[instrument(skip_all)] fn compile(self) -> Result> { trace!("compiling"); let PreprocessedState { sources, mut cache, primary_profiles, preprocessor } = self; @@ -297,7 +301,7 @@ impl<'a, T: ArtifactOutput, C: Compiler> /// /// Writes all output contracts to disk if enabled in the `Project` and if the build was /// successful - #[instrument(skip_all, name = "write-artifacts")] + #[instrument(skip_all)] fn write_artifacts(self) -> Result> { let CompiledState { output, cache, primary_profiles } = self; @@ -365,6 +369,7 @@ impl, C: Compiler> /// Writes the cache file /// /// this concludes the [`Project::compile()`] statemachine + #[instrument(skip_all)] fn write_cache(self) -> Result> { let ArtifactsState { output, cache, compiled_artifacts } = self; let project = cache.project(); @@ -376,7 +381,7 @@ impl, C: Compiler> let skip_write_to_disk = project.no_artifacts || has_error; trace!(has_error, project.no_artifacts, skip_write_to_disk, cache_path=?project.cache_path(),"prepare writing cache file"); - let (cached_artifacts, cached_builds) = + let (cached_artifacts, cached_builds, edges) = cache.consume(&compiled_artifacts, &output.build_infos, !skip_write_to_disk)?; project.artifacts_handler().handle_cached_artifacts(&cached_artifacts)?; @@ -399,6 +404,7 @@ impl, C: Compiler> ignored_file_paths, compiler_severity_filter, builds, + edges, }) } } @@ -436,6 +442,7 @@ impl CompilerSources<'_, L, S> { } /// Filters out all sources that don't need to be compiled, see [`ArtifactsCache::filter`] + #[instrument(name = "CompilerSources::filter", skip_all)] fn filter< T: ArtifactOutput, C: Compiler, diff --git a/crates/compilers/src/compilers/mod.rs b/crates/compilers/src/compilers/mod.rs index 5abb74b..29e1ca7 100644 --- a/crates/compilers/src/compilers/mod.rs +++ b/crates/compilers/src/compilers/mod.rs @@ -1,4 +1,4 @@ -use crate::ProjectPathsConfig; +use crate::{resolver::Node, ProjectPathsConfig}; use alloy_json_abi::JsonAbi; use core::fmt; use foundry_compilers_artifacts::{ @@ -9,11 +9,12 @@ use foundry_compilers_artifacts::{ BytecodeObject, CompactContractRef, Contract, FileToContractsMap, Severity, SourceFile, }; use foundry_compilers_core::error::Result; +use rayon::prelude::*; use semver::{Version, VersionReq}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::{ borrow::Cow, - collections::{BTreeMap, BTreeSet, HashMap, HashSet}, + collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap, HashSet}, fmt::{Debug, Display}, hash::Hash, path::{Path, PathBuf}, @@ -74,7 +75,7 @@ pub trait CompilerSettings: type Restrictions: CompilerSettingsRestrictions; /// Executes given fn with mutable reference to configured [OutputSelection]. - fn update_output_selection(&mut self, f: impl FnOnce(&mut OutputSelection) + Copy); + fn update_output_selection(&mut self, f: impl FnMut(&mut OutputSelection)); /// Returns true if artifacts compiled with given `other` config are compatible with this /// config and if compilation can be skipped. @@ -139,12 +140,40 @@ pub trait CompilerInput: Serialize + Send + Sync + Sized + Debug { fn strip_prefix(&mut self, base: &Path); } +/// [`ParsedSource`] parser. +pub trait SourceParser: Clone + Debug + Send + Sync { + type ParsedSource: ParsedSource; + + /// Creates a new parser for the given config. + fn new(config: &ProjectPathsConfig) -> Self; + + /// Reads and parses the source file at the given path. + fn read(&mut self, path: &Path) -> Result> { + Node::read(path) + } + + /// Parses the sources in the given sources map. + fn parse_sources( + &mut self, + sources: &mut Sources, + ) -> Result)>> { + sources + .0 + .par_iter() + .map(|(path, source)| { + let data = Self::ParsedSource::parse(source.as_ref(), path)?; + Ok((path.clone(), Node::new(path.clone(), source.clone(), data))) + }) + .collect::>() + } +} + /// Parser of the source files which is used to identify imports and version requirements of the /// given source. /// /// Used by path resolver to resolve imports or determine compiler versions needed to compiler given /// sources. -pub trait ParsedSource: Debug + Sized + Send + Clone { +pub trait ParsedSource: Clone + Debug + Sized + Send { type Language: Language; /// Parses the content of the source file. @@ -331,7 +360,7 @@ pub trait Compiler: Send + Sync + Clone { /// Output data for each contract type CompilerContract: CompilerContract; /// Source parser used for resolving imports and version requirements. - type ParsedSource: ParsedSource; + type Parser: SourceParser>; /// Compiler settings. type Settings: CompilerSettings; /// Enum of languages supported by the compiler. @@ -356,20 +385,24 @@ pub(crate) fn cache_version( f: impl FnOnce(&Path) -> Result, ) -> Result { #[allow(clippy::complexity)] - static VERSION_CACHE: OnceLock, Version>>>> = + static VERSION_CACHE: OnceLock), Version>>> = OnceLock::new(); - let mut lock = VERSION_CACHE - .get_or_init(|| Mutex::new(HashMap::new())) + + let mut cache = VERSION_CACHE + .get_or_init(Default::default) .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); - if let Some(version) = lock.get(&path).and_then(|versions| versions.get(args)) { - return Ok(version.clone()); + match cache.entry((path, args.to_vec())) { + Entry::Occupied(entry) => Ok(entry.get().clone()), + Entry::Vacant(entry) => { + let path = &entry.key().0; + let _guard = + debug_span!("get_version", path = %path.file_name().map(|n| n.to_string_lossy()).unwrap_or_else(|| path.to_string_lossy())) + .entered(); + let version = f(path)?; + entry.insert(version.clone()); + Ok(version) + } } - - let version = f(&path)?; - - lock.entry(path).or_default().insert(args.to_vec(), version.clone()); - - Ok(version) } diff --git a/crates/compilers/src/compilers/multi.rs b/crates/compilers/src/compilers/multi.rs index d206076..0953349 100644 --- a/crates/compilers/src/compilers/multi.rs +++ b/crates/compilers/src/compilers/multi.rs @@ -10,9 +10,11 @@ use super::{ }; use crate::{ artifacts::vyper::{VyperCompilationError, VyperSettings}, - resolver::parse::SolData, + parser::VyperParser, + resolver::parse::{SolData, SolParser}, settings::VyperRestrictions, solc::SolcRestrictions, + SourceParser, }; use foundry_compilers_artifacts::{ error::SourceLocation, @@ -66,6 +68,22 @@ pub enum MultiCompilerLanguage { Vyper(VyperLanguage), } +impl Default for MultiCompilerLanguage { + fn default() -> Self { + Self::Solc(SolcLanguage::Solidity) + } +} + +impl MultiCompilerLanguage { + pub fn is_vyper(&self) -> bool { + matches!(self, Self::Vyper(_)) + } + + pub fn is_solc(&self) -> bool { + matches!(self, Self::Solc(_)) + } +} + impl From for MultiCompilerLanguage { fn from(language: SolcLanguage) -> Self { Self::Solc(language) @@ -91,6 +109,35 @@ impl fmt::Display for MultiCompilerLanguage { } } +/// Source parser for the [`MultiCompiler`]. Recognizes Solc and Vyper sources. +#[derive(Clone, Debug)] +pub struct MultiCompilerParser { + solc: SolParser, + vyper: VyperParser, +} + +impl MultiCompilerParser { + /// Returns the parser used to parse Solc sources. + pub fn solc(&self) -> &SolParser { + &self.solc + } + + /// Returns the parser used to parse Solc sources. + pub fn solc_mut(&mut self) -> &mut SolParser { + &mut self.solc + } + + /// Returns the parser used to parse Vyper sources. + pub fn vyper(&self) -> &VyperParser { + &self.vyper + } + + /// Returns the parser used to parse Vyper sources. + pub fn vyper_mut(&mut self) -> &mut VyperParser { + &mut self.vyper + } +} + /// Source parser for the [MultiCompiler]. Recognizes Solc and Vyper sources. #[derive(Clone, Debug)] pub enum MultiCompilerParsedSource { @@ -157,8 +204,8 @@ impl CompilerSettings for MultiCompilerSettings { self.solc.can_use_cached(&other.solc) && self.vyper.can_use_cached(&other.vyper) } - fn update_output_selection(&mut self, f: impl FnOnce(&mut OutputSelection) + Copy) { - self.solc.update_output_selection(f); + fn update_output_selection(&mut self, mut f: impl FnMut(&mut OutputSelection)) { + self.solc.update_output_selection(&mut f); self.vyper.update_output_selection(f); } @@ -277,7 +324,7 @@ impl CompilerInput for MultiCompilerInput { impl Compiler for MultiCompiler { type Input = MultiCompilerInput; type CompilationError = MultiCompilerError; - type ParsedSource = MultiCompilerParsedSource; + type Parser = MultiCompilerParser; type Settings = MultiCompilerSettings; type Language = MultiCompilerLanguage; type CompilerContract = Contract; @@ -317,20 +364,67 @@ impl Compiler for MultiCompiler { } } +impl SourceParser for MultiCompilerParser { + type ParsedSource = MultiCompilerParsedSource; + + fn new(config: &crate::ProjectPathsConfig) -> Self { + Self { solc: SolParser::new(config), vyper: VyperParser::new(config) } + } + + fn read(&mut self, path: &Path) -> Result> { + Ok(match guess_lang(path)? { + MultiCompilerLanguage::Solc(_) => { + self.solc.read(path)?.map_data(MultiCompilerParsedSource::Solc) + } + MultiCompilerLanguage::Vyper(_) => { + self.vyper.read(path)?.map_data(MultiCompilerParsedSource::Vyper) + } + }) + } + + fn parse_sources( + &mut self, + sources: &mut Sources, + ) -> Result)>> { + let mut vyper = Sources::new(); + sources.retain(|path, source| { + if let Ok(lang) = guess_lang(path) { + match lang { + MultiCompilerLanguage::Solc(_) => {} + MultiCompilerLanguage::Vyper(_) => { + vyper.insert(path.clone(), source.clone()); + return false; + } + } + } + true + }); + + let solc_nodes = self.solc.parse_sources(sources)?; + let vyper_nodes = self.vyper.parse_sources(&mut vyper)?; + Ok(solc_nodes + .into_iter() + .map(|(k, v)| (k, v.map_data(MultiCompilerParsedSource::Solc))) + .chain( + vyper_nodes + .into_iter() + .map(|(k, v)| (k, v.map_data(MultiCompilerParsedSource::Vyper))), + ) + .collect()) + } +} + impl ParsedSource for MultiCompilerParsedSource { type Language = MultiCompilerLanguage; - fn parse(content: &str, file: &std::path::Path) -> Result { - let Some(extension) = file.extension().and_then(|e| e.to_str()) else { - return Err(SolcError::msg("failed to resolve file extension")); - }; - - if SOLC_EXTENSIONS.contains(&extension) { - ::parse(content, file).map(Self::Solc) - } else if VYPER_EXTENSIONS.contains(&extension) { - VyperParsedSource::parse(content, file).map(Self::Vyper) - } else { - Err(SolcError::msg("unexpected file extension")) + fn parse(content: &str, file: &Path) -> Result { + match guess_lang(file)? { + MultiCompilerLanguage::Solc(_) => { + ::parse(content, file).map(Self::Solc) + } + MultiCompilerLanguage::Vyper(_) => { + VyperParsedSource::parse(content, file).map(Self::Vyper) + } } } @@ -389,6 +483,24 @@ impl ParsedSource for MultiCompilerParsedSource { } } +fn guess_lang(path: &Path) -> Result { + let extension = path + .extension() + .and_then(|e| e.to_str()) + .ok_or_else(|| SolcError::msg("failed to resolve file extension"))?; + if SOLC_EXTENSIONS.contains(&extension) { + Ok(MultiCompilerLanguage::Solc(match extension { + "sol" => SolcLanguage::Solidity, + "yul" => SolcLanguage::Yul, + _ => unreachable!(), + })) + } else if VYPER_EXTENSIONS.contains(&extension) { + Ok(MultiCompilerLanguage::Vyper(VyperLanguage::default())) + } else { + Err(SolcError::msg("unexpected file extension")) + } +} + impl CompilationError for MultiCompilerError { fn is_warning(&self) -> bool { match self { diff --git a/crates/compilers/src/compilers/solc/compiler.rs b/crates/compilers/src/compilers/solc/compiler.rs index 194b37f..8e67209 100644 --- a/crates/compilers/src/compilers/solc/compiler.rs +++ b/crates/compilers/src/compilers/solc/compiler.rs @@ -2,7 +2,7 @@ use crate::resolver::parse::SolData; use foundry_compilers_artifacts::{sources::Source, CompilerOutput, SolcInput}; use foundry_compilers_core::{ error::{Result, SolcError}, - utils::{self, SUPPORTS_BASE_PATH, SUPPORTS_INCLUDE_PATH}, + utils::{SUPPORTS_BASE_PATH, SUPPORTS_INCLUDE_PATH}, }; use itertools::Itertools; use semver::{Version, VersionReq}; @@ -54,7 +54,7 @@ pub static RELEASES: std::sync::LazyLock<(svm::Releases, Vec, bool)> = (releases, sorted_versions, true) } Err(err) => { - error!("{:?}", err); + error!("failed to deserialize SVM static RELEASES JSON: {err}"); Default::default() } } @@ -89,10 +89,9 @@ impl Solc { /// A new instance which points to `solc`. Invokes `solc --version` to determine the version. /// /// Returns error if `solc` is not found in the system or if the version cannot be retrieved. + #[instrument(name = "Solc::new", skip_all)] pub fn new(path: impl Into) -> Result { - let path = path.into(); - let version = Self::version(path.clone())?; - Ok(Self::new_with_version(path, version)) + Self::new_with_args(path, Vec::::new()) } /// A new instance which points to `solc` with additional cli arguments. Invokes `solc @@ -103,25 +102,38 @@ impl Solc { path: impl Into, extra_args: impl IntoIterator>, ) -> Result { - let args = extra_args.into_iter().map(Into::into).collect::>(); let path = path.into(); - let version = Self::version_with_args(path.clone(), &args)?; - - let mut solc = Self::new_with_version(path, version); - solc.extra_args = args; - - Ok(solc) + let extra_args = extra_args.into_iter().map(Into::into).collect::>(); + let version = Self::version_with_args(path.clone(), &extra_args)?; + Ok(Self::_new(path, version, extra_args)) } /// A new instance which points to `solc` with the given version pub fn new_with_version(path: impl Into, version: Version) -> Self { - Self { - solc: path.into(), + Self::_new(path.into(), version, Default::default()) + } + + fn _new(path: PathBuf, version: Version, extra_args: Vec) -> Self { + let this = Self { + solc: path, version, base_path: None, allow_paths: Default::default(), include_paths: Default::default(), - extra_args: Default::default(), + extra_args, + }; + this.debug_assert(); + this + } + + fn debug_assert(&self) { + if !cfg!(debug_assertions) { + return; + } + if let Ok(v) = Self::version_with_args(&self.solc, &self.extra_args) { + assert_eq!(v.major, self.version.major); + assert_eq!(v.minor, self.version.minor); + assert_eq!(v.patch, self.version.patch); } } @@ -200,17 +212,15 @@ impl Solc { /// /// Ok::<_, Box>(()) /// ``` + #[instrument(skip_all)] + #[cfg(feature = "svm-solc")] pub fn find_svm_installed_version(version: &Version) -> Result> { - let version = format!("{}.{}.{}", version.major, version.minor, version.patch); - let solc = Self::svm_home() - .ok_or_else(|| SolcError::msg("svm home dir not found"))? - .join(&version) - .join(format!("solc-{version}")); - + let version = Version::new(version.major, version.minor, version.patch); + let solc = svm::version_binary(&version.to_string()); if !solc.is_file() { return Ok(None); } - Self::new(&solc).map(Some) + Ok(Some(Self::new_with_version(&solc, version))) } /// Returns the directory in which [svm](https://github.com/roynalnaruto/svm-rs) stores all versions @@ -218,14 +228,9 @@ impl Solc { /// This will be: /// - `~/.svm` on unix, if it exists /// - $XDG_DATA_HOME (~/.local/share/svm) if the svm folder does not exist. + #[cfg(feature = "svm-solc")] pub fn svm_home() -> Option { - if let Some(home_dir) = home::home_dir() { - let home_dot_svm = home_dir.join(".svm"); - if home_dot_svm.exists() { - return Some(home_dot_svm); - } - } - dirs::data_dir().map(|dir| dir.join("svm")) + Some(svm::data_dir().to_path_buf()) } /// Returns the `semver::Version` [svm](https://github.com/roynalnaruto/svm-rs)'s `.global_version` is currently set to. @@ -233,23 +238,21 @@ impl Solc { /// /// This will read the version string (eg: "0.8.9") that the `~/.svm/.global_version` file /// contains + #[cfg(feature = "svm-solc")] pub fn svm_global_version() -> Option { - let home = Self::svm_home()?; - let version = std::fs::read_to_string(home.join(".global_version")).ok()?; - Version::parse(&version).ok() + svm::get_global_version().ok().flatten() } /// Returns the list of all solc instances installed at `SVM_HOME` + #[cfg(feature = "svm-solc")] pub fn installed_versions() -> Vec { - Self::svm_home() - .map(|home| utils::installed_versions(&home).unwrap_or_default()) - .unwrap_or_default() + svm::installed_versions().unwrap_or_default() } /// Returns the list of all versions that are available to download #[cfg(feature = "svm-solc")] pub fn released_versions() -> Vec { - RELEASES.1.clone().into_iter().collect() + RELEASES.1.clone() } /// Installs the provided version of Solc in the machine under the svm dir and returns the @@ -266,6 +269,7 @@ impl Solc { /// # } /// ``` #[cfg(feature = "svm-solc")] + #[instrument(name = "Solc::install", skip_all)] pub async fn install(version: &Version) -> std::result::Result { trace!("installing solc version \"{}\"", version); crate::report::solc_installation_start(version); @@ -283,6 +287,7 @@ impl Solc { /// Blocking version of `Self::install` #[cfg(feature = "svm-solc")] + #[instrument(name = "Solc::blocking_install", skip_all)] pub fn blocking_install(version: &Version) -> std::result::Result { use foundry_compilers_core::utils::RuntimeOrHandle; @@ -293,7 +298,7 @@ impl Solc { trace!("blocking installing solc version \"{}\"", version); crate::report::solc_installation_start(&version); - // The async version `svm::install` is used instead of `svm::blocking_intsall` + // The async version `svm::install` is used instead of `svm::blocking_install` // because the underlying `reqwest::blocking::Client` does not behave well // inside of a Tokio runtime. See: https://github.com/seanmonstar/reqwest/issues/1017 match RuntimeOrHandle::new().block_on(svm::install(&version)) { @@ -311,6 +316,7 @@ impl Solc { /// Verify that the checksum for this version of solc is correct. We check against the SHA256 /// checksum from the build information published by [binaries.soliditylang.org](https://binaries.soliditylang.org/) #[cfg(feature = "svm-solc")] + #[instrument(name = "Solc::verify_checksum", skip_all)] pub fn verify_checksum(&self) -> Result<()> { let version = self.version_short(); let mut version_path = svm::version_path(version.to_string().as_str()); @@ -407,6 +413,7 @@ impl Solc { } /// Compiles with `--standard-json` and deserializes the output as the given `D`. + #[instrument(name = "Solc::compile", skip_all)] pub fn compile_as(&self, input: &T) -> Result { let output = self.compile_output(input)?; @@ -417,7 +424,7 @@ impl Solc { } /// Compiles with `--standard-json` and returns the raw `stdout` output. - #[instrument(name = "compile", level = "debug", skip_all)] + #[instrument(name = "Solc::compile_raw", skip_all)] pub fn compile_output(&self, input: &T) -> Result> { let mut cmd = self.configure_cmd(); @@ -440,42 +447,41 @@ impl Solc { compile_output(output) } - /// Invokes `solc --version` and parses the output as a SemVer [`Version`], stripping the - /// pre-release and build metadata. + /// Returns the SemVer [`Version`], stripping the pre-release and build metadata. pub fn version_short(&self) -> Version { Version::new(self.version.major, self.version.minor, self.version.patch) } /// Invokes `solc --version` and parses the output as a SemVer [`Version`]. - #[instrument(level = "debug", skip_all)] pub fn version(solc: impl Into) -> Result { Self::version_with_args(solc, &[]) } /// Invokes `solc --version` and parses the output as a SemVer [`Version`]. - #[instrument(level = "debug", skip_all)] pub fn version_with_args(solc: impl Into, args: &[String]) -> Result { - crate::cache_version(solc.into(), args, |solc| { - let mut cmd = Command::new(solc); - cmd.args(args) - .arg("--version") - .stdin(Stdio::piped()) - .stderr(Stdio::piped()) - .stdout(Stdio::piped()); - debug!(?cmd, "getting Solc version"); - let output = cmd.output().map_err(|e| SolcError::io(e, solc))?; - trace!(?output); - let version = version_from_output(output)?; - debug!(%version); - Ok(version) - }) + crate::cache_version(solc.into(), args, |solc| Self::version_impl(solc, args)) + } + + fn version_impl(solc: &Path, args: &[String]) -> Result { + let mut cmd = Command::new(solc); + cmd.args(args) + .arg("--version") + .stdin(Stdio::piped()) + .stderr(Stdio::piped()) + .stdout(Stdio::piped()); + debug!(?cmd, "getting Solc version"); + let output = cmd.output().map_err(|e| SolcError::io(e, solc))?; + trace!(?output); + let version = version_from_output(output)?; + debug!(%version); + Ok(version) } fn map_io_err(&self) -> impl FnOnce(std::io::Error) -> SolcError + '_ { move |err| SolcError::io(err, &self.solc) } - /// Configures [Command] object depeending on settings and solc version used. + /// Configures [Command] object depending on settings and solc version used. /// Some features are only supported by newer versions of solc, so we have to disable them for /// older ones. pub fn configure_cmd(&self) -> Command { @@ -755,7 +761,7 @@ mod tests { // This test does not take the lock by default, so we need to manually add it here. take_solc_installer_lock!(_lock); let version = Version::new(0, 8, 6); - if utils::installed_versions(svm::data_dir()) + if svm::installed_versions() .map(|versions| !versions.contains(&version)) .unwrap_or_default() { diff --git a/crates/compilers/src/compilers/solc/mod.rs b/crates/compilers/src/compilers/solc/mod.rs index e4381aa..bf7792e 100644 --- a/crates/compilers/src/compilers/solc/mod.rs +++ b/crates/compilers/src/compilers/solc/mod.rs @@ -2,8 +2,13 @@ use super::{ restrictions::CompilerSettingsRestrictions, CompilationError, Compiler, CompilerInput, CompilerOutput, CompilerSettings, CompilerVersion, Language, ParsedSource, }; -use crate::resolver::parse::SolData; -pub use foundry_compilers_artifacts::SolcLanguage; +use crate::{ + resolver::{ + parse::{SolData, SolParser}, + Node, + }, + SourceParser, +}; use foundry_compilers_artifacts::{ error::SourceLocation, output_selection::OutputSelection, @@ -11,7 +16,8 @@ use foundry_compilers_artifacts::{ sources::{Source, Sources}, BytecodeHash, Contract, Error, EvmVersion, Settings, Severity, SolcInput, }; -use foundry_compilers_core::error::Result; +use foundry_compilers_core::error::{Result, SolcError, SolcIoError}; +use rayon::prelude::*; use semver::Version; use serde::{Deserialize, Serialize}; use std::{ @@ -20,6 +26,9 @@ use std::{ ops::{Deref, DerefMut}, path::{Path, PathBuf}, }; + +pub use foundry_compilers_artifacts::SolcLanguage; + mod compiler; pub use compiler::{Solc, SOLC_EXTENSIONS}; @@ -40,7 +49,7 @@ impl Language for SolcLanguage { impl Compiler for SolcCompiler { type Input = SolcVersionedInput; type CompilationError = Error; - type ParsedSource = SolData; + type Parser = SolParser; type Settings = SolcSettings; type Language = SolcLanguage; type CompilerContract = Contract; @@ -279,8 +288,8 @@ impl CompilerSettingsRestrictions for SolcRestrictions { impl CompilerSettings for SolcSettings { type Restrictions = SolcRestrictions; - fn update_output_selection(&mut self, f: impl FnOnce(&mut OutputSelection) + Copy) { - f(&mut self.settings.output_selection) + fn update_output_selection(&mut self, mut f: impl FnMut(&mut OutputSelection)) { + f(&mut self.settings.output_selection); } fn can_use_cached(&self, other: &Self) -> bool { @@ -297,7 +306,6 @@ impl CompilerSettings for SolcSettings { via_ir, debug, libraries, - eof_version, }, .. } = self; @@ -311,7 +319,6 @@ impl CompilerSettings for SolcSettings { && *via_ir == other.settings.via_ir && *debug == other.settings.debug && *libraries == other.settings.libraries - && *eof_version == other.settings.eof_version && output_selection.is_subset_of(&other.settings.output_selection) } @@ -357,6 +364,91 @@ impl CompilerSettings for SolcSettings { } } +impl SourceParser for SolParser { + type ParsedSource = SolData; + + fn new(config: &crate::ProjectPathsConfig) -> Self { + Self { + compiler: solar::sema::Compiler::new(Self::session_with_opts( + solar::sema::interface::config::Opts { + include_paths: config.include_paths.iter().cloned().collect(), + base_path: Some(config.root.clone()), + import_remappings: config + .remappings + .iter() + .map(|r| solar::sema::interface::config::ImportRemapping { + context: r.context.clone().unwrap_or_default(), + prefix: r.name.clone(), + path: r.path.clone(), + }) + .collect(), + ..Default::default() + }, + )), + } + } + + fn read(&mut self, path: &Path) -> Result> { + let mut sources = Sources::from_iter([(path.to_path_buf(), Source::read_(path)?)]); + let nodes = self.parse_sources(&mut sources)?; + debug_assert_eq!(nodes.len(), 1, "{nodes:#?}"); + Ok(nodes.into_iter().next().unwrap().1) + } + + fn parse_sources( + &mut self, + sources: &mut Sources, + ) -> Result)>> { + self.compiler_mut().enter_mut(|compiler| { + let mut pcx = compiler.parse(); + let files = sources + .par_iter() + .map(|(path, source)| { + pcx.sess + .source_map() + .new_source_file(path.clone(), source.content.as_str()) + .map_err(|e| SolcError::Io(SolcIoError::new(e, path))) + }) + .collect::>>()?; + pcx.add_files(files); + pcx.parse(); + + let parsed = sources.par_iter().map(|(path, source)| { + let sf = compiler.sess().source_map().get_file(path).unwrap(); + let (_, s) = compiler.gcx().sources.get_file(&sf).unwrap(); + let node = Node::new( + path.clone(), + source.clone(), + SolData::parse_from(compiler.gcx().sess, s), + ); + (path.clone(), node) + }); + let mut parsed = parsed.collect::>(); + + // Set error on the first successful source, if any. This doesn't really have to be + // exact, as long as at least one source has an error set it should be enough. + if let Some(Err(diag)) = compiler.gcx().sess.emitted_errors() { + if let Some(idx) = parsed + .iter() + .position(|(_, node)| node.data.parse_result.is_ok()) + .or_else(|| parsed.first().map(|_| 0)) + { + let (_, node) = &mut parsed[idx]; + node.data.parse_result = Err(diag.to_string()); + } + } + + for (path, node) in &parsed { + if let Err(e) = &node.data.parse_result { + debug!("failed parsing {}: {e}", path.display()); + } + } + + Ok(parsed) + }) + } +} + impl ParsedSource for SolData { type Language = SolcLanguage; diff --git a/crates/compilers/src/compilers/vyper/mod.rs b/crates/compilers/src/compilers/vyper/mod.rs index 8304b76..88034cb 100644 --- a/crates/compilers/src/compilers/vyper/mod.rs +++ b/crates/compilers/src/compilers/vyper/mod.rs @@ -1,6 +1,7 @@ -use self::{input::VyperVersionedInput, parser::VyperParsedSource}; +use self::input::VyperVersionedInput; use super::{Compiler, CompilerOutput, Language}; pub use crate::artifacts::vyper::{VyperCompilationError, VyperInput, VyperOutput, VyperSettings}; +use crate::parser::VyperParser; use core::fmt; use foundry_compilers_artifacts::{sources::Source, Contract}; use foundry_compilers_core::error::{Result, SolcError}; @@ -26,7 +27,7 @@ pub const VYPER_EXTENSIONS: &[&str] = &["vy", "vyi"]; pub const VYPER_INTERFACE_EXTENSION: &str = "vyi"; /// Vyper language, used as [Compiler::Language] for the Vyper compiler. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] #[non_exhaustive] pub struct VyperLanguage; @@ -127,6 +128,7 @@ impl Vyper { } /// Compiles with `--standard-json` and deserializes the output as the given `D`. + #[instrument(name = "Vyper::compile", skip_all)] pub fn compile_as(&self, input: &T) -> Result { let output = self.compile_output(input)?; @@ -139,7 +141,7 @@ impl Vyper { } /// Compiles with `--standard-json` and returns the raw `stdout` output. - #[instrument(name = "compile", level = "debug", skip_all)] + #[instrument(name = "Vyper::compile_raw", skip_all)] pub fn compile_output(&self, input: &T) -> Result> { let mut cmd = Command::new(&self.path); cmd.arg("--standard-json") @@ -171,7 +173,6 @@ impl Vyper { } /// Invokes `vyper --version` and parses the output as a SemVer [`Version`]. - #[instrument(level = "debug", skip_all)] pub fn version(vyper: impl Into) -> Result { crate::cache_version(vyper.into(), &[], |vyper| { let mut cmd = Command::new(vyper); @@ -201,7 +202,7 @@ impl Vyper { impl Compiler for Vyper { type Settings = VyperSettings; type CompilationError = VyperCompilationError; - type ParsedSource = VyperParsedSource; + type Parser = VyperParser; type Input = VyperVersionedInput; type Language = VyperLanguage; type CompilerContract = Contract; diff --git a/crates/compilers/src/compilers/vyper/parser.rs b/crates/compilers/src/compilers/vyper/parser.rs index a672056..b2c6e4e 100644 --- a/crates/compilers/src/compilers/vyper/parser.rs +++ b/crates/compilers/src/compilers/vyper/parser.rs @@ -1,7 +1,7 @@ use super::VyperLanguage; use crate::{ compilers::{vyper::VYPER_EXTENSIONS, ParsedSource}, - ProjectPathsConfig, + ProjectPathsConfig, SourceParser, }; use foundry_compilers_core::{ error::{Result, SolcError}, @@ -28,6 +28,19 @@ pub struct VyperImport { pub final_part: Option, } +#[derive(Clone, Debug, Default)] +pub struct VyperParser { + _inner: (), +} + +impl SourceParser for VyperParser { + type ParsedSource = VyperParsedSource; + + fn new(_config: &ProjectPathsConfig) -> Self { + Self { _inner: () } + } +} + #[derive(Clone, Debug)] pub struct VyperParsedSource { path: PathBuf, @@ -38,6 +51,7 @@ pub struct VyperParsedSource { impl ParsedSource for VyperParsedSource { type Language = VyperLanguage; + #[instrument(name = "VyperParsedSource::parse", skip_all)] fn parse(content: &str, file: &Path) -> Result { let version_req = capture_outer_and_inner(content, &RE_VYPER_VERSION, &["version"]) .first() diff --git a/crates/compilers/src/compilers/vyper/settings.rs b/crates/compilers/src/compilers/vyper/settings.rs index 2a815d6..0a4b0f5 100644 --- a/crates/compilers/src/compilers/vyper/settings.rs +++ b/crates/compilers/src/compilers/vyper/settings.rs @@ -21,8 +21,8 @@ impl CompilerSettingsRestrictions for VyperRestrictions { impl CompilerSettings for VyperSettings { type Restrictions = VyperRestrictions; - fn update_output_selection(&mut self, f: impl FnOnce(&mut OutputSelection)) { - f(&mut self.output_selection) + fn update_output_selection(&mut self, mut f: impl FnMut(&mut OutputSelection)) { + f(&mut self.output_selection); } fn can_use_cached(&self, other: &Self) -> bool { diff --git a/crates/compilers/src/config.rs b/crates/compilers/src/config.rs index 356d80b..326bd32 100644 --- a/crates/compilers/src/config.rs +++ b/crates/compilers/src/config.rs @@ -2,7 +2,7 @@ use crate::{ cache::SOLIDITY_FILES_CACHE_FILENAME, compilers::{multi::MultiCompilerLanguage, Language}, flatten::{collect_ordered_deps, combine_version_pragmas}, - resolver::{parse::SolData, SolImportAlias}, + resolver::{parse::SolParser, SolImportAlias}, Graph, }; use foundry_compilers_artifacts::{ @@ -110,7 +110,7 @@ impl ProjectPathsConfig { } let sources = Source::read_all_files(input_files)?; - let graph = Graph::::resolve_sources(self, sources)?; + let graph = Graph::::resolve_sources(self, sources)?; let ordered_deps = collect_ordered_deps(&flatten_target, self, &graph)?; #[cfg(windows)] @@ -524,7 +524,12 @@ impl ProjectPathsConfig { }) .find_map(|r| { import.strip_prefix(&r.name).ok().map(|stripped_import| { - let lib_path = Path::new(&r.path).join(stripped_import); + let lib_path = + if stripped_import.as_os_str().is_empty() && r.path.ends_with(".sol") { + r.path.clone().into() + } else { + Path::new(&r.path).join(stripped_import) + }; // we handle the edge case where the path of a remapping ends with "contracts" // (`/=.../contracts`) and the stripped import also starts with @@ -544,36 +549,14 @@ impl ProjectPathsConfig { } } - pub fn with_language(self) -> ProjectPathsConfig { - let Self { - root, - cache, - artifacts, - build_infos, - sources, - tests, - scripts, - libraries, - remappings, - include_paths, - allowed_paths, - _l, - } = self; + pub fn with_language_ref(&self) -> &ProjectPathsConfig { + // SAFETY: `Lang` is `PhantomData`. + unsafe { std::mem::transmute(self) } + } - ProjectPathsConfig { - root, - cache, - artifacts, - build_infos, - sources, - tests, - scripts, - libraries, - remappings, - include_paths, - allowed_paths, - _l: PhantomData, - } + pub fn with_language(self) -> ProjectPathsConfig { + // SAFETY: `Lang` is `PhantomData`. + unsafe { std::mem::transmute(self) } } pub fn apply_lib_remappings(&self, mut libraries: Libraries) -> Libraries { @@ -633,7 +616,7 @@ impl ProjectPathsConfig { /// Returns the combined set of `Self::read_sources` + `Self::read_tests` + `Self::read_scripts` pub fn read_input_files(&self) -> Result { - Ok(Source::read_all_files(self.input_files())?) + Ok(Source::read_all(self.input_files_iter())?) } } @@ -1196,4 +1179,39 @@ mod tests { dependency.join("A.sol") ); } + + #[test] + fn can_resolve_single_file_mapped_import() { + let dir = tempfile::tempdir().unwrap(); + let mut config = ProjectPathsConfig::builder().root(dir.path()).build::<()>().unwrap(); + config.create_all().unwrap(); + + fs::write( + config.sources.join("A.sol"), + r#"pragma solidity ^0.8.0; import "@my-lib/B.sol"; contract A is B {}"#, + ) + .unwrap(); + + let dependency = config.root.join("my-lib"); + fs::create_dir(&dependency).unwrap(); + fs::write(dependency.join("B.sol"), r"pragma solidity ^0.8.0; contract B {}").unwrap(); + + config.remappings.push(Remapping { + context: None, + name: "@my-lib/B.sol".into(), + path: "my-lib/B.sol".into(), + }); + + // Test that single file import / remapping resolves to file. + assert!(config + .resolve_import_and_include_paths( + &config.sources, + Path::new("@my-lib/B.sol"), + &mut Default::default(), + ) + .unwrap() + .to_str() + .unwrap() + .ends_with("my-lib/B.sol")); + } } diff --git a/crates/compilers/src/filter.rs b/crates/compilers/src/filter.rs index 979b1e5..aa25914 100644 --- a/crates/compilers/src/filter.rs +++ b/crates/compilers/src/filter.rs @@ -3,7 +3,7 @@ use crate::{ compilers::{multi::MultiCompilerParsedSource, CompilerSettings, ParsedSource}, resolver::{parse::SolData, GraphEdges}, - Sources, + SourceParser, Sources, }; use foundry_compilers_artifacts::output_selection::OutputSelection; use std::{ @@ -101,11 +101,11 @@ impl<'a> SparseOutputFilter<'a> { /// /// This also takes the project's graph as input, this allows us to check if the files the /// filter matches depend on libraries that need to be linked - pub fn sparse_sources( + pub fn sparse_sources( &self, sources: &Sources, settings: &mut S, - graph: &GraphEdges, + graph: &GraphEdges

, ) -> Vec { let mut full_compilation: HashSet = sources .dirty_files() diff --git a/crates/compilers/src/flatten.rs b/crates/compilers/src/flatten.rs index de629cc..5ec755d 100644 --- a/crates/compilers/src/flatten.rs +++ b/crates/compilers/src/flatten.rs @@ -3,7 +3,7 @@ use crate::{ compilers::{Compiler, ParsedSource}, filter::MaybeSolData, resolver::parse::SolData, - ArtifactOutput, CompilerSettings, Graph, Project, ProjectPathsConfig, Updates, + ArtifactOutput, CompilerSettings, Graph, Project, ProjectPathsConfig, SourceParser, Updates, }; use foundry_compilers_artifacts::{ ast::{visitor::Visitor, *}, @@ -192,7 +192,7 @@ impl Flattener { target: &Path, ) -> std::result::Result where - C::ParsedSource: MaybeSolData, + C::Parser: SourceParser, { // Configure project to compile the target file and only request AST for target file. project.cached = false; @@ -209,8 +209,8 @@ impl Flattener { let output = output.compiler_output; - let sources = Source::read_all_files(vec![target.to_path_buf()])?; - let graph = Graph::::resolve_sources(&project.paths, sources)?; + let sources = Source::read_all([target.to_path_buf()])?; + let graph = Graph::::resolve_sources(&project.paths, sources)?; let ordered_sources = collect_ordered_deps(target, &project.paths, &graph)?; @@ -317,10 +317,10 @@ impl Flattener { // `loc.path` is expected to be different for each id because there can't be 2 // top-level declarations with the same name in the same file. // - // Sorting by index loc.path in sorted files to make the renaming process - // deterministic. + // Sorting by index loc.path and loc.start in sorted files to make the renaming + // process deterministic. ids.sort_by_key(|(_, loc)| { - self.ordered_sources.iter().position(|p| p == &loc.path).unwrap() + (self.ordered_sources.iter().position(|p| p == &loc.path).unwrap(), loc.start) }); } for (i, (id, loc)) in ids.iter().enumerate() { @@ -794,10 +794,10 @@ impl Flattener { } /// Performs DFS to collect all dependencies of a target -fn collect_deps( +fn collect_deps>( path: &Path, - paths: &ProjectPathsConfig, - graph: &Graph, + paths: &ProjectPathsConfig<::Language>, + graph: &Graph

, deps: &mut HashSet, ) -> Result<()> { if deps.insert(path.to_path_buf()) { @@ -830,10 +830,10 @@ fn collect_deps( /// Instead, we sort files by the number of their dependencies (imports of any depth) in ascending /// order. If files have the same number of dependencies, we sort them alphabetically. /// Target file is always placed last. -pub fn collect_ordered_deps( +pub fn collect_ordered_deps>( path: &Path, - paths: &ProjectPathsConfig, - graph: &Graph, + paths: &ProjectPathsConfig<::Language>, + graph: &Graph

, ) -> Result> { let mut deps = HashSet::new(); collect_deps(path, paths, graph, &mut deps)?; diff --git a/crates/compilers/src/lib.rs b/crates/compilers/src/lib.rs index bd4bc98..ae7d970 100644 --- a/crates/compilers/src/lib.rs +++ b/crates/compilers/src/lib.rs @@ -64,8 +64,10 @@ use foundry_compilers_core::error::{Result, SolcError, SolcIoError}; use output::sources::{VersionedSourceFile, VersionedSourceFiles}; use project::ProjectCompiler; use semver::Version; -use solar_parse::Parser; -use solar_sema::interface::{diagnostics::EmittedDiagnostics, source_map::FileName, Session}; +use solar::parse::{ + interface::{diagnostics::EmittedDiagnostics, source_map::FileName, Session}, + Parser, +}; use solc::SolcSettings; use std::{ collections::{BTreeMap, BTreeSet, HashMap, HashSet}, @@ -173,7 +175,7 @@ where /// Returns standard-json-input to compile the target contract pub fn standard_json_input(&self, target: &Path) -> Result { trace!(?target, "Building standard-json-input"); - let graph = Graph::::resolve(&self.paths)?; + let graph = Graph::::resolve(&self.paths)?; let target_index = graph.files().get(target).ok_or_else(|| { SolcError::msg(format!("cannot resolve file at {:?}", target.display())) })?; @@ -389,7 +391,7 @@ impl, C: Compiler> Pro T: Clone, C: Clone, { - let graph = Graph::::resolve(&self.paths)?; + let graph = Graph::::resolve(&self.paths)?; let mut contracts: HashMap> = HashMap::new(); if !graph.is_empty() { for node in &graph.nodes { @@ -428,10 +430,10 @@ impl, C: Compiler> Pro /// Invokes [CompilerSettings::update_output_selection] on the project's settings and all /// additional settings profiles. - pub fn update_output_selection(&mut self, f: impl FnOnce(&mut OutputSelection) + Copy) { - self.settings.update_output_selection(f); + pub fn update_output_selection(&mut self, mut f: impl FnMut(&mut OutputSelection)) { + self.settings.update_output_selection(&mut f); self.additional_settings.iter_mut().for_each(|(_, s)| { - s.update_output_selection(f); + s.update_output_selection(&mut f); }); } } @@ -923,15 +925,15 @@ pub fn replace_source_content( pub(crate) fn parse_one_source( content: &str, path: &Path, - f: impl FnOnce(solar_sema::ast::SourceUnit<'_>) -> R, + f: impl FnOnce(&Session, &solar::parse::ast::SourceUnit<'_>) -> R, ) -> Result { let sess = Session::builder().with_buffer_emitter(Default::default()).build(); - let res = sess.enter(|| -> solar_parse::interface::Result<_> { - let arena = solar_parse::ast::Arena::new(); + let res = sess.enter_sequential(|| -> solar::parse::interface::Result<_> { + let arena = solar::parse::ast::Arena::new(); let filename = FileName::Real(path.to_path_buf()); let mut parser = Parser::from_source_code(&sess, &arena, filename, content.to_string())?; let ast = parser.parse_file().map_err(|e| e.emit())?; - Ok(f(ast)) + Ok(f(&sess, &ast)) }); // Return if any diagnostics emitted during content parsing. @@ -946,11 +948,10 @@ pub(crate) fn parse_one_source( #[cfg(test)] #[cfg(feature = "svm-solc")] mod tests { + use super::*; use foundry_compilers_artifacts::Remapping; use foundry_compilers_core::utils::{self, mkdir_or_touch, tempdir}; - use super::*; - #[test] #[cfg_attr(windows, ignore = "<0.7 solc is flaky")] fn test_build_all_versions() { diff --git a/crates/compilers/src/project_util/mock.rs b/crates/compilers/src/project_util/mock.rs index 4d47f91..0964cb7 100644 --- a/crates/compilers/src/project_util/mock.rs +++ b/crates/compilers/src/project_util/mock.rs @@ -2,11 +2,7 @@ use foundry_compilers_artifacts::Remapping; use foundry_compilers_core::error::{Result, SolcError}; -use rand::{ - distributions::{Distribution, Uniform}, - seq::SliceRandom, - Rng, -}; +use rand::{seq::SliceRandom, Rng}; use serde::{Deserialize, Serialize}; use std::{ collections::{BTreeSet, HashMap, HashSet, VecDeque}, @@ -14,9 +10,8 @@ use std::{ }; use crate::{ - compilers::{multi::MultiCompilerParsedSource, Language, ParsedSource}, - resolver::GraphEdges, - Graph, ProjectPathsConfig, + compilers::Language, multi::MultiCompilerParser, resolver::GraphEdges, Graph, + ProjectPathsConfig, SourceParser, }; /// Represents the layout of a project @@ -55,9 +50,9 @@ impl MockProjectGenerator { } /// Create a skeleton of a real project - pub fn create(paths: &ProjectPathsConfig) -> Result { - fn get_libs( - edges: &GraphEdges, + pub fn create(paths: &ProjectPathsConfig) -> Result { + fn get_libs( + edges: &GraphEdges

, lib_folder: &Path, ) -> Option>> { let mut libs: HashMap<_, Vec<_>> = HashMap::new(); @@ -69,12 +64,12 @@ impl MockProjectGenerator { Some(libs) } - let graph = Graph::::resolve(paths)?; - let mut gen = Self::default(); + let graph = Graph::::resolve(paths)?; + let mut generated = Self::default(); let (_, edges) = graph.into_sources(); // add all files as source files - gen.add_sources(edges.files().count()); + generated.add_sources(edges.files().count()); // stores libs and their files let libs = get_libs( @@ -85,25 +80,25 @@ impl MockProjectGenerator { // mark all files as libs for (lib_id, lib_files) in libs.into_values().enumerate() { - let lib_name = gen.name_strategy.new_lib_name(lib_id); - let offset = gen.inner.files.len(); + let lib_name = generated.name_strategy.new_lib_name(lib_id); + let offset = generated.inner.files.len(); let lib = MockLib { name: lib_name, id: lib_id, num_files: lib_files.len(), offset }; for lib_file in lib_files { - let file = &mut gen.inner.files[lib_file]; + let file = &mut generated.inner.files[lib_file]; file.lib_id = Some(lib_id); - file.name = gen.name_strategy.new_lib_name(file.id); + file.name = generated.name_strategy.new_lib_name(file.id); } - gen.inner.libraries.push(lib); + generated.inner.libraries.push(lib); } for id in edges.files() { for import in edges.imported_nodes(id).iter().copied() { - let import = gen.get_import(import); - gen.inner.files[id].imports.insert(import); + let import = generated.get_import(import); + generated.inner.files[id].imports.insert(import); } } - Ok(gen) + Ok(generated) } /// Consumes the type and returns the underlying skeleton @@ -243,34 +238,36 @@ impl MockProjectGenerator { self } - /// randomly assign empty file status so that mocked files don't emit artifacts + /// Randomly assign empty file status so that mocked files don't emit artifacts. pub fn assign_empty_files(&mut self) -> &mut Self { - let mut rng = rand::thread_rng(); - let die = Uniform::from(0..self.inner.files.len()); + let mut rng = rand::rng(); + let n = self.inner.files.len(); + for file in self.inner.files.iter_mut() { - let throw = die.sample(&mut rng); + let throw = rng.random_range(0..n); if throw == 0 { - // give it a 1 in num(files) chance that the file will be empty + // 1 in n chance that the file will be empty file.emit_artifacts = false; } } + self } /// Populates the imports of the project pub fn populate_imports(&mut self, settings: &MockProjectSettings) -> &mut Self { - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); // populate imports for id in 0..self.inner.files.len() { let imports = if let Some(lib) = self.inner.files[id].lib_id { let num_imports = rng - .gen_range(settings.min_imports..=settings.max_imports) + .random_range(settings.min_imports..=settings.max_imports) .min(self.inner.libraries[lib].num_files.saturating_sub(1)); self.unique_imports_for_lib(&mut rng, lib, id, num_imports) } else { let num_imports = rng - .gen_range(settings.min_imports..=settings.max_imports) + .random_range(settings.min_imports..=settings.max_imports) .min(self.inner.files.len().saturating_sub(1)); self.unique_imports_for_source(&mut rng, id, num_imports) }; @@ -437,11 +434,16 @@ impl MockFile { pub fn target_path( &self, - gen: &MockProjectGenerator, + generated: &MockProjectGenerator, paths: &ProjectPathsConfig, ) -> PathBuf { let mut target = if let Some(lib) = self.lib_id { - paths.root.join("lib").join(&gen.inner.libraries[lib].name).join("src").join(&self.name) + paths + .root + .join("lib") + .join(&generated.inner.libraries[lib].name) + .join("src") + .join(&self.name) } else { paths.sources.join(&self.name) }; @@ -552,14 +554,14 @@ pub struct MockProjectSettings { impl MockProjectSettings { /// Generates a new instance with random settings within an arbitrary range pub fn random() -> Self { - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); // arbitrary thresholds Self { - num_sources: rng.gen_range(2..25), - num_libs: rng.gen_range(0..5), - num_lib_files: rng.gen_range(1..10), - min_imports: rng.gen_range(0..3), - max_imports: rng.gen_range(4..10), + num_sources: rng.random_range(2..25), + num_libs: rng.random_range(0..5), + num_lib_files: rng.random_range(1..10), + min_imports: rng.random_range(0..3), + max_imports: rng.random_range(4..10), allow_no_artifacts_files: true, } } diff --git a/crates/compilers/src/resolver/mod.rs b/crates/compilers/src/resolver/mod.rs index 4b0c722..4a9cc85 100644 --- a/crates/compilers/src/resolver/mod.rs +++ b/crates/compilers/src/resolver/mod.rs @@ -46,18 +46,17 @@ //! which is defined on a per source file basis. use crate::{ - compilers::{Compiler, CompilerVersion, Language, ParsedSource}, + compilers::{Compiler, CompilerVersion, ParsedSource}, project::VersionedSources, - ArtifactOutput, CompilerSettings, Project, ProjectPathsConfig, + resolver::parse::SolParser, + ArtifactOutput, CompilerSettings, Project, ProjectPathsConfig, SourceParser, }; use core::fmt; use foundry_compilers_artifacts::sources::{Source, Sources}; use foundry_compilers_core::{ error::{Result, SolcError}, - utils::{self, find_case_sensitive_existing_file}, + utils, }; -use parse::SolData; -use rayon::prelude::*; use semver::{Version, VersionReq}; use std::{ collections::{BTreeSet, HashMap, HashSet, VecDeque}, @@ -89,15 +88,15 @@ pub struct ResolvedSources<'a, C: Compiler> { /// a profile suffix. pub primary_profiles: HashMap, /// Graph edges. - pub edges: GraphEdges, + pub edges: GraphEdges, } /// The underlying edges of the graph which only contains the raw relationship data. /// /// This is kept separate from the `Graph` as the `Node`s get consumed when the `Solc` to `Sources` /// set is determined. -#[derive(Debug)] -pub struct GraphEdges { +#[derive(Clone, Debug)] +pub struct GraphEdges { /// The indices of `edges` correspond to the `nodes`. That is, `edges[0]` /// is the set of outgoing edges for `nodes[0]`. edges: Vec>, @@ -109,8 +108,10 @@ pub struct GraphEdges { rev_indices: HashMap, /// the identified version requirement of a file versions: HashMap>, - /// the extracted data from the source file - data: HashMap, + /// the extracted data from the source files + data: Vec, + /// The parser which parsed `data`. + parser: Option

, /// with how many input files we started with, corresponds to `let input_files = /// nodes[..num_input_files]`. /// @@ -127,7 +128,34 @@ pub struct GraphEdges { resolved_solc_include_paths: BTreeSet, } -impl GraphEdges { +impl Default for GraphEdges

{ + fn default() -> Self { + Self { + edges: Default::default(), + rev_edges: Default::default(), + indices: Default::default(), + rev_indices: Default::default(), + versions: Default::default(), + data: Default::default(), + parser: Default::default(), + num_input_files: Default::default(), + unresolved_imports: Default::default(), + resolved_solc_include_paths: Default::default(), + } + } +} + +impl GraphEdges

{ + /// Returns the parser used to parse the sources. + pub fn parser(&self) -> &P { + self.parser.as_ref().unwrap() + } + + /// Returns the parser used to parse the sources. + pub fn parser_mut(&mut self) -> &mut P { + self.parser.as_mut().unwrap() + } + /// How many files are source files pub fn num_source_files(&self) -> usize { self.num_input_files @@ -212,8 +240,11 @@ impl GraphEdges { } /// Returns the parsed source data for the given file - pub fn get_parsed_source(&self, file: &Path) -> Option<&D> { - self.indices.get(file).and_then(|idx| self.data.get(idx)) + pub fn get_parsed_source(&self, file: &Path) -> Option<&P::ParsedSource> + where + P: SourceParser, + { + self.indices.get(file).and_then(|idx| self.data.get(*idx)) } } @@ -223,16 +254,23 @@ impl GraphEdges { /// /// See also #[derive(Debug)] -pub struct Graph { +pub struct Graph { /// all nodes in the project, a `Node` represents a single file - pub nodes: Vec>, + pub nodes: Vec>, /// relationship of the nodes - edges: GraphEdges, + edges: GraphEdges

, /// the root of the project this graph represents root: PathBuf, } -impl> Graph { +type L

= <

::ParsedSource as ParsedSource>::Language; + +impl Graph

{ + /// Returns the parser used to parse the sources. + pub fn parser(&self) -> &P { + self.edges.parser() + } + /// Print the graph to `StdOut` pub fn print(&self) { self.print_with_options(Default::default()) @@ -275,11 +313,11 @@ impl> Graph { /// # Panics /// /// if the `index` node id is not included in the graph - pub fn node(&self, index: usize) -> &Node { + pub fn node(&self, index: usize) -> &Node { &self.nodes[index] } - pub(crate) fn display_node(&self, index: usize) -> DisplayNode<'_, D> { + pub(crate) fn display_node(&self, index: usize) -> DisplayNode<'_, P::ParsedSource> { DisplayNode { node: self.node(index), root: &self.root } } @@ -294,11 +332,11 @@ impl> Graph { } /// Same as `Self::node_ids` but returns the actual `Node` - pub fn nodes(&self, start: usize) -> impl Iterator> + '_ { + pub fn nodes(&self, start: usize) -> impl Iterator> + '_ { self.node_ids(start).map(move |idx| self.node(idx)) } - fn split(self) -> (Vec<(PathBuf, Source)>, GraphEdges) { + fn split(self) -> (Vec<(PathBuf, Source)>, GraphEdges

) { let Self { nodes, mut edges, .. } = self; // need to move the extracted data to the edges, essentially splitting the node so we have // access to the data at a later stage in the compile pipeline @@ -306,7 +344,9 @@ impl> Graph { for (idx, node) in nodes.into_iter().enumerate() { let Node { path, source, data } = node; sources.push((path, source)); - edges.data.insert(idx, data); + let idx2 = edges.data.len(); + edges.data.push(data); + assert_eq!(idx, idx2); } (sources, edges) @@ -314,7 +354,7 @@ impl> Graph { /// Consumes the `Graph`, effectively splitting the `nodes` and the `GraphEdges` off and /// returning the `nodes` converted to `Sources` - pub fn into_sources(self) -> (Sources, GraphEdges) { + pub fn into_sources(self) -> (Sources, GraphEdges

) { let (sources, edges) = self.split(); (sources.into_iter().collect(), edges) } @@ -322,7 +362,7 @@ impl> Graph { /// Returns an iterator that yields only those nodes that represent input files. /// See `Self::resolve_sources` /// This won't yield any resolved library nodes - pub fn input_nodes(&self) -> impl Iterator> { + pub fn input_nodes(&self) -> impl Iterator> { self.nodes.iter().take(self.edges.num_input_files) } @@ -332,15 +372,17 @@ impl> Graph { } /// Resolves a number of sources within the given config + #[instrument(name = "Graph::resolve_sources", skip_all)] pub fn resolve_sources( - paths: &ProjectPathsConfig, - sources: Sources, + paths: &ProjectPathsConfig<::Language>, + mut sources: Sources, ) -> Result { /// checks if the given target path was already resolved, if so it adds its id to the list /// of resolved imports. If it hasn't been resolved yet, it queues in the file for /// processing - fn add_node( - unresolved: &mut VecDeque<(PathBuf, Node)>, + fn add_node( + parser: &mut P, + unresolved: &mut VecDeque<(PathBuf, Node)>, index: &mut HashMap, resolved_imports: &mut Vec, target: PathBuf, @@ -349,7 +391,7 @@ impl> Graph { resolved_imports.push(idx); } else { // imported file is not part of the input files - let node = Node::read(&target)?; + let node = parser.read(&target)?; unresolved.push_back((target.clone(), node)); let idx = index.len(); index.insert(target, idx); @@ -358,16 +400,14 @@ impl> Graph { Ok(()) } + // The cache relies on the absolute paths relative to the project root as cache keys. + sources.make_absolute(&paths.root); + + let mut parser = P::new(paths.with_language_ref()); + // we start off by reading all input files, which includes all solidity files from the // source and test folder - let mut unresolved: VecDeque<_> = sources - .0 - .into_par_iter() - .map(|(path, source)| { - let data = D::parse(source.as_ref(), &path)?; - Ok((path.clone(), Node { path, source, data })) - }) - .collect::>()?; + let mut unresolved: VecDeque<_> = parser.parse_sources(&mut sources)?.into(); // identifiers of all resolved files let mut index: HashMap<_, _> = @@ -400,38 +440,24 @@ impl> Graph { }; for import_path in node.data.resolve_imports(paths, &mut resolved_solc_include_paths)? { - match paths.resolve_import_and_include_paths( + if let Some(err) = match paths.resolve_import_and_include_paths( cwd, &import_path, &mut resolved_solc_include_paths, ) { - Ok(import) => { - add_node(&mut unresolved, &mut index, &mut resolved_imports, import) - .map_err(|err| { - match err { - SolcError::ResolveCaseSensitiveFileName { .. } - | SolcError::Resolve(_) => { - // make the error more helpful by providing additional - // context - SolcError::FailedResolveImport( - Box::new(err), - node.path.clone(), - import_path.clone(), - ) - } - _ => err, - } - })? - } - Err(err) => { - unresolved_imports.insert((import_path.to_path_buf(), node.path.clone())); - trace!( - "failed to resolve import component \"{:?}\" for {:?}", - err, - node.path - ) - } - }; + Ok(import) => add_node( + &mut parser, + &mut unresolved, + &mut index, + &mut resolved_imports, + import, + ) + .err(), + Err(err) => Some(err), + } { + unresolved_imports.insert((import_path.to_path_buf(), node.path.clone())); + trace!("failed to resolve import component \"{:?}\" for {:?}", err, node.path) + } } nodes.push(node); @@ -470,6 +496,7 @@ impl> Graph { .map(|(idx, node)| (idx, node.data.version_req().cloned())) .collect(), data: Default::default(), + parser: Some(parser), unresolved_imports, resolved_solc_include_paths, }; @@ -477,12 +504,12 @@ impl> Graph { } /// Resolves the dependencies of a project's source contracts - pub fn resolve(paths: &ProjectPathsConfig) -> Result { + pub fn resolve( + paths: &ProjectPathsConfig<::Language>, + ) -> Result { Self::resolve_sources(paths, paths.read_input_files()?) } -} -impl> Graph { /// Consumes the nodes of the graph and returns all input files together with their appropriate /// version and the edges of the graph /// @@ -494,7 +521,7 @@ impl> Graph { ) -> Result> where T: ArtifactOutput, - C: Compiler, + C: Compiler::Language>, { /// insert the imports of the given node into the sources map /// There can be following graph: @@ -542,14 +569,14 @@ impl> Graph { let mut versioned_sources = Vec::with_capacity(versioned_nodes.len()); for (version, profile_to_nodes) in versioned_nodes { - for (profile_idx, input_node_indixies) in profile_to_nodes { + for (profile_idx, input_node_indexes) in profile_to_nodes { let mut sources = Sources::new(); // all input nodes will be processed - let mut processed_sources = input_node_indixies.iter().copied().collect(); + let mut processed_sources = input_node_indexes.iter().copied().collect(); // we only process input nodes (from sources, tests for example) - for idx in input_node_indixies { + for idx in input_node_indexes { // insert the input node in the sources set and remove it from the available // set let (path, source) = @@ -806,7 +833,7 @@ impl> Graph { Err(msg) } - fn input_nodes_by_language(&self) -> HashMap> { + fn input_nodes_by_language(&self) -> HashMap, Vec> { let mut nodes = HashMap::new(); for idx in 0..self.edges.num_input_files { @@ -826,13 +853,14 @@ impl> Graph { /// /// This also attempts to prefer local installations over remote available. /// If `offline` is set to `true` then only already installed. + #[allow(clippy::type_complexity)] fn get_input_node_versions< - C: Compiler, + C: Compiler>, T: ArtifactOutput, >( &self, project: &Project, - ) -> Result>>> { + ) -> Result, HashMap>>> { trace!("resolving input node versions"); let mut resulted_nodes = HashMap::new(); @@ -917,8 +945,9 @@ impl> Graph { .collect(), ); } else { - error!("failed to resolve versions"); - return Err(SolcError::msg(errors.join("\n"))); + let s = errors.join("\n"); + debug!("failed to resolve versions: {s}"); + return Err(SolcError::msg(s)); } } @@ -927,13 +956,13 @@ impl> Graph { #[allow(clippy::complexity)] fn resolve_settings< - C: Compiler, + C: Compiler>, T: ArtifactOutput, >( &self, project: &Project, - input_nodes_versions: HashMap>>, - ) -> Result>>>> { + input_nodes_versions: HashMap, HashMap>>, + ) -> Result, HashMap>>>> { let mut resulted_sources = HashMap::new(); let mut errors = Vec::new(); for (language, versions) in input_nodes_versions { @@ -960,8 +989,9 @@ impl> Graph { if errors.is_empty() { Ok(resulted_sources) } else { - error!("failed to resolve settings"); - Err(SolcError::msg(errors.join("\n"))) + let s = errors.join("\n"); + debug!("failed to resolve settings: {s}"); + Err(SolcError::msg(s)) } } @@ -1050,20 +1080,20 @@ impl> Graph { /// An iterator over a node and its dependencies #[derive(Debug)] -pub struct NodesIter<'a, D> { +pub struct NodesIter<'a, P: SourceParser> { /// stack of nodes stack: VecDeque, visited: HashSet, - graph: &'a GraphEdges, + graph: &'a GraphEdges

, } -impl<'a, D> NodesIter<'a, D> { - fn new(start: usize, graph: &'a GraphEdges) -> Self { +impl<'a, P: SourceParser> NodesIter<'a, P> { + fn new(start: usize, graph: &'a GraphEdges

) -> Self { Self { stack: VecDeque::from([start]), visited: HashSet::new(), graph } } } -impl Iterator for NodesIter<'_, D> { +impl Iterator for NodesIter<'_, P> { type Item = usize; fn next(&mut self) -> Option { let node = self.stack.pop_front()?; @@ -1077,38 +1107,35 @@ impl Iterator for NodesIter<'_, D> { } #[derive(Debug)] -pub struct Node { +pub struct Node { /// path of the solidity file path: PathBuf, /// content of the solidity file source: Source, /// parsed data - pub data: D, + pub data: S, +} + +impl Node { + pub fn new(path: PathBuf, source: Source, data: S) -> Self { + Self { path, source, data } + } + + pub fn map_data(self, f: impl FnOnce(S) -> T) -> Node { + Node::new(self.path, self.source, f(self.data)) + } } -impl Node { +impl Node { /// Reads the content of the file and returns a [Node] containing relevant information pub fn read(file: &Path) -> Result { - let source = Source::read(file).map_err(|err| { - let exists = err.path().exists(); - if !exists && err.path().is_symlink() { - SolcError::ResolveBadSymlink(err) - } else { - // This is an additional check useful on OS that have case-sensitive paths, See also - if !exists { - // check if there exists a file with different case - if let Some(existing_file) = find_case_sensitive_existing_file(file) { - SolcError::ResolveCaseSensitiveFileName { error: err, existing_file } - } else { - SolcError::Resolve(err) - } - } else { - SolcError::Resolve(err) - } - } - })?; - let data = D::parse(source.as_ref(), file)?; - Ok(Self { path: file.to_path_buf(), source, data }) + let source = Source::read_(file)?; + Self::parse(file, source) + } + + pub fn parse(file: &Path, source: Source) -> Result { + let data = S::parse(source.as_ref(), file)?; + Ok(Self::new(file.to_path_buf(), source, data)) } /// Returns the path of the file. @@ -1127,12 +1154,12 @@ impl Node { } /// Helper type for formatting a node -pub(crate) struct DisplayNode<'a, D> { - node: &'a Node, +pub(crate) struct DisplayNode<'a, S> { + node: &'a Node, root: &'a PathBuf, } -impl fmt::Display for DisplayNode<'_, D> { +impl fmt::Display for DisplayNode<'_, S> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let path = utils::source_name(&self.node.path, self.root); write!(f, "{}", path.display())?; @@ -1164,7 +1191,7 @@ mod tests { let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data/hardhat-sample"); let paths = ProjectPathsConfig::hardhat(&root).unwrap(); - let graph = Graph::::resolve(&paths).unwrap(); + let graph = Graph::::resolve(&paths).unwrap(); assert_eq!(graph.edges.num_input_files, 1); assert_eq!(graph.files().len(), 2); @@ -1183,7 +1210,7 @@ mod tests { let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data/dapp-sample"); let paths = ProjectPathsConfig::dapptools(&root).unwrap(); - let graph = Graph::::resolve(&paths).unwrap(); + let graph = Graph::::resolve(&paths).unwrap(); assert_eq!(graph.edges.num_input_files, 2); assert_eq!(graph.files().len(), 3); @@ -1206,26 +1233,31 @@ mod tests { } #[test] - #[cfg(not(target_os = "windows"))] fn can_print_dapp_sample_graph() { let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data/dapp-sample"); let paths = ProjectPathsConfig::dapptools(&root).unwrap(); - let graph = Graph::::resolve(&paths).unwrap(); + let graph = Graph::::resolve(&paths).unwrap(); let mut out = Vec::::new(); tree::print(&graph, &Default::default(), &mut out).unwrap(); - assert_eq!( - " + if !cfg!(windows) { + assert_eq!( + " src/Dapp.sol >=0.6.6 src/Dapp.t.sol >=0.6.6 ├── lib/ds-test/src/test.sol >=0.4.23 └── src/Dapp.sol >=0.6.6 " - .trim_start() - .as_bytes() - .to_vec(), - out - ); + .trim_start() + .as_bytes() + .to_vec(), + out + ); + } + + graph.edges.parser().compiler.enter(|c| { + assert_eq!(c.gcx().sources.len(), 3); + }); } #[test] @@ -1233,7 +1265,7 @@ src/Dapp.t.sol >=0.6.6 fn can_print_hardhat_sample_graph() { let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data/hardhat-sample"); let paths = ProjectPathsConfig::hardhat(&root).unwrap(); - let graph = Graph::::resolve(&paths).unwrap(); + let graph = Graph::::resolve(&paths).unwrap(); let mut out = Vec::::new(); tree::print(&graph, &Default::default(), &mut out).unwrap(); assert_eq!( @@ -1252,7 +1284,7 @@ src/Dapp.t.sol >=0.6.6 let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data/incompatible-pragmas"); let paths = ProjectPathsConfig::dapptools(&root).unwrap(); - let graph = Graph::::resolve(&paths).unwrap(); + let graph = Graph::::resolve(&paths).unwrap(); let Err(SolcError::Message(err)) = graph.get_input_node_versions( &ProjectBuilder::::default() .paths(paths) diff --git a/crates/compilers/src/resolver/parse.rs b/crates/compilers/src/resolver/parse.rs index 0627bb0..bebdcaa 100644 --- a/crates/compilers/src/resolver/parse.rs +++ b/crates/compilers/src/resolver/parse.rs @@ -1,11 +1,93 @@ use foundry_compilers_core::utils; use semver::VersionReq; -use solar_parse::{ast, interface::sym}; +use solar::{ + parse::{ast, interface::sym}, + sema::interface, +}; use std::{ ops::Range, path::{Path, PathBuf}, }; +/// Solidity parser. +/// +/// Holds a [`solar::sema::Compiler`] that is used to parse sources incrementally. +/// After project compilation ([`Graph::resolve`]), this will contain all sources parsed by +/// [`Graph`]. +/// +/// This state is currently lost on `Clone`. +/// +/// [`Graph`]: crate::Graph +/// [`Graph::resolve`]: crate::Graph::resolve +#[derive(derive_more::Debug)] +pub struct SolParser { + #[debug(ignore)] + pub(crate) compiler: solar::sema::Compiler, +} + +impl Clone for SolParser { + fn clone(&self) -> Self { + Self { + compiler: solar::sema::Compiler::new(Self::session_with_opts( + self.compiler.sess().opts.clone(), + )), + } + } +} + +impl SolParser { + /// Returns a reference to the compiler. + pub fn compiler(&self) -> &solar::sema::Compiler { + &self.compiler + } + + /// Returns a mutable reference to the compiler. + pub fn compiler_mut(&mut self) -> &mut solar::sema::Compiler { + &mut self.compiler + } + + /// Consumes the parser and returns the compiler. + pub fn into_compiler(self) -> solar::sema::Compiler { + self.compiler + } + + pub(crate) fn session_with_opts( + opts: solar::sema::interface::config::Opts, + ) -> solar::sema::interface::Session { + let sess = solar::sema::interface::Session::builder() + .with_buffer_emitter(Default::default()) + .opts(opts) + .build(); + sess.source_map().set_file_loader(FileLoader); + sess + } +} + +struct FileLoader; +impl interface::source_map::FileLoader for FileLoader { + fn canonicalize_path(&self, path: &Path) -> std::io::Result { + interface::source_map::RealFileLoader.canonicalize_path(path) + } + + fn load_stdin(&self) -> std::io::Result { + interface::source_map::RealFileLoader.load_stdin() + } + + fn load_file(&self, path: &Path) -> std::io::Result { + interface::source_map::RealFileLoader.load_file(path).map(|s| { + if s.contains('\r') { + s.replace('\r', "") + } else { + s + } + }) + } + + fn load_binary_file(&self, path: &Path) -> std::io::Result> { + interface::source_map::RealFileLoader.load_binary_file(path) + } +} + /// Represents various information about a Solidity file. #[derive(Clone, Debug)] #[non_exhaustive] @@ -42,112 +124,28 @@ impl SolData { /// /// This will attempt to parse the solidity AST and extract the imports and version pragma. If /// parsing fails, we'll fall back to extract that info via regex + #[instrument(name = "SolData::parse", skip_all)] pub fn parse(content: &str, file: &Path) -> Self { - let is_yul = file.extension().is_some_and(|ext| ext == "yul"); - let mut version = None; - let mut experimental = None; - let mut imports = Vec::>::new(); - let mut libraries = Vec::new(); - let mut contract_names = Vec::new(); - let mut parse_result = Ok(()); - - let result = crate::parse_one_source(content, file, |ast| { - for item in ast.items.iter() { - let loc = item.span.lo().to_usize()..item.span.hi().to_usize(); - match &item.kind { - ast::ItemKind::Pragma(pragma) => match &pragma.tokens { - ast::PragmaTokens::Version(name, req) if name.name == sym::solidity => { - version = Some(Spanned::new(req.to_string(), loc)); - } - ast::PragmaTokens::Custom(name, value) - if name.as_str() == "experimental" => - { - let value = - value.as_ref().map(|v| v.as_str().to_string()).unwrap_or_default(); - experimental = Some(Spanned::new(value, loc)); - } - _ => {} - }, - - ast::ItemKind::Import(import) => { - let path = import.path.value.to_string(); - let aliases = match &import.items { - ast::ImportItems::Plain(None) => &[][..], - ast::ImportItems::Plain(Some(alias)) - | ast::ImportItems::Glob(alias) => &[(*alias, None)][..], - ast::ImportItems::Aliases(aliases) => aliases, - }; - let sol_import = SolImport::new(PathBuf::from(path)).set_aliases( - aliases - .iter() - .map(|(id, alias)| match alias { - Some(al) => SolImportAlias::Contract( - al.name.to_string(), - id.name.to_string(), - ), - None => SolImportAlias::File(id.name.to_string()), - }) - .collect(), - ); - imports.push(Spanned::new(sol_import, loc)); - } - - ast::ItemKind::Contract(contract) => { - if contract.kind.is_library() { - libraries.push(SolLibrary { is_inlined: library_is_inlined(contract) }); - } - contract_names.push(contract.name.to_string()); - } - - _ => {} - } - } - }); - if let Err(e) = result { - let e = e.to_string(); - trace!("failed parsing {file:?}: {e}"); - parse_result = Err(e); - - if version.is_none() { - version = utils::capture_outer_and_inner( - content, - &utils::RE_SOL_PRAGMA_VERSION, - &["version"], - ) - .first() - .map(|(cap, name)| Spanned::new(name.as_str().to_owned(), cap.range())); - } - if imports.is_empty() { - imports = capture_imports(content); - } - if contract_names.is_empty() { - utils::RE_CONTRACT_NAMES.captures_iter(content).for_each(|cap| { - contract_names.push(cap[1].to_owned()); - }); + match crate::parse_one_source(content, file, |sess, ast| { + SolDataBuilder::parse(content, file, Ok((sess, ast))) + }) { + Ok(data) => data, + Err(e) => { + let e = e.to_string(); + trace!("failed parsing {file:?}: {e}"); + SolDataBuilder::parse(content, file, Err(Some(e))) } } - let license = content.lines().next().and_then(|line| { - utils::capture_outer_and_inner( - line, - &utils::RE_SOL_SDPX_LICENSE_IDENTIFIER, - &["license"], - ) - .first() - .map(|(cap, l)| Spanned::new(l.as_str().to_owned(), cap.range())) - }); - let version_req = version.as_ref().and_then(|v| Self::parse_version_req(v.data()).ok()); + } - Self { - version_req, - version, - experimental, - imports, - license, - libraries, - contract_names, - is_yul, - parse_result, - } + pub(crate) fn parse_from( + sess: &solar::sema::interface::Session, + s: &solar::sema::Source<'_>, + ) -> Self { + let content = s.file.src.as_str(); + let file = s.file.name.as_real().unwrap(); + let ast = s.ast.as_ref().map(|ast| (sess, ast)).ok_or(None); + SolDataBuilder::parse(content, file, ast) } /// Parses the version pragma and returns the corresponding SemVer version requirement. @@ -168,7 +166,7 @@ impl SolData { // Somehow, Solidity semver without an operator is considered to be "exact", // but lack of operator automatically marks the operator as Caret, so we need // to manually patch it? :shrug: - let exact = !matches!(&version[0..1], "*" | "^" | "=" | ">" | "<" | "~"); + let exact = !matches!(version.get(..1), Some("*" | "^" | "=" | ">" | "<" | "~")); let mut version = VersionReq::parse(&version)?; if exact { version.comparators[0].op = semver::Op::Exact; @@ -178,6 +176,141 @@ impl SolData { } } +#[derive(Default)] +struct SolDataBuilder { + version: Option>, + experimental: Option>, + imports: Vec>, + libraries: Vec, + contract_names: Vec, + parse_err: Option, +} + +impl SolDataBuilder { + fn parse( + content: &str, + file: &Path, + ast: Result< + (&solar::sema::interface::Session, &solar::parse::ast::SourceUnit<'_>), + Option, + >, + ) -> SolData { + let mut builder = Self::default(); + match ast { + Ok((sess, ast)) => builder.parse_from_ast(sess, ast), + Err(err) => { + builder.parse_from_regex(content); + if let Some(err) = err { + builder.parse_err = Some(err); + } + } + } + builder.build(content, file) + } + + fn parse_from_ast( + &mut self, + sess: &solar::sema::interface::Session, + ast: &solar::parse::ast::SourceUnit<'_>, + ) { + for item in ast.items.iter() { + let loc = sess.source_map().span_to_source(item.span).unwrap().data; + match &item.kind { + ast::ItemKind::Pragma(pragma) => match &pragma.tokens { + ast::PragmaTokens::Version(name, req) if name.name == sym::solidity => { + self.version = Some(Spanned::new(req.to_string(), loc)); + } + ast::PragmaTokens::Custom(name, value) if name.as_str() == "experimental" => { + let value = + value.as_ref().map(|v| v.as_str().to_string()).unwrap_or_default(); + self.experimental = Some(Spanned::new(value, loc)); + } + _ => {} + }, + + ast::ItemKind::Import(import) => { + let path = import.path.value.to_string(); + let aliases = match &import.items { + ast::ImportItems::Plain(None) => &[][..], + ast::ImportItems::Plain(Some(alias)) | ast::ImportItems::Glob(alias) => { + &[(*alias, None)][..] + } + ast::ImportItems::Aliases(aliases) => aliases, + }; + let sol_import = SolImport::new(PathBuf::from(path)).set_aliases( + aliases + .iter() + .map(|(id, alias)| match alias { + Some(al) => SolImportAlias::Contract( + al.name.to_string(), + id.name.to_string(), + ), + None => SolImportAlias::File(id.name.to_string()), + }) + .collect(), + ); + self.imports.push(Spanned::new(sol_import, loc)); + } + + ast::ItemKind::Contract(contract) => { + if contract.kind.is_library() { + self.libraries + .push(SolLibrary { is_inlined: library_is_inlined(contract) }); + } + self.contract_names.push(contract.name.to_string()); + } + + _ => {} + } + } + } + + fn parse_from_regex(&mut self, content: &str) { + if self.version.is_none() { + self.version = utils::capture_outer_and_inner( + content, + &utils::RE_SOL_PRAGMA_VERSION, + &["version"], + ) + .first() + .map(|(cap, name)| Spanned::new(name.as_str().to_owned(), cap.range())); + } + if self.imports.is_empty() { + self.imports = capture_imports(content); + } + if self.contract_names.is_empty() { + utils::RE_CONTRACT_NAMES.captures_iter(content).for_each(|cap| { + self.contract_names.push(cap[1].to_owned()); + }); + } + } + + fn build(self, content: &str, file: &Path) -> SolData { + let Self { version, experimental, imports, libraries, contract_names, parse_err } = self; + let license = content.lines().next().and_then(|line| { + utils::capture_outer_and_inner( + line, + &utils::RE_SOL_SDPX_LICENSE_IDENTIFIER, + &["license"], + ) + .first() + .map(|(cap, l)| Spanned::new(l.as_str().to_owned(), cap.range())) + }); + let version_req = version.as_ref().and_then(|v| SolData::parse_version_req(v.data()).ok()); + SolData { + license, + version, + experimental, + imports, + version_req, + libraries, + contract_names, + is_yul: file.extension().is_some_and(|ext| ext == "yul"), + parse_result: parse_err.map(Err).unwrap_or(Ok(())), + } + } +} + #[derive(Clone, Debug)] pub struct SolImport { path: PathBuf, @@ -272,7 +405,7 @@ fn library_is_inlined(contract: &ast::ItemContract<'_>) -> bool { }) .all(|f| { !matches!( - f.header.visibility, + f.header.visibility.map(|v| *v), Some(ast::Visibility::Public | ast::Visibility::External) ) }) diff --git a/crates/compilers/src/resolver/tree.rs b/crates/compilers/src/resolver/tree.rs index ca73098..6ffdf1f 100644 --- a/crates/compilers/src/resolver/tree.rs +++ b/crates/compilers/src/resolver/tree.rs @@ -1,4 +1,4 @@ -use crate::{compilers::ParsedSource, Graph}; +use crate::{Graph, SourceParser}; use std::{collections::HashSet, io, io::Write, str::FromStr}; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] @@ -46,8 +46,8 @@ static UTF8_SYMBOLS: Symbols = Symbols { down: "│", tee: "├", ell: "└", ri static ASCII_SYMBOLS: Symbols = Symbols { down: "|", tee: "|", ell: "`", right: "-" }; -pub fn print( - graph: &Graph, +pub fn print( + graph: &Graph

, opts: &TreeOptions, out: &mut dyn Write, ) -> io::Result<()> { @@ -83,8 +83,8 @@ pub fn print( } #[allow(clippy::too_many_arguments)] -fn print_node( - graph: &Graph, +fn print_node( + graph: &Graph

, node_index: usize, symbols: &Symbols, no_dedupe: bool, @@ -137,8 +137,8 @@ fn print_node( /// Prints all the imports of a node #[allow(clippy::too_many_arguments)] -fn print_imports( - graph: &Graph, +fn print_imports( + graph: &Graph, node_index: usize, symbols: &Symbols, no_dedupe: bool, diff --git a/crates/compilers/tests/project.rs b/crates/compilers/tests/project.rs index d8c0852..e5c28b1 100644 --- a/crates/compilers/tests/project.rs +++ b/crates/compilers/tests/project.rs @@ -5,16 +5,14 @@ use foundry_compilers::{ buildinfo::BuildInfo, cache::{CompilerCache, SOLIDITY_FILES_CACHE_FILENAME}, compilers::{ - multi::{ - MultiCompiler, MultiCompilerLanguage, MultiCompilerParsedSource, MultiCompilerSettings, - }, + multi::{MultiCompiler, MultiCompilerLanguage, MultiCompilerSettings}, solc::{Solc, SolcCompiler, SolcLanguage}, vyper::{Vyper, VyperLanguage, VyperSettings}, CompilerOutput, }, flatten::Flattener, info::ContractInfo, - multi::{MultiCompilerInput, MultiCompilerRestrictions}, + multi::{MultiCompilerInput, MultiCompilerParser, MultiCompilerRestrictions}, project::{Preprocessor, ProjectCompiler}, project_util::*, solc::{Restriction, SolcRestrictions, SolcSettings}, @@ -60,7 +58,7 @@ pub static VYPER: LazyLock = LazyLock::new(|| { return Vyper::new(&path).unwrap(); } - let base = "https://github.com/vyperlang/vyper/releases/download/v0.4.0/vyper.0.4.0+commit.e9db8d9f"; + let base = "https://github.com/vyperlang/vyper/releases/download/v0.4.3/vyper.0.4.3+commit.bff19ea2"; let url = format!( "{base}.{}", match platform() { @@ -264,7 +262,7 @@ fn can_compile_dapp_detect_changes_in_libs() { ) .unwrap(); - let graph = Graph::::resolve(project.paths()).unwrap(); + let graph = Graph::::resolve(project.paths()).unwrap(); assert_eq!(graph.files().len(), 2); assert_eq!(graph.files().clone(), HashMap::from([(src, 0), (lib, 1),])); @@ -294,7 +292,7 @@ fn can_compile_dapp_detect_changes_in_libs() { ) .unwrap(); - let graph = Graph::::resolve(project.paths()).unwrap(); + let graph = Graph::::resolve(project.paths()).unwrap(); assert_eq!(graph.files().len(), 2); let compiled = project.compile().unwrap(); @@ -336,7 +334,7 @@ fn can_compile_dapp_detect_changes_in_sources() { ) .unwrap(); - let graph = Graph::::resolve(project.paths()).unwrap(); + let graph = Graph::::resolve(project.paths()).unwrap(); assert_eq!(graph.files().len(), 2); assert_eq!(graph.files().clone(), HashMap::from([(base, 0), (src, 1),])); assert_eq!(graph.imported_nodes(1).to_vec(), vec![0]); @@ -373,7 +371,7 @@ fn can_compile_dapp_detect_changes_in_sources() { ", ) .unwrap(); - let graph = Graph::::resolve(project.paths()).unwrap(); + let graph = Graph::::resolve(project.paths()).unwrap(); assert_eq!(graph.files().len(), 2); let compiled = project.compile().unwrap(); @@ -768,7 +766,6 @@ contract Contract { .unwrap(); let result = project.paths().clone().with_language::().flatten(target.as_path()); - assert!(result.is_err()); println!("{}", result.unwrap_err()); } @@ -2678,7 +2675,7 @@ fn can_create_standard_json_input_with_external_file() { ] ); - let solc = Solc::find_or_install(&Version::new(0, 8, 24)).unwrap(); + let solc = Solc::find_or_install(&Version::new(0, 8, 27)).unwrap(); // can compile using the created json let compiler_errors = solc @@ -2703,7 +2700,7 @@ fn can_compile_std_json_input() { assert!(input.sources.contains_key(Path::new("lib/ds-test/src/test.sol"))); // should be installed - if let Ok(solc) = Solc::find_or_install(&Version::new(0, 8, 24)) { + if let Ok(solc) = Solc::find_or_install(&Version::new(0, 8, 28)) { let out = solc.compile(&input).unwrap(); assert!(out.errors.is_empty()); assert!(out.sources.contains_key(Path::new("lib/ds-test/src/test.sol"))); @@ -2767,7 +2764,7 @@ fn can_create_standard_json_input_with_symlink() { ] ); - let solc = Solc::find_or_install(&Version::new(0, 8, 24)).unwrap(); + let solc = Solc::find_or_install(&Version::new(0, 8, 28)).unwrap(); // can compile using the created json let compiler_errors = solc @@ -2936,7 +2933,7 @@ async fn can_install_solc_and_compile_std_json_input_async() { tmp.assert_no_errors(); let source = tmp.list_source_files().into_iter().find(|p| p.ends_with("Dapp.t.sol")).unwrap(); let input = tmp.project().standard_json_input(&source).unwrap(); - let solc = Solc::find_or_install(&Version::new(0, 8, 24)).unwrap(); + let solc = Solc::find_or_install(&Version::new(0, 8, 27)).unwrap(); assert!(input.settings.remappings.contains(&"ds-test/=lib/ds-test/src/".parse().unwrap())); let input: SolcInput = input.into(); @@ -3679,7 +3676,7 @@ fn can_add_basic_contract_and_library() { let lib = project.add_basic_source("Bar", "^0.8.0").unwrap(); - let graph = Graph::::resolve(project.paths()).unwrap(); + let graph = Graph::::resolve(project.paths()).unwrap(); assert_eq!(graph.files().len(), 2); assert!(graph.files().contains_key(&src)); assert!(graph.files().contains_key(&lib)); From ed3a17ad5ef1b5ae0748f90e9f882aabd8a0ba21 Mon Sep 17 00:00:00 2001 From: cdrappi Date: Fri, 12 Sep 2025 12:27:48 -0400 Subject: [PATCH 56/57] codeowners --- .github/CODEOWNERS | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a489dd4..69e6503 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,5 +1 @@ -<<<<<<< HEAD -* @sfyll -======= -* @danipopes @klkvr @mattsse @grandizzy @yash-atreya @zerosnacks @onbjerg @0xrusowsky ->>>>>>> 0a1c4958f81bebbc2895df8038ae38aefff50649 +* @cdrappi From 067a38bde292629c5fc8b089c9331e0a0a1e259e Mon Sep 17 00:00:00 2001 From: cdrappi Date: Fri, 12 Sep 2025 12:33:16 -0400 Subject: [PATCH 57/57] rustfmt --- crates/compilers/src/cache/iface.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/compilers/src/cache/iface.rs b/crates/compilers/src/cache/iface.rs index 18cfd6c..49b9e0a 100644 --- a/crates/compilers/src/cache/iface.rs +++ b/crates/compilers/src/cache/iface.rs @@ -58,8 +58,9 @@ pub(crate) fn interface_representation_ast( } } } - let updates = - spans_to_remove.iter().map(|&span| (sess.source_map().span_to_source(span).unwrap().data, "")); + let updates = spans_to_remove + .iter() + .map(|&span| (sess.source_map().span_to_source(span).unwrap().data, "")); let content = replace_source_content(content, updates).replace("\n", ""); crate::utils::RE_TWO_OR_MORE_SPACES.replace_all(&content, "").into_owned() }