Skip to main content

lintspec_core/definitions/
error.rs

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