Skip to content

Commit 6eaa2ba

Browse files
phpstan-botclaude
andcommitted
Improve precision of filter_var/filter_input return types for typed array inputs
When the input array has a known value type (e.g. array<int>, array<array<int>>), use that type information instead of always adding generic array to the value type. - When value type is definitely not array (e.g. array<int>): don't add array - When value type is definitely array (e.g. array<array<int>>): recursively compute the precise nested filtered type - When value type is a union of scalar and array (e.g. array<int|array<int>>): extract scalar/array parts and handle each precisely - For mixed/unknown value types: fall back to array<mixed, mixed> as before Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 1eecbdb commit 6eaa2ba

2 files changed

Lines changed: 84 additions & 6 deletions

File tree

src/Type/Php/FilterFunctionReturnTypeHelper.php

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -165,9 +165,21 @@ public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType): T
165165
return $mixedType;
166166
}
167167

168+
$inputValueType = null;
168169
if ($inputIsArray->yes() && ($hasRequireArrayFlag->yes() || $hasForceArrayFlag->yes())) {
169170
$inputArrayKeyType = $inputType->getIterableKeyType();
170171
$inputType = $inputType->getIterableValueType();
172+
$inputValueType = $inputType;
173+
174+
// When the value type is a union of scalar and array types (e.g. int|array<int>),
175+
// only use the scalar part for scalar filtering - array parts are handled separately
176+
// via recursive filtering in addNestedArrayType
177+
if ($inputType->isArray()->maybe()) {
178+
$scalarPart = TypeCombinator::remove($inputType, new ArrayType($mixedType, $mixedType));
179+
if (!$scalarPart instanceof NeverType) {
180+
$inputType = $scalarPart;
181+
}
182+
}
171183
}
172184

173185
if ($inputType->isScalar()->no() && $inputType->isNull()->no()) {
@@ -198,19 +210,15 @@ public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType): T
198210
}
199211

200212
if ($hasRequireArrayFlag->yes()) {
201-
if (!$inputIsArray->no()) {
202-
$type = TypeCombinator::union($type, new ArrayType($mixedType, $mixedType));
203-
}
213+
$type = $this->addNestedArrayType($type, $inputValueType, $inputIsArray, $filterType, $flagsType, $mixedType);
204214
$type = new ArrayType($inputArrayKeyType ?? $mixedType, $type);
205215
if (!$inputIsArray->yes()) {
206216
$type = TypeCombinator::union($type, $defaultType);
207217
}
208218
}
209219

210220
if ($hasRequireArrayFlag->no() && $hasForceArrayFlag->yes()) {
211-
if (!$inputIsArray->no()) {
212-
$type = TypeCombinator::union($type, new ArrayType($mixedType, $mixedType));
213-
}
221+
$type = $this->addNestedArrayType($type, $inputValueType, $inputIsArray, $filterType, $flagsType, $mixedType);
214222
return new ArrayType($inputArrayKeyType ?? $mixedType, $type);
215223
}
216224

@@ -221,6 +229,40 @@ public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType): T
221229
return $type;
222230
}
223231

232+
private function addNestedArrayType(Type $type, ?Type $inputValueType, TrinaryLogic $inputIsArray, ?Type $filterType, ?Type $flagsType, MixedType $mixedType): Type
233+
{
234+
if ($inputValueType !== null) {
235+
// Input was unwrapped - check if the value type could be an array
236+
$valueTypeIsArray = $inputValueType->isArray();
237+
if ($valueTypeIsArray->yes()) {
238+
// Value type is definitely an array - recursively compute precise nested type
239+
// Replace $type entirely since the scalar filtering path produces incorrect
240+
// results for array inputs (arrays are recursively filtered, not failed)
241+
return $this->getType($inputValueType, $filterType, $flagsType);
242+
}
243+
if ($valueTypeIsArray->maybe()) {
244+
// Value type is a union of scalar and array types (e.g. int|array<int>)
245+
// Try to extract the array part for precise recursive filtering
246+
$inputArrayPart = TypeCombinator::intersect($inputValueType, new ArrayType($mixedType, $mixedType));
247+
if (!$inputArrayPart instanceof NeverType && !$inputArrayPart->getIterableValueType() instanceof MixedType) {
248+
$nestedType = $this->getType($inputArrayPart, $filterType, $flagsType);
249+
return TypeCombinator::union($type, $nestedType);
250+
}
251+
// Fall back to generic array for mixed-like value types
252+
return TypeCombinator::union($type, new ArrayType($mixedType, $mixedType));
253+
}
254+
// Value type is definitely not an array - don't add array to type
255+
return $type;
256+
}
257+
258+
// No unwrap happened (input is maybe-array, e.g. from filter_input with mixed)
259+
if (!$inputIsArray->no()) {
260+
return TypeCombinator::union($type, new ArrayType($mixedType, $mixedType));
261+
}
262+
263+
return $type;
264+
}
265+
224266
/**
225267
* @return array<int, Type>
226268
*/

tests/PHPStan/Analyser/nsrt/bug-11339.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,39 @@
2424

2525
$k = filter_var('foo', FILTER_VALIDATE_INT, FILTER_FORCE_ARRAY);
2626
assertType('array<false>', $k);
27+
28+
/**
29+
* @param array<int> $arrayOfInt
30+
* @param array<array<int>> $arrayOfArrayOfInt
31+
* @param array<int|array<int>> $arrayOfIntOrArrayOfInt
32+
*/
33+
function typedArrayInputs(array $arrayOfInt, array $arrayOfArrayOfInt, array $arrayOfIntOrArrayOfInt): void
34+
{
35+
// array<int> - values are ints, not arrays, so no array in value type
36+
$l = filter_var($arrayOfInt, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY]);
37+
assertType('array<int>', $l);
38+
39+
$m = filter_var($arrayOfInt, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_NULL_ON_FAILURE]);
40+
assertType('array<int>', $m);
41+
42+
// array<array<int>> - values are arrays, recursively filtered
43+
$n = filter_var($arrayOfArrayOfInt, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY]);
44+
assertType('array<array<int>>', $n);
45+
46+
$o = filter_var($arrayOfArrayOfInt, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_NULL_ON_FAILURE]);
47+
assertType('array<array<int>>', $o);
48+
49+
// array<int|array<int>> - mixed values, both scalar and array possible
50+
$p = filter_var($arrayOfIntOrArrayOfInt, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY]);
51+
assertType('array<array<int>|int>', $p);
52+
53+
$q = filter_var($arrayOfIntOrArrayOfInt, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_NULL_ON_FAILURE]);
54+
assertType('array<array<int>|int>', $q);
55+
56+
// FORCE_ARRAY with typed arrays
57+
$r = filter_var($arrayOfInt, FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY]);
58+
assertType('array<int>', $r);
59+
60+
$s = filter_var($arrayOfArrayOfInt, FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY]);
61+
assertType('array<array<int>>', $s);
62+
}

0 commit comments

Comments
 (0)