Repository: gakonst/ethers-rs
Pull request #1284 (2022-05-19 15:28:05)

feat(solc): include id in artifact
<!--
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
include the `id` in the artifact in order to recover the `SourceFile` of the artifact
<!--
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/ethers-solc/src/artifact_output/configurable.rs b/ethers-solc/src/artifact_output/configurable.rs
index 588d0533..ea373cf7 100644
--- a/ethers-solc/src/artifact_output/configurable.rs
+++ b/ethers-solc/src/artifact_output/configurable.rs
@@ -55,6 +55,9 @@ pub struct ConfigurableContractArtifact {
     pub ewasm: Option<Ewasm>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub ast: Option<Ast>,
+    /// The identifier of the source file
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub id: Option<u32>,
 }
 
 impl ConfigurableContractArtifact {
@@ -77,6 +80,11 @@ impl ConfigurableContractArtifact {
         }
         links
     }
+
+    /// Returns the source file of this artifact's contract
+    pub fn source_file(&self) -> Option<SourceFile> {
+        self.id.map(|id| SourceFile { id, ast: self.ast.clone() })
+    }
 }
 
 impl From<ConfigurableContractArtifact> for CompactContractBytecode {
@@ -316,6 +324,7 @@ impl ArtifactOutput for ConfigurableArtifacts {
             ir: artifact_ir,
             ir_optimized: artifact_ir_optimized,
             ewasm: artifact_ewasm,
+            id: source_file.as_ref().map(|s| s.id),
             ast: source_file.and_then(|s| s.ast.clone()),
             generated_sources: generated_sources.unwrap_or_default(),
         }
diff --git a/ethers-solc/src/artifacts/mod.rs b/ethers-solc/src/artifacts/mod.rs
index 497e008a..1547313f 100644
--- a/ethers-solc/src/artifacts/mod.rs
+++ b/ethers-solc/src/artifacts/mod.rs
@@ -1299,15 +1299,17 @@ pub struct UserDoc {
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub kind: Option<String>,
     #[serde(default, skip_serializing_if = "::std::collections::BTreeMap::is_empty")]
-    pub methods: BTreeMap<String, MethodNotice>,
+    pub methods: BTreeMap<String, UserDocNotice>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub notice: Option<String>,
 }
 
-#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
-pub struct MethodNotice {
-    #[serde(default, skip_serializing_if = "Option::is_none")]
-    pub notice: Option<String>,
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
+#[serde(untagged)]
+pub enum UserDocNotice {
+    // NOTE: this a variant used for constructors on older solc versions
+    Constructor(String),
+    Method { notice: String },
 }
 
 #[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
diff --git a/ethers-solc/src/compile/output/mod.rs b/ethers-solc/src/compile/output/mod.rs
index 7730df88..e6e0a61c 100644
--- a/ethers-solc/src/compile/output/mod.rs
+++ b/ethers-solc/src/compile/output/mod.rs
@@ -73,6 +73,11 @@ impl<T: ArtifactOutput> ProjectCompileOutput<T> {
     }
 
     /// All artifacts together with their ID and the sources of the project.
+    ///
+    /// Note: this only returns the `SourceFiles` for freshly compiled contracts because, if not
+    /// included in the `Artifact` itself (see
+    /// [`crate::ConfigurableContractArtifact::source_file()`]), is only available via the solc
+    /// `CompilerOutput`
     pub fn into_artifacts_with_sources(
         self,
     ) -> (BTreeMap<ArtifactId, T::Artifact>, VersionedSourceFiles) {


Test patch:
diff --git a/ethers-solc/tests/project.rs b/ethers-solc/tests/project.rs
index f2cee711..55529d51 100644
--- a/ethers-solc/tests/project.rs
+++ b/ethers-solc/tests/project.rs
@@ -9,7 +9,10 @@ use std::{
 
 use ethers_core::types::Address;
 use ethers_solc::{
-    artifacts::{BytecodeHash, Libraries, ModelCheckerEngine::CHC, ModelCheckerSettings},
+    artifacts::{
+        BytecodeHash, Libraries, ModelCheckerEngine::CHC, ModelCheckerSettings, UserDoc,
+        UserDocNotice,
+    },
     cache::{SolFilesCache, SOLIDITY_FILES_CACHE_FILENAME},
     project_util::*,
     remappings::Remapping,
@@ -1411,3 +1414,78 @@ fn can_purge_obsolete_artifacts() {
     assert!(!compiled.is_unchanged());
     assert_eq!(compiled.into_artifacts().count(), 1);
 }
+
+#[test]
+fn can_parse_notice() {
+    let mut project = TempProject::<ConfigurableArtifacts>::dapptools().unwrap();
+    project.project_mut().artifacts.additional_values.userdoc = true;
+    project.project_mut().solc_config.settings = project.project_mut().artifacts.settings();
+
+    let contract = r#"
+    pragma solidity $VERSION;
+
+   contract Contract {
+      string greeting;
+
+        /**
+         * @notice hello
+         */    
+         constructor(string memory _greeting) public {
+            greeting = _greeting;
+        }
+        
+        /**
+         * @notice hello
+         */
+        function xyz() public {
+        }
+        
+        /// @notice hello
+        function abc() public {
+        }
+   }
+   "#;
+    project.add_source("Contract", contract.replace("$VERSION", "=0.5.17")).unwrap();
+
+    let mut compiled = project.compile().unwrap();
+    assert!(!compiled.has_compiler_errors());
+    assert!(!compiled.is_unchanged());
+    assert!(compiled.find("Contract").is_some());
+    let userdoc = compiled.remove("Contract").unwrap().userdoc;
+
+    assert_eq!(
+        userdoc,
+        Some(UserDoc {
+            version: None,
+            kind: None,
+            methods: BTreeMap::from([
+                ("abc()".to_string(), UserDocNotice::Method { notice: "hello".to_string() }),
+                ("xyz()".to_string(), UserDocNotice::Method { notice: "hello".to_string() }),
+                ("constructor".to_string(), UserDocNotice::Constructor("hello".to_string())),
+            ]),
+            notice: None
+        })
+    );
+
+    project.add_source("Contract", contract.replace("$VERSION", "^0.8.10")).unwrap();
+
+    let mut compiled = project.compile().unwrap();
+    assert!(!compiled.has_compiler_errors());
+    assert!(!compiled.is_unchanged());
+    assert!(compiled.find("Contract").is_some());
+    let userdoc = compiled.remove("Contract").unwrap().userdoc;
+
+    assert_eq!(
+        userdoc,
+        Some(UserDoc {
+            version: Some(1),
+            kind: Some("user".to_string()),
+            methods: BTreeMap::from([
+                ("abc()".to_string(), UserDocNotice::Method { notice: "hello".to_string() }),
+                ("xyz()".to_string(), UserDocNotice::Method { notice: "hello".to_string() }),
+                ("constructor".to_string(), UserDocNotice::Method { notice: "hello".to_string() }),
+            ]),
+            notice: None
+        })
+    );
+}