Skip to content
Closed
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
76 changes: 60 additions & 16 deletions src/Type/Php/FilterFunctionReturnTypeHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeTraverser;
use PHPStan\Type\UnionType;
use function array_key_exists;
use function array_merge;
Expand Down Expand Up @@ -167,9 +168,42 @@

if ($inputIsArray->yes() && ($hasRequireArrayFlag->yes() || $hasForceArrayFlag->yes())) {
$inputArrayKeyType = $inputType->getIterableKeyType();
$inputType = $inputType->getIterableValueType();
$inputValueType = $inputType->getIterableValueType();
$filteredValueType = $this->filterTypeComponents($inputValueType, $filterType, $flagsType, $filterValue, $defaultType, $mixedType, $hasOptions, $options);

return new ArrayType($inputArrayKeyType, $filteredValueType);
}

if ($hasRequireArrayFlag->yes()) {

Check warning on line 177 in src/Type/Php/FilterFunctionReturnTypeHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ return new ArrayType($inputArrayKeyType, $filteredValueType); } - if ($hasRequireArrayFlag->yes()) { + if (!$hasRequireArrayFlag->no()) { $type = $this->filterTypeComponents($inputType, $filterType, $flagsType, $filterValue, $defaultType, $mixedType, $hasOptions, $options); $type = new ArrayType($mixedType, $type); if (!$inputIsArray->yes()) {

Check warning on line 177 in src/Type/Php/FilterFunctionReturnTypeHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ return new ArrayType($inputArrayKeyType, $filteredValueType); } - if ($hasRequireArrayFlag->yes()) { + if (!$hasRequireArrayFlag->no()) { $type = $this->filterTypeComponents($inputType, $filterType, $flagsType, $filterValue, $defaultType, $mixedType, $hasOptions, $options); $type = new ArrayType($mixedType, $type); if (!$inputIsArray->yes()) {
$type = $this->filterTypeComponents($inputType, $filterType, $flagsType, $filterValue, $defaultType, $mixedType, $hasOptions, $options);
$type = new ArrayType($mixedType, $type);
if (!$inputIsArray->yes()) {
$type = TypeCombinator::union($type, $defaultType);
}
return $type;
}

if ($hasForceArrayFlag->yes()) {

Check warning on line 186 in src/Type/Php/FilterFunctionReturnTypeHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ return $type; } - if ($hasForceArrayFlag->yes()) { + if (!$hasForceArrayFlag->no()) { $type = $this->filterTypeComponents($inputType, $filterType, $flagsType, $filterValue, $defaultType, $mixedType, $hasOptions, $options); return new ArrayType($mixedType, $type); }

Check warning on line 186 in src/Type/Php/FilterFunctionReturnTypeHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ return $type; } - if ($hasForceArrayFlag->yes()) { + if (!$hasForceArrayFlag->no()) { $type = $this->filterTypeComponents($inputType, $filterType, $flagsType, $filterValue, $defaultType, $mixedType, $hasOptions, $options); return new ArrayType($mixedType, $type); }
$type = $this->filterTypeComponents($inputType, $filterType, $flagsType, $filterValue, $defaultType, $mixedType, $hasOptions, $options);
return new ArrayType($mixedType, $type);
}

$type = $this->filterScalarType($inputType, $filterValue, $defaultType, $flagsType, $mixedType, $hasOptions, $options);

if ($this->hasFlag('FILTER_THROW_ON_FAILURE', $flagsType)->yes()) {

Check warning on line 193 in src/Type/Php/FilterFunctionReturnTypeHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $type = $this->filterScalarType($inputType, $filterValue, $defaultType, $flagsType, $mixedType, $hasOptions, $options); - if ($this->hasFlag('FILTER_THROW_ON_FAILURE', $flagsType)->yes()) { + if (!$this->hasFlag('FILTER_THROW_ON_FAILURE', $flagsType)->no()) { $type = TypeCombinator::remove($type, $defaultType); }

Check warning on line 193 in src/Type/Php/FilterFunctionReturnTypeHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $type = $this->filterScalarType($inputType, $filterValue, $defaultType, $flagsType, $mixedType, $hasOptions, $options); - if ($this->hasFlag('FILTER_THROW_ON_FAILURE', $flagsType)->yes()) { + if (!$this->hasFlag('FILTER_THROW_ON_FAILURE', $flagsType)->no()) { $type = TypeCombinator::remove($type, $defaultType); }
$type = TypeCombinator::remove($type, $defaultType);
}

return $type;
}

/**
* Applies the filter to a scalar input type (no array wrapping).
*
* @param array<string, ?Type> $options
*/
private function filterScalarType(Type $inputType, int $filterValue, Type $defaultType, ?Type $flagsType, MixedType $mixedType, TrinaryLogic $hasOptions, array $options): Type
{
if ($inputType->isScalar()->no() && $inputType->isNull()->no()) {
$exactType = $defaultType;
} else {
Expand Down Expand Up @@ -197,24 +231,34 @@
}
}

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

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

if ($this->hasFlag('FILTER_THROW_ON_FAILURE', $flagsType)->yes()) {
$type = TypeCombinator::remove($type, $defaultType);
}

return $type;
}

/**
* Recursively filters each component of a type that may contain arrays.
* Array components are recursively filtered via getType(), scalar components
* are filtered via filterScalarType(), and maybe-array components produce
* a union of both.
*
* @param array<string, ?Type> $options
*/
private function filterTypeComponents(Type $type, ?Type $filterType, ?Type $flagsType, int $filterValue, Type $defaultType, MixedType $mixedType, TrinaryLogic $hasOptions, array $options): Type
{
return TypeTraverser::map($type, function (Type $innerType, callable $traverse) use ($filterType, $flagsType, $filterValue, $defaultType, $mixedType, $hasOptions, $options): Type {
if ($innerType instanceof UnionType || $innerType instanceof IntersectionType) {
return $traverse($innerType);
}
if ($innerType->isArray()->yes()) {
return $this->getType($innerType, $filterType, $flagsType);
}
$scalarResult = $this->filterScalarType($innerType, $filterValue, $defaultType, $flagsType, $mixedType, $hasOptions, $options);
if ($innerType->isArray()->maybe()) {
return TypeCombinator::union($scalarResult, new ArrayType($mixedType, $mixedType));
}
return $scalarResult;
});
}

/**
* @return array<int, Type>
*/
Expand Down
92 changes: 92 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-11339.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php declare(strict_types = 1);

namespace Bug11339;

use function PHPStan\Testing\assertType;

// assume query string to be ?a[a][b][]=a&a[a][b][]=b

$f = filter_input(INPUT_GET, 'a', FILTER_DEFAULT, FILTER_FORCE_ARRAY);
assertType('array<array|string|false>|null', $f);

$g = filter_input(INPUT_GET, 'a', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY);
assertType('array<array|string|false>|false|null', $g);

$h = filter_input(INPUT_GET, 'a', FILTER_VALIDATE_INT, FILTER_FORCE_ARRAY);
assertType('array<array|int|false>|null', $h);

$i = filter_input(INPUT_GET, 'a', FILTER_VALIDATE_INT, FILTER_REQUIRE_ARRAY);
assertType('array<array|int|false>|false|null', $i);

// filter_var with known scalar should not include array in value type
$j = filter_var('foo', FILTER_DEFAULT, FILTER_FORCE_ARRAY);
assertType("array<'foo'>", $j);

$k = filter_var('foo', FILTER_VALIDATE_INT, FILTER_FORCE_ARRAY);
assertType('array<false>', $k);

/**
* @param array<int> $arrayOfInt
* @param array<array<int>> $arrayOfArrayOfInt
* @param array<int|array<int>> $arrayOfIntOrArrayOfInt
*/
function typedArrayInputs(array $arrayOfInt, array $arrayOfArrayOfInt, array $arrayOfIntOrArrayOfInt): void
{
// array<int> - values are ints, not arrays, so no array in value type
$l = filter_var($arrayOfInt, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY]);
assertType('array<int>', $l);

$m = filter_var($arrayOfInt, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_NULL_ON_FAILURE]);
assertType('array<int>', $m);

// array<array<int>> - values are arrays, recursively filtered
$n = filter_var($arrayOfArrayOfInt, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY]);
assertType('array<array<int>>', $n);

$o = filter_var($arrayOfArrayOfInt, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_NULL_ON_FAILURE]);
assertType('array<array<int>>', $o);

// array<int|array<int>> - mixed values, both scalar and array possible
$p = filter_var($arrayOfIntOrArrayOfInt, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY]);
assertType('array<array<int>|int>', $p);

