Skip to content

Commit 01f7309

Browse files
committed
Merge branch '1.9.x' into 1.10.x
2 parents bb83b50 + 4dd92cd commit 01f7309

6 files changed

+185
-24
lines changed

Diff for: src/Type/Php/FilterVarDynamicReturnTypeExtension.php

+53-21
Original file line numberDiff line numberDiff line change
@@ -152,15 +152,21 @@ public function getTypeFromFunctionCall(
152152
$filterValue = $filterType->getValue();
153153
}
154154

155-
$flagsType = isset($functionCall->getArgs()[2]) ? $scope->getType($functionCall->getArgs()[2]->value) : null;
155+
$flagsType = isset($functionCall->getArgs()[2]) ? $scope->getType($functionCall->getArgs()[2]->value) : new ConstantIntegerType(0);
156156
$inputType = $scope->getType($functionCall->getArgs()[0]->value);
157157
$defaultType = $this->hasFlag($this->getConstant('FILTER_NULL_ON_FAILURE'), $flagsType)
158158
? new NullType()
159159
: new ConstantBooleanType(false);
160+
161+
if ($inputType->isScalar()->no() && $inputType->isNull()->no()) {
162+
return $defaultType;
163+
}
164+
160165
$exactType = $this->determineExactType($inputType, $filterValue, $defaultType, $flagsType);
161166
$type = $exactType ?? $this->getFilterTypeMap()[$filterValue] ?? $mixedType;
162167

163-
$options = $flagsType !== null && $this->hasOptions($flagsType)->yes() ? $this->getOptions($flagsType, $filterValue) : [];
168+
$hasOptions = $this->hasOptions($flagsType);
169+
$options = $hasOptions->yes() ? $this->getOptions($flagsType, $filterValue) : [];
164170
$otherTypes = $this->getOtherTypes($flagsType, $options, $defaultType);
165171

166172
if ($inputType->isNonEmptyString()->yes()
@@ -185,7 +191,7 @@ public function getTypeFromFunctionCall(
185191
}
186192
}
187193

