lintspec/definitions/
structure.rs

1//! Parsing and validation of struct definitions.
2use crate::{
3    lint::{check_notice_and_dev, check_params, 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(check_notice_and_dev(
59            &self.natspec,
60            opts.notice,
61            opts.dev,
62            options.notice_or_dev,
63            self.span(),
64        ));
65        out.diags.extend(check_params(
66            &self.natspec,
67            opts.param,
68            &self.members,
69            self.span(),
70        ));
71        out
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use std::sync::LazyLock;
78
79    use semver::Version;
80    use similar_asserts::assert_eq;
81    use slang_solidity::{cst::NonterminalKind, parser::Parser};
82
83    use crate::{config::WithParamsRules, parser::slang::Extract as _};
84
85    use super::*;
86
87    static OPTIONS: LazyLock<ValidationOptions> = LazyLock::new(|| {
88        ValidationOptions::builder()
89            .inheritdoc(false)
90            .structs(WithParamsRules::required())
91            .build()
92    });
93
94    fn parse_file(contents: &str) -> StructDefinition {
95        let parser = Parser::create(Version::new(0, 8, 0)).unwrap();
96        let output = parser.parse(NonterminalKind::SourceUnit, contents);
97        assert!(output.is_valid(), "{:?}", output.errors());
98        let cursor = output.create_tree_cursor();
99        let m = cursor
100            .query(vec![StructDefinition::query()])
101            .next()
102            .unwrap();
103        let def = StructDefinition::extract(m).unwrap();
104        def.to_struct().unwrap()
105    }
106
107    #[test]
108    fn test_struct() {
109        let contents = "contract Test {
110            /// @notice A struct
111            struct Foobar {
112                uint256 a;
113                bool b;
114            }
115        }";
116        let res =
117            parse_file(contents).validate(&ValidationOptions::builder().inheritdoc(false).build());
118        assert!(res.diags.is_empty(), "{:#?}", res.diags);
119    }
120
121    #[test]
122    fn test_struct_missing() {
123        let contents = "contract Test {
124            struct Foobar {
125                uint256 a;
126                bool b;
127            }
128        }";
129        let res = parse_file(contents).validate(&OPTIONS);
130        assert_eq!(res.diags.len(), 3);
131        assert_eq!(res.diags[0].message, "@notice is missing");
132        assert_eq!(res.diags[1].message, "@param a is missing");
133        assert_eq!(res.diags[2].message, "@param b is missing");
134    }
135
136    #[test]
137    fn test_struct_params() {
138        let contents = "contract Test {
139            /// @notice A struct
140            /// @param a The first
141            /// @param b The second
142            struct Foobar {
143                uint256 a;
144                bool b;
145            }
146        }";
147        let res = parse_file(contents).validate(&OPTIONS);
148        assert!(res.diags.is_empty(), "{:#?}", res.diags);
149    }
150
151    #[test]
152    fn test_struct_only_notice() {
153        let contents = "contract Test {
154            /// @notice A struct
155            struct Foobar {
156                uint256 a;
157                bool b;
158            }
159        }";
160        let res = parse_file(contents).validate(&OPTIONS);
161        assert_eq!(res.diags.len(), 2);
162        assert_eq!(res.diags[0].message, "@param a is missing");
163        assert_eq!(res.diags[1].message, "@param b is missing");
164    }
165
166    #[test]
167    fn test_struct_one_missing() {
168        let contents = "contract Test {
169            /// @notice A struct
170            /// @param a The first
171            struct Foobar {
172                uint256 a;
173                bool b;
174            }
175        }";
176        let res = parse_file(contents).validate(&OPTIONS);
177        assert_eq!(res.diags.len(), 1);
178        assert_eq!(res.diags[0].message, "@param b is missing");
179    }
180
181    #[test]
182    fn test_struct_multiline() {
183        let contents = "contract Test {
184            /**
185             * @notice A struct
186             * @param a The first
187             * @param b The second
188             */
189            struct Foobar {
190                uint256 a;
191                bool b;
192            }
193        }";
194        let res = parse_file(contents).validate(&OPTIONS);
195        assert!(res.diags.is_empty(), "{:#?}", res.diags);
196    }
197
198    #[test]
199    fn test_struct_duplicate() {
200        let contents = "contract Test {
201            /// @notice A struct
202            /// @param a The first
203            /// @param a The first twice
204            struct Foobar {
205                uint256 a;
206            }
207        }";
208        let res = parse_file(contents).validate(&OPTIONS);
209        assert_eq!(res.diags.len(), 1);
210        assert_eq!(res.diags[0].message, "@param a is present more than once");
211    }
212
213    #[test]
214    fn test_struct_inheritdoc() {
215        // inheritdoc should be ignored as it doesn't apply to structs
216        let contents = "contract Test {
217            /// @inheritdoc ISomething
218            struct Foobar {
219                uint256 a;
220            }
221        }";
222        let res = parse_file(contents).validate(
223            &ValidationOptions::builder()
224                .inheritdoc(true)
225                .structs(WithParamsRules::required())
226                .build(),
227        );
228        assert_eq!(res.diags.len(), 2);
229        assert_eq!(res.diags[0].message, "@notice is missing");
230        assert_eq!(res.diags[1].message, "@param a is missing");
231    }
232
233    #[test]
234    fn test_struct_no_contract() {
235        let contents = "
236            /// @notice A struct
237            /// @param a The first
238            /// @param b The second
239            struct Foobar {
240                uint256 a;
241                bool b;
242            }";
243        let res = parse_file(contents).validate(&OPTIONS);
244        assert!(res.diags.is_empty(), "{:#?}", res.diags);
245    }
246
247    #[test]
248    fn test_struct_no_contract_missing() {
249        let contents = "struct Foobar {
250                uint256 a;
251                bool b;
252            }";
253        let res = parse_file(contents).validate(&OPTIONS);
254        assert_eq!(res.diags.len(), 3);
255        assert_eq!(res.diags[0].message, "@notice is missing");
256        assert_eq!(res.diags[1].message, "@param a is missing");
257        assert_eq!(res.diags[2].message, "@param b is missing");
258    }
259
260    #[test]
261    fn test_struct_no_contract_one_missing() {
262        let contents = "
263            /// @notice A struct
264            /// @param a The first
265            struct Foobar {
266                uint256 a;
267                bool b;
268            }";
269        let res = parse_file(contents).validate(&OPTIONS);
270        assert_eq!(res.diags.len(), 1);
271        assert_eq!(res.diags[0].message, "@param b is missing");
272    }
273
274    #[test]
275    fn test_struct_missing_space() {
276        let contents = "
277            /// @notice A struct
278            /// @param fooThe param
279            struct Test {
280                uint256 foo;
281            }";
282        let res = parse_file(contents).validate(&OPTIONS);
283        assert_eq!(res.diags.len(), 1);
284        assert_eq!(res.diags[0].message, "@param foo is missing");
285    }
286}