diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php index 5814096332..23ee4b5a36 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -394,15 +394,19 @@ private function isGroupOptional(RegexCapturingGroup $captureGroup, TrinaryLogic private function createGroupValueType(RegexCapturingGroup $captureGroup, TrinaryLogic $wasMatched, int $flags, bool $isTrailingOptional, bool $isLastGroup, bool $matchesAll): Type { if ($matchesAll) { - $groupValueType = $this->getValueType($captureGroup->getType(), $flags, $matchesAll); - - if (!$isTrailingOptional && $this->containsUnmatchedAsNull($flags, $matchesAll) && !$captureGroup->isOptional()) { + if (!$this->containsSetOrder($flags) && !$this->containsUnmatchedAsNull($flags, $matchesAll) && $captureGroup->isOptional()) { + $groupValueType = $this->getValueType( + TypeCombinator::union($captureGroup->getType(), new ConstantStringType('')), + $flags, + $matchesAll, + ); $groupValueType = TypeCombinator::removeNull($groupValueType); + } else { + $groupValueType = $this->getValueType($captureGroup->getType(), $flags, $matchesAll); } - if (!$this->containsSetOrder($flags) && !$this->containsUnmatchedAsNull($flags, $matchesAll) && $captureGroup->isOptional()) { + if (!$isTrailingOptional && $this->containsUnmatchedAsNull($flags, $matchesAll) && !$captureGroup->isOptional()) { $groupValueType = TypeCombinator::removeNull($groupValueType); - $groupValueType = TypeCombinator::union($groupValueType, new ConstantStringType('')); } if ($this->containsPatternOrder($flags)) { @@ -471,11 +475,10 @@ private function getValueType(Type $baseType, int $flags, bool $matchesAll): Typ { $valueType = $baseType; - $offsetType = IntegerRangeType::fromInterval(0, null); + // unmatched groups return -1 as offset + $offsetType = IntegerRangeType::fromInterval(-1, null); if ($this->containsUnmatchedAsNull($flags, $matchesAll)) { $valueType = TypeCombinator::addNull($valueType); - // unmatched groups return -1 as offset - $offsetType = IntegerRangeType::fromInterval(-1, null); } if ($this->containsOffsetCapture($flags)) { diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php index 3bfb4213ec..c415fca42f 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php @@ -112,13 +112,13 @@ function (string $size): void { function (string $size): void { if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_SET_ORDER|PREG_OFFSET_CAPTURE)) { - assertType("list}, 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); + assertType("list}, 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); } }; function (string $size): void { if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_PATTERN_ORDER|PREG_OFFSET_CAPTURE)) { - assertType("array{0: list}>, num: list}>, 1: list}>, suffix: list<''|array{'ab', int<0, max>}>, 2: list<''|array{'ab', int<0, max>}>}", $matches); + assertType("array{0: list}>, num: list}>, 1: list}>, suffix: list}>, 2: list}>}", $matches); } }; @@ -142,7 +142,7 @@ public function sayHello(string $content): void return; } - assertType('array{list}>}', $matches); + assertType('array{list}>}', $matches); } public function sayFoo(string $content): void @@ -162,4 +162,16 @@ public function sayBar(string $content): void assertType('array{list}', $matches); } + + function doFoobar(string $s): void { + if (preg_match_all('/(foo)?(bar)?(baz)?/', $s, $matches, PREG_OFFSET_CAPTURE)) { + assertType("array{list}>, list}>, list}>, list}>}", $matches); + } + } + + function doFoobarNull(string $s): void { + if (preg_match_all('/(foo)?(bar)?(baz)?/', $s, $matches, PREG_OFFSET_CAPTURE|PREG_UNMATCHED_AS_NULL)) { + assertType("array{list}>, list}>, list}>, list}>}", $matches); + } + } } diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php index 39bdea3e68..3e633455ab 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php @@ -115,9 +115,9 @@ function doNamedSubpattern(string $s): void { function doOffsetCapture(string $s): void { if (preg_match('/(foo)(bar)(baz)/', $s, $matches, PREG_OFFSET_CAPTURE)) { - assertType("array{array{string, int<0, max>}, array{'foo', int<0, max>}, array{'bar', int<0, max>}, array{'baz', int<0, max>}}", $matches); + assertType("array{array{string, int<-1, max>}, array{'foo', int<-1, max>}, array{'bar', int<-1, max>}, array{'baz', int<-1, max>}}", $matches); } - assertType("array{}|array{array{string, int<0, max>}, array{'foo', int<0, max>}, array{'bar', int<0, max>}, array{'baz', int<0, max>}}", $matches); + assertType("array{}|array{array{string, int<-1, max>}, array{'foo', int<-1, max>}, array{'bar', int<-1, max>}, array{'baz', int<-1, max>}}", $matches); } function doUnknownFlags(string $s, int $flags): void { @@ -652,14 +652,14 @@ function (string $s): void { function (string $value): void { if (preg_match('/^(x)*$/', $value, $matches, PREG_OFFSET_CAPTURE)) { - assertType("array{0: array{string, int<0, max>}, 1?: array{non-empty-string, int<0, max>}}", $matches); + assertType("array{0: array{string, int<-1, max>}, 1?: array{non-empty-string, int<-1, max>}}", $matches); } }; function (string $value): void { if (preg_match('/^(?:(x)|(y))*$/', $value, $matches, PREG_OFFSET_CAPTURE)) { - 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); + 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); } }; diff --git a/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes.php index 9c44d355df..36b6a51f1f 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes.php @@ -22,7 +22,7 @@ function (string $s): void { preg_replace_callback( '/(foo)?(bar)?(baz)?/', function ($matches) { - 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); + 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); return ''; }, $s,