Report ?? null / ??= null on an always-set left side as an unnecessary null coalesce#5865
Open
phpstan-bot wants to merge 3 commits into
Open
Report ?? null / ??= null on an always-set left side as an unnecessary null coalesce#5865phpstan-bot wants to merge 3 commits into
?? null / ??= null on an always-set left side as an unnecessary null coalesce#5865phpstan-bot wants to merge 3 commits into
Conversation
…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.
c6159f0 to
2636415
Compare
VincentLanglet
approved these changes
Jun 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
hello1($name) ?? nullis equivalent tohello1($name): a??(or??=) with anullfallback 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: newfeatureToggles.unnecessaryNullCoalesce(false by default, true under bleeding edge).src/Rules/Variables/NullCoalesceRule.php: after the existingIssetCheckpass, runcheckUnnecessaryNullCoalesce(). It reportsnullCoalesce.unnecessarywhen:null(isNull()->yes()), andMutatingScope::issetCheck($left, static fn (): bool => true) === true.?? nullinstances 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(severalarray_last(...) ?? null)src/Reflection/SignatureMap/Php8SignatureMapProvider.php($phpDocReturnType ?? null)tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php: pass the toggle, addtestBug4337, and account for the extra (correct) report intestBug14213.Root cause
This is not a regression but a missing check. The existing
NullCoalesceRule/IssetCheckonly reported the left side being "not nullable" or "always null". The third redundant shape — left always set but nullable, right null — produced no diagnostic becauseIssetCheck::check()returnsnullfor 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 alwaysnull.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 intests/.../data/bug-4337.php:hello1($name) ?? null(the reported case)$foo->method() ?? nullFoo::staticMethod() ?? null$x ?? null$foo->stringOrNull ?? nullFoo::$staticStringOrNull ?? null$arr['a'] ?? null$x ??= nullNegative 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 existingIssetChecknullsafe branch, so it short-circuits before the new check and does not double-report.Test
NullCoalesceRuleTest::testBug4337analysesdata/bug-4337.php(copied from the issue's example plus the analogous constructs) and asserts the eight expectednullCoalesce.unnecessaryreports plus the three negative cases. Verified failing before the fix (no errors) and passing after. The full suite,make phpstan,make cs-fixall pass.Fixes phpstan/phpstan#4337