feat(solc): improve error on case mismatch (#1998)

This commit is contained in:
Matthias Seitz 2023-01-03 14:14:01 +01:00 committed by GitHub
parent 6ac3e75c6a
commit d1df3417f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 86 additions and 11 deletions

View File

@ -27,18 +27,19 @@ pub enum SolcError {
/// Filesystem IO error /// Filesystem IO error
#[error(transparent)] #[error(transparent)]
Io(#[from] SolcIoError), Io(#[from] SolcIoError),
#[error("File could not be resolved due to broken symlink: {0}.")]
ResolveBadSymlink(SolcIoError),
/// Failed to resolve a file /// Failed to resolve a file
#[error("Failed to resolve file: {0}.\n Check configured remappings.")] #[error("Failed to resolve file: {0}.\n Check configured remappings.")]
Resolve(SolcIoError), Resolve(SolcIoError),
#[error("File could not be resolved due to broken symlink: {0}.")] #[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.")]
ResolveBadSymlink(SolcIoError), ResolveCaseSensitiveFileName { error: SolcIoError, existing_file: PathBuf },
#[error( #[error(
r#"Failed to resolve file: {0}. r#"{0}.
--> {1:?} --> {1:?}
{2:?} {2:?}"#
Check configured remappings."#
)] )]
FailedResolveImport(SolcIoError, PathBuf, PathBuf), FailedResolveImport(Box<SolcError>, PathBuf, PathBuf),
#[cfg(feature = "svm-solc")] #[cfg(feature = "svm-solc")]
#[error(transparent)] #[error(transparent)]
SvmError(#[from] svm::SolcVmError), SvmError(#[from] svm::SolcVmError),

View File

@ -59,6 +59,7 @@ use std::{
mod parse; mod parse;
mod tree; mod tree;
use crate::utils::find_case_sensitive_existing_file;
pub use parse::SolImportAlias; pub use parse::SolImportAlias;
pub use tree::{print, Charset, TreeOptions}; pub use tree::{print, Charset, TreeOptions};
@ -371,10 +372,12 @@ impl Graph {
add_node(&mut unresolved, &mut index, &mut resolved_imports, import) add_node(&mut unresolved, &mut index, &mut resolved_imports, import)
.map_err(|err| { .map_err(|err| {
match err { match err {
SolcError::Resolve(err) => { err @ SolcError::ResolveCaseSensitiveFileName { .. } |
// make the error more verbose err @ SolcError::Resolve(_) => {
// make the error more helpful by providing additional
// context
SolcError::FailedResolveImport( SolcError::FailedResolveImport(
err, Box::new(err),
node.path.clone(), node.path.clone(),
import_path.clone(), import_path.clone(),
) )
@ -832,11 +835,22 @@ impl Node {
pub fn read(file: impl AsRef<Path>) -> Result<Self> { pub fn read(file: impl AsRef<Path>) -> Result<Self> {
let file = file.as_ref(); let file = file.as_ref();
let source = Source::read(file).map_err(|err| { 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) SolcError::ResolveBadSymlink(err)
} else {
// This is an additional check useful on OS that have case-sensitive paths, See also <https://docs.soliditylang.org/en/v0.8.17/path-resolution.html#import-callback>
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 { } else {
SolcError::Resolve(err) SolcError::Resolve(err)
} }
} else {
SolcError::Resolve(err)
}
}
})?; })?;
let data = SolData::parse(source.as_ref(), file); let data = SolData::parse(source.as_ref(), file);
Ok(Self { path: file.to_path_buf(), source, data }) Ok(Self { path: file.to_path_buf(), source, data })

View File

@ -385,6 +385,26 @@ pub(crate) fn find_fave_or_alt_path(root: impl AsRef<Path>, fave: &str, alt: &st
p 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<PathBuf> {
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"))] #[cfg(not(target_arch = "wasm32"))]
use tokio::runtime::{Handle, Runtime}; use tokio::runtime::{Handle, Runtime};
@ -453,6 +473,7 @@ pub fn create_parent_dir_all(file: impl AsRef<Path>) -> Result<(), SolcError> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::resolver::Node;
use solang_parser::pt::SourceUnitPart; use solang_parser::pt::SourceUnitPart;
use std::{ use std::{
collections::HashSet, collections::HashSet,
@ -460,6 +481,45 @@ mod tests {
}; };
use tempdir; 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] #[test]
fn can_create_parent_dirs_with_ext() { fn can_create_parent_dirs_with_ext() {
let tmp_dir = tempdir("out").unwrap(); let tmp_dir = tempdir("out").unwrap();