Skip to content

Fix #13526: False positive with array_key_exists and union types#5103

Open
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-9jd3jcc
Open

Fix #13526: False positive with array_key_exists and union types#5103
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-9jd3jcc

Conversation

@phpstan-bot
Copy link
Collaborator

Summary

Fixes a false positive where array_key_exists($item, $map) with a union type array ($map = $a ? $map1 : $map2) still reported "Offset ... might not exist" when accessing $map[$item] inside the truthy branch.

Changes

  • Modified src/Analyser/MutatingScope.php line 3683: Added && $this->hasExpressionType($expr)->yes() to the performance optimization condition in addTypeToExpression()
  • Added regression test tests/PHPStan/Rules/Arrays/data/bug-13526.php and test method testBug13526 in NonexistentOffsetInArrayDimFetchRuleTest

Root cause

When array_key_exists($item, $map) is processed as a truthy condition, the ArrayKeyExistsFunctionTypeSpecifyingExtension creates a SpecifiedTypes that includes a sure type for $map[$item] with the correct value type (1|2).

During filterBySpecifiedTypes, this sure type is applied via addTypeToExpression(). However, addTypeToExpression() has a performance optimization that returns early (without calling specifyExpressionType()) when the intersected type equals the already-computed type and is a constant scalar value. The problem is that for ArrayDimFetch expressions, the type is computed on-the-fly via getTypeFromArrayDimFetch() without being stored in expressionTypes. So the optimization returned early, and the expression type was never stored.

Later, NonexistentOffsetInArrayDimFetchRule checks $scope->hasExpressionType($node)->yes() to determine if the dim fetch was explicitly narrowed (e.g., by array_key_exists). Since the expression type was never stored, this returned no, and the rule proceeded to report the false positive.

The fix adds $this->hasExpressionType($expr)->yes() to the optimization condition, ensuring the early return only happens when the expression type is already stored. This preserves the optimization for the common case (variables and already-stored expressions) while fixing the issue for computed-but-not-stored expressions like ArrayDimFetch.

Test

Added tests/PHPStan/Rules/Arrays/data/bug-13526.php with a reproduction case: two constant arrays selected via a ternary, followed by array_key_exists + access. The test expects no errors, confirming the false positive is fixed.

Fixes phpstan/phpstan#13526

- Fixed optimization in MutatingScope::addTypeToExpression that skipped
  storing expression types for ArrayDimFetch when the computed type was
  already correct, causing hasExpressionType() to return false
- Added regression test for the false positive
- Root cause: the performance optimization early-returned without checking
  if the expression type was already stored in expressionTypes

Closes phpstan/phpstan#13526
@VincentLanglet
Copy link
Contributor

Related to #3933 cc @staabm

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