From 0d9d0ed60db4ab0b91ced8e359ab1dcd90277f65 Mon Sep 17 00:00:00 2001 From: Emmanuel GUITON Date: Wed, 30 Nov 2022 17:17:38 +0100 Subject: [PATCH 1/4] Fixes vimeo#8112 --- .../Type/Comparator/ObjectComparator.php | 12 ++ tests/Template/NestedTemplateTest.php | 123 ++++++++++++++++++ 2 files changed, 135 insertions(+) diff --git a/src/Psalm/Internal/Type/Comparator/ObjectComparator.php b/src/Psalm/Internal/Type/Comparator/ObjectComparator.php index a0a15900294..5baa516305c 100644 --- a/src/Psalm/Internal/Type/Comparator/ObjectComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ObjectComparator.php @@ -6,6 +6,7 @@ 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; @@ -32,6 +33,17 @@ public static function isShallowlyContainedBy( bool $allow_interface_equality, ?TypeComparisonResult $atomic_comparison_result ): bool { + if ($container_type_part instanceof TTemplateParam && $input_type_part instanceof TTemplateParam + && 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, current($input_type_part->as->getAtomicTypes()), current($container_type_part->as->getAtomicTypes()), $allow_interface_equality, $atomic_comparison_result); + } else if ($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/NestedTemplateTest.php b/tests/Template/NestedTemplateTest.php index fceb2620d9d..2f23faeea41 100644 --- a/tests/Template/NestedTemplateTest.php +++ b/tests/Template/NestedTemplateTest.php @@ -124,6 +124,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 {}' + ] ]; } From fcbd23c801e6a0f5ab17bfb5a4a4f0fbfe4fb47f Mon Sep 17 00:00:00 2001 From: Emmanuel GUITON Date: Wed, 30 Nov 2022 17:33:28 +0100 Subject: [PATCH 2/4] Style fix. --- .../Type/Comparator/ObjectComparator.php | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/Psalm/Internal/Type/Comparator/ObjectComparator.php b/src/Psalm/Internal/Type/Comparator/ObjectComparator.php index 5baa516305c..dd879548e84 100644 --- a/src/Psalm/Internal/Type/Comparator/ObjectComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ObjectComparator.php @@ -12,6 +12,8 @@ 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; @@ -33,13 +35,21 @@ public static function isShallowlyContainedBy( bool $allow_interface_equality, ?TypeComparisonResult $atomic_comparison_result ): bool { - if ($container_type_part instanceof TTemplateParam && $input_type_part instanceof TTemplateParam - && 1 == count($container_type_part->as->getAtomicTypes()) && 1 == count($input_type_part->as->getAtomicTypes())) { + if ($container_type_part instanceof TTemplateParam + && $input_type_part instanceof TTemplateParam + && 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, current($input_type_part->as->getAtomicTypes()), current($container_type_part->as->getAtomicTypes()), $allow_interface_equality, $atomic_comparison_result); - } else if ($containerAs instanceof TMixed && $inputAs instanceof TMixed) { + return self::isShallowlyContainedBy( + $codebase, + $inputAs, + $containerAs, + $allow_interface_equality, + $atomic_comparison_result + ); + } elseif ($containerAs instanceof TMixed && $inputAs instanceof TMixed) { return true; } } From 3364bd97e1b447e6fbcdce2ca3e0f67881c4997e Mon Sep 17 00:00:00 2001 From: Emmanuel GUITON Date: Wed, 21 Dec 2022 11:51:42 +0100 Subject: [PATCH 3/4] Additional code style fixes (vimeo#8112). --- src/Psalm/Internal/Type/Comparator/ObjectComparator.php | 2 +- tests/Template/NestedTemplateTest.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Psalm/Internal/Type/Comparator/ObjectComparator.php b/src/Psalm/Internal/Type/Comparator/ObjectComparator.php index 1e35c93df93..246e2ed4f9b 100644 --- a/src/Psalm/Internal/Type/Comparator/ObjectComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ObjectComparator.php @@ -46,7 +46,7 @@ public static function isShallowlyContainedBy( $inputAs, $containerAs, $allow_interface_equality, - $atomic_comparison_result + $atomic_comparison_result, ); } elseif ($containerAs instanceof TMixed && $inputAs instanceof TMixed) { return true; diff --git a/tests/Template/NestedTemplateTest.php b/tests/Template/NestedTemplateTest.php index 935fc39cd20..fe2839b63a3 100644 --- a/tests/Template/NestedTemplateTest.php +++ b/tests/Template/NestedTemplateTest.php @@ -156,7 +156,7 @@ interface K3 extends J {} * @template V of B * @implements J */ - class K1 implements J {}' + class K1 implements J {}', ], '4levelNestedTemplatesOfObjects' => [ 'code' => ' */ abstract class AnObjectDbRepositoryWrapper - extends DbRepositoryWrapper {}' - ] + extends DbRepositoryWrapper {}', + ], ]; } From 164e279d08f31720abe079921d64afb0693467d1 Mon Sep 17 00:00:00 2001 From: Emmanuel GUITON Date: Thu, 22 Dec 2022 15:15:16 +0100 Subject: [PATCH 4/4] Fixed object comparison that aims to improve nested template analysis (vimeo#8112). --- src/Psalm/Internal/Type/Comparator/ObjectComparator.php | 3 +++ tests/Template/ClassTemplateExtendsTest.php | 3 --- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Internal/Type/Comparator/ObjectComparator.php b/src/Psalm/Internal/Type/Comparator/ObjectComparator.php index 246e2ed4f9b..46d1f731cd2 100644 --- a/src/Psalm/Internal/Type/Comparator/ObjectComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ObjectComparator.php @@ -36,6 +36,9 @@ public static function isShallowlyContainedBy( ): 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()); 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); }