Skip to content

Commit 5f4ef46

Browse files
phpstan-botclaudestaabmVincentLanglet
committed
Fix phpstan/phpstan#14362: Intersection loses __invoke() return type (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>
1 parent b44a8cd commit 5f4ef46

2 files changed

Lines changed: 113 additions & 3 deletions

File tree

src/Type/IntersectionType.php

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

33
namespace PHPStan\Type;
44

5+
use PHPStan\Internal\CombinationsHelper;
56
use PHPStan\Php\PhpVersion;
67
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode;
78
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
89
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
910
use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode;
1011
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
12+
use PHPStan\Reflection\Callables\CallableParametersAcceptor;
1113
use PHPStan\Reflection\ClassConstantReflection;
1214
use PHPStan\Reflection\ClassMemberAccessAnswerer;
1315
use PHPStan\Reflection\ExtendedMethodReflection;
@@ -16,6 +18,7 @@
1618
use PHPStan\Reflection\MissingConstantFromReflectionException;
1719
use PHPStan\Reflection\MissingMethodFromReflectionException;
1820
use PHPStan\Reflection\MissingPropertyFromReflectionException;
21+
use PHPStan\Reflection\ParametersAcceptorSelector;
1922
use PHPStan\Reflection\TrivialParametersAcceptor;
2023
use PHPStan\Reflection\Type\IntersectionTypeUnresolvedMethodPrototypeReflection;
2124
use PHPStan\Reflection\Type\IntersectionTypeUnresolvedPropertyPrototypeReflection;
@@ -1124,11 +1127,34 @@ public function isCallable(): TrinaryLogic
11241127

11251128
public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array
11261129
{
1127-
if ($this->isCallable()->no()) {
1128-
throw new ShouldNotHappenException();
1130+
$yesAcceptors = [];
1131+
1132+
foreach ($this->types as $type) {
1133+
if (!$type->isCallable()->yes()) {
1134+
continue;
1135+
}
1136+
$yesAcceptors[] = $type->getCallableParametersAcceptors($scope);
1137+
}
1138+
1139+
if (count($yesAcceptors) === 0) {
1140+
if ($this->isCallable()->no()) {
1141+
throw new ShouldNotHappenException();
1142+
}
1143+
1144+
return [new TrivialParametersAcceptor()];
1145+
}
1146+
1147+
$result = [];
1148+
$combinations = CombinationsHelper::combinations($yesAcceptors);
1149+
foreach ($combinations as $combination) {
1150+
$combined = ParametersAcceptorSelector::combineAcceptors($combination);
1151+
if (!$combined instanceof CallableParametersAcceptor) {
1152+
throw new ShouldNotHappenException();
1153+
}
1154+
$result[] = $combined;
11291155
}
11301156

1131-
return [new TrivialParametersAcceptor()];
1157+
return $result;
11321158
}
11331159

11341160
public function isCloneable(): TrinaryLogic
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php // lint >= 8.1
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug14362;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
interface A
10+
{
11+
public function __invoke(B $b): int;
12+
}
13+
14+
interface B
15+
{
16+
17+
}
18+
19+
class C {
20+
public static function u(): A&B {
21+
return new class() implements A, B {
22+
public function __invoke(B $b): int {
23+
return 1;
24+
}
25+
};
26+
}
27+
}
28+
29+
class D {
30+
public static function u(): A {
31+
return new class() implements A {
32+
public function __invoke(B $b): int {
33+
return 1;
34+
}
35+
};
36+
}
37+
}
38+
39+
interface E
40+
{
41+
public function __invoke(B $b, bool $option = true): int;
42+
}
43+
44+
interface F
45+
{
46+
47+
}
48+
49+
class G {
50+
public static function u(): A&E {
51+
return new class() implements A, E {
52+
public function __invoke(B $b, bool $option = true): int {
53+
return 1;
54+
}
55+
};
56+
}
57+
}
58+
59+
class H {
60+
public static function u(): B&F {
61+
return new class() implements B, F {
62+
};
63+
}
64+
}
65+
66+
function doBar() : void {
67+
assertType('Closure(Bug14362\B): int', C::u()(...));
68+
assertType('Closure(Bug14362\B): int', D::u()(...));
69+
70+
// Intersection with two yes-callable compatible
71+
assertType('Closure(Bug14362\B, bool=): int', G::u()(...));
72+
73+
// Intersection with only maybe-callable types (neither has __invoke)
74+
assertType('Closure', H::u()(...));
75+
}
76+
77+
function doFoo(string $c):void {
78+
if (is_callable($c)) {
79+
$a = $c;
80+
} else {
81+
$a = C::u()(...);
82+
}
83+
assertType('callable-string|(Closure(Bug14362\B): int)', $a);
84+
}

0 commit comments

Comments
 (0)