Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 72 additions & 28 deletions src/Type/Php/FilterFunctionReturnTypeHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\UnionType;
use function array_key_exists;
use function array_merge;
use function hexdec;
Expand Down Expand Up @@ -56,9 +57,14 @@ public function __construct(private ReflectionProvider $reflectionProvider, priv

private function getOffsetValueType(Type $inputType, Type $offsetType, ?Type $filterType, ?Type $flagsType): Type
{
$inexistentOffsetType = $this->hasFlag('FILTER_NULL_ON_FAILURE', $flagsType)
? new ConstantBooleanType(false)
: new NullType();
$hasNullOnFailure = $this->hasFlag('FILTER_NULL_ON_FAILURE', $flagsType);
if ($hasNullOnFailure->yes()) {
$inexistentOffsetType = new ConstantBooleanType(false);
} elseif ($hasNullOnFailure->no()) {
$inexistentOffsetType = new NullType();
} else {
$inexistentOffsetType = new UnionType([new ConstantBooleanType(false), new NullType()]);
}

$hasOffsetValueType = $inputType->hasOffsetValueType($offsetType);
if ($hasOffsetValueType->no()) {
Expand Down Expand Up @@ -122,18 +128,37 @@ public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType): T
$hasOptions = $this->hasOptions($flagsType);
$options = $hasOptions->yes() ? $this->getOptions($flagsType, $filterValue) : [];

$defaultType = $options['default'] ?? ($this->hasFlag('FILTER_NULL_ON_FAILURE', $flagsType)
? new NullType()
: new ConstantBooleanType(false));
if (isset($options['default'])) {
$defaultType = $options['default'];
} else {
$hasNullOnFailure = $this->hasFlag('FILTER_NULL_ON_FAILURE', $flagsType);
if ($hasNullOnFailure->yes()) {
$defaultType = new NullType();
} elseif ($hasNullOnFailure->no()) {
$defaultType = new ConstantBooleanType(false);
} else {
$defaultType = new UnionType([new ConstantBooleanType(false), new NullType()]);
}
}

$inputIsArray = $inputType->isArray();
$hasRequireArrayFlag = $this->hasFlag('FILTER_REQUIRE_ARRAY', $flagsType);
if ($inputIsArray->no() && $hasRequireArrayFlag) {
if ($hasRequireArrayFlag->maybe()) {
// Too complicated
return $mixedType;
}

$inputIsArray = $inputType->isArray();
if ($inputIsArray->no() && $hasRequireArrayFlag->yes()) {
return $defaultType;
}

$hasForceArrayFlag = $this->hasFlag('FILTER_FORCE_ARRAY', $flagsType);
if ($inputIsArray->yes() && ($hasRequireArrayFlag || $hasForceArrayFlag)) {
if ($hasRequireArrayFlag->no() && $hasForceArrayFlag->maybe()) {
// Too complicated
return $mixedType;
}

if ($inputIsArray->yes() && ($hasRequireArrayFlag->yes() || $hasForceArrayFlag->yes())) {
$inputArrayKeyType = $inputType->getIterableKeyType();
$inputType = $inputType->getIterableValueType();
}
Expand All @@ -147,9 +172,11 @@ public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType): T
$type = $exactType ?? $this->getFilterTypeMap()[$filterValue] ?? $mixedType;
$type = $this->applyRangeOptions($type, $options, $defaultType);

if ($inputType->isNonEmptyString()->yes()
if (
$inputType->isNonEmptyString()->yes()
&& $type->isString()->yes()
&& !$this->canStringBeSanitized($filterValue, $flagsType)) {
&& $this->canStringBeSanitized($filterValue, $flagsType)->no()
) {
$accessory = new AccessoryNonEmptyStringType();
if ($inputType->isNonFalsyString()->yes()) {
$accessory = new AccessoryNonFalsyStringType();
Expand All @@ -163,14 +190,14 @@ public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType): T
}
}

if ($hasRequireArrayFlag) {
if ($hasRequireArrayFlag->yes()) {
$type = new ArrayType($inputArrayKeyType ?? $mixedType, $type);
if (!$inputIsArray->yes()) {
$type = TypeCombinator::union($type, $defaultType);
}
}

if (!$hasRequireArrayFlag && $hasForceArrayFlag) {
if ($hasRequireArrayFlag->no() && $hasForceArrayFlag->yes()) {
return new ArrayType($inputArrayKeyType ?? $mixedType, $type);
}

Expand Down Expand Up @@ -329,16 +356,19 @@ private function determineExactType(Type $in, int $filterValue, Type $defaultTyp
}

if ($in instanceof ConstantStringType) {
$value = $in->getValue();
$allowOctal = $this->hasFlag('FILTER_FLAG_ALLOW_OCTAL', $flagsType);
$allowHex = $this->hasFlag('FILTER_FLAG_ALLOW_HEX', $flagsType);
if ($allowOctal->maybe() || $allowHex->maybe()) {
return null;
}

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

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

if ($filterValue === $this->getConstant('FILTER_DEFAULT')) {
if (!$this->canStringBeSanitized($filterValue, $flagsType) && $in->isString()->yes()) {
if ($this->canStringBeSanitized($filterValue, $flagsType)->no() && $in->isString()->yes()) {
return $in;
}

Expand Down Expand Up @@ -443,20 +473,23 @@ private function getOptions(Type $flagsType, int $filterValue): array
/**
* @param non-empty-string $flagName
*/
private function hasFlag(string $flagName, ?Type $flagsType): bool
private function hasFlag(string $flagName, ?Type $flagsType): TrinaryLogic
{
$flag = $this->getConstant($flagName);
if ($flag === null) {
return false;
return TrinaryLogic::createNo();
}

if ($flagsType === null) {
return false;
if ($flagsType === null) { // Will default to 0
return TrinaryLogic::createNo();
}

$type = $this->getFlagsValue($flagsType);
if (!$type instanceof ConstantIntegerType) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In next PR I'll add support for union of ConstantIntegerType

return TrinaryLogic::createMaybe();
}

return $type instanceof ConstantIntegerType && ($type->getValue() & $flag) === $flag;
return TrinaryLogic::createFromBoolean(($type->getValue() & $flag) === $flag);
}

private function getFlagsValue(Type $exprType): Type
Expand All @@ -465,25 +498,36 @@ private function getFlagsValue(Type $exprType): Type
return $exprType;
}

return $exprType->getOffsetValueType($this->flagsString);
$hasOffsetValue = $exprType->hasOffsetValueType($this->flagsString);
if ($hasOffsetValue->no()) {
return new ConstantIntegerType(0);
}
if ($hasOffsetValue->yes()) {
return $exprType->getOffsetValueType($this->flagsString);
}

return TypeCombinator::union(
new ConstantIntegerType(0),
$exprType->getOffsetValueType($this->flagsString),
);
}

private function canStringBeSanitized(int $filterValue, ?Type $flagsType): bool
private function canStringBeSanitized(int $filterValue, ?Type $flagsType): TrinaryLogic
{
// If it is a validation filter, the string will not be changed
if (($filterValue & self::VALIDATION_FILTER_BITMASK) !== 0) {
return false;
return TrinaryLogic::createNo();
}

// FILTER_DEFAULT will not sanitize, unless it has FILTER_FLAG_STRIP_LOW,
// FILTER_FLAG_STRIP_HIGH, or FILTER_FLAG_STRIP_BACKTICK
if ($filterValue === $this->getConstant('FILTER_DEFAULT')) {
return $this->hasFlag('FILTER_FLAG_STRIP_LOW', $flagsType)
|| $this->hasFlag('FILTER_FLAG_STRIP_HIGH', $flagsType)
|| $this->hasFlag('FILTER_FLAG_STRIP_BACKTICK', $flagsType);
->or($this->hasFlag('FILTER_FLAG_STRIP_HIGH', $flagsType))
->or($this->hasFlag('FILTER_FLAG_STRIP_BACKTICK', $flagsType));
}

return true;
return TrinaryLogic::createYes();
}

}
8 changes: 4 additions & 4 deletions tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7354,8 +7354,8 @@ public static function dataFilterVar(): Generator

$typeAndFlags = [
['%s|false', ''],
['%s|false', ', $mixed'],
['%s|false', ', ["flags" => $mixed]'],
['mixed', ', $mixed'],
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was wrong, with a mixed flag it's not necessary %s|false cause you might pass FILTER_NULL_ON_FAILURE or FILTER_FORCE_ARRAY or FILTER_REQUIRE_ARRAY

['mixed', ', ["flags" => $mixed]'],
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was wrong, with a mixed flag it's not necessary %s|false cause you might pass FILTER_NULL_ON_FAILURE or FILTER_FORCE_ARRAY or FILTER_REQUIRE_ARRAY

['%s|null', ', FILTER_NULL_ON_FAILURE'],
['%s|null', ', ["flags" => FILTER_NULL_ON_FAILURE]'],
['%s|null', ', ["flags" => FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV4]'],
Expand Down Expand Up @@ -7384,8 +7384,8 @@ public static function dataFilterVar(): Generator

$boolFlags = [
['bool', ''],
['bool', ', $mixed'],
['bool', ', ["flags" => $mixed]'],
['mixed', ', $mixed'],
['mixed', ', ["flags" => $mixed]'],
Comment on lines +7387 to +7388
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was wrong, with a mixed flag it's not necessary bool cause you might pass FILTER_NULL_ON_FAILURE or FILTER_FORCE_ARRAY or FILTER_REQUIRE_ARRAY

['bool|null', ', FILTER_NULL_ON_FAILURE'],
['bool|null', ', ["flags" => FILTER_NULL_ON_FAILURE]'],
['bool|null', ', ["flags" => FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV4]'],
Expand Down
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/nsrt/filter-var.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ public function invalidInput(array $arr, object $object, $resource): void
public function intToInt(int $int, array $options): void
{
assertType('int', filter_var($int, FILTER_VALIDATE_INT));
assertType('int|false', filter_var($int, FILTER_VALIDATE_INT, $options));
assertType('mixed', filter_var($int, FILTER_VALIDATE_INT, $options));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was wrong, with a array $options, the flag might be mixed.

And with a mixed flag it's not necessary mixed cause you might pass FILTER_NULL_ON_FAILURE or FILTER_FORCE_ARRAY or FILTER_REQUIRE_ARRAY

assertType('int<0, max>|false', filter_var($int, FILTER_VALIDATE_INT, ['options' => ['min_range' => 0]]));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1009,6 +1009,11 @@ public function testBug11019(): void
$this->analyse([__DIR__ . '/data/bug-11019.php'], []);
}

public function testBug11485(): void
{
$this->analyse([__DIR__ . '/data/bug-11485.php'], []);
}

public function testBug12946(): void
{
$this->analyse([__DIR__ . '/data/bug-12946.php'], []);
Expand Down
18 changes: 18 additions & 0 deletions tests/PHPStan/Rules/Comparison/data/bug-11485.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php declare(strict_types = 1);

namespace Bug11485;

class Foo {

public function bar(string $value, bool $strict = true): bool {

$flags = $strict ? FILTER_NULL_ON_FAILURE : 0;
$result = filter_var($value, FILTER_VALIDATE_BOOLEAN, $flags);
if ($result === null) throw new \Exception("not a boolean");

$result = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
if ($result === null) throw new \Exception("not a boolean");

return $result;
}
}
Loading