Skip to content

Commit 5659924

Browse files
phpstan-botVincentLangletclaude
authored
Do not use callable parameter types as native types for closure and arrow function parameters (#5632)
Co-authored-by: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent dbb0021 commit 5659924

8 files changed

Lines changed: 382 additions & 124 deletions

File tree

src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use PHPStan\Parser\ImmediatelyInvokedClosureVisitor;
2525
use PHPStan\Reflection\Callables\SimpleImpurePoint;
2626
use PHPStan\Reflection\Callables\SimpleThrowPoint;
27+
use PHPStan\Reflection\ExtendedParameterReflection;
2728
use PHPStan\Reflection\Native\NativeParameterReflection;
2829
use PHPStan\Reflection\PassedByReference;
2930
use PHPStan\Reflection\Php\DummyParameter;
@@ -97,29 +98,35 @@ public function getClosureType(
9798
}
9899

99100
$callableParameters = null;
101+
$nativeCallableParameters = null;
100102
$arrayMapArgs = $expr->getAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME);
101103
$immediatelyInvokedArgs = $expr->getAttribute(ImmediatelyInvokedClosureVisitor::ARGS_ATTRIBUTE_NAME);
102104
if ($arrayMapArgs !== null) {
103105
$callableParameters = [];
106+
$nativeCallableParameters = [];
104107
foreach ($arrayMapArgs as $funcCallArg) {
105108
$callableParameters[] = new DummyParameter('item', $scope->getType($funcCallArg->value)->getIterableValueType(), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null);
109+
$nativeCallableParameters[] = new DummyParameter('item', $scope->getNativeType($funcCallArg->value)->getIterableValueType(), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null);
106110
}
107111
} elseif ($immediatelyInvokedArgs !== null) {
108112
foreach ($immediatelyInvokedArgs as $immediatelyInvokedArg) {
109113
$callableParameters[] = new DummyParameter('item', $scope->getType($immediatelyInvokedArg->value), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null);
114+
$nativeCallableParameters[] = new DummyParameter('item', $scope->getNativeType($immediatelyInvokedArg->value), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null);
110115
}
111116
} else {
112117
$inFunctionCallsStackCount = count($scope->inFunctionCallsStack);
113118
if ($inFunctionCallsStackCount > 0) {
114119
[, $inParameter] = $scope->inFunctionCallsStack[$inFunctionCallsStackCount - 1];
115120
if ($inParameter !== null) {
116121
$callableParameters = $this->nodeScopeResolver->createCallableParameters($scope, $expr, null, $inParameter->getType());
122+
$nativeType = $inParameter instanceof ExtendedParameterReflection ? $inParameter->getNativeType() : $inParameter->getType();
123+
$nativeCallableParameters = $this->nodeScopeResolver->createNativeCallableParameters($scope, $expr, null, $nativeType);
117124
}
118125
}
119126
}
120127

