Repository: gakonst/ethers-rs
Pull request #780 (2022-01-10 03:11:13)

chore(deps): bump tempfile from 3.2.0 to 3.3.0
Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.2.0 to 3.3.0.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/Stebalien/tempfile/blob/master/NEWS">tempfile's changelog</a>.</em></p>
<blockquote>
<h1>3.3.0</h1>
<p>Features:</p>
<ul>
<li>Replace rand with fastrand for a significantly smaller dependency tree. Cryptographic randomness
isn't necessary for temporary file names, and isn't all that helpful either.</li>
<li>Add limited WASI support.</li>
<li>Add a function to extract the inner data from a <code>SpooledTempFile</code>.</li>
</ul>
<p>Bug Fixes:</p>
<ul>
<li>Make it possible to persist unnamed temporary files on linux by removing the <code>O_EXCL</code> flag.</li>
<li>Fix redox minimum crate version.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/Stebalien/tempfile/commit/1a40687e06eb656044e3d2dffa1379f04b3ef3fd"><code>1a40687</code></a> release 3.3.0</li>
<li><a href="https://github.com/Stebalien/tempfile/commit/92638c61f30534521d248fb0e708770b9a28cef7"><code>92638c6</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/Stebalien/tempfile/issues/164">#164</a> from Stebalien/chore/cleanup</li>
<li><a href="https://github.com/Stebalien/tempfile/commit/92ae3e9d6e82bdcfcf114be459d234d3602be2d4"><code>92ae3e9</code></a> chore: remove extern crates</li>
<li><a href="https://github.com/Stebalien/tempfile/commit/e49d9dffe8b655d02adbf08f1359b6b9bdebb1d6"><code>e49d9df</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/Stebalien/tempfile/issues/165">#165</a> from Stebalien/pr-116</li>
<li><a href="https://github.com/Stebalien/tempfile/commit/a5f535c95b370f521e318c8b909067cb830a78e9"><code>a5f535c</code></a> fix: doc formatting</li>
<li><a href="https://github.com/Stebalien/tempfile/commit/ab67ccee85fe00df744c4422befafd0ce1ff264d"><code>ab67cce</code></a> Add docs pointing out early dop pitfalls</li>
<li><a href="https://github.com/Stebalien/tempfile/commit/8d40916b419171fa79f978583cf84740bce5e802"><code>8d40916</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/Stebalien/tempfile/issues/163">#163</a> from Stebalien/chore/update-deps</li>
<li><a href="https://github.com/Stebalien/tempfile/commit/9a6321760bcfae15ac488f1f4bd94db4e97e9367"><code>9a63217</code></a> chore: update fastrand</li>
<li><a href="https://github.com/Stebalien/tempfile/commit/09e0b4b1b4382ab0bdb0c4df661e411ff462d5b9"><code>09e0b4b</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/Stebalien/tempfile/issues/162">#162</a> from Stebalien/ref/replace-rand-with-fastrand</li>
<li><a href="https://github.com/Stebalien/tempfile/commit/e24fae3ac2857544c608ad4727710a70d7631a14"><code>e24fae3</code></a> chore: avoid allocation when generating temp names</li>
<li>Additional commits viewable in <a href="https://github.com/Stebalien/tempfile/compare/v3.2.0...v3.3.0">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=tempfile&package-manager=cargo&previous-version=3.2.0&new-version=3.3.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index ac501c20..7e4a572e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1383,6 +1383,15 @@ version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
 
+[[package]]
+name = "fastrand"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "779d043b6a0b90cc4c0ed7ee380a6504394cee7efd7db050e3774eee387324b2"
+dependencies = [
+ "instant",
+]
+
 [[package]]
 name = "ff"
 version = "0.11.0"
