Skip to main content

lintspec_core/definitions/
structure.rs

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