Skip to content

Fix phpstan/phpstan#14117: Incorrect 'Variable $value might not be defined.'#5125

Merged
staabm merged 7 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-c1hi1rz
Mar 4, 2026
Merged

Fix phpstan/phpstan#14117: Incorrect 'Variable $value might not be defined.'#5125
staabm merged 7 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-c1hi1rz

Conversation

@phpstan-bot
Copy link
Collaborator

@phpstan-bot phpstan-bot commented Mar 3, 2026

Summary

Fixes a false positive "Variable $value might not be defined" when a variable is assigned inside an if-block and then used inside a subsequent if-block with the same condition, when there is a preceding if-block that also assigns the same variable under a different condition.

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

Changes

  • Modified createConditionalExpressions() in src/Analyser/MutatingScope.php (line ~4097): Added a check that prevents removing a variable from newVariableTypes when it has the same type in both branches but different certainty (e.g., Yes in the body branch vs Maybe in the falsey branch). This ensures the conditional expression when $key = 1 → $value is definitely defined is created during scope merging.
  • Removed two now-unnecessary @phpstan-ignore variable.undefined comments in src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php (lines 130 and 174) that were suppressing the same false positive pattern.

Root cause

When merging scopes after an if-without-else, MutatingScope::createConditionalExpressions() builds conditional expressions that track "when variable X has type T, variable Y has type U". It first constructs newVariableTypes by removing entries where the "their" (other branch) type matches the merged type, since those variables didn't meaningfully change.

The bug was that this removal only checked whether types matched (via equalTypes), ignoring certainty. When a preceding if-block assigned the same variable under a different condition, the falsey branch already had the variable with Maybe certainty (matching the merged result), so the variable was incorrectly removed from newVariableTypes. This prevented the creation of the conditional expression that would track "when $key equals 1, $value is definitely defined".

The fix adds a targeted check: when the types are the same but the certainties differ (our branch has Yes, their branch has Maybe), the variable is kept in newVariableTypes so the appropriate conditional expression is created.

Test

Added tests/PHPStan/Rules/Variables/data/bug-14117.php with three test cases:

  • foo(): The exact reproduction from the issue — variable assigned in two if-blocks with different conditions, used in a third if with the same condition as the second. No error expected (was false positive).
  • bar(): Similar but using the first if's condition — error still expected (separate, pre-existing issue with conditional expression preservation across intervening assignments).
  • baz(): Control case with a condition that doesn't match any assignment — error correctly expected.

Fixes phpstan/phpstan#14117
Closes phpstan/phpstan#8430
Closes phpstan/phpstan#10657

…ve with same condition

- Fixed createConditionalExpressions in MutatingScope to preserve variables in
  newVariableTypes when their type matches but certainty differs between branches
- This ensures conditional expressions are created to track that a variable is
  definitely defined when the same condition holds as when it was assigned
- Removed now-unnecessary @phpstan-ignore variable.undefined comments in
  OptimizedDirectorySourceLocator.php that were working around this bug
- New regression test in tests/PHPStan/Rules/Variables/data/bug-14117.php
}

if ($key === 3) {
echo $value; // this one SHOULD report "might not be defined" because $key === 3 doesn't guarantee either earlier block ran
Copy link
Contributor

Choose a reason for hiding this comment

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

Technically the variable should be reported as always undefined...

Copy link
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

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

@staabm I feel like it's an ok fix but I almost never worked with ExpressionTypeHolder...
Are you familiar enough with this topic or should we rely on ondrej review ?

@staabm
Copy link
Contributor

staabm commented Mar 3, 2026

I think its good. Will add more regression tests tomorrow

@staabm
Copy link
Contributor

staabm commented Mar 4, 2026

@VincentLanglet added more tests, and I think it looks good.

while this PR touches one of the snippets in bug 6830, it does not resolve the original problem, therefore I don't use a "closes 6830" in the PR description.

}
}

function baz(): void {
Copy link
Contributor

@staabm staabm Mar 4, 2026

Choose a reason for hiding this comment

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

in this file I added a few more tests which e.g. also check the interaction with unset().

while doing so I found a case which does not yet work: phpstan/phpstan#14227

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