Skip to main content

lintspec_core/definitions/
function.rs

1//! Parsing and validation of function definitions.
2use 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/// 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
41    ///
42    /// Public/external functions as well as internal override functions require @inheritdoc if enforced.
43    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        // fallback and receive do not require NatSpec
90        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            // if there is `inheritdoc`, no further validation is required
106            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}