Skip to content

Preserve list type in ConstantArrayType::spliceArray when all keys are integers#5480

Merged
VincentLanglet merged 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-mj87hhk
Apr 15, 2026
Merged

Preserve list type in ConstantArrayType::spliceArray when all keys are integers#5480
VincentLanglet merged 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-mj87hhk

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When array_splice is called on a constant array with only integer keys and a non-constant replacement array, PHPStan incorrectly inferred the result as non-empty-array<int<0, max>, string> instead of non-empty-list<string>. Since array_splice always re-indexes integer keys, the result should be recognized as a list.

Changes

  • In src/Type/Constant/ConstantArrayType.php, spliceArray() method:
    • Added $allKeysInteger check before the main loop to determine if the original constant array has only integer keys
    • After building each result variant, if all keys were integers and the result is not already a list, intersect with AccessoryArrayListType to ensure list-ness is preserved
  • Added regression test tests/PHPStan/Analyser/nsrt/bug-14472.php with three cases:
    • Constant array with integer keys + non-constant replacement (the reported bug)
    • Constant array with integer keys + list replacement
    • Constant array with string keys (already correct, no change)

Root cause

In ConstantArrayType::spliceArray(), when the replacement array is not a ConstantArrayType, the ConstantArrayTypeBuilder is degraded via degradeToGeneralArray(), and then setOffsetValueType() is called with the replacement's key type (non-null). In the degraded builder path, any non-null key in setOffsetValueType sets $this->isList = TrinaryLogic::createNo(), which causes the list property to be lost.

The fix adds a post-processing step: after the builder produces the result, if the original constant array had only integer keys, AccessoryArrayListType is added to the result. This mirrors the same logic already present in ArrayType::spliceArray() (line 546-548).

Analogous cases probed

  • ConstantArrayType::sliceArray() — uses null for auto-increment keys in non-degraded builder, correctly preserves list-ness. No issue.
  • IntersectionType::spliceArray() — delegates to each member type including AccessoryArrayListType::spliceArray() which returns $this. No issue.
  • AccessoryArrayListType::spliceArray() — returns $this. No issue.
  • Sort functions (sort, rsort, usort) — handled in FuncCallHandler by directly constructing list types. No builder degradation involved. No issue.
  • ConstantTypeHelper::getTypeFromValue() — has a similar pattern with degradation for large arrays (>256 elements), but affects a much rarer edge case and uses a different code path. Not fixed in this PR.

Test

  • tests/PHPStan/Analyser/nsrt/bug-14472.php: NSRT test with assertType calls verifying:
    • array_splice on ['last_name', 'first_name', ...] with non-constant replacement produces non-empty-list<string>
    • array_splice on ['a', 'b', 'c'] with list replacement produces non-empty-list<string>
    • array_splice on string-keyed array preserves exact constant array type (no regression)

Fixes phpstan/phpstan#14472

…are integers

- When `array_splice` is called on a constant array with only integer keys and
  a non-constant replacement, the builder degradation caused `isList` to be
  incorrectly set to `No` (via non-null key in `setOffsetValueType`).
- After building the result, check whether all keys in the original constant
  array were integers. If so, intersect the result with `AccessoryArrayListType`
  since `array_splice` always re-indexes integer keys.
- The fix is applied per-replacement-variant before the final union, so it
  correctly handles cases where the replacement is a union of arrays.
- Added regression test covering constant arrays with integer keys (the reported
  bug), list replacements, and string-key arrays (already correct).
- Probed `sliceArray`, `IntersectionType::spliceArray`,
  `AccessoryArrayListType::spliceArray`, sort functions, and
  `ConstantTypeHelper` for analogous issues — none found in the same pattern.
@VincentLanglet VincentLanglet requested a review from staabm April 15, 2026 15:05
Copy link
Copy Markdown
Contributor

@staabm staabm left a comment

Choose a reason for hiding this comment

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

Lgtm

@VincentLanglet VincentLanglet merged commit 58b873f into phpstan:2.1.x Apr 15, 2026
654 of 657 checks passed
@VincentLanglet VincentLanglet deleted the create-pull-request/patch-mj87hhk branch April 15, 2026 17:42
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