Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support integer-range type in min/max #627

Merged
merged 8 commits into from
Aug 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
29 changes: 28 additions & 1 deletion src/Type/Php/MinMaxFunctionReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

namespace PHPStan\Type\Php;

use PhpParser\Node\Expr\BinaryOp\Smaller;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\Ternary;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
Expand Down Expand Up @@ -64,6 +66,31 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
return new ErrorType();
}

// rewrite min($x, $y) as $x < $y ? $x : $y
// we don't handle arrays, which have different semantics
$functionName = $functionReflection->getName();
$args = $functionCall->args;
if (count($functionCall->args) === 2) {
$argType0 = $scope->getType($args[0]->value);
$argType1 = $scope->getType($args[1]->value);

if ($argType0->isArray()->no() && $argType1->isArray()->no()) {
if ($functionName === 'min') {
return $scope->getType(new Ternary(
new Smaller($args[0]->value, $args[1]->value),
$args[0]->value,
$args[1]->value
));
} elseif ($functionName === 'max') {
return $scope->getType(new Ternary(
new Smaller($args[0]->value, $args[1]->value),
$args[1]->value,
$args[0]->value
));
}
}
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't it possible that we could delete some code that's already in the extension thanks to this logic?

Copy link
Contributor Author

@staabm staabm Aug 20, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the code above only handles the 1-arg case and the code below handles the any-number-of-args case.

I guess we can drop it, after we push the

return $scope->getType(new Ternary(
						new Smaller($args[0]->value, $args[1]->value),
						$args[1]->value,
						$args[0]->value
					));

approach into a direction where it supports any number of args (using BooleanAnd and BooleanOr) as you suggested in #604 (comment)

but IMO this should be a separate PR

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool :)

$argumentTypes = [];
foreach ($functionCall->args as $arg) {
$argType = $scope->getType($arg->value);
Expand All @@ -83,7 +110,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
}

return $this->processType(
$functionReflection->getName(),
$functionName,
$argumentTypes
);
}
Expand Down
32 changes: 32 additions & 0 deletions tests/PHPStan/Analyser/data/minmax-arrays.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,35 @@ function dummy5(int $i, int $j): void
function dummy6(string $s, string $t): void {
assertType('array(?0 => non-empty-string, ?1 => non-empty-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));
}
}