Skip to content

Do not propagate MethodCall/StaticCall narrowing from nullsafe chain sub-expressions through conditional expressions#5495

Closed
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-cy60s6p
Closed

Do not propagate MethodCall/StaticCall narrowing from nullsafe chain sub-expressions through conditional expressions#5495
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-cy60s6p

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

After PR #5447 (fixing phpstan/phpstan#9455), assigning a nullsafe chain result to a variable ($x = $a?->b()?->c()) caused sub-expression method calls like $a->b() to be narrowed to non-null through conditional expressions. This caused false positives from NullsafeMethodCallRule ("Using nullsafe method call on non-nullable type") on subsequent uses of $a->b()?->....

Changes

  • src/Analyser/ExprHandler/AssignHandler.php:

    • Added ExprPrinter as a dependency
    • Added collectNullsafeProtectedExprStrings() helper that walks the assigned expression's nullsafe chain and collects expression strings of MethodCall/StaticCall var nodes
    • Both processSureTypesForConditionalExpressionsAfterAssign and processSureNotTypesForConditionalExpressionsAfterAssign now accept and check a $nullsafeProtectedExprStrings parameter, skipping entries that match
    • All 12 call sites updated to pass the protected set
  • Test files added:

    • tests/PHPStan/Rules/Methods/data/bug-14493.php — rule test for the reported false positive (both instance method and static method variants)
    • tests/PHPStan/Rules/Properties/data/bug-14493.php — analogous test for NullsafePropertyFetch chains
    • tests/PHPStan/Analyser/nsrt/bug-14493.php — NSRT test confirming $order->getOrderCustomer() retains OrderCustomerEntity|null type after nullsafe chain narrowing

Root cause

PR #5447 added MethodCall and StaticCall to the expression types allowed in conditional expression propagation during variable assignment. This was correct for the direct case ($hasA = $b->getA() !== null), but it also allowed sub-expression narrowings from nullsafe chain decomposition to leak into the scope. When the TypeSpecifier processes a nullsafe chain like $a?->b()?->c(), it internally decomposes it and creates "not-null" specified types for intermediate expressions like $a->b(). These should remain internal to the chain and not become conditional expressions on the assigned variable.

The fix identifies which expressions in the specified types are nullsafe-chain-internal by walking the assigned expression tree and collecting the var nodes of NullsafeMethodCall/NullsafePropertyFetch nodes. Only MethodCall and StaticCall vars are collected — Variable and PropertyFetch vars are left alone because their narrowing was already working correctly before PR #5447.

Analogous cases probed

  • NullsafePropertyFetch chain ($x = $a?->b()?->prop): tested, already fixed by the same change since the helper walks both NullsafeMethodCall and NullsafePropertyFetch nodes
  • StaticCall in chain ($x = Foo::get()?->method()?->c()): tested in the main regression test data
  • Variable narrowing from nullsafe chain ($x = $var?->prop): verified that existing tests (bug-10482, bug-6991) still pass — Variable narrowing is not affected by the fix
  • PropertyFetch narrowing from nullsafe chain ($x = $obj->prop?->key): verified via bug-6991 test — PropertyFetch narrowing is not affected
  • isset / ?? operators: these use different code paths (IssetCheck), not affected by this change

Test

  • testBug14493 in NullsafeMethodCallRuleTest — false positive for both $order->getOrderCustomer()?->getVatIds() and $order::getStaticOrderCustomer()?->getVatIds() after nullsafe chain narrowing
  • testBug14493 in NullsafePropertyFetchRuleTest — analogous case with property fetch chains
  • NSRT bug-14493.php — type assertions confirming the method call type remains nullable

Fixes phpstan/phpstan#14493

…ain sub-expressions through conditional expressions

- PR phpstan#5447 added MethodCall/StaticCall to the allowed expression types in
  processSureTypesForConditionalExpressionsAfterAssign and
  processSureNotTypesForConditionalExpressionsAfterAssign. This correctly
  fixed bug-9455 ($hasA = $b->getA() !== null; if ($hasA) { ... }).
- However, it also caused nullsafe chain sub-expressions to leak into the
  broader scope: $x = $a?->b()?->c() would narrow $a->b() to non-null via
  conditional expressions, causing "nullsafe on non-nullable type" false
  positives on subsequent $a->b()?->... calls.
- Fix: walk the assigned expression's nullsafe chain and collect exprStrings
  of MethodCall/StaticCall var nodes. Skip these in conditional expression
  propagation. Variable and PropertyFetch vars are not affected (they were
  already in the allowed list before PR phpstan#5447 and their narrowing is correct).
- Also tested the NullsafePropertyFetch analogous case (already fixed by the
  same change) and verified bug-9455/bug-5207 still pass.
@staabm staabm closed this Apr 19, 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.

2 participants