Skip to content

Commit 8a020e0

Browse files
phpstan-botgithub-actions[bot]VincentLanglet
committed
Fix #12363: Spreading an associative array into a generic function/method call may restrict optional arguments to their default values. (phpstan#5079)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Vincent Langlet <vincentlanglet@hotmail.fr>
1 parent d2a336e commit 8a020e0

File tree

8 files changed

+195
-1
lines changed

8 files changed

+195
-1
lines changed

src/Reflection/ParametersAcceptorSelector.php

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -516,7 +516,27 @@ public static function selectFromArgs(
516516
}
517517
if ($originalArg->unpack) {
518518
$unpack = true;
519-
$types[$index] = $type->getIterableValueType();
519+
$constantArrays = $type->getConstantArrays();
520+
if (count($constantArrays) > 0) {
521+
foreach ($constantArrays as $constantArray) {
522+
$values = $constantArray->getValueTypes();
523+
foreach ($constantArray->getKeyTypes() as $j => $keyType) {
524+
$valueType = $values[$j];
525+
$valueIndex = $keyType->getValue();
526+
if (is_string($valueIndex)) {
527+
$hasName = true;
528+
} else {
529+
$valueIndex = $i + $j;
530+
}
531+
532+
$types[$valueIndex] = isset($types[$valueIndex])
533+
? TypeCombinator::union($types[$valueIndex], $valueType)
534+
: $valueType;
535+
}
536+
}
537+
} else {
538+
$types[$index] = $type->getIterableValueType();
539+
}
520540
} else {
521541
$types[$index] = $type;
522542
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug8257;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
interface TreeMapper
8+
{
9+
/**
10+
* @template T of object
11+
*
12+
* @param string|class-string<T> $signature
13+
* @param mixed $source
14+
* @return (
15+
* $signature is class-string<T>
16+
* ? T
17+
* : mixed
18+
* )
19+
*/
20+
public function map(string $signature, $source);
21+
}
22+
23+
/** @var TreeMapper $tm */
24+
$tm;
25+
26+
class A {}
27+
28+
assertType('Bug8257\A', $tm->map(...[A::class, []]));

tests/PHPStan/Rules/Classes/InstantiationRuleTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,12 @@ public function testBug13440(): void
579579
$this->analyse([__DIR__ . '/data/bug-13440.php'], []);
580580
}
581581

582+
#[RequiresPhp('>= 8.0')]
583+
public function testBug12363(): void
584+
{
585+
$this->analyse([__DIR__ . '/../Methods/data/bug-12363.php'], []);
586+
}
587+
582588
public function testNewStaticWithConsistentConstructor(): void
583589
{
584590
$this->analyse([__DIR__ . '/data/instantiation-new-static-consistent-constructor.php'], [

tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2735,4 +2735,10 @@ public function testBug10559(): void
27352735
$this->analyse([__DIR__ . '/data/bug-10559.php'], []);
27362736
}
27372737

2738+
#[RequiresPhp('>= 8.0')]
2739+
public function testBug12363(): void
2740+
{
2741+
$this->analyse([__DIR__ . '/data/bug-12363.php'], []);
2742+
}
2743+
27382744
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php // lint >= 8.0
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug12363;
6+
7+
/**
8+
* @template Y of 'a'|'b'
9+
* @param Y $y
10+
*/
11+
function f(int $x, string $y = 'a'): void {}
12+
13+
// Spreading associative array with required + optional template param
14+
f(...['x' => 5, 'y' => 'b']);
15+
16+
// Without spread - should also work
17+
f(5, 'b');
18+
19+
/**
20+
* @template Y of 'a'|'b'
21+
* @param Y $y
22+
*/
23+
function g(string $y = 'a'): void {}
24+
25+
// Without preceding required arg - already works
26+
g(...['y' => 'b']);

tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3908,6 +3908,19 @@ public function testBug1501(): void
39083908
$this->analyse([__DIR__ . '/data/bug-1501.php'], []);
39093909
}
39103910

3911+
public function testBug7369(): void
3912+
{
3913+
$this->checkThisOnly = false;
3914+
$this->checkNullables = true;
3915+
$this->checkUnionTypes = true;
3916+
$this->analyse([__DIR__ . '/data/bug-7369.php'], [
3917+
[
3918+
'Cannot call method isAllowed() on bool.',
3919+
40,
3920+
],
3921+
]);
3922+
}
3923+
39113924
public function testBug11463(): void
39123925
{
39133926
$this->checkThisOnly = false;
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php // lint >= 8.1
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug12363Methods;
6+
7+
/**
8+
* @template Y of 'a'|'b'
9+
*/
10+
class A
11+
{
12+
/**
13+
* @param Y $y
14+
*/
15+
public function __construct(
16+
public readonly int $x,
17+
public readonly string $y = 'a',
18+
) {
19+
}
20+
}
21+
22+
$a = new A(...['x' => 5, 'y' => 'b']);
23+
24+
$aa = new A(...[5, 'y' => 'b']);
25+
26+
$aaa = new A(...[5, 'b']);
27+
28+
$aaaa = new A(...[1 => 5, 2 => 'b']);
29+
30+
/**
31+
* @template Y of 'a'|'b'
32+
*/
33+
class B
34+
{
35+
/**
36+
* @param Y $y
37+
*/
38+
public function __construct(
39+
public readonly int $init,
40+
public readonly int $x,
41+
public readonly string $y = 'a',
42+
) {
43+
}
44+
}
45+
46+
$a = new B(1, ...['x' => 5, 'y' => 'b']);
47+
48+
$aa = new B(1, ...[5, 'y' => 'b']);
49+
50+
$aaa = new B(1, ...[5, 'b']);
51+
52+
$aaaa = new B(1, ...[1 => 5, 2 => 'b']);
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Bug7369;
4+
5+
interface AccountInterface {}
6+
7+
interface AccessResultInterface {
8+
public function isAllowed(): bool;
9+
}
10+
11+
interface AccessibleInterface {
12+
13+
/**
14+
* @return ($return_as_object is true ? AccessResultInterface : bool)
15+
*/
16+
public function access(string $operation, AccountInterface $account = NULL, bool $return_as_object = FALSE);
17+
}
18+
19+
$class = new class() implements AccessibleInterface {
20+
/**
21+
* {@inheritDoc}
22+
*/
23+
public function access(
24+
string $operation,
25+
AccountInterface $account = null,
26+
bool $return_as_object = false
27+
) {
28+
if ($return_as_object) {
29+
return new class () implements AccessResultInterface {
30+
public function isAllowed(): bool {
31+
return true;
32+
}
33+
};
34+
}
35+
return false;
36+
}
37+
};
38+
39+
$class->access('view', null, true)->isAllowed();
40+
$class->access('view', null, false)->isAllowed();
41+
42+
$params = ['view', null, true];
43+
$class->access(...$params)->isAllowed();

0 commit comments

Comments
 (0)