Skip to content

Commit 6ffd0dd

Browse files
authored
RegexArrayShapeMatcher - Fix optional groups with PREG_UNMATCHED_AS_NULL
1 parent ebce317 commit 6ffd0dd

File tree

4 files changed

+38
-4
lines changed

4 files changed

+38
-4
lines changed

src/Type/Php/RegexArrayShapeMatcher.php

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,13 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched
121121
$flags ?? 0,
122122
);
123123

124-
return TypeCombinator::union(
125-
new ConstantArrayType([new ConstantIntegerType(0)], [new StringType()], [0], [], true),
126-
$combiType,
127-
);
124+
if (!$this->containsUnmatchedAsNull($flags ?? 0)) {
125+
$combiType = TypeCombinator::union(
126+
new ConstantArrayType([new ConstantIntegerType(0)], [new StringType()], [0], [], true),
127+
$combiType,
128+
);
129+
}
130+
return $combiType;
128131
} elseif (
129132
$wasMatched->yes()
130133
&& $onlyTopLevelAlternationId !== null

tests/PHPStan/Analyser/nsrt/bug-11311-php72.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,12 @@ function doUnmatchedAsNull(string $s): void {
1919
assertType('array{}|array{0: string, 1?: string, 2?: string, 3?: string}', $matches);
2020
}
2121

22+
// see https://3v4l.org/VeDob#veol
23+
function unmatchedAsNullWithOptionalGroup(string $s): void {
24+
if (preg_match('/Price: (£|€)?\d+/', $s, $matches, PREG_UNMATCHED_AS_NULL)) {
25+
assertType('array{0: string, 1?: string}', $matches);
26+
} else {
27+
assertType('array{}', $matches);
28+
}
29+
assertType('array{}|array{0: string, 1?: string}', $matches);
30+
}

tests/PHPStan/Analyser/nsrt/bug-11311.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,15 @@ function doUnmatchedAsNull(string $s): void {
1717
}
1818
assertType('array{}|array{string, string|null, string|null, string|null}', $matches);
1919
}
20+
21+
// see https://3v4l.org/VeDob
22+
function unmatchedAsNullWithOptionalGroup(string $s): void {
23+
if (preg_match('/Price: (£|€)?\d+/', $s, $matches, PREG_UNMATCHED_AS_NULL)) {
24+
// with PREG_UNMATCHED_AS_NULL the offset 1 will always exist. It is correct that it's nullable because it's optional though
25+
assertType('array{string, string|null}', $matches);
26+
} else {
27+
assertType('array{}', $matches);
28+
}
29+
assertType('array{}|array{string, string|null}', $matches);
30+
}
31+

tests/PHPStan/Analyser/nsrt/preg_match_shapes.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,3 +383,13 @@ function bug11323b(string $s): void
383383
}
384384
assertType('array{}|array{0: string, currency: string, 1: string}', $matches);
385385
}
386+
387+
function unmatchedAsNullWithMandatoryGroup(string $s): void {
388+
if (preg_match('/Price: (?<currency>£|€)\d+/', $s, $matches, PREG_UNMATCHED_AS_NULL)) {
389+
assertType('array{0: string, currency: string, 1: string}', $matches);
390+
} else {
391+
assertType('array{}', $matches);
392+
}
393+
assertType('array{}|array{0: string, currency: string, 1: string}', $matches);
394+
}
395+

0 commit comments

Comments
 (0)