Skip to content

Commit 399b888

Browse files
committed
RegexArrayShapeMatcher - trailling groups are not optional when PREG_UNMATCHED_AS_NULL
1 parent 10ae71d commit 399b888

File tree

4 files changed

+45
-1
lines changed

4 files changed

+45
-1
lines changed

src/Php/PhpVersion.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,13 @@ public function supportsNeverReturnTypeInArrowFunction(): bool
309309
return $this->versionId >= 80200;
310310
}
311311

312+
public function supportsPregUnmatchedAsNull(): bool
313+
{
314+
// while PREG_UNMATCHED_AS_NULL is defined in php-src since 7.2.x it starts working like described in the report with 7.4.x
315+
// https://3v4l.org/v3HE4
316+
return $this->versionId >= 70400;
317+
}
318+
312319
public function hasDateTimeExceptions(): bool
313320
{
314321
return $this->versionId >= 80300;

src/Type/Php/RegexArrayShapeMatcher.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Hoa\Compiler\Llk\TreeNode;
88
use Hoa\Exception\Exception;
99
use Hoa\File\Read;
10+
use PHPStan\Php\PhpVersion;
1011
use PHPStan\TrinaryLogic;
1112
use PHPStan\Type\Constant\ConstantArrayType;
1213
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
@@ -33,6 +34,12 @@ final class RegexArrayShapeMatcher
3334

3435
private static ?Parser $parser = null;
3536

37+
public function __construct(
38+
private PhpVersion $phpVersion,
39+
)
40+
{
41+
}
42+
3643
public function matchType(Type $patternType, ?Type $flagsType, TrinaryLogic $wasMatched): ?Type
3744
{
3845
if ($wasMatched->no()) {
@@ -111,6 +118,7 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched
111118
$valueType,
112119
$wasMatched,
113120
$trailingOptionals,
121+
$flags ?? 0,
114122
);
115123

116124
return TypeCombinator::union(
@@ -145,6 +153,7 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched
145153
$valueType,
146154
$wasMatched,
147155
$trailingOptionals,
156+
$flags ?? 0,
148157
);
149158

150159
$combiTypes[] = $combiType;
@@ -167,6 +176,7 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched
167176
$valueType,
168177
$wasMatched,
169178
$trailingOptionals,
179+
$flags ?? 0,
170180
);
171181
}
172182

@@ -228,6 +238,7 @@ private function buildArrayType(
228238
Type $valueType,
229239
TrinaryLogic $wasMatched,
230240
int $trailingOptionals,
241+
int $flags,
231242
): Type
232243
{
233244
$builder = ConstantArrayTypeBuilder::createEmpty();
@@ -247,6 +258,8 @@ private function buildArrayType(
247258
} else {
248259
if ($i < $countGroups - $trailingOptionals) {
249260
$optional = false;
261+
} elseif (($flags & PREG_UNMATCHED_AS_NULL) !== 0 && $this->phpVersion->supportsPregUnmatchedAsNull()) {
262+
$optional = false;
250263
} else {
251264
$optional = $captureGroup->isOptional();
252265
}
@@ -285,7 +298,7 @@ private function getValueType(int $flags): Type
285298
{
286299
$valueType = new StringType();
287300
$offsetType = IntegerRangeType::fromInterval(0, null);
288-
if (($flags & PREG_UNMATCHED_AS_NULL) !== 0) {
301+
if (($flags & PREG_UNMATCHED_AS_NULL) !== 0 && $this->phpVersion->supportsPregUnmatchedAsNull()) {
289302
$valueType = TypeCombinator::addNull($valueType);
290303
// unmatched groups return -1 as offset
291304
$offsetType = IntegerRangeType::fromInterval(-1, null);
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php // lint < 7.4
2+
3+
namespace Bug11311;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
function doFoo() {
8+
if (1 === preg_match('/(?<major>\d+)\.(?<minor>\d+)(?:\.(?<patch>\d+))?/', '12.5', $matches, PREG_UNMATCHED_AS_NULL)) {
9+
// on PHP < 7.4, unmatched-as-null does not return null values; see https://3v4l.org/v3HE4
10+
assertType('array{0: string, major: string, 1: string, minor: string, 2: string, patch?: string, 3?: string}', $matches);
11+
}
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php // lint >= 7.4
2+
3+
namespace Bug11311;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
function doFoo() {
8+
if (1 === preg_match('/(?<major>\d+)\.(?<minor>\d+)(?:\.(?<patch>\d+))?/', '12.5', $matches, PREG_UNMATCHED_AS_NULL)) {
9+
10+
assertType('array{0: string, major: string|null, 1: string|null, minor: string|null, 2: string|null, patch: string|null, 3: string|null}', $matches);
11+
}
12+
}

0 commit comments

Comments
 (0)