lintspec_core/definitions/
constructor.rs1use 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#[derive(Debug, Clone, bon::Builder)]
12#[non_exhaustive]
13pub struct ConstructorDefinition {
14 pub parent: Option<Parent>,
16
17 pub span: TextRange,
19
20 pub params: Vec<Identifier>,
22
23 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 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}