Repository navigation
Add null to array_map() callback parameters of shorter arrays - #6874
SanderMuller wants to merge 3 commits into
Conversation
With more than one array, array_map() passes null for the missing indexes of each shorter array. When the size of every array is known, the callback parameter of an array that can be shorter than another one now gets null. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
04e7c92 to
d231b6d
Compare
| // the closure gets one parameter per arg here, not one per unpacked array | ||
| array_map(function ($a, $b) { | ||
| assertType('1|2|3', $a); | ||
| assertType('array{4, 5, 6}', $b); |
There was a problem hiding this comment.
I don't think $b is an array? https://3v4l.org/QsHlT#v
There was a problem hiding this comment.
You are right, $b gets 4, 5 and 6. PHPStan 2.3.4 also infers array{4, 5, 6} there, because an unpacked arg stays one parameter and is not expanded into its arrays. This PR does not change that, but the test locked the wrong type in. I removed that case. I can fix the unpacked args in a separate PR.
There was a problem hiding this comment.
yes, please report a new phpstan issue with expectations
There was a problem hiding this comment.
Opened phpstan/phpstan#15620 with the expected types. I have a fix ready and will open the PR after this one is merged, because both change the same code.
The test asserted array{4, 5, 6} for the parameter of an unpacked arg.
At runtime the parameter gets 4, 5 and 6. PHPStan already inferred this
wrong type before this change, so the test must not lock it in.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
| assertType('1|2', $a); | ||
| assertType('3|null', $b); |
There was a problem hiding this comment.
unrelated side-question:
do we differentiate between types beeing infered from native vs. phpdoc?
so would assertType and assertNativeType lead different results when we feed a phpdoc array-shape or native values like [1, 2] into array_map?
There was a problem hiding this comment.
Yes, they can differ. With native arrays like [1, 2] and [3], assertType and assertNativeType both give 3|null for $b. With a PHPDoc shape, the native type of the array is array, so assertNativeType gives mixed for its value. I added both cases to the test.
| $callbackParameters = []; | ||
| foreach (ArrayMapCallbackParameterTypes::addPaddingNull($callbackValueTypes, $callbackArrayTypes) as $valueType) { | ||
| $callbackParameters[] = new DummyParameter('item', $valueType, optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); | ||
| } | ||
| $nativeCallbackParameters = []; | ||
| foreach (ArrayMapCallbackParameterTypes::addPaddingNull($nativeCallbackValueTypes, $nativeCallbackArrayTypes) as $valueType) { | ||
| $nativeCallbackParameters[] = new DummyParameter('item', $valueType, optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); | ||
| } |
There was a problem hiding this comment.
since we have 2 separate loops for native-types vs. non-native-types, I think we should also have assertType and assertNativeType assertions.
said differently: do we have test-coverage for both these loops here?
There was a problem hiding this comment.
just double checked: if I comment out line 251, no tests fail
There was a problem hiding this comment.
I added assertNativeType lines, but none of them fail when line 251 is commented out. An inline closure gets its native types from ContextualClosureParameterResolver, not from this callable type. I also tried a closure in a ternary, a closure in a variable and a typed arrow function, and the output did not change. I found no case where the native callable type here has an effect. It was built the same way before this PR, so I kept the two loops in line.
| $parameters[] = new DummyParameter('item', $type, optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); | ||
| } | ||
| $nativeParameters = []; | ||
| foreach ($nativeTypes as $nativeType) { |
There was a problem hiding this comment.
same here.. might need assertNativeType to cover this code
There was a problem hiding this comment.
Covered now. The new assertNativeType lines fail when this line or the native padding above it is removed. They also pass with Turbo loaded.
|
ping in case you missed the latest feedback |
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With more than one array,
array_map()calls the callback once per index of the longest array. Each shorter array givesnullfor the missing indexes:The second call gets
$a = null, so the first array is padded too, not only the 2nd+ arrays.PHPStan typed each callback parameter as the value type of its array, without
null. This PR addsnullto the parameter of an array whose largest size is smaller than the largest size of another array. It does this only when the size of every array is known, for example constant arrays orarray{1, 2}|array{1}.It compares the largest sizes and not the smallest, because
array_map($cb, $a, $a)never pads, whatever sizes$acan have. An arg with...is skipped where it is not expanded into its arrays.The logic is in
ArrayMapCallbackParameterTypes. Four places use it:ParametersAcceptorSelector,ContextualClosureParameterResolver,ArrayMapFunctionReturnTypeExtensionandArrayMapParameterClosureTypeExtension. The two shadowed classes call the PHP helper from their C++ twins.A typed callback such as
function (int $a, int $b)over[1, 2]and[3]now gets an argument error. PHP throws a TypeError for that call.General arrays stay as they are. I tried to make them nullable too, but three existing regression tests then report errors:
array_map($cb, $arr, $arr)(bug-12317)array_map($callback, $this->items, array_keys($this->items))(array_map_multiple.php)array_map(fn (string $a, string $b) => ..., ...$tuple)witharray{list<string>, list<string>}(bug-10527)The first is provably the same length, the second is the same length in practice, and the third can differ. Should general arrays become nullable, maybe in bleeding edge, with an exception for the same expression?
Closes phpstan/phpstan#13217
🤖 Generated with Claude Code