lintspec_core/definitions/
structure.rs1use crate::{
3 lint::{CheckNoticeAndDev, CheckParams, ItemDiagnostics},
4 natspec::NatSpec,
5};
6
7use super::{Identifier, ItemType, Parent, SourceItem, TextRange, Validate, ValidationOptions};
8
9#[derive(Debug, Clone, bon::Builder)]
11#[non_exhaustive]
12#[builder(on(String, into))]
13pub struct StructDefinition {
14 pub parent: Option<Parent>,
16
17 pub name: String,
19
20 pub span: TextRange,
22
23 pub members: Vec<Identifier>,
25
26 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 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}