Skip to content

Commit 171be9a

Browse files
committed
Groups in conditional regex subpatterns are optional and conditional subpatterns might match an empty string
Amp-Thread-ID: https://ampcode.com/threads/T-01a177b5-960f-71d2-a3e2-fa7649f39d2c
1 parent 2939435 commit 171be9a

2 files changed

Lines changed: 31 additions & 3 deletions

File tree

‎src/Type/Regex/RegexGroupParser.php‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ final class RegexGroupParser
5353

5454
private const ASSERTIONS = ['#lookahead', '#lookbehind', '#negativelookahead', '#negativelookbehind'];
5555

56+
private const CONDITIONS = ['#namedcondition', '#absolutecondition', '#relativecondition', '#assertioncondition'];
57+
5658
private static ?RegexParser $parser = null;
5759

5860
/** @var array<string, ?RegexNode> */
@@ -291,8 +293,12 @@ private function walkRegexAst(
291293
true,
292294
);
293295
$parentGroup = $group;
294-
} elseif (in_array($ast->getId(), self::NEGATIVE_ASSERTIONS, true)) {
295-
// capturing groups in a negative assertion are never set when the whole pattern matches
296+
} elseif (
297+
in_array($ast->getId(), self::NEGATIVE_ASSERTIONS, true)
298+
|| in_array($ast->getId(), self::CONDITIONS, true)
299+
) {
300+
// capturing groups in a negative assertion are never set when the whole pattern matches,
301+
// capturing groups in a conditional subpattern are set only when its condition says so
296302
$group = new RegexNonCapturingGroup(
297303
$alternation,
298304
true,
@@ -515,6 +521,11 @@ private function walkGroupAst(
515521
return $walkResult;
516522
}
517523

524+
if (in_array($ast->getId(), self::CONDITIONS, true)) {
525+
// which branch of a conditional subpattern matches is not known, it might match an empty string
526+
return $walkResult->onlyLiterals(null)->numeric(TrinaryLogic::createNo());
527+
}
528+
518529
$children = $ast->getChildren();
519530
$quantifiedLiterals = null;
520531

@@ -785,7 +796,7 @@ private function repeatLiterals(array $literals, int $min, int $max): ?array
785796

786797
private function isMaybeEmptyNode(RegexNode $node, string $patternModifiers, bool &$isNonFalsy): bool
787798
{
788-
if (in_array($node->getId(), self::ASSERTIONS, true)) {
799+
if (in_array($node->getId(), self::ASSERTIONS, true) || in_array($node->getId(), self::CONDITIONS, true)) {
789800
return true;
790801
}
791802

‎tests/PHPStan/Analyser/nsrt/preg_match_shapes.php‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1175,3 +1175,20 @@ function testBranchResetNotInOnlyTopLevelAlternation(string $s): void {
11751175
assertType('array<string>', $matches);
11761176
}
11771177
}
1178+
1179+
function testConditionalSubpatterns(string $s): void {
1180+
// from PCRE2 testdata/testinput1
1181+
if (preg_match('/^(\()?blah(?(1)(\)))$/', $s, $matches)) {
1182+
assertType("list{0: non-falsy-string, 1?: ''|'(', 2?: ')'}", $matches);
1183+
}
1184+
1185+
// from PCRE2 testdata/testinput2
1186+
if (preg_match('/((?(-1)a))/', $s, $matches)) {
1187+
assertType('array{string, string}', $matches);
1188+
}
1189+
1190+
// from PCRE2 testdata/testinput1
1191+
if (preg_match('/(?(DEFINE)(?<foo>bar))(?<![-a-z0-9])word/', $s, $matches)) {
1192+
assertType("array{0: non-falsy-string, foo: 'bar', 1: 'bar'}|array{non-falsy-string}", $matches);
1193+
}
1194+
}

0 commit comments

Comments
 (0)