diff --git a/src/Analyser/ExprHandler/ArrowFunctionHandler.php b/src/Analyser/ExprHandler/ArrowFunctionHandler.php index 5390ee8e21..564d7e90ca 100644 --- a/src/Analyser/ExprHandler/ArrowFunctionHandler.php +++ b/src/Analyser/ExprHandler/ArrowFunctionHandler.php @@ -41,7 +41,9 @@ public function supports(Expr $expr): bool public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { - $result = $nodeScopeResolver->processArrowFunctionNode($stmt, $expr, $scope, $storage, $nodeCallback, null); + $arrowFunctionResult = $nodeScopeResolver->processArrowFunctionNode($stmt, $expr, $scope, $storage, $nodeCallback, null); + $this->closureTypeResolver->seedCacheFromArrowFunctionWalk($scope, $expr, $arrowFunctionResult, $storage); + $result = $arrowFunctionResult->getExpressionResult(); return $this->expressionResultFactory->create( $result->getScope(), diff --git a/src/Analyser/ExprHandler/ClosureHandler.php b/src/Analyser/ExprHandler/ClosureHandler.php index b107c5843c..683ebfc885 100644 --- a/src/Analyser/ExprHandler/ClosureHandler.php +++ b/src/Analyser/ExprHandler/ClosureHandler.php @@ -42,6 +42,7 @@ public function supports(Expr $expr): bool public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { $processClosureResult = $nodeScopeResolver->processClosureNode($stmt, $expr, $scope, $storage, $nodeCallback, $context, null); + $this->closureTypeResolver->seedCacheFromClosureWalk($scope, $expr, $processClosureResult, $storage); return $this->expressionResultFactory->create( $processClosureResult->applyByRefUseScope($processClosureResult->getScope()), diff --git a/src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php b/src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php index ff165386f7..b461708e79 100644 --- a/src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php +++ b/src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php @@ -11,8 +11,11 @@ use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ImpurePoint; +use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; +use PHPStan\Analyser\ProcessArrowFunctionResult; +use PHPStan\Analyser\ProcessClosureResult; use PHPStan\Analyser\Scope; use PHPStan\Analyser\StatementContext; use PHPStan\Analyser\ThrowPoint; @@ -25,7 +28,10 @@ use PHPStan\Reflection\Callables\SimpleImpurePoint; use PHPStan\Reflection\Callables\SimpleThrowPoint; use PHPStan\Reflection\ExtendedParameterReflection; +use PHPStan\Reflection\InitializerExprContext; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\Native\NativeParameterReflection; +use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\PassedByReference; use PHPStan\Reflection\Php\DummyParameter; use PHPStan\ShouldNotHappenException; @@ -38,12 +44,15 @@ use PHPStan\Type\MixedType; use PHPStan\Type\NonAcceptingNeverType; use PHPStan\Type\NullType; +use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\VerbosityLevel; use PHPStan\Type\VoidType; use function array_key_exists; use function array_map; use function array_merge; use function count; +use function implode; use function is_string; #[AutowiredService] @@ -54,137 +63,115 @@ final class ClosureTypeResolver public function __construct( private NodeScopeResolver $nodeScopeResolver, + private InitializerExprTypeResolver $initializerExprTypeResolver, ) { } + /** + * Resolves a closure/arrow function type by walking its body itself. Used by + * the paths that have no prior walk to read return/yield types from. A + * self-by-ref closure legitimately re-walks here (the + * $resolveClosureTypeDepth guard answers that ask). + * + * Callers that have already walked the body feed the gathered + * returns/yields to buildClosureTypeForClosure()/ + * buildClosureTypeForArrowFunction() instead, which construct the same + * ClosureType without a second walk. + */ public function getClosureType( MutatingScope $scope, Node\Expr\Closure|ArrowFunction $expr, ): ClosureType { - $parameters = []; - $isVariadic = false; - $firstOptionalParameterIndex = null; - foreach ($expr->params as $i => $param) { - $isOptionalCandidate = $param->default !== null || $param->variadic; + [$parameters, $isVariadic, $callableParameters, $nativeCallableParameters] = $this->buildParametersAndAcceptors($scope, $expr); - if ($isOptionalCandidate) { - if ($firstOptionalParameterIndex === null) { - $firstOptionalParameterIndex = $i; - } - } else { - $firstOptionalParameterIndex = null; - } + $cachedTypes = $expr->getAttribute('phpstanCachedTypes', []); + $cacheKey = $this->closureContextCacheKey($scope, $expr, $callableParameters, $parameters); + if (array_key_exists($cacheKey, $cachedTypes)) { + return $this->createClosureTypeFromCache($expr, $parameters, $isVariadic, $cachedTypes[$cacheKey]); } - - foreach ($expr->params as $i => $param) { - if ($param->variadic) { - $isVariadic = true; - } - if (!$param->var instanceof Variable || !is_string($param->var->name)) { - throw new ShouldNotHappenException(); - } - $parameters[] = new NativeParameterReflection( - $param->var->name, - $firstOptionalParameterIndex !== null && $i >= $firstOptionalParameterIndex, - $scope->getFunctionType($param->type, $scope->isParameterValueNullable($param), false), - $param->byRef - ? PassedByReference::createCreatesNewVariable() - : PassedByReference::createNo(), - $param->variadic, - $param->default !== null ? $scope->getType($param->default) : null, + if (self::$resolveClosureTypeDepth >= 2) { + return new ClosureType( + $parameters, + $scope->getFunctionType($expr->returnType, false, false), + $isVariadic, + isStatic: TrinaryLogic::createFromBoolean($expr->static), ); } - $callableParameters = null; - $nativeCallableParameters = null; - $arrayMapArgs = $expr->getAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME); - $immediatelyInvokedArgs = $expr->getAttribute(ImmediatelyInvokedClosureVisitor::ARGS_ATTRIBUTE_NAME); - if ($arrayMapArgs !== null) { - $callableParameters = []; - $nativeCallableParameters = []; - foreach ($arrayMapArgs as $funcCallArg) { - $callableParameters[] = new DummyParameter('item', $scope->getType($funcCallArg->value)->getIterableValueType(), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); - $nativeCallableParameters[] = new DummyParameter('item', $scope->getNativeType($funcCallArg->value)->getIterableValueType(), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); - } - } elseif ($immediatelyInvokedArgs !== null) { - foreach ($immediatelyInvokedArgs as $immediatelyInvokedArg) { - $callableParameters[] = new DummyParameter('item', $scope->getType($immediatelyInvokedArg->value), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); - $nativeCallableParameters[] = new DummyParameter('item', $scope->getNativeType($immediatelyInvokedArg->value), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); - } - } else { - $inFunctionCallsStackCount = count($scope->inFunctionCallsStack); - if ($inFunctionCallsStackCount > 0) { - [, $inParameter] = $scope->inFunctionCallsStack[$inFunctionCallsStackCount - 1]; - if ($inParameter !== null) { - $callableParameters = $this->nodeScopeResolver->createCallableParameters($scope, $expr, null, $inParameter->getType()); - $nativeType = $inParameter instanceof ExtendedParameterReflection ? $inParameter->getNativeType() : $inParameter->getType(); - $nativeCallableParameters = $this->nodeScopeResolver->createNativeCallableParameters($scope, $expr, null, $nativeType); - } - } - } - if ($expr instanceof ArrowFunction) { $arrowScope = $scope->enterArrowFunctionWithoutReflection($expr, $callableParameters, $nativeCallableParameters); - if ($expr->expr instanceof Yield_ || $expr->expr instanceof YieldFrom) { - $yieldNode = $expr->expr; + $arrowFunctionImpurePoints = []; + $invalidateExpressions = []; + self::$resolveClosureTypeDepth++; + try { + $arrowFunctionExprResult = $this->nodeScopeResolver->processExprNode( + new Node\Stmt\Expression($expr->expr), + $expr->expr, + $arrowScope, + new ExpressionResultStorage(), + static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpurePoints, &$invalidateExpressions): void { + if ($scope->getAnonymousFunctionReflection() !== $arrowScope->getAnonymousFunctionReflection()) { + return; + } - if ($yieldNode instanceof Yield_) { - if ($yieldNode->key === null) { - $keyType = new IntegerType(); - } else { - $keyType = $arrowScope->getType($yieldNode->key); - } + if ($node instanceof InvalidateExprNode) { + $invalidateExpressions[] = $node; + return; + } - if ($yieldNode->value === null) { - $valueType = new NullType(); - } else { - $valueType = $arrowScope->getType($yieldNode->value); - } - } else { - $yieldFromType = $arrowScope->getType($yieldNode->expr); - $keyType = $arrowScope->getIterableKeyType($yieldFromType); - $valueType = $arrowScope->getIterableValueType($yieldFromType); - } + if (!$node instanceof PropertyAssignNode) { + return; + } - $returnType = new GenericObjectType(Generator::class, [ - $keyType, - $valueType, - new MixedType(), - new VoidType(), - ]); - } else { - $returnType = $arrowScope->getKeepVoidType($expr->expr); - if ($expr->returnType !== null) { - $nativeReturnType = $scope->getFunctionType($expr->returnType, false, false); - $returnType = MutatingScope::intersectButNotNever($nativeReturnType, $returnType); - } + $arrowFunctionImpurePoints[] = new ImpurePoint( + $scope, + $node, + 'propertyAssign', + 'property assignment', + true, + ); + $invalidateExpressions[] = new InvalidateExprNode($node->getPropertyFetch()); + }, + ExpressionContext::createDeep(), + ); + } finally { + self::$resolveClosureTypeDepth--; } + $throwPoints = array_map(static fn ($throwPoint) => $throwPoint->toPublic(), $arrowFunctionExprResult->getThrowPoints()); + $impurePoints = array_merge($arrowFunctionImpurePoints, $arrowFunctionExprResult->getImpurePoints()); - $arrowFunctionImpurePoints = []; - $invalidateExpressions = []; - $arrowFunctionExprResult = $this->nodeScopeResolver->processExprNode( - new Node\Stmt\Expression($expr->expr), - $expr->expr, - $arrowScope, - new ExpressionResultStorage(), - static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpurePoints, &$invalidateExpressions): void { - if ($scope->getAnonymousFunctionReflection() !== $arrowScope->getAnonymousFunctionReflection()) { - return; - } + // the body was processed just above; resolve the return type from its stored + // result rather than reading the still-unprocessed body expression + $returnType = $this->resolveArrowFunctionReturnType($scope, $arrowScope, $expr); - if ($node instanceof InvalidateExprNode) { - $invalidateExpressions[] = $node; - return; - } + return $this->assembleClosureType($scope, $expr, $parameters, $isVariadic, $returnType, $throwPoints, $impurePoints, $invalidateExpressions, [], $cacheKey); + } - if (!$node instanceof PropertyAssignNode) { - return; - } + self::$resolveClosureTypeDepth++; + + $closureScope = $scope->enterAnonymousFunctionWithoutReflection($expr, $callableParameters, $nativeCallableParameters); + $closureReturnStatements = []; + $closureYieldStatements = []; + $closureExecutionEnds = []; + $closureImpurePoints = []; + $invalidateExpressions = []; + + try { + $closureStatementResult = $this->nodeScopeResolver->processStmtNodes($expr, $expr->stmts, $closureScope, static function (Node $node, Scope $scope) use ($closureScope, &$closureReturnStatements, &$closureYieldStatements, &$closureExecutionEnds, &$closureImpurePoints, &$invalidateExpressions): void { + if ($scope->getAnonymousFunctionReflection() !== $closureScope->getAnonymousFunctionReflection()) { + return; + } - $arrowFunctionImpurePoints[] = new ImpurePoint( + if ($node instanceof InvalidateExprNode) { + $invalidateExpressions[] = $node; + return; + } + + if ($node instanceof PropertyAssignNode) { + $closureImpurePoints[] = new ImpurePoint( $scope, $node, 'propertyAssign', @@ -192,219 +179,624 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu true, ); $invalidateExpressions[] = new InvalidateExprNode($node->getPropertyFetch()); - }, - ExpressionContext::createDeep(), - ); - $throwPoints = array_map(static fn ($throwPoint) => $throwPoint->toPublic(), $arrowFunctionExprResult->getThrowPoints()); - $impurePoints = array_merge($arrowFunctionImpurePoints, $arrowFunctionExprResult->getImpurePoints()); - $usedVariables = []; - } else { - $cachedTypes = $expr->getAttribute('phpstanCachedTypes', []); - $cacheKey = $scope->getClosureScopeCacheKey(); - if (array_key_exists($cacheKey, $cachedTypes)) { - $cachedClosureData = $cachedTypes[$cacheKey]; - - $mustUseReturnValue = TrinaryLogic::createNo(); - foreach ($expr->attrGroups as $attrGroup) { - foreach ($attrGroup->attrs as $attr) { - if ($attr->name->toLowerString() === 'nodiscard') { - $mustUseReturnValue = TrinaryLogic::createYes(); - break; - } - } + return; } - return new ClosureType( - $parameters, - $cachedClosureData['returnType'], - $isVariadic, - TemplateTypeMap::createEmpty(), - TemplateTypeMap::createEmpty(), - TemplateTypeVarianceMap::createEmpty(), - throwPoints: $cachedClosureData['throwPoints'], - impurePoints: $cachedClosureData['impurePoints'], - invalidateExpressions: $cachedClosureData['invalidateExpressions'], - usedVariables: $cachedClosureData['usedVariables'], - acceptsNamedArguments: TrinaryLogic::createYes(), - mustUseReturnValue: $mustUseReturnValue, - isStatic: TrinaryLogic::createFromBoolean($expr->static), - ); - } - if (self::$resolveClosureTypeDepth >= 2) { - return new ClosureType( - $parameters, - $scope->getFunctionType($expr->returnType, false, false), - $isVariadic, - isStatic: TrinaryLogic::createFromBoolean($expr->static), - ); - } + if ($node instanceof ExecutionEndNode) { + $closureExecutionEnds[] = $node; + return; + } - self::$resolveClosureTypeDepth++; + if ($node instanceof Node\Stmt\Return_) { + $closureReturnStatements[] = [$node, $scope]; + } - $closureScope = $scope->enterAnonymousFunctionWithoutReflection($expr, $callableParameters, $nativeCallableParameters); - $closureReturnStatements = []; - $closureYieldStatements = []; - $onlyNeverExecutionEnds = null; - $closureImpurePoints = []; - $invalidateExpressions = []; + if (!$node instanceof Yield_ && !$node instanceof YieldFrom) { + return; + } - try { - $closureStatementResult = $this->nodeScopeResolver->processStmtNodes($expr, $expr->stmts, $closureScope, static function (Node $node, Scope $scope) use ($closureScope, &$closureReturnStatements, &$closureYieldStatements, &$onlyNeverExecutionEnds, &$closureImpurePoints, &$invalidateExpressions): void { - if ($scope->getAnonymousFunctionReflection() !== $closureScope->getAnonymousFunctionReflection()) { - return; - } + $closureYieldStatements[] = [$node, $scope]; + }, StatementContext::createTopLevel()); + } finally { + self::$resolveClosureTypeDepth--; + } - if ($node instanceof InvalidateExprNode) { - $invalidateExpressions[] = $node; - return; - } + $throwPoints = $closureStatementResult->getThrowPoints(); + $impurePoints = array_merge($closureImpurePoints, $closureStatementResult->getImpurePoints()); - if ($node instanceof PropertyAssignNode) { - $closureImpurePoints[] = new ImpurePoint( - $scope, - $node, - 'propertyAssign', - 'property assignment', - true, - ); - $invalidateExpressions[] = new InvalidateExprNode($node->getPropertyFetch()); - return; - } + return $this->buildClosureTypeFromClosureWalk( + $scope, + $expr, + $parameters, + $isVariadic, + $closureReturnStatements, + $closureYieldStatements, + $closureExecutionEnds, + $throwPoints, + $impurePoints, + $invalidateExpressions, + $cacheKey, + ); + } - if ($node instanceof ExecutionEndNode) { - if ($node->getStatementResult()->isAlwaysTerminating()) { - foreach ($node->getStatementResult()->getExitPoints() as $exitPoint) { - if ($exitPoint->getStatement() instanceof Node\Stmt\Return_) { - $onlyNeverExecutionEnds = false; - continue; - } - - if ($onlyNeverExecutionEnds === null) { - $onlyNeverExecutionEnds = true; - } - - break; - } - - if (count($node->getStatementResult()->getExitPoints()) === 0) { - if ($onlyNeverExecutionEnds === null) { - $onlyNeverExecutionEnds = true; - } - } - } else { - $onlyNeverExecutionEnds = false; - } + /** + * Constructs a closure type from data the engine already gathered while + * walking the body once (see NodeScopeResolver::processClosureNode()), + * without a second walk. The return/yield expression types are read from + * their stored results. + * + * @param list $returnStatements + * @param list $yieldStatements + * @param list $executionEnds + * @param InternalThrowPoint[] $throwPoints the single body walk's internal throw points + * @param ImpurePoint[] $impurePoints already merged (property-assign impure points + statement result impure points) + * @param InvalidateExprNode[] $invalidateExpressions + */ + public function buildClosureTypeForClosure( + MutatingScope $scope, + Node\Expr\Closure $expr, + array $returnStatements, + array $yieldStatements, + array $executionEnds, + array $throwPoints, + array $impurePoints, + array $invalidateExpressions, + bool $native = false, + ): ClosureType + { + if ($this->bodyWalkHasOwnParameterTypes($expr)) { + return $this->getClosureType($native ? $scope->doNotTreatPhpDocTypesAsCertain() : $scope, $expr); + } - return; - } + [$parameters, $isVariadic, $callableParameters, $nativeCallableParameters] = $this->buildParametersAndAcceptors($scope, $expr); - if ($node instanceof Node\Stmt\Return_) { - $closureReturnStatements[] = [$node, $scope]; - } + return $this->buildClosureTypeFromClosureWalk( + $scope, + $expr, + $parameters, + $isVariadic, + $returnStatements, + $yieldStatements, + $executionEnds, + array_map(static fn (InternalThrowPoint $throwPoint) => $throwPoint->toPublic(), $throwPoints), + $impurePoints, + $invalidateExpressions, + // the flavour-correct key both keeps the native build from clobbering + // the phpdoc cache slot and lets a later getClosureType() ask on the + // promoted scope answer from this build instead of re-walking + $this->closureContextCacheKey( + $native ? $scope->doNotTreatPhpDocTypesAsCertain() : $scope, + $expr, + $native ? $nativeCallableParameters : $callableParameters, + $parameters, + ), + $native, + ); + } - if (!$node instanceof Yield_ && !$node instanceof YieldFrom) { - return; - } + /** + * Constructs an arrow function type from data the engine already gathered + * while walking the body once (see NodeScopeResolver:: + * processArrowFunctionNode()), without a second walk. The return/yield + * expression types are read from their stored results on $arrowScope. + * + * @param ThrowPoint[] $throwPoints + * @param ImpurePoint[] $impurePoints already merged (property-assign impure points + expression result impure points) + * @param InvalidateExprNode[] $invalidateExpressions + */ + public function buildClosureTypeForArrowFunction( + MutatingScope $scope, + ArrowFunction $expr, + MutatingScope $arrowScope, + array $throwPoints, + array $impurePoints, + array $invalidateExpressions, + bool $native = false, + ): ClosureType + { + if ($this->bodyWalkHasOwnParameterTypes($expr)) { + return $this->getClosureType($native ? $scope->doNotTreatPhpDocTypesAsCertain() : $scope, $expr); + } - $closureYieldStatements[] = [$node, $scope]; - }, StatementContext::createTopLevel()); - } finally { - self::$resolveClosureTypeDepth--; + [$parameters, $isVariadic, $callableParameters, $nativeCallableParameters] = $this->buildParametersAndAcceptors($scope, $expr); + + $returnType = $this->resolveArrowFunctionReturnType($scope, $arrowScope, $expr, $native); + + // the flavour-correct key both keeps the native build from clobbering the + // phpdoc cache slot and lets a later getClosureType() ask on the promoted + // scope answer from this build instead of re-walking + return $this->assembleClosureType($scope, $expr, $parameters, $isVariadic, $returnType, $throwPoints, $impurePoints, $invalidateExpressions, [], $this->closureContextCacheKey( + $native ? $scope->doNotTreatPhpDocTypesAsCertain() : $scope, + $expr, + $native ? $nativeCallableParameters : $callableParameters, + $parameters, + )); + } + + /** + * Seeds the per-node closure type cache from the single body walk the + * engine just performed (see ClosureHandler::processExpr()), so later + * getClosureType() asks answer from the walk instead of walking the + * body again. + * + * A closure's native type equals its phpdoc type - the native-flavour + * slot is seeded with the same build, keyed exactly as the + * promoted-scope ask computes its key. + */ + public function seedCacheFromClosureWalk(MutatingScope $scope, Node\Expr\Closure $expr, ProcessClosureResult $processClosureResult, ExpressionResultStorage $storage): void + { + // a parked fiber may still append to the walk's gathered data - the + // invalidate expressions of a write like $this->prop[] = ... arrive + // only when the fiber flushes (see the invalidate-expressions note in + // NodeScopeResolver::processArgs()). Seed only when nothing is parked, + // so a seeded entry is never incomplete; otherwise the lazy ask keeps + // re-walking with the fibers flushed, as before. + if ($storage->pendingFibers !== []) { + return; + } + + // for array_map() callbacks and immediately invoked closures this + // delegates to a getClosureType() walk with the call-site parameter + // types; either way the phpdoc build lands in the cache under the + // key the plain ask computes + $this->buildClosureTypeForClosure( + $scope, + $expr, + $processClosureResult->getGatheredReturnStatements(), + $processClosureResult->getGatheredYieldStatements(), + $processClosureResult->getExecutionEnds(), + $processClosureResult->getThrowPoints(), + $processClosureResult->getClosureTypeImpurePoints(), + $processClosureResult->getInvalidateExpressions(), + ); + + [$parameters, , $callableParameters] = $this->buildParametersAndAcceptors($scope, $expr); + $phpdocKey = $this->closureContextCacheKey($scope, $expr, $callableParameters, $parameters); + $cachedTypes = $expr->getAttribute('phpstanCachedTypes', []); + if (!array_key_exists($phpdocKey, $cachedTypes)) { + return; + } + + $promotedScope = $scope->doNotTreatPhpDocTypesAsCertain(); + [$promotedParameters, , $promotedCallableParameters] = $this->buildParametersAndAcceptors($promotedScope, $expr); + $cachedTypes[$this->closureContextCacheKey($promotedScope, $expr, $promotedCallableParameters, $promotedParameters)] = $cachedTypes[$phpdocKey]; + $expr->setAttribute('phpstanCachedTypes', $cachedTypes); + } + + /** + * Arrow-function counterpart of seedCacheFromClosureWalk() (see + * ArrowFunctionHandler::processExpr()). Unlike a closure, an arrow + * function's native return type genuinely differs from its phpdoc one, + * so the native-flavour slot is seeded with its own build reading the + * walked body's native types. + */ + public function seedCacheFromArrowFunctionWalk(MutatingScope $scope, ArrowFunction $expr, ProcessArrowFunctionResult $arrowFunctionResult, ExpressionResultStorage $storage): void + { + // see the parked-fiber note in seedCacheFromClosureWalk() + if ($storage->pendingFibers !== []) { + return; + } + + $this->buildClosureTypeForArrowFunction( + $scope, + $expr, + $arrowFunctionResult->getArrowFunctionScope(), + $arrowFunctionResult->getClosureTypeThrowPoints(), + $arrowFunctionResult->getClosureTypeImpurePoints(), + $arrowFunctionResult->getInvalidateExpressions(), + ); + $this->buildClosureTypeForArrowFunction( + $scope, + $expr, + $arrowFunctionResult->getArrowFunctionScope(), + $arrowFunctionResult->getClosureTypeThrowPoints(), + $arrowFunctionResult->getClosureTypeImpurePoints(), + $arrowFunctionResult->getInvalidateExpressions(), + true, + ); + } + + /** + * The cache key of everything this closure's type can depend on: the + * enclosing scope plus the parameter types the caller feeds in - + * array_map style callers type the same closure node per element + * through the callable parameters. + * + * @param array|null $callableParameters + * @param array $parameters + */ + private function closureContextCacheKey(MutatingScope $scope, Node\Expr\Closure|ArrowFunction $expr, ?array $callableParameters, array $parameters): string + { + $parts = []; + foreach ($callableParameters ?? $parameters as $parameter) { + $parts[] = $parameter->getType()->describe(VerbosityLevel::cache()); + } + + // only arrow functions get a flavour-separated slot: their native return + // type genuinely differs from the phpdoc one. A closure's two flavours are + // distinguished by the scope hash alone - and deliberately share values + // when the seeding pin copies the phpdoc build to the promoted key + $flavour = $expr instanceof ArrowFunction ? ($scope->nativeTypesPromoted ? '/native' : '/phpdoc') : ''; + + return $scope->getClosureScopeCacheKey() . '/' . implode('|', $parts) . $flavour; + } + + /** + * Whether getClosureType() would walk the body with different parameter types + * than NodeScopeResolver's single walk (processClosureNode()/ + * processArrowFunctionNode()) did. array_map() callbacks and immediately + * invoked closures get their parameter types from the array element type / + * the invocation arguments in getClosureType(), whereas the single walk types + * them from the closure's passed-to callable type - so the return type read + * from the gathered scopes would differ, and getClosureType() must re-walk. + */ + private function bodyWalkHasOwnParameterTypes(Node\Expr\Closure|ArrowFunction $expr): bool + { + return $expr->getAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME) !== null + || $expr->getAttribute(ImmediatelyInvokedClosureVisitor::ARGS_ATTRIBUTE_NAME) !== null; + } + + /** + * @param list $parameters + * @param list $returnStatements + * @param list $yieldStatements + * @param list $executionEnds + * @param ThrowPoint[] $throwPoints + * @param ImpurePoint[] $impurePoints + * @param InvalidateExprNode[] $invalidateExpressions + */ + private function buildClosureTypeFromClosureWalk( + MutatingScope $scope, + Node\Expr\Closure $expr, + array $parameters, + bool $isVariadic, + array $returnStatements, + array $yieldStatements, + array $executionEnds, + array $throwPoints, + array $impurePoints, + array $invalidateExpressions, + ?string $cacheKey = null, + bool $native = false, + ): ClosureType + { + $onlyNeverExecutionEnds = $this->deriveOnlyNeverExecutionEnds($executionEnds); + + // like resolveArrowFunctionReturnType(): the single walk stored both + // flavours on the gathered scopes, so the native flavour just reads the + // stored native types - no second body walk on the promoted scope + $returnTypes = []; + $hasNull = false; + foreach ($returnStatements as [$returnNode, $returnScope]) { + if ($returnNode->expr === null) { + $hasNull = true; + continue; + } + + $readScope = $returnScope->toMutatingScope(); + if ($native) { + $readScope = $readScope->doNotTreatPhpDocTypesAsCertain(); + } + $returnTypes[] = $readScope->getType($returnNode->expr); + } + + if (count($returnTypes) === 0) { + if ($onlyNeverExecutionEnds === true && !$hasNull) { + $returnType = new NonAcceptingNeverType(); + } else { + $returnType = new VoidType(); } + } else { + if ($onlyNeverExecutionEnds === true) { + $returnTypes[] = new NonAcceptingNeverType(); + } + if ($hasNull) { + $returnTypes[] = new NullType(); + } + $returnType = TypeCombinator::union(...$returnTypes); + } - $throwPoints = $closureStatementResult->getThrowPoints(); - $impurePoints = array_merge($closureImpurePoints, $closureStatementResult->getImpurePoints()); + if (count($yieldStatements) > 0) { + $keyTypes = []; + $valueTypes = []; + foreach ($yieldStatements as [$yieldNode, $yieldScope]) { + $readScope = $yieldScope->toMutatingScope(); + if ($native) { + $readScope = $readScope->doNotTreatPhpDocTypesAsCertain(); + } + if ($yieldNode instanceof Yield_) { + if ($yieldNode->key === null) { + $keyTypes[] = new IntegerType(); + } else { + $keyTypes[] = $readScope->getType($yieldNode->key); + } + + if ($yieldNode->value === null) { + $valueTypes[] = new NullType(); + } else { + $valueTypes[] = $readScope->getType($yieldNode->value); + } - $returnTypes = []; - $hasNull = false; - foreach ($closureReturnStatements as [$returnNode, $returnScope]) { - if ($returnNode->expr === null) { - $hasNull = true; continue; } - $returnTypes[] = $returnScope->toMutatingScope()->getType($returnNode->expr); + $yieldFromType = $readScope->getType($yieldNode->expr); + $keyTypes[] = $readScope->getIterableKeyType($yieldFromType); + $valueTypes[] = $readScope->getIterableValueType($yieldFromType); + } + + $returnType = new GenericObjectType(Generator::class, [ + TypeCombinator::union(...$keyTypes), + TypeCombinator::union(...$valueTypes), + new MixedType(), + $returnType, + ]); + } else { + if ($expr->returnType !== null) { + $nativeReturnType = $scope->getFunctionType($expr->returnType, false, false); + $returnType = MutatingScope::intersectButNotNever($nativeReturnType, $returnType); + } + } + + $usedVariables = []; + foreach ($expr->uses as $use) { + if (!is_string($use->var->name)) { + continue; + } + + $usedVariables[] = $use->var->name; + } + + foreach ($expr->uses as $use) { + if (!$use->byRef) { + continue; } - if (count($returnTypes) === 0) { - if ($onlyNeverExecutionEnds === true && !$hasNull) { - $returnType = new NonAcceptingNeverType(); + $impurePoints[] = new ImpurePoint( + $scope, + $expr, + 'functionCall', + 'call to a Closure with by-ref use', + true, + ); + break; + } + + return $this->assembleClosureType($scope, $expr, $parameters, $isVariadic, $returnType, $throwPoints, $impurePoints, $invalidateExpressions, $usedVariables, $cacheKey); + } + + private function resolveArrowFunctionReturnType( + MutatingScope $scope, + MutatingScope $arrowScope, + ArrowFunction $expr, + bool $native = false, + ): Type + { + // Unlike a closure (whose native type equals its phpdoc type), an arrow + // function's native return type is the body expression's native type + // (e.g. fn () => methodReturningPositiveInt() is Closure(): int natively, + // Closure(): int<1, max> in phpdoc). The body was already processed in the + // single walk, so the native flavour just reads the stored native types off + // the same arrowScope - no second walk. + $readScope = $native ? $arrowScope->doNotTreatPhpDocTypesAsCertain() : $arrowScope; + + if ($expr->expr instanceof Yield_ || $expr->expr instanceof YieldFrom) { + $yieldNode = $expr->expr; + + if ($yieldNode instanceof Yield_) { + if ($yieldNode->key === null) { + $keyType = new IntegerType(); } else { - $returnType = new VoidType(); + $keyType = $readScope->getType($yieldNode->key); } - } else { - if ($onlyNeverExecutionEnds === true) { - $returnTypes[] = new NonAcceptingNeverType(); - } - if ($hasNull) { - $returnTypes[] = new NullType(); + + if ($yieldNode->value === null) { + $valueType = new NullType(); + } else { + $valueType = $readScope->getType($yieldNode->value); } - $returnType = TypeCombinator::union(...$returnTypes); + } else { + $yieldFromType = $readScope->getType($yieldNode->expr); + $keyType = $readScope->getIterableKeyType($yieldFromType); + $valueType = $readScope->getIterableValueType($yieldFromType); } - if (count($closureYieldStatements) > 0) { - $keyTypes = []; - $valueTypes = []; - foreach ($closureYieldStatements as [$yieldNode, $yieldScope]) { - if ($yieldNode instanceof Yield_) { - if ($yieldNode->key === null) { - $keyTypes[] = new IntegerType(); - } else { - $keyTypes[] = $yieldScope->toMutatingScope()->getType($yieldNode->key); - } + return new GenericObjectType(Generator::class, [ + $keyType, + $valueType, + new MixedType(), + new VoidType(), + ]); + } - if ($yieldNode->value === null) { - $valueTypes[] = new NullType(); - } else { - $valueTypes[] = $yieldScope->toMutatingScope()->getType($yieldNode->value); - } + $returnType = $readScope->getKeepVoidType($expr->expr); + if ($expr->returnType !== null) { + $nativeReturnType = $scope->getFunctionType($expr->returnType, false, false); + $returnType = MutatingScope::intersectButNotNever($nativeReturnType, $returnType); + } + + return $returnType; + } + /** + * Whether every execution end of the closure body is a "never" terminator + * (throw/exit) rather than a return: null when there were no execution ends, + * false once a return (or non-terminating end) is seen. + * + * @param list $executionEnds + */ + private function deriveOnlyNeverExecutionEnds(array $executionEnds): ?bool + { + $onlyNeverExecutionEnds = null; + foreach ($executionEnds as $node) { + if ($node->getStatementResult()->isAlwaysTerminating()) { + foreach ($node->getStatementResult()->getExitPoints() as $exitPoint) { + if ($exitPoint->getStatement() instanceof Node\Stmt\Return_) { + $onlyNeverExecutionEnds = false; continue; } - $yieldFromType = $yieldScope->toMutatingScope()->getType($yieldNode->expr); - $keyTypes[] = $yieldScope->toMutatingScope()->getIterableKeyType($yieldFromType); - $valueTypes[] = $yieldScope->toMutatingScope()->getIterableValueType($yieldFromType); + if ($onlyNeverExecutionEnds === null) { + $onlyNeverExecutionEnds = true; + } + + break; } - $returnType = new GenericObjectType(Generator::class, [ - TypeCombinator::union(...$keyTypes), - TypeCombinator::union(...$valueTypes), - new MixedType(), - $returnType, - ]); - } else { - if ($expr->returnType !== null) { - $nativeReturnType = $scope->getFunctionType($expr->returnType, false, false); - $returnType = MutatingScope::intersectButNotNever($nativeReturnType, $returnType); + if (count($node->getStatementResult()->getExitPoints()) === 0) { + if ($onlyNeverExecutionEnds === null) { + $onlyNeverExecutionEnds = true; + } } + } else { + $onlyNeverExecutionEnds = false; } + } - $usedVariables = []; - foreach ($expr->uses as $use) { - if (!is_string($use->var->name)) { - continue; + return $onlyNeverExecutionEnds; + } + + /** + * Builds the closure/arrow function's declared parameters (independent of the + * body walk) and the callable parameter acceptors derived from the call site. + * + * @return array{list, bool, ParameterReflection[]|null, ParameterReflection[]|null} + */ + private function buildParametersAndAcceptors( + MutatingScope $scope, + Node\Expr\Closure|ArrowFunction $expr, + ): array + { + $parameters = []; + $isVariadic = false; + $firstOptionalParameterIndex = null; + foreach ($expr->params as $i => $param) { + $isOptionalCandidate = $param->default !== null || $param->variadic; + + if ($isOptionalCandidate) { + if ($firstOptionalParameterIndex === null) { + $firstOptionalParameterIndex = $i; } + } else { + $firstOptionalParameterIndex = null; + } + } - $usedVariables[] = $use->var->name; + foreach ($expr->params as $i => $param) { + if ($param->variadic) { + $isVariadic = true; + } + if (!$param->var instanceof Variable || !is_string($param->var->name)) { + throw new ShouldNotHappenException(); } + $parameters[] = new NativeParameterReflection( + $param->var->name, + $firstOptionalParameterIndex !== null && $i >= $firstOptionalParameterIndex, + $scope->getFunctionType($param->type, $scope->isParameterValueNullable($param), false), + $param->byRef + ? PassedByReference::createCreatesNewVariable() + : PassedByReference::createNo(), + $param->variadic, + // a default is a constant expression - price it without a scope + // walk, the same way parameter defaults are priced elsewhere + $param->default !== null ? $this->initializerExprTypeResolver->getType($param->default, InitializerExprContext::fromScope($scope)) : null, + ); + } - foreach ($expr->uses as $use) { - if (!$use->byRef) { - continue; + $callableParameters = null; + $nativeCallableParameters = null; + $arrayMapArgs = $expr->getAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME); + $immediatelyInvokedArgs = $expr->getAttribute(ImmediatelyInvokedClosureVisitor::ARGS_ATTRIBUTE_NAME); + if ($arrayMapArgs !== null) { + $callableParameters = []; + $nativeCallableParameters = []; + foreach ($arrayMapArgs as $funcCallArg) { + $callableParameters[] = new DummyParameter('item', $scope->getType($funcCallArg->value)->getIterableValueType(), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); + $nativeCallableParameters[] = new DummyParameter('item', $scope->getNativeType($funcCallArg->value)->getIterableValueType(), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); + } + } elseif ($immediatelyInvokedArgs !== null) { + foreach ($immediatelyInvokedArgs as $immediatelyInvokedArg) { + $callableParameters[] = new DummyParameter('item', $scope->getType($immediatelyInvokedArg->value), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); + $nativeCallableParameters[] = new DummyParameter('item', $scope->getNativeType($immediatelyInvokedArg->value), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); + } + } else { + $inFunctionCallsStackCount = count($scope->inFunctionCallsStack); + if ($inFunctionCallsStackCount > 0) { + [, $inParameter] = $scope->inFunctionCallsStack[$inFunctionCallsStackCount - 1]; + if ($inParameter !== null) { + $callableParameters = $this->nodeScopeResolver->createCallableParameters($scope, $expr, null, $inParameter->getType()); + $nativeType = $inParameter instanceof ExtendedParameterReflection ? $inParameter->getNativeType() : $inParameter->getType(); + $nativeCallableParameters = $this->nodeScopeResolver->createNativeCallableParameters($scope, $expr, null, $nativeType); } + } + } - $impurePoints[] = new ImpurePoint( - $scope, - $expr, - 'functionCall', - 'call to a Closure with by-ref use', - true, - ); - break; + return [$parameters, $isVariadic, $callableParameters, $nativeCallableParameters]; + } + + /** + * @param list $parameters + * @param array{returnType: Type, throwPoints: SimpleThrowPoint[], impurePoints: SimpleImpurePoint[], invalidateExpressions: InvalidateExprNode[], usedVariables: string[]} $cachedClosureData + */ + private function createClosureTypeFromCache( + Node\Expr\Closure|ArrowFunction $expr, + array $parameters, + bool $isVariadic, + array $cachedClosureData, + ): ClosureType + { + $mustUseReturnValue = TrinaryLogic::createNo(); + foreach ($expr->attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + if ($attr->name->toLowerString() === 'nodiscard') { + $mustUseReturnValue = TrinaryLogic::createYes(); + break; + } } } + return new ClosureType( + $parameters, + $cachedClosureData['returnType'], + $isVariadic, + TemplateTypeMap::createEmpty(), + TemplateTypeMap::createEmpty(), + TemplateTypeVarianceMap::createEmpty(), + throwPoints: $cachedClosureData['throwPoints'], + impurePoints: $cachedClosureData['impurePoints'], + invalidateExpressions: $cachedClosureData['invalidateExpressions'], + usedVariables: $cachedClosureData['usedVariables'], + acceptsNamedArguments: TrinaryLogic::createYes(), + mustUseReturnValue: $mustUseReturnValue, + isStatic: TrinaryLogic::createFromBoolean($expr->static), + ); + } + + /** + * Constructs the final ClosureType from the resolved return type and the + * gathered throw/impure/invalidate points. Adds the by-ref-parameter impure + * point, populates the per-scope phpdoc-type cache (closures only), and + * resolves the #[NoDiscard] attribute. + * + * @param list $parameters + * @param ThrowPoint[] $throwPoints + * @param ImpurePoint[] $impurePoints + * @param InvalidateExprNode[] $invalidateExpressions + * @param string[] $usedVariables + */ + private function assembleClosureType( + MutatingScope $scope, + Node\Expr\Closure|ArrowFunction $expr, + array $parameters, + bool $isVariadic, + Type $returnType, + array $throwPoints, + array $impurePoints, + array $invalidateExpressions, + array $usedVariables, + ?string $cacheKey = null, + ): ClosureType + { foreach ($parameters as $parameter) { if ($parameter->passedByReference()->no()) { continue; @@ -423,7 +815,8 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu $impurePointsForClosureType = array_map(static fn (ImpurePoint $impurePoint) => new SimpleImpurePoint($impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()), $impurePoints); $cachedTypes = $expr->getAttribute('phpstanCachedTypes', []); - $cachedTypes[$scope->getClosureScopeCacheKey()] = [ + $cacheKey ??= $this->closureContextCacheKey($scope, $expr, null, $parameters); + $cachedTypes[$cacheKey] = [ 'returnType' => $returnType, 'throwPoints' => $throwPointsForClosureType, 'impurePoints' => $impurePointsForClosureType, diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index aefe7850be..cf0cba7f77 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -49,6 +49,7 @@ use PhpParser\NodeFinder; use PhpParser\NodeTraverser; use PHPStan\Analyser\ExprHandler\AssignHandler; +use PHPStan\Analyser\ExprHandler\Helper\ClosureTypeResolver; use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; use PHPStan\Analyser\ExprHandler\Helper\MethodThrowPointHelper; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass; @@ -3038,10 +3039,12 @@ public function processClosureNode( $executionEnds = []; $gatheredReturnStatements = []; + $gatheredReturnStatementsWithScope = []; $gatheredYieldStatements = []; + $gatheredYieldStatementsWithScope = []; $closureImpurePoints = []; $invalidateExpressions = []; - $closureStmtsCallback = new GatheringNodeCallback(static function (Node $node, Scope $scope) use (&$executionEnds, &$gatheredReturnStatements, &$gatheredYieldStatements, &$closureScope, &$closureImpurePoints, &$invalidateExpressions): void { + $closureStmtsCallback = new GatheringNodeCallback(static function (Node $node, Scope $scope) use (&$executionEnds, &$gatheredReturnStatements, &$gatheredReturnStatementsWithScope, &$gatheredYieldStatements, &$gatheredYieldStatementsWithScope, &$closureScope, &$closureImpurePoints, &$invalidateExpressions): void { if ($scope->getAnonymousFunctionReflection() !== $closureScope->getAnonymousFunctionReflection()) { return; } @@ -3066,12 +3069,14 @@ public function processClosureNode( } if ($node instanceof Expr\Yield_ || $node instanceof Expr\YieldFrom) { $gatheredYieldStatements[] = $node; + $gatheredYieldStatementsWithScope[] = [$node, $scope]; } if (!$node instanceof Return_) { return; } $gatheredReturnStatements[] = new ReturnStatement($scope, $node); + $gatheredReturnStatementsWithScope[] = [$node, $scope]; }, $nodeCallback); if (count($byRefUses) === 0) { @@ -3086,7 +3091,16 @@ public function processClosureNode( array_merge($publicStatementResult->getImpurePoints(), $closureImpurePoints), ), $closureScope, $storage); - return new ProcessClosureResult($scope, $statementResult->getThrowPoints(), $statementResult->getImpurePoints(), $invalidateExpressions); + return new ProcessClosureResult( + $scope, + $statementResult->getThrowPoints(), + $statementResult->getImpurePoints(), + $invalidateExpressions, + $gatheredReturnStatementsWithScope, + $gatheredYieldStatementsWithScope, + $executionEnds, + array_merge($closureImpurePoints, $statementResult->getImpurePoints()), + ); } $originalStorage = $storage; @@ -3136,7 +3150,18 @@ public function processClosureNode( array_merge($publicStatementResult->getImpurePoints(), $closureImpurePoints), ), $closureScope, $storage); - return new ProcessClosureResult($scope, $statementResult->getThrowPoints(), $statementResult->getImpurePoints(), $invalidateExpressions, $closureResultScope, $byRefUses); + return new ProcessClosureResult( + $scope, + $statementResult->getThrowPoints(), + $statementResult->getImpurePoints(), + $invalidateExpressions, + $gatheredReturnStatementsWithScope, + $gatheredYieldStatementsWithScope, + $executionEnds, + array_merge($closureImpurePoints, $statementResult->getImpurePoints()), + $closureResultScope, + $byRefUses, + ); } /** @@ -3174,7 +3199,7 @@ public function processArrowFunctionNode( callable $nodeCallback, ?Type $passedToType, ?Type $nativePassedToType = null, - ): ExpressionResult + ): ProcessArrowFunctionResult { foreach ($expr->params as $param) { $this->processParamNode($stmt, $param, $scope, $storage, $nodeCallback); @@ -3192,9 +3217,49 @@ public function processArrowFunctionNode( throw new ShouldNotHappenException(); } $this->callNodeCallback($nodeCallback, new InArrowFunctionNode($arrowFunctionType, $expr), $arrowFunctionScope, $storage); - $exprResult = $this->processExprNode($stmt, $expr->expr, $arrowFunctionScope, $storage, $nodeCallback, ExpressionContext::createTopLevel()); - return $this->expressionResultFactory->create($scope, beforeScope: $scope, expr: $expr, hasYield: false, isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), impurePoints: $exprResult->getImpurePoints()); + // Gather the property-assign impure points and invalidate expressions the + // arrow function type needs (mirroring ClosureTypeResolver::getClosureType()), + // on top of the regular rule node callback, so the single body walk here + // feeds ClosureTypeResolver::buildClosureTypeForArrowFunction(). + $arrowFunctionImpurePoints = []; + $invalidateExpressions = []; + $arrowFunctionStmtsCallback = new GatheringNodeCallback(static function (Node $node, Scope $innerScope) use ($arrowFunctionScope, &$arrowFunctionImpurePoints, &$invalidateExpressions): void { + if ($innerScope->getAnonymousFunctionReflection() !== $arrowFunctionScope->getAnonymousFunctionReflection()) { + return; + } + + if ($node instanceof InvalidateExprNode) { + $invalidateExpressions[] = $node; + return; + } + + if (!$node instanceof PropertyAssignNode) { + return; + } + + $arrowFunctionImpurePoints[] = new ImpurePoint( + $innerScope, + $node, + 'propertyAssign', + 'property assignment', + true, + ); + $invalidateExpressions[] = new InvalidateExprNode($node->getPropertyFetch()); + }, $nodeCallback); + + $exprResult = $this->processExprNode($stmt, $expr->expr, $arrowFunctionScope, $storage, $arrowFunctionStmtsCallback, ExpressionContext::createTopLevel()); + + $closureTypeThrowPoints = array_map(static fn (InternalThrowPoint $throwPoint) => $throwPoint->toPublic(), $exprResult->getThrowPoints()); + $closureTypeImpurePoints = array_merge($arrowFunctionImpurePoints, $exprResult->getImpurePoints()); + + return new ProcessArrowFunctionResult( + $this->expressionResultFactory->create($scope, beforeScope: $scope, expr: $expr, hasYield: false, isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), impurePoints: $exprResult->getImpurePoints()), + $arrowFunctionScope, + $closureTypeThrowPoints, + $closureTypeImpurePoints, + $invalidateExpressions, + ); } /** @@ -3811,6 +3876,10 @@ public function processArgs( $this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $storage, $context); $closureResult = $this->processClosureNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $context, $parameterType, $parameterNativeType); + // the preferred ClosureType read below now answers from this seed + // instead of walking the body again (unless a parked fiber may + // still complete the gathered data - then it keeps re-walking) + $this->container->getByType(ClosureTypeResolver::class)->seedCacheFromClosureWalk($scopeToPass, $arg->value, $closureResult, $storage); if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) { $throwPoints = array_merge($throwPoints, array_map(static fn (InternalThrowPoint $throwPoint) => $throwPoint->isExplicit() ? InternalThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : InternalThrowPoint::createImplicit($scope, $arg->value), $closureResult->getThrowPoints())); $impurePoints = array_merge($impurePoints, $closureResult->getImpurePoints()); @@ -3894,7 +3963,12 @@ public function processArgs( } $this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $storage, $context); - $arrowFunctionResult = $this->processArrowFunctionNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $parameterType, $parameterNativeType); + $processArrowFunctionResult = $this->processArrowFunctionNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $parameterType, $parameterNativeType); + // the invalidation read below now answers from this seed instead + // of walking the body again (unless a parked fiber may still + // complete the gathered data - then it keeps re-walking) + $this->container->getByType(ClosureTypeResolver::class)->seedCacheFromArrowFunctionWalk($scopeToPass, $arg->value, $processArrowFunctionResult, $storage); + $arrowFunctionResult = $processArrowFunctionResult->getExpressionResult(); if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) { $throwPoints = array_merge($throwPoints, array_map(static fn (InternalThrowPoint $throwPoint) => $throwPoint->isExplicit() ? InternalThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : InternalThrowPoint::createImplicit($scope, $arg->value), $arrowFunctionResult->getThrowPoints())); $impurePoints = array_merge($impurePoints, $arrowFunctionResult->getImpurePoints()); diff --git a/src/Analyser/ProcessArrowFunctionResult.php b/src/Analyser/ProcessArrowFunctionResult.php new file mode 100644 index 0000000000..a571ced99f --- /dev/null +++ b/src/Analyser/ProcessArrowFunctionResult.php @@ -0,0 +1,59 @@ +expressionResult; + } + + public function getArrowFunctionScope(): MutatingScope + { + return $this->arrowFunctionScope; + } + + /** + * @return ThrowPoint[] + */ + public function getClosureTypeThrowPoints(): array + { + return $this->closureTypeThrowPoints; + } + + /** + * @return ImpurePoint[] + */ + public function getClosureTypeImpurePoints(): array + { + return $this->closureTypeImpurePoints; + } + + /** + * @return InvalidateExprNode[] + */ + public function getInvalidateExpressions(): array + { + return $this->invalidateExpressions; + } + +} diff --git a/src/Analyser/ProcessClosureResult.php b/src/Analyser/ProcessClosureResult.php index cb4b609e1f..e114ac55b8 100644 --- a/src/Analyser/ProcessClosureResult.php +++ b/src/Analyser/ProcessClosureResult.php @@ -3,6 +3,10 @@ namespace PHPStan\Analyser; use PhpParser\Node\ClosureUse; +use PhpParser\Node\Expr\Yield_; +use PhpParser\Node\Expr\YieldFrom; +use PhpParser\Node\Stmt\Return_; +use PHPStan\Node\ExecutionEndNode; use PHPStan\Node\InvalidateExprNode; final class ProcessClosureResult @@ -12,6 +16,10 @@ final class ProcessClosureResult * @param InternalThrowPoint[] $throwPoints * @param ImpurePoint[] $impurePoints * @param InvalidateExprNode[] $invalidateExpressions + * @param list $gatheredReturnStatements + * @param list $gatheredYieldStatements + * @param list $executionEnds + * @param ImpurePoint[] $closureTypeImpurePoints already merged in getClosureType() order (property-assign impure points first, then statement result impure points) * @param ClosureUse[] $byRefUses */ public function __construct( @@ -19,6 +27,10 @@ public function __construct( private array $throwPoints, private array $impurePoints, private array $invalidateExpressions, + private array $gatheredReturnStatements, + private array $gatheredYieldStatements, + private array $executionEnds, + private array $closureTypeImpurePoints, private ?MutatingScope $byRefClosureResultScope = null, private array $byRefUses = [], ) @@ -63,4 +75,40 @@ public function getInvalidateExpressions(): array return $this->invalidateExpressions; } + /** + * @return list + */ + public function getGatheredReturnStatements(): array + { + return $this->gatheredReturnStatements; + } + + /** + * @return list + */ + public function getGatheredYieldStatements(): array + { + return $this->gatheredYieldStatements; + } + + /** + * @return list + */ + public function getExecutionEnds(): array + { + return $this->executionEnds; + } + + /** + * The closure body's impure points already merged in getClosureType() order + * (property-assign impure points first, then statement result impure points), + * ready to feed ClosureTypeResolver::buildClosureTypeForClosure(). + * + * @return ImpurePoint[] + */ + public function getClosureTypeImpurePoints(): array + { + return $this->closureTypeImpurePoints; + } + }