Skip to content

Fix phpstan/phpstan#12597: 'Variable might not be undefined' in if-statement after in_array check.#5379

Open
phpstan-bot wants to merge 6 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-f4qw099
Open

Fix phpstan/phpstan#12597: 'Variable might not be undefined' in if-statement after in_array check.#5379
phpstan-bot wants to merge 6 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-f4qw099

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

Fixes phpstan/phpstan#12597

When in_array($type, [self::TYPE_1, self::TYPE_2], true) narrows $type to a union type like 1|2, and a variable $message is assigned inside that block, PHPStan creates a conditional expression: "when $type is 1|2, $message is defined." However, a subsequent if ($type === self::TYPE_1) narrows $type to just 1, and the conditional expression didn't fire because filterBySpecifiedTypes used exact type matching via equals()1 does not equal 1|2.

Changes

  • src/Analyser/MutatingScope.php: Modified filterBySpecifiedTypes to allow conditional expression guards to match when the specified type is a strict subtype of a finite union guard type. The subtype matching is carefully restricted to prevent regressions:

    • Only activates for guard types with >1 finite types (unions of constants/literals)
    • Requires YES certainty on both the guard, specified expression, and the conditional's type holder
    • The YES certainty requirement on the type holder prevents matching stale "undefined" conditionals from earlier scope merges (prevents regression in bug-14117)
    • Subtype matches don't cascade into specifiedExpressions to prevent mutual conditional loops from corrupting type information (prevents regression in dynamic-access test)
  • tests/PHPStan/Rules/Variables/data/bug-12597.php: New regression test data file

  • tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php: Added testBug12597()

  • tests/PHPStan/Analyser/nsrt/bug-5051.php: Updated test expectations for improved type inference — several bool assertions are now correctly narrowed to false when the guard variable is narrowed to a specific constant value

Test plan

  • New regression test testBug12597 passes (no false positive about undefined variable)
  • testDynamicAccess passes (no regression in conditional variable tracking)
  • testBug14117 passes (no regression with stale conditionals)
  • bug-5051 type inference test updated and passes
  • Full test suite passes (11722 tests, 0 failures)
  • PHPStan self-analysis passes (make phpstan)
  • Coding standards pass (make cs-fix)

…rray check

When `in_array($type, [TYPE_1, TYPE_2], true)` narrows `$type` to a union
like `1|2` and a variable is assigned inside that block, a conditional
expression is created with guard type `1|2`. Later, when `$type === TYPE_1`
narrows to just `1`, the conditional didn't match because `equals()` requires
exact type equality.

Fix: In `filterBySpecifiedTypes`, allow conditional expression guards to match
when the specified type is a strict subtype of a finite union guard type. This
enables `$type = 1` to match the guard `$type = 1|2`, correctly resolving the
variable as defined.

The subtype matching is restricted to:
- Guards with >1 finite types (union of constants/literals)
- Both guard and specified having YES certainty
- The conditional's type holder having YES certainty (prevents matching stale
  "undefined" conditionals from earlier scope merges)
- Matches don't cascade (not added to specifiedExpressions) to prevent mutual
  conditional loops from corrupting type information
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.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Tests also confirmed passing from the background run. Everything is green and already pushed.

@staabm staabm requested a review from VincentLanglet April 2, 2026 06:15
Copy link
Copy Markdown
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.

Why is it limited to finites types ? Cant we have a conditional expression "if $x is int|string then $message is defined" and later the need to know that $messgae is defined when $x is int ?

Replace the `count(getFiniteTypes()) > 1` restriction with a
`!$guardType instanceof MixedType` check. This allows conditional
expressions with non-finite union guards (e.g. int|string) to match
when the specified type is a strict subtype, while still preventing
incorrect matches against mixed-based types that would cause *NEVER*
type collapses.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Done. Here's a summary of the changes made to address VincentLanglet's review:

Problem: The subtype matching in filterBySpecifiedTypes was restricted to guard types with count(getFiniteTypes()) > 1, which excluded non-finite union types like int|string.

Fix: Replaced the finite types restriction with !$guardType instanceof MixedType. This allows:

  • Finite unions like 1|2 (already worked)
  • Non-finite unions like int|string (now works too)
  • But excludes mixed-based types like mixed~false, mixed~null etc. which would cause incorrect *NEVER* type collapses when matched against concrete types

Test: Added testBug12597NonFinite with a test case using is_int($type) || is_string($type) to verify the non-finite union guard type scenario works correctly.

Verification: All 11726 tests pass, make phpstan reports no errors.

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.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Done. Added and pushed two regression test files:

  • tests/PHPStan/Analyser/nsrt/bug-4090.php — Tests that after in_array($value, $items, true), the array is correctly narrowed to non-empty-array<string> and current() returns string (not string|false).

  • tests/PHPStan/Analyser/nsrt/bug-11218.php — Tests that a variable assigned inside a for loop is correctly typed after the loop.

All 11728 tests pass and make phpstan reports no errors.

@VincentLanglet
Copy link
Copy Markdown
Contributor

Add regression test for

I think the regression test added for 4090 is not correct ; might need to be updated manually @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.

3 participants