Skip to main content

lintspec_core/definitions/
variable.rs

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