use crate::error::{Error, Result};
use crate::explain::{Explainer, Recorder};
use crate::pattern::Pattern;
use crate::variable::{varname_prefix, Value, VariableMap};
use crate::MatchRange;
use regex::{Captures, Regex};
use std::borrow::Cow;
use std::cmp::max;
use std::collections::HashMap;
use std::fmt::{self, Display, Formatter};
use std::mem;
enum Directive {
Check(Pattern),
SameLn(Pattern),
NextLn(Pattern),
Unordered(Pattern),
Not(Pattern),
Regex(String, String),
}
const DIRECTIVE_RX: &str = r"\b(check|sameln|nextln|unordered|not|regex):\s+(.*)";
impl Directive {
fn new(caps: Captures) -> Result<Directive> {
let cmd = caps.get(1).map(|m| m.as_str()).expect("group 1 must match");
let rest = caps.get(2).map(|m| m.as_str()).expect("group 2 must match");
if cmd == "regex" {
return Directive::regex(rest);
}
let pat = rest.parse()?;
match cmd {
"check" => Ok(Directive::Check(pat)),
"sameln" => Ok(Directive::SameLn(pat)),
"nextln" => Ok(Directive::NextLn(pat)),
"unordered" => Ok(Directive::Unordered(pat)),
"not" => {
if !pat.defs().is_empty() {
let msg = format!(
"can't define variables '$({}=...' in not: {}",
pat.defs()[0],
rest
);
Err(Error::DuplicateDef(msg))
} else {
Ok(Directive::Not(pat))
}
}
_ => panic!("unexpected command {} in regex match", cmd),
}
}
fn regex(rest: &str) -> Result<Directive> {
let varlen = varname_prefix(rest);
if varlen == 0 {
return Err(Error::Syntax(format!(
"invalid variable name in regex: {}",
rest
)));
}
let var = rest[0..varlen].to_string();
if !rest[varlen..].starts_with('=') {
return Err(Error::Syntax(format!(
"expected '=' after variable '{}' in regex: {}",
var, rest
)));
}
Ok(Directive::Regex(
var,
rest[varlen + 1..].trim_end().to_string(),
))
}
}
pub struct CheckerBuilder {
directives: Vec<Directive>,
linerx: Regex,
}
impl CheckerBuilder {
pub fn new() -> Self {
Self {
directives: Vec::new(),
linerx: Regex::new(DIRECTIVE_RX).unwrap(),
}
}
pub fn directive(&mut self, l: &str) -> Result<bool> {
match self.linerx.captures(l) {
Some(caps) => {
self.directives.push(Directive::new(caps)?);
Ok(true)
}
None => Ok(false),
}
}
pub fn text(&mut self, t: &str) -> Result<&mut Self> {
for caps in self.linerx.captures_iter(t) {
self.directives.push(Directive::new(caps)?);
}
Ok(self)
}
pub fn finish(&mut self) -> Checker {
let new_directives = mem::replace(&mut self.directives, Vec::new());
Checker::new(new_directives)
}
}
pub struct Checker {
directives: Vec<Directive>,
}
impl Checker {
fn new(directives: Vec<Directive>) -> Self {
Self { directives }
}
pub fn is_empty(&self) -> bool {
self.directives.is_empty()
}
pub fn check(&self, text: &str, vars: &dyn VariableMap) -> Result<bool> {
self.run(text, vars, &mut ())
}
pub fn explain(&self, text: &str, vars: &dyn VariableMap) -> Result<(bool, String)> {
let mut expl = Explainer::new(text);
let success = self.run(text, vars, &mut expl)?;
expl.finish();
Ok((success, expl.to_string()))
}
fn run(&self, text: &str, vars: &dyn VariableMap, recorder: &mut dyn Recorder) -> Result<bool> {
let mut state = State::new(text, vars, recorder);
let mut nots = Vec::new();
for (dct_idx, dct) in self.directives.iter().enumerate() {
let (pat, range) = match *dct {
Directive::Check(ref pat) => (pat, state.check()),
Directive::SameLn(ref pat) => (pat, state.sameln()),
Directive::NextLn(ref pat) => (pat, state.nextln()),
Directive::Unordered(ref pat) => (pat, state.unordered(pat)),
Directive::Not(ref pat) => {
nots.push((dct_idx, state.unordered_begin(pat), pat.resolve(&state)?));
continue;
}
Directive::Regex(ref var, ref rx) => {
state.vars.insert(
var.clone(),
VarDef {
value: Value::Regex(Cow::Borrowed(rx)),
offset: 0,
},
);
continue;
}
};
state.recorder.directive(dct_idx);
if let Some((match_begin, match_end)) = state.match_positive(pat, range)? {
if let Directive::Unordered(_) = *dct {
state.max_match = max(state.max_match, match_end);
} else {
state.last_ordered = match_end;
state.max_match = match_end;
for (not_idx, not_begin, rx) in nots.drain(..) {
state.recorder.directive(not_idx);
if let Some(mat) = rx.find(&text[not_begin..match_begin]) {
state.recorder.matched_not(
rx.as_str(),
(not_begin + mat.start(), not_begin + mat.end()),
);
return Ok(false);
} else {
state
.recorder
.missed_not(rx.as_str(), (not_begin, match_begin));
}
}
}
} else {
return Ok(false);
}
}
for (not_idx, not_begin, rx) in nots.drain(..) {
state.recorder.directive(not_idx);
if rx.find(&text[not_begin..]).is_some() {
return Ok(false);
}
}
Ok(true)
}
}
pub struct VarDef<'a> {
value: Value<'a>,
offset: usize,
}
struct State<'a> {
text: &'a str,
env_vars: &'a dyn VariableMap,
recorder: &'a mut dyn Recorder,
vars: HashMap<String, VarDef<'a>>,
last_ordered: usize,
max_match: usize,
}
impl<'a> State<'a> {
fn new(
text: &'a str,
env_vars: &'a dyn VariableMap,
recorder: &'a mut dyn Recorder,
) -> State<'a> {
State {
text,
env_vars,
recorder,
vars: HashMap::new(),
last_ordered: 0,
max_match: 0,
}
}
fn def_offset(&self, var: &str) -> usize {
self.vars
.get(var)
.map(|&VarDef { offset, .. }| offset)
.unwrap_or(0)
}
fn bol(&self, pos: usize) -> usize {
if let Some(offset) = self.text[pos..].find('\n') {
pos + offset + 1
} else {
self.text.len()
}
}
fn check(&self) -> MatchRange {
(self.max_match, self.text.len())
}
fn sameln(&self) -> MatchRange {
let b = self.max_match;
let e = self.bol(b);
(b, e)
}
fn nextln(&self) -> MatchRange {
let b = self.bol(self.max_match);
let e = self.bol(b);
(b, e)
}
fn unordered_begin(&self, pat: &Pattern) -> usize {
pat.parts()
.iter()
.filter_map(|part| part.ref_var())
.map(|var| self.def_offset(var))
.fold(self.last_ordered, max)
}
fn unordered(&self, pat: &Pattern) -> MatchRange {
(self.unordered_begin(pat), self.text.len())
}
fn match_positive(&mut self, pat: &Pattern, range: MatchRange) -> Result<Option<MatchRange>> {
let rx = pat.resolve(self)?;
let txt = &self.text[range.0..range.1];
let defs = pat.defs();
let matched_range = if defs.is_empty() {
rx.find(txt)
} else {
rx.captures(txt).map(|caps| {
let matched_range = caps.get(0).expect("whole expression must match");
for var in defs {
let txtval = caps.name(var).map(|mat| mat.as_str()).unwrap_or("");
self.recorder.defined_var(var, txtval);
let vardef = VarDef {
value: Value::Text(Cow::Borrowed(txtval)),
offset: range.0 + matched_range.end(),
};
self.vars.insert(var.clone(), vardef);
}
matched_range
})
};
Ok(if let Some(mat) = matched_range {
let r = (range.0 + mat.start(), range.0 + mat.end());
self.recorder.matched_check(rx.as_str(), r);
Some(r)
} else {
self.recorder.missed_check(rx.as_str(), range);
None
})
}
}
impl<'a> VariableMap for State<'a> {
fn lookup(&self, varname: &str) -> Option<Value> {
if let Some(&VarDef { ref value, .. }) = self.vars.get(varname) {
Some(value.clone())
} else {
self.env_vars.lookup(varname)
}
}
}
impl Display for Directive {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
use self::Directive::*;
match *self {
Check(ref pat) => writeln!(f, "check: {}", pat),
SameLn(ref pat) => writeln!(f, "sameln: {}", pat),
NextLn(ref pat) => writeln!(f, "nextln: {}", pat),
Unordered(ref pat) => writeln!(f, "unordered: {}", pat),
Not(ref pat) => writeln!(f, "not: {}", pat),
Regex(ref var, ref rx) => writeln!(f, "regex: {}={}", var, rx),
}
}
}
impl Display for Checker {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
for (idx, dir) in self.directives.iter().enumerate() {
write!(f, "#{} {}", idx, dir)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::CheckerBuilder;
use crate::error::Error;
fn e2s(e: Error) -> String {
e.to_string()
}
#[test]
fn directive() {
let mut b = CheckerBuilder::new();
assert_eq!(b.directive("not here: more text").map_err(e2s), Ok(false));
assert_eq!(
b.directive("not here: regex: X=more text").map_err(e2s),
Ok(true)
);
assert_eq!(
b.directive("regex: X = tommy").map_err(e2s),
Err("expected '=' after variable 'X' in regex: X = tommy".to_string(),)
);
assert_eq!(
b.directive("[arm]not: patt $x $(y) here").map_err(e2s),
Ok(true)
);
assert_eq!(
b.directive("[x86]sameln: $x $(y=[^]]*) there").map_err(e2s),
Ok(true)
);
assert_eq!(b.directive("regex: Y=foo\r").map_err(e2s), Ok(true));
let c = b.finish();
assert_eq!(
c.to_string(),
"#0 regex: X=more text\n#1 not: patt $(x) $(y) here\n#2 sameln: $(x) \
$(y=[^]]*) there\n#3 regex: Y=foo\n"
);
}
}