Skip to content

Commit 9c72fc1

Browse files
authored
Specify return type for filter_input()
1 parent 492b927 commit 9c72fc1

File tree

8 files changed

+150
-2
lines changed

8 files changed

+150
-2
lines changed

conf/config.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1456,6 +1456,11 @@ services:
14561456
-
14571457
class: PHPStan\Type\Php\FilterFunctionReturnTypeHelper
14581458

1459+
-
1460+
class: PHPStan\Type\Php\FilterInputDynamicReturnTypeExtension
1461+
tags:
1462+
- phpstan.broker.dynamicFunctionReturnTypeExtension
1463+
14591464
-
14601465
class: PHPStan\Type\Php\FilterVarDynamicReturnTypeExtension
14611466
tags:

src/Type/Php/FilterFunctionReturnTypeHelper.php

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,25 @@ public function __construct(private ReflectionProvider $reflectionProvider)
5151
$this->flagsString = new ConstantStringType('flags');
5252
}
5353

54-
public function getTypeFromFunctionCall(Type $inputType, ?Type $filterType, ?Type $flagsType): Type
54+
public function getOffsetValueType(Type $inputType, Type $offsetType, ?Type $filterType, ?Type $flagsType): ?Type
55+
{
56+
$inexistentOffsetType = $this->hasFlag($this->getConstant('FILTER_NULL_ON_FAILURE'), $flagsType)
57+
? new ConstantBooleanType(false)
58+
: new NullType();
59+
60+
$hasOffsetValueType = $inputType->hasOffsetValueType($offsetType);
61+
if ($hasOffsetValueType->no()) {
62+
return $inexistentOffsetType;
63+
}
64+
65+
$filteredType = $this->getType($inputType->getOffsetValueType($offsetType), $filterType, $flagsType);
66+
67+
return $hasOffsetValueType->maybe()
68+
? TypeCombinator::union($filteredType, $inexistentOffsetType)
69+
: $filteredType;
70+
}
71+
72+
public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType): Type
5573
{
5674
$mixedType = new MixedType();
5775

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Php;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Expr\FuncCall;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Reflection\FunctionReflection;
9+
use PHPStan\Reflection\ReflectionProvider;
10+
use PHPStan\Type\ArrayType;
11+
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
12+
use PHPStan\Type\MixedType;
13+
use PHPStan\Type\StringType;
14+
use PHPStan\Type\Type;
15+
use PHPStan\Type\TypeCombinator;
16+
use function count;
17+
18+
class FilterInputDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
19+
{
20+
21+
public function __construct(private FilterFunctionReturnTypeHelper $filterFunctionReturnTypeHelper, private ReflectionProvider $reflectionProvider)
22+
{
23+
}
24+
25+
public function isFunctionSupported(FunctionReflection $functionReflection): bool
26+
{
27+
return $functionReflection->getName() === 'filter_input';
28+
}
29+
30+
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type
31+
{
32+
if (count($functionCall->getArgs()) < 2) {
33+
return null;
34+
}
35+
36+
$supportedTypes = TypeCombinator::union(
37+
$this->reflectionProvider->getConstant(new Node\Name('INPUT_GET'), null)->getValueType(),
38+
$this->reflectionProvider->getConstant(new Node\Name('INPUT_POST'), null)->getValueType(),
39+
$this->reflectionProvider->getConstant(new Node\Name('INPUT_COOKIE'), null)->getValueType(),
40+
$this->reflectionProvider->getConstant(new Node\Name('INPUT_SERVER'), null)->getValueType(),
41+
$this->reflectionProvider->getConstant(new Node\Name('INPUT_ENV'), null)->getValueType(),
42+
);
43+
$typeType = $scope->getType($functionCall->getArgs()[0]->value);
44+
if (!$typeType->isInteger()->yes() || $supportedTypes->isSuperTypeOf($typeType)->no()) {
45+
return null;
46+
}
47+
48+
// Pragmatical solution since global expressions are not passed through the scope for performance reasons
49+
// See https://github.com/phpstan/phpstan-src/pull/2012 for details
50+
$inputType = new ArrayType(new StringType(), new MixedType());
51+
52+
return $this->filterFunctionReturnTypeHelper->getOffsetValueType(
53+
$inputType,
54+
$scope->getType($functionCall->getArgs()[1]->value),
55+
isset($functionCall->getArgs()[2]) ? $scope->getType($functionCall->getArgs()[2]->value) : null,
56+
isset($functionCall->getArgs()[3]) ? $scope->getType($functionCall->getArgs()[3]->value) : null,
57+
);
58+
}
59+
60+
}

src/Type/Php/FilterVarDynamicReturnTypeExtension.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
3232
$filterType = isset($functionCall->getArgs()[1]) ? $scope->getType($functionCall->getArgs()[1]->value) : null;
3333
$flagsType = isset($functionCall->getArgs()[2]) ? $scope->getType($functionCall->getArgs()[2]->value) : null;
3434

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

3838
}

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,7 @@ public function dataFileAsserts(): iterable
623623
}
624624

