Skip to content

Add null to array_map() callback parameters of shorter arrays - #6874

Open
SanderMuller wants to merge 3 commits into
phpstan:2.3.xfrom
SanderMuller:array-map-null-padding
Open

SanderMuller wants to merge 3 commits into
phpstan:2.3.xfrom
SanderMuller:array-map-null-padding

Conversation

@SanderMuller

Copy link
Copy Markdown
Contributor

With more than one array, array_map() calls the callback once per index of the longest array. Each shorter array gives null for the missing indexes:

php -r 'var_dump(array_map(fn ($a, $b) => [$a, $b], [1], [3, 4]));'

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 adds null to 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 or array{1, 2}|array{1}.

It compares the largest sizes and not the smallest, because array_map($cb, $a, $a) never pads, whatever sizes $a can 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, ArrayMapFunctionReturnTypeExtension and ArrayMapParameterClosureTypeExtension. 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) with array{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

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>
@SanderMuller
SanderMuller force-pushed the array-map-null-padding branch from 04e7c92 to d231b6d Compare October 29, 2026 12:43
// 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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think $b is an array? https://3v4l.org/QsHlT#v

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, please report a new phpstan issue with expectations

@SanderMuller SanderMuller Oct 31, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@staabm
staabm requested a review from VincentLanglet October 30, 2026 08:49
Comment on lines +13 to +14
assertType('1|2', $a);
assertType('3|null', $b);

@staabm staabm Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +245 to +252
$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);
}

@staabm staabm Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just double checked: if I comment out line 251, no tests fail

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here.. might need assertNativeType to cover this code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Covered now. The new assertNativeType lines fail when this line or the native padding above it is removed. They also pass with Turbo loaded.

@staabm

staabm commented Oct 31, 2026

Copy link
Copy Markdown
Contributor

ping in case you missed the latest feedback

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Invalid second array value type in array_map

3 participants