Skip to content

Commit aa285d9

Browse files
ondrejmirtesclaude
andcommitted
Keep the declared type of a variadic closure parameter
A variadic parameter collects arguments of different positions into one list; spread again into positional parameters, the union of all of them fits none. Variadic parameters keep their declared type. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VpbB99tJfUmeArX74xqvHv
1 parent 282d9be commit aa285d9

5 files changed

Lines changed: 36 additions & 4 deletions

File tree

‎src/Analyser/Generics/ClosureSignatureInference.php‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,9 @@ public function getSignatureParameters(MutatingScope $scope, Closure|ArrowFuncti
132132

133133
$parameters = [];
134134
foreach ($declaredParameters as $parameter) {
135-
if (!$parameter->passedByReference()->no()) {
135+
// a variadic parameter collects arguments of different positions into
136+
// one list - spread again, one element type fits none of them
137+
if (!$parameter->passedByReference()->no() || $parameter->isVariadic()) {
136138
$parameters[] = $parameter;
137139
continue;
138140
}
@@ -210,7 +212,11 @@ public function getSignatureReturnType(MutatingScope $scope, Closure|ArrowFuncti
210212
return $returnType;
211213
}
212214
$frame = $this->getFrame($scope);
213-
if ($frame === null || (!$frame->isObservingClosures() && !$frame->isSettledClosureSite($expr)) || !self::returnsContextTypedExpression($expr)) {
215+
if (
216+
$frame === null
217+
|| (!$frame->isObservingClosures() && !$frame->isSettledClosureSite($expr))
218+
|| !self::returnsContextTypedExpression($expr)
219+
) {
214220
return $returnType;
215221
}
216222

‎tests/PHPStan/Analyser/nsrt/closure-signature-from-usages.php‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,7 @@ public function defaultAndVariadic(): void
408408
$c(1);
409409

410410
$d = function (...$xs) {
411-
assertType('array<int<0, max>|string, 1|2>', $xs);
411+
assertType('array<int<0, max>|string, mixed>', $xs);
412412
};
413413
$d(1, 2);
414414
}
@@ -505,4 +505,14 @@ public function settledClosureSentToGenericTarget(): void
505505
assertType('list<int|string>', takesConvertor($convertor));
506506
}
507507

508+
public function variadicParameterKeepsDeclaredType(): void
509+
{
510+
$inner = static fn (string $a, float $b): array => [$a, $b];
511+
$outer = static function (mixed ...$args) use ($inner): array {
512+
assertType('array<int<0, max>|string, mixed>', $args);
513+
return $inner(...$args);
514+
};
515+
$outer('x', 1.0);
516+
}
517+
508518
}

‎tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,12 @@ public function testBug15251(): void
449449
$this->analyse([__DIR__ . '/../Methods/data/bug-15251.php'], []);
450450
}
451451

452+
#[RequiresPhp('>= 8.0.0')]
453+
public function testClosureSignatureFromUsages(): void
454+
{
455+
$this->analyse([__DIR__ . '/data/closure-signature-from-usages.php'], []);
456+
}
457+
452458
#[RequiresPhp('>= 8.2.0')]
453459
public function testBug15285(): void
454460
{

‎tests/PHPStan/Rules/Functions/data/closure-signature-from-usages.php‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,8 @@ function (): void {
5252
return 'x' . $value;
5353
};
5454
takesConvertor($convertor);
55+
56+
$inner = static fn (string $a, float $b): array => [$a, $b];
57+
$outer = static fn (mixed ...$args): array => $inner(...$args);
58+
$outer('x', 1.0);
5559
};

‎turbo-ext/src/ClosureSignatureInference.cpp‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,13 @@ class ClosureSignatureInference
479479
if (UNEXPECTED(passedByReference.isUndef())) return zv::Val();
480480
zend_long mode = pt_passed_by_reference_mode(passedByReference.raw());
481481
if (UNEXPECTED(mode < 0)) return zv::Val();
482-
if (mode != PT_PASSED_BY_REFERENCE_NO) {
482+
bool isVariadicParameter = false;
483+
if (mode == PT_PASSED_BY_REFERENCE_NO) {
484+
zv::Val isVariadic = pt_parameter_reflection_call(parameter, PT_PR_IS_VARIADIC);
485+
if (UNEXPECTED(isVariadic.isUndef())) return zv::Val();
486+
isVariadicParameter = zend_is_true(isVariadic.raw());
487+
}
488+
if (mode != PT_PASSED_BY_REFERENCE_NO || isVariadicParameter) {
483489
parameters.push(zv::Val::copyOf(zv::Ref(parameter)));
484490
continue;
485491
}

0 commit comments

Comments
 (0)