lintspec_core/definitions/
structure.rs1use 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#[derive(Debug, Clone, bon::Builder)]
12#[non_exhaustive]
13#[builder(on(String, into))]
14pub struct StructDefinition {
15 pub parent: Option<Parent>,
17
18 pub name: Symbol,
20
21 pub span: TextRange,
23
24 pub members: Vec<Identifier>,
26
27 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 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}