From c652f4460b6073bee19570dbce20292fbac35e54 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 4 Apr 2026 14:37:36 +0200 Subject: [PATCH 1/2] Add non regression test --- .../WrongVariableNameInVarTagRuleTest.php | 12 ++ .../Rules/PhpDoc/data/generic-subtype.php | 148 ++++++++++++++++++ 2 files changed, 160 insertions(+) create mode 100644 tests/PHPStan/Rules/PhpDoc/data/generic-subtype.php diff --git a/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php b/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php index 838211838ef..90fd45df71e 100644 --- a/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php @@ -586,6 +586,18 @@ public function testBug12457(): void ]); } + public function testGenericSubtype(): void + { + $this->checkTypeAgainstPhpDocType = true; + $this->strictWideningCheck = true; + $this->analyse([__DIR__ . '/data/generic-subtype.php'], [ + [ + 'PHPDoc tag @var with type GenericSubtype\IRepository is not subtype of type GenericSubtype\IRepository.', + 131, + ], + ]); + } + public function testNewIsAlwaysFinalClass(): void { $this->checkTypeAgainstPhpDocType = true; diff --git a/tests/PHPStan/Rules/PhpDoc/data/generic-subtype.php b/tests/PHPStan/Rules/PhpDoc/data/generic-subtype.php new file mode 100644 index 00000000000..b9f1329cd73 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/generic-subtype.php @@ -0,0 +1,148 @@ + + */ + public function getRepository(): IRepository; +} + +interface IProperty {} + +interface IPropertyContainer extends IProperty {} + +/** + * @template E of IEntity + */ +interface IEntityAwareProperty extends IProperty {} + +/** + * @template E of IEntity + * @extends IEntityAwareProperty + */ +interface IRelationshipContainer extends IPropertyContainer, IEntityAwareProperty {} + +interface IModel { + /** + * @template E of IEntity + * @template T of IRepository + * @param class-string $className + * @return T + */ + public function getRepository(string $className): IRepository; +} + +/** + * @template E of IEntity + */ +interface IRepository { + public function getModel(): IModel; +} + +class PropertyRelationshipMetadata { + /** @var class-string> */ + public string $repository; +} + +/** + * @template E of IEntity + * @implements IRelationshipContainer + */ +class HasOne implements IRelationshipContainer +{ + /** @var E|null */ + protected ?IEntity $parent = null; + + /** @var IRepository|null */ + protected ?IRepository $targetRepository = null; + + protected PropertyRelationshipMetadata $metadataRelationship; + + /** + * @return E + */ + protected function getParentEntity(): IEntity + { + return $this->parent ?? throw new \InvalidArgumentException('Relationship is not attached to a parent entity.'); + } + + /** + * @return IRepository + */ + protected function getTargetRepository(): IRepository + { + if ($this->targetRepository === null) { + /** @var IRepository $targetRepository */ + $targetRepository = $this->getParentEntity() + ->getRepository() + ->getModel() + ->getRepository($this->metadataRelationship->repository); + + $this->test($targetRepository); + + $this->targetRepository = $targetRepository; + } + + return $this->targetRepository; + } + + /** + * @param IRepository + */ + protected function test(): void {} +} + +class Foo implements IEntity { + public function getRepository(): IRepository { + throw new \BadMethodCallException(); + } +} + +/** + * @implements IRelationshipContainer + */ +class HasOne2 implements IRelationshipContainer +{ + /** @var Foo|null */ + protected ?IEntity $parent = null; + + /** @var IRepository|null */ + protected ?IRepository $targetRepository = null; + + protected PropertyRelationshipMetadata $metadataRelationship; + + /** + * @return Foo + */ + protected function getParentEntity(): IEntity + { + return $this->parent ?? throw new \InvalidArgumentException('Relationship is not attached to a parent entity.'); + } + + /** + * @return IRepository + */ + protected function getTargetRepository(): IRepository + { + if ($this->targetRepository === null) { + /** @var IRepository $targetRepository */ + $targetRepository = $this->getParentEntity() + ->getRepository() + ->getModel() + ->getRepository($this->metadataRelationship->repository); + + $this->test($targetRepository); + + $this->targetRepository = $targetRepository; + } + + return $this->targetRepository; + } + + /** + * @param IRepository $repository + */ + protected function test($repository): void {} +} From d85f2ab6d826748680eccb7ff26386deb24c0753 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 4 Apr 2026 14:44:08 +0200 Subject: [PATCH 2/2] Fix lint --- tests/PHPStan/Rules/PhpDoc/data/generic-subtype.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Rules/PhpDoc/data/generic-subtype.php b/tests/PHPStan/Rules/PhpDoc/data/generic-subtype.php index b9f1329cd73..adc29e95b15 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/generic-subtype.php +++ b/tests/PHPStan/Rules/PhpDoc/data/generic-subtype.php @@ -1,4 +1,4 @@ -= 8.0 namespace GenericSubtype;