Skip to content

Commit 32fc5cb

Browse files
ondrejmirtesclaude
andcommitted
Seed the arrow function type cache from the handler's single body walk
processArrowFunctionNode() now returns ProcessArrowFunctionResult: the walked arrow scope plus the throw points, impure points (merged in getClosureType() order) and invalidate expressions its type needs, gathered during the single body walk. ArrowFunctionHandler seeds the per-node type cache with both flavours built from the walk - unlike a closure, an arrow function's native return type genuinely differs from its phpdoc one, so the native slot gets its own build reading the walked body's native types. Arrow functions now participate in the cache read: the flavour- and parameter-aware key answers each ask from its own seeded slot. Both seeding paths (this one and the closure one) are gated on the storage having no parked fibers: 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 - so a seeded entry could be incomplete. When anything is parked, the lazy ask keeps re-walking with the fibers flushed, as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DaBZjgksga4c5s6Q9FniY7
1 parent 38c5d3b commit 32fc5cb

5 files changed

Lines changed: 153 additions & 8 deletions

File tree

‎src/Analyser/ExprHandler/ArrowFunctionHandler.php‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ public function supports(Expr $expr): bool
4141

4242
public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult
4343
{
44-
$result = $nodeScopeResolver->processArrowFunctionNode($stmt, $expr, $scope, $storage, $nodeCallback, null);
44+
$arrowFunctionResult = $nodeScopeResolver->processArrowFunctionNode($stmt, $expr, $scope, $storage, $nodeCallback, null);
45+
$this->closureTypeResolver->seedCacheFromArrowFunctionWalk($scope, $expr, $arrowFunctionResult, $storage);
46+
$result = $arrowFunctionResult->getExpressionResult();
4547

4648
return $this->expressionResultFactory->create(
4749
$result->getScope(),

‎src/Analyser/ExprHandler/ClosureHandler.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public function supports(Expr $expr): bool
4242
public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult
4343
{
4444
$processClosureResult = $nodeScopeResolver->processClosureNode($stmt, $expr, $scope, $storage, $nodeCallback, $context, null);
45-
$this->closureTypeResolver->seedCacheFromClosureWalk($scope, $expr, $processClosureResult);
45+
$this->closureTypeResolver->seedCacheFromClosureWalk($scope, $expr, $processClosureResult, $storage);
4646

4747
return $this->expressionResultFactory->create(
4848
$processClosureResult->applyByRefUseScope($processClosureResult->getScope()),

‎src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php‎

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use PHPStan\Analyser\InternalThrowPoint;
1515
use PHPStan\Analyser\MutatingScope;
1616
use PHPStan\Analyser\NodeScopeResolver;
17+
use PHPStan\Analyser\ProcessArrowFunctionResult;
1718
use PHPStan\Analyser\ProcessClosureResult;
1819
use PHPStan\Analyser\Scope;
1920
use PHPStan\Analyser\StatementContext;
@@ -87,7 +88,7 @@ public function getClosureType(
8788

8889
$cachedTypes = $expr->getAttribute('phpstanCachedTypes', []);
8990
$cacheKey = $this->closureContextCacheKey($scope, $expr, $callableParameters, $parameters);
90-
if (!$expr instanceof ArrowFunction && array_key_exists($cacheKey, $cachedTypes)) {
91+
if (array_key_exists($cacheKey, $cachedTypes)) {
9192
return $this->createClosureTypeFromCache($expr, $parameters, $isVariadic, $cachedTypes[$cacheKey]);
9293
}
9394
if (self::$resolveClosureTypeDepth >= 2) {
@@ -322,8 +323,18 @@ public function buildClosureTypeForArrowFunction(
322323
* slot is seeded with the same build, keyed exactly as the
323324
* promoted-scope ask computes its key.
324325
*/
325-
public function seedCacheFromClosureWalk(MutatingScope $scope, Node\Expr\Closure $expr, ProcessClosureResult $processClosureResult): void
326+
public function seedCacheFromClosureWalk(MutatingScope $scope, Node\Expr\Closure $expr, ProcessClosureResult $processClosureResult, ExpressionResultStorage $storage): void
326327
{
328+
// a parked fiber may still append to the walk's gathered data - the
329+
// invalidate expressions of a write like $this->prop[] = ... arrive
330+
// only when the fiber flushes (see the invalidate-expressions note in
331+
// NodeScopeResolver::processArgs()). Seed only when nothing is parked,
332+
// so a seeded entry is never incomplete; otherwise the lazy ask keeps
333+
// re-walking with the fibers flushed, as before.
334+
if ($storage->pendingFibers !== []) {
335+
return;
336+
}
337+
327338
// for array_map() callbacks and immediately invoked closures this
328339
// delegates to a getClosureType() walk with the call-site parameter
329340
// types; either way the phpdoc build lands in the cache under the
@@ -352,6 +363,39 @@ public function seedCacheFromClosureWalk(MutatingScope $scope, Node\Expr\Closure
352363
$expr->setAttribute('phpstanCachedTypes', $cachedTypes);
353364
}
354365

366+
/**
367+
* Arrow-function counterpart of seedCacheFromClosureWalk() (see
368+
* ArrowFunctionHandler::processExpr()). Unlike a closure, an arrow
369+
* function's native return type genuinely differs from its phpdoc one,
370+
* so the native-flavour slot is seeded with its own build reading the
371+
* walked body's native types.
372+
*/
373+
public function seedCacheFromArrowFunctionWalk(MutatingScope $scope, ArrowFunction $expr, ProcessArrowFunctionResult $arrowFunctionResult, ExpressionResultStorage $storage): void
374+
{
375+
// see the parked-fiber note in seedCacheFromClosureWalk()
376+
if ($storage->pendingFibers !== []) {
377+
return;
378+
}
379+
380+
$this->buildClosureTypeForArrowFunction(
381+
$scope,
382+
$expr,
383+
$arrowFunctionResult->getArrowFunctionScope(),
384+
$arrowFunctionResult->getClosureTypeThrowPoints(),
385+
$arrowFunctionResult->getClosureTypeImpurePoints(),
386+
$arrowFunctionResult->getInvalidateExpressions(),
387+
);
388+
$this->buildClosureTypeForArrowFunction(
389+
$scope,
390+
$expr,
391+
$arrowFunctionResult->getArrowFunctionScope(),
392+
$arrowFunctionResult->getClosureTypeThrowPoints(),
393+
$arrowFunctionResult->getClosureTypeImpurePoints(),
394+
$arrowFunctionResult->getInvalidateExpressions(),
395+
true,
396+
);
397+
}
398+
355399
/**
356400
* The cache key of everything this closure's type can depend on: the
357401
* enclosing scope plus the parameter types the caller feeds in -

‎src/Analyser/NodeScopeResolver.php‎

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3198,7 +3198,7 @@ public function processArrowFunctionNode(
31983198
callable $nodeCallback,
31993199
?Type $passedToType,
32003200
?Type $nativePassedToType = null,
3201-
): ExpressionResult
3201+
): ProcessArrowFunctionResult
32023202
{
32033203
foreach ($expr->params as $param) {
32043204
$this->processParamNode($stmt, $param, $scope, $storage, $nodeCallback);
@@ -3216,9 +3216,49 @@ public function processArrowFunctionNode(
32163216
throw new ShouldNotHappenException();
32173217
}
32183218
$this->callNodeCallback($nodeCallback, new InArrowFunctionNode($arrowFunctionType, $expr), $arrowFunctionScope, $storage);
3219-
$exprResult = $this->processExprNode($stmt, $expr->expr, $arrowFunctionScope, $storage, $nodeCallback, ExpressionContext::createTopLevel());
32203219

3221-
return $this->expressionResultFactory->create($scope, beforeScope: $scope, expr: $expr, hasYield: false, isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), impurePoints: $exprResult->getImpurePoints());
3220+
// Gather the property-assign impure points and invalidate expressions the
3221+
// arrow function type needs (mirroring ClosureTypeResolver::getClosureType()),
3222+
// on top of the regular rule node callback, so the single body walk here
3223+
// feeds ClosureTypeResolver::buildClosureTypeForArrowFunction().
3224+
$arrowFunctionImpurePoints = [];
3225+
$invalidateExpressions = [];
3226+
$arrowFunctionStmtsCallback = new GatheringNodeCallback(static function (Node $node, Scope $innerScope) use ($arrowFunctionScope, &$arrowFunctionImpurePoints, &$invalidateExpressions): void {
3227+
if ($innerScope->getAnonymousFunctionReflection() !== $arrowFunctionScope->getAnonymousFunctionReflection()) {
3228+
return;
3229+
}
3230+
3231+
if ($node instanceof InvalidateExprNode) {
3232+
$invalidateExpressions[] = $node;
3233+
return;
3234+
}
3235+
3236+
if (!$node instanceof PropertyAssignNode) {
3237+
return;
3238+
}
3239+
3240+
$arrowFunctionImpurePoints[] = new ImpurePoint(
3241+
$innerScope,
3242+
$node,
3243+
'propertyAssign',
3244+
'property assignment',
3245+
true,
3246+
);
3247+
$invalidateExpressions[] = new InvalidateExprNode($node->getPropertyFetch());
3248+
}, $nodeCallback);
3249+
3250+
$exprResult = $this->processExprNode($stmt, $expr->expr, $arrowFunctionScope, $storage, $arrowFunctionStmtsCallback, ExpressionContext::createTopLevel());
3251+
3252+
$closureTypeThrowPoints = array_map(static fn (InternalThrowPoint $throwPoint) => $throwPoint->toPublic(), $exprResult->getThrowPoints());
3253+
$closureTypeImpurePoints = array_merge($arrowFunctionImpurePoints, $exprResult->getImpurePoints());
3254+
3255+
return new ProcessArrowFunctionResult(
3256+
$this->expressionResultFactory->create($scope, beforeScope: $scope, expr: $expr, hasYield: false, isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), impurePoints: $exprResult->getImpurePoints()),
3257+
$arrowFunctionScope,
3258+
$closureTypeThrowPoints,
3259+
$closureTypeImpurePoints,
3260+
$invalidateExpressions,
3261+
);
32223262
}
32233263

32243264
/**
@@ -3918,7 +3958,7 @@ public function processArgs(
39183958
}
39193959

39203960
$this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $storage, $context);
3921-
$arrowFunctionResult = $this->processArrowFunctionNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $parameterType, $parameterNativeType);
3961+
$arrowFunctionResult = $this->processArrowFunctionNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $parameterType, $parameterNativeType)->getExpressionResult();
39223962
if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) {
39233963
$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()));
39243964
$impurePoints = array_merge($impurePoints, $arrowFunctionResult->getImpurePoints());
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Analyser;
4+
5+
use PHPStan\Node\InvalidateExprNode;
6+
7+
final class ProcessArrowFunctionResult
8+
{
9+
10+
/**
11+
* @param ThrowPoint[] $closureTypeThrowPoints public throw points for building the arrow function type
12+
* @param ImpurePoint[] $closureTypeImpurePoints already merged in getClosureType() order (property-assign impure points first, then expression result impure points)
13+
* @param InvalidateExprNode[] $invalidateExpressions
14+
*/
15+
public function __construct(
16+
private ExpressionResult $expressionResult,
17+
private MutatingScope $arrowFunctionScope,
18+
private array $closureTypeThrowPoints,
19+
private array $closureTypeImpurePoints,
20+
private array $invalidateExpressions,
21+
)
22+
{
23+
}
24+
25+
public function getExpressionResult(): ExpressionResult
26+
{
27+
return $this->expressionResult;
28+
}
29+
30+
public function getArrowFunctionScope(): MutatingScope
31+
{
32+
return $this->arrowFunctionScope;
33+
}
34+
35+
/**
36+
* @return ThrowPoint[]
37+
*/
38+
public function getClosureTypeThrowPoints(): array
39+
{
40+
return $this->closureTypeThrowPoints;
41+
}
42+
43+
/**
44+
* @return ImpurePoint[]
45+
*/
46+
public function getClosureTypeImpurePoints(): array
47+
{
48+
return $this->closureTypeImpurePoints;
49+
}
50+
51+
/**
52+
* @return InvalidateExprNode[]
53+
*/
54+
public function getInvalidateExpressions(): array
55+
{
56+
return $this->invalidateExpressions;
57+
}
58+
59+
}

0 commit comments

Comments
 (0)