Skip to content

Commit

Permalink
fix, improve and simplify
Browse files Browse the repository at this point in the history
  • Loading branch information
herndlm committed Feb 25, 2023
1 parent 159f405 commit 30070eb
Show file tree
Hide file tree
Showing 4 changed files with 47 additions and 56 deletions.
25 changes: 19 additions & 6 deletions src/Type/Php/FilterFunctionReturnTypeHelper.php
Expand Up @@ -51,7 +51,25 @@ public function __construct(private ReflectionProvider $reflectionProvider)
$this->flagsString = new ConstantStringType('flags');
}

public function getTypeFromFunctionCall(Type $inputType, ?Type $filterType, ?Type $flagsType): Type
public function getArrayOffsetValueType(Type $arrayType, Type $offsetType, ?Type $filterType, ?Type $flagsType): ?Type
{
$inexistentOffsetType = $this->hasFlag($this->getConstant('FILTER_NULL_ON_FAILURE'), $flagsType)
? new ConstantBooleanType(false)
: new NullType();

$arrayTypeHasOffsetValueType = $arrayType->hasOffsetValueType($offsetType);
if ($arrayTypeHasOffsetValueType->no()) {
return $inexistentOffsetType;
}

$filteredType = $this->getType($arrayType->getOffsetValueType($offsetType), $filterType, $flagsType);

return $arrayTypeHasOffsetValueType->maybe()
? TypeCombinator::union($filteredType, $inexistentOffsetType)
: $filteredType;
}

