Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 1300ea4

Browse files
phpstan-botVincentLangletclaude
authored
Collect all remaining callable parameter types for variadic closure parameters instead of using only the matching index (#5634)
Co-authored-by: VincentLanglet <[email protected]> Co-authored-by: Vincent Langlet <[email protected]> Co-authored-by: Claude Opus 4.6 <[email protected]>
1 parent 745b400 commit 1300ea4

6 files changed

Lines changed: 307 additions & 4 deletions

File tree

src/Analyser/MutatingScope.php

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2021,7 +2021,7 @@ public function enterAnonymousFunctionWithoutReflection(
20212021
$isNullable = $this->isParameterValueNullable($parameter);
20222022
$parameterType = $this->getFunctionType($parameter->type, $isNullable, $parameter->variadic);
20232023
if ($callableParameters !== null) {
2024-
$parameterType = self::intersectButNotNever($parameterType, $this->getCallableParameterType($callableParameters, $i));
2024+
$parameterType = self::intersectButNotNever($parameterType, $this->getCallableParameterType($parameter, $callableParameters, $i));
20252025
}
20262026
$holder = ExpressionTypeHolder::createYes($parameter->var, $parameterType);
20272027
$expressionTypes[$paramExprString] = $holder;
@@ -2221,7 +2221,7 @@ public function enterArrowFunctionWithoutReflection(Expr\ArrowFunction $arrowFun
22212221
$isNullable = $this->isParameterValueNullable($parameter);
22222222
$parameterType = $this->getFunctionType($parameter->type, $isNullable, $parameter->variadic);
22232223
if ($callableParameters !== null) {
2224-
$parameterType = self::intersectButNotNever($parameterType, $this->getCallableParameterType($callableParameters, $i));
2224+
$parameterType = self::intersectButNotNever($parameterType, $this->getCallableParameterType($parameter, $callableParameters, $i));
22252225
}
22262226

22272227
if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) {
@@ -2290,8 +2290,12 @@ public function getFunctionType($type, bool $isNullable, bool $isVariadic): Type
22902290
/**
22912291
* @param ParameterReflection[] $callableParameters
22922292
*/
2293-
private function getCallableParameterType(array $callableParameters, int $index): Type
2293+
private function getCallableParameterType(Node\Param $parameter, array $callableParameters, int $index): Type
22942294
{
2295+
if ($parameter->variadic) {
2296+
return $this->buildVariadicArrayTypeFromCallableParameters($callableParameters, $index);
2297+
}
2298+
22952299
if (isset($callableParameters[$index])) {
22962300
return $callableParameters[$index]->getType();
22972301
}
@@ -2308,6 +2312,40 @@ private function getCallableParameterType(array $callableParameters, int $index)
23082312
return new MixedType();
23092313
}
23102314

2315+
/**
2316+
* @param array<ParameterReflection> $callableParameters
2317+
*/
2318+
private function buildVariadicArrayTypeFromCallableParameters(array $callableParameters, int $startIndex): Type
2319+
{
2320+
$elementTypes = [];
2321+
$callableParametersCount = count($callableParameters);
2322+
for ($j = $startIndex; $j < $callableParametersCount; $j++) {
2323+
$elementTypes[] = $callableParameters[$j]->getType();
2324+
if ($callableParameters[$j]->isVariadic()) {
2325+
break;
2326+
}
2327+
}
2328+
2329+
if ($elementTypes === [] && $callableParametersCount > 0) {
2330+
$lastParameter = array_last($callableParameters);
2331+
if ($lastParameter->isVariadic()) {
2332+
$elementTypes[] = $lastParameter->getType();
2333+
}
2334+
}
2335+
2336+
if ($elementTypes === []) {
2337+
return new MixedType();
2338+
}
2339+
2340+
$elementType = TypeCombinator::union(...$elementTypes);
2341+
2342+
if (!$this->getPhpVersion()->supportsNamedArguments()->no()) {
2343+
return new ArrayType(new UnionType([new IntegerType(), new StringType()]), $elementType);
2344+
}
2345+
2346+
return new IntersectionType([new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), $elementType), new AccessoryArrayListType()]);
2347+
}
2348+
23112349
public static function intersectButNotNever(Type $nativeType, Type $inferredType): Type
23122350
{
23132351
if ($nativeType->isSuperTypeOf($inferredType)->no()) {

src/Analyser/NodeScopeResolver.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2965,7 +2965,15 @@ public function createCallableParameters(Scope $scope, Expr $closureExpr, ?array
29652965
continue;
29662966
}
29672967

2968-
$type = $scope->getType($args[$index]->value);
2968+
if ($callableParameter->isVariadic()) {
2969+
$argTypes = [];
2970+
for ($j = $index; $j < count($args); $j++) {
2971+
$argTypes[] = $scope->getType($args[$j]->value);
2972+
}
2973+
$type = TypeCombinator::union(...$argTypes);
2974+
} else {
2975+
$type = $scope->getType($args[$index]->value);
2976+
}
29692977
$callableParameters[$index] = new NativeParameterReflection(
29702978
$callableParameter->getName(),
29712979
$callableParameter->isOptional(),
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?php // lint < 8.0
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug9240Php7;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
/**
10+
* @phpstan-type PhpFileArray array{error: int, name: string}
11+
*/
12+
class Upload
13+
{
14+
/**
15+
* @param \Closure(PhpFileArray, PhpFileArray, PhpFileArray): bool $fx
16+
*/
17+
public function onUpload(\Closure $fx): bool
18+
{
19+
$v = ['error' => 1, 'name' => 'x'];
20+
$postFiles = [$v, $v, $v];
21+
22+
return $fx(...$postFiles);
23+
}
24+
}
25+
26+
function test(): void
27+
{
28+
$u = new Upload();
29+
$u->onUpload(function (...$postFiles): bool {
30+
assertType('list<array{error: int, name: string}>', $postFiles);
31+
foreach ($postFiles as $postFile) {
32+
assertType('array{error: int, name: string}', $postFile);
33+
if ($postFile['error'] !== 0) {
34+
return false;
35+
}
36+
}
37+
38+
return true;
39+
});
40+
}
41+
42+
/**
43+
* @param \Closure(int, string, float): void $fx
44+
*/
45+
function mixedTypes(\Closure $fx): void
46+
{
47+
$fx(1, 'hello', 3.14);
48+
}
49+
50+
function testMixedTypes(): void
51+
{
52+
mixedTypes(function (...$args): void {
53+
assertType('list<float|int|string>', $args);
54+
});
55+
}
56+
57+
/**
58+
* @param \Closure(int, string): void $fx
59+
*/
60+
function twoParams(\Closure $fx): void
61+
{
62+
$fx(1, 'hello');
63+
}
64+
65+
function testVariadicNotFirst(): void
66+
{
67+
twoParams(function (int $first, string ...$rest): void {
68+
assertType('int', $first);
69+
assertType('list<string>', $rest);
70+
});
71+
}
72+
73+
// Arrow function version
74+
function testArrowFunction(): void
75+
{
76+
$u = new Upload();
77+
$u->onUpload(fn (...$postFiles) => assertType('list<array{error: int, name: string}>', $postFiles) || true);
78+
}
79+
80+
// Immediately-invoked closure with variadic
81+
function testImmediatelyInvoked(): void
82+
{
83+
$result = (function (...$args): string {
84+
assertType('list<1|3.14|\'hello\'>', $args);
85+
return implode(', ', $args);
86+
})(1, 'hello', 3.14);
87+
}
88+
89+
// Immediately-invoked arrow function with variadic
90+
function testImmediatelyInvokedArrow(): void
91+
{
92+
$result = (fn (...$args) => assertType('list<1|3.14|\'hello\'>', $args))(1, 'hello', 3.14);
93+
}
94+
95+
// Variadic param with last callable parameter also variadic
96+
/**
97+
* @param \Closure(string, int...): void $fx
98+
*/
99+
function variadicExpected(\Closure $fx): void
100+
{
101+
}
102+
103+
function testVariadicExpected(): void
104+
{
105+
variadicExpected(function (...$args): void {
106+
assertType('list<int|string>', $args);
107+
});
108+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?php // lint >= 8.0
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug9240;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
/**
10+
* @phpstan-type PhpFileArray array{error: int, name: string}
11+
*/
12+
class Upload
13+
{
14+
/**
15+
* @param \Closure(PhpFileArray, PhpFileArray, PhpFileArray): bool $fx
16+
*/
17+
public function onUpload(\Closure $fx): bool
18+
{
19+
$v = ['error' => 1, 'name' => 'x'];
20+
$postFiles = [$v, $v, $v];
21+
22+
return $fx(...$postFiles);
23+
}
24+
}
25+
26+
function test(): void
27+
{
28+
$u = new Upload();
29+
$u->onUpload(function (...$postFiles): bool {
30+
assertType('array<int|string, array{error: int, name: string}>', $postFiles);
31+
foreach ($postFiles as $postFile) {
32+
assertType('array{error: int, name: string}', $postFile);
33+
if ($postFile['error'] !== 0) {
34+
return false;
35+
}
36+
}
37+
38+
return true;
39+
});
40+
}
41+
42+
/**
43+
* @param \Closure(int, string, float): void $fx
44+
*/
45+
function mixedTypes(\Closure $fx): void
46+
{
47+
$fx(1, 'hello', 3.14);
48+
}
49+
50+
function testMixedTypes(): void
51+
{
52+
mixedTypes(function (...$args): void {
53+
assertType('array<int|string, float|int|string>', $args);
54+
});
55+
}
56+
57+
/**
58+
* @param \Closure(int, string): void $fx
59+
*/
60+
function twoParams(\Closure $fx): void
61+
{
62+
$fx(1, 'hello');
63+
}
64+
65+
function testVariadicNotFirst(): void
66+
{
67+
twoParams(function (int $first, string ...$rest): void {
68+
assertType('int', $first);
69+
assertType('array<int|string, string>', $rest);
70+
});
71+
}
72+
73+
// Arrow function version
74+
function testArrowFunction(): void
75+
{
76+
$u = new Upload();
77+
$u->onUpload(fn (...$postFiles) => assertType('array<int|string, array{error: int, name: string}>', $postFiles) || true);
78+
}
79+
80+
// Immediately-invoked closure with variadic
81+
function testImmediatelyInvoked(): void
82+
{
83+
$result = (function (...$args): string {
84+
assertType('array<int|string, 1|3.14|\'hello\'>', $args);
85+
return implode(', ', $args);
86+
})(1, 'hello', 3.14);
87+
}
88+
89+
// Immediately-invoked arrow function with variadic
90+
function testImmediatelyInvokedArrow(): void
91+
{
92+
$result = (fn (...$args) => assertType('array<int|string, 1|3.14|\'hello\'>', $args))(1, 'hello', 3.14);
93+
}
94+
95+
// Variadic param with last callable parameter also variadic
96+
/**
97+
* @param \Closure(string, int...): void $fx
98+
*/
99+
function variadicExpected(\Closure $fx): void
100+
{
101+
}
102+
103+
function testVariadicExpected(): void
104+
{
105+
variadicExpected(function (...$args): void {
106+
assertType('array<int|string, int|string>', $args);
107+
});
108+
}

tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1321,6 +1321,11 @@ public function testArraySearchExisting(): void
13211321
]);
13221322
}
13231323

1324+
public function testBug9240(): void
1325+
{
1326+
$this->analyse([__DIR__ . '/data/bug-9240.php'], []);
1327+
}
1328+
13241329
#[RequiresPhp('>= 8.4.0')]
13251330
public function testArrayFindKeyExisting(): void
13261331
{
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug9240Rule;
6+
7+
/**
8+
* @phpstan-type PhpFileArray array{error: int, name: string}
9+
*/
10+
class Upload
11+
{
12+
/**
13+
* @param \Closure(PhpFileArray, PhpFileArray, PhpFileArray): bool $fx
14+
*/
15+
public function onUpload(\Closure $fx): bool
16+
{
17+
$v = ['error' => 1, 'name' => 'x'];
18+
$postFiles = [$v, $v, $v];
19+
20+
return $fx(...$postFiles);
21+
}
22+
}
23+
24+
function test(): void
25+
{
26+
$u = new Upload();
27+
$u->onUpload(function (...$postFiles): bool {
28+
foreach ($postFiles as $postFile) {
29+
if ($postFile['error'] !== 0) {
30+
return false;
31+
}
32+
}
33+
34+
return true;
35+
});
36+
}

0 commit comments

Comments
 (0)