Skip to content

Fix phpstan/phpstan#14362: Intersection loses __invoke() return type#5291

Merged
VincentLanglet merged 12 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-q79a61v
Mar 25, 2026
Merged

Fix phpstan/phpstan#14362: Intersection loses __invoke() return type#5291
VincentLanglet merged 12 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-q79a61v

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When creating a first-class callable from an intersection type like A&B where A has __invoke(B $b): int, PHPStan inferred Closure(): mixed instead of Closure(B): int. This fix makes IntersectionType::getCallableParametersAcceptors() properly delegate to its inner types.

Changes

  • Modified src/Type/IntersectionType.php: Replaced the getCallableParametersAcceptors() implementation that always returned TrivialParametersAcceptor with one that collects acceptors from inner types, prioritizing types that are definitely callable (yes) over maybe-callable types
  • Removed unused TrivialParametersAcceptor import from IntersectionType.php
  • Added regression test tests/PHPStan/Analyser/nsrt/bug-14362.php

Root cause

IntersectionType::getCallableParametersAcceptors() unconditionally returned [new TrivialParametersAcceptor()], which has a MixedType return type and no parameters. This discarded the actual __invoke() signature from the inner types. The fix collects callable parameter acceptors from each inner type, preferring types where isCallable() returns yes (which have actual __invoke methods) over types where it returns maybe (non-final classes/interfaces without __invoke).

Test

Added tests/PHPStan/Analyser/nsrt/bug-14362.php which verifies that (A&B)(...) where A has __invoke(B): int produces Closure(B): int, matching the behavior of plain A(...).

Fixes phpstan/phpstan#14362

- IntersectionType::getCallableParametersAcceptors() returned TrivialParametersAcceptor, losing all type info
- Now delegates to inner types, preferring types that are definitely callable over maybe-callable
- New regression test in tests/PHPStan/Analyser/nsrt/bug-14362.php
Comment thread src/Type/IntersectionType.php Outdated
…ersAcceptors()

Collect acceptor arrays and merge once outside the loop instead of
merging on each iteration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread tests/PHPStan/Analyser/nsrt/bug-14362.php
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread tests/PHPStan/Analyser/nsrt/bug-14362.php Outdated
…ine @var

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@staabm staabm requested a review from VincentLanglet March 24, 2026 15:59
Comment thread tests/PHPStan/Analyser/nsrt/bug-14362.php Outdated
A&E was impossible since both had __invoke with incompatible signatures.
Replace with realistic test cases: A&B&E (one callable + maybe-callable types)
and B&E (only maybe-callable types).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread src/Type/IntersectionType.php Outdated
@staabm staabm changed the title Fix phpstan/phpstan#14362: Union loses __invoke() return type Fix phpstan/phpstan#14362: Intersection loses __invoke() return type Mar 24, 2026
@VincentLanglet VincentLanglet force-pushed the create-pull-request/patch-q79a61v branch from d2f99e9 to c5049d1 Compare March 24, 2026 22:55
@VincentLanglet VincentLanglet force-pushed the create-pull-request/patch-q79a61v branch from c5049d1 to c104db1 Compare March 24, 2026 22:56
Comment thread src/Type/IntersectionType.php Outdated
if ($isCallable->yes()) {
$yesAcceptors[] = $type->getCallableParametersAcceptors($scope);
} else {
$maybeAcceptors[] = $type->getCallableParametersAcceptors($scope);
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.

would it make sense to return early with return [new TrivialParametersAcceptor()]; on the first maybe-acceptor we find?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

That was the earlier test run which confirmed all 11656 tests passed. Everything is already committed and pushed.

staabm and others added 3 commits March 25, 2026 08:18
…ypes

Instead of collecting maybe-callable acceptors and combining them,
return TrivialParametersAcceptor early when no yes-callable types exist.
Remove unverifiable conditional return type from combineAcceptors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@VincentLanglet VincentLanglet merged commit fb6208a into phpstan:2.1.x Mar 25, 2026
654 of 656 checks passed
@VincentLanglet VincentLanglet deleted the create-pull-request/patch-q79a61v branch March 25, 2026 08:42
phpstan-bot added a commit to phpstan-bot/phpstan-src that referenced this pull request Apr 7, 2026
phpstan#5291)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Markus Staab <markus.staab@redaxo.de>
Co-authored-by: Vincent Langlet <vincentlanglet@hotmail.fr>
Co-authored-by: Markus Staab <maggus.staab@googlemail.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.

3 participants