mod java_case;
mod java_fold;
mod java_property;
pub(crate) use self::java_fold::BackrefFold;
use self::java_fold::{Consumed, Escape};
#[derive(Debug)]
pub(crate) struct JavaTranslation {
pub(crate) pattern: String,
pub(crate) backref_folds: Vec<BackrefFold>,
pub(crate) linebreak_atoms: Vec<bool>,
pub(crate) group_repeats: Vec<bool>,
pub(crate) look_behind_code_points: Vec<bool>,
}
pub(crate) fn translate_java(pattern: &str) -> Option<JavaTranslation> {
let chars = java_fold::remove_qe_quoting(&pattern.chars().collect::<Vec<char>>());
let mut translator = Translator {
chars: &chars,
out: String::with_capacity(pattern.len()),
scope: Scope::default(),
outer: Vec::new(),
groups: Vec::new(),
classes: Vec::new(),
class_start: false,
run: 0,
prior_run: 0,
at: 0,
backref_folds: Vec::new(),
linebreak_atoms: Vec::new(),
group_names: Vec::new(),
group_repeats: vec![false],
last_supplementary: chars.iter().rposition(|&c| u32::from(c) >= 0x1_0000),
look_behinds: Vec::new(),
quantified: Quantified::Start,
};
while translator.at < chars.len() {
translator.step()?;
}
let look_behind_code_points =
look_behind_code_points(&translator.out, &translator.look_behinds);
Some(JavaTranslation {
pattern: translator.out,
backref_folds: translator.backref_folds,
linebreak_atoms: translator.linebreak_atoms,
group_repeats: translator.group_repeats,
look_behind_code_points,
})
}
fn look_behind_code_points(pattern: &str, java: &[(usize, bool)]) -> Vec<bool> {
let bytes = pattern.as_bytes();
let mut java = java.iter().peekable();
let mut code_points = Vec::new();
let mut nest = 0_usize;
let mut at = 0;
while at < bytes.len() {
at += match (bytes[at], nest) {
(b'\\', _) => 2,
(b'(', 0)
if bytes[at + 1..].starts_with(b"?<=") || bytes[at + 1..].starts_with(b"?<!") =>
{
code_points.push(
java.next_if(|&&(opens, _)| opens == at)
.is_some_and(|&(_, code_points)| code_points),
);
4
}
(b'[', _) => {
nest += 1;
let after = usize::from(bytes.get(at + 1) == Some(&b'^'));
1 + after + usize::from(bytes.get(at + 1 + after) == Some(&b']'))
}
(b']', 1..) => {
nest -= 1;
1
}
_ => 1,
};
}
code_points
}
struct Translator<'a> {
chars: &'a [char],
out: String,
scope: Scope,
outer: Vec<Scope>,
groups: Vec<Option<usize>>,
classes: Vec<OpenClass>,
class_start: bool,
run: usize,
prior_run: usize,
at: usize,
backref_folds: Vec<BackrefFold>,
linebreak_atoms: Vec<bool>,
group_names: Vec<String>,
group_repeats: Vec<bool>,
last_supplementary: Option<usize>,
look_behinds: Vec<(usize, bool)>,
quantified: Quantified,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Quantified {
No,
Open,
Closed,
Start,
Empty,
}
impl Translator<'_> {
fn in_class(&self) -> bool { !self.classes.is_empty() }
fn copy(&mut self, len: usize) {
self.out.extend(&self.chars[self.at..self.at + len]);
self.at += len;
}
fn step(&mut self) -> Option<()> {
let c = self.chars[self.at];
let first_in_class = std::mem::take(&mut self.class_start);
let ignored = self.ignored_len(self.at);
if ignored > 0 {
self.class_start = first_in_class;
self.at += ignored;
return Some(());
}
self.prior_run = std::mem::take(&mut self.run);
let quantified = std::mem::replace(&mut self.quantified, Quantified::No);
if !self.in_class() {
match (quantified, c) {
(Quantified::No, '*' | '+' | '?') => self.quantified = Quantified::Open,
(Quantified::Open, '?' | '+') => self.quantified = Quantified::Closed,
(Quantified::Empty, '?' | '+') => {
self.quantified = Quantified::Start;
self.at += 1;
return Some(());
}
(_, '*' | '+' | '?') => return None,
(
Quantified::Open | Quantified::Closed | Quantified::Start | Quantified::Empty,
'{',
) => {
self.at += self.bounds_len(self.at)?;
self.quantified = Quantified::Empty;
return Some(());
}
_ => {}
}
}
let closes = c == ']' && !first_in_class;
let member = (self.in_class() && !closes && !matches!(c, '[' | '&'))
.then_some((self.out.len(), self.at));
match c {
'\\' => self.escape()?,
'(' if !self.in_class() => self.group()?,
')' if !self.in_class() => {
self.scope = self.outer.pop()?;
let number = self.groups.pop().flatten();
self.copy(1);
if let Some(number) = number {
self.group_repeats[number] = self.group_curly_at(self.at);
}
}
'|' if !self.in_class() => {
self.copy(1);
self.quantified = Quantified::Start;
}
'[' => self.open_class(),
']' if self.in_class() && !first_in_class => self.close_class()?,
'.' if !self.in_class() && !self.scope.dot.dotall && !self.scope.dot.unix_lines => {
self.out.push_str(JAVA_DOT);
self.at += 1;
}
'^' if !self.in_class() && self.scope.dot.multiline => {
self.push_anchor(caret(self.scope.dot), 1);
}
'$' if !self.in_class() => self.push_anchor(dollar(self.scope.dot), 1),
'{' if !self.in_class() => self.braces()?,
'&' if self.in_class() => self.ampersand()?,
_ => self.atom(u32::from(c), 1)?,
}
if let Some((out_at, at)) = member {
self.class_member(out_at, at);
}
Some(())
}
fn ignored_len(&self, at: usize) -> usize {
if self.scope.comments {
java_fold::ignorable_len(&self.chars[at..], self.scope.dot.unix_lines)
} else {
0
}
}
fn escape(&mut self) -> Option<()> {
let in_class = self.in_class();
let Consumed { value: escape, len } = java_fold::read_escape(self.chars, self.at, in_class);
let kind = self.chars.get(self.at + 1).copied();
if in_class
&& matches!(
kind,
Some('b' | 'B' | 'A' | 'G' | 'z' | 'Z' | 'R' | 'X' | 'k')
)
{
return None;
}
if kind == Some('k') {
if escape != Escape::BackRef {
return None;
}
let name: String = self.chars[self.at + 3..self.at + len - 1].iter().collect();
if !self.group_names.contains(&name) {
return None;
}
}
match escape {
Escape::Named => return None,
Escape::BackRef if self.scope.case.ignore => {
self.backref_folds.push(if self.scope.case.unicode {
BackrefFold::Unicode
} else {
BackrefFold::Ascii
});
self.out.push_str("(?i:");
self.copy(len);
self.out.push(')');
}
Escape::Char(code) => self.atom(code, len)?,
Escape::Property { name, .. }
if !java_property::java_knows(&name, self.scope.unicode_classes) =>
{
return None;
}
Escape::Property { name, negated } => {
match java_fold::property_class(&name, negated, in_class, self.scope) {
Some(class) => {
self.out.push_str(&class);
self.at += len;
}
None => self.copy(len),
}
}
Escape::Other if !in_class && self.chars.get(self.at + 1) == Some(&'Z') => {
let dot = Dot {
multiline: false,
..self.scope.dot
};
self.push_anchor(dollar(dot), len);
}
Escape::Other
if in_class
&& self.chars.get(self.at + 1) == Some(&'v')
&& self.chars.get(self.at + len) == Some(&'-') =>
{
self.atom(0x0B, len)?;
}
Escape::Other
if self
.chars
.get(self.at + 1)
.is_some_and(|&next| java_space(next, in_class).is_some()) =>
{
let next = self.chars[self.at + 1];
self.out.push_str(java_space(next, in_class)?);
self.at += len;
}
Escape::Other if !in_class && self.chars.get(self.at + 1) == Some(&'R') => {
self.linebreak_atoms.push(self.quantifier_at(self.at + len));
self.copy(len);
}
Escape::BackRef | Escape::Other => {
let ascii = self
.chars
.get(self.at + 1)
.and_then(|&next| ascii_shorthand(next, in_class));
match ascii {
Some(ascii) if !self.scope.unicode_classes => {
self.out.push_str(ascii);
self.at += len;
}
_ => self.copy(len),
}
}
}
Some(())
}
fn atom(&mut self, code: u32, len: usize) -> Option<()> {
let (chars, start, in_class) = (self.chars, self.at, self.in_class());
let fold = self.scope.ascii_fold();
let unicode_fold = self.scope.unicode_fold();
let after = start + len;
let range = if in_class {
self.range_end(after)
} else {
RangeEnd::Single
};
match range {
RangeEnd::Illegal => return None,
RangeEnd::To {
end,
code: end_code,
len: end_len,
} => {
if unicode_fold {
if end_code < code {
return None;
}
java_case::push_members(&mut self.out, &java_case::range(code, end_code));
} else {
let (first, last) = (&chars[start..after], &chars[end..end + end_len]);
java_fold::push_member(&mut self.out, first, code, true);
self.out.push('-');
java_fold::push_member(&mut self.out, last, end_code, true);
if fold {
java_fold::push_other_case_ranges(&mut self.out, code, end_code);
}
}
self.at = end + end_len;
}
RangeEnd::Single => {
let source = &chars[start..after];
if !in_class && !is_meta(source) {
self.run = self.prior_run + 1;
}
if fold {
java_fold::push_folded(&mut self.out, source, code, in_class);
} else if let Some(cases) = unicode_fold.then(|| self.cases(code, after)).flatten()
{
if in_class {
java_case::push_members(&mut self.out, &cases);
} else {
java_case::push_class(&mut self.out, &cases);
}
} else {
java_fold::push_member(&mut self.out, source, code, in_class);
}
self.at = after;
}
}
Some(())
}
fn cases(&self, code: u32, after: usize) -> Option<java_case::Ranges> {
if self.in_class() {
java_case::member(code)
} else {
java_case::literal(code, || self.in_literal_run(after))
}
}
fn push_anchor(&mut self, anchor: &str, len: usize) {
if self.quantifier_at(self.at + len) {
self.out.push_str("(?:");
self.out.push_str(anchor);
self.out.push_str("|(?!))");
} else {
self.out.push_str(anchor);
}
self.at += len;
}
fn quantifier_at(&self, at: usize) -> bool {
let at = at + self.ignored_len(at);
matches!(self.chars.get(at), Some('*' | '+' | '?' | '{'))
}
fn in_literal_run(&self, after: usize) -> bool {
let (chars, mut at, mut following) = (self.chars, after, 0);
loop {
at += self.ignored_len(at);
match chars.get(at) {
Some('\\') => match java_fold::read_escape(chars, at, false) {
Consumed {
value: Escape::Char(_),
len,
} => at += len,
_ => break,
},
Some(&c) if !is_meta(&[c]) && !matches!(c, '(' | ')' | '[') => at += 1,
_ => break,
}
following += 1;
}
let quantified = matches!(chars.get(at), Some('*' | '+' | '?' | '{'));
let unwound = quantified && following == 0;
!unwound && self.run + following - usize::from(quantified) >= 2
}
fn ampersand(&mut self) -> Option<()> {
let second = self.at + 1 + self.ignored_len(self.at + 1);
let (out_at, at) = (self.out.len(), self.at);
if self.chars.get(second) == Some(&'&') {
self.push_class_member(out_at, ClassMember::Intersect);
self.out.push_str("&&");
self.at = second + 1;
} else {
self.atom(u32::from('&'), 1)?;
self.class_member(out_at, at);
}
Some(())
}
fn braces(&mut self) -> Option<()> {
let len = self.bounds_len(self.at)?;
self.copy(len);
self.quantified = Quantified::Open;
Some(())
}
fn bounds_len(&self, at: usize) -> Option<usize> {
let rest = &self.chars[at..];
let end = rest.iter().position(|&c| c == '}')?;
let bounds = &rest[1..end];
if !bounds.first().is_some_and(char::is_ascii_digit)
|| !bounds.iter().all(|&c| c.is_ascii_digit() || c == ',')
{
return None;
}
let text: String = bounds.iter().collect();
let count = |digits: &str| digits.parse::<i32>().ok();
let legal = match text.split_once(',') {
Some((min, "")) => count(min).is_some(),
Some((min, max)) => count(min)
.zip(count(max))
.is_some_and(|(min, max)| min <= max),
None => count(&text).is_some(),
};
legal.then_some(end + 1)
}
fn group_curly_at(&self, at: usize) -> bool {
let at = at + self.ignored_len(at);
let len = match self.chars.get(at) {
Some('*' | '+') => Some(1),
Some('{') => self.bounds_len(at),
_ => None,
};
len.is_some_and(|len| {
let modifier = at + len;
self.chars.get(modifier + self.ignored_len(modifier)) != Some(&'+')
})
}
fn open_group(&mut self, captures: bool) {
self.outer.push(self.scope);
let number = captures.then(|| {
self.group_repeats.push(false);
self.group_repeats.len() - 1
});
self.groups.push(number);
self.quantified = Quantified::Start;
}
fn range_end(&self, after: usize) -> RangeEnd {
let dash = after + self.ignored_len(after);
if self.chars.get(dash) != Some(&'-') {
return RangeEnd::Single;
}
if matches!(self.chars.get(dash + 1), None | Some(']' | '[')) {
return RangeEnd::Single;
}
let end = dash + 1 + self.ignored_len(dash + 1);
match java_fold::read_member(self.chars, end) {
Some(Consumed { value: code, len }) => RangeEnd::To { end, code, len },
None => RangeEnd::Illegal,
}
}
fn open_class(&mut self) {
let out_at = self.out.len();
self.push_class_member(out_at, ClassMember::Nested);
self.class_start = true;
self.copy(1);
if self.chars.get(self.at) == Some(&'^') {
self.copy(1);
}
self.classes.push(OpenClass {
members_at: self.out.len(),
members: Vec::new(),
});
}
fn close_class(&mut self) -> Option<()> {
let class = self.classes.pop()?;
if class
.members
.iter()
.any(|&(_, member)| member == ClassMember::Intersect)
{
let text = self.out.split_off(class.members_at);
let mut members = Vec::with_capacity(class.members.len());
for (index, &(at, member)) in class.members.iter().enumerate() {
let end = class
.members
.get(index + 1)
.map_or(text.len(), |&(next, _)| next - class.members_at);
members.push((member, &text[at - class.members_at..end]));
}
let mut bit_sets = Vec::new();
let mut next = 0;
let set = java_class(&members, &mut next, &mut bit_sets)?;
set.write(&mut self.out, &bit_sets);
}
self.out.push(']');
self.at += 1;
Some(())
}
fn push_class_member(&mut self, out_at: usize, member: ClassMember) {
if let Some(class) = self.classes.last_mut() {
class.members.push((out_at, member));
}
}
fn class_member(&mut self, out_at: usize, at: usize) {
let single = java_fold::read_member(self.chars, at)
.filter(|member| at + member.len == self.at)
.map(|member| member.value);
let unicode_fold = self.scope.unicode_fold();
let bits = single.is_some_and(|code| {
code < 0x100
&& !(unicode_fold
&& matches!(
code,
0xFF | 0xB5 | 0x49 | 0x69 | 0x53 | 0x73 | 0x4B | 0x6B | 0xC5 | 0xE5
))
});
let ampersand = self.chars[at] == '&';
self.push_class_member(
out_at,
if bits {
ClassMember::Bits { ampersand }
} else {
ClassMember::Set { ampersand }
},
);
}
fn group(&mut self) -> Option<()> {
let chars = self.chars;
if chars.get(self.at + 1) != Some(&'?') {
self.open_group(true);
self.copy(1);
return Some(());
}
let rest = &chars[self.at + 2..];
let (opener, captures) = match rest.first() {
Some(':' | '=' | '!' | '>') => (3, false),
Some('<') => match rest.get(1) {
Some('=' | '!') => {
let code_points = self.last_supplementary.is_some_and(|last| last >= self.at);
self.look_behinds.push((self.out.len(), code_points));
(4, false)
}
Some(first) if first.is_ascii_alphabetic() => {
let name = rest[2..].iter().position(|c| !c.is_ascii_alphanumeric())?;
if rest[2 + name] != '>' {
return None;
}
let text: String = rest[1..2 + name].iter().collect();
if self.group_names.contains(&text) {
return None;
}
self.group_names.push(text);
(name + 5, true)
}
_ => return None,
},
Some(_) => return self.flag_group(rest),
None => return None,
};
self.open_group(captures);
self.copy(opener);
Some(())
}
fn flag_group(&mut self, rest: &[char]) -> Option<()> {
let group = java_flag_group(rest)?;
if group.scoped {
self.open_group(false);
} else {
self.quantified = Quantified::Start;
}
for &FlagChange { flag, on } in &group.changes {
self.scope.set(flag, on);
}
self.out.push_str(&group.translation());
self.at += 2 + group.len;
Some(())
}
}
enum RangeEnd {
Single,
To { end: usize, code: u32, len: usize },
Illegal,
}
#[derive(Debug, Clone, Copy, Default)]
struct Scope {
dot: Dot,
unicode_classes: bool,
case: Case,
comments: bool,
}
#[derive(Debug, Clone, Copy, Default)]
struct Dot {
dotall: bool,
unix_lines: bool,
multiline: bool,
}
#[derive(Debug, Clone, Copy, Default)]
struct Case {
ignore: bool,
unicode: bool,
}
impl Scope {
fn set(&mut self, flag: char, on: bool) {
match flag {
's' => self.dot.dotall = on,
'd' => self.dot.unix_lines = on,
'm' => self.dot.multiline = on,
'i' => self.case.ignore = on,
'u' => self.case.unicode = on,
'x' => self.comments = on,
'U' => {
self.unicode_classes = on;
self.case.unicode = on;
}
_ => {}
}
}
fn ascii_fold(self) -> bool { self.case.ignore && !self.case.unicode }
fn unicode_fold(self) -> bool { self.case.ignore && self.case.unicode }
}
fn is_meta(source: &[char]) -> bool {
matches!(source, ['*' | '+' | '?' | '{' | '$' | '.' | '^' | '|'])
}
const JAVA_DOT: &str = "[^\\n\\r\\x{85}\\x{2028}\\x{2029}]";
fn dollar(dot: Dot) -> &'static str {
match (dot.multiline, dot.unix_lines) {
(false, false) => "(?:(?=(?:\\r\\n|(?<!\\r)\\n|[\\r\\x{85}\\x{2028}\\x{2029}])?\\z))",
(true, false) => "(?:(?=\\z|(?<!\\r)\\n|[\\r\\x{85}\\x{2028}\\x{2029}]))",
(false, true) => "(?:(?=\\n?\\z))",
(true, true) => "(?:(?=\\n|\\z))",
}
}
fn caret(dot: Dot) -> &'static str {
if dot.unix_lines {
"(?:(?!\\z)(?:\\A|(?<=\\n)))"
} else {
"(?:(?!\\z)(?:\\A|(?<=[\\n\\x{85}\\x{2028}\\x{2029}])|(?<=\\r)(?!\\n)))"
}
}
fn java_space(escape: char, in_class: bool) -> Option<&'static str> {
Some(match (escape, in_class) {
('h', false) => {
"[\\t\\x20\\xA0\\x{1680}\\x{180E}\\x{2000}-\\x{200A}\\x{202F}\\x{205F}\\x{3000}]"
}
('h', true) => {
"\\t\\x20\\xA0\\x{1680}\\x{180E}\\x{2000}-\\x{200A}\\x{202F}\\x{205F}\\x{3000}"
}
('H', _) => {
"[^\\t\\x20\\xA0\\x{1680}\\x{180E}\\x{2000}-\\x{200A}\\x{202F}\\x{205F}\\x{3000}]"
}
('v', false) => "[\\n-\\r\\x{85}\\x{2028}\\x{2029}]",
('v', true) => "\\n-\\r\\x{85}\\x{2028}\\x{2029}",
('V', _) => "[^\\n-\\r\\x{85}\\x{2028}\\x{2029}]",
_ => return None,
})
}
struct OpenClass {
members_at: usize,
members: Vec<(usize, ClassMember)>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ClassMember {
Bits { ampersand: bool },
Set { ampersand: bool },
Nested,
Intersect,
}
#[derive(Clone)]
enum ClassSet<'a> {
Bits(usize),
Member(&'a str),
Union(Box<Self>, Box<Self>),
And(Box<Self>, Box<Self>),
}
impl ClassSet<'_> {
fn union(left: Option<Self>, right: Self) -> Self {
match left {
Some(left) => Self::Union(Box::new(left), Box::new(right)),
None => right,
}
}
fn write(&self, out: &mut String, bit_sets: &[String]) {
out.push('[');
match self {
Self::Bits(index) => out.push_str(&bit_sets[*index]),
Self::Member(text) => push_class_text(out, text),
Self::Union(left, right) => {
left.write(out, bit_sets);
right.write(out, bit_sets);
}
Self::And(left, right) => {
left.write(out, bit_sets);
out.push_str("&&");
right.write(out, bit_sets);
}
}
out.push(']');
}
}
fn push_class_text(out: &mut String, text: &str) {
if text.starts_with('^') || matches!(text, "&" | "~") {
out.push('\\');
}
out.push_str(text);
}
fn java_class<'a>(
members: &[(ClassMember, &'a str)],
next: &mut usize,
bit_sets: &mut Vec<String>,
) -> Option<ClassSet<'a>> {
let bits = bit_sets.len();
bit_sets.push(String::new());
let (mut prev, mut curr, mut has_bits) = (None, None, false);
while let Some(&(member, text)) = members.get(*next) {
match member {
ClassMember::Bits { .. } => {
push_class_text(&mut bit_sets[bits], text);
curr = None;
has_bits = true;
*next += 1;
}
ClassMember::Set { .. } | ClassMember::Nested => {
curr = Some(ClassSet::Member(text));
prev = Some(ClassSet::union(prev, ClassSet::Member(text)));
*next += 1;
}
ClassMember::Intersect => {
*next += 1;
let mut right = None;
while let Some(&(member, text)) = members.get(*next) {
right = Some(match member {
ClassMember::Intersect
| ClassMember::Bits { ampersand: true }
| ClassMember::Set { ampersand: true } => break,
ClassMember::Nested => {
*next += 1;
ClassSet::union(right, ClassSet::Member(text))
}
_ => ClassSet::union(right, java_class(members, next, bit_sets)?),
});
}
if std::mem::take(&mut has_bits) {
match prev {
None => {
prev = Some(ClassSet::Bits(bits));
curr = Some(ClassSet::Bits(bits));
}
Some(set) => prev = Some(ClassSet::union(Some(set), ClassSet::Bits(bits))),
}
}
if right.is_some() {
curr.clone_from(&right);
}
prev = Some(match prev {
None => right?,
Some(set) => ClassSet::And(Box::new(set), Box::new(curr.clone()?)),
});
}
}
}
match (prev, has_bits) {
(None, false) => None,
(None, true) => Some(ClassSet::Bits(bits)),
(Some(set), false) => Some(set),
(Some(set), true) => Some(ClassSet::union(Some(set), ClassSet::Bits(bits))),
}
}
fn ascii_shorthand(escape: char, in_class: bool) -> Option<&'static str> {
Some(match (escape, in_class) {
('w', false) => "[0-9A-Za-z_]",
('w', true) => "0-9A-Za-z_",
('W', _) => "[^0-9A-Za-z_]",
('d', false) => "[0-9]",
('d', true) => "0-9",
('D', _) => "[^0-9]",
('s', false) => "[ \\t\\n\\x0B\\x0C\\r]",
('s', true) => " \\t\\n\\x0B\\x0C\\r",
('S', _) => "[^ \\t\\n\\x0B\\x0C\\r]",
('b', false) => "(?:(?<=[0-9A-Za-z_])(?![0-9A-Za-z_])|(?<![0-9A-Za-z_])(?=[0-9A-Za-z_]))",
('B', false) => "(?:(?<=[0-9A-Za-z_])(?=[0-9A-Za-z_])|(?<![0-9A-Za-z_])(?![0-9A-Za-z_]))",
_ => return None,
})
}
fn java_flag_group(rest: &[char]) -> Option<FlagGroup> {
let mut on = String::new();
let mut off = String::new();
let mut changes = Vec::new();
let mut clearing = false;
for (n, &c) in rest.iter().enumerate() {
match c {
's' => {
if clearing {
off.push(c);
} else {
on.push(c);
}
changes.push(FlagChange {
flag: c,
on: !clearing,
});
}
'i' | 'u' | 'd' | 'm' | 'x' | 'U' => changes.push(FlagChange {
flag: c,
on: !clearing,
}),
'c' => {}
'-' if !clearing => clearing = true,
')' | ':' => {
return Some(FlagGroup {
on,
off,
len: n + 1,
scoped: c == ':',
changes,
});
}
_ => return None,
}
}
None
}
struct FlagGroup {
on: String,
off: String,
len: usize,
scoped: bool,
changes: Vec<FlagChange>,
}
#[derive(Clone, Copy)]
struct FlagChange {
flag: char,
on: bool,
}
impl FlagGroup {
fn translation(&self) -> String {
let (on, off) = (&self.on, &self.off);
let end = if self.scoped { ':' } else { ')' };
match (on.is_empty() && off.is_empty(), self.scoped) {
(true, false) => String::new(),
(true, true) => "(?:".to_string(),
(false, _) if off.is_empty() => format!("(?{on}{end}"),
(false, _) => format!("(?{on}-{off}{end}"),
}
}
}
#[cfg(test)]
mod tests {
use {
super::translate_java,
crate::JavaPattern,
assert2::check,
fancy_regex::{Expr, LookAround},
};
struct MatchCase<'a> {
source: &'a str,
input: &'a str,
expected: bool,
}
const fn row<'a>(source: &'a str, input: &'a str, expected: bool) -> MatchCase<'a> {
MatchCase {
source,
input,
expected,
}
}
#[test]
fn bounds_with_no_atom_before_them_repeat_the_empty_atom() {
let cases = [
row("{2}", "", true),
row("{2}", "{2}", false),
row("x|{2}", "", true),
row("x|{2}{3}", "", true),
row("a|{2}b", "b", true),
row("a|{2}b", "{2}b", false),
row("({2})", "", true),
row("(?:{2})", "", true),
row("(?<n>{2})", "", true),
row("(?={2})", "", true),
row("(?>{2})a", "a", true),
row("(?i:{2})", "", true),
row("{2}{3}", "", true),
row("{2}+", "", true),
row("{2}?", "", true),
row("(?i){2}", "", true),
row("(?s){2}", "", true),
row("a(?s){2}", "a", true),
row("a(?i){2}b", "ab", true),
row("a(?i){2}b", "aB", true),
row("(?x) {2}", "", true),
row("(?iu){2}a", "A", true),
row("(?ix) {2}a", "A", true),
row("(?ix) {2}a", "", false),
row("a{2}{3}", "aa", true),
row("x{2}{3}", "xx", true),
row("a{2}{3}+", "", false),
row("a{2}{3}b{2}", "aabb", true),
row("(a){2}{3}", "aa", true),
row("(a)*{2}", "aaa", true),
row("a*{2}", "aaa", true),
row("a*{2}", "", true),
row("a*{2}b", "aab", true),
row("a*{2}?b", "aab", true),
row("a*{2}+b", "aab", true),
row("a*?{2}b", "aab", true),
row("a*+{2}b", "aab", true),
row("a*{2}{3}", "aa", true),
row("a*{2}{3}?b", "ab", true),
row("a+{2}", "a", true),
row("a?{2}", "aa", false),
row("a{1,2}{2}", "aaa", false),
row("a*{0}", "a", true),
row("(?x)a* {2} ?b", "aab", true),
row("(?x)ab * {2} c", "abbc", true),
row("\\R*{2}", "\r\n\n", true),
row("\\R*{2}", "\n", true),
row("^{2}", "", true),
row("a {2}", "", false),
row("ab{2}", "abb", true),
row("ab{2}", "abab", false),
row("ab*{2}c", "abbc", true),
row("ab*{2}c", "ac", true),
row("ab*?{2}c", "abbc", true),
row("ab*+{2}c", "abbc", true),
row("abc{2}{3}", "abcc", true),
row("\\R{2}", "\n\n", true),
];
for MatchCase {
source,
input,
expected,
} in cases
{
let matched = JavaPattern::compile(source).map(|pattern| pattern.matches(input).ok());
check!(
matched == Ok(Some(expected)),
"{source:?} against {input:?}"
);
}
}
#[yare::parameterized(
star_after_empty_repeat = { "a*{2}*" },
second_lazy_after_empty_repeat = { "a*{2}??" },
star_after_empty_repeat_past_whitespace = { "(?x)a* {2} *" },
star_after_leading_repeat = { "{2}*" },
second_lazy_after_leading_repeat = { "{2}??" },
star_at_start = { "*" },
star_after_bar = { "x|*" },
star_after_open = { "(*)" },
star_after_emptied_flags = { "(?i)*" },
plus_after_emptied_flags = { "(?i)+" },
question_after_emptied_flags = { "(?i)?" },
star_after_flags = { "(?s)*" },
letter_bounds_after_bounds = { "a{2}{x}" },
no_minimum_after_star = { "a*{,2}" },
reversed_range_after_star = { "a*{2,1}" },
huge_bound_after_star = { "a*{99999999999}" },
)]
fn a_quantifier_java_refuses_is_refused(source: &str) {
check!(JavaPattern::compile(source).is_err(), "{source:?}");
}
#[yare::parameterized(
star = { "()*", &[false, true] },
inside_noncapturing_group = { "(?:())*", &[false, false] },
possessive = { "()*+", &[false, false] },
optional = { "()?", &[false, false] },
lazy = { "(a)*?", &[false, true] },
counted_named = { "(?<n>a){2}", &[false, true] },
nested = { "((a))+", &[false, true, false] },
second = { "(a)(b)*", &[false, false, true] },
past_whitespace = { "(?x)(a) *", &[false, true] },
inside_scoped_flags = { "(?i:(a))*", &[false, false] },
unquantified = { "(a)", &[false, false] },
)]
fn group_repeats_marks_the_groups_a_group_curly_repeats(source: &str, expected: &[bool]) {
let group_repeats = translate_java(source).map(|translation| translation.group_repeats);
check!(group_repeats.as_deref() == Some(expected), "{source:?}");
}
#[yare::parameterized(
plain = { "(?<=a)b", &[false] },
supplementary_after = { "(?<=a)😀", &[true] },
supplementary_before = { "😀(?<=a)b", &[false] },
both_before_it = { "(?<=a)b(?<!c)😀", &[true, true] },
only_the_first_before_it = { "(?<=a)😀(?<!c)", &[true, false] },
escape_is_ascii = { r"(?<=\x{1F600})x", &[false] },
nested_outer_first = { "(?<=(?<=😀)a)b", &[true, true] },
quoted_before = { r"\Q😀\E(?<=a)", &[false] },
quoted_after = { r"(?<=a)\Q😀\E", &[true] },
quoted_opener = { r"\Q(?<=\Ea", &[] },
in_a_class = { "[(?<=]a", &[] },
in_a_nested_class = { "[a[(?<!]]b", &[] },
named_group = { "(?<n>a)(?<=b)", &[false] },
multiline_caret = { "(?m)^(?<=a)😀", &[false, false, true] },
word_boundary = { r"\b(?<=a)", &[false, false, false] },
not_word_boundary = { r"(?<=😀)\B", &[true, false, false] },
dollar = { "(?<=a)$😀", &[true, false] },
quantified_dollar = { "😀$?(?<!a)", &[false, false] },
none = { "abc", &[] },
)]
fn look_behind_code_points_follows_find_supplementary(source: &str, expected: &[bool]) {
let translation = translate_java(source);
let code_points = translation
.as_ref()
.map(|translation| translation.look_behind_code_points.as_slice());
check!(code_points == Some(expected), "{source:?}");
let tree = translation.map(|translation| Expr::parse_tree(&translation.pattern));
let look_behinds = match &tree {
Some(Ok(tree)) => Some(look_behinds_in(&tree.expr)),
_ => None,
};
check!(look_behinds == Some(expected.len()), "{source:?}");
}
fn look_behinds_in(expr: &Expr) -> usize {
let own = usize::from(matches!(
expr,
Expr::LookAround(_, LookAround::LookBehind | LookAround::LookBehindNeg)
));
own + expr.children_iter().map(look_behinds_in).sum::<usize>()
}
}