Skip to main content

lintspec_core/definitions/
modifier.rs

1//! Parsing and validation of modifier definitions.
2use crate::{
3    interner::{INTERNER, Symbol},
4    lint::{CheckNoticeAndDev, CheckParams, Diagnostic, ItemDiagnostics},
5    natspec::{NatSpec, NatSpecKind},
6};
7
8use super::{
9    Attributes, Identifier, ItemType, Parent, SourceItem, TextRange, Validate, ValidationOptions,
10};
11
12/// A modifier definition
13#[derive(Debug, Clone, bon::Builder)]
14#[non_exhaustive]
15#[builder(on(String, into))]
16pub struct ModifierDefinition {
17    /// The parent for the modifier definition (should always be `Some`)
18    pub parent: Option<Parent>,
19
20    /// The name of the modifier
21    pub name: Symbol,
22
23    /// The span of the modifier definition, exluding the body
24    pub span: TextRange,
25
26    /// The name and span of the modifier's parameters
27    pub params: Vec<Identifier>,
28
29    /// The [`NatSpec`] associated with the modifier definition, if any
30    pub natspec: Option<NatSpec>,
31
32    /// The attributes of the modifier (override)
33    pub attributes: Attributes,
34}
35
36impl ModifierDefinition {
37    /// Check whether this modifier requires inheritdoc when we enforce it
38    ///
39    /// `override` modifiers must have inheritdoc.
40    fn requires_inheritdoc(&self, options: &ValidationOptions) -> bool {
41        let parent_is_contract = self.parent.as_ref().is_some_and(Parent::is_contract);
42        options.inheritdoc_override && self.attributes.r#override && parent_is_contract
43    }
44}
45
46impl SourceItem for ModifierDefinition {
47    fn item_type(&self) -> ItemType {
48        ItemType::Modifier
49    }
50
51    fn parent(&self) -> Option<Parent> {
52        self.parent.clone()
53    }
54
55    fn name(&self) -> Symbol {
56        self.name
57    }
58
59    fn span(&self) -> TextRange {
60        self.span.clone()
61    }
62}
63
64impl Validate for ModifierDefinition {
65    fn validate(&self, options: &ValidationOptions) -> ItemDiagnostics {
66        let opts = &options.modifiers;
67        let mut out = ItemDiagnostics {
68            parent: self.parent(),
69            item_type: self.item_type(),
70            name: self.name().resolve_with(&INTERNER),
71            span: self.span(),
72            diags: vec![],
73        };
74        if let Some(natspec) = &self.natspec
75            && natspec
76                .items
77                .iter()
78                .any(|n| matches!(n.kind, NatSpecKind::Inheritdoc { .. }))
79        {
80            // if there is `inheritdoc`, no further validation is required
81            return out;
82        } else if self.requires_inheritdoc(options) {
83            out.diags.push(Diagnostic {
84                span: self.span(),
85                message: "@inheritdoc is missing".to_string(),
86            });
87            return out;
88        }
89        CheckNoticeAndDev::builder()
90            .natspec(&self.natspec)
91            .notice_rule(opts.notice)
92            .dev_rule(opts.dev)
93            .notice_or_dev(options.notice_or_dev)
94            .span(&self.span)
95            .build()
96            .check_into(&mut out.diags);
97        CheckParams::builder()
98            .natspec(&self.natspec)
99            .rule(opts.param)
100            .params(&self.params)
101            .default_span(self.span())
102            .build()
103            .check_into(&mut out.diags);
104        out
105    }
106}
107
108#[cfg(test)]
109#[cfg(feature = "solar")]
110mod tests {
111    use std::sync::LazyLock;
112
113    use similar_asserts::assert_eq;
114
115    use crate::{
116        definitions::Definition,
117        parser::{Parse as _, solar::SolarParser},
118    };
119
120    use super::*;
121
122    static OPTIONS: LazyLock<ValidationOptions> =
123        LazyLock::new(|| ValidationOptions::builder().inheritdoc(false).build());
124
125    fn parse_file(contents: &str) -> ModifierDefinition {
126        let mut parser = SolarParser::default();
127        let doc = parser
128            .parse_document(contents.as_bytes(), None::<std::path::PathBuf>, false)
129            .unwrap();
130        doc.definitions
131            .into_iter()
132            .find_map(Definition::to_modifier)
133            .unwrap()
134    }
135
136    #[test]
137    fn test_modifier() {
138        let contents = "contract Test {
139            /// @notice A modifier
140            /// @param param1 Test
141            /// @param param2 Test2
142            modifier foo(uint256 param1, bytes calldata param2) { _; }
143        }";
144        let res = parse_file(contents).validate(&OPTIONS);
145        assert!(res.diags.is_empty(), "{:#?}", res.diags);
146    }
147
148    #[test]
149    fn test_modifier_no_natspec() {
150        let contents = "contract Test {
151            modifier foo(uint256 param1, bytes calldata param2) { _; }
152        }";
153        let res = parse_file(contents).validate(&OPTIONS);
154        assert_eq!(res.diags.len(), 3);
155        assert_eq!(res.diags[0].message, "@notice is missing");
156        assert_eq!(res.diags[1].message, "@param param1 is missing");
157        assert_eq!(res.diags[2].message, "@param param2 is missing");
158    }
159
160    #[test]
161    fn test_modifier_only_notice() {
162        let contents = "contract Test {
163            /// @notice The modifier
164            modifier foo(uint256 param1, bytes calldata param2) { _; }
165        }";
166        let res = parse_file(contents).validate(&OPTIONS);
167        assert_eq!(res.diags.len(), 2);
168        assert_eq!(res.diags[0].message, "@param param1 is missing");
169        assert_eq!(res.diags[1].message, "@param param2 is missing");
170    }
171
172    #[test]
173    fn test_modifier_one_missing() {
174        let contents = "contract Test {
175            /// @notice A modifier
176            /// @param param1 The first
177            modifier foo(uint256 param1, bytes calldata param2) { _; }
178        }";
179        let res = parse_file(contents).validate(&OPTIONS);
180        assert_eq!(res.diags.len(), 1);
181        assert_eq!(res.diags[0].message, "@param param2 is missing");
182    }
183
184    #[test]
185    fn test_modifier_multiline() {
186        let contents = "contract Test {
187            /**
188             * @notice A modifier
189             * @param param1 Test
190             * @param param2 Test2
191             */
192            modifier foo(uint256 param1, bytes calldata param2) { _; }
193        }";
194        let res = parse_file(contents).validate(&OPTIONS);
195        assert!(res.diags.is_empty(), "{:#?}", res.diags);
196    }
197
198    #[test]
199    fn test_modifier_duplicate() {
200        let contents = "contract Test {
201            /// @notice A modifier
202            /// @param param1 The first
203            /// @param param1 The first again
204            modifier foo(uint256 param1) { _; }
205        }";
206        let res = parse_file(contents).validate(&OPTIONS);
207        assert_eq!(res.diags.len(), 1);
208        assert_eq!(
209            res.diags[0].message,
210            "@param param1 is present more than once"
211        );
212    }
213
214    #[test]
215    fn test_modifier_no_params() {
216        let contents = "contract Test {
217            /// @notice A modifier
218            modifier foo()  { _; }
219        }";
220        let res = parse_file(contents).validate(&OPTIONS);
221        assert!(res.diags.is_empty(), "{:#?}", res.diags);
222    }
223
224    #[test]
225    fn test_modifier_no_params_no_paren() {
226        let contents = "contract Test {
227            /// @notice A modifier
228            modifier foo { _; }
229        }";
230        let res = parse_file(contents).validate(&OPTIONS);
231        assert!(res.diags.is_empty(), "{:#?}", res.diags);
232    }
233
234    #[test]
235    fn test_requires_inheritdoc() {
236        let options = ValidationOptions::builder()
237            .inheritdoc_override(true)
238            .build();
239
240        let contents = "contract Test is ITest {
241            modifier a() { _; }
242        }";
243        let res = parse_file(contents);
244        assert!(!res.requires_inheritdoc(&options));
245        assert!(!res.requires_inheritdoc(&ValidationOptions::default()));
246
247        let contents = "contract Test is ITest {
248            modifier e() override (ITest) { _; }
249        }";
250        let res = parse_file(contents);
251        assert!(res.requires_inheritdoc(&options));
252        assert!(!res.requires_inheritdoc(&ValidationOptions::default()));
253    }
254
255    #[test]
256    fn test_modifier_inheritdoc() {
257        let contents = "contract Test is ITest {
258            /// @inheritdoc ITest
259            modifier foo() override (ITest) { _; }
260        }";
261        let res = parse_file(contents).validate(
262            &ValidationOptions::builder()
263                .inheritdoc_override(true)
264                .build(),
265        );
266        assert!(res.diags.is_empty(), "{:#?}", res.diags);
267    }
268
269    #[test]
270    fn test_modifier_inheritdoc_missing() {
271        let contents = "contract Test is ITest {
272            /// @notice Test
273            modifier foo() override (ITest) { _; }
274        }";
275        let res = parse_file(contents).validate(
276            &ValidationOptions::builder()
277                .inheritdoc_override(true)
278                .build(),
279        );
280        assert_eq!(res.diags.len(), 1);
281        assert_eq!(res.diags[0].message, "@inheritdoc is missing");
282    }
283}