Skip to main content

lintspec_core/definitions/
constructor.rs

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