Skip to content

Commit 5a728f9

Browse files
authored
RegexArrayShapeMatcher - optional non-last groups can be empty-string
1 parent b7fe990 commit 5a728f9

File tree

4 files changed

+41
-13
lines changed

4 files changed

+41
-13
lines changed

src/Type/Php/RegexArrayShapeMatcher.php

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,8 @@ private function buildArrayType(
297297
$i = 0;
298298
foreach ($captureGroups as $captureGroup) {
299299
$isTrailingOptional = $i >= $countGroups - $trailingOptionals;
300-
$groupValueType = $this->createGroupValueType($captureGroup, $wasMatched, $flags, $isTrailingOptional, $matchesAll);
300+
$isLastGroup = $i === $countGroups - 1;
301+
$groupValueType = $this->createGroupValueType($captureGroup, $wasMatched, $flags, $isTrailingOptional, $isLastGroup, $matchesAll);
301302
$optional = $this->isGroupOptional($captureGroup, $wasMatched, $flags, $isTrailingOptional, $matchesAll);
302303

303304
if ($captureGroup->isNamed()) {
@@ -390,11 +391,11 @@ private function isGroupOptional(RegexCapturingGroup $captureGroup, TrinaryLogic
390391
return $optional;
391392
}
392393

393-
private function createGroupValueType(RegexCapturingGroup $captureGroup, TrinaryLogic $wasMatched, int $flags, bool $isTrailingOptional, bool $matchesAll): Type
394+
private function createGroupValueType(RegexCapturingGroup $captureGroup, TrinaryLogic $wasMatched, int $flags, bool $isTrailingOptional, bool $isLastGroup, bool $matchesAll): Type
394395
{
395-
$groupValueType = $this->getValueType($captureGroup->getType(), $flags, $matchesAll);
396-
397396
if ($matchesAll) {
397+
$groupValueType = $this->getValueType($captureGroup->getType(), $flags, $matchesAll);
398+
398399
if (!$isTrailingOptional && $this->containsUnmatchedAsNull($flags, $matchesAll) && !$captureGroup->isOptional()) {
399400
$groupValueType = TypeCombinator::removeNull($groupValueType);
400401
}
@@ -411,16 +412,22 @@ private function createGroupValueType(RegexCapturingGroup $captureGroup, Trinary
411412
return $groupValueType;
412413
}
413414

415+
if (!$isLastGroup && !$this->containsUnmatchedAsNull($flags, $matchesAll) && $captureGroup->isOptional()) {
416+
$groupValueType = $this->getValueType(
417+
TypeCombinator::union($captureGroup->getType(), new ConstantStringType('')),
418+
$flags,
419+
$matchesAll,
420+
);
421+
} else {
422+
$groupValueType = $this->getValueType($captureGroup->getType(), $flags, $matchesAll);
423+
}
424+
414425
if ($wasMatched->yes()) {
415426
if (!$isTrailingOptional && $this->containsUnmatchedAsNull($flags, $matchesAll) && !$captureGroup->isOptional()) {
416427
$groupValueType = TypeCombinator::removeNull($groupValueType);
417428
}
418429
}
419430

420-
if (!$isTrailingOptional && !$this->containsUnmatchedAsNull($flags, $matchesAll) && $captureGroup->isOptional()) {
421-
$groupValueType = TypeCombinator::union($groupValueType, new ConstantStringType(''));
422-
}
423-
424431
return $groupValueType;
425432
}
426433

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ function doFoo(string $s) {
1414

1515
function doUnmatchedAsNull(string $s): void {
1616
if (preg_match('/(foo)?(bar)?(baz)?/', $s, $matches, PREG_UNMATCHED_AS_NULL)) {
17-
assertType("array{0: string, 1?: 'foo', 2?: 'bar', 3?: 'baz'}", $matches);
17+
assertType("array{0: string, 1?: ''|'foo', 2?: ''|'bar', 3?: 'baz'}", $matches);
1818
}
19-
assertType("array{}|array{0: string, 1?: 'foo', 2?: 'bar', 3?: 'baz'}", $matches);
19+
assertType("array{}|array{0: string, 1?: ''|'foo', 2?: ''|'bar', 3?: 'baz'}", $matches);
2020
}
2121

2222
// see https://3v4l.org/VeDob#veol

tests/PHPStan/Analyser/nsrt/preg_match_shapes.php

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ function (string $size): void {
300300
if (preg_match('~^a\.(b)?(c)?d~', $size, $matches) !== 1) {
301301
throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size));
302302
}
303-
assertType("array{0: string, 1?: 'b', 2?: 'c'}", $matches);
303+
assertType("array{0: string, 1?: ''|'b', 2?: 'c'}", $matches);
304304
};
305305

306306
function (string $size): void {
@@ -567,7 +567,7 @@ function (string $s): void {
567567
}
568568

569569
if (preg_match($p, $s, $matches)) {
570-
assertType("array{0: string, 1: non-empty-string, 2?: numeric-string, 3?: 'x'}", $matches);
570+
assertType("array{0: string, 1: non-empty-string, 2?: ''|numeric-string, 3?: 'x'}", $matches);
571571
}
572572
};
573573

@@ -662,3 +662,24 @@ function (string $value): void
662662
assertType("array{0: array{string, int<0, max>}, 1?: array{non-empty-string, int<0, max>}, 2?: array{non-empty-string, int<0, max>}}", $matches);
663663
}
664664
};
665+
666+
class Bug11479
667+
{
668+
static public function sayHello(string $source): void
669+
{
670+
$pattern = "~^(?P<dateFrom>\d)?\-?(?P<dateTo>\d)?$~";
671+
672+
preg_match($pattern, $source, $matches);
673+
674+
// for $source = "-1" in $matches is
675+
// array (
676+
// 0 => '-1',
677+
// 'dateFrom' => '',
678+
// 1 => '',
679+
// 'dateTo' => '1',
680+
// 2 => '1',
681+
//)
682+
683+
assertType("array{0?: string, dateFrom?: ''|numeric-string, 1?: ''|numeric-string, dateTo?: numeric-string, 2?: numeric-string}", $matches);
684+
}
685+
}

tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ function (string $s): void {
2222
preg_replace_callback(
2323
'/(foo)?(bar)?(baz)?/',
2424
function ($matches) {
25-
assertType("array{0: array{string, int<0, max>}, 1?: array{'foo', int<0, max>}, 2?: array{'bar', int<0, max>}, 3?: array{'baz', int<0, max>}}", $matches);
25+
assertType("array{0: array{string, int<0, max>}, 1?: array{''|'foo', int<0, max>}, 2?: array{''|'bar', int<0, max>}, 3?: array{'baz', int<0, max>}}", $matches);
2626
return '';
2727
},
2828
$s,

0 commit comments

Comments
 (0)