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