Skip to main content

lintspec_core/definitions/
error.rs

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