Repository: gakonst/ethers-rs
Pull request #1838 (2022-11-06 21:18:35)

feat: warnings as errors
I would like to submit a new feature that would allow to create a setting for compile warnings to be included as errors.

## Motivation
I wanted to work on this feature request here:
https://github.com/foundry-rs/foundry/issues/3476

Following the guidance from that thread I am starting with making the additions here and I would be interested to also make the feature in Foundry after this is resolved.

With this setting turned on, the compiler when checking for errors, will also look for warnings

## Solution

I have made the code changes that added a property to Project/ProjectBuilder and it goes all the way down to the compiler output, quite similar to how "ignored_error_codes" is done. I also added one test. I feel that I should mention I am a beginner to rust and I would greatly appreciate any feedback or guidance :smile: 

## PR Checklist

-   [x] Added Tests
-   [x] Added Documentation
-   [x] Updated the changelog


jatokz: I added a second commit to go with Severity as an argument instead.
jatokz: I took a look at the logs during the workflows lint step and noticed it exited when it got to my test.

I made another commit to see if formatting that code will fix it, but I am not really sure what went wrong.
jatokz: It was my pleasure :100: 
jatokz: One thing to note, the first test checks all smart contracts in that test-data dir I created.

The second test I was much more specific and only tested 1 smart contract and the error code that goes with it.

Patch:
diff --git a/ethers-solc/src/artifacts/mod.rs b/ethers-solc/src/artifacts/mod.rs
index 4e5b7fe4..e3dd5961 100644
--- a/ethers-solc/src/artifacts/mod.rs
+++ b/ethers-solc/src/artifacts/mod.rs
@@ -1733,8 +1733,9 @@ impl fmt::Display for Error {
     }
 }
 
