diff --git a/src/Psalm/Internal/Type/Comparator/ObjectComparator.php b/src/Psalm/Internal/Type/Comparator/ObjectComparator.php index 7d4101b8c1b..46d1f731cd2 100644 --- a/src/Psalm/Internal/Type/Comparator/ObjectComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ObjectComparator.php @@ -6,11 +6,14 @@ use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer; use Psalm\Type\Atomic; use Psalm\Type\Atomic\TIterable; +use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TObjectWithProperties; use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Union; +use function count; +use function current; use function in_array; use function strpos; use function strtolower; @@ -31,6 +34,28 @@ public static function isShallowlyContainedBy( bool $allow_interface_equality, ?TypeComparisonResult $atomic_comparison_result ): bool { + if ($container_type_part instanceof TTemplateParam + && $input_type_part instanceof TTemplateParam + && $container_type_part->defining_class != $input_type_part->defining_class + && strpos($container_type_part->defining_class, 'fn-') !== 0 + && strpos($input_type_part->defining_class, 'fn-') !== 0 + && 1 == count($container_type_part->as->getAtomicTypes()) + && 1 == count($input_type_part->as->getAtomicTypes())) { + $containerAs = current($container_type_part->as->getAtomicTypes()); + $inputAs = current($input_type_part->as->getAtomicTypes()); + if ($containerAs instanceof TNamedObject && $inputAs instanceof TNamedObject) { + return self::isShallowlyContainedBy( + $codebase, + $inputAs, + $containerAs, + $allow_interface_equality, + $atomic_comparison_result, + ); + } elseif ($containerAs instanceof TMixed && $inputAs instanceof TMixed) { + return true; + } + } + $intersection_input_types = self::getIntersectionTypes($input_type_part); $intersection_container_types = self::getIntersectionTypes($container_type_part); diff --git a/tests/Template/ClassTemplateExtendsTest.php b/tests/Template/ClassTemplateExtendsTest.php index 17abbb12dd2..f5c1b2f13e2 100644 --- a/tests/Template/ClassTemplateExtendsTest.php +++ b/tests/Template/ClassTemplateExtendsTest.php @@ -1635,9 +1635,6 @@ public function __construct(array $elements = []) public function getIterator() { - /** - * @psalm-suppress InvalidReturnStatement - */ return new ArrayIterator($this->elements); } diff --git a/tests/Template/NestedTemplateTest.php b/tests/Template/NestedTemplateTest.php index 7db4c52778f..fe2839b63a3 100644 --- a/tests/Template/NestedTemplateTest.php +++ b/tests/Template/NestedTemplateTest.php @@ -121,6 +121,129 @@ function toList(array $arr): array { return reset($arr); }', ], + '3levelNestedTemplatesOfMixed' => [ + 'code' => ' + */ + interface B {} + + /** @template T */ + interface J {} + + /** + * @template T + * @template U of A + * @implements J + */ + class K2 implements J {} + + /** + * @template T + * @template U of A + * @template V of B + * @extends J + */ + interface K3 extends J {} + + /** + * @template T + * @template U of A + * @template V of B + * @implements J + */ + class K1 implements J {}', + ], + '4levelNestedTemplatesOfObjects' => [ + 'code' => ' + */ + abstract class DbEntityRepository + extends EntityRepository {} + + interface ObjectId {} + + /** + * @template I of ObjectId + */ + interface AnObject {} + + /** + * Base entity repository with common tooling. + * + * @template I of ObjectId + * @template O of AnObject + * @template E of DbEntity + * @extends DbEntityRepository + */ + abstract class AnObjectEntityRepository + extends DbEntityRepository + {} + + /** + * Base repository implementation backed by a Db repository. + * + * @template T + * @template E of DbEntity + * @template R of DbEntityRepository + */ + abstract class DbRepositoryWrapper + { + /** @var R $repo Db repository */ + private DbEntityRepository $repo; + + /** + * Getter for the Db repository. + * + * @return DbEntityRepository The Db repository. + * @psalm-return R + */ + protected function getDbRepo(): DbEntityRepository + { + return $this->repo; + } + } + + /** + * Base implementation for all custom repositories that map to Core objects. + * + * @template I of ObjectId + * @template O of AnObject + * @template E of DbEntity + * @template R of AnObjectEntityRepository + * @extends DbRepositoryWrapper + */ + abstract class AnObjectDbRepositoryWrapper + extends DbRepositoryWrapper {}', + ], ]; }