Skip to content

Fix phpstan/phpstan#14245: list lost after overwriting last element#5169

Merged
staabm merged 9 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-nva5u2r
Mar 10, 2026
Merged

Fix phpstan/phpstan#14245: list lost after overwriting last element#5169
staabm merged 9 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-nva5u2r

Conversation

@phpstan-bot
Copy link
Collaborator

Summary

When a list<int> had its last element overwritten via $list[count($list) - 1] = x, $list[array_key_last($list)] = x, or $list[array_key_first($list)] = x, PHPStan lost the list type and degraded to array<int<0, max>, int>. This fix preserves the list type for these common patterns.

Changes

  • Extended the list type preservation logic in src/Analyser/ExprHandler/AssignHandler.php to recognize three new syntactic patterns:
    • $list[count($list) - n] (BinaryOp\Minus with count() on the same variable)
    • $list[array_key_last($list)] (function call on the same variable)
    • $list[array_key_first($list)] (function call on the same variable)
  • Added a isSameVariable() helper to compare whether two expressions refer to the same variable
  • Added list preservation in src/Type/IntersectionType.php setExistingOffsetValueType() — when the original type is a list, the result preserves the list accessory type

Root cause

The list type preservation logic in AssignHandler::produceArrayDimFetchAssignValueToWrite() only handled BinaryOp\Plus patterns (for $list[$index + 1] style assignments). Assignments using count($list) - 1, array_key_last($list), or array_key_first($list) as the offset fell through without restoring the AccessoryArrayListType, causing the list type to be lost after setOffsetValueType() was called.

Test

Added tests/PHPStan/Analyser/nsrt/bug-14245.php with three test functions covering all three patterns (count() - 1, array_key_last(), array_key_first()), verifying both the non-empty-list<int> type inside the if block and the list<int> type after the block.

Fixes phpstan/phpstan#14245

@staabm staabm self-assigned this Mar 9, 2026
@staabm staabm requested a review from VincentLanglet March 9, 2026 17:15
return $this->intersectTypes(static fn (Type $type): Type => $type->setExistingOffsetValueType($offsetType, $valueType));
$result = $this->intersectTypes(static fn (Type $type): Type => $type->setExistingOffsetValueType($offsetType, $valueType));

if ($this->isList()->yes() && !$result->isList()->yes()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

It's unclear to me why we need this extra check.
I feel like the fix should be somewhere else (in another type ?).

Cause Array&List should already stay the same
Or ConstantArray&List should already stay the same

Copy link
Contributor

@staabm staabm Mar 10, 2026

Choose a reason for hiding this comment

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

the mutations tell the same story. thats a AI leftover. I will remove it.

good catch.

$valueToWrite = TypeCombinator::intersect($valueToWrite, new AccessoryArrayListType());
} elseif ( // keep list for $list[1 + $index] assignments
$arrayDimFetch->dim->left instanceof Variable
$keepList = false;
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it make sens for visibility to have a private shouldKeepList() method ?

@staabm staabm requested a review from VincentLanglet March 10, 2026 07:04
phpstan-bot and others added 6 commits March 10, 2026 10:01
…array_key_last, array_key_first

- Extended list type preservation in AssignHandler to recognize count($list) - n,
  array_key_last($list), and array_key_first($list) as existing-key offset patterns
- Added list preservation in IntersectionType::setExistingOffsetValueType
- New regression test in tests/PHPStan/Analyser/nsrt/bug-14245.php

Closes phpstan/phpstan#14245
@staabm staabm force-pushed the create-pull-request/patch-nva5u2r branch from 7427ed6 to 9a5f88d Compare March 10, 2026 09:01
) {
return true;
}
} elseif ( // keep list for $list[count($list) - n] assignments
Copy link
Contributor

Choose a reason for hiding this comment

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

Cant we risk having n too big and set the offset -1 ?

Copy link
Contributor

Choose a reason for hiding this comment

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

since the logic here will only add non-empty-list to a pre-existing maybe already degraded because too huge array, I don't see how a too big "n" value can be a problem here.

added a test with a huge array and a big offset

&& $offsetValueType->isIterableAtLeastOnce()->yes()
) {
return true;
} elseif ( // keep list for $list[array_key_last($list)] and $list[array_key_first($list)] assignments
Copy link
Contributor

Choose a reason for hiding this comment

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

Array_search works too

Copy link
Contributor

Choose a reason for hiding this comment

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

added 👍

@staabm staabm merged commit 050e6bf into phpstan:2.1.x Mar 10, 2026
629 of 648 checks passed
@staabm staabm deleted the create-pull-request/patch-nva5u2r branch March 10, 2026 14:25
@staabm
Copy link
Contributor

staabm commented Mar 10, 2026

thank you @VincentLanglet 👍

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.

3 participants