188-
if ($exactType !== null) {
194+
if ($exactType !== null && !$hasOptions->maybe() && ($inputType->equals($type) || !$inputType->isSuperTypeOf($type)->yes())) {
189195
unset($otherTypes['default']);
190196
}
191197

@@ -202,32 +208,58 @@ public function getTypeFromFunctionCall(
202208

203209
private function determineExactType(Type $in, int $filterValue, Type $defaultType, ?Type $flagsType): ?Type
204210
{
205-
if (($filterValue === $this->getConstant('FILTER_VALIDATE_BOOLEAN') && $in->isBoolean()->yes())
206-
|| ($filterValue === $this->getConstant('FILTER_VALIDATE_INT') && $in->isInteger()->yes())
207-
|| ($filterValue === $this->getConstant('FILTER_VALIDATE_FLOAT') && $in->isFloat()->yes())) {
208-
return $in;
211+
if ($filterValue === $this->getConstant('FILTER_VALIDATE_BOOLEAN')) {
212+
if ($in->isBoolean()->yes()) {
213+
return $in;
214+
}
209215
}
210216

211-
if ($filterValue === $this->getConstant('FILTER_VALIDATE_INT') && $in instanceof ConstantStringType) {
212-
$value = $in->getValue();
213-
$allowOctal = $this->hasFlag($this->getConstant('FILTER_FLAG_ALLOW_OCTAL'), $flagsType);
214-
$allowHex = $this->hasFlag($this->getConstant('FILTER_FLAG_ALLOW_HEX'), $flagsType);
217+
if ($filterValue === $this->getConstant('FILTER_VALIDATE_FLOAT')) {
218+
if ($in->isFloat()->yes()) {
219+
return $in;
220+
}
215221

216-
if ($allowOctal && preg_match('/\A0[oO][0-7]+\z/', $value) === 1) {
217-
$octalValue = octdec($value);
218-
return is_int($octalValue) ? new ConstantIntegerType($octalValue) : $defaultType;
222+
if ($in->isInteger()->yes()) {
223+
return $in->toFloat();
224+
}
225+
}
226+
227+
if ($filterValue === $this->getConstant('FILTER_VALIDATE_INT')) {
228+
if ($in->isFloat()->yes()) {
229+
return $in->toInteger();
219230
}
220231

221-
if ($allowHex && preg_match('/\A0[xX][0-9A-Fa-f]+\z/', $value) === 1) {
222-
$hexValue = hexdec($value);
223-
return is_int($hexValue) ? new ConstantIntegerType($hexValue) : $defaultType;
232+
if ($in->isInteger()->yes()) {
233+
return $in;
224234
}
225235

226-
return preg_match('/\A[+-]?(?:0|[1-9][0-9]*)\z/', $value) === 1 ? $in->toInteger() : $defaultType;
236+
if ($in instanceof ConstantStringType) {
237+
$value = $in->getValue();
238+
$allowOctal = $this->hasFlag($this->getConstant('FILTER_FLAG_ALLOW_OCTAL'), $flagsType);
239+
$allowHex = $this->hasFlag($this->getConstant('FILTER_FLAG_ALLOW_HEX'), $flagsType);
240+
241+
if ($allowOctal && preg_match('/\A0[oO][0-7]+\z/', $value) === 1) {
242+
$octalValue = octdec($value);
243+
return is_int($octalValue) ? new ConstantIntegerType($octalValue) : $defaultType;
244+
}
245+
246+
if ($allowHex && preg_match('/\A0[xX][0-9A-Fa-f]+\z/', $value) === 1) {
247+
$hexValue = hexdec($value);
248+
return is_int($hexValue) ? new ConstantIntegerType($hexValue) : $defaultType;
249+
}
250+
251+
return preg_match('/\A[+-]?(?:0|[1-9][0-9]*)\z/', $value) === 1 ? $in->toInteger() : $defaultType;
252+
}
227253
}
228254

229-
if ($filterValue === $this->getConstant('FILTER_VALIDATE_FLOAT') && $in->isInteger()->yes()) {
230-
return $in->toFloat();
255+
if ($filterValue === $this->getConstant('FILTER_DEFAULT')) {
256+
if (!$this->canStringBeSanitized($filterValue, $flagsType) && $in->isString()->yes()) {
257+
return $in;
258+
}
259+
260+
if ($in->isBoolean()->yes() || $in->isFloat()->yes() || $in->isInteger()->yes() || $in->isNull()->yes()) {
261+
return $in->toString();
262+
}
231263
}
232264

233265
return null;
@@ -273,7 +305,7 @@ private function getOtherTypes(?Type $flagsType, array $typeOptions, Type $defau
273305

274306
private function hasOptions(Type $flagsType): TrinaryLogic
275307
{
276-
return $flagsType->isConstantArray()
308+
return $flagsType->isArray()
277309
->and($flagsType->hasOffsetValueType(new ConstantStringType('options')));
278310
}
279311

Diff for: tests/PHPStan/Analyser/NodeScopeResolverTest.php

+1
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-var.php');
626627

627628
if (PHP_VERSION_ID >= 80100) {
628629
yield from $this->gatherAssertTypes(__DIR__ . '/data/enums.php');

Diff for: tests/PHPStan/Analyser/data/filter-var-returns-non-empty-string.php

+6-3
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public function run(string $str, int $int, int $positive_int, int $negative_int)
1616
assertType('non-empty-string', $str);
1717

1818
$return = filter_var($str, FILTER_DEFAULT);
19-
assertType('non-empty-string|false', $return);
19+
assertType('non-empty-string', $return);
2020

2121
$return = filter_var($str, FILTER_DEFAULT, FILTER_FLAG_STRIP_LOW);
2222
assertType('string|false', $return);
@@ -82,7 +82,10 @@ public function run(string $str, int $int, int $positive_int, int $negative_int)
8282
assertType('9', $return);
8383

8484
$return = filter_var(1.0, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => 9]]);
85-
assertType('int<1, 9>|false', $return);
85+
assertType('1', $return);
86+
87+
$return = filter_var(11.0, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => 9]]);
88+
assertType('false', $return);
8689

8790
$return = filter_var($str, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => $positive_int]]);
8891
assertType('int<1, max>|false', $return);
@@ -95,7 +98,7 @@ public function run(string $str, int $int, int $positive_int, int $negative_int)
9598

9699
$str2 = '';
97100
$return = filter_var($str2, FILTER_DEFAULT);
98-
assertType('string|false', $return);
101+
assertType("''", $return);
99102

100103
$return = filter_var($str2, FILTER_VALIDATE_URL);
101104
assertType('string|false', $return);

Diff for: tests/PHPStan/Analyser/data/filter-var.php

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace FilterVar;
4+
5+
use stdClass;
6+
use function PHPStan\Testing\assertType;
7+
8+
class FilterVar
9+
{
10+
11+
public function doFoo($mixed): void
12+
{
13+
assertType('int|false', filter_var($mixed, FILTER_VALIDATE_INT));
14+
assertType('int|null', filter_var($mixed, FILTER_VALIDATE_INT, ['flags' => FILTER_NULL_ON_FAILURE]));
15+
assertType('array<int|false>', filter_var($mixed, FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY]));
16+
assertType('array<int|null>', filter_var($mixed, FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE]));
17+
assertType('0|int<17, 19>', filter_var($mixed, FILTER_VALIDATE_INT, ['options' => ['default' => 0, 'min_range' => 17, 'max_range' => 19]]));
18+
19+
assertType('array<false>', filter_var(false, FILTER_VALIDATE_BOOLEAN, FILTER_FORCE_ARRAY | FILTER_NULL_ON_FAILURE));
20+
}
21+
22+
/** @param resource $resource */
23+
public function invalidInput(array $arr, object $object, $resource): void
24+
{
25+
assertType('false', filter_var($arr));
26+
assertType('false', filter_var($object));
27+
assertType('false', filter_var($resource));
28+
assertType('null', filter_var(new stdClass(), FILTER_DEFAULT, FILTER_NULL_ON_FAILURE));
29+
}
30+
31+
public function intToInt(int $int, array $options): void
32+
{
33+
assertType('int', filter_var($int, FILTER_VALIDATE_INT));
34+
assertType('int|false', filter_var($int, FILTER_VALIDATE_INT, $options));
35+
assertType('int<0, max>|false', filter_var($int, FILTER_VALIDATE_INT, ['options' => ['min_range' => 0]]));
36+
}
37+
38+
/**
39+
* @param int<0, 9> $intRange
40+
* @param non-empty-string $nonEmptyString
41+
*/
42+
public function scalars(bool $bool, float $float, int $int, string $string, int $intRange, string $nonEmptyString): void
43+
{
44+
assertType('bool', filter_var($bool, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE));
45+
assertType('true', filter_var(true, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE));
46+
assertType('false', filter_var(false, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE));
47+
assertType('bool|null', filter_var($float, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE));
48+
assertType('bool|null', filter_var(17.0, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); // could be null
49+
assertType('bool|null', filter_var($int, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE));
50+
assertType('bool|null', filter_var($intRange, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE));
51+
assertType('bool|null', filter_var(17, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); // could be null
52+
assertType('bool|null', filter_var($string, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE));
53+
assertType('bool|null', filter_var($nonEmptyString, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE));
54+
assertType('bool|null', filter_var('17', FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); // could be null
55+
assertType('bool|null', filter_var(null, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); // could be null
56+
57+
assertType('float|false', filter_var($bool, FILTER_VALIDATE_FLOAT));
58+
assertType('float|false', filter_var(true, FILTER_VALIDATE_FLOAT)); // could be 1
59+
assertType('float|false', filter_var(false, FILTER_VALIDATE_FLOAT)); // could be false
60+
assertType('float', filter_var($float, FILTER_VALIDATE_FLOAT));
61+
assertType('17.0', filter_var(17.0, FILTER_VALIDATE_FLOAT));
62+
assertType('float', filter_var($int, FILTER_VALIDATE_FLOAT));
63+
assertType('float', filter_var($intRange, FILTER_VALIDATE_FLOAT));
64+
assertType('17.0', filter_var(17, FILTER_VALIDATE_FLOAT));
65+
assertType('float|false', filter_var($string, FILTER_VALIDATE_FLOAT));
66+
assertType('float|false', filter_var($nonEmptyString, FILTER_VALIDATE_FLOAT));
67+
assertType('float|false', filter_var('17', FILTER_VALIDATE_FLOAT)); // could be 17.0
68+
assertType('float|false', filter_var(null, FILTER_VALIDATE_FLOAT)); // could be false
69+
70+
assertType('int|false', filter_var($bool, FILTER_VALIDATE_INT));
71+
assertType('int|false', filter_var(true, FILTER_VALIDATE_INT)); // could be 1
72+
assertType('int|false', filter_var(false, FILTER_VALIDATE_INT)); // could be false
73+
assertType('int', filter_var($float, FILTER_VALIDATE_INT));
74+
assertType('17', filter_var(17.0, FILTER_VALIDATE_INT));
75+
assertType('int', filter_var($int, FILTER_VALIDATE_INT));
76+
assertType('int<0, 9>', filter_var($intRange, FILTER_VALIDATE_INT));
77+
assertType('17', filter_var(17, FILTER_VALIDATE_INT));
78+
assertType('int|false', filter_var($string, FILTER_VALIDATE_INT));
79+
assertType('int|false', filter_var($nonEmptyString, FILTER_VALIDATE_INT));
80+
assertType('17', filter_var('17', FILTER_VALIDATE_INT));
81+
assertType('int|false', filter_var(null, FILTER_VALIDATE_INT)); // could be false
82+
83+
assertType("''|'1'", filter_var($bool));
84+
assertType("'1'", filter_var(true));
85+
assertType("''", filter_var(false));
86+
assertType('numeric-string', filter_var($float));
87+
assertType("'17'", filter_var(17.0));
88+
assertType('numeric-string', filter_var($int));
89+
assertType('numeric-string', filter_var($intRange));
90+
assertType("'17'", filter_var(17));
91+
assertType('string', filter_var($string));
92+
assertType('non-empty-string', filter_var($nonEmptyString));
93+
assertType("'17'", filter_var('17'));
94+
assertType("''", filter_var(null));
95+
}
96+
97+
}

Diff for: tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php

+10
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,16 @@ public function testBug8485(): void
653653
]);
654654
}
655655

656+
public function testBug8516(): void
657+
{
658+
if (PHP_VERSION_ID < 70400) {
659+
$this->markTestSkipped('Test requires PHP 7.4.');
660+
}
661+
662+
$this->checkAlwaysTrueStrictComparison = true;
663+
$this->analyse([__DIR__ . '/data/bug-8516.php'], []);
664+
}
665+
656666
public function testPhpUnitIntegration(): void
657667
{
658668
$this->checkAlwaysTrueStrictComparison = true;

Diff for: tests/PHPStan/Rules/Comparison/data/bug-8516.php

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php declare(strict_types = 1); // lint >= 7.4
2+
3+
namespace Bug8516;
4+
5+
function validate($value, array $options = null): bool
6+
{
7+
if (is_int($value)) {
8+
$options ??= ['options' => ['min_range' => 0]];
9+
if (filter_var($value, FILTER_VALIDATE_INT, $options) === false) {
10+
return false;
11+
}
12+
// ...
13+
}
14+
if (is_string($value)) {
15+
// ...
16+
}
17+
return true;
18+
}

0 commit comments

Comments
 (0)