Skip to content

Commit 13b241e

Browse files
Fix filter var on uncertainty flags
1 parent 193d801 commit 13b241e

File tree

3 files changed

+88
-35
lines changed

3 files changed

+88
-35
lines changed

src/Type/Php/FilterFunctionReturnTypeHelper.php

Lines changed: 83 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@
2626
use PHPStan\Type\StringType;
2727
use PHPStan\Type\Type;
2828
use PHPStan\Type\TypeCombinator;
29+
use PHPStan\Type\UnionType;
30+
31+
use PHPStan\Type\VerbosityLevel;
32+
2933
use function array_key_exists;
3034
use function array_merge;
3135
use function hexdec;
@@ -57,9 +61,14 @@ public function __construct(private ReflectionProvider $reflectionProvider, priv
5761

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

6473
$hasOffsetValueType = $inputType->hasOffsetValueType($offsetType);
6574
if ($hasOffsetValueType->no()) {
@@ -121,24 +130,49 @@ public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType): T
121130
}
122131

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

126-
$defaultType = $options['default'] ?? ($this->hasFlag('FILTER_NULL_ON_FAILURE', $flagsType)
127-
? new NullType()
128-
: new ConstantBooleanType(false));
140+
if (isset($options['default'])) {
141+
$defaultType = $options['default'];
142+
} else {
143+
$hasNullOnFailure = $this->hasFlag('FILTER_NULL_ON_FAILURE', $flagsType);
144+
if ($hasNullOnFailure->yes()) {
145+
$defaultType = new NullType();
146+
} elseif ($hasNullOnFailure->no()) {
147+
$defaultType = new ConstantBooleanType(false);
148+
} else {
149+
$defaultType = new UnionType([new ConstantBooleanType(false), new NullType()]);
150+
}
151+
}
152+
153+
$hasRequireArrayFlag = $this->hasFlag('FILTER_REQUIRE_ARRAY', $flagsType);
154+
if ($hasRequireArrayFlag->maybe()) {
155+
// Too complicated
156+
return $mixedType;
157+
}
129158

