Repository: gakonst/ethers-rs
Pull request #813 (2022-01-19 11:26:19)

fix(solc): flatten duplicates
<!--
Thank you for your Pull Request. Please provide a description above and review
the requirements below.

Bug fixes and new features should include tests.

Contributors guide: https://github.com/gakonst/ethers-rs/blob/master/CONTRIBUTING.md

The contributors guide includes instructions for running rustfmt and building the
documentation.
-->

## Motivation

<!--
Explain the context and why you're making that change. What is the problem
you're trying to solve? In some cases there is not a problem and this can be
thought of as being the motivation for your change.
-->

## Solution

<!--
Summarize the solution and provide any necessary context needed to understand
the code change.
-->

## PR Checklist

- [ ] Added Tests
- [ ] Added Documentation
- [ ] Updated the changelog



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c3be326d..10f1d6fb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -34,6 +34,8 @@
 
 ### Unreleased
 
+- Fix duplicate files during flattening
+  [#813](https://github.com/gakonst/ethers-rs/pull/813)
 - Add ability to flatten file imports
   [#774](https://github.com/gakonst/ethers-rs/pull/774)
 - Add dependency graph and resolve all imported libraryfiles
diff --git a/ethers-solc/src/config.rs b/ethers-solc/src/config.rs
index 8ed92257..6308fdf2 100644
--- a/ethers-solc/src/config.rs
+++ b/ethers-solc/src/config.rs
@@ -176,7 +176,7 @@ impl ProjectPathsConfig {
     pub fn flatten(&self, target: &Path) -> Result<String> {
         tracing::trace!("flattening file");
         let graph = Graph::resolve(self)?;
-        self.flatten_node(target, &graph, false, false)
+        self.flatten_node(target, &graph, &mut vec![], false, false)
     }
 
     /// Flattens a single node from the dependency graph
@@ -184,6 +184,7 @@ impl ProjectPathsConfig {
         &self,
         target: &Path,
         graph: &Graph,
+        imported: &mut Vec<usize>,
         strip_version_pragma: bool,
         strip_license: bool,
     ) -> Result<String> {
@@ -193,6 +194,11 @@ impl ProjectPathsConfig {
         let target_index = graph.files().get(target).ok_or_else(|| {
             SolcError::msg(format!("cannot resolve file at \"{:?}\"", target.display()))
         })?;
+
+        if imported.iter().any(|&idx| idx == *target_index) {
+            return Ok(String::new())
+        }
+
         let target_node = graph.node(*target_index);
 
         let mut imports = target_node.imports().clone();
@@ -219,7 +225,7 @@ impl ProjectPathsConfig {
 
         for import in imports.iter() {
             let import_path = self.resolve_import(target_dir, import.data())?;
-            let import_content = self.flatten_node(&import_path, graph, true, true)?;
+            let import_content = self.flatten_node(&import_path, graph, imported, true, true)?;
             let import_content = import_content.trim().as_bytes().to_owned();
             let import_content_len = import_content.len() as isize;
             let (start, end) = import.loc_by_offset(offset);
@@ -230,6 +236,7 @@ impl ProjectPathsConfig {
         let result = String::from_utf8(content).map_err(|err| {
             SolcError::msg(format!("failed to convert extended bytes to string: {}", err))
         })?;
+        imported.push(*target_index);
 
         Ok(result)
     }


Test patch:
diff --git a/ethers-solc/test-data/flatten-sample/contracts/Bar.sol b/ethers-solc/test-data/flatten-sample/contracts/Bar.sol
new file mode 100644
index 00000000..5cb19aba
--- /dev/null
+++ b/ethers-solc/test-data/flatten-sample/contracts/Bar.sol
@@ -0,0 +1,4 @@
+//SPDX-License-Identifier: Unlicense
+pragma solidity >=0.6.0;
+
+contract Bar {}
diff --git a/ethers-solc/test-data/flatten-sample/contracts/Foo.sol b/ethers-solc/test-data/flatten-sample/contracts/Foo.sol
new file mode 100644
index 00000000..f9cf562d
--- /dev/null
+++ b/ethers-solc/test-data/flatten-sample/contracts/Foo.sol
@@ -0,0 +1,6 @@
+//SPDX-License-Identifier: Unlicense
+pragma solidity >=0.6.0;
+
+import { Bar } from './Bar.sol';
+
+contract Foo {}
diff --git a/ethers-solc/test-data/flatten-sample/contracts/FooBar.sol b/ethers-solc/test-data/flatten-sample/contracts/FooBar.sol
new file mode 100644
index 00000000..eda3b10b
--- /dev/null
+++ b/ethers-solc/test-data/flatten-sample/contracts/FooBar.sol
@@ -0,0 +1,7 @@
+//SPDX-License-Identifier: Unlicense
+pragma solidity >=0.6.0;
+
+import { Bar } from './Bar.sol';
+import { Foo } from './Foo.sol';
+
+contract FooBar {}
diff --git a/ethers-solc/tests/project.rs b/ethers-solc/tests/project.rs
index 2a9a91c8..0025c77e 100644
--- a/ethers-solc/tests/project.rs
+++ b/ethers-solc/tests/project.rs
@@ -360,3 +360,20 @@ fn can_flatten_file_in_dapp_sample() {
     assert!(result.find("contract Dapp").is_some());
     assert!(result.find("contract DappTest").is_some());
 }
+
+#[test]
+fn can_flatten_file_with_duplicates() {
+    let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-data/flatten-sample");
+    let paths = ProjectPathsConfig::builder().sources(root.join("contracts"));
+    let project = TempProject::<MinimalCombinedArtifacts>::new(paths).unwrap();
+
+    let target = root.join("contracts/FooBar.sol");
+
+    let result = project.flatten(&target);
+    assert!(result.is_ok());
+
+    let result = result.unwrap();
+    assert!(result.matches("contract Foo {").collect::<Vec<_>>().len() == 1);
+    assert!(result.matches("contract Bar {").collect::<Vec<_>>().len() == 1);
+    assert!(result.matches("contract FooBar {").collect::<Vec<_>>().len() == 1);
+}