-#[derive(Clone, Debug, Eq, PartialEq, Hash)]
+#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, Default)]
 pub enum Severity {
+    #[default]
     Error,
     Warning,
     Info,
diff --git a/ethers-solc/src/compile/output/mod.rs b/ethers-solc/src/compile/output/mod.rs
index 294c4e48..1a73c794 100644
--- a/ethers-solc/src/compile/output/mod.rs
+++ b/ethers-solc/src/compile/output/mod.rs
@@ -3,7 +3,7 @@
 use crate::{
     artifacts::{
         contract::{CompactContractBytecode, CompactContractRef, Contract},
-        Error,
+        Error, Severity,
     },
     buildinfo::RawBuildInfo,
     info::ContractInfoRef,
@@ -31,6 +31,8 @@ pub struct ProjectCompileOutput<T: ArtifactOutput = ConfigurableArtifacts> {
     pub(crate) cached_artifacts: Artifacts<T::Artifact>,
     /// errors that should be omitted
     pub(crate) ignored_error_codes: Vec<u64>,
+    /// set minimum level of severity that is treated as an error
+    pub(crate) compiler_severity_filter: Severity,
 }
 
 impl<T: ArtifactOutput> ProjectCompileOutput<T> {
@@ -197,7 +199,7 @@ impl<T: ArtifactOutput> ProjectCompileOutput<T> {
 
     /// Whether there were errors
     pub fn has_compiler_errors(&self) -> bool {
-        self.compiler_output.has_error()
+        self.compiler_output.has_error(&self.ignored_error_codes, &self.compiler_severity_filter)
     }
 
     /// Whether there were warnings
@@ -398,7 +400,7 @@ impl<T: ArtifactOutput> fmt::Display for ProjectCompileOutput<T> {
         if self.compiler_output.is_unchanged() {
             f.write_str("Nothing to compile")
         } else {
-            self.compiler_output.diagnostics(&self.ignored_error_codes).fmt(f)
+            self.compiler_output.diagnostics(&self.ignored_error_codes, self.compiler_severity_filter.clone()).fmt(f)
         }
     }
 }
@@ -426,8 +428,16 @@ impl AggregatedCompilerOutput {
     }
 
     /// Whether the output contains a compiler error
-    pub fn has_error(&self) -> bool {
-        self.errors.iter().any(|err| err.severity.is_error())
+    pub fn has_error(&self, ignored_error_codes: &[u64], compiler_severity_filter: &Severity) -> bool {
+        self.errors.iter().any(|err| {
+            if compiler_severity_filter.ge(&err.severity) {
+                if compiler_severity_filter.is_warning()  {
+                    return self.has_warning(ignored_error_codes)
+                }
+                return true
+            }
+            return false
+        })
     }
 
     /// Whether the output contains a compiler warning
@@ -441,8 +451,8 @@ impl AggregatedCompilerOutput {
         })
     }
 
-    pub fn diagnostics<'a>(&'a self, ignored_error_codes: &'a [u64]) -> OutputDiagnostics {
-        OutputDiagnostics { compiler_output: self, ignored_error_codes }
+    pub fn diagnostics<'a>(&'a self, ignored_error_codes: &'a [u64], compiler_severity_filter: Severity) -> OutputDiagnostics {
+        OutputDiagnostics { compiler_output: self, ignored_error_codes, compiler_severity_filter }
     }
 
     pub fn is_empty(&self) -> bool {
@@ -702,12 +712,14 @@ pub struct OutputDiagnostics<'a> {
     compiler_output: &'a AggregatedCompilerOutput,
     /// the error codes to ignore
     ignored_error_codes: &'a [u64],
+    /// set minimum level of severity that is treated as an error
+    compiler_severity_filter: Severity,
 }
 
 impl<'a> OutputDiagnostics<'a> {
     /// Returns true if there is at least one error of high severity
     pub fn has_error(&self) -> bool {
-        self.compiler_output.has_error()
+        self.compiler_output.has_error(&self.ignored_error_codes, &self.compiler_severity_filter)
     }
 
     /// Returns true if there is at least one warning
diff --git a/ethers-solc/src/compile/project.rs b/ethers-solc/src/compile/project.rs
index 5bcf376b..3994da3b 100644
--- a/ethers-solc/src/compile/project.rs
+++ b/ethers-solc/src/compile/project.rs
@@ -311,7 +311,7 @@ impl<'a, T: ArtifactOutput> CompiledState<'a, T> {
                 ctx,
                 &project.paths,
             )
-        } else if output.has_error() {
+        } else if output.has_error(&project.ignored_error_codes, &project.compiler_severity_filter) {
             trace!("skip writing cache file due to solc errors: {:?}", output.errors);
             project.artifacts_handler().output_to_artifacts(
                 &output.contracts,
@@ -358,14 +358,17 @@ impl<'a, T: ArtifactOutput> ArtifactsState<'a, T> {
     fn write_cache(self) -> Result<ProjectCompileOutput<T>> {
         trace!("write cache");
         let ArtifactsState { output, cache, compiled_artifacts } = self;
-        let ignored_error_codes = cache.project().ignored_error_codes.clone();
-        let skip_write_to_disk = cache.project().no_artifacts || output.has_error();
+        let project = cache.project();
+        let ignored_error_codes = project.ignored_error_codes.clone();
+        let compiler_severity_filter = project.compiler_severity_filter.clone();
+        let skip_write_to_disk = project.no_artifacts || output.has_error(&ignored_error_codes, &compiler_severity_filter);
         let cached_artifacts = cache.consume(&compiled_artifacts, !skip_write_to_disk)?;
         Ok(ProjectCompileOutput {
             compiler_output: output,
             compiled_artifacts,
             cached_artifacts,
             ignored_error_codes,
+            compiler_severity_filter
         })
     }
 }
diff --git a/ethers-solc/src/lib.rs b/ethers-solc/src/lib.rs
index 94805ca6..9d2947f7 100644
--- a/ethers-solc/src/lib.rs
+++ b/ethers-solc/src/lib.rs
@@ -42,7 +42,7 @@ use crate::{
     error::{SolcError, SolcIoError},
     sources::{VersionedSourceFile, VersionedSourceFiles},
 };
-use artifacts::contract::Contract;
+use artifacts::{contract::Contract, Severity};
 use compile::output::contracts::VersionedContracts;
 use error::Result;
 use semver::Version;
@@ -73,6 +73,8 @@ pub struct Project<T: ArtifactOutput = ConfigurableArtifacts> {
     pub artifacts: T,
     /// Errors/Warnings which match these error codes are not going to be logged
     pub ignored_error_codes: Vec<u64>,
+    /// The minimum severity level that is treated as a compiler error
+    pub compiler_severity_filter: Severity,
     /// The paths which will be allowed for library inclusion
     pub allowed_paths: AllowedLibPaths,
     /// The paths which will be used with solc's `--include-path` attribute
@@ -569,6 +571,8 @@ pub struct ProjectBuilder<T: ArtifactOutput = ConfigurableArtifacts> {
     artifacts: T,
     /// Which error codes to ignore
     pub ignored_error_codes: Vec<u64>,
+    /// The minimum severity level that is treated as a compiler error
+    compiler_severity_filter: Severity,
     /// All allowed paths for solc's `--allowed-paths`
     allowed_paths: AllowedLibPaths,
     /// Paths to use for solc's `--include-path`
@@ -591,6 +595,7 @@ impl<T: ArtifactOutput> ProjectBuilder<T> {
             slash_paths: true,
             artifacts,
             ignored_error_codes: Vec::new(),
+            compiler_severity_filter: Severity::Error,
             allowed_paths: Default::default(),
             include_paths: Default::default(),
             solc_jobs: None,
@@ -629,6 +634,12 @@ impl<T: ArtifactOutput> ProjectBuilder<T> {
         self
     }
 
+    #[must_use]
+    pub fn set_compiler_severity_filter(mut self, compiler_severity_filter: Severity) -> Self {
+        self.compiler_severity_filter = compiler_severity_filter;
+        self
+    }
+
     /// Disables cached builds
     #[must_use]
     pub fn ephemeral(self) -> Self {
@@ -727,6 +738,7 @@ impl<T: ArtifactOutput> ProjectBuilder<T> {
             no_artifacts,
             auto_detect,
             ignored_error_codes,
+            compiler_severity_filter,
             allowed_paths,
             include_paths,
             solc_jobs,
@@ -746,6 +758,7 @@ impl<T: ArtifactOutput> ProjectBuilder<T> {
             slash_paths,
             artifacts,
             ignored_error_codes,
+            compiler_severity_filter,
             allowed_paths,
             include_paths,
             solc_jobs,
@@ -803,6 +816,7 @@ impl<T: ArtifactOutput> ProjectBuilder<T> {
             auto_detect,
             artifacts,
             ignored_error_codes,
+            compiler_severity_filter,
             mut allowed_paths,
             include_paths,
             solc_jobs,
@@ -834,6 +848,7 @@ impl<T: ArtifactOutput> ProjectBuilder<T> {
             auto_detect,
             artifacts,
             ignored_error_codes,
+            compiler_severity_filter,
             allowed_paths,
             include_paths,
             solc_jobs: solc_jobs.unwrap_or_else(num_cpus::get),


Test patch:
diff --git a/ethers-solc/test-data/test-contract-warnings/LicenseWarning.sol b/ethers-solc/test-data/test-contract-warnings/LicenseWarning.sol
new file mode 100644
index 00000000..aeaa7a51
--- /dev/null
+++ b/ethers-solc/test-data/test-contract-warnings/LicenseWarning.sol
@@ -0,0 +1,3 @@
+pragma solidity 0.8.6;
+
+contract LicenseWarning {}
diff --git a/ethers-solc/tests/project.rs b/ethers-solc/tests/project.rs
index 1814610f..6f99e982 100644
--- a/ethers-solc/tests/project.rs
+++ b/ethers-solc/tests/project.rs
@@ -1618,6 +1618,81 @@ fn can_compile_model_checker_sample() {
     assert!(compiled.has_compiler_warnings());
 }
 
+#[test]
+fn test_compiler_severity_filter() {
+    fn gen_test_data_warning_path() -> ProjectPathsConfig {
+        let root =
+            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-data/test-contract-warnings");
+        let paths = ProjectPathsConfig::builder().sources(root).build().unwrap();
+        paths
+    }
+
+    let project = Project::builder()
+        .no_artifacts()
+        .paths(gen_test_data_warning_path())
+        .ephemeral()
+        .build()
+        .unwrap();
+    let compiled = project.compile().unwrap();
+    assert!(compiled.has_compiler_warnings());
+    assert!(!compiled.has_compiler_errors());
+
+    let project = Project::builder()
+        .no_artifacts()
+        .paths(gen_test_data_warning_path())
+        .ephemeral()
+        .set_compiler_severity_filter(ethers_solc::artifacts::Severity::Warning)
+        .build()
+        .unwrap();
+    let compiled = project.compile().unwrap();
+    assert!(compiled.has_compiler_warnings());
+    assert!(compiled.has_compiler_errors());
+}
+
+#[test]
+fn test_compiler_severity_filter_and_ignored_error_codes() {
+    fn gen_test_data_licensing_warning() -> ProjectPathsConfig {
+        let root =
+            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-data/test-contract-warnings/LicenseWarning.sol");
+        let paths = ProjectPathsConfig::builder().sources(root).build().unwrap();
+        paths
+    }
+
+    let missing_license_error_code = 1878;
+
+    let project = Project::builder()
+    .no_artifacts()
+    .paths(gen_test_data_licensing_warning())
+    .ephemeral()
+    .build()
+    .unwrap();
+    let compiled = project.compile().unwrap();
+    assert!(compiled.has_compiler_warnings());
+
+    let project = Project::builder()
+        .no_artifacts()
+        .paths(gen_test_data_licensing_warning())
+        .ephemeral()
+        .ignore_error_code(missing_license_error_code)
+        .build()
+        .unwrap();
+    let compiled = project.compile().unwrap();
+    assert!(!compiled.has_compiler_warnings());
+    assert!(!compiled.has_compiler_errors());
+
+    let project = Project::builder()
+    .no_artifacts()
+    .paths(gen_test_data_licensing_warning())
+    .ephemeral()
+    .ignore_error_code(missing_license_error_code)
+    .set_compiler_severity_filter(ethers_solc::artifacts::Severity::Warning)
+    .build()
+    .unwrap();
+    let compiled = project.compile().unwrap();
+    assert!(!compiled.has_compiler_warnings());
+    assert!(!compiled.has_compiler_errors());
+}
+
 fn remove_solc_if_exists(version: &Version) {
     if Solc::find_svm_installed_version(version.to_string()).unwrap().is_some() {
         svm::remove_version(version).expect("failed to remove version")