lintspec/definitions/
function.rs

1//! Parsing and validation of function definitions.
2use 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/// A function definition
13#[derive(Debug, Clone, bon::Builder)]
14#[non_exhaustive]
15#[builder(on(String, into))]
16pub struct FunctionDefinition {
17    /// The parent for the function definition (should always be `Some`)
18    pub parent: Option<Parent>,
19
20    /// The name of the function
21    pub name: String,
22
23    /// The span of the function definition, exluding the body
24    pub span: TextRange,
25
26    /// The name and span of the function's parameters
27    pub params: Vec<Identifier>,
28
29    /// The name and span of the function's returns
30    pub returns: Vec<Identifier>,
31
32    /// The [`NatSpec`] associated with the function definition, if any
33    pub natspec: Option<NatSpec>,
34
35    /// The attributes of the function (visibility and override)
36    pub attributes: Attributes,
37}
38
39impl FunctionDefinition {
40    /// Check whether this function requires inheritdoc when we enforce it
41    ///
42    /// External and public functions, as well as overridden internal functions must have inheritdoc.
43    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        // fallback and receive do not require NatSpec
88        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 there is `inheritdoc`, no further validation is required
99            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}