lintspec/definitions/
structure.rs1use 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#[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(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 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}