Skip to main content

foundry_config/
lint.rs

1//! Configuration specific to the `forge lint` command and the `forge_lint` package
2
3use clap::ValueEnum;
4use core::fmt;
5use serde::{Deserialize, Deserializer, Serialize};
6use solar::{
7    ast::{self as ast},
8    interface::diagnostics::Level,
9};
10use std::str::FromStr;
11use yansi::Paint;
12
13/// Contains the config and rule set.
14#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
15pub struct LinterConfig {
16    /// Specifies which lints to run based on severity.
17    ///
18    /// If uninformed, all severities are checked.
19    pub severity: Vec<Severity>,
20
21    /// Deny specific lints based on their ID (e.g. "mixed-case-function").
22    pub exclude_lints: Vec<String>,
23
24    /// Globs to ignore.
25    pub ignore: Vec<String>,
26
27    /// Whether to run linting during `forge build`.
28    ///
29    /// Defaults to true. Set to false to disable automatic linting during builds.
30    pub lint_on_build: bool,
31
32    /// Configuration specific to individual lints.
33    pub lint_specific: LintSpecificConfig,
34}
35
36impl Default for LinterConfig {
37    fn default() -> Self {
38        Self {
39            lint_on_build: true,
40            severity: vec![Severity::High, Severity::Med, Severity::Low],
41            exclude_lints: Vec::new(),
42            ignore: Vec::new(),
43            lint_specific: LintSpecificConfig::default(),
44        }
45    }
46}
47
48/// Contract types that can be exempted from the multi-contract-file lint.
49#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
50#[serde(rename_all = "snake_case")]
51pub enum ContractException {
52    Interface,
53    Library,
54    AbstractContract,
55}
56
57/// Configuration specific to individual lints.
58#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(default)]
60pub struct LintSpecificConfig {
61    /// Configurable patterns that should be excluded when performing `mixedCase` lint checks.
62    ///
63    /// Defaults to ["ERC", "URI"] to allow common names like `rescueERC20`, `ERC721TokenReceiver`
64    /// or `tokenURI`.
65    pub mixed_case_exceptions: Vec<String>,
66
67    /// Contract types that are allowed to appear multiple times in the same file.
68    ///
69    /// Valid values: "interface", "library", "abstract_contract"
70    ///
71    /// Defaults to an empty array (all contract types are flagged when multiple exist).
72    /// Note: Regular contracts cannot be exempted and will always be flagged when multiple exist.
73    pub multi_contract_file_exceptions: Vec<ContractException>,
74}
75
76impl Default for LintSpecificConfig {
77    fn default() -> Self {
78        Self {
79            mixed_case_exceptions: vec![
80                "ERC".to_string(),
81                "URI".to_string(),
82                "ID".to_string(),
83                "URL".to_string(),
84                "API".to_string(),
85                "JSON".to_string(),
86                "XML".to_string(),
87                "HTML".to_string(),
88                "HTTP".to_string(),
89                "HTTPS".to_string(),
90            ],
91            multi_contract_file_exceptions: Vec::new(),
92        }
93    }
94}
95
96impl LintSpecificConfig {
97    /// Checks if a given contract kind is included in the list of exceptions
98    pub fn is_exempted(&self, contract_kind: &ast::ContractKind) -> bool {
99        let exception_to_check = match contract_kind {
100            ast::ContractKind::Interface => ContractException::Interface,
101            ast::ContractKind::Library => ContractException::Library,
102            ast::ContractKind::AbstractContract => ContractException::AbstractContract,
103            // Regular contracts are always linted
104            ast::ContractKind::Contract => return false,
105        };
106
107        self.multi_contract_file_exceptions.contains(&exception_to_check)
108    }
109}
110
111/// Severity of a lint.
112#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
113pub enum Severity {
114    High,
115    Med,
116    Low,
117    Info,
118    Gas,
119    CodeSize,
120}
121
122impl Severity {
123    fn to_str(self) -> &'static str {
124        match self {
125            Self::High => "High",
126            Self::Med => "Med",
127            Self::Low => "Low",
128            Self::Info => "Info",
129            Self::Gas => "Gas",
130            Self::CodeSize => "CodeSize",
131        }
132    }
133
134    fn to_str_kebab(self) -> &'static str {
135        match self {
136            Self::High => "high",
137            Self::Med => "medium",
138            Self::Low => "low",
139            Self::Info => "info",
140            Self::Gas => "gas",
141            Self::CodeSize => "code-size",
142        }
143    }
144
145    pub fn color(&self, message: &str) -> String {
146        match self {
147            Self::High => Paint::red(message).bold().to_string(),
148            Self::Med => Paint::rgb(message, 255, 135, 61).bold().to_string(),
149            Self::Low => Paint::yellow(message).bold().to_string(),
150            Self::Info => Paint::cyan(message).bold().to_string(),
151            Self::Gas => Paint::green(message).bold().to_string(),
152            Self::CodeSize => Paint::green(message).bold().to_string(),
153        }
154    }
155}
156
157impl From<Severity> for Level {
158    fn from(severity: Severity) -> Self {
159        match severity {
160            Severity::High | Severity::Med | Severity::Low => Self::Warning,
161            Severity::Info | Severity::Gas | Severity::CodeSize => Self::Note,
162        }
163    }
164}
165
166impl fmt::Display for Severity {
167    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168        write!(f, "{}", self.color(self.to_str()))
169    }
170}
171
172impl Serialize for Severity {
173    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
174    where
175        S: serde::Serializer,
176    {
177        self.to_str_kebab().serialize(serializer)
178    }
179}
180
181// Custom deserialization to make `Severity` parsing case-insensitive
182impl<'de> Deserialize<'de> for Severity {
183    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
184    where
185        D: Deserializer<'de>,
186    {
187        let s = String::deserialize(deserializer)?;
188        FromStr::from_str(&s).map_err(serde::de::Error::custom)
189    }
190}
191
192impl FromStr for Severity {
193    type Err = String;
194
195    fn from_str(s: &str) -> Result<Self, Self::Err> {
196        match s.to_lowercase().as_str() {
197            "high" => Ok(Self::High),
198            "med" | "medium" => Ok(Self::Med),
199            "low" => Ok(Self::Low),
200            "info" => Ok(Self::Info),
201            "gas" => Ok(Self::Gas),
202            "size" | "codesize" | "code-size" => Ok(Self::CodeSize),
203            _ => Err(format!(
204                "unknown variant: found `{s}`, expected `one of `High`, `Med`, `Low`, `Info`, `Gas`, `CodeSize`"
205            )),
206        }
207    }
208}