diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php index 506f99cabb..6f7bfcb504 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -286,12 +286,13 @@ private function buildArrayType( $countGroups = count($captureGroups); $i = 0; foreach ($captureGroups as $captureGroup) { + $isTrailingOptional = $i >= $countGroups - $trailingOptionals; $groupValueType = $this->getValueType($captureGroup->getType(), $flags); if (!$wasMatched->yes()) { $optional = true; } else { - if ($i < $countGroups - $trailingOptionals) { + if (!$isTrailingOptional) { $optional = false; if ($this->containsUnmatchedAsNull($flags) && !$captureGroup->isOptional()) { $groupValueType = TypeCombinator::removeNull($groupValueType); @@ -303,7 +304,7 @@ private function buildArrayType( } } - if (!$optional && $captureGroup->isOptional() && !$this->containsUnmatchedAsNull($flags)) { + if (!$isTrailingOptional && $captureGroup->isOptional() && !$this->containsUnmatchedAsNull($flags)) { $groupValueType = TypeCombinator::union($groupValueType, new ConstantStringType('')); } diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php index c3211a9d75..5fb5f3a3df 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php @@ -525,3 +525,18 @@ function (string $s): void { assertType("array{0?: string, 1?: ''|numeric-string}", $matches); }; +class Bug11376 +{ + public function test(string $str): void + { + preg_match('~^(?:(\w+)::)?(\w+)$~', $str, $matches); + assertType('array{0?: string, 1?: string, 2?: non-empty-string}', $matches); + } + + public function test2(string $str): void + { + if (preg_match('~^(?:(\w+)::)?(\w+)$~', $str, $matches) === 1) { + assertType('array{string, string, non-empty-string}', $matches); + } + } +}