use alloc::format;
use alloc::string::String;
#[cfg(not(feature = "std"))]
use alloc::collections::BTreeMap as Map;
#[cfg(feature = "std")]
use std::collections::HashMap as Map;
use crate::analyze::Info;
use crate::compile::MAX_SUBROUTINE_RECURSION_DEPTH;
use crate::{write_quantifier, Absent, Assertion, Expr};
pub(crate) fn expr_contains_positional_anchor(expr: &Expr) -> bool {
match expr {
Expr::Assertion(
Assertion::StartText
| Assertion::EndText
| Assertion::EndTextIgnoreTrailingNewlines { .. }
| Assertion::StartLine { .. }
| Assertion::StartLineOniguruma { .. }
| Assertion::EndLine { .. },
) => true,
_ => expr.children_iter().any(expr_contains_positional_anchor),
}
}
pub(crate) fn emit_min_size_placeholder(buf: &mut String, min_size: usize, precedence: u8) {
if min_size == 0 {
return; }
if precedence > 2 {
buf.push_str("(?:");
}
buf.push_str("(?s:.)");
match min_size {
1 => buf.push('+'),
n => {
buf.push('{');
crate::push_usize(buf, n);
buf.push_str(",}");
}
}
if precedence > 2 {
buf.push(')');
}
}
pub(crate) fn build_seek_pattern<'a>(
info: &Info<'a>,
group_info_map: &Map<usize, &'a Info<'a>>,
depth: usize,
buf: &mut String,
precedence: u8,
) {
build_seek_pattern_impl(info, group_info_map, depth, buf, precedence, false);
}
pub(crate) fn build_seek_pattern_impl<'a>(
info: &Info<'a>,
group_info_map: &Map<usize, &'a Info<'a>>,
depth: usize,
buf: &mut String,
precedence: u8,
drop_positional_anchors: bool,
) {
if drop_positional_anchors {
if let Expr::Assertion(
Assertion::StartText
| Assertion::EndText
| Assertion::StartLine { .. }
| Assertion::StartLineOniguruma { .. }
| Assertion::EndLine { .. },
) = info.expr
{
return;
}
if let Expr::Assertion(Assertion::EndTextIgnoreTrailingNewlines { crlf }) = info.expr {
if *crlf {
buf.push_str(r"[\r\n]*");
} else {
buf.push_str(r"\n*");
}
return;
}
}
let has_capture_groups = info.start_group() != info.end_group();
if !info.hard && !has_capture_groups {
if !drop_positional_anchors || !expr_contains_positional_anchor(info.expr) {
info.expr.to_str(buf, precedence);
return;
}
}
match info.expr {
Expr::Empty | Expr::DefineGroup { .. } => {}
Expr::Assertion(assertion) => {
match assertion {
Assertion::EndTextIgnoreTrailingNewlines { crlf: false } => buf.push_str(r"\n*$"),
Assertion::EndTextIgnoreTrailingNewlines { crlf: true } => {
buf.push_str(r"[\r\n]*$")
}
Assertion::WordBoundary => buf.push_str(r"\b"),
Assertion::NotWordBoundary => buf.push_str(r"\B"),
Assertion::LeftWordBoundary | Assertion::RightWordBoundary => buf.push_str(r"\b"),
Assertion::LeftWordHalfBoundary | Assertion::RightWordHalfBoundary => {}
Assertion::StartText => buf.push('^'),
Assertion::EndText => buf.push('$'),
Assertion::StartLine { crlf: false }
| Assertion::StartLineOniguruma { crlf: false } => buf.push_str("(?m:^)"),
Assertion::StartLine { crlf: true }
| Assertion::StartLineOniguruma { crlf: true } => buf.push_str("(?Rm:^)"),
Assertion::EndLine { crlf: false } => buf.push_str("(?m:$)"),
Assertion::EndLine { crlf: true } => buf.push_str("(?Rm:$)"),
}
}
Expr::Concat(_) => {
if precedence > 1 {
buf.push_str("(?:");
}
for child in &info.children {
build_seek_pattern_impl(
child,
group_info_map,
depth,
buf,
2,
drop_positional_anchors,
);
}
if precedence > 1 {
buf.push(')');
}
}
Expr::Alt(_) => {
if precedence > 0 {
buf.push_str("(?:");
}
let mut first = true;
for child in &info.children {
if !first {
buf.push('|');
}
build_seek_pattern_impl(
child,
group_info_map,
depth,
buf,
1,
drop_positional_anchors,
);
first = false;
}
if precedence > 0 {
buf.push(')');
}
}
Expr::Group(_) => {
if !info.children.is_empty() {
build_seek_pattern_impl(
&info.children[0],
group_info_map,
depth,
buf,
precedence,
drop_positional_anchors,
);
}
}
Expr::Repeat { lo, hi, greedy, .. } => {
if precedence > 2 {
buf.push_str("(?:");
}
if !info.children.is_empty() {
build_seek_pattern_impl(
&info.children[0],
group_info_map,
depth,
buf,
3,
drop_positional_anchors,
);
}
write_quantifier(buf, *lo, *hi, *greedy);
if precedence > 2 {
buf.push(')');
}
}
Expr::Backref { group, casei }
| Expr::BackrefWithRelativeRecursionLevel { group, casei, .. } => {
if depth < MAX_SUBROUTINE_RECURSION_DEPTH {
if let Some(group_info) = group_info_map.get(group) {
if !group_info.children.is_empty() {
let child = &group_info.children[0];
if *casei {
let mut inner = String::new();
build_seek_pattern_impl(
child,
group_info_map,
depth + 1,
&mut inner,
0,
true,
);
if !inner.is_empty() {
buf.push_str("(?i:");
buf.push_str(&inner);
buf.push(')');
}
} else {
build_seek_pattern_impl(
child,
group_info_map,
depth + 1,
buf,
precedence,
true,
);
}
return;
}
return;
}
}
emit_min_size_placeholder(buf, info.min_size, precedence);
}
Expr::SubroutineCall(target_group) => {
if depth < MAX_SUBROUTINE_RECURSION_DEPTH {
if let Some(group_info) = group_info_map.get(target_group) {
if !group_info.children.is_empty() {
build_seek_pattern_impl(
&group_info.children[0],
group_info_map,
depth + 1,
buf,
precedence,
drop_positional_anchors,
);
return;
}
return;
}
}
emit_min_size_placeholder(buf, info.min_size, precedence);
}
Expr::LookAround(_, _) => {}
Expr::AtomicGroup(_) => {
if !info.children.is_empty() {
build_seek_pattern_impl(
&info.children[0],
group_info_map,
depth,
buf,
precedence,
drop_positional_anchors,
);
}
}
Expr::GeneralNewline { unicode } => {
if *unicode {
buf.push_str(r"(?:\r\n|[\n\x0B\x0C\r\x85\u{2028}\u{2029}])");
} else {
buf.push_str(r"(?:\r\n|[\n\x0B\x0C\r])");
}
}
Expr::Conditional { .. } => {
let mut cond_pat = String::new();
let mut true_pat = String::new();
let mut false_pat = String::new();
if !info.children.is_empty() {
build_seek_pattern_impl(
&info.children[0],
group_info_map,
depth,
&mut cond_pat,
2,
drop_positional_anchors,
);
}
if info.children.len() >= 2 {
build_seek_pattern_impl(
&info.children[1],
group_info_map,
depth,
&mut true_pat,
2,
drop_positional_anchors,
);
}
if info.children.len() >= 3 {
build_seek_pattern_impl(
&info.children[2],
group_info_map,
depth,
&mut false_pat,
1,
drop_positional_anchors,
);
}
let cond_true = if cond_pat.is_empty() {
true_pat.clone()
} else if true_pat.is_empty() {
cond_pat.clone()
} else {
format!("(?:{}{})", cond_pat, true_pat)
};
match (cond_true.is_empty(), false_pat.is_empty()) {
(true, true) => {}
(true, false) => buf.push_str(&false_pat),
(false, true) => buf.push_str(&cond_true),
(false, false) => {
if precedence > 0 {
buf.push_str("(?:");
}
buf.push_str(&cond_true);
buf.push('|');
buf.push_str(&false_pat);
if precedence > 0 {
buf.push(')');
}
}
}
}
Expr::Absent(Absent::Repeater(_)) => buf.push_str("(?s:.*)"),
Expr::Absent(crate::Absent::Expression { .. }) => {
if info.children.len() >= 2 {
build_seek_pattern_impl(
&info.children[1],
group_info_map,
depth,
buf,
precedence,
drop_positional_anchors,
);
}
}
Expr::KeepOut
| Expr::ContinueFromPreviousMatchEnd
| Expr::BacktrackingControlVerb(_)
| Expr::BackrefExistsCondition { .. }
| Expr::Absent(_) => {}
Expr::Literal { .. } | Expr::Any { .. } | Expr::Delegate { .. } => {
info.expr.to_str(buf, precedence)
}
Expr::AstNode(..) => {
unreachable!("unexpected expr variant after analysis")
}
}
}
pub fn seek_pattern_is_useful(pattern: &str) -> bool {
pattern
.bytes()
.any(|b| matches!(b, b'[' | b'\\' | b'^' | b'$' | b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9'))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::analyze::{analyze, AnalyzeContext};
use crate::compile::populate_group_info_map;
use crate::optimize;
fn get_seek_pattern(re: &str) -> String {
let mut tree = Expr::parse_tree(re).unwrap();
let requires_capture_group_fixup = optimize(&mut tree);
let info = analyze(
&tree,
AnalyzeContext {
explicit_capture_group_0: requires_capture_group_fixup,
..AnalyzeContext::default()
},
)
.unwrap();
let mut group_info_map = Map::new();
populate_group_info_map(&mut group_info_map, &info);
let mut buf = String::new();
build_seek_pattern(&info, &group_info_map, 0, &mut buf, 0);
buf
}
#[test]
fn seek_pattern_backref_no_anchor() {
assert_eq!(get_seek_pattern(r"(abc)\1"), "(?:abc)(?:abc)");
assert_eq!(
get_seek_pattern(r"(?i)(abc)\1"),
"(?:(?i:a)(?i:b)(?i:c))(?i:(?i:a)(?i:b)(?i:c))"
);
}
#[test]
fn seek_pattern_backref_with_start_anchor() {
assert_eq!(get_seek_pattern(r"(^a)\1"), "(?:^a)(?:a)");
}
#[test]
fn seek_pattern_backref_with_start_anchor_variable_length() {
assert_eq!(get_seek_pattern(r"(^a+)\1"), "(?:^a+)(?:a+)");
}
#[test]
fn seek_pattern_backref_with_end_anchor() {
assert_eq!(get_seek_pattern(r"(a$)\1"), "(?:a$)(?:a)");
}
#[test]
fn seek_pattern_backref_only_anchor() {
assert_eq!(get_seek_pattern(r"(^)\1"), "^");
}
#[test]
fn seek_pattern_lookahead_dropped() {
assert_eq!(get_seek_pattern(r"(?=foo)bar"), "bar");
}
#[test]
fn seek_pattern_casei_literal_backref_with_anchor() {
assert_eq!(get_seek_pattern(r"(?i:(^a))\1"), "(?:^(?i:a))(?:(?i:a))");
}
#[test]
fn seek_pattern_easy_expr_preserved() {
assert_eq!(get_seek_pattern(r"abc"), "abc");
assert_eq!(get_seek_pattern(r"a|b"), "a|b");
assert_eq!(get_seek_pattern(r"a+b*c?"), "a+b*c?");
}
#[test]
fn seek_pattern_easy_expr_with_capture_group_strips_group_wrapper() {
assert_eq!(get_seek_pattern(r"(abc)"), "abc");
assert_eq!(
get_seek_pattern(r"(abc(def))(?<named>ghi)"),
"(?:abc(?:def))(?:ghi)"
);
}
#[test]
fn seek_pattern_optimized_easy_expr_strips_group_wrapper() {
assert_eq!(get_seek_pattern(r"(?=abc)"), "(?:abc)");
assert_eq!(
get_seek_pattern(r"(h)(e)(l)(l)(o)\s*(?=world)"),
r"(?:hello\s*)(?:world)"
);
}
#[test]
fn seek_pattern_end_text_ignore_trailing_newlines_non_crlf() {
assert_eq!(get_seek_pattern(r"abc\Z"), r"abc\n*$");
}
#[test]
fn seek_pattern_end_text_ignore_trailing_newlines_crlf() {
assert_eq!(get_seek_pattern(r"(?R)abc\Z"), r"abc[\r\n]*$");
}
#[test]
fn seek_pattern_end_text_ignore_trailing_newlines_only() {
assert_eq!(get_seek_pattern(r"\Z"), r"\n*$");
}
#[test]
fn seek_pattern_backref_with_end_text_ignore_trailing_newlines() {
assert_eq!(get_seek_pattern(r"(a\Z)\1"), r"(?:a\n*$)(?:a\n*)");
}
}