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 shape — notIdentical.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. 🙂
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$xdown tonullin theelse/elseifbranch of thatif. It then reports a subsequent$x !== null/$x === nullcheck asnotIdentical.alwaysFalse/identical.alwaysTrue:The narrowing is unsound: reaching the
elsebranch does not imply$xisnull, because the&&can also be false when$xis a non-null string that simply isn't in the array. In the linked reproducer, callingresolve('foo', ['bar'])takes theelseifbranch ('foo'is not in['bar'], so the&&is false;$haystackis non-empty) and enters the innerif ($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 formin_array($needle, $haystack, true)makes the false positive disappear, which points at the non-strictin_arraytype-specifying logic. It also reproduces with a plainarray $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:
2.2.x-dev(latest,371b67d)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/elseifon a plain local variable, with no enum, no object property, and no condition reused across two separateifs.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 shape —
notIdentical.alwaysFalseon 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$needlebeing 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 tonull, and bisecting across the 2.2.x releases was painless. Thanks for building such a debuggable tool. 🙂