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    /// Number of parallel workers/threads, or 0 to use the number of logical cores
246    ///
247    /// Defaults to 4.
248    #[builder(default = 4)]
249    pub parallel: usize,
250
251    /// Skip the detection of the Solidity version and use the latest version supported by `slang_solidity`
252    #[cfg_attr(not(feature = "slang"), serde(skip))]
253    #[builder(default)]
254    pub skip_version_detection: bool,
255}
256
257impl Default for BaseConfig {
258    fn default() -> Self {
259        Self {
260            paths: Vec::default(),
261            exclude: Vec::default(),
262            inheritdoc: true,
263            inheritdoc_override: false,
264            notice_or_dev: false,
265            parallel: 4,
266            skip_version_detection: false,
267        }
268    }
269}
270
271/// Output config for the tool
272#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, bon::Builder)]
273#[non_exhaustive]
274#[expect(clippy::struct_excessive_bools)]
275pub struct OutputConfig {
276    /// Path to a file to write the output to (instead of stderr)
277    #[serde(skip_serializing_if = "Option::is_none")]
278    pub out: Option<PathBuf>,
279
280    /// Output diagnostics in JSON format
281    #[builder(default)]
282    pub json: bool,
283
284    /// Compact output (minified JSON or compact text representation)
285    #[builder(default)]
286    pub compact: bool,
287
288    /// Sort the results by file path
289    #[builder(default)]
290    pub sort: bool,
291
292    /// Write diagnostics to stdout instead of stderr (when no `--out` file is specified)
293    #[builder(default)]
294    pub stdout: bool,
295
296    /// Exit with code 0 even when there are diagnostics
297    #[builder(default)]
298    pub exit_zero: bool,
299}
300
301/// The parsed and validated config for the tool
302#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
303#[non_exhaustive]
304pub struct Config {
305    /// General config for the tool
306    #[builder(default)]
307    pub lintspec: BaseConfig,
308
309    /// Output config for the tool
310    #[builder(default)]
311    pub output: OutputConfig,
312
313    /// Validation rules for constructors
314    #[serde(rename = "constructor")]
315    #[builder(default = WithParamsRules::default_constructor())]
316    pub constructors: WithParamsRules,
317
318    /// Validation rules for contracts
319    #[serde(rename = "contract")]
320    #[builder(default)]
321    pub contracts: ContractRules,
322
323    /// Validation rules for interfaces
324    #[serde(rename = "interface")]
325    #[builder(default)]
326    pub interfaces: ContractRules,
327
328    /// Validation rules for libraries
329    #[serde(rename = "library")]
330    #[builder(default)]
331    pub libraries: ContractRules,
332
333    /// Validation rules for enums
334    #[serde(rename = "enum")]
335    #[builder(default)]
336    pub enums: WithParamsRules,
337
338    /// Validation rules for errors
339    #[serde(rename = "error")]
340    #[builder(default = WithParamsRules::required())]
341    pub errors: WithParamsRules,
342
343    /// Validation rules for events
344    #[serde(rename = "event")]
345    #[builder(default = WithParamsRules::required())]
346    pub events: WithParamsRules,
347
348    /// Validation rules for functions
349    #[serde(rename = "function")]
350    #[builder(default)]
351    pub functions: FunctionConfig,
352
353    /// Validation rules for modifiers
354    #[serde(rename = "modifier")]
355    #[builder(default = WithParamsRules::required())]
356    pub modifiers: WithParamsRules,
357
358    /// Validation rules for structs
359    #[serde(rename = "struct")]
360    #[builder(default)]
361    pub structs: WithParamsRules,
362
363    /// Validation rules for state variables
364    #[serde(rename = "variable")]
365    #[builder(default)]
366    pub variables: VariableConfig,
367}
368
369impl Default for Config {
370    fn default() -> Self {
371        Self {
372            lintspec: BaseConfig::default(),
373            output: OutputConfig::default(),
374            contracts: ContractRules::default(),
375            interfaces: ContractRules::default(),
376            libraries: ContractRules::default(),
377            constructors: WithParamsRules::default_constructor(),
378            enums: WithParamsRules::default(),
379            errors: WithParamsRules::required(),
380            events: WithParamsRules::required(),
381            functions: FunctionConfig::default(),
382            modifiers: WithParamsRules::required(),
383            structs: WithParamsRules::default(),
384            variables: VariableConfig::default(),
385        }
386    }
387}
388
389impl Config {
390    pub fn from(provider: impl Provider) -> Result<Config, Box<figment::Error>> {
391        Figment::from(provider).extract().map_err(Box::new)
392    }
393
394    /// Create a Figment which reads the config from the default file and environment variables
395    #[must_use]
396    pub fn figment(config_path: Option<PathBuf>) -> Figment {
397        Figment::from(Config::default())
398            .admerge(Toml::file(config_path.unwrap_or(".lintspec.toml".into())))
399            .admerge(Env::prefixed("LS_").split("_").map(|k| {
400                // special case for parameters with an underscore in the name
401                match k.as_str() {
402                    "LINTSPEC.NOTICE.OR.DEV" => "LINTSPEC.NOTICE_OR_DEV".into(),
403                    "LINTSPEC.SKIP.VERSION.DETECTION" => "LINTSPEC.SKIP_VERSION_DETECTION".into(),
404                    _ => k.into(),
405                }
406            }))
407    }
408}
409
410/// Implement [`Provider`] for composability
411impl Provider for Config {
412    fn metadata(&self) -> figment::Metadata {
413        Metadata::named("LintSpec Config")
414    }
415
416    fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
417        figment::providers::Serialized::defaults(Config::default()).data()
418    }
419}
420
421#[cfg(test)]
422mod tests {
423    use similar_asserts::assert_eq;
424
425    use super::*;
426
427    #[test]
428    fn test_default_builder() {
429        assert_eq!(FunctionRules::default(), FunctionRules::builder().build());
430        assert_eq!(FunctionConfig::default(), FunctionConfig::builder().build());
431        assert_eq!(
432            WithReturnsRules::default(),
433            WithReturnsRules::builder().build()
434        );
435        assert_eq!(NoticeDevRules::default(), NoticeDevRules::builder().build());
436        assert_eq!(ContractRules::default(), ContractRules::builder().build());
437        assert_eq!(VariableConfig::default(), VariableConfig::builder().build());
438        assert_eq!(
439            WithParamsRules::default(),
440            WithParamsRules::builder().build()
441        );
442        assert_eq!(BaseConfig::default(), BaseConfig::builder().build());
443        assert_eq!(OutputConfig::default(), OutputConfig::builder().build());
444        assert_eq!(Config::default(), Config::builder().build());
445    }
446}