121128
if ($expr instanceof ArrowFunction) {
122-
$arrowScope = $scope->enterArrowFunctionWithoutReflection($expr, $callableParameters);
129+
$arrowScope = $scope->enterArrowFunctionWithoutReflection($expr, $callableParameters, $nativeCallableParameters);
123130

124131
if ($expr->expr instanceof Yield_ || $expr->expr instanceof YieldFrom) {
125132
$yieldNode = $expr->expr;
@@ -232,7 +239,7 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu
232239

233240
self::$resolveClosureTypeDepth++;
234241

235-
$closureScope = $scope->enterAnonymousFunctionWithoutReflection($expr, $callableParameters);
242+
$closureScope = $scope->enterAnonymousFunctionWithoutReflection($expr, $callableParameters, $nativeCallableParameters);
236243
$closureReturnStatements = [];
237244
$closureYieldStatements = [];
238245
$onlyNeverExecutionEnds = null;

src/Analyser/MutatingScope.php

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1970,18 +1970,20 @@ public function isInClosureBind(): bool
19701970
/**
19711971
* @api
19721972
* @param ParameterReflection[]|null $callableParameters
1973+
* @param ParameterReflection[]|null $nativeCallableParameters
19731974
*/
19741975
public function enterAnonymousFunction(
19751976
Expr\Closure $closure,
19761977
?array $callableParameters,
1978+
?array $nativeCallableParameters = null,
19771979
): self
19781980
{
19791981
$anonymousFunctionReflection = $this->resolveType('__phpstanClosure', $closure);
19801982
if (!$anonymousFunctionReflection instanceof ClosureType) {
19811983
throw new ShouldNotHappenException();
19821984
}
19831985

1984-
$scope = $this->enterAnonymousFunctionWithoutReflection($closure, $callableParameters);
1986+
$scope = $this->enterAnonymousFunctionWithoutReflection($closure, $callableParameters, $nativeCallableParameters);
19851987

19861988
return $this->scopeFactory->create(
19871989
$scope->context,
@@ -2005,10 +2007,12 @@ public function enterAnonymousFunction(
20052007

20062008
/**
20072009
* @param ParameterReflection[]|null $callableParameters
2010+
* @param ParameterReflection[]|null $nativeCallableParameters
20082011
*/
20092012
public function enterAnonymousFunctionWithoutReflection(
20102013
Expr\Closure $closure,
20112014
?array $callableParameters,
2015+
?array $nativeCallableParameters,
20122016
): self
20132017
{
20142018
$expressionTypes = [];
@@ -2019,13 +2023,15 @@ public function enterAnonymousFunctionWithoutReflection(
20192023
}
20202024
$paramExprString = sprintf('$%s', $parameter->var->name);
20212025
$isNullable = $this->isParameterValueNullable($parameter);
2022-
$parameterType = $this->getFunctionType($parameter->type, $isNullable, $parameter->variadic);
2026+
$nativeParameterType = $parameterType = $this->getFunctionType($parameter->type, $isNullable, $parameter->variadic);
20232027
if ($callableParameters !== null) {
20242028
$parameterType = self::intersectButNotNever($parameterType, $this->getCallableParameterType($parameter, $callableParameters, $i));
20252029
}
2026-
$holder = ExpressionTypeHolder::createYes($parameter->var, $parameterType);
2027-
$expressionTypes[$paramExprString] = $holder;
2028-
$nativeTypes[$paramExprString] = $holder;
2030+
if ($nativeCallableParameters !== null) {
2031+
$nativeParameterType = self::intersectButNotNever($nativeParameterType, $this->getCallableParameterType($parameter, $nativeCallableParameters, $i));
2032+
}
2033+
$expressionTypes[$paramExprString] = ExpressionTypeHolder::createYes($parameter->var, $parameterType);
2034+
$nativeTypes[$paramExprString] = ExpressionTypeHolder::createYes($parameter->var, $nativeParameterType);
20292035
}
20302036

20312037
$nonRefVariableNames = [];
@@ -2181,15 +2187,16 @@ private function invalidateStaticExpressions(array $expressionTypes): array
21812187
/**
21822188
* @api
21832189
* @param ParameterReflection[]|null $callableParameters
2190+
* @param ParameterReflection[]|null $nativeCallableParameters
21842191
*/
2185-
public function enterArrowFunction(Expr\ArrowFunction $arrowFunction, ?array $callableParameters): self
2192+
public function enterArrowFunction(Expr\ArrowFunction $arrowFunction, ?array $callableParameters, ?array $nativeCallableParameters = null): self
21862193
{
21872194
$anonymousFunctionReflection = $this->resolveType('__phpStanArrowFn', $arrowFunction);
21882195
if (!$anonymousFunctionReflection instanceof ClosureType) {
21892196
throw new ShouldNotHappenException();
21902197
}
21912198

2192-
$scope = $this->enterArrowFunctionWithoutReflection($arrowFunction, $callableParameters);
2199+
$scope = $this->enterArrowFunctionWithoutReflection($arrowFunction, $callableParameters, $nativeCallableParameters);
21932200

21942201
return $this->scopeFactory->create(
21952202
$scope->context,
@@ -2213,21 +2220,25 @@ public function enterArrowFunction(Expr\ArrowFunction $arrowFunction, ?array $ca
22132220

22142221
/**
22152222
* @param ParameterReflection[]|null $callableParameters
2223+
* @param ParameterReflection[]|null $nativeCallableParameters
22162224
*/
2217-
public function enterArrowFunctionWithoutReflection(Expr\ArrowFunction $arrowFunction, ?array $callableParameters): self
2225+
public function enterArrowFunctionWithoutReflection(Expr\ArrowFunction $arrowFunction, ?array $callableParameters, ?array $nativeCallableParameters): self
22182226
{
22192227
$arrowFunctionScope = $this;
22202228
foreach ($arrowFunction->params as $i => $parameter) {
22212229
$isNullable = $this->isParameterValueNullable($parameter);
2222-
$parameterType = $this->getFunctionType($parameter->type, $isNullable, $parameter->variadic);
2230+
$nativeParameterType = $parameterType = $this->getFunctionType($parameter->type, $isNullable, $parameter->variadic);
22232231
if ($callableParameters !== null) {
22242232
$parameterType = self::intersectButNotNever($parameterType, $this->getCallableParameterType($parameter, $callableParameters, $i));
22252233
}
2234+
if ($nativeCallableParameters !== null) {
2235+
$nativeParameterType = self::intersectButNotNever($nativeParameterType, $this->getCallableParameterType($parameter, $nativeCallableParameters, $i));
2236+
}
22262237

22272238
if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) {
22282239
throw new ShouldNotHappenException();
22292240
}
2230-
$arrowFunctionScope = $arrowFunctionScope->assignVariable($parameter->var->name, $parameterType, $parameterType, TrinaryLogic::createYes());
2241+
$arrowFunctionScope = $arrowFunctionScope->assignVariable($parameter->var->name, $parameterType, $nativeParameterType, TrinaryLogic::createYes());
22312242
}
22322243

22332244
if ($arrowFunction->static) {

src/Analyser/NodeScopeResolver.php

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2837,6 +2837,7 @@ public function processClosureNode(
28372837
callable $nodeCallback,
28382838
ExpressionContext $context,
28392839
?Type $passedToType,
2840+
?Type $nativePassedToType = null,
28402841
): ProcessClosureResult
28412842
{
28422843
foreach ($expr->params as $param) {
@@ -2846,12 +2847,8 @@ public function processClosureNode(
28462847
$byRefUses = [];
28472848

28482849
$closureCallArgs = $expr->getAttribute(ClosureArgVisitor::ATTRIBUTE_NAME);
2849-
$callableParameters = $this->createCallableParameters(
2850-
$scope,
2851-
$expr,
2852-
$closureCallArgs,
2853-
$passedToType,
2854-
);
2850+
$callableParameters = $this->createCallableParameters($scope, $expr, $closureCallArgs, $passedToType);
2851+
$nativeCallableParameters = $this->createNativeCallableParameters($scope, $expr, $closureCallArgs, $nativePassedToType);
28552852

28562853
$useScope = $scope;
28572854
foreach ($expr->uses as $use) {
@@ -2902,7 +2899,7 @@ public function processClosureNode(
29022899
$this->callNodeCallback($nodeCallback, $expr->returnType, $scope, $storage);
29032900
}
29042901

2905-
$closureScope = $scope->enterAnonymousFunction($expr, $callableParameters);
2902+
$closureScope = $scope->enterAnonymousFunction($expr, $callableParameters, $nativeCallableParameters);
29062903
$closureScope = $closureScope->processClosureScope($scope, null, $byRefUses);
29072904
$closureType = $closureScope->getAnonymousFunctionReflection();
29082905
if (!$closureType instanceof ClosureType) {
@@ -2984,7 +2981,7 @@ public function processClosureNode(
29842981
break;
29852982
}
29862983

2987-
$closureScope = $scope->enterAnonymousFunction($expr, $callableParameters);
2984+
$closureScope = $scope->enterAnonymousFunction($expr, $callableParameters, $nativeCallableParameters);
29882985
$closureScope = $closureScope->processClosureScope($intermediaryClosureScope, $prevScope, $byRefUses);
29892986

29902987
if ($closureScope->equals($prevScope)) {
@@ -3049,6 +3046,7 @@ public function processArrowFunctionNode(
30493046
ExpressionResultStorage $storage,
30503047
callable $nodeCallback,
30513048
?Type $passedToType,
3049+
?Type $nativePassedToType = null,
30523050
): ExpressionResult
30533051
{
30543052
foreach ($expr->params as $param) {
@@ -3059,12 +3057,9 @@ public function processArrowFunctionNode(
30593057
}
30603058

30613059
$arrowFunctionCallArgs = $expr->getAttribute(ArrowFunctionArgVisitor::ATTRIBUTE_NAME);
3062-
$arrowFunctionScope = $scope->enterArrowFunction($expr, $this->createCallableParameters(
3063-
$scope,
3064-
$expr,
3065-
$arrowFunctionCallArgs,
3066-
$passedToType,
3067-
));
3060+
$callableParameters = $this->createCallableParameters($scope, $expr, $arrowFunctionCallArgs, $passedToType);
3061+
$nativeCallableParameters = $this->createNativeCallableParameters($scope, $expr, $arrowFunctionCallArgs, $nativePassedToType);
3062+
$arrowFunctionScope = $scope->enterArrowFunction($expr, $callableParameters, $nativeCallableParameters);
30683063
$arrowFunctionType = $arrowFunctionScope->getAnonymousFunctionReflection();
30693064
if ($arrowFunctionType === null) {
30703065
throw new ShouldNotHappenException();
@@ -3076,14 +3071,33 @@ public function processArrowFunctionNode(
30763071
}
30773072

30783073
/**
3079-
* @param Node\Arg[] $args
3074+
* @param Node\Arg[]|null $args
30803075
* @return ParameterReflection[]|null
30813076
*/
30823077
public function createCallableParameters(Scope $scope, Expr $closureExpr, ?array $args, ?Type $passedToType): ?array
3078+
{
3079+
return $this->doCreateCallableParameters($scope, $closureExpr, $args, $passedToType, static fn (Scope $s, Expr $e) => $s->getType($e));
3080+
}
3081+
3082+
/**
3083+
* @param Node\Arg[]|null $args
3084+
* @return ParameterReflection[]|null
3085+
*/
3086+
public function createNativeCallableParameters(Scope $scope, Expr $closureExpr, ?array $args, ?Type $nativePassedToType): ?array
3087+
{
3088+
return $this->doCreateCallableParameters($scope, $closureExpr, $args, $nativePassedToType, static fn (Scope $s, Expr $e) => $s->getNativeType($e));
3089+
}
3090+
3091+
/**
3092+
* @param Node\Arg[]|null $args
3093+
* @param Closure(Scope, Expr): Type $typeGetter
3094+
* @return ParameterReflection[]|null
3095+
*/
3096+
private function doCreateCallableParameters(Scope $scope, Expr $closureExpr, ?array $args, ?Type $passedToType, Closure $typeGetter): ?array
30833097
{
30843098
$callableParameters = null;
30853099
if ($args !== null) {
3086-
$closureType = $scope->getType($closureExpr);
3100+
$closureType = $typeGetter($scope, $closureExpr);
30873101

30883102
if ($closureType->isCallable()->no()) {
30893103
return null;
@@ -3100,12 +3114,13 @@ public function createCallableParameters(Scope $scope, Expr $closureExpr, ?array
31003114

31013115
if ($callableParameter->isVariadic()) {
31023116
$argTypes = [];
3103-
for ($j = $index; $j < count($args); $j++) {
3104-
$argTypes[] = $scope->getType($args[$j]->value);
3117+
$argNumber = count($args);
3118+
for ($j = $index; $j < $argNumber; $j++) {
3119+
$argTypes[] = $typeGetter($scope, $args[$j]->value);
31053120
}
31063121
$type = TypeCombinator::union(...$argTypes);
31073122
} else {
3108-
$type = $scope->getType($args[$index]->value);
3123+
$type = $typeGetter($scope, $args[$index]->value);
31093124
}
31103125
$callableParameters[$index] = new NativeParameterReflection(
31113126
$callableParameter->getName(),
@@ -3524,7 +3539,7 @@ public function processArgs(
35243539
}
35253540

35263541
$this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $storage, $context);
3527-
$closureResult = $this->processClosureNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $context, $parameterType ?? null);
3542+
$closureResult = $this->processClosureNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $context, $parameterType ?? null, $parameterNativeType);
35283543
if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) {
35293544
$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()));
35303545
$impurePoints = array_merge($impurePoints, $closureResult->getImpurePoints());
@@ -3583,7 +3598,7 @@ public function processArgs(
35833598
}
35843599

35853600
$this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $storage, $context);
3586-
$arrowFunctionResult = $this->processArrowFunctionNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $parameterType ?? null);
3601+
$arrowFunctionResult = $this->processArrowFunctionNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $parameterType ?? null, $parameterNativeType);
35873602
if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) {
35883603
$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()));
35893604
$impurePoints = array_merge($impurePoints, $arrowFunctionResult->getImpurePoints());

0 commit comments

Comments
 (0)