lintspec/definitions/
error.rs

1//! Parsing and validation of error definitions.
2use crate::{
3    lint::{check_notice_and_dev, check_params, 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(check_notice_and_dev(
59            &self.natspec,
60            opts.notice,
61            opts.dev,
62            options.notice_or_dev,
63            self.span(),
64        ));
65        out.diags.extend(check_params(
66            &self.natspec,
67            opts.param,
68            &self.params,
69            self.span(),
70        ));
71        out
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use std::sync::LazyLock;
78
79    use semver::Version;
80    use similar_asserts::assert_eq;
81    use slang_solidity::{cst::NonterminalKind, parser::Parser};
82
83    use crate::parser::slang::Extract as _;
84
85    use super::*;
86
87    static OPTIONS: LazyLock<ValidationOptions> =
88        LazyLock::new(|| ValidationOptions::builder().inheritdoc(false).build());
89
90    fn parse_file(contents: &str) -> ErrorDefinition {
91        let parser = Parser::create(Version::new(0, 8, 26)).unwrap();
92        let output = parser.parse(NonterminalKind::SourceUnit, contents);
93        assert!(output.is_valid(), "{:?}", output.errors());
94        let cursor = output.create_tree_cursor();
95        let m = cursor.query(vec![ErrorDefinition::query()]).next().unwrap();
96        let def = ErrorDefinition::extract(m).unwrap();
97        def.to_error().unwrap()
98    }
99
100    #[test]
101    fn test_error() {
102        let contents = "contract Test {
103            /// @notice An error
104            /// @param a The first
105            /// @param b The second
106            error Foobar(uint256 a, uint256 b);
107        }";
108        let res = parse_file(contents).validate(&OPTIONS);
109        assert!(res.diags.is_empty(), "{:#?}", res.diags);
110    }
111
112    #[test]
113    fn test_error_no_natspec() {
114        let contents = "contract Test {
115            error Foobar(uint256 a, uint256 b);
116        }";
117        let res = parse_file(contents).validate(&OPTIONS);
118        assert_eq!(res.diags.len(), 3);
119        assert_eq!(res.diags[0].message, "@notice is missing");
120        assert_eq!(res.diags[1].message, "@param a is missing");
121        assert_eq!(res.diags[2].message, "@param b is missing");
122    }
123
124    #[test]
125    fn test_error_only_notice() {
126        let contents = "contract Test {
127            /// @notice An error
128            error Foobar(uint256 a, uint256 b);
129        }";
130        let res = parse_file(contents).validate(&OPTIONS);
131        assert_eq!(res.diags.len(), 2);
132        assert_eq!(res.diags[0].message, "@param a is missing");
133        assert_eq!(res.diags[1].message, "@param b is missing");
134    }
135
136    #[test]
137    fn test_error_one_missing() {
138        let contents = "contract Test {
139            /// @notice An error
140            /// @param a The first
141            error Foobar(uint256 a, uint256 b);
142        }";
143        let res = parse_file(contents).validate(&OPTIONS);
144        assert_eq!(res.diags.len(), 1);
145        assert_eq!(res.diags[0].message, "@param b is missing");
146    }
147
148    #[test]
149    fn test_error_multiline() {
150        let contents = "contract Test {
151            /**
152             * @notice An error
153             * @param a The first
154             * @param b The second
155             */
156            error Foobar(uint256 a, uint256 b);
157        }";
158        let res = parse_file(contents).validate(&OPTIONS);
159        assert!(res.diags.is_empty(), "{:#?}", res.diags);
160    }
161
162    #[test]
163    fn test_error_duplicate() {
164        let contents = "contract Test {
165            /// @notice An error
166            /// @param a The first
167            /// @param a The first again
168            error Foobar(uint256 a);
169        }";
170        let res = parse_file(contents).validate(&OPTIONS);
171        assert_eq!(res.diags.len(), 1);
172        assert_eq!(res.diags[0].message, "@param a is present more than once");
173    }
174
175    #[test]
176    fn test_error_no_params() {
177        let contents = "contract Test {
178            error Foobar();
179        }";
180        let res = parse_file(contents).validate(&OPTIONS);
181        assert_eq!(res.diags.len(), 1);
182        assert_eq!(res.diags[0].message, "@notice is missing");
183    }
184
185    #[test]
186    fn test_error_inheritdoc() {
187        // inheritdoc should be ignored as it doesn't apply to errors
188        let contents = "contract Test {
189            /// @inheritdoc ITest
190            error Foobar(uint256 a);
191        }";
192        let res = parse_file(contents).validate(&ValidationOptions::default());
193        assert_eq!(res.diags.len(), 2);
194        assert_eq!(res.diags[0].message, "@notice is missing");
195        assert_eq!(res.diags[1].message, "@param a is missing");
196    }
197
198    #[test]
199    fn test_error_no_contract() {
200        let contents = "
201            /// @notice An error
202            /// @param a The first
203            /// @param b The second
204            error Foobar(uint256 a, uint256 b);
205            ";
206        let res = parse_file(contents).validate(&OPTIONS);
207        assert!(res.diags.is_empty(), "{:#?}", res.diags);
208    }
209
210    #[test]
211    fn test_error_no_contract_missing() {
212        let contents = "error Foobar(uint256 a, uint256 b);";
213        let res = parse_file(contents).validate(&OPTIONS);
214        assert_eq!(res.diags.len(), 3);
215        assert_eq!(res.diags[0].message, "@notice is missing");
216        assert_eq!(res.diags[1].message, "@param a is missing");
217        assert_eq!(res.diags[2].message, "@param b is missing");
218    }
219}