Skip to content

Commit da4fd7a

Browse files
committed
Fix problem with closure parameter and generics
1 parent e3df582 commit da4fd7a

File tree

4 files changed

+85
-25
lines changed

4 files changed

+85
-25
lines changed

src/Rules/FunctionCallParametersCheck.php

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use PhpParser\Node;
66
use PhpParser\Node\Expr;
7+
use PHPStan\Analyser\MutatingScope;
78
use PHPStan\Analyser\Scope;
89
use PHPStan\Php\PhpVersion;
910
use PHPStan\Reflection\ParameterReflection;
@@ -79,7 +80,7 @@ public function check(
7980
$functionParametersMaxCount = -1;
8081
}
8182

82-
/** @var array<int, array{Expr, Type, bool, string|null, int}> $arguments */
83+
/** @var array<int, array{Expr, Type|null, bool, string|null, int}> $arguments */
8384
$arguments = [];
8485
/** @var array<int, Node\Arg> $args */
8586
$args = $funcCall->getArgs();
@@ -172,7 +173,7 @@ public function check(
172173

173174
$arguments[] = [
174175
$arg->value,
175-
$type,
176+
null,
176177
false,
177178
$argumentName,
178179
$arg->getStartLine(),
@@ -277,6 +278,17 @@ public function check(
277278
continue;
278279
}
279280

281+
if ($argumentValueType === null) {
282+
if ($scope instanceof MutatingScope) {
283+
$scope = $scope->pushInFunctionCall(null, $parameter);
284+
}
285+
$argumentValueType = $scope->getType($argumentValue);
286+
287+
if ($scope instanceof MutatingScope) {
288+
$scope = $scope->popInFunctionCall();
289+
}
290+
}
291+
280292
if ($this->checkArgumentTypes) {
281293
$parameterType = TypeUtils::resolveLateResolvableTypes($parameter->getType());
282294

@@ -464,8 +476,8 @@ static function (Type $type, callable $traverse) use (&$returnTemplateTypes): Ty
464476
}
465477

466478
/**
467-
* @param array<int, array{Expr, Type, bool, string|null, int}> $arguments
468-
* @return array{list<IdentifierRuleError>, array<int, array{Expr, Type, bool, (string|null), int, (ParameterReflection|null), (ParameterReflection|null)}>}
479+
* @param array<int, array{Expr, Type|null, bool, string|null, int}> $arguments
480+
* @return array{list<IdentifierRuleError>, array<int, array{Expr, Type|null, bool, (string|null), int, (ParameterReflection|null), (ParameterReflection|null)}>}
469481
*/
470482
private function processArguments(
471483
ParametersAcceptor $parametersAcceptor,

tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ public function testCallToFunctionWithoutParameters(): void
3939
public function testCallToFunctionWithIncorrectParameters(): void
4040
{
4141
$setErrorHandlerError = PHP_VERSION_ID < 80000
42-
? 'Parameter #1 $callback of function set_error_handler expects (callable(int, string, string, int, array): bool)|null, Closure(mixed, mixed, mixed, mixed): void given.'
43-
: 'Parameter #1 $callback of function set_error_handler expects (callable(int, string, string, int): bool)|null, Closure(mixed, mixed, mixed, mixed): void given.';
42+
? 'Parameter #1 $callback of function set_error_handler expects (callable(int, string, string, int, array): bool)|null, Closure(int, string, string, int): void given.'
43+
: 'Parameter #1 $callback of function set_error_handler expects (callable(int, string, string, int): bool)|null, Closure(int, string, string, int): void given.';
4444

4545
require_once __DIR__ . '/data/incorrect-call-to-function-definition.php';
4646
$this->analyse([__DIR__ . '/data/incorrect-call-to-function.php'], [
@@ -558,14 +558,14 @@ public function testArrayReduceCallback(): void
558558
5,
559559
],
560560
[
561-
'Parameter #2 $callback of function array_reduce expects callable(non-empty-string|null, 1|2|3): (non-empty-string|null), Closure(string, int): non-empty-string given.',
561+
'Parameter #2 $callback of function array_reduce expects callable(non-empty-string|null, 1|2|3): (non-empty-string|null), Closure(non-empty-string, 1|2|3): non-falsy-string given.',
562562
13,
563-
'Type string of parameter #1 $foo of passed callable needs to be same or wider than parameter type string|null of accepting callable.',
563+
'Type non-empty-string of parameter #1 $foo of passed callable needs to be same or wider than parameter type non-empty-string|null of accepting callable.',
564564
],
565565
[
566-
'Parameter #2 $callback of function array_reduce expects callable(non-empty-string|null, 1|2|3): (non-empty-string|null), Closure(string, int): non-empty-string given.',
566+
'Parameter #2 $callback of function array_reduce expects callable(non-empty-string|null, 1|2|3): (non-empty-string|null), Closure(non-empty-string, 1|2|3): non-falsy-string given.',
567567
22,
568-
'Type string of parameter #1 $foo of passed callable needs to be same or wider than parameter type string|null of accepting callable.',
568+
'Type non-empty-string of parameter #1 $foo of passed callable needs to be same or wider than parameter type non-empty-string|null of accepting callable.',
569569
],
570570
]);
571571
}
@@ -578,14 +578,14 @@ public function testArrayReduceArrowFunctionCallback(): void
578578
5,
579579
],
580580
[
581-
'Parameter #2 $callback of function array_reduce expects callable(non-empty-string|null, 1|2|3): (non-empty-string|null), Closure(string, int): non-empty-string given.',
581+
'Parameter #2 $callback of function array_reduce expects callable(non-empty-string|null, 1|2|3): (non-empty-string|null), Closure(non-empty-string, 1|2|3): non-falsy-string given.',
582582
11,
583-
'Type string of parameter #1 $foo of passed callable needs to be same or wider than parameter type string|null of accepting callable.',
583+
'Type non-empty-string of parameter #1 $foo of passed callable needs to be same or wider than parameter type non-empty-string|null of accepting callable.',
584584
],
585585
[
586-
'Parameter #2 $callback of function array_reduce expects callable(non-empty-string|null, 1|2|3): (non-empty-string|null), Closure(string, int): non-empty-string given.',
586+
'Parameter #2 $callback of function array_reduce expects callable(non-empty-string|null, 1|2|3): (non-empty-string|null), Closure(non-empty-string, 1|2|3): non-falsy-string given.',
587587
18,
588-
'Type string of parameter #1 $foo of passed callable needs to be same or wider than parameter type string|null of accepting callable.',
588+
'Type non-empty-string of parameter #1 $foo of passed callable needs to be same or wider than parameter type non-empty-string|null of accepting callable.',
589589
],
590590
]);
591591
}
@@ -598,11 +598,11 @@ public function testArrayWalkCallback(): void
598598
6,
599599
],
600600
[
601-
'Parameter #2 $callback of function array_walk expects callable(1|2, \'bar\'|\'foo\', \'extra\'): mixed, Closure(int, string, int): \'\' given.',
601+
'Parameter #2 $callback of function array_walk expects callable(1|2, \'bar\'|\'foo\', \'extra\'): mixed, Closure(1|2, \'bar\'|\'foo\', int): \'\' given.',
602602
14,
603603
],
604604
[
605-
'Parameter #2 $callback of function array_walk expects callable(1|2, \'bar\'|\'foo\'): mixed, Closure(int, string, int): \'\' given.',
605+
'Parameter #2 $callback of function array_walk expects callable(1|2, \'bar\'|\'foo\'): mixed, Closure(1|2, \'bar\'|\'foo\', int): \'\' given.',
606606
23,
607607
'Parameter #3 $extra of passed callable is required but accepting callable does not have that parameter. It will be called without it.',
608608
],
@@ -617,11 +617,11 @@ public function testArrayWalkArrowFunctionCallback(): void
617617
6,
618618
],
619619
[
620-
'Parameter #2 $callback of function array_walk expects callable(1|2, \'bar\'|\'foo\', \'extra\'): mixed, Closure(int, string, int): \'\' given.',
620+
'Parameter #2 $callback of function array_walk expects callable(1|2, \'bar\'|\'foo\', \'extra\'): mixed, Closure(1|2, \'bar\'|\'foo\', int): \'\' given.',
621621
12,
622622
],
623623
[
624-
'Parameter #2 $callback of function array_walk expects callable(1|2, \'bar\'|\'foo\'): mixed, Closure(int, string, int): \'\' given.',
624+
'Parameter #2 $callback of function array_walk expects callable(1|2, \'bar\'|\'foo\'): mixed, Closure(1|2, \'bar\'|\'foo\', int): \'\' given.',
625625
19,
626626
'Parameter #3 $extra of passed callable is required but accepting callable does not have that parameter. It will be called without it.',
627627
],
@@ -636,7 +636,7 @@ public function testArrayUdiffCallback(): void
636636
6,
637637
],
638638
[
639-
'Parameter #3 $data_comp_func of function array_udiff expects callable(1|2|3|4|5|6, 1|2|3|4|5|6): int, Closure(int, int): non-falsy-string given.',
639+
'Parameter #3 $data_comp_func of function array_udiff expects callable(1|2|3|4|5|6, 1|2|3|4|5|6): int, Closure(1|2|3|4|5|6, 1|2|3|4|5|6): (literal-string&non-falsy-string) given.',
640640
14,
641641
],
642642
[
@@ -666,7 +666,7 @@ public function testPregReplaceCallback(): void
666666
13,
667667
],
668668
[
669-
'Parameter #2 $callback of function preg_replace_callback expects callable(array<int|string, string>): string, Closure(array): void given.',
669+
'Parameter #2 $callback of function preg_replace_callback expects callable(array<int|string, string>): string, Closure(array<int|string, string>): void given.',
670670
20,
671671
],
672672
[
@@ -688,7 +688,7 @@ public function testMbEregReplaceCallback(): void
688688
13,
689689
],
690690
[
691-
'Parameter #2 $callback of function mb_ereg_replace_callback expects callable(array<int|string, string>): string, Closure(array): void given.',
691+
'Parameter #2 $callback of function mb_ereg_replace_callback expects callable(array<int|string, string>): string, Closure(array<int|string, string>): void given.',
692692
20,
693693
],
694694
[
@@ -1617,11 +1617,11 @@ public function testDiscussion10454(): void
16171617
{
16181618
$this->analyse([__DIR__ . '/data/discussion-10454.php'], [
16191619
[
1620-
"Parameter #2 \$callback of function array_filter expects (callable('bar'|'baz'|'foo'|'quux'|'qux'): bool)|null, Closure(string): stdClass given.",
1620+
"Parameter #2 \$callback of function array_filter expects (callable('bar'|'baz'|'foo'|'quux'|'qux'): bool)|null, Closure('bar'|'baz'|'foo'|'quux'|'qux'): stdClass given.",
16211621
13,
16221622
],
16231623
[
1624-
"Parameter #2 \$callback of function array_filter expects (callable('bar'|'baz'|'foo'|'quux'|'qux'): bool)|null, Closure(string): stdClass given.",
1624+
"Parameter #2 \$callback of function array_filter expects (callable('bar'|'baz'|'foo'|'quux'|'qux'): bool)|null, Closure('bar'|'baz'|'foo'|'quux'|'qux'): stdClass given.",
16251625
23,
16261626
],
16271627
]);

tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,7 @@ public function testCallMethods(): void
504504
1512,
505505
],
506506
[
507-
'Parameter #1 $a of method Test\\CallableWithMixedArray::doBar() expects callable(array<string>): array<string>, Closure(array): (array{\'foo\'}|null) given.',
507+
'Parameter #1 $a of method Test\\CallableWithMixedArray::doBar() expects callable(array<string>): array<string>, Closure(array<string>): (array{\'foo\'}|null) given.',
508508
1533,
509509
],
510510
[
@@ -828,7 +828,7 @@ public function testCallMethodsOnThisOnly(): void
828828
1512,
829829
],
830830
[
831-
'Parameter #1 $a of method Test\\CallableWithMixedArray::doBar() expects callable(array<string>): array<string>, Closure(array): (array{\'foo\'}|null) given.',
831+
'Parameter #1 $a of method Test\\CallableWithMixedArray::doBar() expects callable(array<string>): array<string>, Closure(array<string>): (array{\'foo\'}|null) given.',
832832
1533,
833833
],
834834
[
@@ -3300,4 +3300,14 @@ public function testPureCallable(): void
33003300
]);
33013301
}
33023302

3303+
public function testClosureParameterGenerics(): void
3304+
{
3305+
$this->checkThisOnly = false;
3306+
$this->checkNullables = true;
3307+
$this->checkUnionTypes = true;
3308+
$this->checkExplicitMixed = true;
3309+
3310+
$this->analyse([__DIR__ . '/data/closure-parameter-generics.php'], []);
3311+
}
3312+
33033313
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
namespace ClosureParameterGenerics;
4+
5+
use Closure;
6+
7+
class Transaction
8+
{
9+
10+
}
11+
12+
class RetryableTransaction extends Transaction
13+
{
14+
15+
}
16+
17+
class Foo
18+
{
19+
20+
/**
21+
* @param Closure(RetryableTransaction $transaction): T $callback
22+
* @return T
23+
*
24+
* @template T
25+
*/
26+
public function retryableTransaction(Closure $callback)
27+
{
28+
29+
}
30+
31+
public function doFoo(): void
32+
{
33+
$this->retryableTransaction(function (Transaction $tr) {
34+
return $tr;
35+
});
36+
}
37+
38+
}

0 commit comments

Comments
 (0)