Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 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
69 changes: 53 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 @@ -165,11 +166,62 @@
return $mixedType;
}

// When input is definitely an array with REQUIRE/FORCE_ARRAY,
// use TypeTraverser to recursively filter each component of the value type
if ($inputIsArray->yes() && ($hasRequireArrayFlag->yes() || $hasForceArrayFlag->yes())) {
$inputArrayKeyType = $inputType->getIterableKeyType();
$inputType = $inputType->getIterableValueType();
$inputValueType = $inputType->getIterableValueType();

$filteredValueType = TypeTraverser::map($inputValueType, function (Type $type, callable $traverse) use ($filterType, $flagsType, $filterValue, $defaultType, $mixedType, $hasOptions, $options): Type {
if ($type instanceof UnionType || $type instanceof IntersectionType) {
return $traverse($type);
}
if ($type->isArray()->yes()) {
return $this->getType($type, $filterType, $flagsType);
}
$scalarResult = $this->filterScalarType($type, $filterValue, $defaultType, $flagsType, $mixedType, $hasOptions, $options);
if ($type->isArray()->maybe()) {
return TypeCombinator::union($scalarResult, new ArrayType($mixedType, $mixedType));
}
return $scalarResult;
});

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

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

if ($hasRequireArrayFlag->yes()) {

Check warning on line 194 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 ($hasRequireArrayFlag->yes()) { + if (!$hasRequireArrayFlag->no()) { if (!$inputIsArray->no()) { $type = TypeCombinator::union($type, new ArrayType($mixedType, $mixedType)); }

Check warning on line 194 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 ($hasRequireArrayFlag->yes()) { + if (!$hasRequireArrayFlag->no()) { if (!$inputIsArray->no()) { $type = TypeCombinator::union($type, new ArrayType($mixedType, $mixedType)); }
if (!$inputIsArray->no()) {
$type = TypeCombinator::union($type, new ArrayType($mixedType, $mixedType));
}
$type = new ArrayType($mixedType, $type);
if (!$inputIsArray->yes()) {
$type = TypeCombinator::union($type, $defaultType);
}
}

if ($hasRequireArrayFlag->no() && $hasForceArrayFlag->yes()) {

Check warning on line 204 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": @@ @@ } } - if ($hasRequireArrayFlag->no() && $hasForceArrayFlag->yes()) { + if ($hasRequireArrayFlag->no() && !$hasForceArrayFlag->no()) { if (!$inputIsArray->no()) { $type = TypeCombinator::union($type, new ArrayType($mixedType, $mixedType)); }

Check warning on line 204 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": @@ @@ } } - if ($hasRequireArrayFlag->no() && $hasForceArrayFlag->yes()) { + if (!$hasRequireArrayFlag->yes() && $hasForceArrayFlag->yes()) { if (!$inputIsArray->no()) { $type = TypeCombinator::union($type, new ArrayType($mixedType, $mixedType)); }

Check warning on line 204 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": @@ @@ } } - if ($hasRequireArrayFlag->no() && $hasForceArrayFlag->yes()) { + if ($hasRequireArrayFlag->no() && !$hasForceArrayFlag->no()) { if (!$inputIsArray->no()) { $type = TypeCombinator::union($type, new ArrayType($mixedType, $mixedType)); }

Check warning on line 204 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": @@ @@ } } - if ($hasRequireArrayFlag->no() && $hasForceArrayFlag->yes()) { + if (!$hasRequireArrayFlag->yes() && $hasForceArrayFlag->yes()) { if (!$inputIsArray->no()) { $type = TypeCombinator::union($type, new ArrayType($mixedType, $mixedType)); }
if (!$inputIsArray->no()) {
$type = TypeCombinator::union($type, new ArrayType($mixedType, $mixedType));
}
return new ArrayType($mixedType, $type);
}

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

Check warning on line 211 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($mixedType, $type); } - 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 211 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($mixedType, $type); } - 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,21 +249,6 @@
}
}

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;
}

Expand Down
62 changes: 62 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-11339.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?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);
}
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