Skip to main content

lintspec_core/definitions/
function.rs

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