$q = filter_var($arrayOfIntOrArrayOfInt, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_NULL_ON_FAILURE]);
assertType('array<array<int>|int>', $q);

// FORCE_ARRAY with typed arrays
$r = filter_var($arrayOfInt, FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY]);
assertType('array<int>', $r);

$s = filter_var($arrayOfArrayOfInt, FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY]);
assertType('array<array<int>>', $s);
}

/**
* Union-typed inputs (scalar | array) exercise the inputIsArray->maybe() path
*
* @param int|array<int> $intOrArrayOfInt
* @param string|array<string> $stringOrArrayOfString
*/
function unionTypedInputs(int|array $intOrArrayOfInt, string|array $stringOrArrayOfString): void
{
// FORCE_ARRAY: scalar part filtered, array part recursively filtered
$a = filter_var($intOrArrayOfInt, FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY]);
assertType('array<array<int>|int>', $a);

$b = filter_var($stringOrArrayOfString, FILTER_DEFAULT, ['flags' => FILTER_FORCE_ARRAY]);
assertType('array<array<string>|string>', $b);

// REQUIRE_ARRAY: scalar input returns false, array input recursively filtered
$c = filter_var($intOrArrayOfInt, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY]);
assertType('array<array<int>|int>|false', $c);

$d = filter_var($stringOrArrayOfString, FILTER_DEFAULT, ['flags' => FILTER_REQUIRE_ARRAY]);
assertType('array<array<string>|string>|false', $d);

