Skip to content

Fix false positive nullsafe.neverNull on left side of ??#5211

Closed
kemo wants to merge 1 commit intophpstan:2.1.xfrom
kemo:fix/nullsafe-coalesce-false-positive
Closed

Fix false positive nullsafe.neverNull on left side of ??#5211
kemo wants to merge 1 commit intophpstan:2.1.xfrom
kemo:fix/nullsafe-coalesce-false-positive

Conversation

@kemo
Copy link
Copy Markdown

@kemo kemo commented Mar 12, 2026

Problem

IssetCheck unconditionally reports ?-> property access as unnecessary (nullsafe.neverNull) when it appears on the left side of ??, isset(), or empty().

This is correct for isset()/empty() — PHP handles null objects natively in those constructs without throwing. But for ??, it is a false positive: the null coalescing operator does not catch TypeError from property access on a null object.

class Foo {
    public function getBar(): ?Bar { /* ... */ }
}

// PHPStan reports: Using nullsafe property access "?->baz" on left side of ?? is unnecessary.
// But removing ?-> would cause TypeError at runtime when getBar() returns null:
$foo->getBar()?->baz ?? 'default';
//              ^^^
//  Without ?->: null->baz throws TypeError (not caught by ??)
//  With ?->:    null?->baz evaluates to null, ?? returns 'default'

Root cause

In IssetCheck::check(), the NullsafePropertyFetch block (line 254) fires unconditionally regardless of context. It doesn't distinguish between ?? (where ?-> prevents TypeError) and isset()/empty() (where PHP already handles null objects).

Fix

When the identifier is nullCoalesce, check whether the object being accessed ($expr->var) has a nullable type. If it does, the nullsafe operator is genuinely needed and no error should be reported.

isset() and empty() behavior is unchanged — ?-> remains correctly flagged as unnecessary there since PHP handles null objects natively in those constructs.

Test changes

  • Removed 3 false positive expectations from NullCoalesceRuleTest::testBug7109() (lines 17, 28, 66 of the test data file)
  • Added new regression test testNullsafeCoalesceNullableObject() covering:
    • Chained nullable method calls with ?-> on left of ?? (no error expected)
    • Single nullable return with ?-> (no error expected)
    • Non-nullable object with ?-> (correctly reports "Expression not nullable")

@kemo kemo force-pushed the fix/nullsafe-coalesce-false-positive branch from 19dd4b0 to 9430320 Compare March 12, 2026 15:39
The nullsafe operator ?-> on the left side of ?? was unconditionally reported as unnecessary. This is correct for isset()/empty() which handle null objects natively, but wrong for ?? which does not catch TypeError from property access on null objects.
@kemo kemo force-pushed the fix/nullsafe-coalesce-false-positive branch from 9430320 to 2284215 Compare March 12, 2026 15:42
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.

Please provides a php snippet with leads to an error.

{
$this->analyse([__DIR__ . '/../Properties/data/bug-7109.php'], [
[
'Using nullsafe property access "?->aaa" on left side of ?? is unnecessary. Use -> instead.',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I have no error with
https://3v4l.org/5TbIf#v8.3.30

so the ?-> seems useless.

@kemo
Copy link
Copy Markdown
Author

kemo commented Mar 12, 2026

Closing this — after testing actual PHP behavior, I confirmed that ?? does handle null->property access via FETCH_OBJ_IS opcode, so the nullsafe on property access is genuinely unnecessary on the left side of ??. The distinction is that null->method() throws (method calls always execute), but null->prop is handled by ??. PHPStan's current behavior is correct. Apologies for the noise.

@kemo kemo closed this Mar 12, 2026
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