mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-24 02:31:13 +08:00
fix: 同名捕获组回引与 groups 字段改末次已定义语义
This commit is contained in:
Vendored
+114
-6
@@ -339,15 +339,14 @@ impl<'m> Iterator for NamedGroups<'m> {
|
||||
continue;
|
||||
}
|
||||
|
||||
// This is the first occurrence of this name. Find the best range value.
|
||||
// Prefer a Some value over None when there are duplicate names.
|
||||
// This is the first occurrence of this name. Resolve the value to
|
||||
// the last occurrence of this name that matched: a later duplicate
|
||||
// overrides an earlier one only when it is defined.
|
||||
let mut best_range = self.mat.captures[idx].clone();
|
||||
for check_idx in (idx + 1)..end {
|
||||
if self.mat.group_names[check_idx].as_ref() == name {
|
||||
// Found a duplicate name. Prefer a Some value over None.
|
||||
if best_range.is_none() && self.mat.captures[check_idx].is_some() {
|
||||
best_range = self.mat.captures[check_idx].clone();
|
||||
break; // Stop once we find a Some value
|
||||
if let Some(range) = self.mat.captures[check_idx].as_ref() {
|
||||
best_range = Some(range.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1272,3 +1271,112 @@ mod utf16_tests {
|
||||
assert_eq!(str_matches(&re, text), utf16_matches(&re, &to_units(text)));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod named_groups_tests {
|
||||
use super::Regex;
|
||||
|
||||
/// 同名双定义(循环内各迭代命中不同出现):named_groups 取末次已定义出现,
|
||||
/// 键序为首现源序。
|
||||
#[test]
|
||||
fn named_groups_last_defined_wins() {
|
||||
let re = Regex::new(r"(?:(?<x>a)|(?<x>b)){2}").unwrap();
|
||||
let m = re.find("ab").unwrap();
|
||||
let named: Vec<_> = m.named_groups().collect();
|
||||
assert_eq!(named, vec![("x", Some(1..2))], "末次已定义出现(b)胜出");
|
||||
// 首出现被迭代 2 重入清槽,末次出现命中 b。
|
||||
assert_eq!(m.captures, vec![None, Some(1..2)], "result 数组各出现独立交付");
|
||||
}
|
||||
|
||||
/// 末次出现未定义时回退至更早已定义出现。
|
||||
#[test]
|
||||
fn named_groups_falls_back_to_earlier_defined() {
|
||||
let re = Regex::new(r"(?:(?<x>a)|(?<x>b)|c){2}").unwrap();
|
||||
let m = re.find("ca").unwrap();
|
||||
let named: Vec<_> = m.named_groups().collect();
|
||||
assert_eq!(named, vec![("x", Some(1..2))], "末次出现未定义,回退首出现");
|
||||
assert_eq!(m.captures, vec![Some(1..2), None]);
|
||||
}
|
||||
|
||||
/// 全部出现未定义时取 undefined。
|
||||
#[test]
|
||||
fn named_groups_all_undefined() {
|
||||
let re = Regex::new(r"(?:(?<x>a)|(?<x>b)|c){2}").unwrap();
|
||||
let m = re.find("cc").unwrap();
|
||||
let named: Vec<_> = m.named_groups().collect();
|
||||
assert_eq!(named, vec![("x", None)]);
|
||||
assert_eq!(m.captures, vec![None, None]);
|
||||
}
|
||||
|
||||
/// 同名回引读末次已定义出现:迭代 2 中末次出现命中 b,回引读 b 命中 "aabb"。
|
||||
#[test]
|
||||
fn backref_reads_last_defined() {
|
||||
let re = Regex::new(r"(?:(?:(?<x>a)|(?<x>b))\k<x>){2}").unwrap();
|
||||
let m = re.find("aabb").unwrap();
|
||||
assert_eq!(m.range(), 0..4);
|
||||
assert_eq!(m.captures, vec![None, Some(2..3)], "各出现独立交付");
|
||||
}
|
||||
|
||||
/// 单轮同名回引:末次已定义分别为首出现(aa)与次出现(bb)。
|
||||
#[test]
|
||||
fn backref_single_round() {
|
||||
let re = Regex::new(r"(?:(?<x>a)|(?<x>b))\k<x>").unwrap();
|
||||
let m = re.find("aa").unwrap();
|
||||
assert_eq!(m.range(), 0..2);
|
||||
assert_eq!(m.captures, vec![Some(0..1), None]);
|
||||
let m = re.find("bb").unwrap();
|
||||
assert_eq!(m.range(), 0..2);
|
||||
assert_eq!(m.captures, vec![None, Some(0..1)]);
|
||||
}
|
||||
|
||||
/// 全部出现未定义时回引空匹配;末次已定义值不匹配时整体失败(不回落空匹配,
|
||||
/// 也不回落更早已定义出现)。
|
||||
#[test]
|
||||
fn backref_undefined_empty_and_no_fallthrough() {
|
||||
let re = Regex::new(r"^(?:(?<a>x)|(?<a>y)|z)\k<a>$").unwrap();
|
||||
let m = re.find("z").unwrap();
|
||||
assert_eq!(m.range(), 0..1);
|
||||
assert_eq!(m.captures, vec![None, None]);
|
||||
assert!(re.find("zz").is_none(), "末次已定义值不匹配时整体失败");
|
||||
|
||||
// 末次已定义为 x(位 2 为 y,不匹配 x);更早已定义为 y(位 2 亦不匹配)。
|
||||
let re = Regex::new(r"(?:(?<a>y)|(?<a>x)){2}\k<a>").unwrap();
|
||||
assert!(re.find("yxy").is_none(), "末次已定义值不匹配时整体失败");
|
||||
// 对照:末次已定义值匹配时命中。
|
||||
let m = re.find("yxx").unwrap();
|
||||
assert_eq!(m.range(), 0..3);
|
||||
}
|
||||
|
||||
/// 循环重入清槽后全部未定义:回引空匹配,结果数组各出现 undefined。
|
||||
#[test]
|
||||
fn backref_loop_reentry_clears() {
|
||||
let re = Regex::new(r"^(?:(?<a>x)|(?<a>y)|z){2}\k<a>$").unwrap();
|
||||
let m = re.find("xz").unwrap();
|
||||
assert_eq!(m.range(), 0..2);
|
||||
assert_eq!(m.captures, vec![None, None]);
|
||||
assert!(re.find("xzx").is_none());
|
||||
}
|
||||
|
||||
/// 未定义组回引空匹配(单出现面不变)。
|
||||
#[test]
|
||||
fn backref_single_occurrence_undefined_empty() {
|
||||
let re = Regex::new(r"(?<a>x)|(?:zy\k<a>)").unwrap();
|
||||
let m = re.find("zy").unwrap();
|
||||
assert_eq!(m.range(), 0..2);
|
||||
assert_eq!(m.captures, vec![None]);
|
||||
}
|
||||
|
||||
/// PikeVM 执行器与回退执行器对同名回引口径一致。
|
||||
#[cfg(feature = "backend-pikevm")]
|
||||
#[test]
|
||||
fn pikevm_backref_agrees() {
|
||||
use crate::backends::PikeVMExecutor;
|
||||
use crate::exec::{Executor, Matches};
|
||||
let re = Regex::new(r"(?:(?:(?<x>a)|(?<x>b))\k<x>){2}").unwrap();
|
||||
let m = Matches::new(PikeVMExecutor::new(&re.cr, "aabb"), 0)
|
||||
.next()
|
||||
.expect("pikevm 应命中 aabb");
|
||||
assert_eq!(m.range(), 0..4);
|
||||
assert_eq!(m.captures, vec![None, Some(2..3)]);
|
||||
}
|
||||
}
|
||||
|
||||
+21
@@ -886,6 +886,27 @@ impl<'a, Input: InputIndexer> MatchAttempter<'a, Input> {
|
||||
next_or_bt!(matched)
|
||||
}
|
||||
|
||||
Insn::BackRefLastDefined(cg_indices) => {
|
||||
// Scan the occurrences in reverse source order and match the
|
||||
// text of the last occurrence that is defined; a later
|
||||
// occurrence overrides an earlier one only when it matched.
|
||||
let mut matched = true;
|
||||
for &cg_idx in cg_indices.iter().rev() {
|
||||
let cg = self.s.groups.mat(cg_idx as usize);
|
||||
if let Some(orig_range) = cg.as_range() {
|
||||
if re.flags.icase {
|
||||
matched = matchers::backref_icase(input, dir, orig_range, &mut pos);
|
||||
} else {
|
||||
matched = matchers::backref(input, dir, orig_range, &mut pos);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
// No occurrence is defined and so the match succeeds (ES6
|
||||
// 21.2.2.9).
|
||||
next_or_bt!(matched)
|
||||
}
|
||||
|
||||
&Insn::Lookahead {
|
||||
negate,
|
||||
start_group,
|
||||
|
||||
Vendored
+8
@@ -340,6 +340,14 @@ impl Emitter {
|
||||
self.emit_insn(Insn::BackRef(group - 1))
|
||||
}
|
||||
|
||||
Node::BackRefLastDefined(groups) => {
|
||||
debug_assert!(!groups.is_empty(), "Should not be empty");
|
||||
// -1 because group indices are 1-based in the IR.
|
||||
let indices: Vec<CaptureGroupID> =
|
||||
groups.iter().map(|g| (*g - 1) as CaptureGroupID).collect();
|
||||
self.emit_insn(Insn::BackRefLastDefined(indices))
|
||||
}
|
||||
|
||||
Node::ByteSet(bytes) => self.emit_byte_set_insn(bytes),
|
||||
|
||||
Node::CharSet(chars) => {
|
||||
|
||||
Vendored
+5
@@ -102,6 +102,11 @@ pub enum Insn {
|
||||
/// Perform a backreference match.
|
||||
BackRef(u32),
|
||||
|
||||
/// Perform a backreference match against the last-defined occurrence of a
|
||||
/// name shared by multiple capture groups. Indices are 0-based, in source
|
||||
/// order; if no occurrence is defined the match succeeds.
|
||||
BackRefLastDefined(Vec<CaptureGroupID>),
|
||||
|
||||
/// Match the next character against the bracket contents, stored at the given index in the CompiledRegex.
|
||||
Bracket(usize),
|
||||
|
||||
|
||||
Vendored
+12
-2
@@ -84,10 +84,14 @@ pub enum Node {
|
||||
/// A backreference.
|
||||
/// Indices are 1-based. That is, it matches JS syntax: \1 is the first capture group,
|
||||
/// not everything.
|
||||
/// A backreference that logically may match multiple capture groups (through shared names)
|
||||
/// are emitted through alternations of backreferences.
|
||||
BackRef(u32),
|
||||
|
||||
/// A backreference to a name shared by multiple capture groups.
|
||||
/// Indices are 1-based, one per occurrence in source order. At match time it
|
||||
/// matches the text of the last occurrence that is defined; if none is
|
||||
/// defined, it matches the empty string.
|
||||
BackRefLastDefined(Vec<u32>),
|
||||
|
||||
/// A bracket.
|
||||
Bracket(BracketContents),
|
||||
|
||||
@@ -232,6 +236,7 @@ impl Node {
|
||||
}
|
||||
&Node::WordBoundary { invert } => Node::WordBoundary { invert },
|
||||
&Node::BackRef(idx) => Node::BackRef(idx),
|
||||
Node::BackRefLastDefined(groups) => Node::BackRefLastDefined(groups.clone()),
|
||||
Node::Bracket(bc) => Node::Bracket(bc.clone()),
|
||||
// Do not reverse into lookarounds, they already have the right sense.
|
||||
Node::LookaroundAssertion {
|
||||
@@ -308,6 +313,7 @@ where
|
||||
| Node::CharSet(..)
|
||||
| Node::WordBoundary { .. }
|
||||
| Node::BackRef { .. }
|
||||
| Node::BackRefLastDefined { .. }
|
||||
| Node::Bracket { .. }
|
||||
| Node::MatchAny
|
||||
| Node::MatchAnyExceptLineTerminator
|
||||
@@ -381,6 +387,7 @@ where
|
||||
| Node::Anchor { .. }
|
||||
| Node::WordBoundary { .. }
|
||||
| Node::BackRef { .. }
|
||||
| Node::BackRefLastDefined { .. }
|
||||
| Node::Bracket { .. } => {}
|
||||
Node::Cat(nodes) => {
|
||||
nodes.iter_mut().for_each(|node| self.process(node));
|
||||
@@ -548,6 +555,9 @@ fn display_node(node: &Node, depth: usize, f: &mut fmt::Formatter) -> fmt::Resul
|
||||
&Node::BackRef(group) => {
|
||||
writeln!(f, "BackRef {:?} ", group)?;
|
||||
}
|
||||
Node::BackRefLastDefined(groups) => {
|
||||
writeln!(f, "BackRefLastDefined {:?} ", groups)?;
|
||||
}
|
||||
Node::Bracket(contents) => {
|
||||
writeln!(f, "Bracket {:?}", contents)?;
|
||||
}
|
||||
|
||||
Vendored
+4
-1
@@ -160,7 +160,10 @@ fn remove_empties(n: &mut Node, _w: &Walk) -> PassAction {
|
||||
// Capture groups could in principle be optimized if they only match empties.
|
||||
PassAction::Keep
|
||||
}
|
||||
Node::WordBoundary { .. } | Node::BackRef { .. } | Node::Bracket { .. } => PassAction::Keep,
|
||||
Node::WordBoundary { .. }
|
||||
| Node::BackRef { .. }
|
||||
| Node::BackRefLastDefined { .. }
|
||||
| Node::Bracket { .. } => PassAction::Keep,
|
||||
Node::LookaroundAssertion {
|
||||
negate, contents, ..
|
||||
} => {
|
||||
|
||||
Vendored
+3
-9
@@ -1689,15 +1689,9 @@ where
|
||||
ir::Node::BackRef(group_indices[0] + 1)
|
||||
}
|
||||
_ => {
|
||||
// Unusual case of multiple groups sharing a name: the backref should try each in turn.
|
||||
// Lower to alternations of backreferences. Reverse to keep it right-associative: a | (b | (c | d))...
|
||||
let backrefs = group_indices
|
||||
.iter()
|
||||
.rev()
|
||||
.map(|group_index| ir::Node::BackRef(*group_index + 1));
|
||||
backrefs
|
||||
.reduce(|right, left| ir::Node::Alt(Box::new(left), Box::new(right)))
|
||||
.unwrap()
|
||||
// Multiple groups sharing a name: the backref reads the
|
||||
// last occurrence that is defined at match time.
|
||||
ir::Node::BackRefLastDefined(group_indices.iter().map(|i| i + 1).collect())
|
||||
}
|
||||
};
|
||||
Ok(node)
|
||||
|
||||
Vendored
+21
@@ -238,6 +238,27 @@ fn try_match_state<Input: InputIndexer, Dir: Direction>(
|
||||
nextinsn_or_fail!(matched)
|
||||
}
|
||||
|
||||
Insn::BackRefLastDefined(group_indices) => {
|
||||
// Scan the occurrences in reverse source order and match the text of
|
||||
// the last occurrence that is defined; a later occurrence overrides
|
||||
// an earlier one only when it matched.
|
||||
let mut matched = true;
|
||||
for &group_idx in group_indices.iter().rev() {
|
||||
let group = &s.groups[group_idx as usize];
|
||||
if let Some(orig_range) = group.as_range() {
|
||||
if re.flags.icase {
|
||||
matched = matchers::backref_icase(input, dir, orig_range, &mut s.pos);
|
||||
} else {
|
||||
matched = matchers::backref(input, dir, orig_range, &mut s.pos);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
// No occurrence is defined, and therefore the match succeeds
|
||||
// (ES6 21.2.2.9).
|
||||
nextinsn_or_fail!(matched)
|
||||
}
|
||||
|
||||
&Insn::Lookahead {
|
||||
negate,
|
||||
start_group: _,
|
||||
|
||||
+1
-1
@@ -123,7 +123,7 @@ fn compute_start_predicate(n: &Node) -> Option<AbstractStartPredicate> {
|
||||
|
||||
Node::Empty => arbitrary,
|
||||
Node::Goal => arbitrary,
|
||||
Node::BackRef(..) => arbitrary,
|
||||
Node::BackRef(..) | Node::BackRefLastDefined(..) => arbitrary,
|
||||
|
||||
Node::CharSet(chars) => {
|
||||
// Pick the first bytes out.
|
||||
|
||||
Reference in New Issue
Block a user