Skip to content

Commit 7d9a5b3

Browse files
authored
feat: allow enforcing natspec on specific items (#42)
* fix(variable): raise error on missing natspec * feat: add config to enforce some items have natspec * feat(error): allow to enforce natspec * feat(event): allow to enforce natspec * feat(function): allow to enforce natspec * feat(modifier): allow to enforce natspec * feat(struct): allow to enforce natspec * feat(constructor): allow to enforce natspec * feat(enum): allow to enforce natspec * chore: update readme * docs: update readme * docs: add comment
1 parent 3ca29cc commit 7d9a5b3

20 files changed

+335
-179
lines changed

.env.example

+9-8
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
LINTSPEC_PATHS=[path/to/file.sol,path/to/dir] # paths to files and folders to analyze
2-
LINTSPEC_EXCLUDE=[path/to/ignore] # paths to files or folders to exclude, see also `.nsignore`
3-
LINTSPEC_INHERITDOC=true # enforce that all overridden, public and external items have `@inheritdoc`
4-
LINTSPEC_CONSTRUCTOR=false # enforce that constructors have natspec
5-
LINTSPEC_STRUCT_PARAMS=false # enforce that structs have `@param` for each member
6-
LINTSPEC_ENUM_PARAMS=false # enforce that enums have `@param` for each variant
7-
LINTSPEC_JSON=false # output diagnostics as JSON
8-
LINTSPEC_COMPACT=false # compact output (minified JSON or compact text)
9-
LINTSPEC_SORT=false # sort results by file path
2+
LINTSPEC_EXCLUDE=[path/to/ignore] # paths to files or folders to exclude, see also `.nsignore`
3+
LINTSPEC_INHERITDOC=true # enforce that all overridden, public and external items have `@inheritdoc`
4+
LINTSPEC_CONSTRUCTOR=false # enforce that constructors have natspec
5+
LINTSPEC_STRUCT_PARAMS=false # enforce that structs have `@param` for each member
6+
LINTSPEC_ENUM_PARAMS=false # enforce that enums have `@param` for each variant
7+
LINTSPEC_ENFORCE=[variable,struct] # enforce NatSpec on items even if they don't have params/returns/members
8+
LINTSPEC_JSON=false # output diagnostics as JSON
9+
LINTSPEC_COMPACT=false # compact output (minified JSON or compact text)
10+
LINTSPEC_SORT=false # sort results by file path

.lintspec.toml

+5-3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ inheritdoc = true # enforce that all overridden, public and external items h
55
constructor = false # enforce that constructors have natspec
66
struct_params = false # enforce that structs have `@param` for each member
77
enum_params = false # enforce that enums have `@param` for each variant
8-
json = false # output diagnostics as JSON
9-
compact = false # compact output (minified JSON or compact text)
10-
sort = false # sort results by file path
8+
# enforce NatSpec on items even if they don't have params/returns/members. Possible values: constructor, enum, error, event, function, modifier, struct, variable
9+
enforce = ["variable", "struct"]
10+
json = false # output diagnostics as JSON
11+
compact = false # compact output (minified JSON or compact text)
12+
sort = false # sort results by file path

README.md

+12-11
Original file line numberDiff line numberDiff line change
@@ -68,17 +68,18 @@ Arguments:
6868
[PATH]... One or more paths to files and folders to analyze
6969
7070
Options:
71-
-e, --exclude <EXCLUDE> Path to a file or folder to exclude (can be used more than once)
72-
-o, --out <OUT> Write output to a file instead of stderr
73-
--inheritdoc Enforce that all public and external items have `@inheritdoc`
74-
--constructor Enforce that constructors have NatSpec
75-
--struct-params Enforce that structs have `@param` for each member
76-
--enum-params Enforce that enums have `@param` for each variant
77-
--json Output diagnostics in JSON format
78-
--compact Compact output
79-
--sort Sort the results by file path
80-
-h, --help Print help (see more with '--help')
81-
-V, --version Print version
71+
-e, --exclude <EXCLUDE> Path to a file or folder to exclude (can be used more than once)
72+
-o, --out <OUT> Write output to a file instead of stderr
73+
--inheritdoc Enforce that all public and external items have `@inheritdoc`
74+
--constructor Enforce that constructors have NatSpec
75+
--struct-params Enforce that structs have `@param` for each member
76+
--enum-params Enforce that enums have `@param` for each variant
77+
-f, --enforce <TYPE> Enforce NatSpec (@dev, @notice) on items even if they don't have params/returns/members (can be used more than once)
78+
--json Output diagnostics in JSON format
79+
--compact Compact output
80+
--sort Sort the results by file path
81+
-h, --help Print help (see more with '--help')
82+
-V, --version Print version
8283
```
8384

8485
## Configuration

src/config.rs

+10
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ use figment::{
99
use serde::{Deserialize, Serialize};
1010
use serde_with::skip_serializing_none;
1111

12+
use crate::lint::ItemType;
13+
1214
#[derive(Parser, Debug, Clone, Serialize, Deserialize)]
1315
#[skip_serializing_none]
1416
#[command(version, about, long_about = None)]
@@ -54,6 +56,11 @@ pub struct Args {
5456
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
5557
pub enum_params: Option<bool>,
5658

59+
/// Enforce NatSpec (@dev, @notice) on items even if they don't have params/returns/members
60+
/// (can be used more than once)
61+
#[arg(short = 'f', long, name = "TYPE")]
62+
pub enforce: Vec<ItemType>,
63+
5764
/// Output diagnostics in JSON format
5865
///
5966
/// Can be set with `--json` (means true), `--json true` or `--json false`.
@@ -86,6 +93,7 @@ pub struct Config {
8693
pub constructor: bool,
8794
pub struct_params: bool,
8895
pub enum_params: bool,
96+
pub enforce: Vec<ItemType>,
8997
pub json: bool,
9098
pub compact: bool,
9199
pub sort: bool,
@@ -101,6 +109,7 @@ impl From<Args> for Config {
101109
constructor: value.constructor.unwrap_or_default(),
102110
struct_params: value.struct_params.unwrap_or_default(),
103111
enum_params: value.enum_params.unwrap_or_default(),
112+
enforce: value.enforce,
104113
json: value.json.unwrap_or_default(),
105114
compact: value.compact.unwrap_or_default(),
106115
sort: value.sort.unwrap_or_default(),
@@ -119,6 +128,7 @@ pub fn read_config() -> Result<Config> {
119128
constructor: None,
120129
struct_params: None,
121130
enum_params: None,
131+
enforce: args.enforce,
122132
json: None,
123133
compact: None,
124134
sort: None,

src/definitions/constructor.rs

+33-9
Original file line numberDiff line numberDiff line change
@@ -71,17 +71,21 @@ impl Validate for ConstructorDefinition {
7171
span: self.span(),
7272
diags: vec![],
7373
};
74-
if !options.constructor || self.params.is_empty() {
75-
return out;
76-
}
77-
7874
let Some(natspec) = &self.natspec else {
75+
if (!options.constructor || self.params.is_empty())
76+
&& !options.enforce.contains(&ItemType::Constructor)
77+
{
78+
return out;
79+
}
7980
out.diags.push(Diagnostic {
8081
span: self.span(),
8182
message: "missing NatSpec".to_string(),
8283
});
8384
return out;
8485
};
86+
if !options.constructor || self.params.is_empty() {
87+
return out;
88+
}
8589
// check params
8690
out.diags.append(&mut check_params(natspec, &self.params));
8791
out
@@ -101,6 +105,7 @@ mod tests {
101105
constructor: true,
102106
struct_params: false,
103107
enum_params: false,
108+
enforce: vec![],
104109
};
105110

106111
fn parse_file(contents: &str) -> ConstructorDefinition {
@@ -203,15 +208,34 @@ mod tests {
203208
/// @inheritdoc ITest
204209
constructor(uint256 param1) { }
205210
}";
211+
let res =
212+
parse_file(contents).validate(&ValidationOptions::builder().constructor(true).build());
213+
assert_eq!(res.diags.len(), 1);
214+
assert_eq!(res.diags[0].message, "@param param1 is missing");
215+
}
216+
217+
#[test]
218+
fn test_constructor_enforce() {
219+
let contents = "contract Test {
220+
constructor() { }
221+
}";
206222
let res = parse_file(contents).validate(
207223
&ValidationOptions::builder()
208-
.inheritdoc(true) // has no effect on constructor
209-
.constructor(true)
210-
.struct_params(false)
211-
.enum_params(false)
224+
.enforce(vec![ItemType::Constructor])
212225
.build(),
213226
);
214227
assert_eq!(res.diags.len(), 1);
215-
assert_eq!(res.diags[0].message, "@param param1 is missing");
228+
assert_eq!(res.diags[0].message, "missing NatSpec");
229+
230+
let contents = "contract Test {
231+
/// @notice Some notice
232+
constructor() { }
233+
}";
234+
let res = parse_file(contents).validate(
235+
&ValidationOptions::builder()
236+
.enforce(vec![ItemType::Constructor])
237+
.build(),
238+
);
239+
assert!(res.diags.is_empty(), "{:#?}", res.diags);
216240
}
217241
}

src/definitions/enumeration.rs

+37-16
Original file line numberDiff line numberDiff line change
@@ -75,17 +75,20 @@ impl Validate for EnumDefinition {
7575
span: self.span(),
7676
diags: vec![],
7777
};
78-
if !options.enum_params {
79-
return out;
80-
}
8178
// raise error if no NatSpec is available
8279
let Some(natspec) = &self.natspec else {
80+
if !options.enum_params && !options.enforce.contains(&ItemType::Enum) {
81+
return out;
82+
}
8383
out.diags.push(Diagnostic {
8484
span: self.span(),
8585
message: "missing NatSpec".to_string(),
8686
});
8787
return out;
8888
};
89+
if !options.enum_params {
90+
return out;
91+
}
8992
out.diags = check_params(natspec, &self.members);
9093
out
9194
}
@@ -116,6 +119,7 @@ mod tests {
116119
constructor: false,
117120
struct_params: false,
118121
enum_params: true,
122+
enforce: vec![],
119123
};
120124

121125
fn parse_file(contents: &str) -> EnumDefinition {
@@ -136,14 +140,8 @@ mod tests {
136140
Second
137141
}
138142
}";
139-
let res = parse_file(contents).validate(
140-
&ValidationOptions::builder()
141-
.inheritdoc(false)
142-
.constructor(false)
143-
.enum_params(false)
144-
.struct_params(false)
145-
.build(),
146-
);
143+
let res =
144+
parse_file(contents).validate(&ValidationOptions::builder().inheritdoc(false).build());
147145
assert!(res.diags.is_empty(), "{:#?}", res.diags);
148146
}
149147

@@ -245,16 +243,39 @@ mod tests {
245243
First
246244
}
247245
}";
246+
let res =
247+
parse_file(contents).validate(&ValidationOptions::builder().enum_params(true).build());
248+
assert_eq!(res.diags.len(), 1);
249+
assert_eq!(res.diags[0].message, "@param First is missing");
250+
}
251+
252+
#[test]
253+
fn test_enum_enforce() {
254+
let contents = "contract Test {
255+
enum Foobar {
256+
First
257+
}
258+
}";
248259
let res = parse_file(contents).validate(
249260
&ValidationOptions::builder()
250-
.inheritdoc(true) // has no effect for enums
251-
.constructor(false)
252-
.enum_params(true)
253-
.struct_params(false)
261+
.enforce(vec![ItemType::Enum])
254262
.build(),
255263
);
256264
assert_eq!(res.diags.len(), 1);
257-
assert_eq!(res.diags[0].message, "@param First is missing");
265+
assert_eq!(res.diags[0].message, "missing NatSpec");
266+
267+
let contents = "contract Test {
268+
/// @notice Some notice
269+
enum Foobar {
270+
First
271+
}
272+
}";
273+
let res = parse_file(contents).validate(
274+
&ValidationOptions::builder()
275+
.enforce(vec![ItemType::Enum])
276+
.build(),
277+
);
278+
assert!(res.diags.is_empty(), "{:#?}", res.diags);
258279
}
259280

260281
#[test]

src/definitions/error.rs

+30-9
Original file line numberDiff line numberDiff line change
@@ -67,19 +67,19 @@ impl Validate for ErrorDefinition {
6767
.into())
6868
}
6969

70-
fn validate(&self, _: &ValidationOptions) -> ItemDiagnostics {
70+
fn validate(&self, options: &ValidationOptions) -> ItemDiagnostics {
7171
let mut out = ItemDiagnostics {
7272
parent: self.parent(),
7373
item_type: ItemType::Error,
7474
name: self.name(),
7575
span: self.span(),
7676
diags: vec![],
7777
};
78-
if self.params.is_empty() {
79-
return out;
80-
}
8178
// raise error if no NatSpec is available
8279
let Some(natspec) = &self.natspec else {
80+
if self.params.is_empty() && !options.enforce.contains(&ItemType::Error) {
81+
return out;
82+
}
8383
out.diags.push(Diagnostic {
8484
span: self.span(),
8585
message: "missing NatSpec".to_string(),
@@ -104,6 +104,7 @@ mod tests {
104104
constructor: false,
105105
struct_params: false,
106106
enum_params: false,
107+
enforce: vec![],
107108
};
108109

109110
fn parse_file(contents: &str) -> ErrorDefinition {
@@ -200,16 +201,36 @@ mod tests {
200201
/// @inheritdoc ITest
201202
error Foobar(uint256 a);
202203
}";
204+
let res = parse_file(contents).validate(&ValidationOptions::default());
205+
assert_eq!(res.diags.len(), 1);
206+
assert_eq!(res.diags[0].message, "@param a is missing");
207+
}
208+
209+
#[test]
210+
fn test_error_enforce() {
211+
let contents = "contract Test {
212+
error Foobar();
213+
}";
203214
let res = parse_file(contents).validate(
204215
&ValidationOptions::builder()
205-
.inheritdoc(true) // has no effect on error
206-
.constructor(false)
207-
.struct_params(false)
208-
.enum_params(false)
216+
.inheritdoc(false)
217+
.enforce(vec![ItemType::Error])
209218
.build(),
210219
);
211220
assert_eq!(res.diags.len(), 1);
212-
assert_eq!(res.diags[0].message, "@param a is missing");
221+
assert_eq!(res.diags[0].message, "missing NatSpec");
222+
223+
let contents = "contract Test {
224+
/// @notice Some notice
225+
error Foobar();
226+
}";
227+
let res = parse_file(contents).validate(
228+
&ValidationOptions::builder()
229+
.inheritdoc(false)
230+
.enforce(vec![ItemType::Error])
231+
.build(),
232+
);
233+
assert!(res.diags.is_empty(), "{:#?}", res.diags);
213234
}
214235

215236
#[test]

0 commit comments

Comments
 (0)