From 34c74255d781ad0a0bf7cc5ad4ec1761bef61e04 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Tue, 18 Aug 2026 01:57:02 +0900 Subject: refactor(pcre): drop the two bespoke isMatch variants is_match_named and is_match_with_indexed_captures reshaped a match into a name-keyed map or a number-positioned vec, each allocating a String per group up front for callers that then read one or two of them. Every one of the eleven call sites ports a plain Preg::isMatch in PHP, so they now call is_match3 and reach for the group they want through get(&CaptureKey::ByIndex(N)) / get(&CaptureKey::ByName(..)), the same way the rest of the tree already reads a match. Falling out of that: PregNamedGroups existed only to type the first variant; PregMatches::iter() only to build both; and PregMatches::pattern only to give iter() the capture names. PregMatches is now a plain wrapper over regex::Captures, so preg_replace_callback no longer clones the resolved pattern for every match, and preg_match_map! is internal to the shim again. SvnDriver::get_file_content and get_change_date recover the flat `isMatch(..) && $match[2] !== null` condition the PHP has, which the vec shape had forced into a nested if. Co-Authored-By: Claude Opus 5 (1M context) --- crates/shirabe-php-shim/src/preg.rs | 54 +++++++++++++------------------------ 1 file changed, 18 insertions(+), 36 deletions(-) (limited to 'crates/shirabe-php-shim/src') diff --git a/crates/shirabe-php-shim/src/preg.rs b/crates/shirabe-php-shim/src/preg.rs index 74b7f12d..2686bb59 100644 --- a/crates/shirabe-php-shim/src/preg.rs +++ b/crates/shirabe-php-shim/src/preg.rs @@ -8,8 +8,7 @@ pub enum CaptureKey { } /// Defines a newtype over `IndexMap` for one of the `$matches` shapes the `preg_*` functions fill -/// in. Also used by `shirabe_pcre` for the shapes `Composer\Pcre\Preg` adds on top. -#[macro_export] +/// in. macro_rules! preg_match_map { ($(#[$attr:meta])* $vis:vis struct $name:ident($key:ty => $value:ty);) => { $(#[$attr])* @@ -58,18 +57,17 @@ macro_rules! preg_match_map { }; } -/// A single match's `$matches`: the `regex::Captures` the search produced, held alongside the -/// pattern that produced it so groups can be read by both their named and their numbered form. -/// `'h` is the lifetime of the searched subject, which the group values borrow from. +/// A single match's `$matches`: the `regex::Captures` the search produced, read by either the named +/// or the numbered form of a capture group. `'h` is the lifetime of the searched subject, which the +/// group values borrow from. #[derive(Debug)] pub struct PregMatches<'h> { - pattern: ResolvedPattern, caps: regex::Captures<'h>, } impl<'h> PregMatches<'h> { - fn new(pattern: ResolvedPattern, caps: regex::Captures<'h>) -> Self { - Self { pattern, caps } + fn new(caps: regex::Captures<'h>) -> Self { + Self { caps } } /// The value of the group `key` names, or `None` if that group did not participate in the @@ -82,20 +80,6 @@ impl<'h> PregMatches<'h> { }; group.map(|group| group.as_str()) } - - /// Every capture group under both its named and its numbered key (the name preceding its - /// number), in the order PHP fills `$matches` in. - pub fn iter(&self) -> impl Iterator)> + '_ { - let (re, _anchored) = self.pattern.parts(); - re.capture_names() - .enumerate() - .flat_map(move |(index, name)| { - let value = self.caps.get(index).map(|group| group.as_str()); - name.map(|name| (CaptureKey::ByName(name.to_string()), value)) - .into_iter() - .chain(std::iter::once((CaptureKey::ByIndex(index), value))) - }) - } } preg_match_map! { @@ -169,20 +153,18 @@ pub fn preg_match2<'h>( offset: usize, ) -> Option> { let __resolved = pattern.resolve(); - let caps = { - let (re, anchored) = __resolved.parts(); - // An anchored (`A`) pattern must match starting exactly at `offset`; the `regex` crate - // cannot anchor a `captures_at` search, so search the sub-slice beginning at `offset` and - // require the match to start at its head. - if anchored { - re.captures(&subject[offset..]) - .filter(|c| c.get(0).map(|m| m.start()) == Some(0)) - } else { - re.captures_at(subject, offset) - } + let (re, anchored) = __resolved.parts(); + // An anchored (`A`) pattern must match starting exactly at `offset`; the `regex` crate cannot + // anchor a `captures_at` search, so search the sub-slice beginning at `offset` and require the + // match to start at its head. + let caps = if anchored { + re.captures(&subject[offset..]) + .filter(|c| c.get(0).map(|m| m.start()) == Some(0)) + } else { + re.captures_at(subject, offset) }?; - Some(PregMatches::new(__resolved, caps)) + Some(PregMatches::new(caps)) } // PREG_PATTERN_ORDER: the outer vec is indexed by capture group, the inner by @@ -381,7 +363,7 @@ where for caps in re.captures_iter(subject) { let m = caps.get(0).unwrap(); out.extend_from_slice(&subject.as_bytes()[last..m.start()]); - let matches = PregMatches::new(__resolved.clone(), caps); + let matches = PregMatches::new(caps); out.extend_from_slice(callback(&matches)?.as_bytes()); last = m.end(); } @@ -503,7 +485,7 @@ fn translate_php_pattern(pattern: &str) -> anyhow::Result<(String, bool)> { /// `LazyLock`) rather than an owned `regex::Regex` — `regex::Regex::clone()` does not share /// the underlying meta engine's search-cache pool, so producing a fresh owned clone here would pay /// a ~10us per-call cache warmup cost regardless of which path produced it (measured). -#[derive(Debug, Clone)] +#[derive(Debug)] pub enum ResolvedPattern { Cached(Arc<(regex::Regex, bool)>), Static(&'static regex::Regex, bool), -- cgit v1.3.1-4-g156e