lintspec_core/definitions/
modifier.rs1use 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#[derive(Debug, Clone, bon::Builder)]
14#[non_exhaustive]
15#[builder(on(String, into))]
16pub struct ModifierDefinition {
17 pub parent: Option<Parent>,
19
20 pub name: Symbol,
22
23 pub span: TextRange,
25
26 pub params: Vec<Identifier>,
28
29 pub natspec: Option<NatSpec>,
31
32 pub attributes: Attributes,
34}
35
36impl ModifierDefinition {
37 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 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}