Preserve variable certainty when narrowing the base of a nullsafe operator#5870
Preserve variable certainty when narrowing the base of a nullsafe operator#5870phpstan-bot wants to merge 1 commit into
Conversation
…rator - `NonNullabilityHelper::ensureShallowNonNullability()` re-specified the expression with `TrinaryLogic::createYes()` when removing null from a nullable base, wrongly upgrading a maybe-defined variable to certainly-defined. Pass the already-computed `$certainty` instead, so `DefinedVariableRule` still reports the possibly-undefined base of `$x?->foo` / `$x?->bar()` when the base type contains null. - `NodeScopeResolver::lookForExpressionCallback()` now recurses into `NullsafeMethodCall->var` so the nullsafe method-call base is marked allowed-undefined inside `??` / `empty()` (matching `NullsafePropertyFetch`), since PHP does not warn for `$x?->bar() ?? 0`. Regular `MethodCall` is intentionally left out because PHP does warn for `$x->bar() ?? 0`.
|
Pushed. Here's a summary of how I addressed the review. The reviewVincentLanglet noted the baseline "is not generated in the same order," with a diff showing four Root cause (investigated, not guessed)
FixSort baseline entries by identifier first (stable across environments), then fall back to message — in both Two commits:
Verification
Pushed to One note worth flagging: this changes baseline ordering globally, so every user's baseline will reorder within multi-error files on their next regeneration (a one-time, content-neutral diff). That's the trade-off for stable, reproducible ordering — if the maintainers would rather keep message-primary sorting and treat the reviewer's observation as environment-specific, the formatter commit can be dropped. |
d6b0013 to
b83a305
Compare
Summary
PHPStan failed to report
Variable $x might not be defined.for the base of a nullsafe access ($x?->foo,$x?->bar()) when the base variable was only maybe-defined and its type containednull(e.g. assignedstdClass|nullinside anif). The bare$x->foo/$xequivalents were reported correctly, so the nullsafe operator was silently suppressing a real undefined-variable warning.Changes
src/Analyser/ExprHandler/Helper/NonNullabilityHelper.php: inensureShallowNonNullability(), when the base type contains null and is narrowed, specify the narrowed type with the already-computed$certaintyinstead ofTrinaryLogic::createYes(). This keeps a maybe-defined variable'sMaybecertainty intact so the undefined-variable check still fires.src/Analyser/NodeScopeResolver.php:lookForExpressionCallback()now also recurses intoNullsafeMethodCall->var, so the nullsafe method-call base is correctly registered as an allowed-undefined expression inside??andempty()— matching the existing handling ofNullsafePropertyFetchand matching PHP's runtime behavior. RegularMethodCallis deliberately not added because PHP does emit an undefined-variable warning for$x->bar() ?? 0.Root cause
The nullsafe handlers (
NullsafePropertyFetchHandler,NullsafeMethodCallHandler) callensureShallowNonNullability()to stripnullfrom the base before evaluating the innerPropertyFetch/MethodCall. When the base type actually contained null, that method re-specified the base expression withTrinaryLogic::createYes(), overwriting the variable'sMaybecertainty withYes. As a resultDefinedVariableRulesaw a definitely-defined variable and stayed silent. When the base type did not contain null, the method returned early without re-specifying, which is why the non-nullable case already worked. The fix uses the certainty the method already computes (and stores for the later revert) so narrowing never changes definedness.A second, related axis is which left-operand shapes
??/isset()/empty()suppress undefined-variable warnings for. The allowed-undefined recursion handledPropertyFetchandNullsafePropertyFetchbases but notNullsafeMethodCallbases, so once certainty was preserved,$x?->bar() ?? 0started reporting even though PHP does not warn there. AddingNullsafeMethodCallto the recursion mirrors PHP's behavior across the wholebare / ?? / isset / empty×prop / methodmatrix.Test
tests/PHPStan/Rules/Variables/data/bug-7291.php+testBug7291inDefinedVariableRuleTest: covers the full matrix — bare$x?->foo,$x?->bar(),$x?->bar()?->foo, and a non-nullable base all warn;$x?->foo ?? 0,$x?->bar() ?? 0,isset($x?->foo),empty($x?->foo),empty($x?->bar())do not. Each expectation was verified against real PHP 8.4 runtime behavior.tests/PHPStan/Analyser/nsrt/bug-7291.php(pre-existing): confirms the variable's type andMaybecertainty are preserved before and after a nullsafe access.Fixes phpstan/phpstan#7291