Skip to content

Fix #14177: array_key_exists false positive#5024

Closed
phpstan-bot wants to merge 1 commit into2.1.xfrom
create-pull-request/patch-voz76k1
Closed

Fix #14177: array_key_exists false positive#5024
phpstan-bot wants to merge 1 commit into2.1.xfrom
create-pull-request/patch-voz76k1

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

This PR fixes phpstan/phpstan#14177.

Summary

When using array_key_exists() on a list type with optional trailing keys (e.g. from preg_replace_callback with optional capture groups), PHPStan incorrectly reported subsequent array_key_exists() calls as "always true".

Root cause

ConstantArrayType::unsetOffset() unconditionally set isList to TrinaryLogic::createNo() when removing a key. When this method was called during type narrowing (via tryRemove(HasOffsetType) in the falsy branch of array_key_exists), the resulting ConstantArrayType with isList=No was re-intersected with AccessoryArrayListType by IntersectionType::tryRemove(). Since AccessoryArrayListType::isSuperTypeOf() returns No for non-list types, TypeCombinator::intersect() collapsed the result to NeverType.

The NeverType falsy branch caused scope merging after conditionals (ternaries, if/else) to use only the truthy branch type, propagating incorrect type narrowing beyond the conditional.

Fix

In ConstantArrayType::unsetOffset(), when the key being removed is optional and the original type has isList as Yes or Maybe, check whether the remaining keys still form consecutive integers starting from 0. If so, preserve the original isList value instead of forcing it to No.

The check is limited to optional keys only, so actual unset() operations on required keys continue to set isList=No as before.

Test plan

  • Added regression test tests/PHPStan/Rules/Comparison/data/bug-14177.php reproducing the original issue with preg_replace_callback and optional capture groups
  • Added type inference test tests/PHPStan/Analyser/nsrt/bug-14177.php verifying correct type narrowing in both branches and after merge
  • All existing tests pass including array-is-list-unset.php
  • PHPStan self-analysis passes

…ing keys

When `ConstantArrayType::unsetOffset()` removed an optional trailing key
from a list type, it unconditionally set `isList` to `No`. This caused
`TypeCombinator::remove()` to produce `NeverType` when re-intersecting
with `AccessoryArrayListType`, since the list accessor rejects non-list
arrays. The `NeverType` falsy branch then collapsed scope merging after
conditionals like ternaries, propagating the narrowed (truthy) type
beyond the conditional and triggering false "always true" reports on
subsequent `array_key_exists()` calls.

The fix preserves the original `isList` value when unsetting an optional
key whose removal leaves the remaining keys as consecutive integers
starting from 0.

Fixes phpstan/phpstan#14177
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.

2 participants