625625
yield from $this->gatherAssertTypes(__DIR__ . '/data/filesystem-functions.php');
626+
yield from $this->gatherAssertTypes(__DIR__ . '/data/filter-input.php');
626627
yield from $this->gatherAssertTypes(__DIR__ . '/data/filter-var.php');
627628

628629
if (PHP_VERSION_ID >= 80100) {
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace FilterInput;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class FilterInput
8+
{
9+
10+
public function invalidTypesOrVarNames($mixed): void
11+
{
12+
assertType('int|false|null', filter_input(INPUT_GET, $mixed, FILTER_VALIDATE_INT));
13+
assertType('mixed', filter_input(-1, 'foo', FILTER_VALIDATE_INT));
14+
assertType('null', filter_input(INPUT_GET, 17, FILTER_VALIDATE_INT));
15+
assertType('false', filter_input(INPUT_GET, 17, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE));
16+
}
17+
18+
public function supportedSuperGlobals(): void
19+
{
20+
assertType('int|false|null', filter_input(INPUT_GET, 'foo', FILTER_VALIDATE_INT));
21+
assertType('int|false|null', filter_input(INPUT_POST, 'foo', FILTER_VALIDATE_INT));
22+
assertType('int|false|null', filter_input(INPUT_COOKIE, 'foo', FILTER_VALIDATE_INT));
23+
assertType('int|false|null', filter_input(INPUT_SERVER, 'foo', FILTER_VALIDATE_INT));
24+
assertType('int|false|null', filter_input(INPUT_ENV, 'foo', FILTER_VALIDATE_INT));
25+
}
26+
27+
public function inputTypeUnion(): void
28+
{
29+
assertType('int|false|null', filter_input(rand(0, 1) ? INPUT_GET : INPUT_POST, 'foo', FILTER_VALIDATE_INT));
30+
}
31+
32+
public function doFoo(string $foo): void
33+
{
34+
assertType('int|false|null', filter_input(INPUT_GET, $foo, FILTER_VALIDATE_INT));
35+
assertType('int|false|null', filter_input(INPUT_GET, 'foo', FILTER_VALIDATE_INT));
36+
assertType('int|false|null', filter_input(INPUT_GET, 'foo', FILTER_VALIDATE_INT, ['flags' => FILTER_NULL_ON_FAILURE]));
37+
assertType("'invalid'|int|null", filter_input(INPUT_GET, 'foo', FILTER_VALIDATE_INT, ['options' => ['default' => 'invalid']]));
38+
assertType('array<int|false>|null', filter_input(INPUT_GET, 'foo', FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY]));
39+
assertType('array<int|null>|false', filter_input(INPUT_GET, 'foo', FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE]));
40+
assertType('0|int<17, 19>|null', filter_input(INPUT_GET, 'foo', FILTER_VALIDATE_INT, ['options' => ['default' => 0, 'min_range' => 17, 'max_range' => 19]]));
41+
}
42+
43+
}

tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1130,6 +1130,12 @@ public function testBug5474(): void
11301130
]);
11311131
}
11321132

1133+
public function testBug6261(): void
1134+
{
1135+
$this->checkExplicitMixed = true;
1136+
$this->analyse([__DIR__ . '/data/bug-6261.php'], []);
1137+
}
1138+
11331139
public function testBug6781(): void
11341140
{
11351141
$this->analyse([__DIR__ . '/data/bug-6781.php'], []);
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug6261;
4+
5+
function needs_int(int $x) : void {}
6+
7+
function () {
8+
$x = filter_input(INPUT_POST, 'row_id', FILTER_VALIDATE_INT);
9+
10+
if($x === false || $x === null) {
11+
die("I expected a numeric string!\n");
12+
}
13+
14+
needs_int($x);
15+
};

0 commit comments

Comments
 (0)