diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 05b6a14753..10482dae04 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -2,12 +2,14 @@ namespace PHPStan\Type; +use PHPStan\Internal\CombinationsHelper; use PHPStan\Php\PhpVersion; use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode; use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\Reflection\Callables\CallableParametersAcceptor; use PHPStan\Reflection\ClassConstantReflection; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ExtendedMethodReflection; @@ -16,6 +18,7 @@ use PHPStan\Reflection\MissingConstantFromReflectionException; use PHPStan\Reflection\MissingMethodFromReflectionException; use PHPStan\Reflection\MissingPropertyFromReflectionException; +use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\Reflection\Type\IntersectionTypeUnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\IntersectionTypeUnresolvedPropertyPrototypeReflection; @@ -1124,11 +1127,34 @@ public function isCallable(): TrinaryLogic public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { - if ($this->isCallable()->no()) { - throw new ShouldNotHappenException(); + $yesAcceptors = []; + + foreach ($this->types as $type) { + if (!$type->isCallable()->yes()) { + continue; + } + $yesAcceptors[] = $type->getCallableParametersAcceptors($scope); + } + + if (count($yesAcceptors) === 0) { + if ($this->isCallable()->no()) { + throw new ShouldNotHappenException(); + } + + return [new TrivialParametersAcceptor()]; + } + + $result = []; + $combinations = CombinationsHelper::combinations($yesAcceptors); + foreach ($combinations as $combination) { + $combined = ParametersAcceptorSelector::combineAcceptors($combination); + if (!$combined instanceof CallableParametersAcceptor) { + throw new ShouldNotHappenException(); + } + $result[] = $combined; } - return [new TrivialParametersAcceptor()]; + return $result; } public function isCloneable(): TrinaryLogic diff --git a/tests/PHPStan/Analyser/nsrt/bug-14362.php b/tests/PHPStan/Analyser/nsrt/bug-14362.php new file mode 100644 index 0000000000..69c04c00d2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14362.php @@ -0,0 +1,84 @@ += 8.1 + +declare(strict_types = 1); + +namespace Bug14362; + +use function PHPStan\Testing\assertType; + +interface A +{ + public function __invoke(B $b): int; +} + +interface B +{ + +} + +class C { + public static function u(): A&B { + return new class() implements A, B { + public function __invoke(B $b): int { + return 1; + } + }; + } +} + +class D { + public static function u(): A { + return new class() implements A { + public function __invoke(B $b): int { + return 1; + } + }; + } +} + +interface E +{ + public function __invoke(B $b, bool $option = true): int; +} + +interface F +{ + +} + +class G { + public static function u(): A&E { + return new class() implements A, E { + public function __invoke(B $b, bool $option = true): int { + return 1; + } + }; + } +} + +class H { + public static function u(): B&F { + return new class() implements B, F { + }; + } +} + +function doBar() : void { + assertType('Closure(Bug14362\B): int', C::u()(...)); + assertType('Closure(Bug14362\B): int', D::u()(...)); + + // Intersection with two yes-callable compatible + assertType('Closure(Bug14362\B, bool=): int', G::u()(...)); + + // Intersection with only maybe-callable types (neither has __invoke) + assertType('Closure', H::u()(...)); +} + +function doFoo(string $c):void { + if (is_callable($c)) { + $a = $c; + } else { + $a = C::u()(...); + } + assertType('callable-string|(Closure(Bug14362\B): int)', $a); +}