use alloc::boxed::Box;
use alloc::format;
use alloc::string::String;
use alloc::vec::Vec;
use core::cmp::{max, min};
use bit_set::BitSet;
use crate::alloc::string::ToString;
use crate::parse::ExprTree;
use crate::vm::CaptureGroupRange;
use crate::{Assertion, AstNode, CaptureGroupTarget, CompileError, Error, Expr, Result};
#[cfg(not(feature = "std"))]
use alloc::collections::BTreeMap as Map;
#[cfg(feature = "std")]
use std::collections::HashMap as Map;
#[derive(Debug)]
pub struct Info<'a> {
pub(crate) capture_groups: CaptureGroupRange,
pub min_size: usize,
pub max_size: usize,
pub const_size: bool,
pub(crate) min_pos_in_group: usize,
pub hard: bool,
pub expr: &'a Expr,
pub children: Vec<Info<'a>>,
}
impl<'a> Info<'a> {
pub fn start_group(&self) -> usize {
self.capture_groups.start()
}
pub fn end_group(&self) -> usize {
self.capture_groups.end()
}
pub(crate) fn push_literal(&self, buf: &mut String) {
match *self.expr {
Expr::Literal { ref val, .. } => buf.push_str(val),
Expr::Concat(_) => {
for child in &self.children {
child.push_literal(buf);
}
}
_ => panic!("push_literal called on non-literal"),
}
}
pub(crate) fn is_literal_get_casei(&self) -> Option<bool> {
match *self.expr {
Expr::Literal { casei, .. } => Some(casei),
Expr::Concat(_) => self.children.iter().try_fold(false, |any, child| {
child.is_literal_get_casei().map(|c| any || c)
}),
_ => None,
}
}
pub(crate) fn push_literal_chars(&self, buf: &mut Vec<(char, bool)>) {
match *self.expr {
Expr::Literal { ref val, casei } => buf.extend(val.chars().map(|c| (c, casei))),
Expr::Concat(_) => {
for child in &self.children {
child.push_literal_chars(buf);
}
}
_ => panic!("push_literal_chars called on non-literal"),
}
}
}
struct SizeInfo {
min_size: usize,
const_size: bool,
max_size: usize,
}
#[derive(Debug, Clone)]
struct SubroutineCallInfo {
target_group: usize,
min_pos: usize,
}
struct Analyzer<'a> {
backrefs: &'a BitSet,
next_group_number: usize,
group_info: Map<usize, SizeInfo>,
subroutine_calls: Map<usize, Vec<SubroutineCallInfo>>,
root_groups: BitSet,
group_exprs: Map<usize, &'a Expr>,
analyzing_groups: BitSet,
find_not_empty: bool,
disallow_empty_match_at_eof_after_newline: bool,
allow_input_assertion_overrides: bool,
#[cfg(feature = "leftmost_longest")]
leftmost_longest: bool,
in_lookaround: bool,
}
impl<'a> Analyzer<'a> {
fn visit(
&mut self,
expr: &'a Expr,
min_pos_in_group: usize,
inside_zero_rep: bool,
enclosing_group: usize,
) -> Result<Info<'a>> {
let start_group = self.next_group_number;
let mut children = match *expr {
Expr::Concat(ref v) | Expr::Alt(ref v) => Vec::with_capacity(v.len()),
_ => Vec::new(),
};
let mut min_size: usize = 0;
let mut const_size = false;
let mut max_size: usize = 0;
let mut hard = false;
match *expr {
Expr::Assertion(assertion) if assertion.is_always_hard() => {
const_size = true;
hard = true;
}
Expr::Assertion(Assertion::StartLineOniguruma { .. }) if !self.in_lookaround => {
const_size = true;
hard = true;
}
Expr::Assertion(Assertion::StartText | Assertion::EndText)
if self.allow_input_assertion_overrides =>
{
const_size = true;
hard = true;
}
Expr::Assertion(Assertion::EndText)
if self.disallow_empty_match_at_eof_after_newline =>
{
const_size = true;
hard = true; }
Expr::Assertion(Assertion::EndText) if self.find_not_empty && !self.in_lookaround => {
const_size = true;
hard = true;
}
Expr::Empty | Expr::Assertion(_) => {
const_size = true;
}
Expr::Any { .. } => {
min_size = 1;
const_size = true;
max_size = 1;
}
Expr::GeneralNewline { .. } => {
min_size = 1;
const_size = false;
hard = true; max_size = 2;
}
Expr::Literal { ref val, casei } => {
min_size = 1;
const_size = literal_const_size(val, casei);
max_size = 1;
}
Expr::Concat(ref v) => {
const_size = true;
let mut pos_in_group = min_pos_in_group;
for child in v {
let child_info =
self.visit(child, pos_in_group, inside_zero_rep, enclosing_group)?;
min_size = min_size.saturating_add(child_info.min_size);
max_size = if max_size == usize::MAX || child_info.max_size == usize::MAX {
usize::MAX
} else {
max_size.saturating_add(child_info.max_size)
};
const_size &= child_info.const_size;
hard |= child_info.hard;
pos_in_group = pos_in_group.saturating_add(child_info.min_size);
children.push(child_info);
}
}
Expr::Alt(ref v) => {
let child_info =
self.visit(&v[0], min_pos_in_group, inside_zero_rep, enclosing_group)?;
min_size = child_info.min_size;
max_size = child_info.max_size;
const_size = child_info.const_size;
hard = child_info.hard;
children.push(child_info);
for child in &v[1..] {
let child_info =
self.visit(child, min_pos_in_group, inside_zero_rep, enclosing_group)?;
const_size &= child_info.const_size && min_size == child_info.min_size;
min_size = min(min_size, child_info.min_size);
max_size = max(max_size, child_info.max_size);
hard |= child_info.hard;
children.push(child_info);
}
}
Expr::Group(ref child) => {
let group = self.next_group_number;
self.next_group_number += 1;
self.analyzing_groups.insert(group);
if enclosing_group == 0 && !inside_zero_rep {
self.root_groups.insert(group);
}
let child_info = self.visit(child, 0, inside_zero_rep, group)?;
self.analyzing_groups.remove(group);
min_size = child_info.min_size;
max_size = child_info.max_size;
const_size = child_info.const_size;
self.group_info.insert(
group,
SizeInfo {
min_size,
const_size,
max_size,
},
);
hard = child_info.hard | self.backrefs.contains(group);
children.push(child_info);
}
Expr::LookAround(ref child, _) => {
let was_in_lookaround = self.in_lookaround;
self.in_lookaround = true;
let child_info =
self.visit(child, min_pos_in_group, inside_zero_rep, enclosing_group)?;
self.in_lookaround = was_in_lookaround;
const_size = true;
hard = true;
children.push(child_info);
}
Expr::Repeat {
ref child,
lo,
hi,
#[cfg(feature = "leftmost_longest")]
greedy,
..
} => {
#[cfg(feature = "leftmost_longest")]
if !greedy && self.leftmost_longest {
return Err(Error::CompileError(Box::new(
CompileError::FeatureNotYetSupported(
"non-greedy quantifiers are not supported in leftmost-longest mode"
.to_string(),
),
)));
}
let child_inside_zero_rep = if lo == 0 && hi == 0 {
true
} else {
inside_zero_rep
};
let child_info = self.visit(
child,
min_pos_in_group,
child_inside_zero_rep,
enclosing_group,
)?;
min_size = child_info.min_size * lo;
max_size = if hi == usize::MAX || child_info.max_size == usize::MAX {
usize::MAX
} else {
child_info.max_size.saturating_mul(hi)
};
const_size = child_info.const_size && lo == hi;
hard = child_info.hard;
children.push(child_info);
}
Expr::Delegate { .. } => {
min_size = 1;
const_size = true;
max_size = 1;
}
Expr::Backref { group, .. } => {
if group == 0 {
return Err(Error::CompileError(Box::new(CompileError::InvalidBackref(
group,
))));
}
if self.analyzing_groups.contains(group) {
if let Some(calls) = self.subroutine_calls.get(&group) {
if calls.iter().any(|call| call.target_group == group) {
return Err(Error::CompileError(Box::new(
CompileError::FeatureNotYetSupported(
"Backreference to a capture group from within the same group when it's being recursed".to_string()
)
)));
}
}
}
if let Some(&SizeInfo {
min_size: group_min_size,
max_size: group_max_size,
const_size: group_const_size,
..
}) = self.group_info.get(&group)
{
min_size = group_min_size;
max_size = group_max_size;
const_size = group_const_size;
}
hard = true;
}
Expr::AtomicGroup(ref child) => {
let child_info =
self.visit(child, min_pos_in_group, inside_zero_rep, enclosing_group)?;
min_size = child_info.min_size;
max_size = child_info.max_size;
const_size = child_info.const_size;
hard = true; children.push(child_info);
}
Expr::KeepOut => {
hard = true;
const_size = true;
}
Expr::ContinueFromPreviousMatchEnd => {
hard = true;
const_size = true;
}
Expr::BackrefExistsCondition { .. } => {
hard = true;
const_size = true;
}
Expr::BacktrackingControlVerb(_) => {
hard = true;
const_size = true;
}
Expr::Conditional {
ref condition,
ref true_branch,
ref false_branch,
} => {
hard = true;
let child_info_condition = self.visit(
condition,
min_pos_in_group,
inside_zero_rep,
enclosing_group,
)?;
let child_info_truth = self.visit(
true_branch,
min_pos_in_group + child_info_condition.min_size,
inside_zero_rep,
enclosing_group,
)?;
let child_info_false = self.visit(
false_branch,
min_pos_in_group,
inside_zero_rep,
enclosing_group,
)?;
min_size = child_info_condition.min_size
+ min(child_info_truth.min_size, child_info_false.min_size);
max_size = max(child_info_truth.max_size, child_info_false.max_size);
const_size = child_info_condition.const_size
&& child_info_truth.const_size
&& child_info_false.const_size
&& child_info_condition.min_size + child_info_truth.min_size == child_info_false.min_size;
children.push(child_info_condition);
children.push(child_info_truth);
children.push(child_info_false);
}
Expr::SubroutineCall(target_group) => {
if !inside_zero_rep || enclosing_group != 0 {
self.subroutine_calls
.entry(enclosing_group)
.or_default()
.push(SubroutineCallInfo {
target_group,
min_pos: min_pos_in_group,
});
}
if let Some(&SizeInfo {
min_size: group_min_size,
const_size: group_const_size,
max_size: group_max_size,
}) = self.group_info.get(&target_group)
{
min_size = group_min_size;
max_size = group_max_size;
const_size = group_const_size;
} else if self.analyzing_groups.contains(target_group) {
min_size = 0;
max_size = usize::MAX;
const_size = false;
} else if let Some(&group_expr) = self.group_exprs.get(&target_group) {
self.analyzing_groups.insert(target_group);
let prev_next_group_number = self.next_group_number;
self.next_group_number = target_group + 1;
let group_info = self.visit(group_expr, 0, inside_zero_rep, target_group)?;
self.next_group_number = prev_next_group_number;
self.analyzing_groups.remove(target_group);
min_size = group_info.min_size;
max_size = group_info.max_size;
const_size = group_info.const_size;
self.group_info.insert(
target_group,
SizeInfo {
min_size,
const_size,
max_size,
},
);
} else {
min_size = 0;
max_size = usize::MAX;
const_size = false;
}
hard = true;
}
Expr::AstNode(ref astnode, ix) => {
match astnode {
AstNode::SubroutineCall(CaptureGroupTarget::ByName(name)) => {
return Err(Error::CompileError(Box::new(
CompileError::SubroutineCallTargetNotFound(
format!("named group '{}'", name),
ix,
),
)));
}
AstNode::SubroutineCall(CaptureGroupTarget::ByNumber(n)) => {
return Err(Error::CompileError(Box::new(
CompileError::SubroutineCallTargetNotFound(
format!("group number {}", n),
ix,
),
)));
}
AstNode::SubroutineCall(CaptureGroupTarget::Relative(n)) => {
return Err(Error::CompileError(Box::new(
CompileError::SubroutineCallTargetNotFound(
format!("relative group {}{}", if *n >= 0 { "+" } else { "" }, n),
ix,
),
)));
}
AstNode::AstGroup { .. }
| AstNode::Backref { .. }
| AstNode::BackrefExistsCondition { .. } => {
return Err(Error::CompileError(Box::new(
CompileError::UnresolvedAstNode(ix, format!("{:?}", astnode)),
)));
}
}
}
Expr::BackrefWithRelativeRecursionLevel { .. } => {
return Err(Error::CompileError(Box::new(
CompileError::FeatureNotYetSupported("Backref at recursion level".to_string()),
)));
}
Expr::Absent(ref absent) => {
use crate::Absent::*;
match absent {
Repeater(ref child) => {
let child_info =
self.visit(child, min_pos_in_group, inside_zero_rep, enclosing_group)?;
min_size = 0;
max_size = usize::MAX;
const_size = false;
hard = true;
children.push(child_info);
}
Expression {
ref absent,
ref exp,
} => {
let absent_info =
self.visit(absent, min_pos_in_group, inside_zero_rep, enclosing_group)?;
let exp_info =
self.visit(exp, min_pos_in_group, inside_zero_rep, enclosing_group)?;
min_size = exp_info.min_size;
max_size = exp_info.max_size;
const_size = false;
hard = true;
children.push(absent_info);
children.push(exp_info);
}
Stopper(ref child) => {
let child_info =
self.visit(child, min_pos_in_group, inside_zero_rep, enclosing_group)?;
min_size = 0;
max_size = 0;
const_size = true;
hard = true;
children.push(child_info);
}
Clear => {
min_size = 0;
max_size = 0;
const_size = true;
hard = true;
}
}
}
Expr::DefineGroup { ref definitions } => {
let def_info = self.visit(definitions, 0, inside_zero_rep, enclosing_group)?;
min_size = 0;
max_size = 0;
const_size = true;
children.push(def_info);
}
};
if self.find_not_empty && min_size == 0 && !const_size {
hard = true;
}
#[cfg(feature = "leftmost_longest")]
if self.leftmost_longest && !const_size {
hard = true;
}
Ok(Info {
expr,
children,
capture_groups: CaptureGroupRange(start_group, self.next_group_number),
min_size,
max_size,
const_size,
hard,
min_pos_in_group,
})
}
fn check_left_recursion(&self, named_groups: &Map<String, usize>) -> Result<()> {
let reachable_groups = self.compute_reachable_groups();
for &start_group in self.subroutine_calls.keys() {
if !reachable_groups.contains(start_group) {
continue;
}
let mut visited = BitSet::new();
let mut recursion_stack = BitSet::new();
if self.dfs_check_left_recursion(start_group, &mut visited, &mut recursion_stack)? {
let mut group_names: Map<usize, String> = Map::new();
for (name, &group_num) in named_groups.iter() {
group_names.insert(group_num, name.clone());
}
let group_desc = if let Some(name) = group_names.get(&start_group) {
format!("group '{}' ({})", name, start_group)
} else {
format!("group {}", start_group)
};
return Err(Error::CompileError(Box::new(
CompileError::LeftRecursiveSubroutineCall(group_desc),
)));
}
}
Ok(())
}
fn check_unbounded_recursion(&self, root_expr: &'a Expr) -> Result<()> {
let mut memo = Map::new();
if self.group_can_terminate(0, root_expr, &mut BitSet::new(), &mut memo) {
return Ok(());
}
Err(Error::CompileError(Box::new(
CompileError::NeverEndingRecursion,
)))
}
fn group_can_terminate(
&self,
group: usize,
root_expr: &'a Expr,
recursion_stack: &mut BitSet,
memo: &mut Map<usize, bool>,
) -> bool {
if let Some(&can_terminate) = memo.get(&group) {
return can_terminate;
}
if recursion_stack.contains(group) {
return false;
}
let expr = if group == 0 {
root_expr
} else if let Some(&group_expr) = self.group_exprs.get(&group) {
group_expr
} else {
return true;
};
recursion_stack.insert(group);
let can_terminate = self.expr_can_terminate(expr, root_expr, recursion_stack, memo);
recursion_stack.remove(group);
memo.insert(group, can_terminate);
can_terminate
}
fn expr_can_terminate(
&self,
expr: &'a Expr,
root_expr: &'a Expr,
recursion_stack: &mut BitSet,
memo: &mut Map<usize, bool>,
) -> bool {
match expr {
Expr::Concat(children) => children
.iter()
.all(|child| self.expr_can_terminate(child, root_expr, recursion_stack, memo)),
Expr::Alt(children) => children
.iter()
.any(|child| self.expr_can_terminate(child, root_expr, recursion_stack, memo)),
Expr::Group(child) => self.expr_can_terminate(child, root_expr, recursion_stack, memo),
Expr::LookAround(child, _) => {
self.expr_can_terminate(child, root_expr, recursion_stack, memo)
}
Expr::AtomicGroup(child) => {
self.expr_can_terminate(child, root_expr, recursion_stack, memo)
}
Expr::Repeat { child, lo, .. } => {
*lo == 0 || self.expr_can_terminate(child, root_expr, recursion_stack, memo)
}
Expr::Conditional {
condition,
true_branch,
false_branch,
} => {
self.expr_can_terminate(false_branch, root_expr, recursion_stack, memo)
|| (self.expr_can_terminate(condition, root_expr, recursion_stack, memo)
&& self.expr_can_terminate(true_branch, root_expr, recursion_stack, memo))
}
Expr::SubroutineCall(target_group) => {
self.group_can_terminate(*target_group, root_expr, recursion_stack, memo)
}
Expr::Absent(absent) => {
use crate::Absent::*;
match absent {
Repeater(child) | Stopper(child) => {
self.expr_can_terminate(child, root_expr, recursion_stack, memo)
}
Expression { absent, exp } => {
self.expr_can_terminate(absent, root_expr, recursion_stack, memo)
&& self.expr_can_terminate(exp, root_expr, recursion_stack, memo)
}
Clear => true,
}
}
Expr::BackrefWithRelativeRecursionLevel { .. } => true,
Expr::DefineGroup { definitions } => {
self.expr_can_terminate(definitions, root_expr, recursion_stack, memo)
}
_ => true,
}
}
fn compute_reachable_groups(&self) -> BitSet {
let mut reachable = BitSet::new();
let mut to_visit = Vec::new();
reachable.insert(0);
to_visit.push(0);
for group in self.root_groups.iter() {
if !reachable.contains(group) {
reachable.insert(group);
to_visit.push(group);
}
}
while let Some(group) = to_visit.pop() {
if let Some(calls) = self.subroutine_calls.get(&group) {
for call_info in calls {
if !reachable.contains(call_info.target_group) {
reachable.insert(call_info.target_group);
to_visit.push(call_info.target_group);
}
}
}
}
reachable
}
fn dfs_check_left_recursion(
&self,
group: usize,
visited: &mut BitSet,
recursion_stack: &mut BitSet,
) -> Result<bool> {
if recursion_stack.contains(group) {
return Ok(true);
}
if visited.contains(group) {
return Ok(false);
}
visited.insert(group);
recursion_stack.insert(group);
if let Some(calls) = self.subroutine_calls.get(&group) {
for call_info in calls {
if call_info.min_pos == 0
&& self.dfs_check_left_recursion(
call_info.target_group,
visited,
recursion_stack,
)?
{
return Ok(true);
}
}
}
recursion_stack.remove(group);
Ok(false)
}
}
fn literal_const_size(_: &str, _: bool) -> bool {
true
}
fn collect_groups<'a>(
expr: &'a Expr,
next_group_number: &mut usize,
groups: &mut Map<usize, &'a Expr>,
) {
match expr {
Expr::Group(inner) => {
let current_group = *next_group_number;
*next_group_number += 1;
groups.insert(current_group, inner.as_ref());
collect_groups(inner.as_ref(), next_group_number, groups);
}
_ => {
for child in expr.children_iter() {
collect_groups(child, next_group_number, groups);
}
}
}
}
#[derive(Debug, Clone, Default)]
pub struct AnalyzeContext {
pub explicit_capture_group_0: bool,
pub find_not_empty: bool,
pub disallow_empty_match_at_eof_after_newline: bool,
pub allow_input_assertion_overrides: bool,
#[cfg(feature = "leftmost_longest")]
pub leftmost_longest: bool,
}
pub fn analyze<'a>(tree: &'a ExprTree, ctx: AnalyzeContext) -> Result<Info<'a>> {
let explicit_capture_group_0 = ctx.explicit_capture_group_0;
let find_not_empty = ctx.find_not_empty;
let disallow_empty_match_at_eof_after_newline = ctx.disallow_empty_match_at_eof_after_newline;
let allow_input_assertion_overrides = ctx.allow_input_assertion_overrides;
#[cfg(feature = "leftmost_longest")]
let leftmost_longest = ctx.leftmost_longest;
if tree.numbered_groups_ignored
&& tree.numeric_capture_group_references
&& !tree.named_groups.is_empty()
{
return Err(Error::CompileError(Box::new(
CompileError::NamedBackrefOnly,
)));
}
let start_group = if explicit_capture_group_0 { 0 } else { 1 };
let group_exprs = if tree.contains_subroutines {
let mut groups = Map::new();
let mut next_group_number = start_group;
collect_groups(&tree.expr, &mut next_group_number, &mut groups);
groups
} else {
Map::new()
};
let mut analyzer = Analyzer {
backrefs: &tree.backrefs,
next_group_number: start_group,
group_info: Map::new(),
subroutine_calls: Map::new(),
root_groups: BitSet::new(),
group_exprs,
analyzing_groups: BitSet::new(),
find_not_empty,
disallow_empty_match_at_eof_after_newline,
allow_input_assertion_overrides,
#[cfg(feature = "leftmost_longest")]
leftmost_longest,
in_lookaround: false,
};
let mut analyzed = analyzer.visit(&tree.expr, 0, false, 0)?;
let max_valid_group = if explicit_capture_group_0 {
tree.total_groups.saturating_sub(1)
} else {
tree.total_groups
};
if let Some(group) = tree.out_of_range_backref {
return Err(Error::CompileError(Box::new(CompileError::InvalidBackref(
group,
))));
}
for group in tree.backrefs.iter() {
if group < start_group || group > max_valid_group {
return Err(Error::CompileError(Box::new(CompileError::InvalidBackref(
group,
))));
}
}
if tree.contains_subroutines {
analyzer.check_left_recursion(&tree.named_groups)?;
analyzer.check_unbounded_recursion(&tree.expr)?;
}
if analyzed.min_size == 0 && disallow_empty_match_at_eof_after_newline {
analyzed.hard = true;
}
Ok(analyzed)
}
pub fn can_compile_as_anchored(root_expr: &Expr) -> bool {
match root_expr {
Expr::Concat(children) => match children[0] {
Expr::Assertion(assertion) => assertion == Assertion::StartText,
Expr::ContinueFromPreviousMatchEnd => true,
_ => false,
},
Expr::Assertion(assertion) => *assertion == Assertion::StartText,
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::{analyze, AnalyzeContext};
use crate::parse::ExprTree;
use crate::{can_compile_as_anchored, CompileError, Error, Expr};
use matches::assert_matches;
#[cfg_attr(feature = "track_caller", track_caller)]
fn assert_analyze_ok(tree: &ExprTree, ctx: AnalyzeContext) {
match analyze(tree, ctx) {
Ok(_) => {}
Err(e) => panic!("expected analyze to succeed, but got error: {:?}", e),
}
}
#[cfg_attr(feature = "track_caller", track_caller)]
fn assert_compile_error<T, F>(result: crate::Result<T>, check: F)
where
F: FnOnce(&CompileError) -> bool,
{
match result {
Err(Error::CompileError(ref e)) if check(e) => {}
other => panic!(
"expected a matching CompileError, but got: {:?}",
other.err()
),
}
}
#[cfg_attr(feature = "track_caller", track_caller)]
fn assert_invalid_backref(
pattern: &str,
explicit_capture_group_0: bool,
expected_group: usize,
) {
let tree = Expr::parse_tree(pattern).unwrap();
assert_compile_error(
analyze(
&tree,
AnalyzeContext {
explicit_capture_group_0,
..Default::default()
},
),
|e| matches!(e, CompileError::InvalidBackref(g) if *g == expected_group),
);
}
#[test]
fn invalid_backref_zero() {
assert_invalid_backref(r".\0", false, 0);
assert_invalid_backref(r".\0", true, 0);
assert_invalid_backref(r"(.)\0", false, 0);
assert_invalid_backref(r"(.)\0", true, 0);
assert_invalid_backref(r"(.)\0\1", false, 0);
}
#[test]
fn invalid_backref_no_captures() {
assert_invalid_backref(r"aa\1", false, 1);
assert_invalid_backref(r"aaaa\2", false, 2);
}
#[test]
fn invalid_backref_unreasonably_large_number() {
assert_invalid_backref(r".\1999999999", false, 1999999999);
}
#[test]
fn invalid_backref_with_captures() {
assert_invalid_backref(r"a(a)\2", false, 2);
assert_invalid_backref(r"a(a)\2\1", false, 2);
}
#[test]
fn invalid_backref_with_captures_explict_capture_group_zero() {
assert_invalid_backref(r"(a(b)\2)c", true, 2);
assert_invalid_backref(r"(a(b)\1\2)c", true, 2);
assert_invalid_backref(r"(a\1)b", true, 1);
assert_invalid_backref(r"(a(b))\2", true, 2);
}
#[test]
fn unresolved_subroutine_call_error_takes_precedence_over_invalid_backref() {
let tree = Expr::parse_tree(r"(?<a>a)(?<b>b)\g<no_exist>(?<c>c)\k<a>\k<c>").unwrap();
let result = analyze(&tree, AnalyzeContext::default());
assert_compile_error(
result,
|e| matches!(e, CompileError::SubroutineCallTargetNotFound(s, _) if s.contains("no_exist")),
);
}
#[test]
fn allow_analysis_of_self_backref() {
assert!(!analyze(
&Expr::parse_tree(r"(.\1)").unwrap(),
AnalyzeContext::default()
)
.is_err());
assert!(!analyze(
&Expr::parse_tree(r"((.\1))").unwrap(),
AnalyzeContext {
explicit_capture_group_0: true,
..Default::default()
}
)
.is_err());
assert!(!analyze(
&Expr::parse_tree(r"(([ab]+)\1b)").unwrap(),
AnalyzeContext::default()
)
.is_err());
assert!(!analyze(
&Expr::parse_tree(r"(([ab]+?)(?(1)\1| )c)+").unwrap(),
AnalyzeContext::default(),
)
.is_err());
}
#[test]
fn allow_backref_even_when_capture_group_occurs_after_backref() {
assert!(!analyze(
&Expr::parse_tree(r"\1(.)").unwrap(),
AnalyzeContext::default()
)
.is_err());
assert!(!analyze(
&Expr::parse_tree(r"(\1(.))").unwrap(),
AnalyzeContext {
explicit_capture_group_0: true,
..Default::default()
}
)
.is_err());
}
#[test]
fn valid_backref_occurs_after_capture_group() {
assert!(!analyze(
&Expr::parse_tree(r"(.)\1").unwrap(),
AnalyzeContext::default()
)
.is_err());
assert!(!analyze(
&Expr::parse_tree(r"((.)\1)").unwrap(),
AnalyzeContext {
explicit_capture_group_0: true,
..Default::default()
}
)
.is_err());
assert!(!analyze(
&Expr::parse_tree(r"((.)\2\2)\1").unwrap(),
AnalyzeContext::default()
)
.is_err());
assert!(!analyze(
&Expr::parse_tree(r"(.)\1(.)\2").unwrap(),
AnalyzeContext::default()
)
.is_err());
assert!(!analyze(
&Expr::parse_tree(r"(.)foo(.)\2").unwrap(),
AnalyzeContext::default()
)
.is_err());
assert!(!analyze(
&Expr::parse_tree(r"(.)(foo)(.)\3\2\1").unwrap(),
AnalyzeContext::default(),
)
.is_err());
assert!(!analyze(
&Expr::parse_tree(r"(.)(foo)(.)\3\1").unwrap(),
AnalyzeContext::default()
)
.is_err());
assert!(!analyze(
&Expr::parse_tree(r"(.)(foo)(.)\2\1").unwrap(),
AnalyzeContext::default()
)
.is_err());
}
#[test]
fn feature_not_yet_supported() {
let tree = &Expr::parse_tree(r"(a)\k<1-0>").unwrap();
assert_compile_error(analyze(tree, AnalyzeContext::default()), |e| {
matches!(e, CompileError::FeatureNotYetSupported(_))
});
}
#[test]
fn subroutine_call_undefined() {
let tree = &Expr::parse_tree(r"\g<wrong_name>(?<different_name>a)").unwrap();
assert_compile_error(analyze(tree, AnalyzeContext::default()), |e| {
matches!(e, CompileError::SubroutineCallTargetNotFound(_, _))
});
}
#[test]
fn subroutine_call_undefined_by_name_message() {
let tree = &Expr::parse_tree(r"\g<wrong_name>(?<different_name>a)").unwrap();
assert_compile_error(
analyze(tree, AnalyzeContext::default()),
|e| matches!(e, CompileError::SubroutineCallTargetNotFound(s, _) if s.contains("wrong_name")),
);
}
#[test]
fn subroutine_call_undefined_by_number() {
use crate::parse::NamedGroups;
use bit_set::BitSet;
let tree = ExprTree {
expr: Expr::AstNode(
crate::AstNode::SubroutineCall(crate::CaptureGroupTarget::ByNumber(99)),
0,
),
backrefs: BitSet::new(),
named_groups: NamedGroups::default(),
numeric_capture_group_references: false,
contains_subroutines: true,
self_recursive: false,
total_groups: 0,
out_of_range_backref: None,
numbered_groups_ignored: false,
};
assert_compile_error(
analyze(&tree, AnalyzeContext::default()),
|e| matches!(e, CompileError::SubroutineCallTargetNotFound(s, _) if s.contains("99")),
);
}
#[test]
fn subroutine_call_undefined_by_relative() {
use crate::parse::NamedGroups;
use bit_set::BitSet;
let tree = ExprTree {
expr: Expr::AstNode(
crate::AstNode::SubroutineCall(crate::CaptureGroupTarget::Relative(-1)),
0,
),
backrefs: BitSet::new(),
named_groups: NamedGroups::default(),
numeric_capture_group_references: false,
contains_subroutines: true,
self_recursive: false,
total_groups: 0,
out_of_range_backref: None,
numbered_groups_ignored: false,
};
assert_compile_error(
analyze(&tree, AnalyzeContext::default()),
|e| matches!(e, CompileError::SubroutineCallTargetNotFound(s, _) if s.contains("-1")),
);
}
#[test]
fn unresolved_ast_node_error() {
use crate::parse::NamedGroups;
use bit_set::BitSet;
let tree = ExprTree {
expr: Expr::AstNode(
crate::AstNode::Backref {
target: crate::CaptureGroupTarget::ByNumber(1),
casei: false,
relative_recursion_level: None,
},
0,
),
backrefs: BitSet::new(),
named_groups: NamedGroups::default(),
numeric_capture_group_references: false,
contains_subroutines: false,
self_recursive: false,
total_groups: 0,
out_of_range_backref: None,
numbered_groups_ignored: false,
};
assert_compile_error(
analyze(&tree, AnalyzeContext::default()),
|e| matches!(e, CompileError::UnresolvedAstNode(_, s) if s.contains("Backref")),
);
}
#[test]
fn numeric_capture_group_references_cannot_be_used_with_named_groups() {
use crate::parse_flags::FLAG_IGNORE_NUMBERED_GROUPS_WHEN_NAMED_GROUPS_EXIST;
let tree = Expr::parse_tree_with_flags(
r"(?<name>a)\1",
FLAG_IGNORE_NUMBERED_GROUPS_WHEN_NAMED_GROUPS_EXIST,
)
.unwrap();
assert_compile_error(analyze(&tree, AnalyzeContext::default()), |e| {
matches!(e, CompileError::NamedBackrefOnly)
});
let tree = Expr::parse_tree_with_flags(
r"(a)\1(?<name>b)",
FLAG_IGNORE_NUMBERED_GROUPS_WHEN_NAMED_GROUPS_EXIST,
)
.unwrap();
assert_compile_error(analyze(&tree, AnalyzeContext::default()), |e| {
matches!(e, CompileError::NamedBackrefOnly)
});
let tree = Expr::parse_tree_with_flags(
r"(?<name>a)\1|b",
FLAG_IGNORE_NUMBERED_GROUPS_WHEN_NAMED_GROUPS_EXIST,
)
.unwrap();
assert_compile_error(analyze(&tree, AnalyzeContext::default()), |e| {
matches!(e, CompileError::NamedBackrefOnly)
});
let tree = Expr::parse_tree_with_flags(
r"(?<name>a)|\1",
FLAG_IGNORE_NUMBERED_GROUPS_WHEN_NAMED_GROUPS_EXIST,
)
.unwrap();
assert_compile_error(analyze(&tree, AnalyzeContext::default()), |e| {
matches!(e, CompileError::NamedBackrefOnly)
});
let tree = Expr::parse_tree_with_flags(
r"(a|(?<name>b))\1",
FLAG_IGNORE_NUMBERED_GROUPS_WHEN_NAMED_GROUPS_EXIST,
)
.unwrap();
assert_compile_error(analyze(&tree, AnalyzeContext::default()), |e| {
matches!(e, CompileError::NamedBackrefOnly)
});
let tree = Expr::parse_tree_with_flags(
r"(?<x>a)|(?<y>b)|\1",
FLAG_IGNORE_NUMBERED_GROUPS_WHEN_NAMED_GROUPS_EXIST,
)
.unwrap();
assert_compile_error(analyze(&tree, AnalyzeContext::default()), |e| {
matches!(e, CompileError::NamedBackrefOnly)
});
let tree = Expr::parse_tree_with_flags(
r"(?<foo>\w+)\g<1>",
FLAG_IGNORE_NUMBERED_GROUPS_WHEN_NAMED_GROUPS_EXIST,
)
.unwrap();
assert_compile_error(analyze(&tree, AnalyzeContext::default()), |e| {
matches!(e, CompileError::NamedBackrefOnly)
});
let tree = Expr::parse_tree_with_flags(
r"(?<foo>a)|\g<1>",
FLAG_IGNORE_NUMBERED_GROUPS_WHEN_NAMED_GROUPS_EXIST,
)
.unwrap();
assert_compile_error(analyze(&tree, AnalyzeContext::default()), |e| {
matches!(e, CompileError::NamedBackrefOnly)
});
let tree = Expr::parse_tree_with_flags(
r"(a)(b+)\1\g<2>",
FLAG_IGNORE_NUMBERED_GROUPS_WHEN_NAMED_GROUPS_EXIST,
)
.unwrap();
assert_analyze_ok(&tree, AnalyzeContext::default());
let tree = Expr::parse_tree_with_flags(
r"(?<name>a)\k<name>\g<name>",
FLAG_IGNORE_NUMBERED_GROUPS_WHEN_NAMED_GROUPS_EXIST,
)
.unwrap();
assert_analyze_ok(&tree, AnalyzeContext::default());
let tree = Expr::parse_tree_with_flags(
r"(?<a>a)|(?<b>b)(c)(d+)",
FLAG_IGNORE_NUMBERED_GROUPS_WHEN_NAMED_GROUPS_EXIST,
)
.unwrap();
assert_analyze_ok(&tree, AnalyzeContext::default());
}
#[test]
fn is_literal() {
let tree = Expr::parse_tree("abc").unwrap();
let info = analyze(&tree, AnalyzeContext::default()).unwrap();
assert_eq!(info.is_literal_get_casei(), Some(false));
}
#[test]
fn is_literal_casei() {
let tree = Expr::parse_tree("(?i)abc").unwrap();
let info = analyze(&tree, AnalyzeContext::default()).unwrap();
assert_eq!(info.is_literal_get_casei(), Some(true));
}
#[test]
fn is_literal_with_repeat() {
let tree = Expr::parse_tree("abc*").unwrap();
let info = analyze(&tree, AnalyzeContext::default()).unwrap();
assert_eq!(info.is_literal_get_casei(), None);
}
#[test]
fn anchored_for_starttext_assertions() {
let tree = Expr::parse_tree(r"^(\w+)\1").unwrap();
assert_eq!(can_compile_as_anchored(&tree.expr), true);
let tree = Expr::parse_tree(r"^").unwrap();
assert_eq!(can_compile_as_anchored(&tree.expr), true);
}
#[test]
fn anchored_for_continue_from_prev_match_assertions() {
let tree = Expr::parse_tree(r"\G(\w+)\1").unwrap();
assert_eq!(can_compile_as_anchored(&tree.expr), true);
}
#[test]
fn backref_inherits_group_size_info() {
let tree = Expr::parse_tree(r"(abc)\1").unwrap();
let info = analyze(&tree, AnalyzeContext::default()).unwrap();
assert_eq!(info.min_size, 6);
assert!(info.const_size);
let tree = Expr::parse_tree(r"(a+)\1").unwrap();
let info = analyze(&tree, AnalyzeContext::default()).unwrap();
assert_eq!(info.min_size, 2);
assert!(!info.const_size);
let tree = Expr::parse_tree(r"(a?)\1").unwrap();
let info = analyze(&tree, AnalyzeContext::default()).unwrap();
assert_eq!(info.min_size, 0);
assert!(!info.const_size);
}
#[test]
fn backref_forward_reference() {
let tree = Expr::parse_tree(r"\1(abc)").unwrap();
let info = analyze(&tree, AnalyzeContext::default()).unwrap();
assert_eq!(info.min_size, 3);
assert!(!info.const_size);
}
#[test]
fn backref_in_lookbehind() {
assert!(!analyze(
&Expr::parse_tree(r"(hello)(?<=\b\1)").unwrap(),
AnalyzeContext::default(),
)
.is_err());
assert!(!analyze(
&Expr::parse_tree(r"(..)(?<=\1\1)").unwrap(),
AnalyzeContext::default()
)
.is_err());
assert!(!analyze(
&Expr::parse_tree(r"(abc)(?<=\1)def").unwrap(),
AnalyzeContext::default()
)
.is_err());
}
#[test]
fn backref_inside_recursed_group_not_supported() {
let tree = Expr::parse_tree(r"(?<foo>a|\(\g<foo>\)\k<foo>?)").unwrap();
assert_compile_error(
analyze(&tree, AnalyzeContext::default()),
|e| matches!(e, CompileError::FeatureNotYetSupported(s) if s.contains("Backreference") && s.contains("recursed")),
);
let tree = Expr::parse_tree(r"(\g<1>\1)").unwrap();
assert_compile_error(
analyze(&tree, AnalyzeContext::default()),
|e| matches!(e, CompileError::FeatureNotYetSupported(s) if s.contains("Backreference") && s.contains("recursed")),
);
let tree = Expr::parse_tree(r"(a|\g<1>b\1)").unwrap();
assert_compile_error(
analyze(&tree, AnalyzeContext::default()),
|e| matches!(e, CompileError::FeatureNotYetSupported(s) if s.contains("Backreference") && s.contains("recursed")),
);
}
#[test]
fn backref_outside_recursed_group_is_allowed() {
let tree = Expr::parse_tree(r"(?<foo>a|\(\g<foo>\))\k<foo>").unwrap();
let result = analyze(&tree, AnalyzeContext::default());
assert!(
result.is_ok(),
"Backref outside recursed group should pass analysis"
);
}
#[test]
fn not_anchored_for_startline_assertions() {
let tree = Expr::parse_tree(r"(?m)^(\w+)\1").unwrap();
assert_eq!(can_compile_as_anchored(&tree.expr), false);
}
#[test]
fn start_line_analysis() {
use crate::parse_flags::FLAG_ONIGURUMA_MODE;
use crate::Assertion;
let tree = Expr::parse_tree(r"(?m)^").unwrap();
let info = analyze(&tree, AnalyzeContext::default()).expect("can analyze start line");
assert_eq!(info.min_size, 0);
assert!(info.const_size);
assert!(!info.hard);
assert_matches!(info.expr, Expr::Assertion(Assertion::StartLine { .. }));
let tree = Expr::parse_tree_with_flags(r"(?m)^", FLAG_ONIGURUMA_MODE).unwrap();
let info =
analyze(&tree, AnalyzeContext::default()).expect("can analyze Oniguruma start line");
assert_eq!(info.min_size, 0);
assert!(info.hard);
assert_matches!(
info.expr,
Expr::Assertion(Assertion::StartLineOniguruma { .. })
);
let tree = Expr::parse_tree_with_flags(r"(?m)(?=^)", FLAG_ONIGURUMA_MODE).unwrap();
let info = analyze(&tree, AnalyzeContext::default())
.expect("can analyze Oniguruma start line inside lookaround");
assert_eq!(info.min_size, 0);
assert!(info.hard);
assert_matches!(
info.children[0].expr,
Expr::Assertion(Assertion::StartLineOniguruma { .. })
);
assert!(!info.children[0].hard);
}
#[test]
fn min_pos_in_group_calculated_correctly_with_no_groups() {
let tree = Expr::parse_tree(r"\G").unwrap();
let info = analyze(&tree, AnalyzeContext::default()).unwrap();
assert_eq!(info.min_size, 0);
assert_eq!(info.min_pos_in_group, 0);
assert!(info.const_size);
let tree = Expr::parse_tree(r"\G(?=abc)\w+").unwrap();
let info = analyze(&tree, AnalyzeContext::default()).unwrap();
assert_eq!(info.children[1].min_size, 0);
assert!(info.children[1].const_size);
assert_eq!(info.children[1].children[0].min_size, 3);
assert!(info.children[1].children[0].const_size);
assert_eq!(info.children[2].min_pos_in_group, 0);
assert_eq!(info.children[2].min_size, 1);
assert_eq!(info.min_pos_in_group, 0);
assert!(!info.const_size);
let tree = Expr::parse_tree(r"(?:ab*|cd){2}(?=bar)\w").unwrap();
let info = analyze(&tree, AnalyzeContext::default()).unwrap();
assert_eq!(info.min_size, 3);
assert_eq!(info.children[1].min_pos_in_group, 2);
assert_eq!(info.children[2].min_pos_in_group, 2);
assert_eq!(info.children[2].min_size, 1);
assert!(!info.const_size);
}
#[test]
fn backtracking_control_verb_is_hard_and_const_size() {
let tree = Expr::parse_tree(r"(*FAIL)").unwrap();
let info = analyze(&tree, AnalyzeContext::default()).unwrap();
assert_eq!(info.min_size, 0);
assert_eq!(info.min_pos_in_group, 0);
assert!(info.const_size);
}
#[test]
fn min_pos_in_group_calculated_correctly_with_capture_groups() {
let tree = Expr::parse_tree(r"a(bc)d(e(f)g)").unwrap();
let info = analyze(&tree, AnalyzeContext::default()).unwrap();
assert_eq!(info.min_pos_in_group, 0);
assert_eq!(info.children[1].min_pos_in_group, 1);
assert_matches!(info.children[1].children[0].expr, Expr::Concat(_));
assert_eq!(info.children[1].children[0].min_pos_in_group, 0);
assert!(info.children[1].children[0].const_size);
assert_matches!(info.children[1].children[0].children[1].expr, Expr::Literal { val, casei: false } if val == "c");
assert_eq!(info.children[1].children[0].children[1].min_pos_in_group, 1);
assert_matches!(info.children[2].expr, Expr::Literal { val, casei: false } if val == "d");
assert_eq!(info.children[2].min_pos_in_group, 3);
assert_eq!(info.children[2].start_group(), 2);
assert_eq!(info.children[2].min_size, 1);
assert_matches!(info.children[3].children[0].children[0].expr, Expr::Literal { val, casei: false } if val == "e");
assert_eq!(info.children[3].children[0].children[0].min_pos_in_group, 0);
}
#[test]
fn absent_repeater_is_hard_and_not_const_size() {
let tree = Expr::parse_tree(r"(?~abc)").unwrap();
let info = analyze(&tree, AnalyzeContext::default()).unwrap();
assert_eq!(info.min_size, 0);
assert!(!info.const_size);
assert!(info.hard);
}
#[test]
fn absent_expression_is_hard_and_not_const_size() {
let tree = Expr::parse_tree(r"(?~|abc|\d+)").unwrap();
let info = analyze(&tree, AnalyzeContext::default()).unwrap();
assert_eq!(info.min_size, 1);
assert!(!info.const_size);
assert!(info.hard);
}
#[test]
fn range_clear_is_hard_and_const_size() {
let tree = Expr::parse_tree(r"(?~|)").unwrap();
let info = analyze(&tree, AnalyzeContext::default()).unwrap();
assert_eq!(info.min_size, 0);
assert!(info.const_size);
assert!(info.hard);
}
#[test]
fn left_recursive_subroutine_direct() {
let tree = Expr::parse_tree(r"(\g<1>a)").unwrap();
assert_compile_error(analyze(&tree, AnalyzeContext::default()), |e| {
matches!(e, CompileError::LeftRecursiveSubroutineCall(_))
});
let tree = Expr::parse_tree(r"abc(\g<1>a)").unwrap();
assert_compile_error(analyze(&tree, AnalyzeContext::default()), |e| {
matches!(e, CompileError::LeftRecursiveSubroutineCall(_))
});
}
#[test]
fn not_left_recursive_subroutine_after_group() {
let tree = Expr::parse_tree(r"(a)\g<1>").unwrap();
let result = analyze(&tree, AnalyzeContext::default());
assert!(result.is_ok());
let tree = Expr::parse_tree(r"(?<test>a)\g<test>").unwrap();
let result = analyze(&tree, AnalyzeContext::default());
assert!(result.is_ok());
}
#[test]
fn left_recursive_subroutine_at_start() {
let tree = Expr::parse_tree(r"(\g<1>a)").unwrap();
assert_compile_error(analyze(&tree, AnalyzeContext::default()), |e| {
matches!(e, CompileError::LeftRecursiveSubroutineCall(_))
});
let tree = Expr::parse_tree(r"(?<test>\g<test>a)").unwrap();
assert_compile_error(analyze(&tree, AnalyzeContext::default()), |e| {
matches!(e, CompileError::LeftRecursiveSubroutineCall(_))
});
}
#[test]
fn left_recursive_subroutine_indirect() {
let tree = Expr::parse_tree(r"(\g<2>)(\g<1>)").unwrap();
assert_compile_error(analyze(&tree, AnalyzeContext::default()), |e| {
matches!(e, CompileError::LeftRecursiveSubroutineCall(_))
});
let tree = Expr::parse_tree(r"(\g<2>)(\g<1>a)").unwrap();
assert_compile_error(analyze(&tree, AnalyzeContext::default()), |e| {
matches!(e, CompileError::LeftRecursiveSubroutineCall(_))
});
}
#[test]
fn left_recursive_subroutine_with_alternation() {
let tree = Expr::parse_tree(r"(a|\g<1>)").unwrap();
assert_compile_error(analyze(&tree, AnalyzeContext::default()), |e| {
matches!(e, CompileError::LeftRecursiveSubroutineCall(_))
});
}
#[test]
fn unbounded_recursive_after_char() {
let tree = Expr::parse_tree(r"(a\g<1>)").unwrap();
assert_compile_error(analyze(&tree, AnalyzeContext::default()), |e| {
matches!(e, CompileError::NeverEndingRecursion)
});
}
#[test]
fn bounded_recursive_after_char_is_allowed() {
let tree = Expr::parse_tree(r"(a\g<1>?)").unwrap();
let result = analyze(&tree, AnalyzeContext::default());
assert!(result.is_ok());
}
#[test]
fn not_left_recursive_zero_repetition() {
let tree = Expr::parse_tree(r"(a?\g<1>){0}").unwrap();
let result = analyze(&tree, AnalyzeContext::default());
assert!(result.is_ok());
}
#[test]
fn left_recursive_with_both_positions() {
let tree = Expr::parse_tree(r"(\g<1>a\g<1>)").unwrap();
assert_compile_error(analyze(&tree, AnalyzeContext::default()), |e| {
matches!(e, CompileError::LeftRecursiveSubroutineCall(_))
});
}
#[test]
fn left_recursive_with_lookahead() {
let tree = Expr::parse_tree(r"((?=a)\g<1>)").unwrap();
assert_compile_error(analyze(&tree, AnalyzeContext::default()), |e| {
matches!(e, CompileError::LeftRecursiveSubroutineCall(_))
});
}
#[test]
fn self_recursive_group_zero() {
let tree = Expr::parse_tree(r"a\g<0>").unwrap();
assert_compile_error(analyze(&tree, AnalyzeContext::default()), |e| {
matches!(e, CompileError::NeverEndingRecursion)
});
}
#[test]
fn not_left_recursive_forward_call() {
let tree = Expr::parse_tree(r"\g<1>(a)").unwrap();
let result = analyze(&tree, AnalyzeContext::default());
assert!(result.is_ok());
}
#[test]
fn not_left_recursive_group_zero_explicit() {
let tree = Expr::parse_tree(r"(a\g<0>)").unwrap();
assert_compile_error(
analyze(
&tree,
AnalyzeContext {
explicit_capture_group_0: true,
..Default::default()
},
),
|e| matches!(e, CompileError::NeverEndingRecursion),
);
}
#[test]
fn not_left_recursive_group_zero_subroutine_call_unreachable() {
let tree = Expr::parse_tree(r"\g<0>{0}abc").unwrap();
let result = analyze(
&tree,
AnalyzeContext {
explicit_capture_group_0: true,
..Default::default()
},
);
assert!(result.is_ok());
}
#[test]
fn left_recursive_group_zero_at_start() {
let tree = Expr::parse_tree(r"(\g<0>a)").unwrap();
let result = analyze(
&tree,
AnalyzeContext {
explicit_capture_group_0: true,
..Default::default()
},
);
assert!(result.is_err());
}
#[test]
fn three_way_indirect_recursion() {
let tree = Expr::parse_tree(r"(\g<2>)(\g<3>)(a\g<1>)").unwrap();
assert_compile_error(analyze(&tree, AnalyzeContext::default()), |e| {
matches!(e, CompileError::NeverEndingRecursion)
});
}
#[test]
fn three_way_left_recursive() {
let tree = Expr::parse_tree(r"(\g<2>)(\g<3>)(\g<1>)").unwrap();
let result = analyze(&tree, AnalyzeContext::default());
assert!(result.is_err());
let tree = Expr::parse_tree(r"(\g<2>a)(\g<3>b)(\g<1>c)").unwrap();
let result = analyze(&tree, AnalyzeContext::default());
assert!(result.is_err());
}
#[test]
fn left_recursive_with_call_to_defined_group() {
let tree = Expr::parse_tree(r"(a?\g<2>){0}(\g<1>)").unwrap();
assert_compile_error(analyze(&tree, AnalyzeContext::default()), |e| {
matches!(e, CompileError::LeftRecursiveSubroutineCall(_))
});
}
#[test]
fn no_left_recursion_complex_pattern() {
let tree = Expr::parse_tree(r"(?<n>|\g<m>\g<n>)\z|\zEND (?<m>a(b)\g<m>)").unwrap();
let result = analyze(&tree, AnalyzeContext::default());
assert!(
result.is_ok(),
"Pattern should not be left-recursive because group m has min_size > 0"
);
}
#[test]
fn forward_subroutine_call_single_group() {
let tree = Expr::parse_tree(r"\g<1>(.a.)").unwrap();
let info = analyze(&tree, AnalyzeContext::default()).unwrap();
assert_eq!(info.start_group(), 1);
assert_eq!(info.end_group(), 2);
assert!(matches!(info.expr, Expr::Concat(_)));
assert_eq!(info.children.len(), 2);
assert!(matches!(info.children[0].expr, Expr::SubroutineCall(1)));
assert_eq!(info.children[0].start_group(), 1);
assert_eq!(info.children[0].end_group(), 1);
assert_eq!(info.children[0].min_size, 3);
assert!(info.children[0].const_size);
assert!(matches!(info.children[1].expr, Expr::Group(_)));
assert_eq!(info.children[1].start_group(), 1);
assert_eq!(info.children[1].end_group(), 2);
assert_eq!(info.children[1].min_size, 3);
assert!(info.children[1].const_size);
assert_eq!(info.min_size, 6);
assert!(info.const_size);
}
#[test]
fn forward_subroutine_call_with_multiple_groups() {
let tree = Expr::parse_tree(r"\g<2>(a)(bc+)").unwrap();
let info = analyze(&tree, AnalyzeContext::default()).unwrap();
assert_eq!(info.start_group(), 1);
assert_eq!(info.end_group(), 3);
assert!(matches!(info.expr, Expr::Concat(_)));
assert_eq!(info.children.len(), 3);
assert!(matches!(info.children[0].expr, Expr::SubroutineCall(2)));
assert_eq!(info.children[0].start_group(), 1);
assert_eq!(info.children[0].end_group(), 1);
assert_eq!(info.children[0].min_size, 2);
assert!(!info.children[0].const_size);
assert!(matches!(info.children[1].expr, Expr::Group(_)));
assert_eq!(info.children[1].start_group(), 1);
assert_eq!(info.children[1].end_group(), 2);
assert_eq!(info.children[1].min_size, 1);
assert!(info.children[1].const_size);
assert!(matches!(info.children[2].expr, Expr::Group(_)));
assert_eq!(info.children[2].start_group(), 2);
assert_eq!(info.children[2].end_group(), 3);
assert_eq!(info.children[2].min_size, 2);
assert!(!info.children[2].const_size);
}
#[test]
fn forward_subroutine_call_with_nested_groups() {
let tree = Expr::parse_tree(r"(foo)\g<4>(a(b)?)(c(d))(?!e)").unwrap();
let info = analyze(&tree, AnalyzeContext::default()).unwrap();
assert_eq!(info.start_group(), 1);
assert_eq!(info.end_group(), 6);
assert!(matches!(info.expr, Expr::Concat(_)));
assert_eq!(info.children.len(), 5);
assert!(matches!(info.children[1].expr, Expr::SubroutineCall(4)));
assert_eq!(info.children[1].start_group(), 2);
assert_eq!(info.children[1].end_group(), 2);
assert_eq!(info.children[1].min_size, 2);
assert!(info.children[1].const_size);
assert!(matches!(info.children[2].expr, Expr::Group(_)));
assert_eq!(info.children[2].start_group(), 2);
assert_eq!(info.children[2].end_group(), 4);
assert_eq!(info.children[2].min_size, 1);
assert!(!info.children[2].const_size);
let group_info = &info.children[2].children[0].children[1].children[0];
assert!(matches!(group_info.expr, Expr::Group(_)));
assert_eq!(group_info.start_group(), 3);
assert_eq!(group_info.end_group(), 4);
assert_eq!(group_info.min_size, 1);
assert!(group_info.const_size);
assert!(matches!(info.children[3].expr, Expr::Group(_)));
assert_eq!(info.children[3].start_group(), 4);
assert_eq!(info.children[3].end_group(), 6);
assert_eq!(info.children[3].min_size, 2);
assert!(info.children[3].const_size);
let group_info = &info.children[3].children[0].children[1];
assert!(matches!(group_info.expr, Expr::Group(_)));
assert_eq!(group_info.start_group(), 5);
assert_eq!(group_info.end_group(), 6);
assert_eq!(group_info.min_size, 1);
assert!(group_info.const_size);
assert!(matches!(info.children[4].expr, Expr::LookAround(_, _)));
assert_eq!(info.children[4].start_group(), 6);
assert_eq!(info.children[4].end_group(), 6);
}
#[test]
fn define_group_is_easy_zero_size() {
let tree = Expr::parse_tree(r"(?(DEFINE)(?<word>\w+))").unwrap();
let info = analyze(&tree, AnalyzeContext::default()).unwrap();
assert!(matches!(info.expr, Expr::DefineGroup { .. }));
assert_eq!(info.min_size, 0);
assert!(info.const_size);
assert!(!info.hard);
}
#[test]
fn define_group_assigns_group_numbers() {
let tree = Expr::parse_tree(r"(?(DEFINE)(?<first>a)(?<second>b))").unwrap();
let info = analyze(&tree, AnalyzeContext::default()).unwrap();
assert_eq!(info.children[0].start_group(), 1);
assert_eq!(info.children[0].end_group(), 3);
let tree = Expr::parse_tree(
r"(abc)(?(DEFINE)(?<second>a)ignored: no group(?<third>b(?<fourth>c)))(?<fifth>d)",
)
.unwrap();
let info = analyze(&tree, AnalyzeContext::default()).unwrap();
assert_eq!(info.start_group(), 1);
assert_eq!(info.end_group(), 6);
assert_eq!(info.children[0].start_group(), 1);
assert_eq!(info.children[1].children[0].start_group(), 2);
assert_eq!(info.children[2].start_group(), 5);
assert_eq!(info.children[2].end_group(), 6);
}
#[test]
fn max_size_for_literal() {
let tree = Expr::parse_tree("a").unwrap();
let info = analyze(&tree, AnalyzeContext::default()).unwrap();
assert_eq!(info.max_size, 1);
}
#[test]
fn max_size_for_concat_of_literals() {
let tree = Expr::parse_tree("abc").unwrap();
let info = analyze(&tree, AnalyzeContext::default()).unwrap();
assert_eq!(info.max_size, 3);
}
#[test]
fn max_size_for_general_newline() {
let tree = Expr::parse_tree(r"\R").unwrap();
let info = analyze(&tree, AnalyzeContext::default()).unwrap();
assert_eq!(info.max_size, 2);
}
#[test]
fn max_size_for_greedy_star() {
let tree = Expr::parse_tree("a*").unwrap();
let info = analyze(&tree, AnalyzeContext::default()).unwrap();
assert_eq!(info.max_size, usize::MAX);
}
#[test]
fn max_size_for_greedy_plus() {
let tree = Expr::parse_tree("a+").unwrap();
let info = analyze(&tree, AnalyzeContext::default()).unwrap();
assert_eq!(info.max_size, usize::MAX);
}
#[test]
fn max_size_for_optional() {
let tree = Expr::parse_tree("a?").unwrap();
let info = analyze(&tree, AnalyzeContext::default()).unwrap();
assert_eq!(info.max_size, 1);
}
#[test]
fn max_size_for_bounded_repeat() {
let tree = Expr::parse_tree("a{1,3}").unwrap();
let info = analyze(&tree, AnalyzeContext::default()).unwrap();
assert_eq!(info.max_size, 3);
}
#[test]
fn max_size_for_exact_repeat() {
let tree = Expr::parse_tree("a{3}").unwrap();
let info = analyze(&tree, AnalyzeContext::default()).unwrap();
assert_eq!(info.max_size, 3);
}
#[test]
fn max_size_for_alternation() {
let tree = Expr::parse_tree("a|bc").unwrap();
let info = analyze(&tree, AnalyzeContext::default()).unwrap();
assert_eq!(info.max_size, 2);
}
#[test]
fn max_size_for_group() {
let tree = Expr::parse_tree(r"(abc)").unwrap();
let info = analyze(&tree, AnalyzeContext::default()).unwrap();
assert_eq!(info.max_size, 3);
}
#[test]
fn max_size_for_backref_inherits_group() {
let tree = Expr::parse_tree(r"(ab)\1").unwrap();
let info = analyze(&tree, AnalyzeContext::default()).unwrap();
assert_eq!(info.max_size, 4);
}
#[test]
fn max_size_for_concat_accumulates() {
let tree = Expr::parse_tree("ab*cd").unwrap();
let info = analyze(&tree, AnalyzeContext::default()).unwrap();
assert_eq!(info.max_size, usize::MAX);
}
#[test]
fn max_size_for_empty_assertion_is_zero() {
let tree = Expr::parse_tree(r"\b").unwrap();
let info = analyze(&tree, AnalyzeContext::default()).unwrap();
assert_eq!(info.max_size, 0);
}
#[cfg(feature = "leftmost_longest")]
#[test]
fn leftmost_longest_rejects_non_greedy_star() {
let tree = Expr::parse_tree(r"a*?").unwrap();
let result = analyze(
&tree,
AnalyzeContext {
leftmost_longest: true,
..Default::default()
},
);
assert_compile_error(
result,
|e| matches!(e, CompileError::FeatureNotYetSupported(s) if s.contains("non-greedy")),
);
}
#[cfg(feature = "leftmost_longest")]
#[test]
fn leftmost_longest_rejects_non_greedy_plus() {
let tree = Expr::parse_tree(r"a+?").unwrap();
let result = analyze(
&tree,
AnalyzeContext {
leftmost_longest: true,
..Default::default()
},
);
assert_compile_error(
result,
|e| matches!(e, CompileError::FeatureNotYetSupported(s) if s.contains("non-greedy")),
);
}
#[cfg(feature = "leftmost_longest")]
#[test]
fn leftmost_longest_rejects_non_greedy_optional() {
let tree = Expr::parse_tree(r"a??").unwrap();
let result = analyze(
&tree,
AnalyzeContext {
leftmost_longest: true,
..Default::default()
},
);
assert_compile_error(
result,
|e| matches!(e, CompileError::FeatureNotYetSupported(s) if s.contains("non-greedy")),
);
}
#[cfg(feature = "leftmost_longest")]
#[test]
fn leftmost_longest_rejects_non_greedy_bounded() {
let tree = Expr::parse_tree(r"a{1,3}?").unwrap();
let result = analyze(
&tree,
AnalyzeContext {
leftmost_longest: true,
..Default::default()
},
);
assert_compile_error(
result,
|e| matches!(e, CompileError::FeatureNotYetSupported(s) if s.contains("non-greedy")),
);
}
#[cfg(feature = "leftmost_longest")]
#[test]
fn leftmost_longest_accepts_greedy_quantifiers() {
assert_analyze_ok(
&Expr::parse_tree(r"a*").unwrap(),
AnalyzeContext {
leftmost_longest: true,
..Default::default()
},
);
assert_analyze_ok(
&Expr::parse_tree(r"a+").unwrap(),
AnalyzeContext {
leftmost_longest: true,
..Default::default()
},
);
assert_analyze_ok(
&Expr::parse_tree(r"a?").unwrap(),
AnalyzeContext {
leftmost_longest: true,
..Default::default()
},
);
assert_analyze_ok(
&Expr::parse_tree(r"a{1,3}").unwrap(),
AnalyzeContext {
leftmost_longest: true,
..Default::default()
},
);
}
#[cfg(feature = "leftmost_longest")]
#[test]
fn leftmost_longest_promotes_non_const_to_hard() {
let tree = Expr::parse_tree(r"a*").unwrap();
let info = analyze(&tree, AnalyzeContext::default()).unwrap();
assert!(!info.hard);
assert!(!info.const_size);
let info = analyze(
&tree,
AnalyzeContext {
leftmost_longest: true,
..Default::default()
},
)
.unwrap();
assert!(info.hard);
assert!(!info.const_size);
}
#[cfg(feature = "leftmost_longest")]
#[test]
fn leftmost_longest_keeps_const_size_easy() {
let tree = Expr::parse_tree(r"abc").unwrap();
let info = analyze(
&tree,
AnalyzeContext {
leftmost_longest: true,
..Default::default()
},
)
.unwrap();
assert!(!info.hard);
assert!(info.const_size);
}
#[test]
fn end_text_hard_with_find_not_empty() {
let tree = Expr::parse_tree(r"$").unwrap();
let info = analyze(
&tree,
AnalyzeContext {
find_not_empty: true,
..Default::default()
},
)
.unwrap();
assert!(info.hard);
assert!(info.const_size);
}
#[test]
fn end_text_not_hard_without_find_not_empty() {
let tree = Expr::parse_tree(r"$").unwrap();
let info = analyze(&tree, AnalyzeContext::default()).unwrap();
assert!(!info.hard);
assert!(info.const_size);
}
#[test]
fn end_text_not_hard_in_lookaround_with_find_not_empty() {
let tree = Expr::parse_tree(r"(?=$)").unwrap();
let info = analyze(
&tree,
AnalyzeContext {
find_not_empty: true,
..Default::default()
},
)
.unwrap();
let end_text_info = &info.children[0];
assert!(!end_text_info.hard);
assert!(end_text_info.const_size);
}
}