Repository navigation
Add a dynamic return type extension for clamp() - #6684
Conversation
| foreach ($args as $arg) { | ||
| if ($arg->unpack) { | ||
| return null; | ||
| } | ||
| } |
There was a problem hiding this comment.
i don't remember having such check in other dynamic extension. Is it really needed ? (or can't it be supported ?)
This doesn't seems to be tested
There was a problem hiding this comment.
I guess its inspired by MinMaxFunctionReturnTypeExtension. agree it needs more tests
There was a problem hiding this comment.
Good question, it can be supported. I checked every case against what PHP 8.6 actually returns: arrays compare by count and then element by element, and an array is greater than any scalar. PHPStan keeps those comparisons as bool, so the result is the union of the three arguments, which always contains the real value. Removed in bf9e838; int|array<int> now gives array<int>|int<1, 10> instead of mixed.
There was a problem hiding this comment.
the above comment was about the if ($arg->unpack) { which means we need tests for using this function with splat operator.
I don't think we need precise types for clamp(...$x) now but at least a test which covers the current result would be good
There was a problem hiding this comment.
Thanks for clarifying, and sorry @VincentLanglet, I answered about the wrong line. The check is needed: without it, the extension takes an unpacked array for the argument itself, so clamp(...[$i], ...[1], ...[10]) gives array{int} and clamp($i, 1, ...[10]) gives array{10}|int<1, max>. Added doSplat() in 8362760; it pins the current mixed result and fails without the check.
| $maxType = $scope->getType($maxExpr); | ||
|
|
||
| // arrays are compared by size first, keep the native return type for them | ||
| if (!$valueType->isArray()->no() || !$minType->isArray()->no() || !$maxType->isArray()->no()) { |
There was a problem hiding this comment.
looking at this condition makes me feel we miss tests for values which are maybe an array, like int|array or something
There was a problem hiding this comment.
The condition is gone (see the thread above), and int|array<int>, list<int> and constant arrays are now tested in doArrays().
| foreach ($args as $arg) { | ||
| if ($arg->unpack) { | ||
| return null; | ||
| } | ||
| } |
There was a problem hiding this comment.
I guess its inspired by MinMaxFunctionReturnTypeExtension. agree it needs more tests
| $value = new AlwaysRememberedExpr($valueExpr, $valueType, $scope->getNativeType($valueExpr)); | ||
| $min = new AlwaysRememberedExpr($minExpr, $minType, $scope->getNativeType($minExpr)); | ||
| $max = new AlwaysRememberedExpr($maxExpr, $maxType, $scope->getNativeType($maxExpr)); |
There was a problem hiding this comment.
why do we need AlwaysRememberedExpr. there is no test failling if we strip this to
// same comparisons as php_math_clamp(): $max < $value ? $max : ($value < $min ? $min : $value)
return $scope->getType(new Ternary(
new Smaller($maxExpr, $valueExpr),
$maxExpr,
new Ternary(
new Smaller($valueExpr, $minExpr),
$minExpr,
$valueExpr,
),
));see cc57957 in which it was added to MinMax
There was a problem hiding this comment.
It's needed for impure arguments, for the same reason as cc57957: without it, clamp(impureInt(), 1, 10) is int instead of int<1, 10>, because the call is evaluated again in each branch of the ternary. I added that case, and it fails without the wrapper.
| */ | ||
| function doFoo(int $i, float $f, int|float $n, int $a, int $b, string $s, ?int $maybe, int $percent, int $delta, array $list): void | ||
| { | ||
| assertType('int<1, 10>', clamp($i, 1, 10)); |
There was a problem hiding this comment.
please add tests and asserts for how the return type is inferered when min/max is
- a union of const integers
- a integer range
There was a problem hiding this comment.
Added in doBounds(): unions of constant integers and integer ranges, for both bounds.
|
|
||
| declare(strict_types = 1); | ||
|
|
||
| namespace Clamp; |
There was a problem hiding this comment.
https://www.php.net/clamp contains a example with datetime objects. please also test that
There was a problem hiding this comment.
Added the php.net example in doDates(), plus a mixed DateTime/DateTimeImmutable case.
|
Thank you 🙏 |
Closes phpstan/phpstan#15366
PHP 8.6 adds
clamp(mixed $value, mixed $min, mixed $max): mixed.php_math_clamp()returns a copy of one of its three arguments:$maxwhen$max < $value, otherwise$minwhen$value < $min, otherwise$value.The new extension rewrites the call into that ternary, with
AlwaysRememberedExpr, the wayMinMaxFunctionReturnTypeExtensionhandlesmin($a, $b)andmax($a, $b). Soclamp($int, 1, 10)isint<1, 10>,clamp($int, 1.0, 10.0)is1.0|10.0|int<1, 10>, and constant arguments give a constant result. Arrays, which PHP compares by size first, and unpacked arguments keep the nativemixed.The type inference test needs
clamp()at runtime, so it is gated with// lint >= 8.6.