diff --git a/crates/linking/src/lib.rs b/crates/linking/src/lib.rs index 5c8dab5378fe0..2f48ceb2a83e1 100644 --- a/crates/linking/src/lib.rs +++ b/crates/linking/src/lib.rs @@ -105,18 +105,29 @@ impl<'a> Linker<'a> { ) -> Result<(), LinkerError> { let contract = self.contracts.get(target).ok_or(LinkerError::MissingTargetArtifact)?; - let mut references = BTreeMap::new(); + // Deep merge: collect union of library names per file from creation and deployed bytecode + let mut references: BTreeMap> = BTreeMap::new(); if let Some(bytecode) = &contract.bytecode { - references.extend(bytecode.link_references.clone()); + for (file, libs) in &bytecode.link_references { + let set = references.entry(file.clone()).or_default(); + for name in libs.keys() { + set.insert(name.clone()); + } + } } if let Some(deployed_bytecode) = &contract.deployed_bytecode && let Some(bytecode) = &deployed_bytecode.bytecode { - references.extend(bytecode.link_references.clone()); + for (file, libs) in &bytecode.link_references { + let set = references.entry(file.clone()).or_default(); + for name in libs.keys() { + set.insert(name.clone()); + } + } } for (file, libs) in &references { - for contract in libs.keys() { + for contract in libs { let id = self .find_artifact_id_by_library_path(file, contract, Some(&target.version)) .ok_or_else(|| LinkerError::MissingLibraryArtifact { @@ -320,29 +331,20 @@ mod tests { multi::MultiCompiler, solc::{Solc, SolcCompiler}, }; - use std::sync::OnceLock; - fn testdata() -> &'static Path { - static CACHE: OnceLock = OnceLock::new(); - CACHE.get_or_init(|| { - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../testdata").canonicalize().unwrap() - }) - } - - #[must_use] struct LinkerTest { project: Project, output: ProjectCompileOutput, - dependency_assertions: HashMap<&'static str, Vec<(&'static str, Address)>>, + dependency_assertions: HashMap>, } impl LinkerTest { - fn new(path: &Path, strip_prefixes: bool) -> Self { - assert!(path.exists(), "Path {path:?} does not exist"); + fn new(path: impl Into, strip_prefixes: bool) -> Self { + let path = path.into(); let paths = ProjectPathsConfig::builder() - .root(testdata()) - .lib(testdata().join("lib")) - .sources(path) + .root("../../testdata") + .lib("../../testdata/lib") + .sources(path.clone()) .tests(path) .build() .unwrap(); @@ -366,10 +368,10 @@ mod tests { fn assert_dependencies( mut self, - artifact_id: &'static str, - deps: &[(&'static str, Address)], + artifact_id: String, + deps: Vec<(String, Address)>, ) -> Self { - self.dependency_assertions.insert(artifact_id, deps.to_vec()); + self.dependency_assertions.insert(artifact_id, deps); self } @@ -396,8 +398,7 @@ mod tests { fn iter_linking_targets<'a>( &'a self, linker: &'a Linker<'_>, - ) -> impl Iterator + 'a { - self.sanity_check(linker); + ) -> impl IntoIterator + 'a { linker.contracts.keys().filter_map(move |id| { // If we didn't strip paths, artifacts will have absolute paths. // That's expected and we want to ensure that only `libraries` object has relative @@ -419,17 +420,12 @@ mod tests { }) } - fn sanity_check(&self, linker: &Linker<'_>) { - assert!(!self.dependency_assertions.is_empty(), "Dependency assertions are empty"); - assert!(!linker.contracts.is_empty(), "Linker contracts are empty"); - } - fn validate_assertions(&self, identifier: String, output: LinkOutput) { let LinkOutput { libs_to_deploy, libraries } = output; let assertions = self .dependency_assertions - .get(identifier.as_str()) + .get(&identifier) .unwrap_or_else(|| panic!("Unexpected artifact: {identifier}")); assert_eq!( @@ -441,14 +437,14 @@ mod tests { libs_to_deploy ); - for &(dep_identifier, address) in assertions { + for (dep_identifier, address) in assertions { let (file, name) = dep_identifier.split_once(':').unwrap(); if let Some(lib_address) = libraries.libs.get(Path::new(file)).and_then(|libs| libs.get(name)) { assert_eq!( - lib_address.parse::
().unwrap(), - address, + *lib_address, + address.to_string(), "incorrect library address for dependency {dep_identifier} of {identifier}" ); } else { @@ -458,47 +454,27 @@ mod tests { } } - fn link_test(path: impl AsRef, mut test_fn: impl FnMut(LinkerTest)) { - fn link_test(path: &Path, test_fn: &mut dyn FnMut(LinkerTest)) { - test_fn(LinkerTest::new(path, true)); - test_fn(LinkerTest::new(path, false)); - } - link_test(path.as_ref(), &mut test_fn); - } - - #[test] - #[should_panic = "assertions are empty"] - fn no_assertions() { - link_test(testdata().join("default/linking/simple"), |linker| { - linker.test_with_sender_and_nonce(Address::default(), 1); - }); - } - - #[test] - #[should_panic = "does not exist"] - fn unknown_path() { - link_test("doesnotexist", |linker| { - linker - .assert_dependencies("a:b", &[]) - .test_with_sender_and_nonce(Address::default(), 1); - }); + fn link_test(path: impl Into, test_fn: impl Fn(LinkerTest)) { + let path = path.into(); + test_fn(LinkerTest::new(path.clone(), true)); + test_fn(LinkerTest::new(path, false)); } #[test] fn link_simple() { - link_test(testdata().join("default/linking/simple"), |linker| { + link_test("../../testdata/default/linking/simple", |linker| { linker - .assert_dependencies("default/linking/simple/Simple.t.sol:Lib", &[]) + .assert_dependencies("default/linking/simple/Simple.t.sol:Lib", vec![]) .assert_dependencies( "default/linking/simple/Simple.t.sol:LibraryConsumer", - &[( + vec![( "default/linking/simple/Simple.t.sol:Lib", address!("0x5a443704dd4b594b382c22a083e2bd3090a6fef3"), )], ) .assert_dependencies( "default/linking/simple/Simple.t.sol:SimpleLibraryLinkingTest", - &[( + vec![( "default/linking/simple/Simple.t.sol:Lib", address!("0x5a443704dd4b594b382c22a083e2bd3090a6fef3"), )], @@ -509,19 +485,19 @@ mod tests { #[test] fn link_nested() { - link_test(testdata().join("default/linking/nested"), |linker| { + link_test("../../testdata/default/linking/nested", |linker| { linker - .assert_dependencies("default/linking/nested/Nested.t.sol:Lib", &[]) + .assert_dependencies("default/linking/nested/Nested.t.sol:Lib", vec![]) .assert_dependencies( "default/linking/nested/Nested.t.sol:NestedLib", - &[( + vec![( "default/linking/nested/Nested.t.sol:Lib", address!("0x5a443704dd4b594b382c22a083e2bd3090a6fef3"), )], ) .assert_dependencies( "default/linking/nested/Nested.t.sol:LibraryConsumer", - &[ + vec![ // Lib shows up here twice, because the linker sees it twice, but it should // have the same address and nonce. ( @@ -538,7 +514,7 @@ mod tests { ) .assert_dependencies( "default/linking/nested/Nested.t.sol:NestedLibraryLinkingTest", - &[ + vec![ ( "default/linking/nested/Nested.t.sol:Lib", Address::from_str("0x5a443704dd4b594b382c22a083e2bd3090a6fef3") @@ -557,27 +533,33 @@ mod tests { #[test] fn link_duplicate() { - link_test(testdata().join("default/linking/duplicate"), |linker| { + link_test("../../testdata/default/linking/duplicate", |linker| { linker - .assert_dependencies("default/linking/duplicate/Duplicate.t.sol:A", &[]) - .assert_dependencies("default/linking/duplicate/Duplicate.t.sol:B", &[]) + .assert_dependencies( + "default/linking/duplicate/Duplicate.t.sol:A", + vec![], + ) + .assert_dependencies( + "default/linking/duplicate/Duplicate.t.sol:B", + vec![], + ) .assert_dependencies( "default/linking/duplicate/Duplicate.t.sol:C", - &[( + vec![( "default/linking/duplicate/Duplicate.t.sol:A", address!("0x5a443704dd4b594b382c22a083e2bd3090a6fef3"), )], ) .assert_dependencies( "default/linking/duplicate/Duplicate.t.sol:D", - &[( + vec![( "default/linking/duplicate/Duplicate.t.sol:B", address!("0x5a443704dd4b594b382c22a083e2bd3090a6fef3"), )], ) .assert_dependencies( "default/linking/duplicate/Duplicate.t.sol:E", - &[ + vec![ ( "default/linking/duplicate/Duplicate.t.sol:A", Address::from_str("0x5a443704dd4b594b382c22a083e2bd3090a6fef3") @@ -592,7 +574,7 @@ mod tests { ) .assert_dependencies( "default/linking/duplicate/Duplicate.t.sol:LibraryConsumer", - &[ + vec![ ( "default/linking/duplicate/Duplicate.t.sol:A", Address::from_str("0x5a443704dd4b594b382c22a083e2bd3090a6fef3") @@ -622,7 +604,7 @@ mod tests { ) .assert_dependencies( "default/linking/duplicate/Duplicate.t.sol:DuplicateLibraryLinkingTest", - &[ + vec![ ( "default/linking/duplicate/Duplicate.t.sol:A", Address::from_str("0x5a443704dd4b594b382c22a083e2bd3090a6fef3") @@ -656,11 +638,11 @@ mod tests { #[test] fn link_cycle() { - link_test(testdata().join("default/linking/cycle"), |linker| { + link_test("../../testdata/default/linking/cycle", |linker| { linker .assert_dependencies( "default/linking/cycle/Cycle.t.sol:Foo", - &[ + vec![ ( "default/linking/cycle/Cycle.t.sol:Foo", Address::from_str("0x47e9Fbef8C83A1714F1951F142132E6e90F5fa5D") @@ -675,7 +657,7 @@ mod tests { ) .assert_dependencies( "default/linking/cycle/Cycle.t.sol:Bar", - &[ + vec![ ( "default/linking/cycle/Cycle.t.sol:Foo", Address::from_str("0x47e9Fbef8C83A1714F1951F142132E6e90F5fa5D") @@ -694,19 +676,19 @@ mod tests { #[test] fn link_create2_nested() { - link_test(testdata().join("default/linking/nested"), |linker| { + link_test("../../testdata/default/linking/nested", |linker| { linker - .assert_dependencies("default/linking/nested/Nested.t.sol:Lib", &[]) + .assert_dependencies("default/linking/nested/Nested.t.sol:Lib", vec![]) .assert_dependencies( "default/linking/nested/Nested.t.sol:NestedLib", - &[( + vec![( "default/linking/nested/Nested.t.sol:Lib", address!("0xddb1Cd2497000DAeA687CEa3dc34Af44084BEa74"), )], ) .assert_dependencies( "default/linking/nested/Nested.t.sol:LibraryConsumer", - &[ + vec![ // Lib shows up here twice, because the linker sees it twice, but it should // have the same address and nonce. ( @@ -723,7 +705,7 @@ mod tests { ) .assert_dependencies( "default/linking/nested/Nested.t.sol:NestedLibraryLinkingTest", - &[ + vec![ ( "default/linking/nested/Nested.t.sol:Lib", Address::from_str("0xddb1Cd2497000DAeA687CEa3dc34Af44084BEa74") @@ -745,9 +727,78 @@ mod tests { }); } + #[test] + fn link_samefile_union() { + link_test("../../testdata/default/linking/samefile_union", |linker| { + // Ensure the target artifact is compiled + let artifact_exists = linker.output.artifact_ids().any(|(id, _)| { + let source = id.source.strip_prefix(linker.project.root()).unwrap_or(&id.source); + id.name == "UsesBoth" + && source + == std::path::Path::new( + "default/linking/samefile_union/SameFileUnion.t.sol", + ) + }); + assert!(artifact_exists, "Expected UsesBoth artifact to be compiled"); + + // Skip the test unless BOTH LInit (creation) and LRun (runtime) link refs are present. + let both_refs_present = linker.output.artifact_ids().any(|(id, artifact)| { + let source = id.source.strip_prefix(linker.project.root()).unwrap_or(&id.source); + if id.name != "UsesBoth" + || source + != std::path::Path::new( + "default/linking/samefile_union/SameFileUnion.t.sol", + ) + { + return false; + } + + use std::collections::BTreeSet as Set; + let mut names: Set = Set::new(); + if let Some(b) = &artifact.bytecode { + for libs in b.link_references.values() { + names.extend(libs.keys().cloned()); + } + } + if let Some(db) = &artifact.deployed_bytecode + && let Some(bc) = &db.bytecode + { + for libs in bc.link_references.values() { + names.extend(libs.keys().cloned()); + } + } + names.contains("LInit") && names.contains("LRun") + }); + if !both_refs_present { + return; + } + + linker + // seed empty expectations for libraries to avoid unexpected artifact panics + .assert_dependencies("default/linking/samefile_union/Libs.sol:LInit", vec![]) + .assert_dependencies("default/linking/samefile_union/Libs.sol:LRun", vec![]) + .assert_dependencies( + "default/linking/samefile_union/SameFileUnion.t.sol:UsesBoth", + vec![ + ( + "default/linking/samefile_union/Libs.sol:LInit", + Address::from_str("0x5a443704dd4b594b382c22a083e2bd3090a6fef3") + .unwrap(), + ), + ( + "default/linking/samefile_union/Libs.sol:LRun", + Address::from_str("0x47e9fbef8c83a1714f1951f142132e6e90f5fa5d") + .unwrap(), + ), + ], + ) + .test_with_sender_and_nonce(Address::default(), 1); + }); + } + #[test] fn linking_failure() { - let linker = LinkerTest::new(&testdata().join("default/linking/simple"), true); + let linker = LinkerTest::new("../../testdata/default/linking/simple", true); let linker_instance = Linker::new(linker.project.root(), linker.output.artifact_ids().collect()); diff --git a/testdata/default/linking/samefile_union/Libs.sol b/testdata/default/linking/samefile_union/Libs.sol new file mode 100644 index 0000000000000..1e93f56c66e6a --- /dev/null +++ b/testdata/default/linking/samefile_union/Libs.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.18; + +library LInit { + function f() external view returns (uint256) { + return block.number; + } +} + +library LRun { + function g() external view returns (uint256) { + return block.timestamp; + } +} diff --git a/testdata/default/linking/samefile_union/SameFileUnion.t.sol b/testdata/default/linking/samefile_union/SameFileUnion.t.sol new file mode 100644 index 0000000000000..013f0e0b0c501 --- /dev/null +++ b/testdata/default/linking/samefile_union/SameFileUnion.t.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.18; + +import "./Libs.sol"; + +contract UsesBoth { + uint256 public x; + + constructor() { + // used only in creation bytecode + x = LInit.f(); + } + + function y() external view returns (uint256) { + // used only in deployed bytecode + return LRun.g(); + } +}