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