Skip to content

Commit 7a24c23

Browse files
ondrejmirtesclaude
andcommitted
Break the closure resolver DI cycle by extracting ContextualClosureParameterResolver
ClosureTypeResolver built closure parameters through ClosureParameterResolver, and ClosureParameterResolver priced call arguments through ClosureTypeResolver::getClosureType() - a cycle hidden behind a container lookup. ClosureTypeResolver never passes call arguments, and only the call-argument refinement needs closure types, so the contextual part (array_map() and immediate-invocation values, the passed-to callable type) moves into ContextualClosureParameterResolver, injected into both resolvers. ClosureParameterResolver now injects ClosureTypeResolver directly, and ArgumentsHandler and ClosureProcessor inject both resolvers instead of looking them up. The one lookup left is ClosureProcessor::getParametersProcessor(): closure parameters walk their attributes, attribute arguments are call arguments walked by ArgumentsHandler, and those walk closure arguments through ClosureProcessor again - genuine recursion constructor injection cannot express. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014hgcbwfKQnye2coQXtX5XU
1 parent e1657b4 commit 7a24c23

5 files changed

Lines changed: 213 additions & 152 deletions

File tree

‎src/Analyser/ArgumentsHandler.php‎

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
use PHPStan\DependencyInjection\AutowiredExtensions;
2323
use PHPStan\DependencyInjection\AutowiredParameter;
2424
use PHPStan\DependencyInjection\AutowiredService;
25-
use PHPStan\DependencyInjection\Container;
2625
use PHPStan\DependencyInjection\ExtensionsCollection;
2726
use PHPStan\Node\Expr\TypeExpr;
2827
use PHPStan\Node\InvalidateExprNode;
@@ -94,7 +93,6 @@ final class ArgumentsHandler
9493
* @param ExtensionsCollection<StaticMethodParameterClosureTypeExtension> $staticMethodParameterClosureTypeExtensions
9594
*/
9695
public function __construct(
97-
private Container $container,
9896
private TemplateArgumentObserver $templateArgumentObserver,
9997
private ExpressionResultFactory $expressionResultFactory,
10098
private ClosureProcessor $closureProcessor,
@@ -119,6 +117,8 @@ public function __construct(
119117
#[AutowiredParameter(ref: '%exceptions.implicitThrows%')]
120118
private bool $implicitThrows,
121119
private AssignHandler $assignHandler,
120+
private ClosureTypeResolver $closureTypeResolver,
121+
private ClosureParameterResolver $closureParameterResolver,
122122
)
123123
{
124124
}
@@ -233,7 +233,7 @@ public function processArgs(
233233
$originalArgForGather = $arg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $arg;
234234
$gatheredArgTypeByIndex[$i] = $typeDrivenAcceptorSelection
235235
? $this->gatherClosureArgType($parametersAcceptors, $i, $arg->value, $scope)
236-
: $this->container->getByType(ClosureTypeResolver::class)->getClosureType($scope, $arg->value, true, $storage);
236+
: $this->closureTypeResolver->getClosureType($scope, $arg->value, true, $storage);
237237
$this->addGatheredArgType($gatheredTypes, $gatheredUnpack, $gatheredHasName, $originalArgForGather, $i, $gatheredArgTypeByIndex[$i]);
238238
}
239239

@@ -362,7 +362,6 @@ public function processArgs(
362362
// a miss means "walk it", not "price it silently": pricing skips
363363
// the body walk and never fires the closure's node callbacks
364364
if ($storedClosureArgResult === null && $nodeScopeResolver->isReturningStoredExpressionResults()) {
365-
$closureTypeResolver = $this->container->getByType(ClosureTypeResolver::class);
366365
$storedClosureArgResult = $this->expressionResultFactory->create(
367366
$scopeToPass,
368367
beforeScope: $scopeToPass,
@@ -371,8 +370,8 @@ public function processArgs(
371370
isAlwaysTerminating: false,
372371
throwPoints: [],
373372
impurePoints: [],
374-
type: $closureTypeResolver->getClosureType($scopeToPass, $arg->value, false, $storage),
375-
nativeType: $closureTypeResolver->getClosureType($scopeToPass->doNotTreatPhpDocTypesAsCertain(), $arg->value, false, $storage),
373+
type: $this->closureTypeResolver->getClosureType($scopeToPass, $arg->value, false, $storage),
374+
nativeType: $this->closureTypeResolver->getClosureType($scopeToPass->doNotTreatPhpDocTypesAsCertain(), $arg->value, false, $storage),
376375
typeCallback: null,
377376
specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(),
378377
);
@@ -418,7 +417,6 @@ public function processArgs(
418417
$impurePoints = array_merge($impurePoints, $closureResult->getImpurePoints());
419418
}
420419

421-
$closureTypeResolver = $this->container->getByType(ClosureTypeResolver::class);
422420
$storedClosureResult = $this->expressionResultFactory->create(
423421
$closureResult->getScope(),
424422
$scopeToPass,
@@ -428,7 +426,7 @@ public function processArgs(
428426
isAlwaysTerminating: false,
429427
throwPoints: [],
430428
impurePoints: [],
431-
type: $closureTypeResolver->buildClosureTypeForClosure(
429+
type: $this->closureTypeResolver->buildClosureTypeForClosure(
432430
$scopeToPass,
433431
$arg->value,
434432
$closureResult->getGatheredReturnStatements(),
@@ -442,7 +440,7 @@ public function processArgs(
442440
),
443441
// the native flavour reads the stored native types off the same
444442
// single body walk - no second walk on the promoted scope
445-
nativeType: $closureTypeResolver->buildClosureTypeForClosure(
443+
nativeType: $this->closureTypeResolver->buildClosureTypeForClosure(
446444
$scopeToPass,
447445
$arg->value,
448446
$closureResult->getGatheredReturnStatements(),
@@ -512,7 +510,6 @@ public function processArgs(
512510
// consume mode alone (the nullsafe call's plain-twin walk) is the
513511
// first and only walk of the argument - a miss means walk it
514512
if ($storedClosureArgResult === null && $nodeScopeResolver->isReturningStoredExpressionResults()) {
515-
$closureTypeResolver = $this->container->getByType(ClosureTypeResolver::class);
516513
$storedClosureArgResult = $this->expressionResultFactory->create(
517514
$scopeToPass,
518515
beforeScope: $scopeToPass,
@@ -521,8 +518,8 @@ public function processArgs(
521518
isAlwaysTerminating: false,
522519
throwPoints: [],
523520
impurePoints: [],
524-
type: $closureTypeResolver->getClosureType($scopeToPass, $arg->value, false, $storage),
525-
nativeType: $closureTypeResolver->getClosureType($scopeToPass->doNotTreatPhpDocTypesAsCertain(), $arg->value, false, $storage),
521+
type: $this->closureTypeResolver->getClosureType($scopeToPass, $arg->value, false, $storage),
522+
nativeType: $this->closureTypeResolver->getClosureType($scopeToPass->doNotTreatPhpDocTypesAsCertain(), $arg->value, false, $storage),
526523
typeCallback: null,
527524
specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(),
528525
);
@@ -566,13 +563,12 @@ public function processArgs(
566563
$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), $arrowFunctionExprResult->getThrowPoints()));
567564
$impurePoints = array_merge($impurePoints, $arrowFunctionExprResult->getImpurePoints());
568565
}
569-
$arrowFunctionClosureTypeResolver = $this->container->getByType(ClosureTypeResolver::class);
570566
$arrowFunctionScope = $arrowFunctionResult->getArrowFunctionScope();
571567
// both flavours are built from the single body walk (see
572568
// ArrowFunctionHandler); the built type also answers the
573569
// invalidate-expressions read below without re-walking the
574570
// still-unstored node through Scope::getType()
575-
$arrowFunctionType = $arrowFunctionClosureTypeResolver->buildClosureTypeForArrowFunction(
571+
$arrowFunctionType = $this->closureTypeResolver->buildClosureTypeForArrowFunction(
576572
$scopeToPass,
577573
$arg->value,
578574
$arrowFunctionScope,
@@ -592,7 +588,7 @@ public function processArgs(
592588
throwPoints: $arrowFunctionExprResult->getThrowPoints(),
593589
impurePoints: $arrowFunctionExprResult->getImpurePoints(),
594590
type: $arrowFunctionType,
595-
nativeType: $arrowFunctionClosureTypeResolver->buildClosureTypeForArrowFunction(
591+
nativeType: $this->closureTypeResolver->buildClosureTypeForArrowFunction(
596592
$scopeToPass,
597593
$arg->value,
598594
$arrowFunctionScope,
@@ -932,7 +928,7 @@ private function gatherClosureArgType(array $parametersAcceptors, int $i, Expr $
932928
$scope = $scope->pushInFunctionCall(null, $rawParameter, false);
933929
}
934930

935-
return $this->container->getByType(ClosureParameterResolver::class)->resolveCallableTypeForScope($closureExpr, $scope);
931+
return $this->closureParameterResolver->resolveCallableTypeForScope($closureExpr, $scope);
936932
}
937933

938934
/**

‎src/Analyser/ClosureProcessor.php‎

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,23 @@ final class ClosureProcessor
4747
public function __construct(
4848
private Container $container,
4949
private ExpressionResultFactory $expressionResultFactory,
50+
private ClosureParameterResolver $closureParameterResolver,
51+
private ClosureTypeResolver $closureTypeResolver,
5052
)
5153
{
5254
}
5355

56+
/**
57+
* Looked up instead of injected: parameters walk their attributes, attribute
58+
* arguments are call arguments walked by ArgumentsHandler, and those walk
59+
* closure arguments through this class - a closure in an attribute argument
60+
* recurses back here, which constructor injection cannot express.
61+
*/
62+
private function getParametersProcessor(): ParametersProcessor
63+
{
64+
return $this->container->getByType(ParametersProcessor::class);
65+
}
66+
5467
/**
5568
* @param callable(Node $node, Scope $scope): void $nodeCallback
5669
*/
@@ -84,12 +97,12 @@ private function processClosureNodeInternal(
8497
?Type $nativePassedToType = null,
8598
): ProcessClosureResult
8699
{
87-
$this->container->getByType(ParametersProcessor::class)->processParams($nodeScopeResolver, $stmt, $expr->params, $scope, $storage, $nodeCallback);
100+
$this->getParametersProcessor()->processParams($nodeScopeResolver, $stmt, $expr->params, $scope, $storage, $nodeCallback);
88101

89102
$byRefUses = [];
90103

91104
$closureCallArgs = $expr->getAttribute(ClosureArgVisitor::ATTRIBUTE_NAME);
92-
$parameterTypes = $this->container->getByType(ClosureParameterResolver::class)->resolve($scope, $expr, $storage, $closureCallArgs, $passedToType, $nativePassedToType);
105+
$parameterTypes = $this->closureParameterResolver->resolve($scope, $expr, $storage, $closureCallArgs, $passedToType, $nativePassedToType);
93106
$callableParameters = $parameterTypes->parameters;
94107
$nativeCallableParameters = $parameterTypes->nativeParameters;
95108

@@ -108,7 +121,7 @@ private function processClosureNodeInternal(
108121
// a call's type is carried by the context (see
109122
// ExpressionContext::enterAssignRightSideCallArgs()); a closure
110123
// right side resolves through the closure type resolver
111-
$inAssignRightSideType = $context->getInAssignRightSideType() ?? $this->container->getByType(ClosureParameterResolver::class)->resolveCallableTypeForScope($inAssignRightSideExpr, $scope);
124+
$inAssignRightSideType = $context->getInAssignRightSideType() ?? $this->closureParameterResolver->resolveCallableTypeForScope($inAssignRightSideExpr, $scope);
112125
if ($inAssignRightSideType instanceof ClosureType) {
113126
$variableType = $inAssignRightSideType;
114127
} else {
@@ -119,7 +132,7 @@ private function processClosureNodeInternal(
119132
$variableType = TypeCombinator::union($scope->getVariableType($inAssignRightSideVariableName), $inAssignRightSideType);
120133
}
121134
}
122-
$inAssignRightSideNativeType = $context->getInAssignRightSideNativeType() ?? $this->container->getByType(ClosureParameterResolver::class)->resolveCallableTypeForScope($inAssignRightSideExpr, $scope->doNotTreatPhpDocTypesAsCertain());
135+
$inAssignRightSideNativeType = $context->getInAssignRightSideNativeType() ?? $this->closureParameterResolver->resolveCallableTypeForScope($inAssignRightSideExpr, $scope->doNotTreatPhpDocTypesAsCertain());
123136
if ($inAssignRightSideNativeType instanceof ClosureType) {
124137
$variableNativeType = $inAssignRightSideNativeType;
125138
} else {
@@ -365,7 +378,7 @@ private function refineClosureNodeScope(
365378
ExpressionResultStorage $storage,
366379
): MutatingScope
367380
{
368-
$refinedClosureType = $this->container->getByType(ClosureTypeResolver::class)->buildClosureTypeForClosure(
381+
$refinedClosureType = $this->closureTypeResolver->buildClosureTypeForClosure(
369382
$scope,
370383
$expr,
371384
$gatheredReturnStatementsWithScope,
@@ -421,13 +434,13 @@ public function processArrowFunctionNode(
421434
): ProcessArrowFunctionResult
422435
{
423436
$context ??= ExpressionContext::createTopLevel();
424-
$this->container->getByType(ParametersProcessor::class)->processParams($nodeScopeResolver, $stmt, $expr->params, $scope, $storage, $nodeCallback);
437+
$this->getParametersProcessor()->processParams($nodeScopeResolver, $stmt, $expr->params, $scope, $storage, $nodeCallback);
425438
if ($expr->returnType !== null) {
426439
$nodeScopeResolver->callNodeCallback($nodeCallback, $expr->returnType, $scope, $storage);
427440
}
428441

429442
$arrowFunctionCallArgs = $expr->getAttribute(ArrowFunctionArgVisitor::ATTRIBUTE_NAME);
430-
$parameterTypes = $this->container->getByType(ClosureParameterResolver::class)->resolve($scope, $expr, $storage, $arrowFunctionCallArgs, $passedToType, $nativePassedToType);
443+
$parameterTypes = $this->closureParameterResolver->resolve($scope, $expr, $storage, $arrowFunctionCallArgs, $passedToType, $nativePassedToType);
431444
$callableParameters = $parameterTypes->parameters;
432445
$nativeCallableParameters = $parameterTypes->nativeParameters;
433446
$arrowFunctionScope = $scope->enterArrowFunction($expr, $callableParameters, $nativeCallableParameters);
@@ -482,7 +495,7 @@ public function processArrowFunctionNode(
482495
// run, build the refined arrow function type from the body expression's
483496
// stored type (no second walk) and fire InArrowFunctionNode with it, so the
484497
// node and the return-type rules see the refined expected return.
485-
$refinedArrowFunctionType = $this->container->getByType(ClosureTypeResolver::class)->buildClosureTypeForArrowFunction(
498+
$refinedArrowFunctionType = $this->closureTypeResolver->buildClosureTypeForArrowFunction(
486499
$scope,
487500
$expr,
488501
$arrowFunctionScope,

0 commit comments

Comments
 (0)