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

Refactor FilterVarDynamicReturnTypeExtension to pass around Types instead of Args and the Scope #2109

Merged
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
52 changes: 24 additions & 28 deletions src/Type/Php/FilterVarDynamicReturnTypeExtension.php
Expand Up @@ -3,7 +3,6 @@
namespace PHPStan\Type\Php;

use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
Expand Down Expand Up @@ -143,32 +142,30 @@ public function getTypeFromFunctionCall(
{
$mixedType = new MixedType();

$filterArg = $functionCall->getArgs()[1] ?? null;
if ($filterArg === null) {
$filterType = isset($functionCall->getArgs()[1]) ? $scope->getType($functionCall->getArgs()[1]->value) : null;
if ($filterType === null) {
$filterValue = $this->getConstant('FILTER_DEFAULT');
} else {
$filterType = $scope->getType($filterArg->value);
if (!$filterType instanceof ConstantIntegerType) {
return $mixedType;
}
$filterValue = $filterType->getValue();
}

$flagsArg = $functionCall->getArgs()[2] ?? null;
$flagsType = isset($functionCall->getArgs()[2]) ? $scope->getType($functionCall->getArgs()[2]->value) : null;
$inputType = $scope->getType($functionCall->getArgs()[0]->value);

$defaultType = $this->hasFlag($this->getConstant('FILTER_NULL_ON_FAILURE'), $flagsArg, $scope)
$defaultType = $this->hasFlag($this->getConstant('FILTER_NULL_ON_FAILURE'), $flagsType)
? new NullType()
: new ConstantBooleanType(false);
$exactType = $this->determineExactType($inputType, $filterValue, $defaultType, $flagsArg, $scope);
$exactType = $this->determineExactType($inputType, $filterValue, $defaultType, $flagsType);
$type = $exactType ?? $this->getFilterTypeMap()[$filterValue] ?? $mixedType;

$typeOptionNames = $this->getFilterTypeOptions()[$filterValue] ?? [];
$otherTypes = $this->getOtherTypes($flagsArg, $scope, $typeOptionNames, $defaultType);
$otherTypes = $this->getOtherTypes($flagsType, $typeOptionNames, $defaultType);

if ($inputType->isNonEmptyString()->yes()
&& $type->isString()->yes()
&& !$this->canStringBeSanitized($filterValue, $flagsArg, $scope)) {
&& !$this->canStringBeSanitized($filterValue, $flagsType)) {
$accessory = new AccessoryNonEmptyStringType();
if ($inputType->isNonFalsyString()->yes()) {
$accessory = new AccessoryNonFalsyStringType();
Expand Down Expand Up @@ -196,14 +193,14 @@ public function getTypeFromFunctionCall(
$type = TypeCombinator::union($type, $otherTypes['default']);
}

if ($this->hasFlag($this->getConstant('FILTER_FORCE_ARRAY'), $flagsArg, $scope)) {
if ($this->hasFlag($this->getConstant('FILTER_FORCE_ARRAY'), $flagsType)) {
return new ArrayType(new MixedType(), $type);
}

return $type;
}

private function determineExactType(Type $in, int $filterValue, Type $defaultType, ?Arg $flagsArg, Scope $scope): ?Type
private function determineExactType(Type $in, int $filterValue, Type $defaultType, ?Type $flagsType): ?Type
{
if (($filterValue === $this->getConstant('FILTER_VALIDATE_BOOLEAN') && $in->isBoolean()->yes())
|| ($filterValue === $this->getConstant('FILTER_VALIDATE_INT') && $in->isInteger()->yes())
Expand All @@ -213,8 +210,8 @@ private function determineExactType(Type $in, int $filterValue, Type $defaultTyp

if ($filterValue === $this->getConstant('FILTER_VALIDATE_INT') && $in instanceof ConstantStringType) {
$value = $in->getValue();
$allowOctal = $this->hasFlag($this->getConstant('FILTER_FLAG_ALLOW_OCTAL'), $flagsArg, $scope);
$allowHex = $this->hasFlag($this->getConstant('FILTER_FLAG_ALLOW_HEX'), $flagsArg, $scope);
$allowOctal = $this->hasFlag($this->getConstant('FILTER_FLAG_ALLOW_OCTAL'), $flagsType);
$allowHex = $this->hasFlag($this->getConstant('FILTER_FLAG_ALLOW_HEX'), $flagsType);

if ($allowOctal && preg_match('/\A0[oO][0-7]+\z/', $value) === 1) {
$octalValue = octdec($value);
Expand All @@ -240,14 +237,14 @@ private function determineExactType(Type $in, int $filterValue, Type $defaultTyp
* @param list<string> $typeOptionNames
* @return array{default: Type, range?: Type}
*/
private function getOtherTypes(?Node\Arg $flagsArg, Scope $scope, array $typeOptionNames, Type $defaultType): array
private function getOtherTypes(?Type $flagsType, array $typeOptionNames, Type $defaultType): array
{
$falseType = new ConstantBooleanType(false);
if ($flagsArg === null) {
if ($flagsType === null) {
return ['default' => $falseType];
}

$typeOptions = $this->getOptions($flagsArg, $scope, 'default', ...$typeOptionNames);
$typeOptions = $this->getOptions($flagsType, 'default', ...$typeOptionNames);
$defaultType = $typeOptions['default'] ?? $defaultType;
$otherTypes = ['default' => $defaultType];
$range = [];
Expand Down Expand Up @@ -278,16 +275,15 @@ private function getOtherTypes(?Node\Arg $flagsArg, Scope $scope, array $typeOpt
/**
* @return array<string, ?Type>
*/
private function getOptions(Node\Arg $expression, Scope $scope, string ...$optionNames): array
private function getOptions(Type $flagsType, string ...$optionNames): array
{
$options = [];

$exprType = $scope->getType($expression->value);
if (!$exprType instanceof ConstantArrayType) {
if (!$flagsType instanceof ConstantArrayType) {
return $options;
}

$optionsType = $exprType->getOffsetValueType(new ConstantStringType('options'));
$optionsType = $flagsType->getOffsetValueType(new ConstantStringType('options'));
if (!$optionsType instanceof ConstantArrayType) {
return $options;
}
Expand All @@ -300,13 +296,13 @@ private function getOptions(Node\Arg $expression, Scope $scope, string ...$optio
return $options;
}

private function hasFlag(int $flag, ?Node\Arg $expression, Scope $scope): bool
private function hasFlag(int $flag, ?Type $flagsType): bool
{
if ($expression === null) {
if ($flagsType === null) {
return false;
}

$type = $this->getFlagsValue($scope->getType($expression->value));
$type = $this->getFlagsValue($flagsType);

return $type instanceof ConstantIntegerType && ($type->getValue() & $flag) === $flag;
}
Expand All @@ -320,7 +316,7 @@ private function getFlagsValue(Type $exprType): Type
return $exprType->getOffsetValueType($this->flagsString);
}

private function canStringBeSanitized(int $filterValue, ?Node\Arg $flagsArg, Scope $scope): bool
private function canStringBeSanitized(int $filterValue, ?Type $flagsType): bool
{
// If it is a validation filter, the string will not be changed
if (($filterValue & self::VALIDATION_FILTER_BITMASK) !== 0) {
Expand All @@ -330,9 +326,9 @@ private function canStringBeSanitized(int $filterValue, ?Node\Arg $flagsArg, Sco
// FILTER_DEFAULT will not sanitize, unless it has FILTER_FLAG_STRIP_LOW,
// FILTER_FLAG_STRIP_HIGH, or FILTER_FLAG_STRIP_BACKTICK
if ($filterValue === $this->getConstant('FILTER_DEFAULT')) {
return $this->hasFlag($this->getConstant('FILTER_FLAG_STRIP_LOW'), $flagsArg, $scope)
|| $this->hasFlag($this->getConstant('FILTER_FLAG_STRIP_HIGH'), $flagsArg, $scope)
|| $this->hasFlag($this->getConstant('FILTER_FLAG_STRIP_BACKTICK'), $flagsArg, $scope);
return $this->hasFlag($this->getConstant('FILTER_FLAG_STRIP_LOW'), $flagsType)
|| $this->hasFlag($this->getConstant('FILTER_FLAG_STRIP_HIGH'), $flagsType)
|| $this->hasFlag($this->getConstant('FILTER_FLAG_STRIP_BACKTICK'), $flagsType);
}

return true;
Expand Down