// REQUIRE_ARRAY with NULL_ON_FAILURE
$e = filter_var($intOrArrayOfInt, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_NULL_ON_FAILURE]);
assertType('array<array<int>|int>|null', $e);

$f = filter_var($stringOrArrayOfString, FILTER_DEFAULT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_NULL_ON_FAILURE]);
assertType('array<array<string>|string>|null', $f);
}
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/nsrt/discussion-9134.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

$var = $_GET["data"];
$res = filter_var($var, FILTER_VALIDATE_INT, FILTER_REQUIRE_ARRAY);
assertType('array<int|false>|false', $res);
assertType('array<array|int|false>|false', $res);
if (is_array($res) === false) {
throw new \RuntimeException();
}
4 changes: 2 additions & 2 deletions tests/PHPStan/Analyser/nsrt/filter-input.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ public function doFoo(string $foo): void
assertType('int|false|null', filter_input(INPUT_GET, 'foo', FILTER_VALIDATE_INT));
assertType('int|false|null', filter_input(INPUT_GET, 'foo', FILTER_VALIDATE_INT, ['flags' => FILTER_NULL_ON_FAILURE]));
assertType("'invalid'|int|null", filter_input(INPUT_GET, 'foo', FILTER_VALIDATE_INT, ['options' => ['default' => 'invalid']]));
assertType('array<int|false>|null', filter_input(INPUT_GET, 'foo', FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY]));
assertType('array<int|null>|false', filter_input(INPUT_GET, 'foo', FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE]));
assertType('array<array|int|false>|null', filter_input(INPUT_GET, 'foo', FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY]));
assertType('array<array|int|null>|false', filter_input(INPUT_GET, 'foo', FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE]));
assertType('0|int<17, 19>|null', filter_input(INPUT_GET, 'foo', FILTER_VALIDATE_INT, ['options' => ['default' => 0, 'min_range' => 17, 'max_range' => 19]]));
}

Expand Down
24 changes: 12 additions & 12 deletions tests/PHPStan/Analyser/nsrt/filter-var.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,28 +23,28 @@ public function doFoo($mixed, array $stringMixedMap): void
assertType('null', filter_var(17, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_NULL_ON_FAILURE]));
assertType('false', filter_var('foo', FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY]));
assertType('null', filter_var('foo', FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_NULL_ON_FAILURE]));
assertType('array<int|false>|false', filter_var($mixed, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY]));
assertType('array<int|null>|null', filter_var($mixed, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_NULL_ON_FAILURE]));
assertType('array<string, int|false>', filter_var($stringMixedMap, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY]));
assertType('array<string, int|null>', filter_var($stringMixedMap, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_NULL_ON_FAILURE]));
assertType('array<array|int|false>|false', filter_var($mixed, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY]));
assertType('array<array|int|null>|null', filter_var($mixed, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_NULL_ON_FAILURE]));
assertType('array<string, array|int|false>', filter_var($stringMixedMap, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY]));
assertType('array<string, array|int|null>', filter_var($stringMixedMap, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_NULL_ON_FAILURE]));

assertType('array<17>', filter_var(17, FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY]));
assertType('array<17>', filter_var(17, FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE]));
assertType('array<false>', filter_var('foo', FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY]));
assertType('array<null>', filter_var('foo', FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE]));
assertType('array<int|false>', filter_var($mixed, FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY]));
assertType('array<int|null>', filter_var($mixed, FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE]));
assertType('array<string, int|false>', filter_var($stringMixedMap, FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY]));
assertType('array<string, int|null>', filter_var($stringMixedMap, FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE]));
assertType('array<array|int|false>', filter_var($mixed, FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY]));
assertType('array<array|int|null>', filter_var($mixed, FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE]));
assertType('array<string, array|int|false>', filter_var($stringMixedMap, FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY]));
assertType('array<string, array|int|null>', filter_var($stringMixedMap, FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE]));

assertType('false', filter_var(17, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_FORCE_ARRAY]));
assertType('null', filter_var(17, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE]));
assertType('false', filter_var('foo', FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_FORCE_ARRAY]));
assertType('null', filter_var('foo', FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE]));
assertType('array<int|false>|false', filter_var($mixed, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_FORCE_ARRAY]));
assertType('array<int|null>|null', filter_var($mixed, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE]));
assertType('array<string, int|false>', filter_var($stringMixedMap, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_FORCE_ARRAY]));
assertType('array<string, int|null>', filter_var($stringMixedMap, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE]));
assertType('array<array|int|false>|false', filter_var($mixed, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_FORCE_ARRAY]));
assertType('array<array|int|null>|null', filter_var($mixed, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE]));
assertType('array<string, array|int|false>', filter_var($stringMixedMap, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_FORCE_ARRAY]));
assertType('array<string, array|int|null>', filter_var($stringMixedMap, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE]));

assertType('0|int<17, 19>', filter_var($mixed, FILTER_VALIDATE_INT, ['options' => ['default' => 0, 'min_range' => 17, 'max_range' => 19]]));

Expand Down
Loading
Loading