Skip to content

Commit 9d9f614

Browse files
phpstan-botclaude
andcommitted
Simplify nested array handling using TypeTraverser::map
Replace the manual addNestedArrayType method with TypeTraverser::map to recursively filter each component of the array value type. Extract filterScalarType method for cleaner separation of scalar filtering logic from array wrapping logic. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6eaa2ba commit 9d9f614

1 file changed

Lines changed: 51 additions & 62 deletions

File tree

src/Type/Php/FilterFunctionReturnTypeHelper.php

Lines changed: 51 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
use PHPStan\Type\StringType;
2828
use PHPStan\Type\Type;
2929
use PHPStan\Type\TypeCombinator;
30+
use PHPStan\Type\TypeTraverser;
3031
use PHPStan\Type\UnionType;
3132
use function array_key_exists;
3233
use function array_merge;
@@ -165,23 +166,62 @@ public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType): T
165166
return $mixedType;
166167
}
167168

168-
$inputValueType = null;
169+
// When input is definitely an array with REQUIRE/FORCE_ARRAY,
170+
// use TypeTraverser to recursively filter each component of the value type
169171
if ($inputIsArray->yes() && ($hasRequireArrayFlag->yes() || $hasForceArrayFlag->yes())) {
170172
$inputArrayKeyType = $inputType->getIterableKeyType();
171-
$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;
173+
$inputValueType = $inputType->getIterableValueType();
174+
175+
$filteredValueType = TypeTraverser::map($inputValueType, function (Type $type, callable $traverse) use ($filterType, $flagsType, $filterValue, $defaultType, $mixedType, $hasOptions, $options): Type {
176+
if ($type instanceof UnionType || $type instanceof IntersectionType) {
177+
return $traverse($type);
178+
}
179+
if ($type->isArray()->yes()) {
180+
return $this->getType($type, $filterType, $flagsType);
181181
}
182+
$scalarResult = $this->filterScalarType($type, $filterValue, $defaultType, $flagsType, $mixedType, $hasOptions, $options);
183+
if ($type->isArray()->maybe()) {
184+
return TypeCombinator::union($scalarResult, new ArrayType($mixedType, $mixedType));
185+
}
186+
return $scalarResult;
187+
});
188+
189+
return new ArrayType($inputArrayKeyType, $filteredValueType);
190+
}
191+
192+
$type = $this->filterScalarType($inputType, $filterValue, $defaultType, $flagsType, $mixedType, $hasOptions, $options);
193+
194+
if ($hasRequireArrayFlag->yes()) {
195+
if (!$inputIsArray->no()) {
196+
$type = TypeCombinator::union($type, new ArrayType($mixedType, $mixedType));
197+
}
198+
$type = new ArrayType($mixedType, $type);
199+
if (!$inputIsArray->yes()) {
200+
$type = TypeCombinator::union($type, $defaultType);
182201
}
183202
}
184203

204+
if ($hasRequireArrayFlag->no() && $hasForceArrayFlag->yes()) {
205+
if (!$inputIsArray->no()) {
206+
$type = TypeCombinator::union($type, new ArrayType($mixedType, $mixedType));
207+
}
208+
return new ArrayType($mixedType, $type);
209+
}
210+
211+
if ($this->hasFlag('FILTER_THROW_ON_FAILURE', $flagsType)->yes()) {
212+
$type = TypeCombinator::remove($type, $defaultType);
213+
}
214+
215+
return $type;
216+
}
217+
218+
/**
219+
* Applies the filter to a scalar input type (no array wrapping).
220+
*
221+
* @param array<string, ?Type> $options
222+
*/
223+
private function filterScalarType(Type $inputType, int $filterValue, Type $defaultType, ?Type $flagsType, MixedType $mixedType, TrinaryLogic $hasOptions, array $options): Type
224+
{
185225
if ($inputType->isScalar()->no() && $inputType->isNull()->no()) {
186226
$exactType = $defaultType;
187227
} else {
@@ -209,57 +249,6 @@ public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType): T
209249
}
210250
}
211251

212-
if ($hasRequireArrayFlag->yes()) {
213-
$type = $this->addNestedArrayType($type, $inputValueType, $inputIsArray, $filterType, $flagsType, $mixedType);
214-
$type = new ArrayType($inputArrayKeyType ?? $mixedType, $type);
215-
if (!$inputIsArray->yes()) {
216-
$type = TypeCombinator::union($type, $defaultType);
217-
}
218-
}
219-
220-
if ($hasRequireArrayFlag->no() && $hasForceArrayFlag->yes()) {
221-
$type = $this->addNestedArrayType($type, $inputValueType, $inputIsArray, $filterType, $flagsType, $mixedType);
222-
return new ArrayType($inputArrayKeyType ?? $mixedType, $type);
223-
}
224-
225-
if ($this->hasFlag('FILTER_THROW_ON_FAILURE', $flagsType)->yes()) {
226-
$type = TypeCombinator::remove($type, $defaultType);
227-
}
228-
229-
return $type;
230-
}
231-
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-
263252
return $type;
264253
}
265254

0 commit comments

Comments
 (0)