diff --git a/crates/artifacts/solc/src/remappings/mod.rs b/crates/artifacts/solc/src/remappings/mod.rs index 5899d9b4b..78fb5f8a0 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 356d80b98..45dfd8dd7 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")); + } }