Skip to content

Commit 3d0107c

Browse files
committed
apply all closure type parameter extensions in parameters check
1 parent 9beb618 commit 3d0107c

6 files changed

+280
-14
lines changed

src/Rules/FunctionCallParametersCheck.php

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
use PhpParser\Node;
66
use PhpParser\Node\Expr;
7+
use PhpParser\Node\Expr\CallLike;
8+
use PhpParser\Node\Expr\FuncCall;
9+
use PhpParser\Node\Expr\MethodCall;
10+
use PhpParser\Node\Expr\StaticCall;
711
use PHPStan\Analyser\MutatingScope;
812
use PHPStan\Analyser\Scope;
913
use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider;
@@ -318,17 +322,12 @@ public function check(
318322

319323
if ($this->checkArgumentTypes) {
320324
$parameterType = TypeUtils::resolveLateResolvableTypes($parameter->getType());
321-
322-
// TODO: handle other types of extensions
323-
if ($funcCall instanceof Expr\FuncCall && $callReflection instanceof FunctionReflection) {
324-
foreach ($this->parameterClosureTypeExtensionProvider->getFunctionParameterClosureTypeExtensions() as $functionParameterClosureTypeExtension) {
325-
if (!$functionParameterClosureTypeExtension->isFunctionSupported($callReflection, $parameter)) {
326-
continue;
327-
}
328-
$parameterType = $functionParameterClosureTypeExtension->getTypeFromFunctionCall($callReflection, $funcCall, $parameter, $scope) ?? $parameterType;
329-
330-
}
331-
}
325+
$parameterType = $this->getParameterTypeFromParameterClosureTypeExtension(
326+
$funcCall,
327+
$callReflection,
328+
$parameter,
329+
$scope,
330+
) ?? $parameterType;
332331

333332
if (
334333
!$parameter->passedByReference()->createsNewVariable()
@@ -650,4 +649,34 @@ private function describeParameter(ParameterReflection $parameter, ?int $positio
650649
return implode(' ', $parts);
651650
}
652651

652+
/**
653+
* @param MethodReflection|FunctionReflection|null $calleeReflection
654+
*/
655+
private function getParameterTypeFromParameterClosureTypeExtension(CallLike $callLike, $calleeReflection, ParameterReflection $parameter, Scope $scope): ?Type
656+
{
657+
if ($callLike instanceof FuncCall && $calleeReflection instanceof FunctionReflection) {
658+
foreach ($this->parameterClosureTypeExtensionProvider->getFunctionParameterClosureTypeExtensions() as $functionParameterClosureTypeExtension) {
659+
if ($functionParameterClosureTypeExtension->isFunctionSupported($calleeReflection, $parameter)) {
660+
return $functionParameterClosureTypeExtension->getTypeFromFunctionCall($calleeReflection, $callLike, $parameter, $scope);
661+
}
662+
}
663+
} elseif ($calleeReflection instanceof MethodReflection) {
664+
if ($callLike instanceof StaticCall) {
665+
foreach ($this->parameterClosureTypeExtensionProvider->getStaticMethodParameterClosureTypeExtensions() as $staticMethodParameterClosureTypeExtension) {
666+
if ($staticMethodParameterClosureTypeExtension->isStaticMethodSupported($calleeReflection, $parameter)) {
667+
return $staticMethodParameterClosureTypeExtension->getTypeFromStaticMethodCall($calleeReflection, $callLike, $parameter, $scope);
668+
}
669+
}
670+
} elseif ($callLike instanceof MethodCall) {
671+
foreach ($this->parameterClosureTypeExtensionProvider->getMethodParameterClosureTypeExtensions() as $methodParameterClosureTypeExtension) {
672+
if ($methodParameterClosureTypeExtension->isMethodSupported($calleeReflection, $parameter)) {
673+
return $methodParameterClosureTypeExtension->getTypeFromMethodCall($calleeReflection, $callLike, $parameter, $scope);
674+
}
675+
}
676+
}
677+
}
678+
679+
return null;
680+
}
681+
653682
}

tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1754,8 +1754,12 @@ public function testBug11506(): void
17541754
$this->analyse([__DIR__ . '/data/bug-11506.php'], []);
17551755
}
17561756

1757-
public function testFunctionParameterClosureTypeExtension(): void
1757+
public function testParameterClosureTypeExtension(): void
17581758
{
1759+
if (PHP_VERSION_ID < 70400) {
1760+
$this->markTestSkipped('Test requires PHP 7.4');
1761+
}
1762+
17591763
$this->analyse([__DIR__ . '/data/function-parameter-closure-type-extension.php'], [
17601764
[
17611765
'Parameter #2 $callback of function preg_replace_callback expects Closure(array{0: array{string, int<-1, max>}, 1?: array{\'\'|\'foo\', int<-1, max>}, 2?: array{\'\'|\'bar\', int<-1, max>}, 3?: array{\'baz\', int<-1, max>}}): string, Closure(array<int>): string given.',

tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,26 @@
22

33
namespace PHPStan\Rules\Methods;
44

5+
use PhpParser\Node\Expr\MethodCall;
6+
use PHPStan\Analyser\Scope;
57
use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider;
68
use PHPStan\Php\PhpVersion;
9+
use PHPStan\Reflection\MethodReflection;
10+
use PHPStan\Reflection\Native\NativeParameterReflection;
11+
use PHPStan\Reflection\ParameterReflection;
712
use PHPStan\Rules\FunctionCallParametersCheck;
813
use PHPStan\Rules\NullsafeCheck;
914
use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper;
1015
use PHPStan\Rules\Properties\PropertyReflectionFinder;
1116
use PHPStan\Rules\Rule;
1217
use PHPStan\Rules\RuleLevelHelper;
1318
use PHPStan\Testing\RuleTestCase;
19+
use PHPStan\Type\ClosureType;
20+
use PHPStan\Type\Constant\ConstantIntegerType;
21+
use PHPStan\Type\IntegerType;
22+
use PHPStan\Type\MethodParameterClosureTypeExtension;
23+
use PHPStan\Type\Type;
24+
use PHPStan\Type\TypeCombinator;
1425
use const PHP_VERSION_ID;
1526

1627
/**
@@ -35,9 +46,67 @@ protected function getRule(): Rule
3546
{
3647
$reflectionProvider = $this->createReflectionProvider();
3748
$ruleLevelHelper = new RuleLevelHelper($reflectionProvider, $this->checkNullables, $this->checkThisOnly, $this->checkUnionTypes, $this->checkExplicitMixed, $this->checkImplicitMixed, true, false);
49+
3850
return new CallMethodsRule(
3951
new MethodCallCheck($reflectionProvider, $ruleLevelHelper, true, true),
40-
new FunctionCallParametersCheck($ruleLevelHelper, new NullsafeCheck(), new PhpVersion($this->phpVersion), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), self::getContainer()->getByType(ParameterClosureTypeExtensionProvider::class), true, true, true, true, true),
52+
new FunctionCallParametersCheck(
53+
$ruleLevelHelper,
54+
new NullsafeCheck(),
55+
new PhpVersion($this->phpVersion),
56+
new UnresolvableTypeHelper(),
57+
new PropertyReflectionFinder(),
58+
new class implements ParameterClosureTypeExtensionProvider
59+
{
60+
61+
public function getFunctionParameterClosureTypeExtensions(): array
62+
{
63+
return [];
64+
}
65+
66+
public function getMethodParameterClosureTypeExtensions(): array
67+
{
68+
return [
69+
new class implements MethodParameterClosureTypeExtension
70+
{
71+
72+
public function isMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool
73+
{
74+
return $methodReflection->getName() === 'foo' && $methodReflection->getDeclaringClass()->getName() === 'MethodParameterClosureTypeExtension\\Foo';
75+
}
76+
77+
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, ParameterReflection $parameter, Scope $scope): Type
78+
{
79+
return new ClosureType(
80+
[
81+
new NativeParameterReflection(
82+
$parameter->getName(),
83+
$parameter->isOptional(),
84+
TypeCombinator::union(new ConstantIntegerType(5), new ConstantIntegerType(7)),
85+
$parameter->passedByReference(),
86+
$parameter->isVariadic(),
87+
$parameter->getDefaultValue(),
88+
),
89+
],
90+
new IntegerType(),
91+
);
92+
}
93+
94+
},
95+
];
96+
}
97+
98+
public function getStaticMethodParameterClosureTypeExtensions(): array
99+
{
100+
return [];
101+
}
102+
103+
},
104+
true,
105+
true,
106+
true,
107+
true,
108+
true,
109+
),
41110
);
42111
}
43112

@@ -3346,6 +3415,26 @@ public function testNoNamedArguments(): void
33463415
]);
33473416
}
33483417

3418+
public function testParameterClosureTypeExtension(): void
3419+
{
3420+
if (PHP_VERSION_ID < 70400) {
3421+
$this->markTestSkipped('Test requires PHP 7.4');
3422+
}
3423+
3424+
$this->checkThisOnly = false;
3425+
$this->checkNullables = true;
3426+
$this->checkUnionTypes = true;
3427+
$this->checkExplicitMixed = true;
3428+
3429+
$this->analyse([__DIR__ . '/data/method-parameter-closure-type-extension.php'], [
3430+
[
3431+
'Parameter #1 $fn of method MethodParameterClosureTypeExtension\Foo::foo() expects Closure(5|7): int, Closure(5): int given.',
3432+
16,
3433+
'Type 5 of parameter #1 $a of passed callable needs to be same or wider than parameter type 5|7 of accepting callable.',
3434+
],
3435+
]);
3436+
}
3437+
33493438
public function testTraitMixin(): void
33503439
{
33513440
$this->checkThisOnly = false;

tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,13 @@
22

33
namespace PHPStan\Rules\Methods;
44

5+
use PhpParser\Node\Expr\StaticCall;
6+
use PHPStan\Analyser\Scope;
57
use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider;
68
use PHPStan\Php\PhpVersion;
9+
use PHPStan\Reflection\MethodReflection;
10+
use PHPStan\Reflection\Native\NativeParameterReflection;
11+
use PHPStan\Reflection\ParameterReflection;
712
use PHPStan\Rules\ClassCaseSensitivityCheck;
813
use PHPStan\Rules\ClassForbiddenNameCheck;
914
use PHPStan\Rules\ClassNameCheck;
@@ -14,6 +19,12 @@
1419
use PHPStan\Rules\Rule;
1520
use PHPStan\Rules\RuleLevelHelper;
1621
use PHPStan\Testing\RuleTestCase;
22+
use PHPStan\Type\ClosureType;
23+
use PHPStan\Type\Constant\ConstantIntegerType;
24+
use PHPStan\Type\IntegerType;
25+
use PHPStan\Type\StaticMethodParameterClosureTypeExtension;
26+
use PHPStan\Type\Type;
27+
use PHPStan\Type\TypeCombinator;
1728
use function array_merge;
1829
use function usort;
1930
use const PHP_VERSION_ID;
@@ -51,7 +62,52 @@ protected function getRule(): Rule
5162
new PhpVersion(80000),
5263
new UnresolvableTypeHelper(),
5364
new PropertyReflectionFinder(),
54-
self::getContainer()->getByType(ParameterClosureTypeExtensionProvider::class),
65+
new class implements ParameterClosureTypeExtensionProvider
66+
{
67+
68+
public function getFunctionParameterClosureTypeExtensions(): array
69+
{
70+
return [];
71+
}
72+
73+
public function getMethodParameterClosureTypeExtensions(): array
74+
{
75+
return [];
76+
}
77+
78+
public function getStaticMethodParameterClosureTypeExtensions(): array
79+
{
80+
return [
81+
new class implements StaticMethodParameterClosureTypeExtension
82+
{
83+
84+
public function isStaticMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool
85+
{
86+
return $methodReflection->getName() === 'foo' && $methodReflection->getDeclaringClass()->getName() === 'StaticMethodParameterClosureTypeExtension\\Foo';
87+
}
88+
89+
public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, ParameterReflection $parameter, Scope $scope): Type
90+
{
91+
return new ClosureType(
92+
[
93+
new NativeParameterReflection(
94+
$parameter->getName(),
95+
$parameter->isOptional(),
96+
TypeCombinator::union(new ConstantIntegerType(5), new ConstantIntegerType(7)),
97+
$parameter->passedByReference(),
98+
$parameter->isVariadic(),
99+
$parameter->getDefaultValue(),
100+
),
101+
],
102+
new IntegerType(),
103+
);
104+
}
105+
106+
},
107+
];
108+
}
109+
110+
},
55111
true,
56112
true,
57113
true,
@@ -843,4 +899,22 @@ public function testClosureBind(): void
843899
]);
844900
}
845901

902+
public function testParameterClosureTypeExtension(): void
903+
{
904+
if (PHP_VERSION_ID < 70400) {
905+
$this->markTestSkipped('Test requires PHP 7.4');
906+
}
907+
908+
$this->checkThisOnly = false;
909+
$this->checkExplicitMixed = true;
910+
$this->checkImplicitMixed = true;
911+
$this->analyse([__DIR__ . '/data/static-method-parameter-closure-type-extension.php'], [
912+
[
913+
'Parameter #1 $fn of static method StaticMethodParameterClosureTypeExtension\Foo::foo() expects Closure(5|7): int, Closure(5): int given.',
914+
16,
915+
'Type 5 of parameter #1 $a of passed callable needs to be same or wider than parameter type 5|7 of accepting callable.',
916+
],
917+
]);
918+
}
919+
846920
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php declare(strict_types = 1); // lint >= 8.1
2+
3+
namespace MethodParameterClosureTypeExtension;
4+
5+
class Foo
6+
{
7+
/** @param callable(int): int $fn */
8+
public function foo(callable $fn): void
9+
{
10+
}
11+
12+
public function bar(): void
13+
{
14+
$this->foo($this->callback1(...));
15+
$this->foo($this->callback2(...));
16+
$this->foo($this->callback3(...));
17+
}
18+
19+
private function callback1(int $a): int
20+
{
21+
return $a;
22+
}
23+
24+
/** @param 5|7 $a */
25+
private function callback2(int $a): int
26+
{
27+
return $a;
28+
}
29+
30+
/** @param 5 $a */
31+
private function callback3(int $a): int
32+
{
33+
return $a;
34+
}
35+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace StaticMethodParameterClosureTypeExtension;
4+
5+
class Foo
6+
{
7+
/** @param callable(int): int $fn */
8+
public static function foo(callable $fn): void
9+
{
10+
}
11+
12+
public function bar(): void
13+
{
14+
self::foo($this->callback1(...));
15+
self::foo($this->callback2(...));
16+
self::foo($this->callback3(...));
17+
}
18+
19+
private function callback1(int $a): int
20+
{
21+
return $a;
22+
}
23+
24+
/** @param 5|7 $a */
25+
private function callback2(int $a): int
26+
{
27+
return $a;
28+
}
29+
30+
/** @param 5 $a */
31+
private function callback3(int $a): int
32+
{
33+
return $a;
34+
}
35+
}

0 commit comments

Comments
 (0)