Skip to content

Commit c7d9703

Browse files
authored
feat(lint): fail on configured diagnostic level (#11445)
1 parent 9698e42 commit c7d9703

File tree

19 files changed

+728
-444
lines changed

19 files changed

+728
-444
lines changed

Cargo.lock

Lines changed: 229 additions & 226 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -418,5 +418,8 @@ rexpect = { git = "https://github.com/rust-cli/rexpect", rev = "2ed0b1898d7edaf6
418418
# foundry-compilers = { git = "https://github.com/foundry-rs/compilers.git", branch = "dani/bump-solar" }
419419
# foundry-fork-db = { git = "https://github.com/foundry-rs/foundry-fork-db", rev = "eee6563" }
420420

421-
## solar
421+
# solar
422422
# solar = { package = "solar-compiler", git = "https://github.com/paradigmxyz/solar.git", branch = "main" }
423+
# solar-interface = { package = "solar-interface", git = "https://github.com/paradigmxyz/solar.git", branch = "main" }
424+
# solar-ast = { package = "solar-ast", git = "https://github.com/paradigmxyz/solar.git", branch = "main" }
425+
# solar-sema = { package = "solar-sema", git = "https://github.com/paradigmxyz/solar.git", branch = "main" }

crates/cli/src/opts/build/core.rs

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ use foundry_compilers::{
99
utils::canonicalized,
1010
};
1111
use foundry_config::{
12-
Config, Remappings, figment,
12+
Config, DenyLevel, Remappings,
1313
figment::{
14-
Figment, Metadata, Profile, Provider,
14+
self, Figment, Metadata, Profile, Provider,
1515
error::Kind::InvalidType,
1616
value::{Dict, Map, Value},
1717
},
@@ -48,9 +48,26 @@ pub struct BuildOpts {
4848
#[serde(skip_serializing_if = "Vec::is_empty")]
4949
pub ignored_error_codes: Vec<u64>,
5050

51-
/// Warnings will trigger a compiler error
52-
#[arg(long, help_heading = "Compiler options")]
51+
/// A compiler error will be triggered at the specified diagnostic level.
52+
///
53+
/// Replaces the deprecated `--deny-warnings` flag.
54+
///
55+
/// Possible values:
56+
/// - `never`: Do not treat any diagnostics as errors.
57+
/// - `warnings`: Treat warnings as errors.
58+
/// - `notes`: Treat both, warnings and notes, as errors.
59+
#[arg(
60+
long,
61+
short = 'D',
62+
help_heading = "Compiler options",
63+
value_name = "LEVEL",
64+
conflicts_with = "deny_warnings"
65+
)]
5366
#[serde(skip)]
67+
pub deny: Option<DenyLevel>,
68+
69+
/// Deprecated: use `--deny=warnings` instead.
70+
#[arg(long = "deny-warnings", hide = true)]
5471
pub deny_warnings: bool,
5572

5673
/// Do not auto-detect the `solc` version.
@@ -220,7 +237,10 @@ impl Provider for BuildOpts {
220237
}
221238

222239
if self.deny_warnings {
223-
dict.insert("deny_warnings".to_string(), true.into());
240+
dict.insert("deny".to_string(), figment::value::Value::serialize(DenyLevel::Warnings)?);
241+
_ = sh_warn!("`--deny-warnings` is being deprecated in favor of `--deny warnings`.");
242+
} else if let Some(deny) = self.deny {
243+
dict.insert("deny".to_string(), figment::value::Value::serialize(deny)?);
224244
}
225245

226246
if self.via_ir {

crates/config/README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,11 @@ etherscan_api_key = "YOURETHERSCANAPIKEY"
107107
# additional warnings can be added using their numeric error code: ["license", 1337]
108108
ignored_error_codes = ["license", "code-size"]
109109
ignored_warnings_from = ["path_to_ignore"]
110-
deny_warnings = false
110+
# Deny compiler warnings and/or notes with:
111+
# - "never": default behavior, no denial
112+
# - "warnings": warnings treated as errors
113+
# - "notes": notes and warnings treated as notes
114+
deny = "never"
111115
match_test = "Foo"
112116
no_match_test = "Bar"
113117
match_contract = "Foo"

crates/config/src/inline/natspec.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -494,8 +494,8 @@ contract FuzzInlineConf is DSTest {
494494

495495
fn natspec() -> NatSpec {
496496
let conf = r"
497-
forge-config: default.fuzz.runs = 600
498-
forge-config: ci.fuzz.runs = 500
497+
forge-config: default.fuzz.runs = 600
498+
forge-config: ci.fuzz.runs = 500
499499
========= SOME NOISY TEXT =============
500500
䩹𧀫Jx닧Ʀ̳盅K擷􅟽Ɂw첊}ꏻk86ᖪk-檻ܴ렝[Dz𐤬oᘓƤ
501501
꣖ۻ%Ƅ㪕ς:(饁΍av/烲ڻ̛߉橞㗡𥺃̹M봓䀖ؿ̄󵼁)𯖛d􂽰񮍃

crates/config/src/lib.rs

Lines changed: 130 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ use foundry_compilers::{
4242
use regex::Regex;
4343
use revm::primitives::hardfork::SpecId;
4444
use semver::Version;
45-
use serde::{Deserialize, Serialize, Serializer};
45+
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
4646
use std::{
4747
borrow::Cow,
4848
collections::BTreeMap,
@@ -306,7 +306,10 @@ pub struct Config {
306306
/// list of file paths to ignore
307307
#[serde(rename = "ignored_warnings_from")]
308308
pub ignored_file_paths: Vec<PathBuf>,
309-
/// When true, compiler warnings are treated as errors
309+
/// Diagnostic level (minimum) at which the process should finish with a non-zero exit.
310+
pub deny: DenyLevel,
311+
/// DEPRECATED: use `deny` instead.
312+
#[serde(default, skip_serializing)]
310313
pub deny_warnings: bool,
311314
/// Only run test functions matching the specified regex pattern.
312315
#[serde(rename = "match_test")]
@@ -562,13 +565,109 @@ pub struct Config {
562565
pub _non_exhaustive: (),
563566
}
564567

568+
/// Diagnostic level (minimum) at which the process should finish with a non-zero exit.
569+
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum, Default, Serialize)]
570+
#[serde(rename_all = "lowercase")]
571+
pub enum DenyLevel {
572+
/// Always exit with zero code.
573+
#[default]
574+
Never,
575+
/// Exit with a non-zero code if any warnings are found.
576+
Warnings,
577+
/// Exit with a non-zero code if any notes or warnings are found.
578+
Notes,
579+
}
580+
581+
// Custom deserialization to make `DenyLevel` parsing case-insensitive and backwards compatible with
582+
// booleans.
583+
impl<'de> Deserialize<'de> for DenyLevel {
584+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
585+
where
586+
D: Deserializer<'de>,
587+
{
588+
struct DenyLevelVisitor;
589+
590+
impl<'de> de::Visitor<'de> for DenyLevelVisitor {
591+
type Value = DenyLevel;
592+
593+
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
594+
formatter.write_str("one of the following strings: `never`, `warnings`, `notes`")
595+
}
596+
597+
fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
598+
where
599+
E: de::Error,
600+
{
601+
Ok(DenyLevel::from(value))
602+
}
603+
604+
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
605+
where
606+
E: de::Error,
607+
{
608+
DenyLevel::from_str(value).map_err(de::Error::custom)
609+
}
610+
}
611+
612+
deserializer.deserialize_any(DenyLevelVisitor)
613+
}
614+
}
615+
616+
impl FromStr for DenyLevel {
617+
type Err = String;
618+
619+
fn from_str(s: &str) -> Result<Self, Self::Err> {
620+
match s.to_lowercase().as_str() {
621+
"warnings" | "warning" | "w" => Ok(Self::Warnings),
622+
"notes" | "note" | "n" => Ok(Self::Notes),
623+
"never" | "false" | "f" => Ok(Self::Never),
624+
_ => Err(format!(
625+
"unknown variant: found `{s}`, expected one of `never`, `warnings`, `notes`"
626+
)),
627+
}
628+
}
629+
}
630+
631+
impl From<bool> for DenyLevel {
632+
fn from(deny: bool) -> Self {
633+
if deny { Self::Warnings } else { Self::Never }
634+
}
635+
}
636+
637+
impl DenyLevel {
638+
/// Returns `true` if the deny level includes warnings.
639+
pub fn warnings(&self) -> bool {
640+
match self {
641+
Self::Never => false,
642+
Self::Warnings | Self::Notes => true,
643+
}
644+
}
645+
646+
/// Returns `true` if the deny level includes notes.
647+
pub fn notes(&self) -> bool {
648+
match self {
649+
Self::Never | Self::Warnings => false,
650+
Self::Notes => true,
651+
}
652+
}
653+
654+
/// Returns `true` if the deny level is set to never (only errors).
655+
pub fn never(&self) -> bool {
656+
match self {
657+
Self::Never => true,
658+
Self::Warnings | Self::Notes => false,
659+
}
660+
}
661+
}
662+
565663
/// Mapping of fallback standalone sections. See [`FallbackProfileProvider`].
566664
pub const STANDALONE_FALLBACK_SECTIONS: &[(&str, &str)] = &[("invariant", "fuzz")];
567665

568666
/// Deprecated keys and their replacements.
569667
///
570668
/// See [Warning::DeprecatedKey]
571-
pub const DEPRECATIONS: &[(&str, &str)] = &[("cancun", "evm_version = Cancun")];
669+
pub const DEPRECATIONS: &[(&str, &str)] =
670+
&[("cancun", "evm_version = Cancun"), ("deny_warnings", "deny = warnings")];
572671

573672
impl Config {
574673
/// The default profile: "default"
@@ -1051,7 +1150,7 @@ impl Config {
10511150
.paths(paths)
10521151
.ignore_error_codes(self.ignored_error_codes.iter().copied().map(Into::into))
10531152
.ignore_paths(self.ignored_file_paths.clone())
1054-
.set_compiler_severity_filter(if self.deny_warnings {
1153+
.set_compiler_severity_filter(if self.deny.warnings() {
10551154
Severity::Warning
10561155
} else {
10571156
Severity::Error
@@ -2160,6 +2259,13 @@ impl Config {
21602259
figment = figment.merge(("evm_version", version));
21612260
}
21622261

2262+
// Normalize `deny` based on the provided `deny_warnings` version.
2263+
if self.deny_warnings
2264+
&& let Ok(DenyLevel::Never) = figment.extract_inner("deny")
2265+
{
2266+
figment = figment.merge(("deny", DenyLevel::Warnings));
2267+
}
2268+
21632269
figment
21642270
}
21652271
}
@@ -2432,6 +2538,7 @@ impl Default for Config {
24322538
SolidityErrorCode::TransientStorageUsed,
24332539
],
24342540
ignored_file_paths: vec![],
2541+
deny: DenyLevel::Never,
24352542
deny_warnings: false,
24362543
via_ir: false,
24372544
ast: false,
@@ -3780,7 +3887,7 @@ mod tests {
37803887
gas_reports = ['*']
37813888
ignored_error_codes = [1878]
37823889
ignored_warnings_from = ["something"]
3783-
deny_warnings = false
3890+
deny = "never"
37843891
initial_balance = '0xffffffffffffffffffffffff'
37853892
libraries = []
37863893
libs = ['lib']
@@ -6221,6 +6328,24 @@ mod tests {
62216328
});
62226329
}
62236330

6331+
#[test]
6332+
fn test_deprecated_deny_warnings_is_handled() {
6333+
figment::Jail::expect_with(|jail| {
6334+
jail.create_file(
6335+
"foundry.toml",
6336+
r#"
6337+
[profile.default]
6338+
deny_warnings = true
6339+
"#,
6340+
)?;
6341+
let config = Config::load().unwrap();
6342+
6343+
// Assert that the deprecated flag is correctly interpreted
6344+
assert_eq!(config.deny, DenyLevel::Warnings);
6345+
Ok(())
6346+
});
6347+
}
6348+
62246349
#[test]
62256350
fn test_evm_version_solc_compatibility_warning() {
62266351
figment::Jail::expect_with(|jail| {

crates/config/src/lint.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use solar::interface::diagnostics::Level;
77
use std::str::FromStr;
88
use yansi::Paint;
99

10-
/// Contains the config and rule set
10+
/// Contains the config and rule set.
1111
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1212
pub struct LinterConfig {
1313
/// Specifies which lints to run based on severity.
@@ -18,7 +18,7 @@ pub struct LinterConfig {
1818
/// Deny specific lints based on their ID (e.g. "mixed-case-function").
1919
pub exclude_lints: Vec<String>,
2020

21-
/// Globs to ignore
21+
/// Globs to ignore.
2222
pub ignore: Vec<String>,
2323

2424
/// Whether to run linting during `forge build`.
@@ -45,7 +45,7 @@ impl Default for LinterConfig {
4545
}
4646
}
4747

48-
/// Severity of a lint
48+
/// Severity of a lint.
4949
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Serialize)]
5050
pub enum Severity {
5151
High,

crates/config/src/providers/ext.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,12 @@ impl<P: Provider> Provider for BackwardsCompatTomlProvider<P> {
302302
dict.insert("solc".to_string(), v);
303303
}
304304
}
305+
if let Some(v) = dict.remove("deny_warnings")
306+
&& !dict.contains_key("deny")
307+
{
308+
dict.insert("deny".to_string(), v);
309+
}
310+
305311
map.insert(profile, dict);
306312
}
307313
Ok(map)

crates/forge/src/cmd/bind_json.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ impl BindJsonArgs {
177177
.source_map()
178178
.new_source_file(path.clone(), source.content.as_str())
179179
{
180-
target_files.insert(src_file.clone());
180+
target_files.insert(Arc::clone(&src_file));
181181
pcx.add_file(src_file);
182182
}
183183
}

crates/forge/src/cmd/build.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ impl BuildArgs {
175175

176176
if !input_files.is_empty() {
177177
let compiler = output.parser_mut().solc_mut().compiler_mut();
178-
linter.lint(&input_files, compiler)?;
178+
linter.lint(&input_files, config.deny, compiler)?;
179179
}
180180
}
181181

0 commit comments

Comments
 (0)