|
25 | 25 | use PHPStan\Node\ExecutionEndNode; |
26 | 26 | use PHPStan\Node\InvalidateExprNode; |
27 | 27 | use PHPStan\Node\PropertyAssignNode; |
| 28 | +use PHPStan\Parser\ArrayReduceArgVisitor; |
28 | 29 | use PHPStan\Reflection\Callables\SimpleImpurePoint; |
29 | 30 | use PHPStan\Reflection\Callables\SimpleThrowPoint; |
30 | 31 | use PHPStan\Reflection\ExtendedParameterReflection; |
|
33 | 34 | use PHPStan\Reflection\Native\NativeParameterReflection; |
34 | 35 | use PHPStan\Reflection\ParameterReflection; |
35 | 36 | use PHPStan\Reflection\PassedByReference; |
| 37 | +use PHPStan\Reflection\Php\DummyParameter; |
36 | 38 | use PHPStan\ShouldNotHappenException; |
37 | 39 | use PHPStan\TrinaryLogic; |
38 | 40 | use PHPStan\Turbo\ShadowedByTurboExtension; |
39 | 41 | use PHPStan\Type\ClosureType; |
| 42 | +use PHPStan\Type\GeneralizePrecision; |
40 | 43 | use PHPStan\Type\Generic\GenericObjectType; |
41 | 44 | use PHPStan\Type\Generic\TemplateTypeMap; |
42 | 45 | use PHPStan\Type\Generic\TemplateTypeVarianceMap; |
@@ -150,6 +153,104 @@ public function getClosureType( |
150 | 153 | ); |
151 | 154 | } |
152 | 155 |
|
| 156 | + $arrayReduceArgs = $expr->getAttribute(ArrayReduceArgVisitor::ATTRIBUTE_NAME); |
| 157 | + if ($arrayReduceArgs !== null) { |
| 158 | + return $this->resolveArrayReduceCallbackType($scope, $expr, $storage, $arrayReduceArgs, $parameters, $isVariadic, $contextFree); |
| 159 | + } |
| 160 | + |
| 161 | + return $this->resolveClosureTypeWithParameters($scope, $expr, $parameters, $isVariadic, $callableParameters, $nativeCallableParameters, $contextFree); |
| 162 | + } |
| 163 | + |
| 164 | + /** |
| 165 | + * Resolves an array_reduce() callback with a bounded fixed-point iteration |
| 166 | + * over its $carry parameter. |
| 167 | + * |
| 168 | + * The first pass types $carry as the initial argument. When the callback's return |
| 169 | + * type escapes it, the carry type is widened and verified in a second pass. When |
| 170 | + * even the widened type is not a fixed point, the callback is analysed once more |
| 171 | + * with an unknown carry instead of claiming a shape the carry can outgrow. |
| 172 | + * |
| 173 | + * @param array{Node\Arg, Node\Arg|null} $arrayReduceArgs |
| 174 | + * @param list<NativeParameterReflection> $parameters |
| 175 | + */ |
| 176 | + private function resolveArrayReduceCallbackType( |
| 177 | + MutatingScope $scope, |
| 178 | + Node\Expr\Closure|ArrowFunction $expr, |
| 179 | + ?ExpressionResultStorage $storage, |
| 180 | + array $arrayReduceArgs, |
| 181 | + array $parameters, |
| 182 | + bool $isVariadic, |
| 183 | + bool $contextFree, |
| 184 | + ): ClosureType |
| 185 | + { |
| 186 | + [$arrayArg, $initialArg] = $arrayReduceArgs; |
| 187 | + $itemType = $this->readExprType($storage, $arrayArg->value, $scope, false)->getIterableValueType(); |
| 188 | + $nativeCallableParameters = self::createArrayReduceCallbackParameters( |
| 189 | + new MixedType(), |
| 190 | + $this->readExprType($storage, $arrayArg->value, $scope->doNotTreatPhpDocTypesAsCertain(), true)->getIterableValueType(), |
| 191 | + ); |
| 192 | + |
| 193 | + $carryType = $initialArg !== null ? $this->readExprType($storage, $initialArg->value, $scope, false) : new NullType(); |
| 194 | + $closureType = $this->resolveClosureTypeWithParameters($scope, $expr, $parameters, $isVariadic, self::createArrayReduceCallbackParameters($carryType, $itemType), $nativeCallableParameters, $contextFree); |
| 195 | + if ($carryType->isSuperTypeOf($closureType->getReturnType())->yes()) { |
| 196 | + return $closureType; |
| 197 | + } |
| 198 | + |
| 199 | + $carryType = self::widenArrayReduceCarryType($carryType, $closureType->getReturnType()); |
| 200 | + $closureType = $this->resolveClosureTypeWithParameters($scope, $expr, $parameters, $isVariadic, self::createArrayReduceCallbackParameters($carryType, $itemType), $nativeCallableParameters, $contextFree); |
| 201 | + if ($carryType->isSuperTypeOf($closureType->getReturnType())->yes()) { |
| 202 | + return $closureType; |
| 203 | + } |
| 204 | + |
| 205 | + return $this->resolveClosureTypeWithParameters($scope, $expr, $parameters, $isVariadic, self::createArrayReduceCallbackParameters(new MixedType(), $itemType), $nativeCallableParameters, $contextFree); |
| 206 | + } |
| 207 | + |
| 208 | + /** |
| 209 | + * @return list<DummyParameter> |
| 210 | + */ |
| 211 | + private static function createArrayReduceCallbackParameters(Type $carryType, Type $itemType): array |
| 212 | + { |
| 213 | + return [ |
| 214 | + new DummyParameter('carry', $carryType, optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), |
| 215 | + new DummyParameter('item', $itemType, optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), |
| 216 | + ]; |
| 217 | + } |
| 218 | + |
| 219 | + /** Widens while preserving constant array shapes so that the second pass can converge. */ |
| 220 | + private static function widenArrayReduceCarryType(Type $initialType, Type $returnType): Type |
| 221 | + { |
| 222 | + $union = TypeCombinator::union($initialType, $returnType); |
| 223 | + $constantArrays = $union->getConstantArrays(); |
| 224 | + if (count($constantArrays) > 0) { |
| 225 | + $generalized = []; |
| 226 | + foreach ($constantArrays as $constantArray) { |
| 227 | + $generalized[] = $constantArray->generalizeValues(); |
| 228 | + } |
| 229 | + |
| 230 | + return TypeCombinator::union(...$generalized); |
| 231 | + } |
| 232 | + |
| 233 | + return $union->generalize(GeneralizePrecision::lessSpecific()); |
| 234 | + } |
| 235 | + |
| 236 | + /** |
| 237 | + * Walks the body with the given callable parameters, or answers from the |
| 238 | + * per-context cache. |
| 239 | + * |
| 240 | + * @param list<NativeParameterReflection> $parameters |
| 241 | + * @param ParameterReflection[]|null $callableParameters |
| 242 | + * @param ParameterReflection[]|null $nativeCallableParameters |
| 243 | + */ |
| 244 | + private function resolveClosureTypeWithParameters( |
| 245 | + MutatingScope $scope, |
| 246 | + Node\Expr\Closure|ArrowFunction $expr, |
| 247 | + array $parameters, |
| 248 | + bool $isVariadic, |
| 249 | + ?array $callableParameters, |
| 250 | + ?array $nativeCallableParameters, |
| 251 | + bool $contextFree, |
| 252 | + ): ClosureType |
| 253 | + { |
153 | 254 | $cachedTypes = $this->findCachedTypes($expr); |
154 | 255 | $cacheKey = $this->closureContextCacheKey($scope, $expr, $callableParameters, $parameters); |
155 | 256 | if (array_key_exists($cacheKey, $cachedTypes)) { |
|
0 commit comments