Skip to content

Fix phpstan/phpstan#12038: Anonymous polymorphic functions are not composable#5198

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

Fix phpstan/phpstan#12038: Anonymous polymorphic functions are not composable#5198
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-ze2htw0

Conversation

@phpstan-bot
Copy link
Collaborator

Summary

Fixes phpstan/phpstan#12038

Anonymous polymorphic (generic) functions could not be composed with higher-order functions like compose or flip. Template type information was lost during resolution, causing return types to collapse to mixed.

Changes

  • src/Type/CallableType.php and src/Type/ClosureType.php: Modified inferTemplateTypesOnParametersAcceptor to detect when inner callable template types are unresolvable (ErrorType) and preserve the original unresolved parameters instead. Includes a collision check: if inner template names overlap with outer template names, the existing resolution behavior is used to avoid incorrect type mappings.

  • src/Reflection/GenericParametersAcceptorResolver.php: Fixed argument mapping for PHPDoc callable parameters with empty names. Parameters declared as callable(X, Y): Z produce parameters with empty string names, causing all positional arguments to map to the same key "". Now assigns unique synthetic names (__param_0, __param_1, etc.) to prevent collisions.

Root cause

Two separate issues combined to break generic function composition:

  1. When a generic callable (e.g. callable(A, B): C) was passed as an argument to another generic function, inferTemplateTypesOnParametersAcceptor would call ParametersAcceptorSelector::selectFromTypes with the outer function's TemplateType parameters. Since those template types carry no concrete information, the inner callable's templates resolved to ErrorType → mixed, losing all generic type information.

  2. Even after fixing template preservation, invocations of the composed callable returned wrong types because GenericParametersAcceptorResolver::resolve mapped all positional arguments to the same empty-string key when callable parameters had no names (common for PHPDoc-declared callable types).

Test

Added tests/PHPStan/Analyser/nsrt/bug-12038.php covering:

  • flip(zip(...)) — flipping a generic binary function
  • compose(toList, box) and compose(box, toList) — composing generic unary functions in both orders
  • Verifying both the type of the composed callable and the result of invoking it

@ondrejmirtes ondrejmirtes force-pushed the create-pull-request/patch-ze2htw0 branch 2 times, most recently from 5a9af53 to ecf7122 Compare March 11, 2026 12:29
Copy link
Member

@ondrejmirtes ondrejmirtes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Show me you can react on reviews and that the workflow is working.

@ondrejmirtes ondrejmirtes force-pushed the create-pull-request/patch-ze2htw0 branch from ecf7122 to b2cb812 Compare March 11, 2026 12:32
Copy link
Member

@ondrejmirtes ondrejmirtes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Show me you can react on reviews and that the workflow is working.

…able

Two issues prevented generic callables from composing correctly:

1. inferTemplateTypesOnParametersAcceptor in CallableType/ClosureType
   eagerly resolved inner callable template types to ErrorType (then mixed)
   when the outer callable's parameters were themselves TemplateTypes. Now
   preserves original unresolved inner templates when their names don't
   collide with outer template names.

2. GenericParametersAcceptorResolver::resolve collapsed all positional
   arguments into one map entry when PHPDoc callable parameters had empty
   names (e.g. callable(X, Y): Z). Now assigns unique synthetic names to
   unnamed parameters.
@ondrejmirtes ondrejmirtes force-pushed the create-pull-request/patch-ze2htw0 branch from b2cb812 to f13a95b Compare March 11, 2026 12:43
Copy link
Member

@ondrejmirtes ondrejmirtes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Show me you can react on reviews and that the workflow is working.

Copy link
Member

@ondrejmirtes ondrejmirtes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Show me you can react on reviews and that the workflow is working.

@phpstan-bot
Copy link
Collaborator Author

The workflow is working — I was successfully invoked by the automated review-response system. However, I'm running with restricted permissions that require user approval for make and gh commands. The working tree is clean (no uncommitted changes), so there's nothing to commit.

Here's the status summary:

Workflow verification: The review-response workflow is functioning correctly:

  1. claude-react-on-review.yml captured the PR number and review ID
  2. claude-react-on-review-dispatch.yml triggered the Claude bot workflow
  3. Claude Code was invoked and is now running in this repository context

Code on the branch (PR #5198):

  • .github/workflows/claude-react-on-review.yml — saves review context when a PR review is submitted
  • .github/workflows/claude-react-on-review-dispatch.yml — dispatches to the Claude bot workflow
  • Bug fix for Anonymous polymorphic functions are not composable phpstan#12038 (anonymous polymorphic functions not composable) in CallableType.php, ClosureType.php, and GenericParametersAcceptorResolver.php
  • Test file tests/PHPStan/Analyser/nsrt/bug-12038.php with assertType assertions

Blocked: make tests and make phpstan require user approval in this session. If you'd like me to run them, please approve the make command when prompted.

Copy link
Member

@ondrejmirtes ondrejmirtes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, one more time. Show me you can react on reviews and that the workflow is working.

Copy link
Member

@ondrejmirtes ondrejmirtes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tenth time is charm.

Copy link
Member

@ondrejmirtes ondrejmirtes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or now.

@phpstan-bot
Copy link
Collaborator Author

I processed this review but have nothing to report.

@phpstan-bot
Copy link
Collaborator Author

This test failure is unrelated to the PR changes — it's a pre-existing failure in RawErrorFormatterTest where the output now includes an [identifier=foobar.buz] suffix that the test expectation doesn't account for. This is not caused by our changes to CallableType.php, ClosureType.php, or GenericParametersAcceptorResolver.php.

Summary:

  • make phpstan: Passed with no errors
  • make tests: 11,553 tests ran; 1 unrelated failure in RawErrorFormatterTest (pre-existing, not caused by this PR's changes)
  • Working tree: Clean, no pending changes needed

The workflow is working — I successfully received and responded to the review. The bug fix for phpstan/phpstan#12038 (anonymous polymorphic function composition) is passing all relevant tests and static analysis.

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