Skip to content

Commit 6338a0d

Browse files
Fix filter var on uncertainty flags
1 parent 71f7b78 commit 6338a0d

File tree

3 files changed

+86
-34
lines changed

3 files changed

+86
-34
lines changed

src/Type/Php/FilterFunctionReturnTypeHelper.php

Lines changed: 81 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@
2525
use PHPStan\Type\StringType;
2626
use PHPStan\Type\Type;
2727
use PHPStan\Type\TypeCombinator;
28+
use PHPStan\Type\UnionType;
29+
30+
use PHPStan\Type\VerbosityLevel;
31+
2832
use function array_key_exists;
2933
use function array_merge;
3034
use function hexdec;
@@ -56,9 +60,14 @@ public function __construct(private ReflectionProvider $reflectionProvider, priv
5660

5761
private function getOffsetValueType(Type $inputType, Type $offsetType, ?Type $filterType, ?Type $flagsType): Type
5862
{
59-
$inexistentOffsetType = $this->hasFlag('FILTER_NULL_ON_FAILURE', $flagsType)
60-
? new ConstantBooleanType(false)
61-
: new NullType();
63+
$hasNullOnFailure = $this->hasFlag('FILTER_NULL_ON_FAILURE', $flagsType);
64+
if ($hasNullOnFailure->yes()) {
65+
$inexistentOffsetType = new ConstantBooleanType(false);
66+
} elseif ($hasNullOnFailure->no()) {
67+
$inexistentOffsetType = new NullType();
68+
} else {
69+
$inexistentOffsetType = new UnionType([new ConstantBooleanType(false), new NullType()]);
70+
}
6271

6372
$hasOffsetValueType = $inputType->hasOffsetValueType($offsetType);
6473
if ($hasOffsetValueType->no()) {
@@ -120,20 +129,44 @@ public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType): T
120129
}
121130

122131
$hasOptions = $this->hasOptions($flagsType);
132+
if ($hasOptions->maybe()) {
133+
// Too complicated
134+
return $mixedType;
135+
}
136+
123137
$options = $hasOptions->yes() ? $this->getOptions($flagsType, $filterValue) : [];
124138

125-
$defaultType = $options['default'] ?? ($this->hasFlag('FILTER_NULL_ON_FAILURE', $flagsType)
126-
? new NullType()
127-
: new ConstantBooleanType(false));
139+
if (isset($options['default'])) {
140+
$defaultType = $options['default'];
141+
} else {
142+
$hasNullOnFailure = $this->hasFlag('FILTER_NULL_ON_FAILURE', $flagsType);
143+
if ($hasNullOnFailure->yes()) {
144+
$defaultType = new NullType();
145+
} elseif ($hasNullOnFailure->no()) {
146+
$defaultType = new ConstantBooleanType(false);
147+
} else {
148+
$defaultType = new UnionType([new ConstantBooleanType(false), new NullType()]);
149+
}
150+
}
128151

129-
$inputIsArray = $inputType->isArray();
130152
$hasRequireArrayFlag = $this->hasFlag('FILTER_REQUIRE_ARRAY', $flagsType);
131-
if ($inputIsArray->no() && $hasRequireArrayFlag) {
153+
if ($hasRequireArrayFlag->maybe()) {
154+
// Too complicated
155+
return $mixedType;
156+
}
157+
158+
$inputIsArray = $inputType->isArray();
159+
if ($inputIsArray->no() && $hasRequireArrayFlag->yes()) {
132160
return $defaultType;
133161
}
134162

135163
$hasForceArrayFlag = $this->hasFlag('FILTER_FORCE_ARRAY', $flagsType);
136-
if ($inputIsArray->yes() && ($hasRequireArrayFlag || $hasForceArrayFlag)) {
164+
if ($hasRequireArrayFlag->no() && $hasForceArrayFlag->maybe()) {
165+
// Too complicated
166+
return $mixedType;
167+
}
168+
169+
if ($inputIsArray->yes() && ($hasRequireArrayFlag->yes() || $hasForceArrayFlag->yes())) {
137170
$inputArrayKeyType = $inputType->getIterableKeyType();
138171
$inputType = $inputType->getIterableValueType();
139172
}
@@ -147,9 +180,11 @@ public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType): T
147180
$type = $exactType ?? $this->getFilterTypeMap()[$filterValue] ?? $mixedType;
148181
$type = $this->applyRangeOptions($type, $options, $defaultType);
149182

150-
if ($inputType->isNonEmptyString()->yes()
183+
if (
184+
$inputType->isNonEmptyString()->yes()
151185
&& $type->isString()->yes()
152-
&& !$this->canStringBeSanitized($filterValue, $flagsType)) {
186+
&& $this->canStringBeSanitized($filterValue, $flagsType)->no()
187+
) {
153188
$accessory = new AccessoryNonEmptyStringType();
154189
if ($inputType->isNonFalsyString()->yes()) {
155190
$accessory = new AccessoryNonFalsyStringType();
@@ -163,14 +198,14 @@ public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType): T
163198
}
164199
}
165200

166-
if ($hasRequireArrayFlag) {
167-
$type = new ArrayType($inputArrayKeyType ?? $mixedType, $type);
201+
if ($hasRequireArrayFlag->yes()) {
202+
$type = new ArrayType($inputArrayKeyType ?? new MixedType(), $type);
168203
if (!$inputIsArray->yes()) {
169204
$type = TypeCombinator::union($type, $defaultType);
170205
}
171206
}
172207

173-
if (!$hasRequireArrayFlag && $hasForceArrayFlag) {
208+
if ($hasRequireArrayFlag->no() && $hasForceArrayFlag->yes()) {
174209
return new ArrayType($inputArrayKeyType ?? $mixedType, $type);
175210
}
176211

@@ -329,16 +364,19 @@ private function determineExactType(Type $in, int $filterValue, Type $defaultTyp
329364
}
330365

331366
if ($in instanceof ConstantStringType) {
332-
$value = $in->getValue();
333367
$allowOctal = $this->hasFlag('FILTER_FLAG_ALLOW_OCTAL', $flagsType);
334368
$allowHex = $this->hasFlag('FILTER_FLAG_ALLOW_HEX', $flagsType);
369+
if ($allowOctal->maybe() || $allowHex->maybe()) {
370+
return null;
371+
}
335372

336-
if ($allowOctal && preg_match('/\A0[oO][0-7]+\z/', $value) === 1) {
373+
$value = $in->getValue();
374+
if ($allowOctal->yes() && preg_match('/\A0[oO][0-7]+\z/', $value) === 1) {
337375
$octalValue = octdec($value);
338376
return is_int($octalValue) ? new ConstantIntegerType($octalValue) : $defaultType;
339377
}
340378

341-
if ($allowHex && preg_match('/\A0[xX][0-9A-Fa-f]+\z/', $value) === 1) {
379+
if ($allowHex->yes() && preg_match('/\A0[xX][0-9A-Fa-f]+\z/', $value) === 1) {
342380
$hexValue = hexdec($value);
343381
return is_int($hexValue) ? new ConstantIntegerType($hexValue) : $defaultType;
344382
}
@@ -348,7 +386,7 @@ private function determineExactType(Type $in, int $filterValue, Type $defaultTyp
348386
}
349387

350388
if ($filterValue === $this->getConstant('FILTER_DEFAULT')) {
351-
if (!$this->canStringBeSanitized($filterValue, $flagsType) && $in->isString()->yes()) {
389+
if ($this->canStringBeSanitized($filterValue, $flagsType)->no() && $in->isString()->yes()) {
352390
return $in;
353391
}
354392

@@ -443,20 +481,23 @@ private function getOptions(Type $flagsType, int $filterValue): array
443481
/**
444482
* @param non-empty-string $flagName
445483
*/
446-
private function hasFlag(string $flagName, ?Type $flagsType): bool
484+
private function hasFlag(string $flagName, ?Type $flagsType): TrinaryLogic
447485
{
448486
$flag = $this->getConstant($flagName);
449487
if ($flag === null) {
450-
return false;
488+
return TrinaryLogic::createNo();
451489
}
452490

453-
if ($flagsType === null) {
454-
return false;
491+
if ($flagsType === null) { // Will default to 0
492+
return TrinaryLogic::createNo();
455493
}
456494

457495
$type = $this->getFlagsValue($flagsType);
496+
if (!$type instanceof ConstantIntegerType) {
497+
return TrinaryLogic::createMaybe();
498+
}
458499

459-
return $type instanceof ConstantIntegerType && ($type->getValue() & $flag) === $flag;
500+
return TrinaryLogic::createFromBoolean(($type->getValue() & $flag) === $flag);
460501
}
461502

462503
private function getFlagsValue(Type $exprType): Type
@@ -465,25 +506,36 @@ private function getFlagsValue(Type $exprType): Type
465506
return $exprType;
466507
}
467508

468-
return $exprType->getOffsetValueType($this->flagsString);
509+
$hasOffsetValue = $exprType->hasOffsetValueType($this->flagsString);
510+
if ($hasOffsetValue->no()) {
511+
return new ConstantIntegerType(0);
512+
}
513+
if ($hasOffsetValue->yes()) {
514+
return $exprType->getOffsetValueType($this->flagsString);
515+
}
516+
517+
return TypeCombinator::union(
518+
new ConstantIntegerType(0),
519+
$exprType->getOffsetValueType($this->flagsString),
520+
);
469521
}
470522

471-
private function canStringBeSanitized(int $filterValue, ?Type $flagsType): bool
523+
private function canStringBeSanitized(int $filterValue, ?Type $flagsType): TrinaryLogic
472524
{
473525
// If it is a validation filter, the string will not be changed
474526
if (($filterValue & self::VALIDATION_FILTER_BITMASK) !== 0) {
475-
return false;
527+
return TrinaryLogic::createNo();
476528
}
477529

478530
// FILTER_DEFAULT will not sanitize, unless it has FILTER_FLAG_STRIP_LOW,
479531
// FILTER_FLAG_STRIP_HIGH, or FILTER_FLAG_STRIP_BACKTICK
480532
if ($filterValue === $this->getConstant('FILTER_DEFAULT')) {
481533
return $this->hasFlag('FILTER_FLAG_STRIP_LOW', $flagsType)
482-
|| $this->hasFlag('FILTER_FLAG_STRIP_HIGH', $flagsType)
483-
|| $this->hasFlag('FILTER_FLAG_STRIP_BACKTICK', $flagsType);
534+
->or($this->hasFlag('FILTER_FLAG_STRIP_HIGH', $flagsType))
535+
->or($this->hasFlag('FILTER_FLAG_STRIP_BACKTICK', $flagsType));
484536
}
485537

486-
return true;
538+
return TrinaryLogic::createYes();
487539
}
488540

489541
}

tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7354,8 +7354,8 @@ public static function dataFilterVar(): Generator
73547354

73557355
$typeAndFlags = [
73567356
['%s|false', ''],
7357-
['%s|false', ', $mixed'],
7358-
['%s|false', ', ["flags" => $mixed]'],
7357+
['mixed', ', $mixed'],
7358+
['mixed', ', ["flags" => $mixed]'],
73597359
['%s|null', ', FILTER_NULL_ON_FAILURE'],
73607360
['%s|null', ', ["flags" => FILTER_NULL_ON_FAILURE]'],
73617361
['%s|null', ', ["flags" => FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV4]'],
@@ -7384,8 +7384,8 @@ public static function dataFilterVar(): Generator
73847384

73857385
$boolFlags = [
73867386
['bool', ''],
7387-
['bool', ', $mixed'],
7388-
['bool', ', ["flags" => $mixed]'],
7387+
['mixed', ', $mixed'],
7388+
['mixed', ', ["flags" => $mixed]'],
73897389
['bool|null', ', FILTER_NULL_ON_FAILURE'],
73907390
['bool|null', ', ["flags" => FILTER_NULL_ON_FAILURE]'],
73917391
['bool|null', ', ["flags" => FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV4]'],

tests/PHPStan/Analyser/nsrt/filter-var.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public function invalidInput(array $arr, object $object, $resource): void
9090
public function intToInt(int $int, array $options): void
9191
{
9292
assertType('int', filter_var($int, FILTER_VALIDATE_INT));
93-
assertType('int|false', filter_var($int, FILTER_VALIDATE_INT, $options));
93+
assertType('mixed', filter_var($int, FILTER_VALIDATE_INT, $options));
9494
assertType('int<0, max>|false', filter_var($int, FILTER_VALIDATE_INT, ['options' => ['min_range' => 0]]));
9595
}
9696

0 commit comments

Comments
 (0)