public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType): Type
{
$mixedType = new MixedType();

Expand Down Expand Up @@ -106,11 +124,6 @@ public function getTypeFromFunctionCall(Type $inputType, ?Type $filterType, ?Typ
return $type;
}

public function hasConstantFlag(string $constant, ?Type $flagsType): bool
{
return $this->hasFlag($this->getConstant($constant), $flagsType);
}

/**
* @return array<int, Type>
*/
Expand Down
69 changes: 23 additions & 46 deletions src/Type/Php/FilterInputDynamicReturnTypeExtension.php
Expand Up @@ -2,30 +2,22 @@

namespace PHPStan\Type\Php;

use PhpParser\Node\Arg;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\Variable;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\ArrayType;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\NullType;
use PHPStan\Type\MixedType;
use PHPStan\Type\NeverType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function count;
use function in_array;

class FilterInputDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
{

private const INPUT_TYPE_VARIABLE_NAME_MAP = [
'INPUT_GET' => '_GET',
'INPUT_POST' => '_POST',
'INPUT_COOKIE' => '_COOKIE',
'INPUT_SERVER' => '_SERVER',
'INPUT_ENV' => '_ENV',
];

public function __construct(private FilterFunctionReturnTypeHelper $filterFunctionReturnTypeHelper)
{
}
Expand All @@ -41,46 +33,31 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
return null;
}

$inputVariable = $this->determineInputVariable($functionCall->getArgs()[0]);
$typeArgExpr = $functionCall->getArgs()[0]->value;
$varNameType = $scope->getType($functionCall->getArgs()[1]->value);
if ($inputVariable === null || !$varNameType->isString()->yes()) {
return null;
}

$inputVariableType = $scope->getType($inputVariable);
$inputHasOffsetValue = $inputVariableType->hasOffsetValueType($varNameType);
$flagsType = isset($functionCall->getArgs()[3]) ? $scope->getType($functionCall->getArgs()[3]->value) : null;
if ($inputHasOffsetValue->no()) {
return $this->determineNonExistentOffsetReturnType($flagsType);
$varNameTypeIsString = $varNameType->isString();
if (
$varNameTypeIsString->no()
|| !($typeArgExpr instanceof ConstFetch)
|| !in_array((string) $typeArgExpr->name, ['INPUT_GET', 'INPUT_POST', 'INPUT_COOKIE', 'INPUT_SERVER', 'INPUT_ENV'], true)
) {
return new NeverType();
}

$filterType = isset($functionCall->getArgs()[2]) ? $scope->getType($functionCall->getArgs()[2]->value) : null;
$filteredType = $this->filterFunctionReturnTypeHelper->getTypeFromFunctionCall(
$inputVariableType->getOffsetValueType($varNameType),
$filterType,
$flagsType,
);

return !$inputVariableType->hasOffsetValueType($varNameType)->yes()
? TypeCombinator::union($filteredType, $this->determineNonExistentOffsetReturnType($flagsType))
: $filteredType;
}

private function determineInputVariable(Arg $type): ?Variable
{
if (!$type->value instanceof ConstFetch) {
if ($varNameTypeIsString->maybe()) {
return null;
}

$variableName = self::INPUT_TYPE_VARIABLE_NAME_MAP[(string) $type->value->name] ?? null;
return $variableName === null ? null : new Variable($variableName);
}
// Pragmatical solution since global expressions are not passed through the scope for performance reasons
// See https://github.com/phpstan/phpstan-src/pull/2012 for details
$inputType = new ArrayType(new StringType(), new MixedType());

private function determineNonExistentOffsetReturnType(?Type $flagsType): Type
{
return $flagsType === null || !$this->filterFunctionReturnTypeHelper->hasConstantFlag('FILTER_NULL_ON_FAILURE', $flagsType)
? new NullType()
: new ConstantBooleanType(false);
return $this->filterFunctionReturnTypeHelper->getArrayOffsetValueType(
$inputType,
$varNameType,
isset($functionCall->getArgs()[2]) ? $scope->getType($functionCall->getArgs()[2]->value) : null,
isset($functionCall->getArgs()[3]) ? $scope->getType($functionCall->getArgs()[3]->value) : null,
);
}

}
2 changes: 1 addition & 1 deletion src/Type/Php/FilterVarDynamicReturnTypeExtension.php
Expand Up @@ -32,7 +32,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
$filterType = isset($functionCall->getArgs()[1]) ? $scope->getType($functionCall->getArgs()[1]->value) : null;
$flagsType = isset($functionCall->getArgs()[2]) ? $scope->getType($functionCall->getArgs()[2]->value) : null;

return $this->filterFunctionReturnTypeHelper->getTypeFromFunctionCall($inputType, $filterType, $flagsType);
return $this->filterFunctionReturnTypeHelper->getType($inputType, $filterType, $flagsType);
}

}
7 changes: 4 additions & 3 deletions tests/PHPStan/Analyser/data/filter-input.php
Expand Up @@ -7,11 +7,11 @@
class FilterInput
{

public function invalidTypesOrVarNamesAreIgnored($mixed): void
public function invalidTypesOrVarNames($mixed): void
{
assertType('mixed', filter_input(INPUT_GET, $mixed, FILTER_VALIDATE_INT));
assertType('mixed', filter_input(INPUT_GET, 17, FILTER_VALIDATE_INT));
assertType('mixed', filter_input(-1, 'foo', FILTER_VALIDATE_INT));
assertType('*NEVER*', filter_input(INPUT_GET, 17, FILTER_VALIDATE_INT));
assertType('*NEVER*', filter_input(-1, 'foo', FILTER_VALIDATE_INT));
}

public function supportedSuperGlobals(): void
Expand All @@ -28,6 +28,7 @@ public function doFoo(string $foo): void
assertType('int|false|null', filter_input(INPUT_GET, $foo, FILTER_VALIDATE_INT));
assertType('int|false|null', filter_input(INPUT_GET, 'foo', FILTER_VALIDATE_INT));
assertType('int|false|null', filter_input(INPUT_GET, 'foo', FILTER_VALIDATE_INT, ['flags' => FILTER_NULL_ON_FAILURE]));
assertType("'invalid'|int|null", filter_input(INPUT_GET, 'foo', FILTER_VALIDATE_INT, ['options' => ['default' => 'invalid']]));
assertType('array<int|false>|null', filter_input(INPUT_GET, 'foo', FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY]));
assertType('array<int|null>|false', filter_input(INPUT_GET, 'foo', FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE]));
assertType('0|int<17, 19>|null', filter_input(INPUT_GET, 'foo', FILTER_VALIDATE_INT, ['options' => ['default' => 0, 'min_range' => 17, 'max_range' => 19]]));
Expand Down

0 comments on commit 30070eb

Please sign in to comment.