From 2c33acb3ad4fc9e92739c560dae6d25d76aa5845 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Fri, 12 Aug 2022 21:11:08 +0200 Subject: [PATCH] feat(solc): resolve absolute imports in libraries (#1590) * feat(solc): resolve absolute imports in libraries * feat(solc): support --include-path * update test * only add base path if not empty * simplify solc config * include root in include paths * test: add test for absolute imports * fix: bad predicate * cleanup * fix: use base-path directly * fix: exclude root from include set --- ethers-solc/src/compile/mod.rs | 9 ++ ethers-solc/src/compile/project.rs | 11 ++- ethers-solc/src/config.rs | 146 +++++++++++++++++++++++++---- ethers-solc/src/lib.rs | 117 ++++++++++++++++------- ethers-solc/src/resolver/mod.rs | 69 +++++++++----- ethers-solc/src/utils.rs | 31 ++++++ ethers-solc/tests/project.rs | 109 ++++++++++++++++++++- 7 files changed, 415 insertions(+), 77 deletions(-) diff --git a/ethers-solc/src/compile/mod.rs b/ethers-solc/src/compile/mod.rs index dde89f04..46ebed6c 100644 --- a/ethers-solc/src/compile/mod.rs +++ b/ethers-solc/src/compile/mod.rs @@ -40,6 +40,14 @@ pub const BERLIN_SOLC: Version = Version::new(0, 8, 5); /// pub const LONDON_SOLC: Version = Version::new(0, 8, 7); +// `--base-path` was introduced in 0.6.9 +pub static SUPPORTS_BASE_PATH: once_cell::sync::Lazy = + once_cell::sync::Lazy::new(|| VersionReq::parse(">=0.6.9").unwrap()); + +// `--include-path` was introduced in 0.8.8 +pub static SUPPORTS_INCLUDE_PATH: once_cell::sync::Lazy = + once_cell::sync::Lazy::new(|| VersionReq::parse(">=0.8.8").unwrap()); + #[cfg(any(test, feature = "tests"))] use std::sync::Mutex; @@ -527,6 +535,7 @@ impl Solc { let mut cmd = Command::new(&self.solc); if let Some(ref base_path) = self.base_path { cmd.current_dir(base_path); + cmd.arg("--base-path").arg(base_path); } let mut child = cmd .args(&self.args) diff --git a/ethers-solc/src/compile/project.rs b/ethers-solc/src/compile/project.rs index 5275efbe..f0fdce64 100644 --- a/ethers-solc/src/compile/project.rs +++ b/ethers-solc/src/compile/project.rs @@ -156,8 +156,7 @@ impl<'a, T: ArtifactOutput> ProjectCompiler<'a, T> { let graph = Graph::resolve_sources(&project.paths, sources)?; let (versions, edges) = graph.into_sources_by_version(project.offline)?; - let base_path = project.root(); - let sources_by_version = versions.get(&project.allowed_lib_paths, base_path)?; + let sources_by_version = versions.get(project)?; let sources = if project.solc_jobs > 1 && sources_by_version.len() > 1 { // if there are multiple different versions, and we can use multiple jobs we can compile @@ -178,6 +177,14 @@ impl<'a, T: ArtifactOutput> ProjectCompiler<'a, T> { ) -> Result { let version = solc.version()?; let (sources, edges) = Graph::resolve_sources(&project.paths, sources)?.into_sources(); + + // make sure `solc` has all required arguments + let solc = project.configure_solc_with_version( + solc, + Some(version.clone()), + edges.include_paths().clone(), + ); + let sources_by_version = BTreeMap::from([(solc, (version, sources))]); let sources = CompilerSources::Sequential(sources_by_version); diff --git a/ethers-solc/src/config.rs b/ethers-solc/src/config.rs index 30714b76..41a52ad3 100644 --- a/ethers-solc/src/config.rs +++ b/ethers-solc/src/config.rs @@ -1,19 +1,17 @@ use crate::{ - artifacts::Settings, + artifacts::{output_selection::ContractOutputSelection, Settings}, cache::SOLIDITY_FILES_CACHE_FILENAME, error::{Result, SolcError, SolcIoError}, remappings::Remapping, resolver::{Graph, SolImportAlias}, utils, Source, Sources, }; - -use crate::artifacts::output_selection::ContractOutputSelection; - use serde::{Deserialize, Serialize}; use std::{ collections::{BTreeSet, HashSet}, fmt::{self, Formatter}, fs, + ops::{Deref, DerefMut}, path::{Component, Path, PathBuf}, }; @@ -86,6 +84,15 @@ impl ProjectPathsConfig { paths } + /// Returns all `--include-path` paths that should be used for this project + /// + /// See [IncludePaths] + pub fn include_paths(&self) -> Vec { + // Note: root must not be included, since it will be used as base-path, which would be a + // conflict + vec![self.sources.clone(), self.tests.clone(), self.scripts.clone()] + } + /// Creates all configured dirs and files pub fn create_all(&self) -> std::result::Result<(), SolcIoError> { if let Some(parent) = self.cache.parent() { @@ -214,11 +221,20 @@ impl ProjectPathsConfig { /// Attempts to resolve an `import` from the given working directory. /// /// The `cwd` path is the parent dir of the file that includes the `import` - pub fn resolve_import(&self, cwd: &Path, import: &Path) -> Result { + /// + /// This will also populate the `include_paths` with any nested library root paths that should + /// be provided to solc via `--include-path` because it uses absolute imports. + pub fn resolve_import_and_include_paths( + &self, + cwd: &Path, + import: &Path, + include_paths: &mut IncludePaths, + ) -> Result { let component = import .components() .next() .ok_or_else(|| SolcError::msg(format!("Empty import path {}", import.display())))?; + if component == Component::CurDir || component == Component::ParentDir { // if the import is relative we assume it's already part of the processed input // file set @@ -227,7 +243,32 @@ impl ProjectPathsConfig { }) } else { // resolve library file - self.resolve_library_import(import.as_ref()).ok_or_else(|| { + let resolved = self.resolve_library_import(import.as_ref()); + + if resolved.is_none() { + // absolute paths in solidity are a thing for example `import + // "src/interfaces/IConfig.sol"` which could either point to `cwd + + // src/interfaces/IConfig.sol`, or make use of a remapping (`src/=....`) + if let Some(lib) = self.find_library_ancestor(cwd) { + if let Some((include_path, import)) = + utils::resolve_absolute_library(lib, cwd, import) + { + // track the path for this absolute import inside a nested library + include_paths.insert(include_path); + return Ok(import) + } + } + // also try to resolve absolute imports from the project paths + for path in [&self.root, &self.sources, &self.tests, &self.scripts] { + if cwd.starts_with(path) { + if let Ok(import) = utils::canonicalize(path.join(import)) { + return Ok(import) + } + } + } + } + + resolved.ok_or_else(|| { SolcError::msg(format!( "failed to resolve library import \"{:?}\"", import.display() @@ -236,6 +277,13 @@ impl ProjectPathsConfig { } } + /// Attempts to resolve an `import` from the given working directory. + /// + /// The `cwd` path is the parent dir of the file that includes the `import` + pub fn resolve_import(&self, cwd: &Path, import: &Path) -> Result { + self.resolve_import_and_include_paths(cwd, import, &mut Default::default()) + } + /// Attempts to find the path to the real solidity file that's imported via the given `import` /// path by applying the configured remappings and checking the library dirs /// @@ -751,6 +799,49 @@ impl SolcConfigBuilder { } } +/// Container for all `--include-path` arguments for Solc, se also [Solc docs](https://docs.soliditylang.org/en/v0.8.9/using-the-compiler.html#base-path-and-import-remapping +/// +/// The `--include--path` flag: +/// > Makes an additional source directory available to the default import callback. Use this option +/// > if you want to import contracts whose location is not fixed in relation to your main source +/// > tree, e.g. third-party libraries installed using a package manager. Can be used multiple +/// > times. Can only be used if base path has a non-empty value. +/// +/// In contrast to `--allow-paths` [`AllowedLibPaths`], which takes multiple arguments, +/// `--include-path` only takes a single path argument. +#[derive(Clone, Debug, Default)] +pub struct IncludePaths(pub(crate) BTreeSet); + +// === impl IncludePaths === + +impl IncludePaths { + /// Returns the [Command](std::process::Command) arguments for this type + /// + /// For each entry in the set, it will return `--include-path` + `` + pub fn args(&self) -> impl Iterator + '_ { + self.paths().flat_map(|path| ["--include-path".to_string(), format!("{}", path.display())]) + } + + /// Returns all paths that exist + pub fn paths(&self) -> impl Iterator + '_ { + self.0.iter().filter(|path| path.exists()) + } +} + +impl Deref for IncludePaths { + type Target = BTreeSet; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for IncludePaths { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + /// Helper struct for serializing `--allow-paths` arguments to Solc /// /// From the [Solc docs](https://docs.soliditylang.org/en/v0.8.9/using-the-compiler.html#base-path-and-import-remapping): @@ -761,23 +852,46 @@ impl SolcConfigBuilder { /// can be allowed via the --allow-paths /sample/path,/another/sample/path switch. /// Everything inside the path specified via --base-path is always allowed. #[derive(Clone, Debug, Default)] -pub struct AllowedLibPaths(pub(crate) Vec); +pub struct AllowedLibPaths(pub(crate) BTreeSet); + +// === impl AllowedLibPaths === impl AllowedLibPaths { - pub fn is_empty(&self) -> bool { - self.0.is_empty() + /// Returns the [Command](std::process::Command) arguments for this type + /// + /// `--allow-paths` takes a single value: all comma separated paths + pub fn args(&self) -> Option<[String; 2]> { + let args = self.to_string(); + if args.is_empty() { + return None + } + Some(["--allow-paths".to_string(), args]) + } + + /// Returns all paths that exist + pub fn paths(&self) -> impl Iterator + '_ { + self.0.iter().filter(|path| path.exists()) + } +} + +impl Deref for AllowedLibPaths { + type Target = BTreeSet; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for AllowedLibPaths { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 } } impl fmt::Display for AllowedLibPaths { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let lib_paths = self - .0 - .iter() - .filter(|path| path.exists()) - .map(|path| format!("{}", path.display())) - .collect::>() - .join(","); + let lib_paths = + self.paths().map(|path| format!("{}", path.display())).collect::>().join(","); write!(f, "{}", lib_paths) } } diff --git a/ethers-solc/src/lib.rs b/ethers-solc/src/lib.rs index 0e39c8a7..424c26c7 100644 --- a/ethers-solc/src/lib.rs +++ b/ethers-solc/src/lib.rs @@ -38,6 +38,7 @@ pub use filter::{FileFilter, TestFileFilter}; use crate::{ artifacts::Sources, cache::SolFilesCache, + config::IncludePaths, error::{SolcError, SolcIoError}, sources::{VersionedSourceFile, VersionedSourceFiles}, }; @@ -73,7 +74,9 @@ pub struct Project { /// Errors/Warnings which match these error codes are not going to be logged pub ignored_error_codes: Vec, /// The paths which will be allowed for library inclusion - pub allowed_lib_paths: AllowedLibPaths, + pub allowed_paths: AllowedLibPaths, + /// The paths which will be used with solc's `--include-path` attribute + pub include_paths: IncludePaths, /// Maximum number of `solc` processes to run simultaneously. solc_jobs: usize, /// Offline mode, if set, network access (download solc) is disallowed @@ -151,16 +154,48 @@ impl Project { SolFilesCache::read_joined(&self.paths) } + /// Applies the configured arguments to the given `Solc` + /// + /// See [Self::configure_solc_with_version()] + pub(crate) fn configure_solc(&self, solc: Solc) -> Solc { + let version = solc.version().ok(); + self.configure_solc_with_version(solc, version, Default::default()) + } + /// Applies the configured arguments to the given `Solc` /// /// This will set the `--allow-paths` to the paths configured for the `Project`, if any. - fn configure_solc(&self, mut solc: Solc) -> Solc { - if !self.allowed_lib_paths.0.is_empty() && - !solc.args.iter().any(|arg| arg == "--allow-paths") - { - solc = solc.arg("--allow-paths").arg(self.allowed_lib_paths.to_string()); + /// + /// If a version is provided and it is applicable it will also set `--base-path` and + /// `--include-path` This will set the `--allow-paths` to the paths configured for the + /// `Project`, if any. + /// This also accepts additional `include_paths` + pub(crate) fn configure_solc_with_version( + &self, + mut solc: Solc, + version: Option, + mut include_paths: IncludePaths, + ) -> Solc { + if !solc.args.iter().any(|arg| arg == "--allow-paths") { + if let Some([allow, libs]) = self.allowed_paths.args() { + solc = solc.arg(allow).arg(libs); + } } - solc.with_base_path(self.root()) + if let Some(version) = version { + if SUPPORTS_BASE_PATH.matches(&version) { + let base_path = format!("{}", self.root().display()); + if !base_path.is_empty() { + solc = solc.with_base_path(self.root()); + if SUPPORTS_INCLUDE_PATH.matches(&version) { + include_paths.extend(self.include_paths.paths().cloned()); + solc = solc.args(include_paths.args()); + } + } + } else { + solc.base_path.take(); + } + } + solc } /// Sets the maximum number of parallel `solc` processes to run simultaneously. @@ -232,9 +267,7 @@ impl Project { return self.svm_compile(sources) } - let solc = self.configure_solc(self.solc.clone()); - - self.compile_with_version(&solc, sources) + self.compile_with_version(&self.solc, sources) } /// Compiles a set of contracts using `svm` managed solc installs @@ -360,13 +393,9 @@ impl Project { .compile() } - project::ProjectCompiler::with_sources_and_solc( - self, - sources, - self.configure_solc(self.solc.clone()), - )? - .with_sparse_output(filter) - .compile() + project::ProjectCompiler::with_sources_and_solc(self, sources, self.solc.clone())? + .with_sparse_output(filter) + .compile() } /// Compiles the given source files with the exact `Solc` executable @@ -397,12 +426,7 @@ impl Project { solc: &Solc, sources: Sources, ) -> Result> { - project::ProjectCompiler::with_sources_and_solc( - self, - sources, - self.configure_solc(solc.clone()), - )? - .compile() + project::ProjectCompiler::with_sources_and_solc(self, sources, solc.clone())?.compile() } /// Removes the project's artifacts and cache file @@ -541,8 +565,10 @@ pub struct ProjectBuilder { artifacts: T, /// Which error codes to ignore pub ignored_error_codes: Vec, - /// All allowed paths - pub allowed_paths: Vec, + /// All allowed paths for solc's `--allowed-paths` + allowed_paths: AllowedLibPaths, + /// Paths to use for solc's `--include-path` + include_paths: IncludePaths, solc_jobs: Option, } @@ -561,7 +587,8 @@ impl ProjectBuilder { slash_paths: true, artifacts, ignored_error_codes: Vec::new(), - allowed_paths: vec![], + allowed_paths: Default::default(), + include_paths: Default::default(), solc_jobs: None, } } @@ -697,6 +724,7 @@ impl ProjectBuilder { auto_detect, ignored_error_codes, allowed_paths, + include_paths, solc_jobs, offline, build_info, @@ -715,6 +743,7 @@ impl ProjectBuilder { artifacts, ignored_error_codes, allowed_paths, + include_paths, solc_jobs, build_info, } @@ -723,7 +752,7 @@ impl ProjectBuilder { /// Adds an allowed-path to the solc executable #[must_use] pub fn allowed_path>(mut self, path: P) -> Self { - self.allowed_paths.push(path.into()); + self.allowed_paths.insert(path.into()); self } @@ -740,6 +769,26 @@ impl ProjectBuilder { self } + /// Adds an `--include-path` to the solc executable + #[must_use] + pub fn include_path>(mut self, path: P) -> Self { + self.include_paths.insert(path.into()); + self + } + + /// Adds multiple include-path to the solc executable + #[must_use] + pub fn include_paths(mut self, args: I) -> Self + where + I: IntoIterator, + S: Into, + { + for arg in args { + self = self.include_path(arg); + } + self + } + pub fn build(self) -> Result> { let Self { paths, @@ -751,6 +800,7 @@ impl ProjectBuilder { artifacts, ignored_error_codes, mut allowed_paths, + mut include_paths, solc_jobs, offline, build_info, @@ -767,10 +817,10 @@ impl ProjectBuilder { let solc = solc.unwrap_or_default(); let solc_config = solc_config.unwrap_or_else(|| SolcConfig::builder().build()); - if allowed_paths.is_empty() { - // allow every contract under root by default - allowed_paths.push(paths.root.clone()) - } + // allow every contract under root by default + allowed_paths.insert(paths.root.clone()); + // allow paths where contracts are stored by default + include_paths.extend(paths.include_paths()); Ok(Project { paths, @@ -782,8 +832,9 @@ impl ProjectBuilder { auto_detect, artifacts, ignored_error_codes, - allowed_lib_paths: allowed_paths.into(), - solc_jobs: solc_jobs.unwrap_or_else(::num_cpus::get), + allowed_paths, + include_paths, + solc_jobs: solc_jobs.unwrap_or_else(num_cpus::get), offline, slash_paths, }) diff --git a/ethers-solc/src/resolver/mod.rs b/ethers-solc/src/resolver/mod.rs index 374f678e..3c68f2d6 100644 --- a/ethers-solc/src/resolver/mod.rs +++ b/ethers-solc/src/resolver/mod.rs @@ -46,19 +46,16 @@ //! [version pragma](https://docs.soliditylang.org/en/develop/layout-of-source-files.html#version-pragma), //! which is defined on a per source file basis. +use crate::{error::Result, utils, IncludePaths, ProjectPathsConfig, SolcError, Source, Sources}; +use parse::{SolData, SolDataUnit, SolImport}; +use rayon::prelude::*; +use semver::VersionReq; use std::{ collections::{HashMap, HashSet, VecDeque}, fmt, io, path::{Path, PathBuf}, }; -use parse::{SolData, SolDataUnit, SolImport}; -use rayon::prelude::*; - -use semver::VersionReq; - -use crate::{error::Result, utils, ProjectPathsConfig, SolcError, Source, Sources}; - mod parse; mod tree; @@ -90,6 +87,13 @@ pub struct GraphEdges { num_input_files: usize, /// tracks all imports that we failed to resolve for a file unresolved_imports: HashSet<(PathBuf, PathBuf)>, + /// tracks additional include paths resolved by scanning all imports of the graph + /// + /// Absolute imports, like `import "src/Contract.sol"` are possible, but this does not play + /// nice with the standard-json import format, since the VFS won't be able to resolve + /// "src/Contract.sol" without help via `--include-path` + #[allow(unused)] + resolved_solc_include_paths: IncludePaths, } impl GraphEdges { @@ -113,6 +117,11 @@ impl GraphEdges { self.files().skip(self.num_input_files) } + /// Returns all additional `--include-paths` + pub fn include_paths(&self) -> &IncludePaths { + &self.resolved_solc_include_paths + } + /// Returns all imports that we failed to resolve pub fn unresolved_imports(&self) -> &HashSet<(PathBuf, PathBuf)> { &self.unresolved_imports @@ -333,6 +342,10 @@ impl Graph { let mut nodes = Vec::with_capacity(unresolved.len()); let mut edges = Vec::with_capacity(unresolved.len()); + // tracks additional paths that should be used with `--include-path`, these are libraries + // that use absolute imports like `import "src/Contract.sol"` + let mut resolved_solc_include_paths = IncludePaths::default(); + // keep track of all unique paths that we failed to resolve to not spam the reporter with // the same path let mut unresolved_imports = HashSet::new(); @@ -349,7 +362,11 @@ impl Graph { for import in node.data.imports.iter() { let import_path = import.data().path(); - match paths.resolve_import(cwd, import_path) { + 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)?; } @@ -391,6 +408,7 @@ impl Graph { .collect(), data: Default::default(), unresolved_imports, + resolved_solc_include_paths, }; Ok(Graph { nodes, edges, root: paths.root.clone() }) } @@ -461,7 +479,14 @@ impl Graph { } versioned_sources.insert(version, sources); } - Ok((VersionedSources { inner: versioned_sources, offline }, edges)) + Ok(( + VersionedSources { + inner: versioned_sources, + offline, + resolved_solc_include_paths: edges.resolved_solc_include_paths.clone(), + }, + edges, + )) } /// Writes the list of imported files into the given formatter: @@ -713,6 +738,7 @@ impl<'a> Iterator for NodesIter<'a> { #[cfg(all(feature = "svm-solc"))] #[derive(Debug)] pub struct VersionedSources { + resolved_solc_include_paths: IncludePaths, inner: HashMap, offline: bool, } @@ -724,17 +750,11 @@ impl VersionedSources { /// This will also configure following solc arguments: /// - `allowed_paths` /// - `base_path` - pub fn get( + pub fn get( self, - allowed_lib_paths: &crate::AllowedLibPaths, - base_path: impl AsRef, + project: &crate::Project, ) -> Result> { use crate::Solc; - - // `--base-path` was introduced in 0.6.9 - static SUPPORTS_BASE_PATH: once_cell::sync::Lazy = - once_cell::sync::Lazy::new(|| VersionReq::parse(">=0.6.9").unwrap()); - // we take the installer lock here to ensure installation checking is done in sync #[cfg(any(test, feature = "tests"))] let _lock = crate::compile::take_solc_installer_lock(); @@ -771,16 +791,15 @@ impl VersionedSources { tracing::trace!("reinstalled solc: \"{}\"", version); } } - let mut solc = solc - .arg("--allow-paths") - .arg(allowed_lib_paths.to_string()) - .with_base_path(base_path.as_ref()); + let version = solc.version()?; - if SUPPORTS_BASE_PATH.matches(&version) { - solc = solc.arg("--base-path").arg(format!("{}", base_path.as_ref().display())); - } - + // this will configure the `Solc` executable and its arguments + let solc = project.configure_solc_with_version( + solc, + Some(version.clone()), + self.resolved_solc_include_paths.clone(), + ); sources_by_version.insert(solc, (version, sources)); } Ok(sources_by_version) diff --git a/ethers-solc/src/utils.rs b/ethers-solc/src/utils.rs index a14e6d27..89636366 100644 --- a/ethers-solc/src/utils.rs +++ b/ethers-solc/src/utils.rs @@ -223,6 +223,37 @@ pub fn resolve_library(libs: &[impl AsRef], source: impl AsRef) -> O } } +/// Tries to find an absolute import like `src/interfaces/IConfig.sol` in `cwd`, moving up the path +/// until the `root` is reached. +/// +/// If an existing file under `root` is found, this returns the path up to the `import` path and the +/// canonicalized `import` path itself: +/// +/// For example for following layout: +/// +/// ```text +/// /mydependency/ +/// ├── src (`cwd`) +/// │ ├── interfaces +/// │ │ ├── IConfig.sol +/// ``` +/// and `import` as `src/interfaces/IConfig.sol` and `cwd` as `src` this will return +/// (`/mydependency/`, `/mydependency/src/interfaces/IConfig.sol`) +pub fn resolve_absolute_library( + root: &Path, + cwd: &Path, + import: &Path, +) -> Option<(PathBuf, PathBuf)> { + let mut parent = cwd.parent()?; + while parent != root { + if let Ok(import) = canonicalize(parent.join(import)) { + return Some((parent.to_path_buf(), import)) + } + parent = parent.parent()?; + } + None +} + /// Reads the list of Solc versions that have been installed in the machine. The version list is /// sorted in ascending order. /// Checks for installed solc versions under the given path as diff --git a/ethers-solc/tests/project.rs b/ethers-solc/tests/project.rs index 7ce00970..4944e0ee 100644 --- a/ethers-solc/tests/project.rs +++ b/ethers-solc/tests/project.rs @@ -1202,7 +1202,7 @@ library MyLib { #[test] fn can_recompile_with_changes() { let mut tmp = TempProject::dapptools().unwrap(); - tmp.project_mut().allowed_lib_paths = vec![tmp.root().join("modules")].into(); + tmp.project_mut().allowed_paths = vec![tmp.root().join("modules")].into(); let content = r#" pragma solidity ^0.8.10; @@ -2211,3 +2211,110 @@ fn can_add_basic_contract_and_library() { assert!(compiled.find_first("Foo").is_some()); assert!(compiled.find_first("Bar").is_some()); } + +// +#[test] +fn can_handle_nested_absolute_imports() { + let mut project = TempProject::dapptools().unwrap(); + + let remapping = project.paths().libraries[0].join("myDepdendency"); + project + .paths_mut() + .remappings + .push(Remapping::from_str(&format!("myDepdendency/={}/", remapping.display())).unwrap()); + + project + .add_lib( + "myDepdendency/src/interfaces/IConfig.sol", + r#" + pragma solidity ^0.8.10; + + interface IConfig {} + "#, + ) + .unwrap(); + + project + .add_lib( + "myDepdendency/src/Config.sol", + r#" + pragma solidity ^0.8.10; + import "src/interfaces/IConfig.sol"; + + contract Config {} + "#, + ) + .unwrap(); + + project + .add_source( + "Greeter", + r#" + pragma solidity ^0.8.10; + import "myDepdendency/src/Config.sol"; + + contract Greeter {} + "#, + ) + .unwrap(); + + let compiled = project.compile().unwrap(); + assert!(!compiled.has_compiler_errors()); + assert!(compiled.find_first("Greeter").is_some()); + assert!(compiled.find_first("Config").is_some()); + assert!(compiled.find_first("IConfig").is_some()); +} + +#[test] +fn can_handle_nested_test_absolute_imports() { + let project = TempProject::dapptools().unwrap(); + + project + .add_source( + "Contract.sol", + r#" +// SPDX-License-Identifier: UNLICENSED +pragma solidity =0.8.13; + +library Library { + function f(uint256 a, uint256 b) public pure returns (uint256) { + return a + b; + } +} + +contract Contract { + uint256 c; + + constructor() { + c = Library.f(1, 2); + } +} + "#, + ) + .unwrap(); + + project + .add_test( + "Contract.t.sol", + r#" +// SPDX-License-Identifier: UNLICENSED +pragma solidity =0.8.13; + +import "src/Contract.sol"; + +contract ContractTest { + function setUp() public { + } + + function test() public { + new Contract(); + } +} + "#, + ) + .unwrap(); + + let compiled = project.compile().unwrap(); + assert!(!compiled.has_compiler_errors()); + assert!(compiled.find_first("Contract").is_some()); +}