Fix #14097: Incorrect type with empty check #4908
Merged
+50
−1
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Summary
When spreading a constant array with optional named keys into a constructor/function call (e.g.,
new Foo(...$manipulations)where$manipulationsisnon-empty-array{width?: int, bgColor?: string}), PHPStan incorrectly reported a type error like "Parameter #1 $width expects int|null, int|string given." The fix ensures named optional keys are properly expanded as individual named arguments instead of falling through to a generic fallback that unions all value types.Changes
src/Rules/FunctionCallParametersCheck.php: Changed the optional key skip condition (line 182) to only skip optional keys that don't have a named key ($keyArgumentName === null). Named optional keys are now expanded as individual named arguments with their correct per-key types.tests/PHPStan/Rules/Classes/data/bug-14097.phptests/PHPStan/Rules/Classes/InstantiationRuleTest.phpCLAUDE.mdwith documentation about the spread argument expansion patternRoot cause
In
FunctionCallParametersCheck::check(), when expanding a spread constant array argument, optional keys were unconditionally skipped (line 182-184). When ALL keys were optional (as happens after!empty()narrowing of an array with all-optional keys), no arguments were expanded. The code then fell into a fallback path (lines 195-203) that usedgetIterableValueType()— the union of ALL value types — as a single generic unpacked argument. This lost the key-to-type correspondence:{width?: int, bgColor?: string}becameint|stringinstead of mappingwidth -> intandbgColor -> stringto their respective parameters.The fix preserves the skip for optional integer-keyed (positional) entries but expands optional string-keyed (named) entries as named arguments, maintaining the correct type mapping per parameter.
Test
Added
tests/PHPStan/Rules/Classes/data/bug-14097.phpreproducing the exact scenario from the issue: an array built up conditionally with$manipulations['width'] = (int)$matches['w']and$manipulations['bgColor'] = $matches['rgb'], then spread intonew Manipulations(...$manipulations)inside an!empty($manipulations)check. The test verifies no errors are reported.Fixes phpstan/phpstan#14097