1use std::fmt;
6
7macro_rules! catalogue {
8 ($( $variant:ident, $code:literal, $exit:literal, $meaning:literal, $remedy:literal; )+) => {
9 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
11 pub enum ErrorCode { $( #[doc = $meaning] $variant, )+ }
12
13 impl ErrorCode {
14 pub const ALL: &'static [ErrorCode] = &[ $( ErrorCode::$variant, )+ ];
16
17 pub const fn code(self) -> &'static str {
19 match self { $( ErrorCode::$variant => $code, )+ }
20 }
21 pub const fn exit_code(self) -> u8 {
23 match self { $( ErrorCode::$variant => $exit, )+ }
24 }
25 pub const fn meaning(self) -> &'static str {
27 match self { $( ErrorCode::$variant => $meaning, )+ }
28 }
29 pub const fn remedy(self) -> &'static str {
31 match self { $( ErrorCode::$variant => $remedy, )+ }
32 }
33 }
34 };
35}
36
37catalogue! {
38 P001, "RX-P001", 3, "Syntax error in the pattern for the chosen dialect", "Position and caret; the expected construct";
39 P002, "RX-P002", 3, "Unknown dialect name", "List of valid dialects";
40 P003, "RX-P003", 3, "Flag not valid for the dialect, or conflicting flags", "Valid flags for the dialect";
41 P004, "RX-P004", 3, "Invalid escape sequence for the dialect", "The valid escapes of the dialect";
42 P005, "RX-P005", 3, "Invalid quantifier (nothing to repeat, reversed bounds, bound out of range)", "Corrected form";
43 P006, "RX-P006", 3, "Invalid character class (empty or reversed range, unknown class name)", "Corrected form";
44 P007, "RX-P007", 3, "Unbalanced group or class", "Position of the opener";
45 P008, "RX-P008", 3, "Unknown Unicode property or script for the pinned Unicode version", "Nearest valid names";
46 P009, "RX-P009", 3, "Invalid group name or duplicate name where the dialect forbids it", "Rename";
47 P010, "RX-P010", 3, "Invalid /body/flags literal", "Expected literal shape";
48 U001, "RX-U001", 4, "Backreference (language is not regular)", "Rewrite without the backreference, or analyse the parts separately";
49 U002, "RX-U002", 4, "Recursion or subroutine call", "Not supported";
50 U003, "RX-U003", 4, "Conditional group", "Rewrite as alternation where equivalent";
51 U004, "RX-U004", 4, "Atomic group or possessive quantifier (changes matching semantics, not only the language)", "Remove, or use the analysis on the underlying pattern with a stated caveat";
52 U005, "RX-U005", 4, "Lookaround form outside the restrictions", "Simplify or raise the nesting limit";
53 U006, "RX-U006", 4, "Callout or embedded code", "Remove";
54 U007, "RX-U007", 4, "Dialect feature recognised but not yet supported", "Listed in the support matrix";
55 L001, "RX-L001", 2, "State limit reached", "Raise --limit-states or simplify";
56 L002, "RX-L002", 2, "Time limit reached", "Raise --limit-time";
57 L003, "RX-L003", 2, "Memory limit reached", "Raise --limit-memory";
58 L004, "RX-L004", 2, "Repetition expansion limit reached", "Raise --limit-repeat or reduce the counts";
59 L005, "RX-L005", 3, "Pattern too long", "Raise --limit-length";
60 L006, "RX-L006", 3, "Nesting too deep", "Raise --limit-depth";
61 L007, "RX-L007", 2, "Enumeration limit reached (witness or difference synthesis)", "Raise --limit-enumerate";
62 L008, "RX-L008", 2, "Synthesised pattern size cap reached", "Raise the cap; examples are still shown";
63 I001, "RX-I001", 6, "File not found or unreadable", "Path";
64 I002, "RX-I002", 3, "Input is not valid UTF-8 where text is required", "Use --alphabet bytes or fix encoding";
65 I003, "RX-I003", 6, "Not a git repository or unknown revision", "Valid revisions";
66 I004, "RX-I004", 6, "Reference engine required but unavailable", "Install, or drop the requirement";
67 I005, "RX-I005", 3, "Unknown source language for extraction", "Supported languages";
68 C001, "RX-C001", 3, "Malformed configuration file", "Position";
69 C002, "RX-C002", 3, "Ignore entry without a reason", "Add reason";
70 C003, "RX-C003", 3, "Unknown option or value in configuration", "Valid names";
71 E001, "RX-E001", 5, "Certificate rejected by the independent checker", "Report the case; includes the failing pair";
72 E002, "RX-E002", 5, "Counterexample rejected by the independent matcher", "Report the case";
73 E003, "RX-E003", 5, "Reference engine disagrees with the verdict", "Shows the engine, version and input";
74 E004, "RX-E004", 5, "Certificate malformed or of an unknown version", "Regenerate";
75 X001, "RX-X001", 70, "Internal invariant violated", "Report with version, command and a minimal pattern";
76}
77
78impl fmt::Display for ErrorCode {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 write!(f, "{}", self.code())
81 }
82}
83
84#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct RexactError {
88 pub kind: ErrorCode,
90 pub detail: Option<String>,
92 pub position: Option<(usize, usize)>,
94}
95
96impl RexactError {
97 pub fn new(kind: ErrorCode) -> Self {
99 Self {
100 kind,
101 detail: None,
102 position: None,
103 }
104 }
105 pub fn code(&self) -> &'static str {
107 self.kind.code()
108 }
109}
110
111impl fmt::Display for RexactError {
112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113 write!(f, "{}: {}", self.kind.code(), self.kind.meaning())?;
114 if let Some(d) = &self.detail {
115 write!(f, " ({d})")?;
116 }
117 if let Some((l, c)) = self.position {
118 write!(f, " at line {l}, column {c}")?;
119 }
120 write!(f, ". Remedy: {}", self.kind.remedy())
121 }
122}
123
124impl std::error::Error for RexactError {}
125
126#[cfg(test)]
127#[allow(clippy::expect_used)]
128mod tests {
129 use super::*;
130 use std::collections::HashSet;
131
132 #[test]
133 fn codes_are_unique_and_well_formed() {
134 let mut seen = HashSet::new();
135 for e in ErrorCode::ALL {
136 assert!(seen.insert(e.code()), "duplicate {}", e.code());
137 assert!(e.code().starts_with("RX-") && e.code().len() == 7);
138 assert!(!e.meaning().is_empty() && !e.remedy().is_empty());
139 }
140 }
141
142 #[test]
143 fn exit_codes_are_in_the_documented_set() {
144 for e in ErrorCode::ALL {
145 assert!([2, 3, 4, 5, 6, 70].contains(&e.exit_code()), "{}", e.code());
146 }
147 }
148
149 #[test]
152 fn matches_documentation_table() {
153 let path = concat!(
154 env!("CARGO_MANIFEST_DIR"),
155 "/../../docs/07-error-catalogue.md"
156 );
157 let doc = std::fs::read_to_string(path).expect("docs/07 readable");
158 let mut documented = Vec::new();
159 for line in doc.lines() {
160 let cells: Vec<&str> = line.split('|').map(str::trim).collect();
161 if cells.len() > 3 && cells[1].starts_with("RX-") {
162 documented.push((cells[1].to_string(), cells[2].parse::<u8>().expect("exit")));
163 }
164 }
165 let data: Vec<(String, u8)> = ErrorCode::ALL
166 .iter()
167 .map(|e| (e.code().to_string(), e.exit_code()))
168 .collect();
169 assert_eq!(documented, data);
170 }
171
172 #[test]
173 fn display_includes_code_position_and_remedy() {
174 let mut e = RexactError::new(ErrorCode::P007);
175 e.position = Some((1, 1));
176 let s = e.to_string();
177 assert!(s.contains("RX-P007") && s.contains("line 1, column 1") && s.contains("Remedy"));
178 }
179}