Skip to main content

rexact_syntax/
error.rs

1//! The error catalogue (docs/07). Single source of truth (RULE-17): the enum,
2//! code strings, exit codes, meanings and remedies are all generated from the
3//! one table below. A test cross-checks it against the documentation.
4
5use std::fmt;
6
7macro_rules! catalogue {
8    ($( $variant:ident, $code:literal, $exit:literal, $meaning:literal, $remedy:literal; )+) => {
9        /// One variant per catalogue entry, one-to-one with docs/07.
10        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
11        pub enum ErrorCode { $( #[doc = $meaning] $variant, )+ }
12
13        impl ErrorCode {
14            /// Every code, in catalogue order.
15            pub const ALL: &'static [ErrorCode] = &[ $( ErrorCode::$variant, )+ ];
16
17            /// The stable code string, e.g. `RX-P001`.
18            pub const fn code(self) -> &'static str {
19                match self { $( ErrorCode::$variant => $code, )+ }
20            }
21            /// Process exit code for this error (docs/06 exit-code table).
22            pub const fn exit_code(self) -> u8 {
23                match self { $( ErrorCode::$variant => $exit, )+ }
24            }
25            /// Human-readable meaning.
26            pub const fn meaning(self) -> &'static str {
27                match self { $( ErrorCode::$variant => $meaning, )+ }
28            }
29            /// Remedy shown to the user.
30            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/// A located catalogue error. `position` is (line, column), 1-based, counted
85/// in Unicode scalar values (docs/07 rule 2).
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct RexactError {
88    /// Which catalogue entry this is.
89    pub kind: ErrorCode,
90    /// Optional detail appended to the catalogue meaning.
91    pub detail: Option<String>,
92    /// Position in the pattern text, when the error relates to one.
93    pub position: Option<(usize, usize)>,
94}
95
96impl RexactError {
97    /// Error with no detail or position.
98    pub fn new(kind: ErrorCode) -> Self {
99        Self {
100            kind,
101            detail: None,
102            position: None,
103        }
104    }
105    /// Stable code string (`RX-...`).
106    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    /// Catalogue consistency check (docs/11 section 4): every row of docs/07
150    /// is in the data with the same exit code, in the same order.
151    #[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}