From fa2528b6a8d4f0c209cb618e7560bf53c2316a59 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 16:04:17 +0000 Subject: [PATCH] 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 https://github.com/phpstan/phpstan/issues/12363 --- src/Reflection/ParametersAcceptorSelector.php | 31 ++++++++++++++++++- .../Rules/Classes/InstantiationRuleTest.php | 5 +++ .../CallToFunctionParametersRuleTest.php | 5 +++ .../Rules/Functions/data/bug-12363.php | 26 ++++++++++++++++ .../PHPStan/Rules/Methods/data/bug-12363.php | 22 +++++++++++++ 5 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Rules/Functions/data/bug-12363.php create mode 100644 tests/PHPStan/Rules/Methods/data/bug-12363.php diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index 20317a1405..5793adbd14 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -516,7 +516,36 @@ public static function selectFromArgs( } if ($originalArg->unpack) { $unpack = true; - $types[$index] = $type->getIterableValueType(); + $constantArrays = $type->getConstantArrays(); + $expanded = false; + if (count($constantArrays) > 0) { + $allStringKeys = true; + foreach ($constantArrays as $constantArray) { + foreach ($constantArray->getKeyTypes() as $keyType) { + if (!$keyType->isString()->yes()) { + $allStringKeys = false; + break 2; + } + } + } + if ($allStringKeys) { + foreach ($constantArrays as $constantArray) { + foreach ($constantArray->getKeyTypes() as $j => $keyType) { + $keyName = $keyType->getValue(); + if (!isset($types[$keyName])) { + $types[$keyName] = $constantArray->getValueTypes()[$j]; + } else { + $types[$keyName] = TypeCombinator::union($types[$keyName], $constantArray->getValueTypes()[$j]); + } + } + } + $hasName = true; + $expanded = true; + } + } + if (!$expanded) { + $types[$index] = $type->getIterableValueType(); + } } else { $types[$index] = $type; } diff --git a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php index 511bf6136d..68aaf59d41 100644 --- a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php @@ -570,6 +570,11 @@ public function testBug14097(): void $this->analyse([__DIR__ . '/data/bug-14097.php'], []); } + public function testBug12363(): void + { + $this->analyse([__DIR__ . '/../Methods/data/bug-12363.php'], []); + } + public function testNewStaticWithConsistentConstructor(): void { $this->analyse([__DIR__ . '/data/instantiation-new-static-consistent-constructor.php'], [ diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 652e6e6778..6f684c56bf 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -2730,4 +2730,9 @@ public function testBug10559(): void $this->analyse([__DIR__ . '/data/bug-10559.php'], []); } + public function testBug12363(): void + { + $this->analyse([__DIR__ . '/data/bug-12363.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Functions/data/bug-12363.php b/tests/PHPStan/Rules/Functions/data/bug-12363.php new file mode 100644 index 0000000000..cf4c801e75 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-12363.php @@ -0,0 +1,26 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug12363; + +/** + * @template Y of 'a'|'b' + * @param Y $y + */ +function f(int $x, string $y = 'a'): void {} + +// Spreading associative array with required + optional template param +f(...['x' => 5, 'y' => 'b']); + +// Without spread - should also work +f(5, 'b'); + +/** + * @template Y of 'a'|'b' + * @param Y $y + */ +function g(string $y = 'a'): void {} + +// Without preceding required arg - already works +g(...['y' => 'b']); diff --git a/tests/PHPStan/Rules/Methods/data/bug-12363.php b/tests/PHPStan/Rules/Methods/data/bug-12363.php new file mode 100644 index 0000000000..6abbca2b8d --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-12363.php @@ -0,0 +1,22 @@ += 8.1 + +declare(strict_types = 1); + +namespace Bug12363Methods; + +/** + * @template Y of 'a'|'b' + */ +class A +{ + /** + * @param Y $y + */ + public function __construct( + public readonly int $x, + public readonly string $y = 'a', + ) { + } +} + +$a = new A(...['x' => 5, 'y' => 'b']);