Skip to content

Treat first-class callable NullsafeMethodCall as a MethodCall instead of throwing#5888

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

Treat first-class callable NullsafeMethodCall as a MethodCall instead of throwing#5888
phpstan-bot wants to merge 2 commits into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-3pnp41f

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

Summary

Using the first-class callable syntax together with the nullsafe operator
($foo?->bar(...)) crashed PHPStan with Internal error.. While this is a
fatal error in PHP itself ("Cannot combine nullsafe operator with Closure
creation"), PHPStan must not crash on it. This change makes the analyser treat
such an expression as a regular method-call first-class callable.

Changes

  • src/Analyser/NodeScopeResolver.php: in the first-class callable branch of
    processExprNode(), handle Expr\NullsafeMethodCall by building a
    MethodCallableNode from an equivalent MethodCall, instead of falling
    through to the throw new ShouldNotHappenException().
  • tests/PHPStan/Analyser/nsrt/bug-9746.php: regression test asserting the
    inferred types and (implicitly) that no internal error is thrown.

Root cause

processExprNode() enumerated the Expr\CallLike subclasses that support the
first-class callable syntax — FuncCall, MethodCall, StaticCall, New_
and threw ShouldNotHappenException for anything else. NullsafeMethodCall is
also an Expr\CallLike whose isFirstClassCallable() can be true (the parser
accepts $foo?->bar(...) even though PHP rejects it at runtime), so it hit the
else branch and crashed. It was the only Expr\CallLike subclass not covered.

Test

  • tests/PHPStan/Analyser/nsrt/bug-9746.php reproduces the original crash via
    $foo?->doFoo(...) (nullable and non-nullable receivers, plus an assignment)
    and asserts (Closure(): int)|null / Closure(): int. It errors with an
    internal error before the fix and passes after.
  • Verified manually that the expression no longer crashes in any context
    (statement, assignment, call argument, array element, return).

Analogous cases probed

  • The other first-class callable Expr\CallLike subclasses (FuncCall,
    MethodCall, StaticCall, New_) were already handled; NullsafeMethodCall
    was the only missing one.
  • MutatingScope::getKeepVoidType() already lists NullsafeMethodCall in its
    first-class-callable guard, and type resolution flows through
    NullsafeMethodCallHandler, so no internal error occurs when the expression
    is typed.

Fixes phpstan/phpstan#9746

…tead of throwing

- `NodeScopeResolver::processExprNode()` handled the first-class callable
  syntax (`foo(...)`) for `FuncCall`, `MethodCall`, `StaticCall` and `New_`,
  but `NullsafeMethodCall` (`$foo?->bar(...)`) fell through to the `else`
  branch and threw `ShouldNotHappenException`, surfacing as an internal error.
- `NullsafeMethodCall` is the only `Expr\CallLike` subclass that was missing
  from this branch. It is added by wrapping the call in a `MethodCallableNode`
  built from an equivalent `MethodCall`, since `$foo?->bar(...)` is a fatal
  error in PHP ("Cannot combine nullsafe operator with Closure creation") and
  must not crash the analyser.
- Type resolution already routed correctly through `NullsafeMethodCallHandler`,
  so no internal error occurs anywhere the expression is typed (assignment,
  argument, array element, return).

@staabm staabm left a comment

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.

Crash bug fixes need a AnalyserIntegrationTest

…ll crash

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@phpstan-bot

Copy link
Copy Markdown
Collaborator Author

Done. I addressed staabm's review by adding the requested AnalyserIntegrationTest coverage for the crash fix.

What I did

  1. New data file tests/PHPStan/Analyser/data/bug-9746.php — reproduces the original crash path (the first-class callable NullsafeMethodCall used as a statement, which is exactly what hit CallToMethodStatementWithoutSideEffectsRuleMutatingScope::resolveType()ShouldNotHappenException in the issue's stack trace).

  2. New test testBug9746() in AnalyserIntegrationTest — runs the full analyser over the file and asserts the three resulting errors, confirming analysis completes without an internal error.

Verification

  • With the fix: test passes (10 assertions).
  • Without the fix (temporarily reverted the committed NullsafeMethodCall branch): the test errors with the ShouldNotHappenException internal error — confirming it genuinely catches the crash.
  • make phpstan: No errors.

Committed as a2149b421 and pushed to create-pull-request/patch-3pnp41f. The existing nsrt/bug-9746.php type-inference test remains as-is; this adds the integration-level crash regression the reviewer asked for.

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.

Internal error when using first class callable with NullsafeMethodCall

2 participants