diff --git a/src/Type/Php/MinMaxFunctionReturnTypeExtension.php b/src/Type/Php/MinMaxFunctionReturnTypeExtension.php index b01fd3f6bb..cd0943619f 100644 --- a/src/Type/Php/MinMaxFunctionReturnTypeExtension.php +++ b/src/Type/Php/MinMaxFunctionReturnTypeExtension.php @@ -6,6 +6,7 @@ use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\Ternary; use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Constant\ConstantArrayType; @@ -28,6 +29,12 @@ class MinMaxFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExte 'max' => '', ]; + public function __construct( + private PhpVersion $phpVersion, + ) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return isset($this->functionNames[$functionReflection->getName()]); @@ -107,12 +114,12 @@ private function processArrayType(string $functionName, Type $argType): Type $resultTypes = []; foreach ($constArrayTypes as $constArrayType) { $isIterable = $constArrayType->isIterableAtLeastOnce(); - if ($isIterable->no()) { + if ($isIterable->no() && !$this->phpVersion->throwsValueErrorForInternalFunctions()) { $resultTypes[] = new ConstantBooleanType(false); continue; } $argumentTypes = []; - if (!$isIterable->yes()) { + if (!$isIterable->yes() && !$this->phpVersion->throwsValueErrorForInternalFunctions()) { $argumentTypes[] = new ConstantBooleanType(false); } @@ -127,12 +134,12 @@ private function processArrayType(string $functionName, Type $argType): Type } $isIterable = $argType->isIterableAtLeastOnce(); - if ($isIterable->no()) { + if ($isIterable->no() && !$this->phpVersion->throwsValueErrorForInternalFunctions()) { return new ConstantBooleanType(false); } $iterableValueType = $argType->getIterableValueType(); $argumentTypes = []; - if (!$isIterable->yes()) { + if (!$isIterable->yes() && !$this->phpVersion->throwsValueErrorForInternalFunctions()) { $argumentTypes[] = new ConstantBooleanType(false); } diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 92c810f60f..43efba4291 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -133,7 +133,13 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/../Reflection/data/staticReturnType.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/minmax-arrays.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/minmax.php'); + if (PHP_VERSION_ID < 80000) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/minmax-arrays.php'); + } + if (PHP_VERSION_ID >= 80000) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/minmax-php8.php'); + } yield from $this->gatherAssertTypes(__DIR__ . '/data/classPhpDocs.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/non-empty-array-key-type.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3133.php'); @@ -509,7 +515,12 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5529.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/sizeof.php'); + if (PHP_VERSION_ID >= 80000) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/sizeof-php8.php'); + } + if (PHP_VERSION_ID < 80000) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/sizeof.php'); + } yield from $this->gatherAssertTypes(__DIR__ . '/data/div-by-zero.php'); diff --git a/tests/PHPStan/Analyser/data/minmax-arrays.php b/tests/PHPStan/Analyser/data/minmax-arrays.php index 1006b71e9b..1a68d50b56 100644 --- a/tests/PHPStan/Analyser/data/minmax-arrays.php +++ b/tests/PHPStan/Analyser/data/minmax-arrays.php @@ -109,47 +109,8 @@ function dummy4(\DateTimeInterface $dateA, ?\DateTimeInterface $dateB): void assertType('DateTimeInterface|false', max(array_filter([$dateB]))); } -function dummy5(int $i, int $j): void -{ - assertType('array{0?: int|int<1, max>, 1?: int|int<1, max>}', array_filter([$i, $j])); - assertType('array{1: true}', array_filter([false, true])); -} - -function dummy6(string $s, string $t): void { - assertType('array{0?: non-falsy-string, 1?: non-falsy-string}', array_filter([$s, $t])); -} - class HelloWorld { - public function setRange(int $range): void - { - if ($range < 0) { - return; - } - assertType('int<0, 100>', min($range, 100)); - assertType('int<0, 100>', min(100, $range)); - } - - public function setRange2(int $range): void - { - if ($range > 100) { - return; - } - assertType('int<0, 100>', max($range, 0)); - assertType('int<0, 100>', max(0, $range)); - } - - public function boundRange(): void - { - /** - * @var int<1, 6> $range - */ - $range = getFoo(); - - assertType('int<1, 4>', min($range, 4)); - assertType('int<4, 6>', max(4, $range)); - } - public function unionType(): void { /** @@ -162,13 +123,5 @@ public function unionType(): void assertType('0|1|2|3|4|5|6|7|8|9|false', max($numbers)); assertType('9', max([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])); - - /** - * @var array{0, 1, 2}|array{4, 5, 6} $numbers2 - */ - $numbers2 = getFoo(); - - assertType('0|4', min($numbers2)); - assertType('2|6', max($numbers2)); } } diff --git a/tests/PHPStan/Analyser/data/minmax-php8.php b/tests/PHPStan/Analyser/data/minmax-php8.php new file mode 100644 index 0000000000..3d738d4c38 --- /dev/null +++ b/tests/PHPStan/Analyser/data/minmax-php8.php @@ -0,0 +1,128 @@ + 0) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('*ERROR*', min($ints)); + assertType('*ERROR*', max($ints)); + } + if (count($ints) >= 1) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('*ERROR*', min($ints)); + assertType('*ERROR*', max($ints)); + } + if (count($ints) >= 2) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('int', min($ints)); + assertType('int', max($ints)); + } + if (count($ints) <= 0) { + assertType('*ERROR*', min($ints)); + assertType('*ERROR*', max($ints)); + } else { + assertType('int', min($ints)); + assertType('int', max($ints)); + } + if (count($ints) < 1) { + assertType('*ERROR*', min($ints)); + assertType('*ERROR*', max($ints)); + } else { + assertType('int', min($ints)); + assertType('int', max($ints)); + } + if (count($ints) < 2) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('int', min($ints)); + assertType('int', max($ints)); + } +} + +/** + * @param int[] $ints + */ +function dummy3(array $ints): void +{ + assertType('int', min($ints)); + assertType('int', max($ints)); +} + + +function dummy4(\DateTimeInterface $dateA, ?\DateTimeInterface $dateB): void +{ + assertType('array{0: DateTimeInterface, 1?: DateTimeInterface}', array_filter([$dateA, $dateB])); + assertType('DateTimeInterface', min(array_filter([$dateA, $dateB]))); + assertType('DateTimeInterface', max(array_filter([$dateA, $dateB]))); + assertType('array{0?: DateTimeInterface}', array_filter([$dateB])); + assertType('DateTimeInterface', min(array_filter([$dateB]))); + assertType('DateTimeInterface', max(array_filter([$dateB]))); +} + + +class HelloWorld +{ + public function unionType(): void + { + /** + * @var array<0|1|2|3|4|5|6|7|8|9> + */ + $numbers = getFoo(); + + assertType('0|1|2|3|4|5|6|7|8|9', min($numbers)); + assertType('0', min([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])); + + assertType('0|1|2|3|4|5|6|7|8|9', max($numbers)); + assertType('9', max([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])); + } +} diff --git a/tests/PHPStan/Analyser/data/minmax.php b/tests/PHPStan/Analyser/data/minmax.php new file mode 100644 index 0000000000..d4cbb77c44 --- /dev/null +++ b/tests/PHPStan/Analyser/data/minmax.php @@ -0,0 +1,66 @@ +|int<1, max>, 1?: int|int<1, max>}', array_filter([$i, $j])); + assertType('array{1: true}', array_filter([false, true])); +} + +function dummy6(string $s, string $t): void { + assertType('array{0?: non-falsy-string, 1?: non-falsy-string}', array_filter([$s, $t])); +} + +class HelloWorld +{ + public function setRange(int $range): void + { + if ($range < 0) { + return; + } + assertType('int<0, 100>', min($range, 100)); + assertType('int<0, 100>', min(100, $range)); + } + + public function setRange2(int $range): void + { + if ($range > 100) { + return; + } + assertType('int<0, 100>', max($range, 0)); + assertType('int<0, 100>', max(0, $range)); + } + + public function boundRange(): void + { + /** + * @var int<1, 6> $range + */ + $range = getFoo(); + + assertType('int<1, 4>', min($range, 4)); + assertType('int<4, 6>', max(4, $range)); + } + + public function unionType(): void + { + /** + * @var array{0, 1, 2}|array{4, 5, 6} $numbers2 + */ + $numbers2 = getFoo(); + + assertType('0|4', min($numbers2)); + assertType('2|6', max($numbers2)); + } +} diff --git a/tests/PHPStan/Analyser/data/sizeof-php8.php b/tests/PHPStan/Analyser/data/sizeof-php8.php new file mode 100644 index 0000000000..0af3b4062c --- /dev/null +++ b/tests/PHPStan/Analyser/data/sizeof-php8.php @@ -0,0 +1,63 @@ +