Skip to main content

lintspec_core/definitions/
variable.rs

1//! Parsing and validation of state variable declarations.
2use 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/// A state variable declaration
14#[derive(Debug, Clone, bon::Builder)]
15#[non_exhaustive]
16#[builder(on(String, into))]
17pub struct VariableDeclaration {
18    /// The parent for the state variable declaration (should always be `Some`)
19    pub parent: Option<Parent>,
20
21    /// The name of the state variable
22    pub name: Symbol,
23
24    /// The span of the state variable declaration
25    pub span: TextRange,
26
27    /// The [`NatSpec`] associated with the state variable declaration, if any
28    pub natspec: Option<NatSpec>,
29
30    /// The attributes of the state variable (visibility)
31    pub attributes: Attributes,
32}
33
34impl VariableDeclaration {
35    /// Check whether this variable requires inheritdoc when we enforce it
36    ///
37    /// Public state variables must have inheritdoc.
38    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            // if there is `inheritdoc`, no further validation is required
102            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}