use alloc::{string::String, vec::Vec};
use core::fmt;
use crate::RegexError;
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Error {
InvalidRegex {
path: String,
error: RegexError,
},
EmptyRegex {
path: String,
},
InvalidScope {
path: String,
scope: String,
offset: usize,
reason: &'static str,
},
InvalidSelector {
path: String,
selector: String,
offset: usize,
reason: &'static str,
},
InvalidName {
path: String,
name: String,
reason: &'static str,
},
DuplicateRepository {
name: String,
},
DuplicateInjection {
selector: String,
},
DuplicateCapture {
path: String,
group: u32,
},
CaptureOutOfRange {
path: String,
group: u32,
groups: u32,
},
ShadowedCaptures {
path: String,
},
UndefinedInclude {
path: String,
name: String,
},
IncludeCycle {
cycle: Vec<String>,
},
TooDeep {
path: String,
},
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidRegex { path, error } => {
write!(f, "invalid regular expression at `{path}`, {error}")
}
Self::EmptyRegex { path } => write!(f, "empty regular expression at `{path}`"),
Self::InvalidScope {
path,
scope,
offset,
reason,
} => write!(
f,
"invalid scope name {scope:?} at `{path}`, byte {offset}: {reason}"
),
Self::InvalidSelector {
path,
selector,
offset,
reason,
} => write!(
f,
"invalid scope selector {selector:?} at `{path}`, byte {offset}: {reason}"
),
Self::InvalidName { path, name, reason } => {
write!(f, "invalid name {name:?} at `{path}`: {reason}")
}
Self::DuplicateRepository { name } => {
write!(f, "repository entry {name:?} is defined more than once")
}
Self::DuplicateInjection { selector } => {
write!(f, "injection selector {selector:?} is given more than once")
}
Self::DuplicateCapture { path, group } => {
write!(f, "capture group {group} is set more than once at `{path}`")
}
Self::CaptureOutOfRange {
path,
group,
groups,
} => write!(
f,
"`{path}` refers to capture group {group}, but the highest group in the pattern is {groups}"
),
Self::ShadowedCaptures { path } => write!(
f,
"`{path}/captures` never applies: both sides of the region have their own captures"
),
Self::UndefinedInclude { path, name } => {
write!(
f,
"`{path}` includes repository entry {name:?}, which is not defined"
)
}
Self::IncludeCycle { cycle } => {
f.write_str("includes form a cycle that consumes no text: ")?;
for (i, entry) in cycle.iter().enumerate() {
if i > 0 {
f.write_str(" -> ")?;
}
f.write_str(entry)?;
}
Ok(())
}
Self::TooDeep { path } => write!(
f,
"rules nest more than {} deep at `{path}`",
crate::MAX_RULE_DEPTH
),
}
}
}
impl core::error::Error for Error {
fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
match self {
Self::InvalidRegex { error, .. } => Some(error),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use alloc::{string::ToString, vec};
use super::*;
use crate::RegexErrorKind;
#[test]
fn test_every_variant_displays_its_details() {
let p = || String::from("/p");
let all = [
Error::InvalidRegex {
path: p(),
error: RegexError {
offset: 3,
kind: RegexErrorKind::UnclosedGroup,
},
},
Error::EmptyRegex { path: p() },
Error::InvalidScope {
path: p(),
scope: "a..b".into(),
offset: 2,
reason: "r",
},
Error::InvalidSelector {
path: p(),
selector: "(".into(),
offset: 0,
reason: "r",
},
Error::InvalidName {
path: p(),
name: "a b".into(),
reason: "r",
},
Error::DuplicateCapture {
path: p(),
group: 1,
},
Error::CaptureOutOfRange {
path: p(),
group: 3,
groups: 2,
},
Error::ShadowedCaptures { path: p() },
Error::UndefinedInclude {
path: p(),
name: "x".into(),
},
Error::TooDeep { path: p() },
];
for e in all {
assert!(e.to_string().contains("/p"), "{e}");
}
assert!(
Error::DuplicateRepository { name: "s".into() }
.to_string()
.contains("\"s\"")
);
assert!(
Error::DuplicateInjection {
selector: "L:x".into()
}
.to_string()
.contains("L:x")
);
let cycle = Error::IncludeCycle {
cycle: vec!["#a".into(), "#b".into(), "#a".into()],
};
assert!(cycle.to_string().ends_with("#a -> #b -> #a"));
}
#[test]
fn test_regex_error_is_the_source() {
use core::error::Error as _;
let e = Error::InvalidRegex {
path: "/x".into(),
error: RegexError {
offset: 0,
kind: RegexErrorKind::TrailingBackslash,
},
};
assert!(e.source().is_some());
assert!(Error::EmptyRegex { path: "/x".into() }.source().is_none());
assert_eq!(
e.to_string(),
"invalid regular expression at `/x`, at byte 0: the pattern ends with a lone `\\`"
);
}
}