lintspec_core/definitions/
function.rs1use crate::{
3 lint::{CheckNoticeAndDev, CheckParams, CheckReturns, Diagnostic, ItemDiagnostics},
4 natspec::{NatSpec, NatSpecKind},
5};
6
7use super::{
8 Attributes, Identifier, ItemType, Parent, SourceItem, TextRange, Validate, ValidationOptions,
9 Visibility,
10};
11
12#[derive(Debug, Clone, bon::Builder)]
14#[non_exhaustive]
15#[builder(on(String, into))]
16pub struct FunctionDefinition {
17 pub parent: Option<Parent>,
19
20 pub name: String,
22
23 pub span: TextRange,
25
26 pub params: Vec<Identifier>,
28
29 pub returns: Vec<Identifier>,
31
32 pub natspec: Option<NatSpec>,
34
35 pub attributes: Attributes,
37}
38
39impl FunctionDefinition {
40 fn requires_inheritdoc(&self, options: &ValidationOptions) -> bool {
44 let parent_is_contract = matches!(self.parent, Some(Parent::Contract(_)));
45 let internal_override =
46 self.attributes.visibility == Visibility::Internal && self.attributes.r#override;
47 let public_external = matches!(
48 self.attributes.visibility,
49 Visibility::External | Visibility::Public
50 );
51 ((options.inheritdoc && public_external)
52 || (options.inheritdoc_override && internal_override))
53 && parent_is_contract
54 }
55}
56
57impl SourceItem for FunctionDefinition {
58 fn item_type(&self) -> ItemType {
59 match self.attributes.visibility {
60 Visibility::External => ItemType::ExternalFunction,
61 Visibility::Internal => ItemType::InternalFunction,
62 Visibility::Private => ItemType::PrivateFunction,
63 Visibility::Public => ItemType::PublicFunction,
64 }
65 }
66
67 fn parent(&self) -> Option<Parent> {
68 self.parent.clone()
69 }
70
71 fn name(&self) -> String {
72 self.name.clone()
73 }
74
75 fn span(&self) -> TextRange {
76 self.span.clone()
77 }
78}
79
80impl Validate for FunctionDefinition {
81 fn validate(&self, options: &ValidationOptions) -> ItemDiagnostics {
82 let mut out = ItemDiagnostics {
83 parent: self.parent(),
84 item_type: self.item_type(),
85 name: self.name(),
86 span: self.span(),
87 diags: vec![],
88 };
89 if self.name == "receive" || self.name == "fallback" {
91 return out;
92 }
93 let opts = match self.attributes.visibility {
94 Visibility::External => options.functions.external,
95 Visibility::Internal => options.functions.internal,
96 Visibility::Private => options.functions.private,
97 Visibility::Public => options.functions.public,
98 };
99 if let Some(natspec) = &self.natspec
100 && natspec
101 .items
102 .iter()
103 .any(|n| matches!(n.kind, NatSpecKind::Inheritdoc { .. }))
104 {
105 return out;
107 } else if self.requires_inheritdoc(options) {
108 out.diags.push(Diagnostic {
109 span: self.span(),
110 message: "@inheritdoc is missing".to_string(),
111 });
112 return out;
113 }
114 out.diags.extend(
115 CheckNoticeAndDev::builder()
116 .natspec(&self.natspec)
117 .notice_rule(opts.notice)
118 .dev_rule(opts.dev)
119 .notice_or_dev(options.notice_or_dev)
120 .span(&self.span)
121 .build()
122 .check(),
123 );
124 out.diags.extend(
125 CheckParams::builder()
126 .natspec(&self.natspec)
127 .rule(opts.param)
128 .params(&self.params)
129 .default_span(self.span())
130 .build()
131 .check(),
132 );
133 out.diags.extend(
134 CheckReturns::builder()
135 .natspec(&self.natspec)
136 .rule(opts.returns)
137 .returns(&self.returns)
138 .default_span(self.span())
139 .is_var(false)
140 .build()
141 .check(),
142 );
143 out
144 }
145}
146
147#[cfg(test)]
148#[cfg(feature = "solar")]
149mod tests {
150 use std::sync::LazyLock;
151
152 use similar_asserts::assert_eq;
153
154 use crate::{
155 definitions::Definition,
156 parser::{Parse as _, solar::SolarParser},
157 };
158
159 use super::*;
160
161 static OPTIONS: LazyLock<ValidationOptions> =
162 LazyLock::new(|| ValidationOptions::builder().inheritdoc(false).build());
163
164 fn parse_file(contents: &str) -> FunctionDefinition {
165 let mut parser = SolarParser::default();
166 let doc = parser
167 .parse_document(contents.as_bytes(), None::<std::path::PathBuf>, false)
168 .unwrap();
169 doc.definitions
170 .into_iter()
171 .find_map(Definition::to_function)
172 .unwrap()
173 }
174
175 #[test]
176 fn test_function() {
177 let contents = "contract Test {
178 /// @notice A function
179 /// @param param1 Test
180 /// @param param2 Test2
181 /// @return First output
182 /// @return out Second output
183 function foo(uint256 param1, bytes calldata param2) public returns (uint256, uint256 out) { }
184 }";
185 let res = parse_file(contents).validate(&OPTIONS);
186 assert!(res.diags.is_empty(), "{:#?}", res.diags);
187 }
188
189 #[test]
190 fn test_function_no_natspec() {
191 let contents = "contract Test {
192 function foo(uint256 param1, bytes calldata param2) public returns (uint256, uint256 out) { }
193 }";
194 let res = parse_file(contents).validate(&OPTIONS);
195 assert_eq!(res.diags.len(), 5);
196 assert_eq!(res.diags[0].message, "@notice is missing");
197 assert_eq!(res.diags[1].message, "@param param1 is missing");
198 assert_eq!(res.diags[2].message, "@param param2 is missing");
199 assert_eq!(
200 res.diags[3].message,
201 "@return missing for unnamed return #1"
202 );
203 assert_eq!(res.diags[4].message, "@return out is missing");
204 }
205
206 #[test]
207 fn test_function_only_notice() {
208 let contents = "contract Test {
209 /// @notice The function
210 function foo(uint256 param1, bytes calldata param2) public returns (uint256, uint256 out) { }
211 }";
212 let res = parse_file(contents).validate(&OPTIONS);
213 assert_eq!(res.diags.len(), 4);
214 assert_eq!(res.diags[0].message, "@param param1 is missing");
215 assert_eq!(res.diags[1].message, "@param param2 is missing");
216 assert_eq!(
217 res.diags[2].message,
218 "@return missing for unnamed return #1"
219 );
220 assert_eq!(res.diags[3].message, "@return out is missing");
221 }
222
223 #[test]
224 fn test_function_one_missing() {
225 let contents = "contract Test {
226 /// @notice A function
227 /// @param param1 The first
228 function foo(uint256 param1, bytes calldata param2) public { }
229 }";
230 let res = parse_file(contents).validate(&OPTIONS);
231 assert_eq!(res.diags.len(), 1);
232 assert_eq!(res.diags[0].message, "@param param2 is missing");
233 }
234
235 #[test]
236 fn test_function_multiline() {
237 let contents = "contract Test {
238 /**
239 * @notice A function
240 * @param param1 Test
241 * @param param2 Test2
242 */
243 function foo(uint256 param1, bytes calldata param2) public { }
244 }";
245 let res = parse_file(contents).validate(&OPTIONS);
246 assert!(res.diags.is_empty(), "{:#?}", res.diags);
247 }
248
249 #[test]
250 fn test_function_duplicate() {
251 let contents = "contract Test {
252 /// @notice A function
253 /// @param param1 The first
254 /// @param param1 The first again
255 function foo(uint256 param1) public { }
256 }";
257 let res = parse_file(contents).validate(&OPTIONS);
258 assert_eq!(res.diags.len(), 1);
259 assert_eq!(
260 res.diags[0].message,
261 "@param param1 is present more than once"
262 );
263 }
264
265 #[test]
266 fn test_function_duplicate_return() {
267 let contents = "contract Test {
268 /// @notice A function
269 /// @return out The output
270 /// @return out The output again
271 function foo() public returns (uint256 out) { }
272 }";
273 let res = parse_file(contents).validate(&OPTIONS);
274 assert_eq!(res.diags.len(), 1);
275 assert_eq!(
276 res.diags[0].message,
277 "@return out is present more than once"
278 );
279 }
280
281 #[test]
282 fn test_function_duplicate_unnamed_return() {
283 let contents = "contract Test {
284 /// @notice A function
285 /// @return The output
286 /// @return The output again
287 function foo() public returns (uint256) { }
288 }";
289 let res = parse_file(contents).validate(&OPTIONS);
290 assert_eq!(res.diags.len(), 1);
291 assert_eq!(res.diags[0].message, "too many unnamed returns");
292 }
293
294 #[test]
295 fn test_function_no_params() {
296 let contents = "contract Test {
297 function foo() public { }
298 }";
299 let res = parse_file(contents).validate(&OPTIONS);
300 assert_eq!(res.diags.len(), 1);
301 assert_eq!(res.diags[0].message, "@notice is missing");
302 }
303
304 #[test]
305 fn test_requires_inheritdoc() {
306 let options = ValidationOptions::builder()
307 .inheritdoc(true)
308 .inheritdoc_override(true)
309 .build();
310 let contents = "contract Test is ITest {
311 function a() internal returns (uint256) { }
312 }";
313 let res = parse_file(contents);
314 assert!(!res.requires_inheritdoc(&options));
315
316 let contents = "contract Test is ITest {
317 function b() private returns (uint256) { }
318 }";
319 let res = parse_file(contents);
320 assert!(!res.requires_inheritdoc(&options));
321
322 let contents = "contract Test is ITest {
323 function c() external returns (uint256) { }
324 }";
325 let res = parse_file(contents);
326 assert!(res.requires_inheritdoc(&options));
327 assert!(!res.requires_inheritdoc(&ValidationOptions::builder().inheritdoc(false).build()));
328
329 let contents = "contract Test is ITest {
330 function d() public returns (uint256) { }
331 }";
332 let res = parse_file(contents);
333 assert!(res.requires_inheritdoc(&options));
334 assert!(!res.requires_inheritdoc(&ValidationOptions::builder().inheritdoc(false).build()));
335
336 let contents = "contract Test is ITest {
337 function e() internal override (ITest) returns (uint256) { }
338 }";
339 let res = parse_file(contents);
340 assert!(res.requires_inheritdoc(&options));
341 assert!(!res.requires_inheritdoc(&ValidationOptions::default()));
342 }
343
344 #[test]
345 fn test_function_inheritdoc() {
346 let contents = "contract Test is ITest {
347 /// @inheritdoc ITest
348 function foo() external { }
349 }";
350 let res = parse_file(contents).validate(&ValidationOptions::default());
351 assert!(res.diags.is_empty(), "{:#?}", res.diags);
352 }
353
354 #[test]
355 fn test_function_inheritdoc_missing() {
356 let contents = "contract Test is ITest {
357 /// @notice Test
358 function foo() external { }
359 }";
360 let res = parse_file(contents).validate(&ValidationOptions::default());
361 assert_eq!(res.diags.len(), 1);
362 assert_eq!(res.diags[0].message, "@inheritdoc is missing");
363 }
364}