1use crate::{
3 lint::{check_notice_and_dev, check_params, check_returns, 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) -> 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 parent_is_contract && (internal_override || public_external)
52 }
53}
54
55impl SourceItem for FunctionDefinition {
56 fn item_type(&self) -> ItemType {
57 match self.attributes.visibility {
58 Visibility::External => ItemType::ExternalFunction,
59 Visibility::Internal => ItemType::InternalFunction,
60 Visibility::Private => ItemType::PrivateFunction,
61 Visibility::Public => ItemType::PublicFunction,
62 }
63 }
64
65 fn parent(&self) -> Option<Parent> {
66 self.parent.clone()
67 }
68
69 fn name(&self) -> String {
70 self.name.clone()
71 }
72
73 fn span(&self) -> TextRange {
74 self.span.clone()
75 }
76}
77
78impl Validate for FunctionDefinition {
79 fn validate(&self, options: &ValidationOptions) -> ItemDiagnostics {
80 let mut out = ItemDiagnostics {
81 parent: self.parent(),
82 item_type: self.item_type(),
83 name: self.name(),
84 span: self.span(),
85 diags: vec![],
86 };
87 if self.name == "receive" || self.name == "fallback" {
89 return out;
90 }
91 let opts = match self.attributes.visibility {
92 Visibility::External => options.functions.external,
93 Visibility::Internal => options.functions.internal,
94 Visibility::Private => options.functions.private,
95 Visibility::Public => options.functions.public,
96 };
97 if let Some(natspec) = &self.natspec {
98 if natspec
100 .items
101 .iter()
102 .any(|n| matches!(n.kind, NatSpecKind::Inheritdoc { .. }))
103 {
104 return out;
105 } else if options.inheritdoc && self.requires_inheritdoc() {
106 out.diags.push(Diagnostic {
107 span: self.span(),
108 message: "@inheritdoc is missing".to_string(),
109 });
110 return out;
111 }
112 } else if options.inheritdoc && self.requires_inheritdoc() {
113 out.diags.push(Diagnostic {
114 span: self.span(),
115 message: "@inheritdoc is missing".to_string(),
116 });
117 return out;
118 }
119 out.diags.extend(check_notice_and_dev(
120 &self.natspec,
121 opts.notice,
122 opts.dev,
123 options.notice_or_dev,
124 self.span(),
125 ));
126 out.diags.extend(check_params(
127 &self.natspec,
128 opts.param,
129 &self.params,
130 self.span(),
131 ));
132 out.diags.extend(check_returns(
133 &self.natspec,
134 opts.returns,
135 &self.returns,
136 self.span(),
137 false,
138 ));
139 out
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use std::sync::LazyLock;
146
147 use semver::Version;
148 use similar_asserts::assert_eq;
149 use slang_solidity::{cst::NonterminalKind, parser::Parser};
150
151 use crate::parser::slang::Extract as _;
152
153 use super::*;
154
155 static OPTIONS: LazyLock<ValidationOptions> =
156 LazyLock::new(|| ValidationOptions::builder().inheritdoc(false).build());
157
158 fn parse_file(contents: &str) -> FunctionDefinition {
159 let parser = Parser::create(Version::new(0, 8, 0)).unwrap();
160 let output = parser.parse(NonterminalKind::SourceUnit, contents);
161 assert!(output.is_valid(), "{:?}", output.errors());
162 let cursor = output.create_tree_cursor();
163 let m = cursor
164 .query(vec![FunctionDefinition::query()])
165 .next()
166 .unwrap();
167 let def = FunctionDefinition::extract(m).unwrap();
168 def.to_function().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 contents = "contract Test is ITest {
303 function a() internal returns (uint256) { }
304 }";
305 let res = parse_file(contents);
306 assert!(!res.requires_inheritdoc());
307
308 let contents = "contract Test is ITest {
309 function b() private returns (uint256) { }
310 }";
311 let res = parse_file(contents);
312 assert!(!res.requires_inheritdoc());
313
314 let contents = "contract Test is ITest {
315 function c() external returns (uint256) { }
316 }";
317 let res = parse_file(contents);
318 assert!(res.requires_inheritdoc());
319
320 let contents = "contract Test is ITest {
321 function d() public returns (uint256) { }
322 }";
323 let res = parse_file(contents);
324 assert!(res.requires_inheritdoc());
325
326 let contents = "contract Test is ITest {
327 function e() internal override (ITest) returns (uint256) { }
328 }";
329 let res = parse_file(contents);
330 assert!(res.requires_inheritdoc());
331 }
332
333 #[test]
334 fn test_function_inheritdoc() {
335 let contents = "contract Test is ITest {
336 /// @inheritdoc ITest
337 function foo() external { }
338 }";
339 let res = parse_file(contents).validate(&ValidationOptions::default());
340 assert!(res.diags.is_empty(), "{:#?}", res.diags);
341 }
342
343 #[test]
344 fn test_function_inheritdoc_missing() {
345 let contents = "contract Test is ITest {
346 /// @notice Test
347 function foo() external { }
348 }";
349 let res = parse_file(contents).validate(&ValidationOptions::default());
350 assert_eq!(res.diags.len(), 1);
351 assert_eq!(res.diags[0].message, "@inheritdoc is missing");
352 }
353}