From 4e8fbe02293bc3e56e972477ddf49a42d8bd3299 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 00:12:37 +0000 Subject: [PATCH] Fix phpstan/phpstan#5357: Intersection of generics returning *NEVER* - When two GenericObjectTypes of the same class are intersected (e.g. PagerInterface & PagerInterface), merge their type parameters instead of returning never - This fixes the case where a raw generic class (Datagrid) in an intersection with a generic interface (DatagridInterface) caused method return types to resolve to *NEVER* - New regression test in tests/PHPStan/Analyser/nsrt/bug-5357.php - Added baseline entry for instanceof GenericObjectType in TypeCombinator --- phpstan-baseline.neon | 6 ++ src/Type/TypeCombinator.php | 38 ++++++++- tests/PHPStan/Analyser/nsrt/bug-5357.php | 103 +++++++++++++++++++++++ 3 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-5357.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 6906e22da9d..eb0e91d0bad 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1713,6 +1713,12 @@ parameters: count: 2 path: src/Type/TypeCombinator.php + - + rawMessage: Doing instanceof PHPStan\Type\Generic\GenericObjectType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 2 + path: src/Type/TypeCombinator.php + - rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. identifier: phpstanApi.instanceofType diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index e7a0d6bdf52..3693c1c196a 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -20,6 +20,7 @@ use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Generic\GenericClassStringType; +use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Generic\TemplateArrayType; use PHPStan\Type\Generic\TemplateBenevolentUnionType; use PHPStan\Type\Generic\TemplateMixedType; @@ -1503,9 +1504,42 @@ public static function intersect(Type ...$types): Type continue 2; } - if ($isSuperTypeA->no()) { - return new NeverType(); + if (!$isSuperTypeA->no()) { + continue; } + + if ( + $types[$i] instanceof GenericObjectType + && $types[$j] instanceof GenericObjectType + && $types[$i]->getClassName() === $types[$j]->getClassName() + && count($types[$i]->getTypes()) === count($types[$j]->getTypes()) + ) { + $mergedParams = []; + foreach ($types[$i]->getTypes() as $k => $paramTypeI) { + $paramTypeJ = $types[$j]->getTypes()[$k]; + $merged = self::intersect($paramTypeI, $paramTypeJ); + if ($merged instanceof NeverType) { + return $merged; + } + $mergedParams[] = $merged; + } + $subtractedType = $types[$i]->getSubtractedType(); + if ($subtractedType !== null && $types[$j]->getSubtractedType() !== null) { + $subtractedType = self::union($subtractedType, $types[$j]->getSubtractedType()); + } elseif ($types[$j]->getSubtractedType() !== null) { + $subtractedType = $types[$j]->getSubtractedType(); + } + $types[$j] = new GenericObjectType( + $types[$j]->getClassName(), + $mergedParams, + $subtractedType, + ); + array_splice($types, $i--, 1); + $typesCount--; + continue 2; + } + + return new NeverType(); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-5357.php b/tests/PHPStan/Analyser/nsrt/bug-5357.php new file mode 100644 index 00000000000..6d00d98cc60 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5357.php @@ -0,0 +1,103 @@ + + */ +class Pager implements PagerInterface +{} + +/** + * @phpstan-template T of ProxyQueryInterface + */ +interface DatagridInterface +{ + /** + * @phpstan-return PagerInterface + */ + public function getPager(): PagerInterface; +} + +/** + * @phpstan-template T of ProxyQueryInterface + * @phpstan-implements DatagridInterface + */ +class Datagrid implements DatagridInterface +{ + /** + * @phpstan-return PagerInterface + */ + public function getPager(): PagerInterface + { + throw new \Exception(); + } +} + +interface ProxyQueryInterface {} + +class Proxy implements ProxyQueryInterface {} + +class MockObject {} + +/** + * @phpstan-template T of ProxyQueryInterface + */ +interface DatagridBuilderInterface +{ + /** + * @param AdminInterface $admin + * @param array $values + * + * @phpstan-return DatagridInterface + */ + public function getBaseDatagrid(AdminInterface $admin, array $values = []): DatagridInterface; +} + +/** + * @phpstan-implements DatagridBuilderInterface + */ +class DatagridBuilder implements DatagridBuilderInterface +{ + /** + * @param AdminInterface $admin + * @param array $values + * + * @phpstan-return DatagridInterface + */ + public function getBaseDatagrid(AdminInterface $admin, array $values = []): DatagridInterface + { + throw new \Exception(); + } +} + +class HelloWorld +{ + /** + * @var MockObject&AdminInterface + */ + public $admin; + + public DatagridBuilder $datagridBuilder; + + public function sayHello(): void + { + $datagrid = $this->datagridBuilder->getBaseDatagrid($this->admin); + assert($datagrid instanceof Datagrid); + assertType('Bug5357\Datagrid&Bug5357\DatagridInterface', $datagrid); + assertType('Bug5357\PagerInterface', $datagrid->getPager()); + } +}