Skip to main content

lintspec_core/definitions/
constructor.rs

1//! Parsing and validation of constructors.
2use crate::{
3    interner::{INTERNER, Symbol},
4    lint::{CheckNoticeAndDev, CheckParams, ItemDiagnostics},
5    natspec::NatSpec,
6};
7
8use super::{Identifier, ItemType, Parent, SourceItem, TextRange, Validate, ValidationOptions};
9
10/// A constructor definition
11#[derive(Debug, Clone, bon::Builder)]
12#[non_exhaustive]
13pub struct ConstructorDefinition {
14    /// The parent contract (should always be a [`Parent::Contract`])
15    pub parent: Option<Parent>,
16
17    /// The span corresponding to the constructor definition, excluding the body
18    pub span: TextRange,
19
20    /// The name and span of the constructor's parameters
21    pub params: Vec<Identifier>,
22
23    /// The [`NatSpec`] associated with the constructor, if any
24    pub natspec: Option<NatSpec>,
25}
26
27impl SourceItem for ConstructorDefinition {
28    fn item_type(&self) -> ItemType {
29        ItemType::Constructor
30    }
31
32    fn parent(&self) -> Option<Parent> {
33        self.parent.clone()
34    }
35
36    fn name(&self) -> Symbol {
37        INTERNER.get_or_intern_static("constructor")
38    }
39
40    fn span(&self) -> TextRange {
41        self.span.clone()
42    }
43}
44
45impl Validate for ConstructorDefinition {
46    fn validate(&self, options: &ValidationOptions) -> ItemDiagnostics {
47        let opts = &options.constructors;
48        let mut out = ItemDiagnostics {
49            parent: self.parent(),
50            item_type: self.item_type(),
51            name: self.name().resolve_with(&INTERNER),
52            span: self.span(),
53            diags: vec![],
54        };
55        CheckNoticeAndDev::builder()
56            .natspec(&self.natspec)
57            .notice_rule(opts.notice)
58            .dev_rule(opts.dev)
59            .notice_or_dev(options.notice_or_dev)
60            .span(&self.span)
61            .build()
62            .check_into(&mut out.diags);
63        CheckParams::builder()
64            .natspec(&self.natspec)
65            .rule(opts.param)
66            .params(&self.params)
67            .default_span(self.span())
68            .build()
69            .check_into(&mut out.diags);
70        out
71    }
72}
73
74#[cfg(test)]
75#[cfg(feature = "solar")]
76mod tests {
77    use std::sync::LazyLock;
78
79    use similar_asserts::assert_eq;
80
81    use crate::{
82        config::{Req, WithParamsRules},
83        definitions::Definition,
84        parser::{Parse as _, solar::SolarParser},
85    };
86
87    use super::*;
88
89    static OPTIONS: LazyLock<ValidationOptions> =
90        LazyLock::new(|| ValidationOptions::builder().inheritdoc(false).build());
91
92    fn parse_file(contents: &str) -> ConstructorDefinition {
93        let mut parser = SolarParser::default();
94        let doc = parser
95            .parse_document(contents.as_bytes(), None::<std::path::PathBuf>, false)
96            .unwrap();
97        doc.definitions
98            .into_iter()
99            .find_map(Definition::to_constructor)
100            .unwrap()
101    }
102
103    #[test]
104    fn test_constructor() {
105        let contents = "contract Test {
106            /// @param param1 Test
107            /// @param param2 Test2
108            constructor(uint256 param1, bytes calldata param2) { }
109        }";
110        let res = parse_file(contents).validate(&OPTIONS);
111        assert!(res.diags.is_empty(), "{:#?}", res.diags);
112    }
113
114    #[test]
115    fn test_constructor_no_natspec() {
116        let contents = "contract Test {
117            constructor(uint256 param1, bytes calldata param2) { }
118        }";
119        let res = parse_file(contents).validate(&OPTIONS);
120        assert_eq!(res.diags.len(), 2);
121        assert_eq!(res.diags[0].message, "@param param1 is missing");
122        assert_eq!(res.diags[1].message, "@param param2 is missing");
123    }
124
125    #[test]
126    fn test_constructor_only_notice() {
127        let contents = "contract Test {
128            /// @notice The constructor
129            constructor(uint256 param1, bytes calldata param2) { }
130        }";
131        let res = parse_file(contents).validate(&OPTIONS);
132        assert_eq!(res.diags.len(), 2);
133        assert_eq!(res.diags[0].message, "@param param1 is missing");
134        assert_eq!(res.diags[1].message, "@param param2 is missing");
135    }
136
137    #[test]
138    fn test_constructor_one_missing() {
139        let contents = "contract Test {
140            /// @param param1 The first
141            constructor(uint256 param1, bytes calldata param2) { }
142        }";
143        let res = parse_file(contents).validate(&OPTIONS);
144        assert_eq!(res.diags.len(), 1);
145        assert_eq!(res.diags[0].message, "@param param2 is missing");
146    }
147
148    #[test]
149    fn test_constructor_multiline() {
150        let contents = "contract Test {
151            /**
152             * @param param1 Test
153             * @param param2 Test2
154             */
155            constructor(uint256 param1, bytes calldata param2) { }
156        }";
157        let res = parse_file(contents).validate(&OPTIONS);
158        assert!(res.diags.is_empty(), "{:#?}", res.diags);
159    }
160
161    #[test]
162    fn test_constructor_duplicate() {
163        let contents = "contract Test {
164            /// @param param1 The first
165            /// @param param1 The first again
166            constructor(uint256 param1) { }
167        }";
168        let res = parse_file(contents).validate(&OPTIONS);
169        assert_eq!(res.diags.len(), 1);
170        assert_eq!(
171            res.diags[0].message,
172            "@param param1 is present more than once"
173        );
174    }
175
176    #[test]
177    fn test_constructor_no_params() {
178        let contents = "contract Test {
179            constructor() { }
180        }";
181        let res = parse_file(contents).validate(&OPTIONS);
182        assert!(res.diags.is_empty(), "{:#?}", res.diags);
183    }
184
185    #[test]
186    fn test_constructor_notice() {
187        // inheritdoc should be ignored since it's a constructor
188        let contents = "contract Test {
189            /// @inheritdoc ITest
190            constructor(uint256 param1) { }
191        }";
192        let res = parse_file(contents).validate(
193            &ValidationOptions::builder()
194                .inheritdoc(true)
195                .constructors(WithParamsRules::required())
196                .build(),
197        );
198        assert_eq!(res.diags.len(), 2);
199        assert_eq!(res.diags[0].message, "@notice is missing");
200        assert_eq!(res.diags[1].message, "@param param1 is missing");
201    }
202
203    #[test]
204    fn test_constructor_enforce() {
205        let opts = ValidationOptions::builder()
206            .constructors(WithParamsRules {
207                notice: Req::Required,
208                dev: Req::default(),
209                param: Req::default(),
210            })
211            .build();
212        let contents = "contract Test {
213            constructor() { }
214        }";
215        let res = parse_file(contents).validate(&opts);
216        assert_eq!(res.diags.len(), 1);
217        assert_eq!(res.diags[0].message, "@notice is missing");
218
219        let contents = "contract Test {
220            /// @notice Some notice
221            constructor() { }
222        }";
223        let res = parse_file(contents).validate(&opts);
224        assert!(res.diags.is_empty(), "{:#?}", res.diags);
225    }
226}