130159
$inputIsArray = $inputType->isArray();
131160
$hasRequireArrayFlag = $this->hasFlag('FILTER_REQUIRE_ARRAY', $flagsType);
132-
if ($inputIsArray->no() && $hasRequireArrayFlag) {
133-
if ($this->hasFlag('FILTER_THROW_ON_FAILURE', $flagsType)) {
161+
if ($inputIsArray->no() && $hasRequireArrayFlag->yes()) {
162+
if ($this->hasFlag('FILTER_THROW_ON_FAILURE', $flagsType)->yes()) {
134163
return new ErrorType();
135164
}
136165

137166
return $defaultType;
138167
}
139168

140169
$hasForceArrayFlag = $this->hasFlag('FILTER_FORCE_ARRAY', $flagsType);
141-
if ($inputIsArray->yes() && ($hasRequireArrayFlag || $hasForceArrayFlag)) {
170+
if ($hasRequireArrayFlag->no() && $hasForceArrayFlag->maybe()) {
171+
// Too complicated
172+
return $mixedType;
173+
}
174+
175+
if ($inputIsArray->yes() && ($hasRequireArrayFlag->yes() || $hasForceArrayFlag->yes())) {
142176
$inputArrayKeyType = $inputType->getIterableKeyType();
143177
$inputType = $inputType->getIterableValueType();
144178
}
@@ -152,9 +186,11 @@ public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType): T
152186
$type = $exactType ?? $this->getFilterTypeMap()[$filterValue] ?? $mixedType;
153187
$type = $this->applyRangeOptions($type, $options, $defaultType);
154188

155-
if ($inputType->isNonEmptyString()->yes()
189+
if (
190+
$inputType->isNonEmptyString()->yes()
156191
&& $type->isString()->yes()
157-
&& !$this->canStringBeSanitized($filterValue, $flagsType)) {
192+
&& $this->canStringBeSanitized($filterValue, $flagsType)->no()
193+
) {
158194
$accessory = new AccessoryNonEmptyStringType();
159195
if ($inputType->isNonFalsyString()->yes()) {
160196
$accessory = new AccessoryNonFalsyStringType();
@@ -168,18 +204,18 @@ public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType): T
168204
}
169205
}
170206

171-
if ($hasRequireArrayFlag) {
172-
$type = new ArrayType($inputArrayKeyType ?? $mixedType, $type);
207+
if ($hasRequireArrayFlag->yes()) {
208+
$type = new ArrayType($inputArrayKeyType ?? new MixedType(), $type);
173209
if (!$inputIsArray->yes()) {
174210
$type = TypeCombinator::union($type, $defaultType);
175211
}
176212
}
177213

178-
if (!$hasRequireArrayFlag && $hasForceArrayFlag) {
214+
if ($hasRequireArrayFlag->no() && $hasForceArrayFlag->yes()) {
179215
return new ArrayType($inputArrayKeyType ?? $mixedType, $type);
180216
}
181217

182-
if ($this->hasFlag('FILTER_THROW_ON_FAILURE', $flagsType)) {
218+
if ($this->hasFlag('FILTER_THROW_ON_FAILURE', $flagsType)->yes()) {
183219
$type = TypeCombinator::remove($type, $defaultType);
184220
}
185221

@@ -338,16 +374,19 @@ private function determineExactType(Type $in, int $filterValue, Type $defaultTyp
338374
}
339375

340376
if ($in instanceof ConstantStringType) {
341-
$value = $in->getValue();
342377
$allowOctal = $this->hasFlag('FILTER_FLAG_ALLOW_OCTAL', $flagsType);
343378
$allowHex = $this->hasFlag('FILTER_FLAG_ALLOW_HEX', $flagsType);
379+
if ($allowOctal->maybe() || $allowHex->maybe()) {
380+
return null;
381+
}
344382

345-
if ($allowOctal && preg_match('/\A0[oO][0-7]+\z/', $value) === 1) {
383+
$value = $in->getValue();
384+
if ($allowOctal->yes() && preg_match('/\A0[oO][0-7]+\z/', $value) === 1) {
346385
$octalValue = octdec($value);
347386
return is_int($octalValue) ? new ConstantIntegerType($octalValue) : $defaultType;
348387
}
349388

350-
if ($allowHex && preg_match('/\A0[xX][0-9A-Fa-f]+\z/', $value) === 1) {
389+
if ($allowHex->yes() && preg_match('/\A0[xX][0-9A-Fa-f]+\z/', $value) === 1) {
351390
$hexValue = hexdec($value);
352391
return is_int($hexValue) ? new ConstantIntegerType($hexValue) : $defaultType;
353392
}
@@ -357,7 +396,7 @@ private function determineExactType(Type $in, int $filterValue, Type $defaultTyp
357396
}
358397

359398
if ($filterValue === $this->getConstant('FILTER_DEFAULT')) {
360-
if (!$this->canStringBeSanitized($filterValue, $flagsType) && $in->isString()->yes()) {
399+
if ($this->canStringBeSanitized($filterValue, $flagsType)->no() && $in->isString()->yes()) {
361400
return $in;
362401
}
363402

@@ -452,20 +491,23 @@ private function getOptions(Type $flagsType, int $filterValue): array
452491
/**
453492
* @param non-empty-string $flagName
454493
*/
455-
private function hasFlag(string $flagName, ?Type $flagsType): bool
494+
private function hasFlag(string $flagName, ?Type $flagsType): TrinaryLogic
456495
{
457496
$flag = $this->getConstant($flagName);
458497
if ($flag === null) {
459-
return false;
498+
return TrinaryLogic::createNo();
460499
}
461500

462-
if ($flagsType === null) {
463-
return false;
501+
if ($flagsType === null) { // Will default to 0
502+
return TrinaryLogic::createNo();
464503
}
465504

466505
$type = $this->getFlagsValue($flagsType);
506+
if (!$type instanceof ConstantIntegerType) {
507+
return TrinaryLogic::createMaybe();
508+
}
467509

468-
return $type instanceof ConstantIntegerType && ($type->getValue() & $flag) === $flag;
510+
return TrinaryLogic::createFromBoolean(($type->getValue() & $flag) === $flag);
469511
}
470512

471513
private function getFlagsValue(Type $exprType): Type
@@ -474,25 +516,36 @@ private function getFlagsValue(Type $exprType): Type
474516
return $exprType;
475517
}
476518

477-
return $exprType->getOffsetValueType($this->flagsString);
519+
$hasOffsetValue = $exprType->hasOffsetValueType($this->flagsString);
520+
if ($hasOffsetValue->no()) {
521+
return new ConstantIntegerType(0);
522+
}
523+
if ($hasOffsetValue->yes()) {
524+
return $exprType->getOffsetValueType($this->flagsString);
525+
}
526+
527+
return TypeCombinator::union(
528+
new ConstantIntegerType(0),
529+
$exprType->getOffsetValueType($this->flagsString),
530+
);
478531
}
479532

480-
private function canStringBeSanitized(int $filterValue, ?Type $flagsType): bool
533+
private function canStringBeSanitized(int $filterValue, ?Type $flagsType): TrinaryLogic
481534
{
482535
// If it is a validation filter, the string will not be changed
483536
if (($filterValue & self::VALIDATION_FILTER_BITMASK) !== 0) {
484-
return false;
537+
return TrinaryLogic::createNo();
485538
}
486539

487540
// FILTER_DEFAULT will not sanitize, unless it has FILTER_FLAG_STRIP_LOW,
488541
// FILTER_FLAG_STRIP_HIGH, or FILTER_FLAG_STRIP_BACKTICK
489542
if ($filterValue === $this->getConstant('FILTER_DEFAULT')) {
490543
return $this->hasFlag('FILTER_FLAG_STRIP_LOW', $flagsType)
491-
|| $this->hasFlag('FILTER_FLAG_STRIP_HIGH', $flagsType)
492-
|| $this->hasFlag('FILTER_FLAG_STRIP_BACKTICK', $flagsType);
544+
->or($this->hasFlag('FILTER_FLAG_STRIP_HIGH', $flagsType))
545+
->or($this->hasFlag('FILTER_FLAG_STRIP_BACKTICK', $flagsType));
493546
}
494547

495-
return true;
548+
return TrinaryLogic::createYes();
496549
}
497550

498551
}

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)