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::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    /// If uninformed, all severities are checked.
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    /// Configurable patterns that should be excluded when performing `mixedCase` lint checks.
30    ///
31    /// Defaults to common abbreviations: `ERC`, `URI`, `ID`, `URL`, `API`, `JSON`, `XML`, `HTML`,
32    /// `HTTP`, `HTTPS`. This allows names like `marketID`, `tokenURI`, `apiURL`, `parseJSON`, etc.
33    pub mixed_case_exceptions: Vec<String>,
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            mixed_case_exceptions: vec![
44                "ERC".to_string(),
45                "URI".to_string(),
46                "ID".to_string(),
47                "URL".to_string(),
48                "API".to_string(),
49                "JSON".to_string(),
50                "XML".to_string(),
51                "HTML".to_string(),
52                "HTTP".to_string(),
53                "HTTPS".to_string(),
54            ],
55        }
56    }
57}
58
59/// Severity of a lint.
60#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
61pub enum Severity {
62    High,
63    Med,
64    Low,
65    Info,
66    Gas,
67    CodeSize,
68}
69
70impl Severity {
71    fn to_str(self) -> &'static str {
72        match self {
73            Self::High => "High",
74            Self::Med => "Med",
75            Self::Low => "Low",
76            Self::Info => "Info",
77            Self::Gas => "Gas",
78            Self::CodeSize => "CodeSize",
79        }
80    }
81
82    fn to_str_kebab(self) -> &'static str {
83        match self {
84            Self::High => "high",
85            Self::Med => "medium",
86            Self::Low => "low",
87            Self::Info => "info",
88            Self::Gas => "gas",
89            Self::CodeSize => "code-size",
90        }
91    }
92
93    pub fn color(&self, message: &str) -> String {
94        match self {
95            Self::High => Paint::red(message).bold().to_string(),
96            Self::Med => Paint::rgb(message, 255, 135, 61).bold().to_string(),
97            Self::Low => Paint::yellow(message).bold().to_string(),
98            Self::Info => Paint::cyan(message).bold().to_string(),
99            Self::Gas => Paint::green(message).bold().to_string(),
100            Self::CodeSize => Paint::green(message).bold().to_string(),
101        }
102    }
103}
104
105impl From<Severity> for Level {
106    fn from(severity: Severity) -> Self {
107        match severity {
108            Severity::High | Severity::Med | Severity::Low => Self::Warning,
109            Severity::Info | Severity::Gas | Severity::CodeSize => Self::Note,
110        }
111    }
112}
113
114impl fmt::Display for Severity {
115    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116        write!(f, "{}", self.color(self.to_str()))
117    }
118}
119
120impl Serialize for Severity {
121    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
122    where
123        S: serde::Serializer,
124    {
125        self.to_str_kebab().serialize(serializer)
126    }
127}
128
129// Custom deserialization to make `Severity` parsing case-insensitive
130impl<'de> Deserialize<'de> for Severity {
131    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
132    where
133        D: Deserializer<'de>,
134    {
135        let s = String::deserialize(deserializer)?;
136        FromStr::from_str(&s).map_err(serde::de::Error::custom)
137    }
138}
139
140impl FromStr for Severity {
141    type Err = String;
142
143    fn from_str(s: &str) -> Result<Self, Self::Err> {
144        match s.to_lowercase().as_str() {
145            "high" => Ok(Self::High),
146            "med" | "medium" => Ok(Self::Med),
147            "low" => Ok(Self::Low),
148            "info" => Ok(Self::Info),
149            "gas" => Ok(Self::Gas),
150            "size" | "codesize" | "code-size" => Ok(Self::CodeSize),
151            _ => Err(format!(
152                "unknown variant: found `{s}`, expected `one of `High`, `Med`, `Low`, `Info`, `Gas`, `CodeSize`"
153            )),
154        }
155    }
156}