Skip to main content

lintspec_core/
config.rs

1//! Tool configuration parsing and validation
2use std::path::PathBuf;
3
4use derive_more::IsVariant;
5use figment::{
6    Figment, Metadata, Profile, Provider,
7    providers::{Env, Format as _, Toml},
8    value::{Dict, Map},
9};
10use serde::{Deserialize, Serialize};
11
12/// The requirement for a specific tag in the natspec comment
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, IsVariant)]
14#[serde(rename_all = "lowercase")]
15pub enum Req {
16    /// The tag is ignored, can be present or not
17    #[default]
18    Ignored,
19
20    /// The tag is required, must be present
21    Required,
22
23    /// The tag is forbidden, must not be present
24    Forbidden,
25}
26
27impl Req {
28    /// Helper function to check if the tag is required or ignored (not forbidden)
29    #[must_use]
30    pub fn is_required_or_ignored(&self) -> bool {
31        match self {
32            Req::Required | Req::Ignored => true,
33            Req::Forbidden => false,
34        }
35    }
36
37    /// Helper function to check if the tag is forbidden or ignored (not required)
38    #[must_use]
39    pub fn is_forbidden_or_ignored(&self) -> bool {
40        match self {
41            Req::Forbidden | Req::Ignored => true,
42            Req::Required => false,
43        }
44    }
45}
46
47/// Validation rules for a function natspec comment
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
49#[non_exhaustive]
50pub struct FunctionRules {
51    /// Requirement for the `@notice` tag
52    #[builder(default = Req::Required)]
53    pub notice: Req,
54
55    /// Requirement for the `@dev` tag
56    #[builder(default)]
57    pub dev: Req,
58
59    /// Requirement for the `@param` tags
60    #[builder(default = Req::Required)]
61    pub param: Req,
62
63    /// Requirement for the `@return` tags
64    #[serde(rename = "return")]
65    #[builder(default = Req::Required)]
66    pub returns: Req,
67}
68
69impl Default for FunctionRules {
70    fn default() -> Self {
71        Self {
72            notice: Req::Required,
73            dev: Req::default(),
74            param: Req::Required,
75            returns: Req::Required,
76        }
77    }
78}
79
80/// Validation rules for each function visibility (private, internal, public, external)
81#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, bon::Builder)]
82#[non_exhaustive]
83pub struct FunctionConfig {
84    /// Rules for private functions
85    #[builder(default)]
86    pub private: FunctionRules,
87
88    /// Rules for internal functions
89    #[builder(default)]
90    pub internal: FunctionRules,
91
92    /// Rules for public functions
93    #[builder(default)]
94    pub public: FunctionRules,
95
96    /// Rules for external functions
97    #[builder(default)]
98    pub external: FunctionRules,
99}
100
101/// Validation rules for items which have return values but no params (public state variables)
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
103#[non_exhaustive]
104pub struct WithReturnsRules {
105    /// Requirement for the `@notice` tag
106    #[builder(default = Req::Required)]
107    pub notice: Req,
108
109    /// Requirement for the `@dev` tag
110    #[builder(default)]
111    pub dev: Req,
112
113    /// Requirement for the `@param` tags
114    #[serde(rename = "return")]
115    #[builder(default = Req::Required)]
116    pub returns: Req,
117}
118
119impl Default for WithReturnsRules {
120    fn default() -> Self {
121        Self {
122            notice: Req::Required,
123            dev: Req::default(),
124            returns: Req::Required,
125        }
126    }
127}
128
129/// Validation rules for items which have no return values and no params (private and internal state variables)
130#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
131#[non_exhaustive]
132pub struct NoticeDevRules {
133    #[builder(default = Req::Required)]
134    pub notice: Req,
135    #[builder(default)]
136    pub dev: Req,
137}
138
139impl Default for NoticeDevRules {
140    fn default() -> Self {
141        Self {
142            notice: Req::Required,
143            dev: Req::default(),
144        }
145    }
146}
147
148/// Validation rules for contracts, interfaces and libraries
149#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
150#[non_exhaustive]
151pub struct ContractRules {
152    #[builder(default)]
153    pub title: Req,
154    #[builder(default)]
155    pub author: Req,
156    #[builder(default)]
157    pub notice: Req,
158    #[builder(default)]
159    pub dev: Req,
160}
161
162/// Validation rules for each state variable visibility (private, internal, public)
163#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
164#[non_exhaustive]
165pub struct VariableConfig {
166    #[builder(default)]
167    pub private: NoticeDevRules,
168    #[builder(default)]
169    pub internal: NoticeDevRules,
170    #[builder(default)]
171    pub public: WithReturnsRules,
172}
173
174/// Validation rules for items which have params but no returns (constructor, enum, error, event, modifier, struct)
175///
176/// The default value does not enforce that `@param` is present, because it's not part of the official spec for enums
177/// and structs.
178#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
179#[non_exhaustive]
180pub struct WithParamsRules {
181    #[builder(default = Req::Required)]
182    pub notice: Req,
183    #[builder(default)]
184    pub dev: Req,
185    #[builder(default)]
186    pub param: Req,
187}
188
189impl Default for WithParamsRules {
190    fn default() -> Self {
191        Self {
192            notice: Req::Required,
193            dev: Req::default(),
194            param: Req::default(),
195        }
196    }
197}
198
199impl WithParamsRules {
200    /// Helper function to get a set of rules which enforces params (default for error, event, modifier)
201    #[must_use]
202    pub fn required() -> Self {
203        Self {
204            param: Req::Required,
205            ..Default::default()
206        }
207    }
208
209    /// Helper function to get a default set of rules for constructors (`@notice` is not enforced, but `@param` is)
210    #[must_use]
211    pub fn default_constructor() -> Self {
212        Self {
213            notice: Req::Ignored,
214            param: Req::Required,
215            ..Default::default()
216        }
217    }
218}
219
220/// General config for the tool
221#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
222#[non_exhaustive]
223#[expect(clippy::struct_excessive_bools)]
224pub struct BaseConfig {
225    /// Paths to files and folders to analyze
226    #[builder(default)]
227    pub paths: Vec<PathBuf>,
228
229    /// Paths to files and folders to exclude
230    #[builder(default)]
231    pub exclude: Vec<PathBuf>,
232
233    /// Enforce that all public and external items have `@inheritdoc`
234    #[builder(default = true)]
235    pub inheritdoc: bool,
236
237    /// Enforce that internal functions and modifiers which are `override` have `@inheritdoc`
238    #[builder(default = false)]
239    pub inheritdoc_override: bool,
240
241    /// Do not distinguish between `@notice` and `@dev` when considering "required" validation rules
242    #[builder(default)]
243    pub notice_or_dev: bool,
244
245    /// Skip the detection of the Solidity version and use the latest version supported by `slang_solidity`
246    #[cfg_attr(not(feature = "slang"), serde(skip))]
247    #[builder(default)]
248    pub skip_version_detection: bool,
249}
250
251impl Default for BaseConfig {
252    fn default() -> Self {
253        Self {
254            paths: Vec::default(),
255            exclude: Vec::default(),
256            inheritdoc: true,
257            inheritdoc_override: false,
258            notice_or_dev: false,
259            skip_version_detection: false,
260        }
261    }
262}
263
264/// Output config for the tool
265#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, bon::Builder)]
266#[non_exhaustive]
267pub struct OutputConfig {
268    /// Path to a file to write the output to (instead of stderr)
269    #[serde(skip_serializing_if = "Option::is_none")]
270    pub out: Option<PathBuf>,
271
272    /// Output diagnostics in JSON format
273    #[builder(default)]
274    pub json: bool,
275
276    /// Compact output (minified JSON or compact text representation)
277    #[builder(default)]
278    pub compact: bool,
279
280    /// Sort the results by file path
281    #[builder(default)]
282    pub sort: bool,
283}
284
285/// The parsed and validated config for the tool
286#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
287#[non_exhaustive]
288pub struct Config {
289    /// General config for the tool
290    #[builder(default)]
291    pub lintspec: BaseConfig,
292
293    /// Output config for the tool
294    #[builder(default)]
295    pub output: OutputConfig,
296
297    /// Validation rules for constructors
298    #[serde(rename = "constructor")]
299    #[builder(default = WithParamsRules::default_constructor())]
300    pub constructors: WithParamsRules,
301
302    /// Validation rules for contracts
303    #[serde(rename = "contract")]
304    #[builder(default)]
305    pub contracts: ContractRules,
306
307    /// Validation rules for interfaces
308    #[serde(rename = "interface")]
309    #[builder(default)]
310    pub interfaces: ContractRules,
311
312    /// Validation rules for libraries
313    #[serde(rename = "library")]
314    #[builder(default)]
315    pub libraries: ContractRules,
316
317    /// Validation rules for enums
318    #[serde(rename = "enum")]
319    #[builder(default)]
320    pub enums: WithParamsRules,
321
322    /// Validation rules for errors
323    #[serde(rename = "error")]
324    #[builder(default = WithParamsRules::required())]
325    pub errors: WithParamsRules,
326
327    /// Validation rules for events
328    #[serde(rename = "event")]
329    #[builder(default = WithParamsRules::required())]
330    pub events: WithParamsRules,
331
332    /// Validation rules for functions
333    #[serde(rename = "function")]
334    #[builder(default)]
335    pub functions: FunctionConfig,
336
337    /// Validation rules for modifiers
338    #[serde(rename = "modifier")]
339    #[builder(default = WithParamsRules::required())]
340    pub modifiers: WithParamsRules,
341
342    /// Validation rules for structs
343    #[serde(rename = "struct")]
344    #[builder(default)]
345    pub structs: WithParamsRules,
346
347    /// Validation rules for state variables
348    #[serde(rename = "variable")]
349    #[builder(default)]
350    pub variables: VariableConfig,
351}
352
353impl Default for Config {
354    fn default() -> Self {
355        Self {
356            lintspec: BaseConfig::default(),
357            output: OutputConfig::default(),
358            contracts: ContractRules::default(),
359            interfaces: ContractRules::default(),
360            libraries: ContractRules::default(),
361            constructors: WithParamsRules::default_constructor(),
362            enums: WithParamsRules::default(),
363            errors: WithParamsRules::required(),
364            events: WithParamsRules::required(),
365            functions: FunctionConfig::default(),
366            modifiers: WithParamsRules::required(),
367            structs: WithParamsRules::default(),
368            variables: VariableConfig::default(),
369        }
370    }
371}
372
373impl Config {
374    pub fn from(provider: impl Provider) -> Result<Config, Box<figment::Error>> {
375        Figment::from(provider).extract().map_err(Box::new)
376    }
377
378    /// Create a Figment which reads the config from the default file and environment variables
379    #[must_use]
380    pub fn figment(config_path: Option<PathBuf>) -> Figment {
381        Figment::from(Config::default())
382            .admerge(Toml::file(config_path.unwrap_or(".lintspec.toml".into())))
383            .admerge(Env::prefixed("LS_").split("_").map(|k| {
384                // special case for parameters with an underscore in the name
385                match k.as_str() {
386                    "LINTSPEC.NOTICE.OR.DEV" => "LINTSPEC.NOTICE_OR_DEV".into(),
387                    "LINTSPEC.SKIP.VERSION.DETECTION" => "LINTSPEC.SKIP_VERSION_DETECTION".into(),
388                    _ => k.into(),
389                }
390            }))
391    }
392}
393
394/// Implement [`Provider`] for composability
395impl Provider for Config {
396    fn metadata(&self) -> figment::Metadata {
397        Metadata::named("LintSpec Config")
398    }
399
400    fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
401        figment::providers::Serialized::defaults(Config::default()).data()
402    }
403}
404
405#[cfg(test)]
406mod tests {
407    use similar_asserts::assert_eq;
408
409    use super::*;
410
411    #[test]
412    fn test_default_builder() {
413        assert_eq!(FunctionRules::default(), FunctionRules::builder().build());
414        assert_eq!(FunctionConfig::default(), FunctionConfig::builder().build());
415        assert_eq!(
416            WithReturnsRules::default(),
417            WithReturnsRules::builder().build()
418        );
419        assert_eq!(NoticeDevRules::default(), NoticeDevRules::builder().build());
420        assert_eq!(ContractRules::default(), ContractRules::builder().build());
421        assert_eq!(VariableConfig::default(), VariableConfig::builder().build());
422        assert_eq!(
423            WithParamsRules::default(),
424            WithParamsRules::builder().build()
425        );
426        assert_eq!(BaseConfig::default(), BaseConfig::builder().build());
427        assert_eq!(OutputConfig::default(), OutputConfig::builder().build());
428        assert_eq!(Config::default(), Config::builder().build());
429    }
430}