🌐 US-Proxy
class="logged-out env-production page-responsive" style="word-wrap: break-word;" >
Skip to content

False positive notIdentical.alwaysFalse: A !== null && in_array($A, $arr) wrongly narrows $A to null in the else branch (regression in 2.2.3, related to #14908) #14966

Description

Bug report

When a variable is checked with a non-strict in_array() as the second operand of an && ($x !== null && in_array($x, $arr)), PHPStan narrows $x down to null in the else/elseif branch of that if. It then reports a subsequent $x !== null / $x === null check as notIdentical.alwaysFalse / identical.alwaysTrue:

Strict comparison using !== between null and null will always evaluate to false.  (notIdentical.alwaysFalse)
💡 Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false ...

The narrowing is unsound: reaching the else branch does not imply $x is null, because the && can also be false when $x is a non-null string that simply isn't in the array. In the linked reproducer, calling resolve('foo', ['bar']) takes the elseif branch ('foo' is not in ['bar'], so the && is false; $haystack is non-empty) and enters the inner if ($needle !== null) at runtime with $needle === 'foo' — so the branch flagged "always false" is reachable and does run.

Only the non-strict in_array() triggers it. Switching the call to the strict form in_array($needle, $haystack, true) makes the false positive disappear, which points at the non-strict in_array type-specifying logic. It also reproduces with a plain array $haystack (no PHPDoc); the @param list<string> is only there to keep the reproducer to a single reported error. Fires at level 4 and up (silent at level 3 and below).

Regression range:

PHPStan version Result
2.1.56 ✅ no error
2.2.0 / 2.2.1 / 2.2.2 ✅ no error
2.2.3 / 2.2.4 / 2.2.5 ❌ false positive (introduced in 2.2.3)
2.2.x-dev (latest, 371b67d) ❌ still present

2.2.3 is the release that partially fixed #14807, so this looks like the same regression tracked in #14908 — just reached through a simpler code path: a single if/elseif on a plain local variable, with no enum, no object property, and no condition reused across two separate ifs.

Heads up for the in-flight #14908 fixes: I built PHPStan from both PRs currently linked to #14908 (phpstan/phpstan-src#5983 and phpstan/phpstan-src#5857) and checked this reproducer against each. Both PRs fully fix #14908's posted reproducer, but neither fixes this shapenotIdentical.alwaysFalse on the linked reproducer is still reported on both branches. So this simpler $x !== null && in_array($x, $arr) case would remain broken after #14908 is closed. Might be worth covering it in the same fix.

Code snippet that reproduces the problem

https://phpstan.org/r/042db032-3963-49a2-9c3f-7d923c3cda0a

Expected output

No errors. The inner if ($needle !== null) is reachable with $needle being a non-null string (e.g. resolve('foo', ['bar'])), so the comparison is not statically decidable and should not be reported as always false.

Did PHPStan help you today? Did it make you happy in any way?

Yes — tracking this down was genuinely pleasant. \PHPStan\dumpType() made it trivial to see exactly where the type collapsed to null, and bisecting across the 2.2.x releases was painless. Thanks for building such a debuggable tool. 🙂

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions