lintspec_core/definitions/
constructor.rs1use crate::{
3 lint::{CheckNoticeAndDev, CheckParams, ItemDiagnostics},
4 natspec::NatSpec,
5};
6
7use super::{Identifier, ItemType, Parent, SourceItem, TextRange, Validate, ValidationOptions};
8
9#[derive(Debug, Clone, bon::Builder)]
11#[non_exhaustive]
12pub struct ConstructorDefinition {
13 pub parent: Option<Parent>,
15
16 pub span: TextRange,
18
19 pub params: Vec<Identifier>,
21
22 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 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}