diff --git a/ethers-solc/src/error.rs b/ethers-solc/src/error.rs index 72dbc95e..0b77fde3 100644 --- a/ethers-solc/src/error.rs +++ b/ethers-solc/src/error.rs @@ -27,18 +27,19 @@ pub enum SolcError { /// Filesystem IO error #[error(transparent)] Io(#[from] SolcIoError), + #[error("File could not be resolved due to broken symlink: {0}.")] + ResolveBadSymlink(SolcIoError), /// Failed to resolve a file #[error("Failed to resolve file: {0}.\n Check configured remappings.")] Resolve(SolcIoError), - #[error("File could not be resolved due to broken symlink: {0}.")] - ResolveBadSymlink(SolcIoError), + #[error("File cannot be resolved due to mismatch of file name case: {error}.\n Found existing file: {existing_file:?}\n Please check the case of the import.")] + ResolveCaseSensitiveFileName { error: SolcIoError, existing_file: PathBuf }, #[error( - r#"Failed to resolve file: {0}. + r#"{0}. --> {1:?} - {2:?} - Check configured remappings."# + {2:?}"# )] - FailedResolveImport(SolcIoError, PathBuf, PathBuf), + FailedResolveImport(Box, PathBuf, PathBuf), #[cfg(feature = "svm-solc")] #[error(transparent)] SvmError(#[from] svm::SolcVmError), diff --git a/ethers-solc/src/resolver/mod.rs b/ethers-solc/src/resolver/mod.rs index 848713ad..732b5d1a 100644 --- a/ethers-solc/src/resolver/mod.rs +++ b/ethers-solc/src/resolver/mod.rs @@ -59,6 +59,7 @@ use std::{ mod parse; mod tree; +use crate::utils::find_case_sensitive_existing_file; pub use parse::SolImportAlias; pub use tree::{print, Charset, TreeOptions}; @@ -371,10 +372,12 @@ impl Graph { add_node(&mut unresolved, &mut index, &mut resolved_imports, import) .map_err(|err| { match err { - SolcError::Resolve(err) => { - // make the error more verbose + err @ SolcError::ResolveCaseSensitiveFileName { .. } | + err @ SolcError::Resolve(_) => { + // make the error more helpful by providing additional + // context SolcError::FailedResolveImport( - err, + Box::new(err), node.path.clone(), import_path.clone(), ) @@ -832,10 +835,21 @@ impl Node { pub fn read(file: impl AsRef) -> Result { let file = file.as_ref(); let source = Source::read(file).map_err(|err| { - if !err.path().exists() && err.path().is_symlink() { + let exists = err.path().exists(); + if !exists && err.path().is_symlink() { SolcError::ResolveBadSymlink(err) } else { - SolcError::Resolve(err) + // 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 = SolData::parse(source.as_ref(), file); diff --git a/ethers-solc/src/utils.rs b/ethers-solc/src/utils.rs index 71c10c37..bb4d0a3b 100644 --- a/ethers-solc/src/utils.rs +++ b/ethers-solc/src/utils.rs @@ -385,6 +385,26 @@ pub(crate) fn find_fave_or_alt_path(root: impl AsRef, fave: &str, alt: &st p } +/// Attempts to find a file with different case that exists next to the `non_existing` file +pub(crate) fn find_case_sensitive_existing_file(non_existing: &Path) -> Option { + let non_existing_file_name = non_existing.file_name()?; + let parent = non_existing.parent()?; + WalkDir::new(parent) + .max_depth(1) + .into_iter() + .filter_map(Result::ok) + .filter(|e| e.file_type().is_file()) + .find_map(|e| { + let existing_file_name = e.path().file_name()?; + if existing_file_name.eq_ignore_ascii_case(non_existing_file_name) && + existing_file_name != non_existing_file_name + { + return Some(e.path().to_path_buf()) + } + None + }) +} + #[cfg(not(target_arch = "wasm32"))] use tokio::runtime::{Handle, Runtime}; @@ -453,6 +473,7 @@ pub fn create_parent_dir_all(file: impl AsRef) -> Result<(), SolcError> { #[cfg(test)] mod tests { use super::*; + use crate::resolver::Node; use solang_parser::pt::SourceUnitPart; use std::{ collections::HashSet, @@ -460,6 +481,45 @@ mod tests { }; use tempdir; + #[test] + fn can_find_different_case() { + let tmp_dir = tempdir("out").unwrap(); + let path = tmp_dir.path().join("forge-std"); + create_dir_all(&path).unwrap(); + let existing = path.join("Test.sol"); + let non_existing = path.join("test.sol"); + std::fs::write(&existing, b"").unwrap(); + + #[cfg(target_os = "linux")] + assert!(!non_existing.exists()); + + let found = find_case_sensitive_existing_file(&non_existing).unwrap(); + assert_eq!(found, existing); + } + + #[cfg(target_os = "linux")] + #[test] + fn can_read_different_case() { + let tmp_dir = tempdir("out").unwrap(); + let path = tmp_dir.path().join("forge-std"); + create_dir_all(&path).unwrap(); + let existing = path.join("Test.sol"); + let non_existing = path.join("test.sol"); + std::fs::write( + &existing, + r#" +pragma solidity ^0.8.10; +contract A {} + "#, + ) + .unwrap(); + + assert!(!non_existing.exists()); + + let found = Node::read(&non_existing).unwrap_err(); + matches!(found, SolcError::ResolveCaseSensitiveFileName { .. }); + } + #[test] fn can_create_parent_dirs_with_ext() { let tmp_dir = tempdir("out").unwrap();