Skip to content

Commit bdec861

Browse files
phpstan-botclaude
andcommitted
Compute native callable parameters from array args for array_map and array_filter
For constant arrays passed to array_map/array_filter, the native types are known (e.g. literal string unions), but the callback parameter's native PHP type is just `callable` which loses this information. Tag array_filter callbacks with the array expression (like ArrayMapArgVisitor does for array_map), then compute native callable parameters from the actual array arguments' native types in processClosureNode, processArrowFunctionNode, and ClosureTypeResolver. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8e47fa3 commit bdec861

3 files changed

Lines changed: 33 additions & 5 deletions

File tree

src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use PHPStan\Node\ExecutionEndNode;
2121
use PHPStan\Node\InvalidateExprNode;
2222
use PHPStan\Node\PropertyAssignNode;
23+
use PHPStan\Parser\ArrayFilterArgVisitor;
2324
use PHPStan\Parser\ArrayMapArgVisitor;
2425
use PHPStan\Parser\ImmediatelyInvokedClosureVisitor;
2526
use PHPStan\Reflection\ExtendedParameterReflection;
@@ -99,12 +100,13 @@ public function getClosureType(
99100

100101
$callableParameters = null;
101102
$nativeCallableParameters = null;
102-
$arrayMapArgs = $expr->getAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME);
103+
$arrayArgs = $expr->getAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME)
104+
?? $expr->getAttribute(ArrayFilterArgVisitor::CALLBACK_ATTRIBUTE_NAME);
103105
$immediatelyInvokedArgs = $expr->getAttribute(ImmediatelyInvokedClosureVisitor::ARGS_ATTRIBUTE_NAME);
104-
if ($arrayMapArgs !== null) {
106+
if ($arrayArgs !== null) {
105107
$callableParameters = [];
106108
$nativeCallableParameters = [];
107-
foreach ($arrayMapArgs as $funcCallArg) {
109+
foreach ($arrayArgs as $funcCallArg) {
108110
$callableParameters[] = new DummyParameter('item', $scope->getType($funcCallArg->value)->getIterableValueType(), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null);
109111
$nativeCallableParameters[] = new DummyParameter('item', $scope->getNativeType($funcCallArg->value)->getIterableValueType(), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null);
110112
}

src/Analyser/NodeScopeResolver.php

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@
107107
use PHPStan\Node\UnreachableStatementNode;
108108
use PHPStan\Node\VariableAssignNode;
109109
use PHPStan\Node\VarTagChangedExpressionTypeNode;
110+
use PHPStan\Parser\ArrayFilterArgVisitor;
111+
use PHPStan\Parser\ArrayMapArgVisitor;
110112
use PHPStan\Parser\ArrowFunctionArgVisitor;
111113
use PHPStan\Parser\ClosureArgVisitor;
112114
use PHPStan\Parser\ImmediatelyInvokedClosureVisitor;
@@ -2717,7 +2719,8 @@ public function processClosureNode(
27172719

27182720
$closureCallArgs = $expr->getAttribute(ClosureArgVisitor::ATTRIBUTE_NAME);
27192721
$callableParameters = $this->createCallableParameters($scope, $expr, $closureCallArgs, $passedToType);
2720-
$nativeCallableParameters = $this->createNativeCallableParameters($scope, $expr, $closureCallArgs, $nativePassedToType);
2722+
$nativeCallableParameters = $this->createNativeCallableParametersFromArrayArgs($scope, $expr)
2723+
?? $this->createNativeCallableParameters($scope, $expr, $closureCallArgs, $nativePassedToType);
27212724

27222725
$useScope = $scope;
27232726
foreach ($expr->uses as $use) {
@@ -2927,7 +2930,8 @@ public function processArrowFunctionNode(
29272930

29282931
$arrowFunctionCallArgs = $expr->getAttribute(ArrowFunctionArgVisitor::ATTRIBUTE_NAME);
29292932
$callableParameters = $this->createCallableParameters($scope, $expr, $arrowFunctionCallArgs, $passedToType);
2930-
$nativeCallableParameters = $this->createNativeCallableParameters($scope, $expr, $arrowFunctionCallArgs, $nativePassedToType);
2933+
$nativeCallableParameters = $this->createNativeCallableParametersFromArrayArgs($scope, $expr)
2934+
?? $this->createNativeCallableParameters($scope, $expr, $arrowFunctionCallArgs, $nativePassedToType);
29312935
$arrowFunctionScope = $scope->enterArrowFunction($expr, $callableParameters, $nativeCallableParameters);
29322936
$arrowFunctionType = $arrowFunctionScope->getAnonymousFunctionReflection();
29332937
if ($arrowFunctionType === null) {
@@ -2957,6 +2961,23 @@ public function createNativeCallableParameters(Scope $scope, Expr $closureExpr,
29572961
return $this->doCreateCallableParameters($scope, $closureExpr, $args, $nativePassedToType, static fn (Scope $s, Expr $e) => $s->getNativeType($e));
29582962
}
29592963

2964+
/**
2965+
* @return ParameterReflection[]|null
2966+
*/
2967+
private function createNativeCallableParametersFromArrayArgs(Scope $scope, Expr $closureExpr): ?array
2968+
{
2969+
$arrayArgs = $closureExpr->getAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME)
2970+
?? $closureExpr->getAttribute(ArrayFilterArgVisitor::CALLBACK_ATTRIBUTE_NAME);
2971+
if ($arrayArgs === null) {
2972+
return null;
2973+
}
2974+
$params = [];
2975+
foreach ($arrayArgs as $funcCallArg) {
2976+
$params[] = new DummyParameter('item', $scope->getNativeType($funcCallArg->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null);
2977+
}
2978+
return $params;
2979+
}
2980+
29602981
/**
29612982
* @param Node\Arg[]|null $args
29622983
* @param Closure(Scope, Expr): Type $typeGetter

src/Parser/ArrayFilterArgVisitor.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ final class ArrayFilterArgVisitor extends NodeVisitorAbstract
1313

1414
public const ATTRIBUTE_NAME = 'isArrayFilterArg';
1515

16+
public const CALLBACK_ATTRIBUTE_NAME = 'arrayFilterCallbackArgs';
17+
1618
#[Override]
1719
public function enterNode(Node $node): ?Node
1820
{
@@ -23,6 +25,9 @@ public function enterNode(Node $node): ?Node
2325
if (isset($args[0])) {
2426
$args[0]->setAttribute(self::ATTRIBUTE_NAME, true);
2527
}
28+
if (isset($args[0]) && isset($args[1]) && !isset($args[2])) {
29+
$args[1]->value->setAttribute(self::CALLBACK_ATTRIBUTE_NAME, [$args[0]]);
30+
}
2631
}
2732
}
2833
return null;

0 commit comments

Comments
 (0)