Skip to content

Commit 684e023

Browse files
github-actions[bot]VincentLanglet
authored andcommitted
Fix template type resolution for spread of associative arrays
- When spreading an associative constant array into a function/method call, ParametersAcceptorSelector::selectFromArgs() now expands string-keyed constant arrays into individual named type entries instead of collapsing them into a single getIterableValueType() call - This allows GenericParametersAcceptorResolver to correctly map argument types to parameters by name, preventing template types from being incorrectly inferred from default values - Added regression tests for both function calls and class constructors Closes phpstan/phpstan#12363
1 parent f08de42 commit 684e023

File tree

5 files changed

+88
-1
lines changed

5 files changed

+88
-1
lines changed

src/Reflection/ParametersAcceptorSelector.php

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -516,7 +516,36 @@ public static function selectFromArgs(
516516
}
517517
if ($originalArg->unpack) {
518518
$unpack = true;
519-
$types[$index] = $type->getIterableValueType();
519+
$constantArrays = $type->getConstantArrays();
520+
$expanded = false;
521+
if (count($constantArrays) > 0) {
522+
$allStringKeys = true;
523+
foreach ($constantArrays as $constantArray) {
524+
foreach ($constantArray->getKeyTypes() as $keyType) {
525+
if (!$keyType->isString()->yes()) {
526+
$allStringKeys = false;
527+
break 2;
528+
}
529+
}
530+
}
531+
if ($allStringKeys) {
532+
foreach ($constantArrays as $constantArray) {
533+
foreach ($constantArray->getKeyTypes() as $j => $keyType) {
534+
$keyName = $keyType->getValue();
535+
if (!isset($types[$keyName])) {
536+
$types[$keyName] = $constantArray->getValueTypes()[$j];
537+
} else {
538+
$types[$keyName] = TypeCombinator::union($types[$keyName], $constantArray->getValueTypes()[$j]);
539+
}
540+
}
541+
}
542+
$hasName = true;
543+
$expanded = true;
544+
}
545+
}
546+
if (!$expanded) {
547+
$types[$index] = $type->getIterableValueType();
548+
}
520549
} else {
521550
$types[$index] = $type;
522551
}

tests/PHPStan/Rules/Classes/InstantiationRuleTest.php

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

582+
public function testBug12363(): void
583+
{
584+
$this->analyse([__DIR__ . '/../Methods/data/bug-12363.php'], []);
585+
}
586+
582587
public function testNewStaticWithConsistentConstructor(): void
583588
{
584589
$this->analyse([__DIR__ . '/data/instantiation-new-static-consistent-constructor.php'], [

tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php

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

2738+
public function testBug12363(): void
2739+
{
2740+
$this->analyse([__DIR__ . '/data/bug-12363.php'], []);
2741+
}
2742+
27382743
}
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']);
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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']);

0 commit comments

Comments
 (0)