Skip to content

Commit 6568982

Browse files
phpstan-botclaude
andcommitted
Use TypeTraverser::map for nested array type in maybe-array input paths
Replace conservative `array<mixed, mixed>` with precise nested array type computation using TypeTraverser::map when input type carries array structure information (e.g., `int|array<int>`). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9d9f614 commit 6568982

2 files changed

Lines changed: 54 additions & 2 deletions

File tree

src/Type/Php/FilterFunctionReturnTypeHelper.php

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,8 @@ public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType): T
193193

194194
if ($hasRequireArrayFlag->yes()) {
195195
if (!$inputIsArray->no()) {
196-
$type = TypeCombinator::union($type, new ArrayType($mixedType, $mixedType));
196+
$nestedArrayType = $this->computeNestedArrayType($inputType, $filterType, $flagsType, $mixedType);
197+
$type = TypeCombinator::union($type, $nestedArrayType);
197198
}
198199
$type = new ArrayType($mixedType, $type);
199200
if (!$inputIsArray->yes()) {
@@ -203,7 +204,8 @@ public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType): T
203204

204205
if ($hasRequireArrayFlag->no() && $hasForceArrayFlag->yes()) {
205206
if (!$inputIsArray->no()) {
206-
$type = TypeCombinator::union($type, new ArrayType($mixedType, $mixedType));
207+
$nestedArrayType = $this->computeNestedArrayType($inputType, $filterType, $flagsType, $mixedType);
208+
$type = TypeCombinator::union($type, $nestedArrayType);
207209
}
208210
return new ArrayType($mixedType, $type);
209211
}
@@ -252,6 +254,26 @@ private function filterScalarType(Type $inputType, int $filterValue, Type $defau
252254
return $type;
253255
}
254256

257+
/**
258+
* Computes the nested array type for the array portion of a maybe-array input.
259+
* Uses TypeTraverser to extract array components and recursively filter them.
260+
*/
261+
private function computeNestedArrayType(Type $inputType, ?Type $filterType, ?Type $flagsType, MixedType $mixedType): Type
262+
{
263+
return TypeTraverser::map($inputType, function (Type $type, callable $traverse) use ($filterType, $flagsType, $mixedType): Type {
264+
if ($type instanceof UnionType || $type instanceof IntersectionType) {
265+
return $traverse($type);
266+
}
267+
if ($type->isArray()->yes()) {
268+
return $this->getType($type, $filterType, $flagsType);
269+
}
270+
if ($type->isArray()->maybe()) {
271+
return new ArrayType($mixedType, $mixedType);
272+
}
273+
return new NeverType();
274+
});
275+
}
276+
255277
/**
256278
* @return array<int, Type>
257279
*/

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,33 @@ function typedArrayInputs(array $arrayOfInt, array $arrayOfArrayOfInt, array $ar
6060
$s = filter_var($arrayOfArrayOfInt, FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY]);
6161
assertType('array<array<int>>', $s);
6262
}
63+
64+
/**
65+
* Union-typed inputs (scalar | array) exercise the inputIsArray->maybe() path
66+
*
67+
* @param int|array<int> $intOrArrayOfInt
68+
* @param string|array<string> $stringOrArrayOfString
69+
*/
70+
function unionTypedInputs(int|array $intOrArrayOfInt, string|array $stringOrArrayOfString): void
71+
{
72+
// FORCE_ARRAY: scalar part filtered, array part recursively filtered
73+
$a = filter_var($intOrArrayOfInt, FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY]);
74+
assertType('array<array<int>|int|false>', $a);
75+
76+
$b = filter_var($stringOrArrayOfString, FILTER_DEFAULT, ['flags' => FILTER_FORCE_ARRAY]);
77+
assertType('array<array<string>|string|false>', $b);
78+
79+
// REQUIRE_ARRAY: scalar input returns false, array input recursively filtered
80+
$c = filter_var($intOrArrayOfInt, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY]);
81+
assertType('array<array<int>|int|false>|false', $c);
82+
83+
$d = filter_var($stringOrArrayOfString, FILTER_DEFAULT, ['flags' => FILTER_REQUIRE_ARRAY]);
84+
assertType('array<array<string>|string|false>|false', $d);
85+
86+
// REQUIRE_ARRAY with NULL_ON_FAILURE
87+
$e = filter_var($intOrArrayOfInt, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_NULL_ON_FAILURE]);
88+
assertType('array<array<int>|int|null>|null', $e);
89+
90+
$f = filter_var($stringOrArrayOfString, FILTER_DEFAULT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_NULL_ON_FAILURE]);
91+
assertType('array<array<string>|string|null>|null', $f);
92+
}

0 commit comments

Comments
 (0)