aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-php-shim/src
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-08-17 07:20:29 +0900
committernsfisis <nsfisis@gmail.com>2026-08-17 07:20:29 +0900
commit6b4ce98f20b3cfc14f1c955565a8acd9abcc16a2 (patch)
tree1c752ee23480e71fabad42c3bbe2b7ffc7f6acdc /crates/shirabe-php-shim/src
parentc7aa10384a2548167466b4c61f9eff29ed8611f6 (diff)
downloadphp-shirabe-6b4ce98f20b3cfc14f1c955565a8acd9abcc16a2.tar.gz
php-shirabe-6b4ce98f20b3cfc14f1c955565a8acd9abcc16a2.tar.zst
php-shirabe-6b4ce98f20b3cfc14f1c955565a8acd9abcc16a2.zip
refactor(preg): report unmatched groups as null in the vec-shaped preg_*
preg_match_all() and preg_match_all_set_order() were the last preg_* functions handing back a bare Vec<String>, where a group that did not participate is indistinguishable from one that captured "". Hand back Option<String> as the map-shaped functions already do; php_match_row(), the last of the truncate-then-pad helpers, goes with them. preg_split_delim_capture() keeps its Vec<String>. preg_split() accepts no PREG_UNMATCHED_AS_NULL, so there is no null form to move it to, and its result interleaves split segments -- which can never be absent -- with the captured delimiters. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe-php-shim/src')
-rw-r--r--crates/shirabe-php-shim/src/preg.rs46
1 files changed, 16 insertions, 30 deletions
diff --git a/crates/shirabe-php-shim/src/preg.rs b/crates/shirabe-php-shim/src/preg.rs
index 0aa2c43e..b16ee81d 100644
--- a/crates/shirabe-php-shim/src/preg.rs
+++ b/crates/shirabe-php-shim/src/preg.rs
@@ -167,19 +167,15 @@ pub fn preg_match2(
}
// PREG_PATTERN_ORDER: the outer vec is indexed by capture group, the inner by
-// match occurrence. Non-participating groups are reported as "".
-pub fn preg_match_all(pattern: impl PregPattern, subject: &str) -> Vec<Vec<String>> {
+// match occurrence. A non-participating group is reported as None.
+pub fn preg_match_all(pattern: impl PregPattern, subject: &str) -> Vec<Vec<Option<String>>> {
let __resolved = pattern.resolve();
let (re, _anchored) = __resolved.parts();
let group_count = re.captures_len();
- let mut groups: Vec<Vec<String>> = vec![Vec::new(); group_count];
+ let mut groups: Vec<Vec<Option<String>>> = vec![Vec::new(); group_count];
for caps in re.captures_iter(subject) {
for (g, group) in groups.iter_mut().enumerate() {
- group.push(
- caps.get(g)
- .map(|m| m.as_str().to_string())
- .unwrap_or_default(),
- );
+ group.push(caps.get(g).map(|m| m.as_str().to_string()));
}
}
groups
@@ -218,17 +214,22 @@ pub fn preg_match_all2(
}
// PREG_SET_ORDER: the outer vec is indexed by match occurrence, the inner by
-// capture group (a classic `$matches` row).
+// capture group (a `$matches` row). A non-participating group is reported as
+// None.
pub fn preg_match_all_set_order(
pattern: impl PregPattern,
subject: &str,
- matches: &mut Vec<Vec<String>>,
+ matches: &mut Vec<Vec<Option<String>>>,
) -> usize {
let __resolved = pattern.resolve();
let (re, _anchored) = __resolved.parts();
- let mut rows: Vec<Vec<String>> = Vec::new();
+ let mut rows: Vec<Vec<Option<String>>> = Vec::new();
for caps in re.captures_iter(subject) {
- rows.push(php_match_row(&caps));
+ rows.push(
+ (0..caps.len())
+ .map(|g| caps.get(g).map(|m| m.as_str().to_string()))
+ .collect(),
+ );
}
let count = rows.len();
*matches = rows;
@@ -299,8 +300,9 @@ fn preg_split_impl(pattern: impl PregPattern, subject: &str, delim_capture: bool
let m = caps.get(0).unwrap();
result.push(subject[last..m.start()].to_string());
if delim_capture {
- // Mirror preg_match: trailing unmatched groups are dropped, interior
- // unmatched groups are emitted as "".
+ // `preg_split` accepts no PREG_UNMATCHED_AS_NULL, so the split list
+ // holds strings only: trailing unmatched groups are dropped,
+ // interior ones are emitted as "".
if let Some(last_g) = (1..caps.len()).rev().find(|&g| caps.get(g).is_some()) {
for g in 1..=last_g {
result.push(caps.get(g).map(|x| x.as_str()).unwrap_or("").to_string());
@@ -641,22 +643,6 @@ fn php_replacement_group(bytes: &[u8]) -> (usize, usize) {
(group, consumed)
}
-// Classic preg_match `$matches` row: index 0 is the full match, trailing
-// unmatched groups are truncated and interior unmatched groups become "".
-fn php_match_row(caps: &regex::Captures) -> Vec<String> {
- let last = (0..caps.len())
- .rev()
- .find(|&g| caps.get(g).is_some())
- .unwrap_or(0);
- (0..=last)
- .map(|g| {
- caps.get(g)
- .map(|m| m.as_str().to_string())
- .unwrap_or_default()
- })
- .collect()
-}
-
// Builds a single match's `$matches` map with both named and numbered keys
// (the named key precedes its number). Every group is present; a
// non-participating one is None.