diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index f6cd6fd593..997cbddd6a 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1437,12 +1437,24 @@ parameters: count: 1 path: src/Type/IntersectionType.php + - + rawMessage: Doing instanceof PHPStan\Type\Generic\GenericObjectType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 5 + path: src/Type/IntersectionType.php + - rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. identifier: phpstanApi.instanceofType count: 3 path: src/Type/IntersectionType.php + - + rawMessage: 'Doing instanceof PHPStan\Type\ObjectType is error-prone and deprecated. Use Type::isObject() or Type::getObjectClassNames() instead.' + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/IntersectionType.php + - rawMessage: 'Method PHPStan\Type\IntersectionType::getConstantArrays() should return list but returns array{PHPStan\Type\Type}.' identifier: return.type diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index de09752cac..e636f94d49 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -43,6 +43,7 @@ use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Enum\EnumCaseObjectType; +use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Generic\TemplateArrayType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeMap; @@ -669,6 +670,10 @@ public function getUnresolvedInstancePropertyPrototype(string $propertyName, Cla continue; } + if ($type instanceof ObjectType && !$type instanceof GenericObjectType && $this->hasGenericAncestorWithPropertyInIntersection($type, $propertyName, false)) { + continue; + } + $propertyPrototypes[] = $type->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->withFechedOnType($this); } @@ -702,6 +707,10 @@ public function getUnresolvedStaticPropertyPrototype(string $propertyName, Class continue; } + if ($type instanceof ObjectType && !$type instanceof GenericObjectType && $this->hasGenericAncestorWithPropertyInIntersection($type, $propertyName, true)) { + continue; + } + $propertyPrototypes[] = $type->getUnresolvedStaticPropertyPrototype($propertyName, $scope)->withFechedOnType($this); } @@ -740,6 +749,10 @@ public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAcce continue; } + if ($type instanceof ObjectType && !$type instanceof GenericObjectType && $this->hasGenericAncestorWithMethodInIntersection($type, $methodName)) { + continue; + } + $methodPrototypes[] = $type->getUnresolvedMethodPrototype($methodName, $scope)->withCalledOnType($this); } @@ -755,6 +768,62 @@ public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAcce return new IntersectionTypeUnresolvedMethodPrototypeReflection($methodName, $methodPrototypes); } + private function hasGenericAncestorWithMethodInIntersection(ObjectType $type, string $methodName): bool + { + $nakedReflection = $type->getNakedClassReflection(); + if ($nakedReflection === null || !$nakedReflection->isGeneric()) { + return false; + } + + foreach ($this->types as $otherType) { + if ($otherType === $type) { + continue; + } + if (!$otherType instanceof GenericObjectType) { + continue; + } + if ($type->getAncestorWithClassName($otherType->getClassName()) === null) { + continue; + } + if ($otherType->hasMethod($methodName)->yes()) { + return true; + } + } + + return false; + } + + private function hasGenericAncestorWithPropertyInIntersection(ObjectType $type, string $propertyName, bool $static): bool + { + $nakedReflection = $type->getNakedClassReflection(); + if ($nakedReflection === null || !$nakedReflection->isGeneric()) { + return false; + } + + foreach ($this->types as $otherType) { + if ($otherType === $type) { + continue; + } + if (!$otherType instanceof GenericObjectType) { + continue; + } + if ($type->getAncestorWithClassName($otherType->getClassName()) === null) { + continue; + } + if ($static) { + if ($otherType->hasStaticProperty($propertyName)->yes()) { + return true; + } + } else { + if ($otherType->hasInstanceProperty($propertyName)->yes()) { + return true; + } + } + } + + return false; + } + public function canAccessConstants(): TrinaryLogic { return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->canAccessConstants()); diff --git a/tests/PHPStan/Analyser/nsrt/bug-5357.php b/tests/PHPStan/Analyser/nsrt/bug-5357.php new file mode 100644 index 0000000000..28409396bd --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5357.php @@ -0,0 +1,172 @@ + + */ +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(); + } + + /** + * Method unique to Datagrid (not on DatagridInterface) + * @phpstan-return T + */ + public function getQuery(): ProxyQueryInterface + { + throw new \Exception(); + } +} + +interface ProxyQueryInterface {} + +class Proxy implements ProxyQueryInterface {} + +class MockObject {} + +/** + * @phpstan-template T of ProxyQueryInterface + * @phpstan-extends Datagrid + */ +class ChildDatagrid extends Datagrid +{ +} + +/** + * @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()); + } + + /** + * @param Datagrid&DatagridInterface $datagrid + */ + public function testDirect($datagrid): void + { + assertType('Bug5357\PagerInterface', $datagrid->getPager()); + } + + /** + * @param Datagrid $datagrid + */ + public function testGeneric($datagrid): void + { + assertType('Bug5357\PagerInterface', $datagrid->getPager()); + } + + /** + * Test with parent class generic intersection (not interface) + * @param ChildDatagrid&Datagrid $datagrid + */ + public function testWithGenericParentClass($datagrid): void + { + assertType('Bug5357\PagerInterface', $datagrid->getPager()); + } + + /** + * Test with multiple generic levels in hierarchy + * @param ChildDatagrid&DatagridInterface $datagrid + */ + public function testWithGrandparentInterface($datagrid): void + { + assertType('Bug5357\PagerInterface', $datagrid->getPager()); + } + + /** + * When both types already have explicit generics, existing intersection behavior should be preserved + * @param Datagrid&DatagridInterface $datagrid + */ + public function testBothGeneric($datagrid): void + { + assertType('Bug5357\PagerInterface', $datagrid->getPager()); + } + + /** + * Method unique to Datagrid should still resolve to bounds when raw type is skipped + * (it won't be skipped because DatagridInterface doesn't have getQuery) + * @param Datagrid&DatagridInterface $datagrid + */ + public function testUniqueMethod($datagrid): void + { + assertType('Bug5357\ProxyQueryInterface', $datagrid->getQuery()); + } +}