lintspec_core/definitions/
variable.rs1use crate::{
3 lint::{CheckNoticeAndDev, 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#[derive(Debug, Clone, bon::Builder)]
14#[non_exhaustive]
15#[builder(on(String, into))]
16pub struct VariableDeclaration {
17 pub parent: Option<Parent>,
19
20 pub name: String,
22
23 pub span: TextRange,
25
26 pub natspec: Option<NatSpec>,
28
29 pub attributes: Attributes,
31}
32
33impl VariableDeclaration {
34 fn requires_inheritdoc(&self) -> bool {
38 let parent_is_contract = matches!(self.parent, Some(Parent::Contract(_)));
39 let public = self.attributes.visibility == Visibility::Public;
40 parent_is_contract && public
41 }
42}
43
44impl SourceItem for VariableDeclaration {
45 fn item_type(&self) -> ItemType {
46 match self.attributes.visibility {
47 Visibility::External => unreachable!("variables cannot be external"),
48 Visibility::Internal => ItemType::InternalVariable,
49 Visibility::Private => ItemType::PrivateVariable,
50 Visibility::Public => ItemType::PublicVariable,
51 }
52 }
53
54 fn parent(&self) -> Option<Parent> {
55 self.parent.clone()
56 }
57
58 fn name(&self) -> String {
59 self.name.clone()
60 }
61
62 fn span(&self) -> TextRange {
63 self.span.clone()
64 }
65}
66
67impl Validate for VariableDeclaration {
68 fn validate(&self, options: &ValidationOptions) -> ItemDiagnostics {
69 let (notice, dev, returns) = match self.attributes.visibility {
70 Visibility::External => unreachable!("variables cannot be external"),
71 Visibility::Internal => (
72 options.variables.internal.notice,
73 options.variables.internal.dev,
74 None,
75 ),
76 Visibility::Private => (
77 options.variables.private.notice,
78 options.variables.private.dev,
79 None,
80 ),
81 Visibility::Public => (
82 options.variables.public.notice,
83 options.variables.public.dev,
84 Some(options.variables.public.returns),
85 ),
86 };
87 let mut out = ItemDiagnostics {
88 parent: self.parent(),
89 item_type: self.item_type(),
90 name: self.name(),
91 span: self.span(),
92 diags: vec![],
93 };
94 if let Some(natspec) = &self.natspec
95 && natspec
96 .items
97 .iter()
98 .any(|n| matches!(n.kind, NatSpecKind::Inheritdoc { .. }))
99 {
100 return out;
102 } else if options.inheritdoc && self.requires_inheritdoc() {
103 out.diags.push(Diagnostic {
104 span: self.span(),
105 message: "@inheritdoc is missing".to_string(),
106 });
107 return out;
108 }
109 out.diags.extend(
110 CheckNoticeAndDev::builder()
111 .natspec(&self.natspec)
112 .notice_rule(notice)
113 .dev_rule(dev)
114 .notice_or_dev(options.notice_or_dev)
115 .span(&self.span)
116 .build()
117 .check(),
118 );
119 if let Some(returns) = returns {
120 out.diags.extend(
121 CheckReturns::builder()
122 .natspec(&self.natspec)
123 .rule(returns)
124 .returns(&[Identifier {
125 name: None,
126 span: self.span(),
127 }])
128 .default_span(self.span())
129 .is_var(true)
130 .build()
131 .check(),
132 );
133 }
134 out
135 }
136}
137
138#[cfg(test)]
139#[cfg(feature = "solar")]
140mod tests {
141 use std::sync::LazyLock;
142
143 use similar_asserts::assert_eq;
144
145 use crate::{
146 definitions::Definition,
147 parser::{Parse as _, solar::SolarParser},
148 };
149
150 use super::*;
151
152 static OPTIONS: LazyLock<ValidationOptions> = LazyLock::new(Default::default);
153
154 fn parse_file(contents: &str) -> VariableDeclaration {
155 let mut parser = SolarParser::default();
156 let doc = parser
157 .parse_document(contents.as_bytes(), None::<std::path::PathBuf>, false)
158 .unwrap();
159 doc.definitions
160 .into_iter()
161 .find_map(Definition::to_variable)
162 .unwrap()
163 }
164
165 #[test]
166 fn test_variable() {
167 let contents = "contract Test is ITest {
168 /// @inheritdoc ITest
169 uint256 public a;
170 }";
171 let res = parse_file(contents).validate(&OPTIONS);
172 assert!(res.diags.is_empty(), "{:#?}", res.diags);
173 }
174
175 #[test]
176 fn test_variable_no_natspec() {
177 let contents = "contract Test {
178 uint256 public a;
179 }";
180 let res =
181 parse_file(contents).validate(&ValidationOptions::builder().inheritdoc(false).build());
182 assert_eq!(res.diags.len(), 2);
183 assert_eq!(res.diags[0].message, "@notice is missing");
184 assert_eq!(res.diags[1].message, "@return is missing");
185 }
186
187 #[test]
188 fn test_variable_no_natspec_inheritdoc() {
189 let contents = "contract Test {
190 uint256 public a;
191 }";
192 let res = parse_file(contents).validate(&OPTIONS);
193 assert_eq!(res.diags.len(), 1);
194 assert_eq!(res.diags[0].message, "@inheritdoc is missing");
195 }
196
197 #[test]
198 fn test_variable_only_notice() {
199 let contents = "contract Test {
200 /// @notice The variable
201 uint256 public a;
202 }";
203 let res = parse_file(contents).validate(&OPTIONS);
204 assert_eq!(res.diags.len(), 1);
205 assert_eq!(res.diags[0].message, "@inheritdoc is missing");
206 }
207
208 #[test]
209 fn test_variable_multiline() {
210 let contents = "contract Test is ITest {
211 /**
212 * @inheritdoc ITest
213 */
214 uint256 public a;
215 }";
216 let res = parse_file(contents).validate(&OPTIONS);
217 assert!(res.diags.is_empty(), "{:#?}", res.diags);
218 }
219
220 #[test]
221 fn test_requires_inheritdoc() {
222 let contents = "contract Test is ITest {
223 uint256 public a;
224 }";
225 let res = parse_file(contents);
226 assert!(res.requires_inheritdoc());
227
228 let contents = "contract Test is ITest {
229 uint256 internal a;
230 }";
231 let res = parse_file(contents);
232 assert!(!res.requires_inheritdoc());
233 }
234
235 #[test]
236 fn test_variable_no_inheritdoc() {
237 let contents = "contract Test {
238 /// @notice A variable
239 uint256 internal a;
240 }";
241 let res =
242 parse_file(contents).validate(&ValidationOptions::builder().inheritdoc(false).build());
243 assert!(res.diags.is_empty(), "{:#?}", res.diags);
244 }
245}