|
32 | 32 | use PHPStan\Reflection\ExtendedMethodReflection; |
33 | 33 | use PHPStan\Reflection\ExtendedParameterReflection; |
34 | 34 | use PHPStan\Reflection\FunctionReflection; |
35 | | -use PHPStan\Reflection\InitializerExprContext; |
36 | | -use PHPStan\Reflection\InitializerExprTypeResolver; |
37 | 35 | use PHPStan\Reflection\MethodReflection; |
38 | 36 | use PHPStan\Reflection\ParameterReflection; |
39 | 37 | use PHPStan\Reflection\ParametersAcceptor; |
@@ -125,7 +123,6 @@ public function __construct( |
125 | 123 | private AssignHandler $assignHandler, |
126 | 124 | private ClosureTypeResolver $closureTypeResolver, |
127 | 125 | private ClosureParameterResolver $closureParameterResolver, |
128 | | - private InitializerExprTypeResolver $initializerExprTypeResolver, |
129 | 126 | ) |
130 | 127 | { |
131 | 128 | } |
@@ -242,94 +239,78 @@ public function processArgs( |
242 | 239 | ? $this->gatherClosureArgType($parametersAcceptors, $i, $arg->value, $scope) |
243 | 240 | : $this->closureTypeResolver->getClosureType($scope, $arg->value, true, $storage); |
244 | 241 | $this->addGatheredArgType($gatheredTypes, $gatheredUnpack, $gatheredHasName, $originalArgForGather, $i, $gatheredArgTypeByIndex[$i]); |
245 | | - } elseif ( |
246 | | - $argMetadataIsTypeDriven |
247 | | - && !$arg->unpack |
248 | | - && $arg->value instanceof Expr\Array_ |
249 | | - && $this->argConsumesResolvedParameterType($arg->value) |
250 | | - ) { |
251 | | - // An array literal holding closures decides the templates of its own |
252 | | - // parameter through its keys and its non-closure values - and those |
253 | | - // very templates type the closures nested in it. Pin a SKELETON of the |
254 | | - // array (declared closure signatures, scope-known leaves, mixed for |
255 | | - // anything that needs a walk) so the per-argument resolution below sees |
256 | | - // them; the walk that follows overwrites it with the real type. |
257 | | - $gatheredArgTypeByIndex[$i] = $this->gatherArrayArgTypeSkeleton($nodeScopeResolver, $arg->value, $scope); |
258 | 242 | } |
259 | 243 |
|
260 | 244 | $argMetadataAcceptor = $metadataAcceptor; |
| 245 | + $passedToTypeResolver = null; |
261 | 246 | if ($metadataAcceptor !== null && $argMetadataIsTypeDriven) { |
262 | | - if ($this->argConsumesResolvedParameterType($arg->value)) { |
263 | | - // Resolve the acceptor for this argument from the args gathered SO FAR, padded to the |
264 | | - // full argument count with mixed. Closures sort last and by-ref out-params follow the |
265 | | - // args that pin them, so determining siblings are already processed; the mixed pad keeps |
266 | | - // the argument COUNT correct so the by-ref/variadic variant stays stable (e.g. sscanf), |
267 | | - // while processed siblings resolve a generic callable(T) parameter. No forward read. |
268 | | - $paddedTypes = []; |
269 | | - $paddedUnpack = false; |
270 | | - $paddedHasName = false; |
271 | | - foreach ($args as $j => $paddedArg) { |
272 | | - $paddedOriginalArg = $paddedArg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $paddedArg; |
273 | | - $this->addGatheredArgType($paddedTypes, $paddedUnpack, $paddedHasName, $paddedOriginalArg, $j, $gatheredArgTypeByIndex[$j] ?? new MixedType()); |
274 | | - } |
275 | | - $argMetadataAcceptor = $this->selectArgsMetadataAcceptor($nodeScopeResolver, $args, $paddedTypes, $parametersAcceptors, $namedArgumentsVariants, $paddedHasName, $paddedUnpack, $scope); |
276 | | - } else { |
| 247 | + if (!$this->argConsumesResolvedParameterType($arg->value)) { |
277 | 248 | // Only a closure/arrow function consumes the generic-RESOLVED |
278 | 249 | // parameter type: its body is inferred from the resolved |
279 | 250 | // callable(T) - directly, or through the in-function-call stack |
280 | 251 | // when nested anywhere inside the argument. Every other argument |
281 | 252 | // reads variant-stable facts off its parameter (by-ref flag, |
282 | 253 | // callable bookkeeping), so one all-mixed count-stable selection |
283 | 254 | // serves them all instead of a full template inference per argument. |
284 | | - if ($countStableMetadataAcceptor === null) { |
| 255 | + $countStableMetadataAcceptor ??= $this->selectCountStableMetadataAcceptor($nodeScopeResolver, $args, $parametersAcceptors, $namedArgumentsVariants, $scope); |
| 256 | + $argMetadataAcceptor = $countStableMetadataAcceptor; |
| 257 | + } elseif (!$arg->unpack && $arg->value instanceof Expr\Array_) { |
| 258 | + // An array literal holding closures decides the templates of its |
| 259 | + // own parameter through its keys and its other values - and those |
| 260 | + // very templates type the closures in it. ArrayHandler walks the |
| 261 | + // other items first and resolves the parameter from their types; |
| 262 | + // the argument's own metadata is variant-stable - the declared |
| 263 | + // parameter of a single variant, as gatherClosureArgType() pushes. |
| 264 | + $passedToTypeResolver = function (Type $arrayType) use ($nodeScopeResolver, $args, $arg, $i, $gatheredArgTypeByIndex, $parametersAcceptors, $namedArgumentsVariants, $scope): array { |
285 | 265 | $paddedTypes = []; |
286 | 266 | $paddedUnpack = false; |
287 | 267 | $paddedHasName = false; |
288 | 268 | foreach ($args as $j => $paddedArg) { |
289 | 269 | $paddedOriginalArg = $paddedArg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $paddedArg; |
290 | | - $this->addGatheredArgType($paddedTypes, $paddedUnpack, $paddedHasName, $paddedOriginalArg, $j, new MixedType()); |
| 270 | + $this->addGatheredArgType($paddedTypes, $paddedUnpack, $paddedHasName, $paddedOriginalArg, $j, $j === $i ? $arrayType : ($gatheredArgTypeByIndex[$j] ?? new MixedType())); |
| 271 | + } |
| 272 | + $parameter = $this->matchArgParameter( |
| 273 | + $this->selectArgsMetadataAcceptor($nodeScopeResolver, $args, $paddedTypes, $parametersAcceptors, $namedArgumentsVariants, $paddedHasName, $paddedUnpack, $scope), |
| 274 | + $arg, |
| 275 | + $i, |
| 276 | + ); |
| 277 | + if ($parameter === null) { |
| 278 | + return [null, null]; |
291 | 279 | } |
292 | | - $countStableMetadataAcceptor = $this->selectArgsMetadataAcceptor($nodeScopeResolver, $args, $paddedTypes, $parametersAcceptors, $namedArgumentsVariants, $paddedHasName, $paddedUnpack, $scope); |
| 280 | + |
| 281 | + return [$parameter->getType(), $parameter instanceof ExtendedParameterReflection ? $parameter->getNativeType() : null]; |
| 282 | + }; |
| 283 | + if (count($parametersAcceptors) > 1 || $namedArgumentsVariants !== null) { |
| 284 | + $countStableMetadataAcceptor ??= $this->selectCountStableMetadataAcceptor($nodeScopeResolver, $args, $parametersAcceptors, $namedArgumentsVariants, $scope); |
| 285 | + $argMetadataAcceptor = $countStableMetadataAcceptor; |
293 | 286 | } |
294 | | - $argMetadataAcceptor = $countStableMetadataAcceptor; |
| 287 | + } else { |
| 288 | + // Resolve the acceptor for this argument from the args gathered SO FAR, padded to the |
| 289 | + // full argument count with mixed. Closures sort last and by-ref out-params follow the |
| 290 | + // args that pin them, so determining siblings are already processed; the mixed pad keeps |
| 291 | + // the argument COUNT correct so the by-ref/variadic variant stays stable (e.g. sscanf), |
| 292 | + // while processed siblings resolve a generic callable(T) parameter. No forward read. |
| 293 | + $paddedTypes = []; |
| 294 | + $paddedUnpack = false; |
| 295 | + $paddedHasName = false; |
| 296 | + foreach ($args as $j => $paddedArg) { |
| 297 | + $paddedOriginalArg = $paddedArg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $paddedArg; |
| 298 | + $this->addGatheredArgType($paddedTypes, $paddedUnpack, $paddedHasName, $paddedOriginalArg, $j, $gatheredArgTypeByIndex[$j] ?? new MixedType()); |
| 299 | + } |
| 300 | + $argMetadataAcceptor = $this->selectArgsMetadataAcceptor($nodeScopeResolver, $args, $paddedTypes, $parametersAcceptors, $namedArgumentsVariants, $paddedHasName, $paddedUnpack, $scope); |
295 | 301 | } |
296 | 302 | } |
297 | | - $parameters = $argMetadataAcceptor !== null ? $argMetadataAcceptor->getParameters() : null; |
298 | 303 |
|
299 | 304 | $assignByReference = false; |
300 | | - $parameter = null; |
| 305 | + $parameter = $argMetadataAcceptor !== null ? $this->matchArgParameter($argMetadataAcceptor, $arg, $i) : null; |
301 | 306 | $parameterType = null; |
302 | 307 | $parameterNativeType = null; |
303 | | - if ($parameters !== null) { |
304 | | - $matchedParameter = null; |
305 | | - if ($arg->name !== null) { |
306 | | - foreach ($parameters as $p) { |
307 | | - if ($p->getName() === $arg->name->toString()) { |
308 | | - $matchedParameter = $p; |
309 | | - break; |
310 | | - } |
311 | | - } |
312 | | - } elseif (isset($parameters[$i])) { |
313 | | - $matchedParameter = $parameters[$i]; |
314 | | - } |
315 | | - |
316 | | - if ($matchedParameter !== null) { |
317 | | - $assignByReference = $matchedParameter->passedByReference()->createsNewVariable(); |
318 | | - $parameterType = $matchedParameter->getType(); |
| 308 | + if ($parameter !== null) { |
| 309 | + $assignByReference = $parameter->passedByReference()->createsNewVariable(); |
| 310 | + $parameterType = $parameter->getType(); |
319 | 311 |
|
320 | | - if ($matchedParameter instanceof ExtendedParameterReflection) { |
321 | | - $parameterNativeType = $matchedParameter->getNativeType(); |
322 | | - } |
323 | | - $parameter = $matchedParameter; |
324 | | - } elseif (count($parameters) > 0 && $argMetadataAcceptor->isVariadic()) { |
325 | | - $lastParameter = array_last($parameters); |
326 | | - $assignByReference = $lastParameter->passedByReference()->createsNewVariable(); |
327 | | - $parameterType = $lastParameter->getType(); |
328 | | - |
329 | | - if ($lastParameter instanceof ExtendedParameterReflection) { |
330 | | - $parameterNativeType = $lastParameter->getNativeType(); |
331 | | - } |
332 | | - $parameter = $lastParameter; |
| 312 | + if ($parameter instanceof ExtendedParameterReflection) { |
| 313 | + $parameterNativeType = $parameter->getNativeType(); |
333 | 314 | } |
334 | 315 | } |
335 | 316 |
|
@@ -645,6 +626,9 @@ public function processArgs( |
645 | 626 | $argContext = $context->enterDeep(); |
646 | 627 | if (!$arg->unpack && $arg->value instanceof Expr\Array_) { |
647 | 628 | $argContext = $argContext->enterPassedToType($parameterType, $parameterNativeType); |
| 629 | + if ($passedToTypeResolver !== null) { |
| 630 | + $argContext = $argContext->enterPassedToTypeResolver($passedToTypeResolver); |
| 631 | + } |
648 | 632 | } |
649 | 633 | $exprResult = $nodeScopeResolver->processExprNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $argContext); |
650 | 634 | $argResults[spl_object_id($arg->value)] = $exprResult; |
@@ -991,37 +975,45 @@ private function gatherClosureArgType(array $parametersAcceptors, int $i, Expr $ |
991 | 975 | } |
992 | 976 |
|
993 | 977 | /** |
994 | | - * A structural stand-in for an array literal argument that holds closures, |
995 | | - * built without walking anything: a nested array literal recurses, a closure / |
996 | | - * arrow function contributes its DECLARED signature |
997 | | - * (ClosureTypeResolver::getDeclaredClosureType()), and every other key/value |
998 | | - * is priced by the scope state it is already tracked as, falling back to the |
999 | | - * constant-expression resolver (literals, ::class, constants, concatenation) |
1000 | | - * and ultimately to mixed. Widening a slot to mixed is safe: a template is |
1001 | | - * never resolved below its bound, so the skeleton can only ever sharpen the |
1002 | | - * resolution. |
1003 | | - * |
1004 | | - * Unlike gatherClosureArgType() this type never reaches $gatheredTypes: it |
1005 | | - * exists solely to resolve the parameter type the nested closures are typed |
1006 | | - * from. The argument's real type replaces it once the walk is done. |
| 978 | + * @param Node\Arg[] $args |
| 979 | + * @param ParametersAcceptor[] $parametersAcceptors |
| 980 | + * @param ParametersAcceptor[]|null $namedArgumentsVariants |
1007 | 981 | */ |
1008 | | - private function gatherArrayArgTypeSkeleton(NodeScopeResolver $nodeScopeResolver, Expr\Array_ $expr, MutatingScope $scope): Type |
| 982 | + private function selectCountStableMetadataAcceptor(NodeScopeResolver $nodeScopeResolver, array $args, array $parametersAcceptors, ?array $namedArgumentsVariants, MutatingScope $scope): ParametersAcceptor |
1009 | 983 | { |
1010 | | - $initializerContext = InitializerExprContext::fromScope($scope); |
1011 | | - $getType = function (Expr $inner) use (&$getType, $nodeScopeResolver, $scope, $initializerContext): Type { |
1012 | | - if ($inner instanceof Expr\Closure || $inner instanceof Expr\ArrowFunction) { |
1013 | | - return $this->closureTypeResolver->getDeclaredClosureType($scope, $inner); |
1014 | | - } |
| 984 | + $paddedTypes = []; |
| 985 | + $paddedUnpack = false; |
| 986 | + $paddedHasName = false; |
| 987 | + foreach ($args as $j => $paddedArg) { |
| 988 | + $paddedOriginalArg = $paddedArg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $paddedArg; |
| 989 | + $this->addGatheredArgType($paddedTypes, $paddedUnpack, $paddedHasName, $paddedOriginalArg, $j, new MixedType()); |
| 990 | + } |
1015 | 991 |
|
1016 | | - if ($inner instanceof Expr\Array_) { |
1017 | | - return $this->initializerExprTypeResolver->getArrayType($inner, $getType); |
| 992 | + return $this->selectArgsMetadataAcceptor($nodeScopeResolver, $args, $paddedTypes, $parametersAcceptors, $namedArgumentsVariants, $paddedHasName, $paddedUnpack, $scope); |
| 993 | + } |
| 994 | + |
| 995 | + /** |
| 996 | + * The parameter the argument is passed to: by name, by position, or the |
| 997 | + * variadic one. |
| 998 | + */ |
| 999 | + private function matchArgParameter(ParametersAcceptor $acceptor, Node\Arg $arg, int $i): ?ParameterReflection |
| 1000 | + { |
| 1001 | + $parameters = $acceptor->getParameters(); |
| 1002 | + if ($arg->name !== null) { |
| 1003 | + foreach ($parameters as $parameter) { |
| 1004 | + if ($parameter->getName() === $arg->name->toString()) { |
| 1005 | + return $parameter; |
| 1006 | + } |
1018 | 1007 | } |
| 1008 | + } elseif (isset($parameters[$i])) { |
| 1009 | + return $parameters[$i]; |
| 1010 | + } |
1019 | 1011 |
|
1020 | | - return $nodeScopeResolver->findScopeStateType($inner, $scope) |
1021 | | - ?? $this->initializerExprTypeResolver->getType($inner, $initializerContext); |
1022 | | - }; |
| 1012 | + if (count($parameters) > 0 && $acceptor->isVariadic()) { |
| 1013 | + return array_last($parameters); |
| 1014 | + } |
1023 | 1015 |
|
1024 | | - return $this->initializerExprTypeResolver->getArrayType($expr, $getType); |
| 1016 | + return null; |
1025 | 1017 | } |
1026 | 1018 |
|
1027 | 1019 | /** |
|
0 commit comments