Skip to content

Report ?? null / ??= null on an always-set left side as an unnecessary null coalesce#5865

Open
phpstan-bot wants to merge 3 commits into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-fje3gw5
Open

Report ?? null / ??= null on an always-set left side as an unnecessary null coalesce#5865
phpstan-bot wants to merge 3 commits into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-fje3gw5

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

Summary

hello1($name) ?? null is equivalent to hello1($name): a ?? (or ??=) with a null fallback only does something when the left side can be undefined. When the left side is always set, the coalesce never changes the result and is redundant. PHPStan did not report this. This change adds detection for it.

Changes

  • conf/config.neon, conf/bleedingEdge.neon, conf/parametersSchema.neon: new featureToggles.unnecessaryNullCoalesce (false by default, true under bleeding edge).
  • src/Rules/Variables/NullCoalesceRule.php: after the existing IssetCheck pass, run checkUnnecessaryNullCoalesce(). It reports nullCoalesce.unnecessary when:
    1. the right-hand operand's type is always null (isNull()->yes()), and
    2. the left-hand operand is always set, determined by MutatingScope::issetCheck($left, static fn (): bool => true) === true.
  • Removed redundant ?? null instances surfaced by the new rule in PHPStan's own code:
    • src/Analyser/NodeScopeResolver.php (array_last($stmt->cond) ?? null, $parameterType ?? null ×2)
    • src/Type/FileTypeMapper.php (several array_last(...) ?? null)
    • src/Reflection/SignatureMap/Php8SignatureMapProvider.php ($phpDocReturnType ?? null)
  • tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php: pass the toggle, add testBug4337, and account for the extra (correct) report in testBug14213.

Root cause

This is not a regression but a missing check. The existing NullCoalesceRule/IssetCheck only reported the left side being "not nullable" or "always null". The third redundant shape — left always set but nullable, right null — produced no diagnostic because IssetCheck::check() returns null for that case. The fix reuses the already-existing "always set" traversal (MutatingScope::issetCheck) and only adds the new diagnostic when the right side is itself always null.

Analogous cases

All of the following go through the single issetCheck(..., fn () => true) "always set" probe and are covered by one code path; each is exercised in tests/.../data/bug-4337.php:

  • function call hello1($name) ?? null (the reported case)
  • instance method call $foo->method() ?? null
  • static method call Foo::staticMethod() ?? null
  • always-defined nullable variable $x ?? null
  • always-set nullable property $foo->stringOrNull ?? null
  • always-set nullable static property Foo::$staticStringOrNull ?? null
  • always-set nullable array offset $arr['a'] ?? null
  • assignment form $x ??= null

Negative cases verified to not report: maybe-undefined variable, maybe-undefined array offset, and any ??/??= whose right side is not null. Nullsafe access ($obj?->prop ?? null) is already handled by the existing IssetCheck nullsafe branch, so it short-circuits before the new check and does not double-report.

Test

NullCoalesceRuleTest::testBug4337 analyses data/bug-4337.php (copied from the issue's example plus the analogous constructs) and asserts the eight expected nullCoalesce.unnecessary reports plus the three negative cases. Verified failing before the fix (no errors) and passing after. The full suite, make phpstan, make cs-fix all pass.

Fixes phpstan/phpstan#4337

…ssary null coalesce

- Add a `unnecessaryNullCoalesce` feature toggle (off by default, enabled in bleedingEdge.neon).
- In `NullCoalesceRule`, after the existing isset-based check, report a `nullCoalesce.unnecessary` error when the right side of `??`/`??=` is always `null` and the left side is always set (so the coalesce can never change the result). "Always set" is determined via `MutatingScope::issetCheck()` with a trivial callback, which correctly handles function/method/static calls, defined nullable variables, always-set nullable properties and array offsets, etc.
- Clean up the redundant `?? null` instances this surfaced in PHPStan's own source (`NodeScopeResolver`, `FileTypeMapper`, `Php8SignatureMapProvider`), mostly `array_last(...) ?? null` and a few always-initialized locals.
Comment thread tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php
Comment thread src/Rules/Variables/NullCoalesceRule.php Outdated
@VincentLanglet VincentLanglet force-pushed the create-pull-request/patch-fje3gw5 branch from c6159f0 to 2636415 Compare June 14, 2026 08:55
@VincentLanglet VincentLanglet self-assigned this Jun 14, 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.

Report unnecessary null coalesce

2 participants