diff --git a/src/Type/Php/PregMatchParameterOutTypeExtension.php b/src/Type/Php/PregMatchParameterOutTypeExtension.php index b8bd415b45..58135a1dcb 100644 --- a/src/Type/Php/PregMatchParameterOutTypeExtension.php +++ b/src/Type/Php/PregMatchParameterOutTypeExtension.php @@ -8,8 +8,10 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParameterReflection; use PHPStan\TrinaryLogic; +use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\FunctionParameterOutTypeExtension; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use function in_array; use function strtolower; @@ -49,8 +51,17 @@ public function getParameterOutTypeFromFunctionCall(FunctionReflection $function } if ($functionReflection->getName() === 'preg_match') { - return $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createMaybe(), $scope); + $matchedType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createYes(), $scope); + if ($matchedType === null) { + return null; + } + + return TypeCombinator::union( + ConstantArrayTypeBuilder::createEmpty()->getArray(), + $matchedType, + ); } + return $this->regexShapeMatcher->matchAllExpr($patternArg->value, $flagsType, TrinaryLogic::createMaybe(), $scope); } diff --git a/tests/PHPStan/Analyser/data/param-out.php b/tests/PHPStan/Analyser/data/param-out.php index e34e1c9082..3eed881843 100644 --- a/tests/PHPStan/Analyser/data/param-out.php +++ b/tests/PHPStan/Analyser/data/param-out.php @@ -494,10 +494,10 @@ function fooMatch(string $input): void { assertType('list', $matches); preg_match('/@[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}(?!\w)/', $input, $matches, PREG_UNMATCHED_AS_NULL); - assertType("array{0?: string}", $matches); + assertType("array{}|array{non-falsy-string}", $matches); } function testMatch() { preg_match('#.*#', 'foo', $matches); - assertType('array{0?: string}', $matches); + assertType('array{}|array{string}', $matches); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-11293.php b/tests/PHPStan/Analyser/nsrt/bug-11293.php index caf95180ab..49f8d008a6 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11293.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11293.php @@ -30,7 +30,7 @@ public function sayHello3(string $s): void public function sayHello4(string $s): void { if (preg_match('/data-(\d{6})\.json$/', $s, $matches) <= 0) { - assertType('list{0?: string, 1?: non-falsy-string&numeric-string}', $matches); + assertType('array{}|array{non-falsy-string, non-falsy-string&numeric-string}', $matches); return; } @@ -41,7 +41,7 @@ public function sayHello4(string $s): void public function sayHello5(string $s): void { if (preg_match('/data-(\d{6})\.json$/', $s, $matches) < 1) { - assertType('list{0?: string, 1?: non-falsy-string&numeric-string}', $matches); + assertType('array{}|array{non-falsy-string, non-falsy-string&numeric-string}', $matches); return; } @@ -52,7 +52,7 @@ public function sayHello5(string $s): void public function sayHello6(string $s): void { if (1 > preg_match('/data-(\d{6})\.json$/', $s, $matches)) { - assertType('list{0?: string, 1?: non-falsy-string&numeric-string}', $matches); + assertType('array{}|array{non-falsy-string, non-falsy-string&numeric-string}', $matches); return; } diff --git a/tests/PHPStan/Analyser/nsrt/bug-11311.php b/tests/PHPStan/Analyser/nsrt/bug-11311.php index 96b810431d..79fbf3aca4 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11311.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11311.php @@ -191,12 +191,12 @@ function (string $s): void { function (string $s): void { preg_match('/%a(\d*)/', $s, $matches, PREG_UNMATCHED_AS_NULL); - assertType("list{0?: string, 1?: ''|numeric-string|null}", $matches); // could be array{0?: string, 1?: ''|numeric-string} + assertType("array{}|array{non-falsy-string, ''|numeric-string}", $matches); }; function (string $s): void { preg_match('/%a(\d*)?/', $s, $matches, PREG_UNMATCHED_AS_NULL); - assertType("list{0?: string, 1?: ''|numeric-string|null}", $matches); // could be array{0?: string, 1?: ''|numeric-string} + assertType("array{}|array{non-falsy-string, ''|numeric-string|null}", $matches); }; function (string $s): void { @@ -222,5 +222,5 @@ function (string $s): void { function (string $s): void { preg_match('~a|(\d)|(\s)~', $s, $matches, PREG_UNMATCHED_AS_NULL); - assertType("list{0?: string, 1?: numeric-string|null, 2?: non-empty-string|null}", $matches); + assertType("array{}|array{non-empty-string, numeric-string|null, non-empty-string|null}", $matches); }; diff --git a/tests/PHPStan/Analyser/nsrt/bug-11580.php b/tests/PHPStan/Analyser/nsrt/bug-11580.php index 039a1895f5..2e4f8200db 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11580.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11580.php @@ -26,7 +26,7 @@ public function bad2(string $in): void public function bad3(string $in): void { $result = preg_match('~^/xxx/([\w\-]+)/?([\w\-]+)?/?$~', $in, $matches); - assertType('list{0?: string, 1?: non-empty-string, 2?: non-empty-string}', $matches); + assertType('array{}|array{0: non-falsy-string, 1: non-empty-string, 2?: non-empty-string}', $matches); if ($result) { assertType('array{0: non-falsy-string, 1: non-empty-string, 2?: non-empty-string}', $matches); } diff --git a/tests/PHPStan/Analyser/nsrt/count-type.php b/tests/PHPStan/Analyser/nsrt/count-type.php index 1deb2e8695..8ebe0c445e 100644 --- a/tests/PHPStan/Analyser/nsrt/count-type.php +++ b/tests/PHPStan/Analyser/nsrt/count-type.php @@ -94,7 +94,7 @@ public function constantArrayWhichCanBecomeList(string $h): void return; } - assertType('array{string, non-empty-string}', $matches); + assertType('array{non-falsy-string, non-empty-string}', $matches); } } diff --git a/tests/PHPStan/Analyser/nsrt/if.php b/tests/PHPStan/Analyser/nsrt/if.php index de9ebc3cb9..0cb3980f10 100644 --- a/tests/PHPStan/Analyser/nsrt/if.php +++ b/tests/PHPStan/Analyser/nsrt/if.php @@ -392,7 +392,7 @@ function () { assertVariableCertainty(TrinaryLogic::createYes(), $anotherF); assertType('int<1, max>', $anotherF); assertVariableCertainty(TrinaryLogic::createYes(), $matches); - assertType('array{0?: string}', $matches); + assertType('array{}|array{string}', $matches); assertVariableCertainty(TrinaryLogic::createYes(), $anotherArray); assertType('array{test: array{\'another\'}}', $anotherArray); assertVariableCertainty(TrinaryLogic::createYes(), $ifVar); @@ -405,7 +405,7 @@ function () { assertType('1|2|3', $ifNotNestedVar); assertVariableCertainty(TrinaryLogic::createNo(), $variableOnlyInEarlyTerminatingElse); assertVariableCertainty(TrinaryLogic::createMaybe(), $matches2); - assertType('array{0?: string}', $matches2); + assertType('array{}|array{string}', $matches2); assertVariableCertainty(TrinaryLogic::createYes(), $inTry); assertType('1', $inTry); assertVariableCertainty(TrinaryLogic::createYes(), $matches3); diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php index bf26bd5506..543a1b84ac 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php @@ -525,7 +525,7 @@ function bug11323(string $s): void { function (string $s): void { preg_match('/%a(\d*)/', $s, $matches); - assertType("list{0?: string, 1?: ''|numeric-string}", $matches); + assertType("array{}|array{non-falsy-string, ''|numeric-string}", $matches); }; class Bug11376 @@ -533,7 +533,7 @@ class Bug11376 public function test(string $str): void { preg_match('~^(?:(\w+)::)?(\w+)$~', $str, $matches); - assertType('list{0?: string, 1?: string, 2?: non-empty-string}', $matches); + assertType("array{}|array{non-empty-string, string, non-empty-string}", $matches); } public function test2(string $str): void @@ -706,7 +706,7 @@ static public function sayHello(string $source): void // 2 => '1', //) - assertType("array{0?: string, dateFrom?: ''|numeric-string, 1?: ''|numeric-string, dateTo?: numeric-string, 2?: numeric-string}", $matches); + assertType("array{}|array{0: string, dateFrom?: ''|numeric-string, 1?: ''|numeric-string, dateTo?: numeric-string, 2?: numeric-string}", $matches); } } @@ -730,7 +730,7 @@ function (string $s): void { function (string $s): void { preg_match('~a|(\d)|(\s)~', $s, $matches); - assertType("list{0?: string, 1?: '', 2?: non-empty-string}|list{0?: string, 1?: numeric-string}", $matches); + assertType("array{}|array{0: non-empty-string, 1?: numeric-string}|array{non-empty-string, '', non-empty-string}", $matches); }; function bug11490 (string $expression): void { @@ -1013,7 +1013,7 @@ function bug12749f(string $str): void function bug12397(string $string): void { $m = preg_match('#\b([A-Z]{2,})-(\d+)#', $string, $match); - assertType('list{0?: string, 1?: non-falsy-string, 2?: numeric-string}', $match); + assertType("array{}|array{non-falsy-string, non-falsy-string, numeric-string}", $match); } function bug12792(string $string): void { diff --git a/tests/PHPStan/Rules/Arrays/ArrayDestructuringRuleTest.php b/tests/PHPStan/Rules/Arrays/ArrayDestructuringRuleTest.php index b105abcd36..f0b14c9667 100644 --- a/tests/PHPStan/Rules/Arrays/ArrayDestructuringRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/ArrayDestructuringRuleTest.php @@ -49,6 +49,11 @@ public function testRule(): void ]); } + public function testBug14270(): void + { + $this->analyse([__DIR__ . '/data/bug-14270.php'], []); + } + #[RequiresPhp('>= 8.0')] public function testRuleWithNullsafeVariant(): void { diff --git a/tests/PHPStan/Rules/Arrays/data/bug-14270.php b/tests/PHPStan/Rules/Arrays/data/bug-14270.php new file mode 100644 index 0000000000..d9781c936d --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-14270.php @@ -0,0 +1,14 @@ +analyse([__DIR__ . '/../../Analyser/nsrt/bug-12973.php'], []); } + public function testBug12397(): void + { + $this->checkNullables = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-12397.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Functions/data/bug-12397.php b/tests/PHPStan/Rules/Functions/data/bug-12397.php new file mode 100644 index 0000000000..430142f07d --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-12397.php @@ -0,0 +1,11 @@ + + */ +function matchStuff(string $string) : array { + $m = preg_match('#\b([A-Z]{2,})-(\d+)#', $string, $match); + return $match; +}