Skip to content

Analyse partial function applications as closures - #6932

Open
SanderMuller wants to merge 3 commits into
phpstan:2.3.xfrom
SanderMuller:partial-function-application
Open

SanderMuller wants to merge 3 commits into
phpstan:2.3.xfrom
SanderMuller:partial-function-application

Conversation

@SanderMuller

Copy link
Copy Markdown
Contributor

PHPStan crashes on a partial function application, for example str_replace(' ', '-', ?). The guards in front of getArgs() only skip first-class callables, so a call with a ? placeholder gets through. The crash starts in the parser visitors (assert(!$this->isPartialFunctionApplication()), as in the playground link in the issue) or later in ArgumentsHandler.

This PR does not add precise closure types yet. The result type is Closure, and a follow-up can build the signature from the RFC rules.

  • NodeScopeResolver replaces a partial function application with a new virtual node, PartialFunctionApplicationNode, like it does for first-class callables. Its handler processes the callee and the bound arguments, because PHP evaluates them when it creates the closure. Rules for the call itself do not see the original node.
  • The guards in front of getArgs() use isPartialFunctionApplication() now. Code that needs a first-class callable, like the pipe operator support, still checks for one.
  • A new level 0 rule reports partial function application below PHP 8.6. It also reports $a?->b(?) and new Foo(?), which PHP does not compile. $a?->b(...) crashed before this change too, and now gets the same error.
  • ClassStatementsGatherer counts $this->foo(?) as a call, so a private method that only a partial application uses is not reported as unused.
  • The Turbo extension has the same changes. The new handler has no C++ mirror, because it only runs for partial applications. I can port it if you want one.

Calls in constant expressions, for example a parameter default strlen(?), get the type Closure too.

Part of phpstan/phpstan#15364

🤖 Generated with Claude Code

SanderMuller and others added 3 commits November 3, 2026 23:20
A call with a ? placeholder (PHP 8.6) passed the guards that only
skipped first-class callables and crashed in getArgs(). NodeScopeResolver
now replaces it with PartialFunctionApplicationNode, which processes the
callee and the bound arguments and has the type Closure. The guards in
front of getArgs() check isPartialFunctionApplication().

PartialFunctionApplicationRule reports partial function application
below PHP 8.6, and the nullsafe and new forms that PHP does not compile.
ClassStatementsGatherer counts a partially applied method as a call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sion check

The detector parses with the tokenizer of PHP 8.5, which cannot read
the ? placeholder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant