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