@@ -2784,15 +2793,16 @@ dependencies = [
 
 [[package]]
 name = "reqwest"
-version = "0.11.8"
+version = "0.11.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7c4e0a76dc12a116108933f6301b95e83634e0c47b0afbed6abbaa0601e99258"
+checksum = "87f242f1488a539a79bac6dbe7c8609ae43b7914b7736210f239a37cccb32525"
 dependencies = [
  "base64 0.13.0",
  "bytes",
  "encoding_rs",
  "futures-core",
  "futures-util",
+ "h2",
  "http",
  "http-body",
  "hyper",
@@ -3493,13 +3503,13 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
 
 [[package]]
 name = "tempfile"
-version = "3.2.0"
+version = "3.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22"
+checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
 dependencies = [
  "cfg-if 1.0.0",
+ "fastrand",
  "libc",
- "rand 0.8.4",
  "redox_syscall",
  "remove_dir_all",
  "winapi",
diff --git a/ethers-core/src/types/chain.rs b/ethers-core/src/types/chain.rs
index 217e2d3b..c522b1e5 100644
--- a/ethers-core/src/types/chain.rs
+++ b/ethers-core/src/types/chain.rs
@@ -1,6 +1,7 @@
 use serde::Deserialize;
 use thiserror::Error;
 
+use core::convert::TryFrom;
 use std::{fmt, str::FromStr};
 
 use crate::types::U256;
@@ -9,25 +10,26 @@ use crate::types::U256;
 #[error("Failed to parse chain: {0}")]
 pub struct ParseChainError(String);
 
+#[repr(u64)]
 #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Deserialize)]
 #[serde(rename_all = "snake_case")]
 pub enum Chain {
-    Mainnet,
-    Ropsten,
-    Rinkeby,
-    Goerli,
-    Kovan,
-    XDai,
-    Polygon,
-    PolygonMumbai,
-    Avalanche,
-    AvalancheFuji,
-    Sepolia,
-    Moonbeam,
-    MoonbeamDev,
-    Moonriver,
-    Optimism,
-    OptimismKovan,
+    Mainnet = 1,
+    Ropsten = 3,
+    Rinkeby = 4,
+    Goerli = 5,
+    Kovan = 42,
+    XDai = 100,
+    Polygon = 137,
+    PolygonMumbai = 80001,
+    Avalanche = 43114,
+    AvalancheFuji = 43113,
+    Sepolia = 11155111,
+    Moonbeam = 1287,
+    MoonbeamDev = 1281,
+    Moonriver = 1285,
+    Optimism = 10,
+    OptimismKovan = 69,
 }
 
 impl fmt::Display for Chain {
@@ -38,24 +40,7 @@ impl fmt::Display for Chain {
 
 impl From<Chain> for u32 {
     fn from(chain: Chain) -> Self {
-        match chain {
-            Chain::Mainnet => 1,
-            Chain::Ropsten => 3,
-            Chain::Rinkeby => 4,
-            Chain::Goerli => 5,
-            Chain::Kovan => 42,
-            Chain::XDai => 100,
-            Chain::Polygon => 137,
-            Chain::PolygonMumbai => 80001,
-            Chain::Avalanche => 43114,
-            Chain::AvalancheFuji => 43113,
-            Chain::Sepolia => 11155111,
-            Chain::Moonbeam => 1287,
-            Chain::MoonbeamDev => 1281,
-            Chain::Moonriver => 1285,
-            Chain::Optimism => 10,
-            Chain::OptimismKovan => 69,
-        }
+        chain as u32
     }
 }
 
@@ -71,6 +56,32 @@ impl From<Chain> for u64 {
     }
 }
 
+impl TryFrom<u64> for Chain {
+    type Error = ParseChainError;
+
+    fn try_from(chain: u64) -> Result<Chain, Self::Error> {
+        match chain {
+            1 => Ok(Chain::Mainnet),
+            3 => Ok(Chain::Ropsten),
+            4 => Ok(Chain::Rinkeby),
+            5 => Ok(Chain::Goerli),
+            42 => Ok(Chain::Kovan),
+            100 => Ok(Chain::XDai),
+            137 => Ok(Chain::Polygon),
+            80001 => Ok(Chain::PolygonMumbai),
+            43114 => Ok(Chain::Avalanche),
+            43113 => Ok(Chain::AvalancheFuji),
+            11155111 => Ok(Chain::Sepolia),
+            1287 => Ok(Chain::Moonbeam),
+            1281 => Ok(Chain::MoonbeamDev),
+            1285 => Ok(Chain::Moonriver),
+            10 => Ok(Chain::Optimism),
+            69 => Ok(Chain::OptimismKovan),
+            _ => return Err(ParseChainError(chain.to_string())),
+        }
+    }
+}
+
 impl FromStr for Chain {
     type Err = ParseChainError;
     fn from_str(chain: &str) -> Result<Self, Self::Err> {
diff --git a/ethers-etherscan/Cargo.toml b/ethers-etherscan/Cargo.toml
index 7fd7a050..e1f6efa2 100644
--- a/ethers-etherscan/Cargo.toml
+++ b/ethers-etherscan/Cargo.toml
@@ -15,7 +15,7 @@ keywords = ["ethereum", "web3", "etherscan", "ethers"]
 
 [dependencies]
 ethers-core = { version = "^0.6.0", path = "../ethers-core", default-features = false }
-reqwest = { version = "0.11.7", default-features = false, features = ["json"] }
+reqwest = { version = "0.11.9", default-features = false, features = ["json"] }
 serde = { version = "1.0.124", default-features = false, features = ["derive"] }
 serde_json = { version = "1.0.64", default-features = false }
 serde-aux = { version = "3.0.1", default-features = false }
diff --git a/ethers-middleware/Cargo.toml b/ethers-middleware/Cargo.toml
index 562a62d0..ea3cae31 100644
--- a/ethers-middleware/Cargo.toml
+++ b/ethers-middleware/Cargo.toml
@@ -28,7 +28,7 @@ tracing = { version = "0.1.29", default-features = false }
 tracing-futures = { version = "0.2.5", default-features = false }
 
 # for gas oracles
-reqwest = { version = "0.11.7", default-features = false, features = ["json", "rustls-tls"] }
+reqwest = { version = "0.11.9", default-features = false, features = ["json", "rustls-tls"] }
 url = { version = "2.2.2", default-features = false }
 
 serde_json = { version = "1.0.64", default-features = false }
diff --git a/ethers-providers/Cargo.toml b/ethers-providers/Cargo.toml
index 38869e7e..b68d0652 100644
--- a/ethers-providers/Cargo.toml
+++ b/ethers-providers/Cargo.toml
@@ -18,7 +18,7 @@ ethers-core = { version = "^0.6.0", path = "../ethers-core", default-features =
 
 async-trait = { version = "0.1.50", default-features = false }
 hex = { version = "0.4.3", default-features = false, features = ["std"] }
-reqwest = { version = "0.11.7", default-features = false, features = ["json"] }
+reqwest = { version = "0.11.9", default-features = false, features = ["json"] }
 serde = { version = "1.0.124", default-features = false, features = ["derive"] }
 serde_json = { version = "1.0.64", default-features = false }
 thiserror = { version = "1.0.30", default-features = false }
@@ -55,7 +55,7 @@ parking_lot = { version = "0.11", features = ["wasm-bindgen"] }
 
 [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
 tokio = { version = "1.5", default-features = false, features = ["rt", "macros"] }
-tempfile = "3.2.0"
+tempfile = "3.3.0"
 
 [features]
 default = ["ws", "rustls"]
diff --git a/ethers-signers/Cargo.toml b/ethers-signers/Cargo.toml
index 77c21228..40bc5561 100644
--- a/ethers-signers/Cargo.toml
+++ b/ethers-signers/Cargo.toml
@@ -51,7 +51,7 @@ yubihsm = { version = "0.40.0", features = ["secp256k1", "usb", "mockhsm"] }
 [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
 yubihsm = { version = "0.40.0", features = ["secp256k1", "usb", "mockhsm"] }
 tokio = { version = "1.5", default-features = false, features = ["macros", "rt"] }
-tempfile = "3.2.0"
+tempfile = "3.3.0"
 
 [features]
 celo = ["ethers-core/celo"]
diff --git a/ethers-solc/Cargo.toml b/ethers-solc/Cargo.toml
index 7daef9b7..042ae8aa 100644
--- a/ethers-solc/Cargo.toml
+++ b/ethers-solc/Cargo.toml
@@ -31,7 +31,7 @@ glob = "0.3.0"
 tracing = "0.1.29"
 num_cpus = "1.13.1"
 tiny-keccak = { version = "2.0.2", default-features = false }
-tempfile = { version = "3.2.0", optional = true }
+tempfile = { version = "3.3.0", optional = true }
 fs_extra = { version = "1.2.0", optional = true }
 sha2 = { version = "0.9.8", default-features = false }
 dunce = "1.0.2"
@@ -51,7 +51,7 @@ getrandom = { version = "0.2", features = ["js"] }
 [dev-dependencies]
 criterion = { version = "0.3", features = ["async_tokio"] }
 pretty_assertions = "1.0.0"
-tempfile = "3.2.0"
+tempfile = "3.3.0"
 tokio = { version = "1.15.0", features = ["full"] }
 
 [[bench]]
diff --git a/ethers-solc/src/cache.rs b/ethers-solc/src/cache.rs
index aa6a3dd1..72b95c7a 100644
--- a/ethers-solc/src/cache.rs
+++ b/ethers-solc/src/cache.rs
@@ -3,11 +3,11 @@ use crate::{
     artifacts::{Contracts, Sources},
     config::SolcConfig,
     error::{Result, SolcError},
-    utils, ArtifactOutput,
+    utils, ArtifactOutput, ProjectPathsConfig,
 };
 use serde::{Deserialize, Serialize};
 use std::{
-    collections::{BTreeMap, HashMap},
+    collections::{BTreeMap, HashMap, HashSet},
     fs::{self, File},
     path::{Path, PathBuf},
     time::{Duration, UNIX_EPOCH},
@@ -133,17 +133,15 @@ impl SolFilesCache {
         &'a self,
         sources: Sources,
         config: Option<&'a SolcConfig>,
-        artifacts_root: &Path,
+        paths: &ProjectPathsConfig,
     ) -> Sources {
+        // all file hashes
+        let content_hashes: HashMap<_, _> =
+            sources.iter().map(|(file, source)| (file.clone(), source.content_hash())).collect();
         sources
             .into_iter()
-            .filter(move |(file, source)| {
-                self.has_changed_or_missing_artifact::<T>(
-                    file,
-                    source.content_hash().as_bytes(),
-                    config,
-                    artifacts_root,
-                )
+            .filter(move |(file, _)| {
+                self.has_changed_or_missing_artifact::<T>(file, &content_hashes, config, paths)
             })
             .collect()
     }
@@ -153,10 +151,11 @@ impl SolFilesCache {
     pub fn has_changed_or_missing_artifact<T: ArtifactOutput>(
         &self,
         file: &Path,
-        hash: &[u8],
+        hashes: &HashMap<PathBuf, String>,
         config: Option<&SolcConfig>,
-        artifacts_root: &Path,
+        paths: &ProjectPathsConfig,
     ) -> bool {
+        let hash = hashes.get(file).unwrap().as_bytes();
         if let Some(entry) = self.files.get(file) {
             if entry.content_hash.as_bytes() != hash {
                 tracing::trace!("changed content hash for cached artifact \"{}\"", file.display());
@@ -172,21 +171,60 @@ impl SolFilesCache {
                 }
             }
 
-            let missing_artifacts =
-                entry.artifacts.iter().any(|name| !T::output_exists(file, name, artifacts_root));
-            if missing_artifacts {
+            // checks whether an artifact this file depends on was removed
+            if entry.artifacts.iter().any(|name| !T::output_exists(file, name, &paths.artifacts)) {
                 tracing::trace!(
                     "missing linked artifacts for cached artifact \"{}\"",
                     file.display()
                 );
+                return true
             }
-            missing_artifacts
+
+            // check if any of the file's imported files changed
+            self.has_changed_imports(file, entry, hashes, paths, &mut HashSet::new())
         } else {
             tracing::trace!("missing cached artifact for \"{}\"", file.display());
             true
         }
     }
 
+    /// Returns true if the entry has any imports that were changed
+    fn has_changed_imports(
+        &self,
+        path: &Path,
+        entry: &CacheEntry,
+        hashes: &HashMap<PathBuf, String>,
+        paths: &ProjectPathsConfig,
+        traversed: &mut HashSet<PathBuf>,
+    ) -> bool {
+        let cwd = match path.parent() {
+            Some(inner) => inner,
+            None => return true,
+        };
+        if !traversed.insert(path.to_path_buf()) {
+            // skip already traversed files, this prevents SO for circular imports
+            return false
+        }
+
+        for import in entry.imports.iter() {
+            if let Some((import, import_path)) = paths
+                .resolve_import(cwd, Path::new(import.as_str()))
+                .ok()
+                .and_then(|import| self.files.get(&import).map(|e| (e, import)))
+            {
+                if let Some(hash) = hashes.get(&import_path) {
+                    if import.content_hash == hash.as_str() &&
+                        !self.has_changed_imports(&import_path, import, hashes, paths, traversed)
+                    {
+                        return false
+                    }
+                }
+            }
+        }
+
+        !entry.imports.is_empty()
+    }
+
     /// Checks if all artifact files exist
     pub fn all_artifacts_exist<T: ArtifactOutput>(&self, artifacts_root: &Path) -> bool {
         self.files.iter().all(|(file, entry)| {
@@ -211,6 +249,7 @@ impl SolFilesCache {
     }
 }
 
+// async variants for read and write
 #[cfg(feature = "async")]
 impl SolFilesCache {
     pub async fn async_read(path: impl AsRef<Path>) -> Result<Self> {
@@ -259,7 +298,14 @@ impl SolFilesCacheBuilder {
         self
     }
 
-    pub fn insert_files(self, sources: Sources, dest: Option<PathBuf>) -> Result<SolFilesCache> {
+    /// Creates a new `SolFilesCache` instance
+    ///
+    /// If a `cache_file` path was provided it's used as base.
+    pub fn insert_files(
+        self,
+        sources: Sources,
+        cache_file: Option<PathBuf>,
+    ) -> Result<SolFilesCache> {
         let format = self.format.unwrap_or_else(|| ETHERS_FORMAT_VERSION.to_string());
         let solc_config =
             self.solc_config.map(Ok).unwrap_or_else(|| SolcConfig::builder().build())?;
@@ -298,14 +344,18 @@ impl SolFilesCacheBuilder {
             files.insert(file, entry);
         }
 
-        let cache = if let Some(dest) = dest.as_ref().filter(|dest| dest.exists()) {
+        let cache = if let Some(dest) = cache_file.as_ref().filter(|dest| dest.exists()) {
             // read the existing cache and extend it by the files that changed
             // (if we just wrote to the cache file, we'd overwrite the existing data)
             let reader =
                 std::io::BufReader::new(File::open(dest).map_err(|err| SolcError::io(err, dest))?);
-            let mut cache: SolFilesCache = serde_json::from_reader(reader)?;
-            cache.files.extend(files);
-            cache
+            if let Ok(mut cache) = serde_json::from_reader::<_, SolFilesCache>(reader) {
+                cache.files.extend(files);
+                cache
+            } else {
+                tracing::error!("Failed to read existing cache file {}", dest.display());
+                SolFilesCache { format, files }
+            }
         } else {
             SolFilesCache { format, files }
         };
diff --git a/ethers-solc/src/config.rs b/ethers-solc/src/config.rs
index e2a69da2..ee8dbd8a 100644
--- a/ethers-solc/src/config.rs
+++ b/ethers-solc/src/config.rs
@@ -14,7 +14,7 @@ use std::{
     fmt,
     fmt::Formatter,
     fs, io,
-    path::{Path, PathBuf},
+    path::{Component, Path, PathBuf},
 };
 
 /// Where to find all files or where to write them
@@ -101,6 +101,31 @@ impl ProjectPathsConfig {
         Ok(Source::read_all_files(self.input_files())?)
     }
 
+    /// 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<PathBuf> {
+        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
+            utils::canonicalize(cwd.join(import)).map_err(|err| {
+                SolcError::msg(format!("failed to resolve relative import \"{:?}\"", err))
+            })
+        } else {
+            // resolve library file
+            self.resolve_library_import(import.as_ref()).ok_or_else(|| {
+                SolcError::msg(format!(
+                    "failed to resolve library import \"{:?}\"",
+                    import.display()
+                ))
+            })
+        }
+    }
+
     /// 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
     pub fn resolve_library_import(&self, import: &Path) -> Option<PathBuf> {
diff --git a/ethers-solc/src/lib.rs b/ethers-solc/src/lib.rs
index 058d3479..d1fa2c21 100644
--- a/ethers-solc/src/lib.rs
+++ b/ethers-solc/src/lib.rs
@@ -213,6 +213,16 @@ impl<Artifacts: ArtifactOutput> Project<Artifacts> {
     /// `CompilerOutput::has_error` instead.
     /// NB: If the `svm` feature is enabled, this function will automatically detect
     /// solc versions across files.
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// use ethers_solc::Project;
+    /// # fn demo(project: Project) {
+    /// let project = Project::builder().build().unwrap();
+    /// let output = project.compile().unwrap();
+    /// # }
+    /// ```
     #[tracing::instrument(skip_all, name = "compile")]
     pub fn compile(&self) -> Result<ProjectCompileOutput<Artifacts>> {
         let sources = self.paths.read_input_files()?;
@@ -349,6 +359,22 @@ impl<Artifacts: ArtifactOutput> Project<Artifacts> {
     /// sources and their existing artifacts are read instead. This will also update the cache
     /// file and cleans up entries for files which may have been removed. Unchanged files that
     /// for which an artifact exist, are not compiled again.
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// use ethers_solc::{Project, Solc};
+    /// # fn demo(project: Project) {
+    /// let project = Project::builder().build().unwrap();
+    /// let sources = project.paths.read_sources().unwrap();
+    /// project
+    ///     .compile_with_version(
+    ///         &Solc::find_svm_installed_version("0.8.11").unwrap().unwrap(),
+    ///         sources,
+    ///     )
+    ///     .unwrap();
+    /// # }
+    /// ```
     pub fn compile_with_version(
         &self,
         solc: &Solc,
@@ -436,7 +462,7 @@ impl<Artifacts: ArtifactOutput> Project<Artifacts> {
             let changed_files = cache.get_changed_or_missing_artifacts_files::<Artifacts>(
                 sources,
                 Some(&self.solc_config),
-                &self.paths.artifacts,
+                &self.paths,
             );
             tracing::trace!("detected {} changed files", changed_files.len());
             cache.remove_changed_files(&changed_files);
@@ -830,7 +856,7 @@ where
 }
 
 impl<T: ArtifactOutput + 'static> ProjectCompileOutput<T> {
-    /// All artifacts together with their contract name
+    /// All artifacts together with their contract file name and name `<file name>:<name>`
     ///
     /// # Example
     ///
@@ -844,16 +870,22 @@ impl<T: ArtifactOutput + 'static> ProjectCompileOutput<T> {
     /// ```
     pub fn into_artifacts(mut self) -> Box<dyn Iterator<Item = (String, T::Artifact)>> {
         let artifacts = self.artifacts.into_iter().filter_map(|(path, art)| {
-            T::contract_name(&path)
-                .map(|name| (format!("{:?}:{}", path.file_name().unwrap(), name), art))
+            T::contract_name(&path).map(|name| {
+                (format!("{}:{}", path.file_name().unwrap().to_string_lossy(), name), art)
+            })
         });
 
-        let artifacts: Box<dyn Iterator<Item = (String, T::Artifact)>> =
-            if let Some(output) = self.compiler_output.take() {
-                Box::new(artifacts.chain(T::output_to_artifacts(output).into_values().flatten()))
-            } else {
-                Box::new(artifacts)
-            };
+        let artifacts: Box<dyn Iterator<Item = (String, T::Artifact)>> = if let Some(output) =
+            self.compiler_output.take()
+        {
+            Box::new(artifacts.chain(T::output_to_artifacts(output).into_values().flatten().map(
+                |(name, artifact)| {
+                    (format!("{}:{}", T::output_file_name(&name).display(), name), artifact)
+                },
+            )))
+        } else {
+            Box::new(artifacts)
+        };
         artifacts
     }
 }
diff --git a/ethers-solc/src/resolver.rs b/ethers-solc/src/resolver.rs
index 3786cb91..c6640f33 100644
--- a/ethers-solc/src/resolver.rs
+++ b/ethers-solc/src/resolver.rs
@@ -28,7 +28,7 @@
 
 use std::{
     collections::{HashMap, VecDeque},
-    path::{Component, Path, PathBuf},
+    path::{Path, PathBuf},
 };
 
 use rayon::prelude::*;
@@ -131,39 +131,19 @@ impl Graph {
         // locations
         while let Some((path, node)) = unresolved.pop_front() {
             let mut resolved_imports = Vec::with_capacity(node.data.imports.len());
-
             // parent directory of the current file
-            let node_dir = match path.parent() {
+            let cwd = match path.parent() {
                 Some(inner) => inner,
                 None => continue,
             };
 
             for import in node.data.imports.iter() {
-                let component = match import.components().next() {
-                    Some(inner) => inner,
-                    None => continue,
-                };
-                if component == Component::CurDir || component == Component::ParentDir {
-                    // if the import is relative we assume it's already part of the processed input
-                    // file set
-                    match utils::canonicalize(node_dir.join(import)) {
-                        Ok(target) => {
-                            // the file at least exists,
-                            add_node(&mut unresolved, &mut index, &mut resolved_imports, target)?;
-                        }
-                        Err(err) => {
-                            tracing::trace!("failed to resolve relative import \"{:?}\"", err);
-                        }
+                match paths.resolve_import(cwd, import) {
+                    Ok(import) => {
+                        add_node(&mut unresolved, &mut index, &mut resolved_imports, import)?;
                     }
-                } else {
-                    // resolve library file
-                    if let Some(lib) = paths.resolve_library_import(import.as_ref()) {
-                        add_node(&mut unresolved, &mut index, &mut resolved_imports, lib)?;
-                    } else {
-                        tracing::trace!(
-                            "failed to resolve library import \"{:?}\"",
-                            import.display()
-                        );
+                    Err(err) => {
+                        tracing::trace!("{}", err)
                     }
                 }
             }


Test patch:
diff --git a/ethers-solc/tests/project.rs b/ethers-solc/tests/project.rs
index f178dfff..0534c4e4 100644
--- a/ethers-solc/tests/project.rs
+++ b/ethers-solc/tests/project.rs
@@ -137,6 +137,90 @@ fn can_compile_dapp_detect_changes_in_libs() {
     assert!(!compiled.is_unchanged());
 }
 
+#[test]
+fn can_compile_dapp_detect_changes_in_sources() {
+    let project = TempProject::<MinimalCombinedArtifacts>::dapptools().unwrap();
+
+    let src = project
+        .add_source(
+            "DssSpell.t",
+            r#"
+    pragma solidity ^0.8.10;
+    import "./DssSpell.t.base.sol";
+
+   contract DssSpellTest is DssSpellTestBase { }
+   "#,
+        )
+        .unwrap();
+
+    let base = project
+        .add_source(
+            "DssSpell.t.base",
+            r#"
+    pragma solidity ^0.8.10;
+
+  contract DssSpellTestBase {
+       address deployed_spell;
+       function setUp() public {
+           deployed_spell = address(0xA867399B43aF7790aC800f2fF3Fa7387dc52Ec5E);
+       }
+  }
+   "#,
+        )
+        .unwrap();
+
+    let graph = Graph::resolve(project.paths()).unwrap();
+    assert_eq!(graph.files().len(), 2);
+    assert_eq!(graph.files().clone(), HashMap::from([(base, 0), (src, 1),]));
+    assert_eq!(graph.imported_nodes(1).to_vec(), vec![0]);
+
+    let compiled = project.compile().unwrap();
+    assert!(!compiled.has_compiler_errors());
+    assert!(compiled.find("DssSpellTest").is_some());
+    assert!(compiled.find("DssSpellTestBase").is_some());
+
+    // nothing to compile
+    let compiled = project.compile().unwrap();
+    assert!(compiled.is_unchanged());
+    assert!(compiled.find("DssSpellTest").is_some());
+    assert!(compiled.find("DssSpellTestBase").is_some());
+
+    let cache = SolFilesCache::read(&project.paths().cache).unwrap();
+    assert_eq!(cache.files.len(), 2);
+
+    let mut artifacts = compiled.into_artifacts().collect::<HashMap<_, _>>();
+
+    // overwrite import
+    let _ = project
+        .add_source(
+            "DssSpell.t.base",
+            r#"
+    pragma solidity ^0.8.10;
+
+  contract DssSpellTestBase {
+       address deployed_spell;
+       function setUp() public {
+           deployed_spell = address(0);
+       }
+  }
+   "#,
+        )
+        .unwrap();
+    let graph = Graph::resolve(project.paths()).unwrap();
+    assert_eq!(graph.files().len(), 2);
+
+    let compiled = project.compile().unwrap();
+    assert!(compiled.find("DssSpellTest").is_some());
+    assert!(compiled.find("DssSpellTestBase").is_some());
+    // ensure change is detected
+    assert!(!compiled.is_unchanged());
+    // and all recompiled artifacts are different
+    for (p, artifact) in compiled.into_artifacts() {
+        let other = artifacts.remove(&p).unwrap();
+        assert_ne!(artifact, other);
+    }
+}
+
 #[test]
 fn can_compile_dapp_sample_with_cache() {
     let tmp_dir = tempfile::tempdir().unwrap();
@@ -184,9 +268,9 @@ fn can_compile_dapp_sample_with_cache() {
     assert_eq!(
         compiled.into_artifacts().map(|(name, _)| name).collect::<Vec<_>>(),
         vec![
-            r#""Dapp.json":Dapp"#,
-            r#""DappTest.json":DappTest"#,
-            r#""DSTest.json":DSTest"#,
+            r#"Dapp.json:Dapp"#,
+            r#"DappTest.json:DappTest"#,
+            r#"DSTest.json:DSTest"#,
             "NewContract"
         ]
     );
@@ -197,9 +281,9 @@ fn can_compile_dapp_sample_with_cache() {
     assert_eq!(
         compiled.into_artifacts().map(|(name, _)| name).collect::<Vec<_>>(),
         vec![
-            r#""DappTest.json":DappTest"#,
-            r#""NewContract.json":NewContract"#,
-            r#""DSTest.json":DSTest"#,
+            r#"DappTest.json:DappTest"#,
+            r#"NewContract.json:NewContract"#,
+            r#"DSTest.json:DSTest"#,
             "Dapp"
         ]
     );