use std::fmt;
macro_rules! catalogue {
($( $variant:ident, $code:literal, $exit:literal, $meaning:literal, $remedy:literal; )+) => {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum ErrorCode { $( #[doc = $meaning] $variant, )+ }
impl ErrorCode {
pub const ALL: &'static [ErrorCode] = &[ $( ErrorCode::$variant, )+ ];
pub const fn code(self) -> &'static str {
match self { $( ErrorCode::$variant => $code, )+ }
}
pub const fn exit_code(self) -> u8 {
match self { $( ErrorCode::$variant => $exit, )+ }
}
pub const fn meaning(self) -> &'static str {
match self { $( ErrorCode::$variant => $meaning, )+ }
}
pub const fn remedy(self) -> &'static str {
match self { $( ErrorCode::$variant => $remedy, )+ }
}
}
};
}
catalogue! {
P001, "RX-P001", 3, "Syntax error in the pattern for the chosen dialect", "Position and caret; the expected construct";
P002, "RX-P002", 3, "Unknown dialect name", "List of valid dialects";
P003, "RX-P003", 3, "Flag not valid for the dialect, or conflicting flags", "Valid flags for the dialect";
P004, "RX-P004", 3, "Invalid escape sequence for the dialect", "The valid escapes of the dialect";
P005, "RX-P005", 3, "Invalid quantifier (nothing to repeat, reversed bounds, bound out of range)", "Corrected form";
P006, "RX-P006", 3, "Invalid character class (empty or reversed range, unknown class name)", "Corrected form";
P007, "RX-P007", 3, "Unbalanced group or class", "Position of the opener";
P008, "RX-P008", 3, "Unknown Unicode property or script for the pinned Unicode version", "Nearest valid names";
P009, "RX-P009", 3, "Invalid group name or duplicate name where the dialect forbids it", "Rename";
P010, "RX-P010", 3, "Invalid /body/flags literal", "Expected literal shape";
U001, "RX-U001", 4, "Backreference (language is not regular)", "Rewrite without the backreference, or analyse the parts separately";
U002, "RX-U002", 4, "Recursion or subroutine call", "Not supported";
U003, "RX-U003", 4, "Conditional group", "Rewrite as alternation where equivalent";
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";
U005, "RX-U005", 4, "Lookaround form outside the restrictions", "Simplify or raise the nesting limit";
U006, "RX-U006", 4, "Callout or embedded code", "Remove";
U007, "RX-U007", 4, "Dialect feature recognised but not yet supported", "Listed in the support matrix";
L001, "RX-L001", 2, "State limit reached", "Raise --limit-states or simplify";
L002, "RX-L002", 2, "Time limit reached", "Raise --limit-time";
L003, "RX-L003", 2, "Memory limit reached", "Raise --limit-memory";
L004, "RX-L004", 2, "Repetition expansion limit reached", "Raise --limit-repeat or reduce the counts";
L005, "RX-L005", 3, "Pattern too long", "Raise --limit-length";
L006, "RX-L006", 3, "Nesting too deep", "Raise --limit-depth";
L007, "RX-L007", 2, "Enumeration limit reached (witness or difference synthesis)", "Raise --limit-enumerate";
L008, "RX-L008", 2, "Synthesised pattern size cap reached", "Raise the cap; examples are still shown";
I001, "RX-I001", 6, "File not found or unreadable", "Path";
I002, "RX-I002", 3, "Input is not valid UTF-8 where text is required", "Use --alphabet bytes or fix encoding";
I003, "RX-I003", 6, "Not a git repository or unknown revision", "Valid revisions";
I004, "RX-I004", 6, "Reference engine required but unavailable", "Install, or drop the requirement";
I005, "RX-I005", 3, "Unknown source language for extraction", "Supported languages";
C001, "RX-C001", 3, "Malformed configuration file", "Position";
C002, "RX-C002", 3, "Ignore entry without a reason", "Add reason";
C003, "RX-C003", 3, "Unknown option or value in configuration", "Valid names";
E001, "RX-E001", 5, "Certificate rejected by the independent checker", "Report the case; includes the failing pair";
E002, "RX-E002", 5, "Counterexample rejected by the independent matcher", "Report the case";
E003, "RX-E003", 5, "Reference engine disagrees with the verdict", "Shows the engine, version and input";
E004, "RX-E004", 5, "Certificate malformed or of an unknown version", "Regenerate";
X001, "RX-X001", 70, "Internal invariant violated", "Report with version, command and a minimal pattern";
}
impl fmt::Display for ErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.code())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RexactError {
pub kind: ErrorCode,
pub detail: Option<String>,
pub position: Option<(usize, usize)>,
}
impl RexactError {
pub fn new(kind: ErrorCode) -> Self {
Self {
kind,
detail: None,
position: None,
}
}
pub fn code(&self) -> &'static str {
self.kind.code()
}
}
impl fmt::Display for RexactError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.kind.code(), self.kind.meaning())?;
if let Some(d) = &self.detail {
write!(f, " ({d})")?;
}
if let Some((l, c)) = self.position {
write!(f, " at line {l}, column {c}")?;
}
write!(f, ". Remedy: {}", self.kind.remedy())
}
}
impl std::error::Error for RexactError {}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn codes_are_unique_and_well_formed() {
let mut seen = HashSet::new();
for e in ErrorCode::ALL {
assert!(seen.insert(e.code()), "duplicate {}", e.code());
assert!(e.code().starts_with("RX-") && e.code().len() == 7);
assert!(!e.meaning().is_empty() && !e.remedy().is_empty());
}
}
#[test]
fn exit_codes_are_in_the_documented_set() {
for e in ErrorCode::ALL {
assert!([2, 3, 4, 5, 6, 70].contains(&e.exit_code()), "{}", e.code());
}
}
#[test]
fn matches_documentation_table() {
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../docs/07-error-catalogue.md"
);
let doc = std::fs::read_to_string(path).expect("docs/07 readable");
let mut documented = Vec::new();
for line in doc.lines() {
let cells: Vec<&str> = line.split('|').map(str::trim).collect();
if cells.len() > 3 && cells[1].starts_with("RX-") {
documented.push((cells[1].to_string(), cells[2].parse::<u8>().expect("exit")));
}
}
let data: Vec<(String, u8)> = ErrorCode::ALL
.iter()
.map(|e| (e.code().to_string(), e.exit_code()))
.collect();
assert_eq!(documented, data);
}
#[test]
fn display_includes_code_position_and_remedy() {
let mut e = RexactError::new(ErrorCode::P007);
e.position = Some((1, 1));
let s = e.to_string();
assert!(s.contains("RX-P007") && s.contains("line 1, column 1") && s.contains("Remedy"));
}
}