|
27 | 27 | use PHPStan\Type\StringType; |
28 | 28 | use PHPStan\Type\Type; |
29 | 29 | use PHPStan\Type\TypeCombinator; |
| 30 | +use PHPStan\Type\TypeTraverser; |
30 | 31 | use PHPStan\Type\UnionType; |
31 | 32 | use function array_key_exists; |
32 | 33 | use function array_merge; |
@@ -165,23 +166,62 @@ public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType): T |
165 | 166 | return $mixedType; |
166 | 167 | } |
167 | 168 |
|
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 |
169 | 171 | if ($inputIsArray->yes() && ($hasRequireArrayFlag->yes() || $hasForceArrayFlag->yes())) { |
170 | 172 | $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); |
181 | 181 | } |
| 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); |
182 | 201 | } |
183 | 202 | } |
184 | 203 |
|
| 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 | + { |
185 | 225 | if ($inputType->isScalar()->no() && $inputType->isNull()->no()) { |
186 | 226 | $exactType = $defaultType; |
187 | 227 | } else { |
@@ -209,57 +249,6 @@ public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType): T |
209 | 249 | } |
210 | 250 | } |
211 | 251 |
|
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 | | - |
263 | 252 | return $type; |
264 | 253 | } |
265 | 254 |
|
|
0 commit comments