1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, IsVariant)]
14#[serde(rename_all = "lowercase")]
15pub enum Req {
16 #[default]
18 Ignored,
19
20 Required,
22
23 Forbidden,
25}
26
27impl Req {
28 #[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 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
49#[non_exhaustive]
50pub struct FunctionRules {
51 #[builder(default = Req::Required)]
53 pub notice: Req,
54
55 #[builder(default)]
57 pub dev: Req,
58
59 #[builder(default = Req::Required)]
61 pub param: Req,
62
63 #[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#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, bon::Builder)]
82#[non_exhaustive]
83pub struct FunctionConfig {
84 #[builder(default)]
86 pub private: FunctionRules,
87
88 #[builder(default)]
90 pub internal: FunctionRules,
91
92 #[builder(default)]
94 pub public: FunctionRules,
95
96 #[builder(default)]
98 pub external: FunctionRules,
99}
100
101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
103#[non_exhaustive]
104pub struct WithReturnsRules {
105 #[builder(default = Req::Required)]
107 pub notice: Req,
108
109 #[builder(default)]
111 pub dev: Req,
112
113 #[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#[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#[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#[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#[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 #[must_use]
202 pub fn required() -> Self {
203 Self {
204 param: Req::Required,
205 ..Default::default()
206 }
207 }
208
209 #[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
222#[non_exhaustive]
223#[expect(clippy::struct_excessive_bools)]
224pub struct BaseConfig {
225 #[builder(default)]
227 pub paths: Vec<PathBuf>,
228
229 #[builder(default)]
231 pub exclude: Vec<PathBuf>,
232
233 #[builder(default = true)]
235 pub inheritdoc: bool,
236
237 #[builder(default = false)]
239 pub inheritdoc_override: bool,
240
241 #[builder(default)]
243 pub notice_or_dev: bool,
244
245 #[builder(default = 4)]
249 pub parallel: usize,
250
251 #[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#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, bon::Builder)]
273#[non_exhaustive]
274#[expect(clippy::struct_excessive_bools)]
275pub struct OutputConfig {
276 #[serde(skip_serializing_if = "Option::is_none")]
278 pub out: Option<PathBuf>,
279
280 #[builder(default)]
282 pub json: bool,
283
284 #[builder(default)]
286 pub compact: bool,
287
288 #[builder(default)]
290 pub sort: bool,
291
292 #[builder(default)]
294 pub stdout: bool,
295
296 #[builder(default)]
298 pub exit_zero: bool,
299}
300
301#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
303#[non_exhaustive]
304pub struct Config {
305 #[builder(default)]
307 pub lintspec: BaseConfig,
308
309 #[builder(default)]
311 pub output: OutputConfig,
312
313 #[serde(rename = "constructor")]
315 #[builder(default = WithParamsRules::default_constructor())]
316 pub constructors: WithParamsRules,
317
318 #[serde(rename = "contract")]
320 #[builder(default)]
321 pub contracts: ContractRules,
322
323 #[serde(rename = "interface")]
325 #[builder(default)]
326 pub interfaces: ContractRules,
327
328 #[serde(rename = "library")]
330 #[builder(default)]
331 pub libraries: ContractRules,
332
333 #[serde(rename = "enum")]
335 #[builder(default)]
336 pub enums: WithParamsRules,
337
338 #[serde(rename = "error")]
340 #[builder(default = WithParamsRules::required())]
341 pub errors: WithParamsRules,
342
343 #[serde(rename = "event")]
345 #[builder(default = WithParamsRules::required())]
346 pub events: WithParamsRules,
347
348 #[serde(rename = "function")]
350 #[builder(default)]
351 pub functions: FunctionConfig,
352
353 #[serde(rename = "modifier")]
355 #[builder(default = WithParamsRules::required())]
356 pub modifiers: WithParamsRules,
357
358 #[serde(rename = "struct")]
360 #[builder(default)]
361 pub structs: WithParamsRules,
362
363 #[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 #[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 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
410impl 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}