Skip to main content

lintspec_core/definitions/
structure.rs

1//! Parsing and validation of struct definitions.
2use crate::{
3    lint::{CheckNoticeAndDev, CheckParams, ItemDiagnostics},
4    natspec::NatSpec,
5};
6
7use super::{Identifier, ItemType, Parent, SourceItem, TextRange, Validate, ValidationOptions};
8
9/// A struct definition
10#[derive(Debug, Clone, bon::Builder)]
11#[non_exhaustive]
12#[builder(on(String, into))]
13pub struct StructDefinition {
14    /// The parent for the struct definition, if any
15    pub parent: Option<Parent>,
16
17    /// The name of the struct
18    pub name: String,
19
20    /// The span of the struct definition
21    pub span: TextRange,
22
23    /// The name and span of the struct members
24    pub members: Vec<Identifier>,
25
26    /// The [`NatSpec`] associated with the struct definition, if any
27    pub natspec: Option<NatSpec>,
28}
29
30impl SourceItem for StructDefinition {
31    fn item_type(&self) -> ItemType {
32        ItemType::Struct
33    }
34
35    fn parent(&self) -> Option<Parent> {
36        self.parent.clone()
37    }
38
39    fn name(&self) -> String {
40        self.name.clone()
41    }
42
43    fn span(&self) -> TextRange {
44        self.span.clone()
45    }
46}
47
48impl Validate for StructDefinition {
49    fn validate(&self, options: &ValidationOptions) -> ItemDiagnostics {
50        let opts = &options.structs;
51        let mut out = ItemDiagnostics {
52            parent: self.parent(),
53            item_type: self.item_type(),
54            name: self.name(),
55            span: self.span(),
56            diags: vec![],
57        };
58        out.diags.extend(
59            CheckNoticeAndDev::builder()
60                .natspec(&self.natspec)
61                .notice_rule(opts.notice)
62                .dev_rule(opts.dev)
63                .notice_or_dev(options.notice_or_dev)
64                .span(&self.span)
65                .build()
66                .check(),
67        );
68        out.diags.extend(
69            CheckParams::builder()
70                .natspec(&self.natspec)
71                .rule(opts.param)
72                .params(&self.members)
73                .default_span(self.span())
74                .build()
75                .check(),
76        );
77        out
78    }
79}
80
81#[cfg(test)]
82#[cfg(feature = "solar")]
83mod tests {
84    use std::sync::LazyLock;
85
86    use similar_asserts::assert_eq;
87
88    use crate::{
89        config::WithParamsRules,
90        definitions::Definition,
91        parser::{Parse as _, solar::SolarParser},
92    };
93
94    use super::*;
95
96    static OPTIONS: LazyLock<ValidationOptions> = LazyLock::new(|| {
97        ValidationOptions::builder()
98            .inheritdoc(false)
99            .structs(WithParamsRules::required())
100            .build()
101    });
102
103    fn parse_file(contents: &str) -> StructDefinition {
104        let mut parser = SolarParser::default();
105        let doc = parser
106            .parse_document(contents.as_bytes(), None::<std::path::PathBuf>, false)
107            .unwrap();
108        doc.definitions
109            .into_iter()
110            .find_map(Definition::to_struct)
111            .unwrap()
112    }
113
114    #[test]
115    fn test_struct() {
116        let contents = "contract Test {
117            /// @notice A struct
118            struct Foobar {
119                uint256 a;
120                bool b;
121            }
122        }";
123        let res =
124            parse_file(contents).validate(&ValidationOptions::builder().inheritdoc(false).build());
125        assert!(res.diags.is_empty(), "{:#?}", res.diags);
126    }
127
128    #[test]
129    fn test_struct_missing() {
130        let contents = "contract Test {
131            struct Foobar {
132                uint256 a;
133                bool b;
134            }
135        }";
136        let res = parse_file(contents).validate(&OPTIONS);
137        assert_eq!(res.diags.len(), 3);
138        assert_eq!(res.diags[0].message, "@notice is missing");
139        assert_eq!(res.diags[1].message, "@param a is missing");
140        assert_eq!(res.diags[2].message, "@param b is missing");
141    }
142
143    #[test]
144    fn test_struct_params() {
145        let contents = "contract Test {
146            /// @notice A struct
147            /// @param a The first
148            /// @param b The second
149            struct Foobar {
150                uint256 a;
151                bool b;
152            }
153        }";
154        let res = parse_file(contents).validate(&OPTIONS);
155        assert!(res.diags.is_empty(), "{:#?}", res.diags);
156    }
157
158    #[test]
159    fn test_struct_only_notice() {
160        let contents = "contract Test {
161            /// @notice A struct
162            struct Foobar {
163                uint256 a;
164                bool b;
165            }
166        }";
167        let res = parse_file(contents).validate(&OPTIONS);
168        assert_eq!(res.diags.len(), 2);
169        assert_eq!(res.diags[0].message, "@param a is missing");
170        assert_eq!(res.diags[1].message, "@param b is missing");
171    }
172
173    #[test]
174    fn test_struct_one_missing() {
175        let contents = "contract Test {
176            /// @notice A struct
177            /// @param a The first
178            struct Foobar {
179                uint256 a;
180                bool b;
181            }
182        }";
183        let res = parse_file(contents).validate(&OPTIONS);
184        assert_eq!(res.diags.len(), 1);
185        assert_eq!(res.diags[0].message, "@param b is missing");
186    }
187
188    #[test]
189    fn test_struct_multiline() {
190        let contents = "contract Test {
191            /**
192             * @notice A struct
193             * @param a The first
194             * @param b The second
195             */
196            struct Foobar {
197                uint256 a;
198                bool b;
199            }
200        }";
201        let res = parse_file(contents).validate(&OPTIONS);
202        assert!(res.diags.is_empty(), "{:#?}", res.diags);
203    }
204
205    #[test]
206    fn test_struct_duplicate() {
207        let contents = "contract Test {
208            /// @notice A struct
209            /// @param a The first
210            /// @param a The first twice
211            struct Foobar {
212                uint256 a;
213            }
214        }";
215        let res = parse_file(contents).validate(&OPTIONS);
216        assert_eq!(res.diags.len(), 1);
217        assert_eq!(res.diags[0].message, "@param a is present more than once");
218    }
219
220    #[test]
221    fn test_struct_inheritdoc() {
222        // inheritdoc should be ignored as it doesn't apply to structs
223        let contents = "contract Test {
224            /// @inheritdoc ISomething
225            struct Foobar {
226                uint256 a;
227            }
228        }";
229        let res = parse_file(contents).validate(
230            &ValidationOptions::builder()
231                .inheritdoc(true)
232                .structs(WithParamsRules::required())
233                .build(),
234        );
235        assert_eq!(res.diags.len(), 2);
236        assert_eq!(res.diags[0].message, "@notice is missing");
237        assert_eq!(res.diags[1].message, "@param a is missing");
238    }
239
240    #[test]
241    fn test_struct_no_contract() {
242        let contents = "
243            /// @notice A struct
244            /// @param a The first
245            /// @param b The second
246            struct Foobar {
247                uint256 a;
248                bool b;
249            }";
250        let res = parse_file(contents).validate(&OPTIONS);
251        assert!(res.diags.is_empty(), "{:#?}", res.diags);
252    }
253
254    #[test]
255    fn test_struct_no_contract_missing() {
256        let contents = "struct Foobar {
257                uint256 a;
258                bool b;
259            }";
260        let res = parse_file(contents).validate(&OPTIONS);
261        assert_eq!(res.diags.len(), 3);
262        assert_eq!(res.diags[0].message, "@notice is missing");
263        assert_eq!(res.diags[1].message, "@param a is missing");
264        assert_eq!(res.diags[2].message, "@param b is missing");
265    }
266
267    #[test]
268    fn test_struct_no_contract_one_missing() {
269        let contents = "
270            /// @notice A struct
271            /// @param a The first
272            struct Foobar {
273                uint256 a;
274                bool b;
275            }";
276        let res = parse_file(contents).validate(&OPTIONS);
277        assert_eq!(res.diags.len(), 1);
278        assert_eq!(res.diags[0].message, "@param b is missing");
279    }
280
281    #[test]
282    fn test_struct_missing_space() {
283        let contents = "
284            /// @notice A struct
285            /// @param fooThe param
286            struct Test {
287                uint256 foo;
288            }";
289        let res = parse_file(contents).validate(&OPTIONS);
290        assert_eq!(res.diags.len(), 2);
291        assert_eq!(res.diags[0].message, "extra @param fooThe");
292        assert_eq!(res.diags[1].message, "@param foo is missing");
293    }
294
295    #[test]
296    fn test_struct_extra_param() {
297        let contents = "
298            /// @notice A struct
299            /// @param foo The param
300            /// @param bar Some other param
301            struct Test {
302                uint256 foo;
303            }";
304        let res = parse_file(contents).validate(&OPTIONS);
305        assert_eq!(res.diags.len(), 1);
306        assert_eq!(res.diags[0].message, "extra @param bar");
307    }
308}