Skip to content

Commit 427a319

Browse files
authored
RegexArrayShapeMatcher - fix capturing item-array-shapes for preg_match_all
1 parent 82d19c6 commit 427a319

File tree

4 files changed

+31
-16
lines changed

4 files changed

+31
-16
lines changed

src/Type/Php/RegexArrayShapeMatcher.php

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -394,15 +394,19 @@ private function isGroupOptional(RegexCapturingGroup $captureGroup, TrinaryLogic
394394
private function createGroupValueType(RegexCapturingGroup $captureGroup, TrinaryLogic $wasMatched, int $flags, bool $isTrailingOptional, bool $isLastGroup, bool $matchesAll): Type
395395
{
396396
if ($matchesAll) {
397-
$groupValueType = $this->getValueType($captureGroup->getType(), $flags, $matchesAll);
398-
399-
if (!$isTrailingOptional && $this->containsUnmatchedAsNull($flags, $matchesAll) && !$captureGroup->isOptional()) {
397+
if (!$this->containsSetOrder($flags) && !$this->containsUnmatchedAsNull($flags, $matchesAll) && $captureGroup->isOptional()) {
398+
$groupValueType = $this->getValueType(
399+
TypeCombinator::union($captureGroup->getType(), new ConstantStringType('')),
400+
$flags,
401+
$matchesAll,
402+
);
400403
$groupValueType = TypeCombinator::removeNull($groupValueType);
404+
} else {
405+
$groupValueType = $this->getValueType($captureGroup->getType(), $flags, $matchesAll);
401406
}
402407

403-
if (!$this->containsSetOrder($flags) && !$this->containsUnmatchedAsNull($flags, $matchesAll) && $captureGroup->isOptional()) {
408+
if (!$isTrailingOptional && $this->containsUnmatchedAsNull($flags, $matchesAll) && !$captureGroup->isOptional()) {
404409
$groupValueType = TypeCombinator::removeNull($groupValueType);
405-
$groupValueType = TypeCombinator::union($groupValueType, new ConstantStringType(''));
406410
}
407411

408412
if ($this->containsPatternOrder($flags)) {
@@ -471,11 +475,10 @@ private function getValueType(Type $baseType, int $flags, bool $matchesAll): Typ
471475
{
472476
$valueType = $baseType;
473477

474-
$offsetType = IntegerRangeType::fromInterval(0, null);
478+
// unmatched groups return -1 as offset
479+
$offsetType = IntegerRangeType::fromInterval(-1, null);
475480
if ($this->containsUnmatchedAsNull($flags, $matchesAll)) {
476481
$valueType = TypeCombinator::addNull($valueType);
477-
// unmatched groups return -1 as offset
478-
$offsetType = IntegerRangeType::fromInterval(-1, null);
479482
}
480483

481484
if ($this->containsOffsetCapture($flags)) {

tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,13 +112,13 @@ function (string $size): void {
112112

113113
function (string $size): void {
114114
if (preg_match_all('/ab(?P<num>\d+)(?P<suffix>ab)?/', $size, $matches, PREG_SET_ORDER|PREG_OFFSET_CAPTURE)) {
115-
assertType("list<array{0: array{string, int<0, max>}, num: array{numeric-string, int<0, max>}, 1: array{numeric-string, int<0, max>}, suffix?: array{'ab', int<0, max>}, 2?: array{'ab', int<0, max>}}>", $matches);
115+
assertType("list<array{0: array{string, int<-1, max>}, num: array{numeric-string, int<-1, max>}, 1: array{numeric-string, int<-1, max>}, suffix?: array{'ab', int<-1, max>}, 2?: array{'ab', int<-1, max>}}>", $matches);
116116
}
117117
};
118118

119119
function (string $size): void {
120120
if (preg_match_all('/ab(?P<num>\d+)(?P<suffix>ab)?/', $size, $matches, PREG_PATTERN_ORDER|PREG_OFFSET_CAPTURE)) {
121-
assertType("array{0: list<array{string, int<0, max>}>, num: list<array{numeric-string, int<0, max>}>, 1: list<array{numeric-string, int<0, max>}>, suffix: list<''|array{'ab', int<0, max>}>, 2: list<''|array{'ab', int<0, max>}>}", $matches);
121+
assertType("array{0: list<array{string, int<-1, max>}>, num: list<array{numeric-string, int<-1, max>}>, 1: list<array{numeric-string, int<-1, max>}>, suffix: list<array{''|'ab', int<-1, max>}>, 2: list<array{''|'ab', int<-1, max>}>}", $matches);
122122
}
123123
};
124124

@@ -142,7 +142,7 @@ public function sayHello(string $content): void
142142
return;
143143
}
144144

145-
assertType('array{list<array{string, int<0, max>}>}', $matches);
145+
assertType('array{list<array{string, int<-1, max>}>}', $matches);
146146
}
147147

148148
public function sayFoo(string $content): void
@@ -162,4 +162,16 @@ public function sayBar(string $content): void
162162

163163
assertType('array{list<string>}', $matches);
164164
}
165+
166+
function doFoobar(string $s): void {
167+
if (preg_match_all('/(foo)?(bar)?(baz)?/', $s, $matches, PREG_OFFSET_CAPTURE)) {
168+
assertType("array{list<array{string, int<-1, max>}>, list<array{''|'foo', int<-1, max>}>, list<array{''|'bar', int<-1, max>}>, list<array{''|'baz', int<-1, max>}>}", $matches);
169+
}
170+
}
171+
172+
function doFoobarNull(string $s): void {
173+
if (preg_match_all('/(foo)?(bar)?(baz)?/', $s, $matches, PREG_OFFSET_CAPTURE|PREG_UNMATCHED_AS_NULL)) {
174+
assertType("array{list<array{string|null, int<-1, max>}>, list<array{'foo'|null, int<-1, max>}>, list<array{'bar'|null, int<-1, max>}>, list<array{'baz'|null, int<-1, max>}>}", $matches);
175+
}
176+
}
165177
}

tests/PHPStan/Analyser/nsrt/preg_match_shapes.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,9 @@ function doNamedSubpattern(string $s): void {
115115

116116
function doOffsetCapture(string $s): void {
117117
if (preg_match('/(foo)(bar)(baz)/', $s, $matches, PREG_OFFSET_CAPTURE)) {
118-
assertType("array{array{string, int<0, max>}, array{'foo', int<0, max>}, array{'bar', int<0, max>}, array{'baz', int<0, max>}}", $matches);
118+
assertType("array{array{string, int<-1, max>}, array{'foo', int<-1, max>}, array{'bar', int<-1, max>}, array{'baz', int<-1, max>}}", $matches);
119119
}
120-
assertType("array{}|array{array{string, int<0, max>}, array{'foo', int<0, max>}, array{'bar', int<0, max>}, array{'baz', int<0, max>}}", $matches);
120+
assertType("array{}|array{array{string, int<-1, max>}, array{'foo', int<-1, max>}, array{'bar', int<-1, max>}, array{'baz', int<-1, max>}}", $matches);
121121
}
122122

123123
function doUnknownFlags(string $s, int $flags): void {
@@ -652,14 +652,14 @@ function (string $s): void {
652652
function (string $value): void
653653
{
654654
if (preg_match('/^(x)*$/', $value, $matches, PREG_OFFSET_CAPTURE)) {
655-
assertType("array{0: array{string, int<0, max>}, 1?: array{non-empty-string, int<0, max>}}", $matches);
655+
assertType("array{0: array{string, int<-1, max>}, 1?: array{non-empty-string, int<-1, max>}}", $matches);
656656
}
657657
};
658658

659659
function (string $value): void
660660
{
661661
if (preg_match('/^(?:(x)|(y))*$/', $value, $matches, PREG_OFFSET_CAPTURE)) {
662-
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);
662+
assertType("array{0: array{string, int<-1, max>}, 1?: array{non-empty-string, int<-1, max>}, 2?: array{non-empty-string, int<-1, max>}}", $matches);
663663
}
664664
};
665665

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<-1, max>}, 1?: array{''|'foo', int<-1, max>}, 2?: array{''|'bar', int<-1, max>}, 3?: array{'baz', int<-1, max>}}", $matches);
2626
return '';
2727
},
2828
$s,

0 commit comments

Comments
 (0)