pub(crate) fn is_escaped_literal(ch: char) -> bool {
matches!(
ch,
'[' | ']' | '(' | ')' | '.' | '*' | '+' | '?' | '{' | '}' | '\\' | '|' | '^' | '$'
)
}
pub(crate) const MIN_INNER_LITERAL_CHARS: usize = 4;
#[cfg(test)]
pub(crate) fn extract_inner_literals(pattern: &str) -> Vec<String> {
use regex_syntax::ast::{parse::Parser, Ast};
let Ok(ast) = Parser::new().parse(pattern) else {
return Vec::new();
};
fn has_complete_trigger_coverage(ast: &Ast) -> bool {
match ast {
Ast::Concat(concat) => {
let mut run_bytes = 0usize;
for node in &concat.asts {
if let Ast::Literal(literal) = node {
run_bytes += literal.c.len_utf8();
if run_bytes >= MIN_INNER_LITERAL_CHARS {
return true;
}
} else {
run_bytes = 0;
if has_complete_trigger_coverage(node) {
return true;
}
}
}
false
}
Ast::Group(group) => has_complete_trigger_coverage(&group.ast),
Ast::Alternation(alternation) => {
!alternation.asts.is_empty()
&& alternation.asts.iter().all(has_complete_trigger_coverage)
}
_ => false,
}
}
if !has_complete_trigger_coverage(&ast) {
return Vec::new();
}
let mut out = Vec::new();
walk_ast(&ast, &mut out);
out.retain(|s| s.len() >= MIN_INNER_LITERAL_CHARS);
let mut seen = std::collections::HashSet::new();
out.retain(|s| seen.insert(s.clone()));
out
}
#[cfg(test)]
fn walk_ast(ast: ®ex_syntax::ast::Ast, out: &mut Vec<String>) {
use regex_syntax::ast::Ast;
match ast {
Ast::Concat(concat) => {
let mut run = String::new();
for inner in concat.asts.iter() {
match inner {
Ast::Literal(lit) => run.push(lit.c),
_ => {
if run.len() >= MIN_INNER_LITERAL_CHARS {
out.push(std::mem::take(&mut run));
} else {
run.clear();
}
walk_ast(inner, out);
}
}
}
if run.len() >= MIN_INNER_LITERAL_CHARS {
out.push(run);
}
}
Ast::Group(group) => walk_ast(&group.ast, out),
Ast::Alternation(alt) => {
for branch in alt.asts.iter() {
walk_ast(branch, out);
}
}
Ast::Literal(lit) => {
let s = lit.c.to_string();
if s.len() >= MIN_INNER_LITERAL_CHARS {
out.push(s);
}
}
Ast::Repetition(_)
| Ast::ClassUnicode(_)
| Ast::ClassPerl(_)
| Ast::ClassBracketed(_)
| Ast::Dot(_)
| Ast::Empty(_)
| Ast::Flags(_)
| Ast::Assertion(_) => {}
}
}
pub(crate) const MIN_DISTINCTIVE_INFIX_CHARS: usize = 8;
pub(crate) fn regex_has_required_literal_run(pattern: &str, min_len: usize) -> bool {
use regex_syntax::ast::{parse::Parser, Ast};
fn max_required_run(ast: &Ast) -> usize {
match ast {
Ast::Literal(_) => 1,
Ast::Concat(concat) => {
let mut best = 0usize;
let mut run = 0usize;
for node in concat.asts.iter() {
if matches!(node, Ast::Literal(_)) {
run += 1;
best = best.max(run);
} else {
best = best.max(max_required_run(node));
run = 0;
}
}
best
}
Ast::Group(group) => max_required_run(&group.ast),
_ => 0,
}
}
let Ok(ast) = Parser::new().parse(pattern) else {
return false;
};
max_required_run(&ast) >= min_len
}