Skip to content

Fix phpstan/phpstan#14227: "Variable might not be defined" false positive#5129

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

Fix phpstan/phpstan#14227: "Variable might not be defined" false positive#5129
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-ov4e6vs

Conversation

@phpstan-bot
Copy link
Collaborator

Summary

Fixes a false positive "Variable $value might not be defined" when a variable is assigned under one condition and unset() under a mutually exclusive condition. This was found while reviewing #5125.

$key = rand(0, 2);
if ($key === 1) { $value = 'test'; }
if ($key === 2) { unset($value); }
if ($key === 1) { echo $value; } // false positive: "Variable $value might not be defined"

Changes

  • Added preserveSafeConditionalExpressions() method to src/Analyser/MutatingScope.php that preserves conditional expressions through scope merges when one branch invalidates a variable but the guard condition is disjoint from the other branch's types
  • Called the new method symmetrically in mergeWith() for both scope sides, between intersectConditionalExpressions() and createConditionalExpressions()
  • Restricted preservation to simple Variable expressions to avoid stale method call narrowing issues
  • Updated tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php:
    • Added testBug14227() regression test
    • Updated testDynamicAccess() expectations (resolved false positives for variables in mutually exclusive branches)
    • Updated testBug4173() expectations (resolved a known false positive marked "could be fixed")
  • New test data file tests/PHPStan/Rules/Variables/data/bug-14227.php with both the fix case and a counterexample

Root cause

When unset($value) was called inside an if-block, invalidateExpression() removed both the variable and its conditional expressions from the truthy scope. When merging back with the falsey scope, intersectConditionalExpressions() dropped conditional expressions that existed only in the falsey scope (because the truthy scope no longer had them). This caused the information "when $key === 1, $value is defined" to be lost.

The fix preserves these conditional expressions when:

  1. The subject variable doesn't exist in the other scope's expression types (it was invalidated)
  2. The guard condition's type is disjoint from the other scope's type for the guard variable (meaning the conditional could never have been relevant in that branch)
  3. The subject is a simple variable (to avoid stale method call narrowing)

Test

Added tests/PHPStan/Rules/Variables/data/bug-14227.php with:

  • foo(): the reported case where unset() in a mutually exclusive branch should not cause a false positive (no errors expected)
  • bar(): a counterexample where unset() runs under a compatible condition, so the error should still be reported

Fixes phpstan/phpstan#14227

…ve with unset in mutually exclusive branch

- Added preserveSafeConditionalExpressions() to MutatingScope to preserve conditional
  expressions through merges when one branch invalidates a variable (e.g. via unset())
  but the guard condition is disjoint from the other branch's types
- This also fixes related false positives for variables defined in different branches of
  if/elseif/else (bug-4173, dynamic-access test improvements)
- Restricted preservation to simple Variable expressions to avoid stale method call narrowing
- New regression test in tests/PHPStan/Rules/Variables/data/bug-14227.php
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.

1 participant