Fix phpstan/phpstan#10862: Invalid array key for constant array append when negative keys are used since PHP 8.3#5412
Conversation
VincentLanglet
left a comment
There was a problem hiding this comment.
Instead of a single nsrt file use two
bug-10862-php82.php
bug-10862-php83.php
One for php <=8.2 behavior and one for php >= 8.3.
| if ($offsetValue >= $max) { | ||
| if ( | ||
| $offsetValue >= $max | ||
| || ($offsetValue < 0 && $max === 0 && PhpVersionStaticAccessor::getInstance()->getVersionId() >= 80300) |
There was a problem hiding this comment.
Instead of
PhpVersionStaticAccessor::getInstance()->getVersionId() >= 80300
check,
introduce a dedicated method inside PHPVersion
There was a problem hiding this comment.
Done. Here's what I changed to address the review:
-
Added
PhpVersion::updatesAutoIncrementKeyForNegativeValues()— a dedicated method insrc/Php/PhpVersion.phpthat returnstruefor PHP >= 8.3, replacing the inlinegetVersionId() >= 80300check inConstantArrayTypeBuilder. -
Split the test file — replaced
bug-10862.phpwith two version-specific files:bug-10862-php82.php(// lint <= 8.2) — asserts the old behavior where negative keys don't affect auto-index (next key is 0)bug-10862-php83.php(// lint >= 8.3) — asserts the new behavior where negative keys update auto-index
Both make phpstan and make tests pass cleanly.
|
Everything is already in good shape. The remote branch already has both review comments addressed:
Both static analysis and tests pass. The commits are already pushed to the remote. |
| $offsetValue >= $max | ||
| || ($offsetValue < 0 && $max === 0 && PhpVersionStaticAccessor::getInstance()->updatesAutoIncrementKeyForNegativeValues()) |
There was a problem hiding this comment.
extract this condition into a private method, to make the code more readable
There was a problem hiding this comment.
Already handled — tests passed as well. Everything is done: the review comment has been addressed, tests and static analysis pass, and the commit is pushed.
| @@ -0,0 +1,45 @@ | |||
| <?php // lint <= 8.2 | |||
There was a problem hiding this comment.
per comment in phpstan/phpstan#10862 (comment) we should have separate assertions for pre-PHP 8, PHP 8+, and PHP 8.3+ behaviour for this snippet https://3v4l.org/cSM3m
There was a problem hiding this comment.
Pushed successfully. Here's a summary of the changes:
Addressing staabm's review comments:
-
Extracted condition into private method (
shouldUpdateAutoIndex) — the upstream already had this partially done, I enhanced it to also handle the PHP 8.0+ literal array case. -
Three PHP version behaviors for the 3v4l.org/cSM3m snippet:
- Pre-PHP 8.0: Negative keys never affect auto-index (both imperative and literal → next key is 0)
- PHP 8.0-8.2: Array literals update auto-index for negative keys, but imperative assignment does not
- PHP 8.3+: Negative keys always update auto-index
Code changes:
PhpVersion::updatesAutoIncrementKeyForNegativeValuesInArrayLiteral()— new method for>= 8.0ConstantArrayTypeBuilder::setLiteralArray()— flag to distinguish array literal construction from imperative assignmentInitializerExprTypeResolver::getArrayType()— sets the literal flag when building array literalsshouldUpdateAutoIndex()— checks both PHP 8.3+ (always) and PHP 8.0+ (literal only)
Test files:
bug-10862-php74.php(lint < 8.0) — imperative + literal, all next=0bug-10862-php80.php(lint >= 8.0) — literal cases, next follows negative keybug-10862-php82.php(lint <= 8.2) — imperative cases, next=0bug-10862-php83.php(lint >= 8.3) — imperative + literal, all follow negative key
- PHP 8.3 fixed an incomplete RFC implementation where $a[] after negative keys should use max_key+1, not 0 - Updated ConstantArrayTypeBuilder to account for this when computing nextAutoIndexes - Added regression test in tests/PHPStan/Analyser/nsrt/bug-10862.php Closes phpstan/phpstan#10862
Replace inline version check with a dedicated PhpVersion method for the PHP 8.3+ negative array auto-index behavior. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Separate version-specific assertions into two test files with lint version constraints instead of a single file. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…array literal behavior PHP 8.0 changed array literals to update the auto-increment key for negative values, while imperative assignment only got this fix in PHP 8.3. Extract the condition into shouldUpdateAutoIndexForOffset() for readability and add isLiteralArray flag to ConstantArrayTypeBuilder to distinguish the two cases. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Split tests into four files covering all three PHP version behaviors: - php74: pre-PHP 8.0, negative keys never affect auto-index - php80: PHP 8.0+, array literals with negative keys update auto-index - php82: PHP <= 8.2, imperative assignment does not update for negative keys - php83: PHP 8.3+, negative keys always update auto-index Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ef47efc to
9ac8db3
Compare
|
|
||
| private bool $oversized = false; | ||
|
|
||
| private bool $isLiteralArray = false; |
There was a problem hiding this comment.
I don't think the "isLiteralArray" naming matches reality. we need to find a better name
There was a problem hiding this comment.
Pushed. Here's what I did to address staabm's review:
Renamed isLiteralArray → isArrayExpression across all three files:
ConstantArrayTypeBuilder::$isLiteralArray→$isArrayExpressionConstantArrayTypeBuilder::setLiteralArray()→setArrayExpression()PhpVersion::updatesAutoIncrementKeyForNegativeValuesInArrayLiteral()→updatesAutoIncrementKeyForNegativeValuesInArrayExpression()InitializerExprTypeResolvercall site updated accordingly
The term "array expression" better matches PHP parser terminology (Expr\Array_) and PHPStan's own naming conventions.
Regarding staabm's second concern (whether this is needed beyond InitializerExprTypeResolver::getArrayType()): The flag is correctly scoped. It only needs to be set when processing Expr\Array_ nodes because that's the only context where PHP 8.0-8.2 changed the negative auto-index behavior. When createFromConstantArray() is used (e.g., for $a[] = 2 after $a = [-4 => 1]), the nextAutoIndexes are already correctly inherited from the source ConstantArrayType — the flag was already applied when the array expression was first built. Other createEmpty() call sites build arrays from PHPDoc types, function return types, etc., where PHP runtime auto-index semantics don't apply.
| } | ||
|
|
||
| $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); | ||
| $arrayBuilder->setLiteralArray(); |
There was a problem hiding this comment.
does it really make sense only in this scenario? don't we need this behaviour every time a array-shape with known/constant keys is involved?
The flag marks builders processing Expr\Array_ nodes (PHP array expression syntax), which have different auto-index semantics for negative keys in PHP 8.0-8.2 vs imperative assignment. The term "array expression" better matches the PHP parser terminology. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
VincentLanglet
left a comment
There was a problem hiding this comment.
Given the comment php/php-src#11154
The bug fixed in PHP 8.3 was on empty array.
So, it seems like
- Prior to PHP8, it doesnt update auto increment
- From PHP8 to PHP 8.2 it does update auto imprecrement if the array was initialized as non empty.
- After PHP 8.3 it does update every time
So Rather than ArrayLiteral or ArrayExpression, is should express the fact the array is initialized with values [$foo => $bar] or not [].
…array initialization The distinction is whether the array was initialized with values (e.g. [-4 => 1]) vs initialized empty ([]). This matches the actual PHP behavior: - Pre PHP 8.0: negative keys never update auto-increment - PHP 8.0-8.2: updates only when array initialized non-empty - PHP 8.3+: always updates Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Done. Here's what changed:
The naming now correctly expresses the PHP behavior: the flag indicates the array was initialized with values ( |
Summary
PHP 8.3 completed the negative array index RFC (originally from PHP 8.0) by fixing
$a[] = valueto usemax_existing_key + 1even when all keys are negative. PHPStan always computed the next auto-index asmax(0, max_key + 1), producing incorrect types likearray{-4: 1, 0: 2}instead ofarray{-4: 1, -3: 2}on PHP 8.3+.Changes
src/Type/Constant/ConstantArrayTypeBuilder.php: Extended the auto-index update condition to also trigger for negative keys on PHP 8.3+ when no positive key has been set yet (i.e., when$max === 0, the initial default)PhpVersionStaticAccessorimport to access PHP version infoRoot cause
In
ConstantArrayTypeBuilder::setOffsetValueType(), the condition$offsetValue >= $maxcontrolled whether a key updates the next auto-index. SincenextAutoIndexesstarts at[0], negative keys (e.g.,-4) never satisfied$offsetValue >= 0, so the auto-index stayed at 0. On PHP 8.3+, negative keys should update the auto-index because PHP's internal behavior changed to track negative keys for auto-increment.Test
Added
tests/PHPStan/Analyser/nsrt/bug-10862.phpwith four scenarios:$a[-4] = 1; $a[] = 2→ keys -4, -3)$a[-1] = 'x'; $a[] = 'y'→ keys -1, 0)$a[-10] = 'a'; $a[-5] = 'b'; $a[] = 'c'→ keys -10, -5, -4)$a[-3] = 'a'; $a[5] = 'b'; $a[] = 'c'→ keys -3, 5, 6)Fixes phpstan/phpstan#10862