diff --git a/src/Psalm/Internal/Type/Comparator/ObjectComparator.php b/src/Psalm/Internal/Type/Comparator/ObjectComparator.php index b7b4ff301db..43facc0cf49 100644 --- a/src/Psalm/Internal/Type/Comparator/ObjectComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ObjectComparator.php @@ -7,12 +7,10 @@ use Psalm\Type\Atomic; use Psalm\Type\Atomic\TIterable; use Psalm\Type\Atomic\TNamedObject; -use Psalm\Type\Atomic\TNull; -use Psalm\Type\Atomic\TObject; use Psalm\Type\Atomic\TObjectWithProperties; use Psalm\Type\Atomic\TTemplateParam; +use Psalm\Type\Union; -use function array_merge; use function in_array; use function strpos; use function strtolower; @@ -34,35 +32,10 @@ public static function isShallowlyContainedBy( bool $allow_interface_equality, ?TypeComparisonResult $atomic_comparison_result ): bool { - $intersection_input_types = $input_type_part->extra_types ?: []; - $intersection_input_types[$input_type_part->getKey(false)] = $input_type_part; - - if ($input_type_part instanceof TTemplateParam) { - foreach ($input_type_part->as->getAtomicTypes() as $g) { - if ($g instanceof TNamedObject && $g->extra_types) { - $intersection_input_types = array_merge( - $intersection_input_types, - $g->extra_types - ); - } - } - } - - $intersection_container_types = $container_type_part->extra_types ?: []; - $intersection_container_types[$container_type_part->getKey(false)] = $container_type_part; - - if ($container_type_part instanceof TTemplateParam) { - foreach ($container_type_part->as->getAtomicTypes() as $g) { - if ($g instanceof TNamedObject && $g->extra_types) { - $intersection_container_types = array_merge( - $intersection_container_types, - $g->extra_types - ); - } - } - } + $intersection_input_types = self::getIntersectionTypes($input_type_part); + $intersection_container_types = self::getIntersectionTypes($container_type_part); - foreach ($intersection_container_types as $container_type_key => $intersection_container_type) { + foreach ($intersection_container_types as $intersection_container_type) { $container_was_static = false; if ($intersection_container_type instanceof TIterable) { @@ -70,73 +43,7 @@ public static function isShallowlyContainedBy( } elseif ($intersection_container_type instanceof TObjectWithProperties) { $intersection_container_type_lower = 'object'; } elseif ($intersection_container_type instanceof TTemplateParam) { - if (!$allow_interface_equality) { - if (isset($intersection_input_types[$container_type_key])) { - continue; - } - - foreach ($intersection_input_types as $intersection_input_type) { - if ($intersection_input_type instanceof TTemplateParam - && (strpos($intersection_container_type->defining_class, 'fn-') === 0 - || strpos($intersection_input_type->defining_class, 'fn-') === 0) - ) { - if (strpos($intersection_input_type->defining_class, 'fn-') === 0 - && strpos($intersection_container_type->defining_class, 'fn-') === 0 - && $intersection_input_type->defining_class - !== $intersection_container_type->defining_class - ) { - continue 2; - } - - foreach ($intersection_input_type->as->getAtomicTypes() as $input_as_atomic) { - if ($input_as_atomic->equals($intersection_container_type, false)) { - continue 3; - } - } - } elseif ($intersection_input_type instanceof TTemplateParam) { - $container_param = $intersection_container_type->param_name; - $container_class = $intersection_container_type->defining_class; - $input_class_like = $codebase->classlikes - ->getStorageFor($intersection_input_type->defining_class); - - if ($codebase->classlikes->traitExists($container_class) - && $input_class_like !== null - && isset( - $input_class_like->template_extended_params[$container_class][$container_param] - )) { - continue 2; - } - } - } - - return false; - } - - if ($intersection_container_type->as->isMixed()) { - continue; - } - $intersection_container_type_lower = null; - - foreach ($intersection_container_type->as->getAtomicTypes() as $g) { - if ($g instanceof TNull) { - continue; - } - - if ($g instanceof TObject) { - continue 2; - } - - if (!$g instanceof TNamedObject) { - continue 2; - } - - $intersection_container_type_lower = strtolower($g->value); - } - - if ($intersection_container_type_lower === null) { - return false; - } } else { $container_was_static = $intersection_container_type->was_static; @@ -147,165 +54,268 @@ public static function isShallowlyContainedBy( ); } - foreach ($intersection_input_types as $intersection_input_key => $intersection_input_type) { - $input_was_static = false; + $any_inputs_contained = false; - if ($intersection_input_type instanceof TIterable) { - $intersection_input_type_lower = 'iterable'; - } elseif ($intersection_input_type instanceof TObjectWithProperties) { - $intersection_input_type_lower = 'object'; - } elseif ($intersection_input_type instanceof TTemplateParam) { - if ($intersection_input_type->as->isMixed()) { - continue; - } - - $intersection_input_type_lower = null; + $container_type_is_interface = $intersection_container_type_lower + && $codebase->interfaceExists($intersection_container_type_lower); - foreach ($intersection_input_type->as->getAtomicTypes() as $g) { - if ($g instanceof TNull) { - continue; - } + foreach ($intersection_input_types as $input_type_key => $intersection_input_type) { + if ($allow_interface_equality + && $container_type_is_interface + && !isset($intersection_container_types[$input_type_key]) + ) { + $any_inputs_contained = true; + } elseif (self::isIntersectionShallowlyContainedBy( + $codebase, + $intersection_input_type, + $intersection_container_type, + $intersection_container_type_lower, + $container_was_static, + $allow_interface_equality, + $atomic_comparison_result + )) { + $any_inputs_contained = true; + } + } - if (!$g instanceof TNamedObject) { - continue 2; - } + if (!$any_inputs_contained) { + return false; + } + } - $intersection_input_type_lower = strtolower($g->value); - } + return true; + } - if ($intersection_input_type_lower === null) { - return false; + /** + * @param TNamedObject|TTemplateParam|TIterable $type_part + * @return array + */ + private static function getIntersectionTypes(Atomic $type_part): array + { + if (!$type_part->extra_types) { + if ($type_part instanceof TTemplateParam) { + $intersection_types = []; + + foreach ($type_part->as->getAtomicTypes() as $as_atomic_type) { + // T1 as T2 as object becomes (T1 as object) & (T2 as object) + if ($as_atomic_type instanceof TTemplateParam) { + $intersection_types += self::getIntersectionTypes($as_atomic_type); + $type_part = clone $type_part; + $type_part->as = $as_atomic_type->as; + $intersection_types[$type_part->getKey()] = $type_part; + + return $intersection_types; } - } else { - $input_was_static = $intersection_input_type->was_static; - - $intersection_input_type_lower = strtolower( - $codebase->classlikes->getUnAliasedName( - $intersection_input_type->value - ) - ); } + } - if ($intersection_container_type instanceof TTemplateParam - && $intersection_input_type instanceof TTemplateParam + return [$type_part->getKey() => $type_part]; + } + + $type_part = clone $type_part; + + $extra_types = $type_part->extra_types; + $type_part->extra_types = null; + + $extra_types[$type_part->getKey()] = $type_part; + + return $extra_types; + } + + /** + * @param TNamedObject|TTemplateParam|TIterable|TObjectWithProperties $intersection_input_type + * @param TNamedObject|TTemplateParam|TIterable|TObjectWithProperties $intersection_container_type + * + */ + private static function isIntersectionShallowlyContainedBy( + Codebase $codebase, + Atomic $intersection_input_type, + Atomic $intersection_container_type, + ?string $intersection_container_type_lower, + bool $container_was_static, + bool $allow_interface_equality, + ?TypeComparisonResult $atomic_comparison_result + ): bool { + if ($intersection_container_type instanceof TTemplateParam + && $intersection_input_type instanceof TTemplateParam + ) { + if (!$allow_interface_equality) { + if (strpos($intersection_container_type->defining_class, 'fn-') === 0 + || strpos($intersection_input_type->defining_class, 'fn-') === 0 ) { - if ($intersection_container_type->param_name !== $intersection_input_type->param_name - || ($intersection_container_type->defining_class - !== $intersection_input_type->defining_class - && strpos($intersection_input_type->defining_class, 'fn-') !== 0 - && strpos($intersection_container_type->defining_class, 'fn-') !== 0) + if (strpos($intersection_input_type->defining_class, 'fn-') === 0 + && strpos($intersection_container_type->defining_class, 'fn-') === 0 + && $intersection_input_type->defining_class + !== $intersection_container_type->defining_class ) { - if (strpos($intersection_input_type->defining_class, 'fn-') !== 0) { - $input_class_storage = $codebase->classlike_storage_provider->get( - $intersection_input_type->defining_class - ); - - if (isset($input_class_storage->template_extended_params - [$intersection_container_type->defining_class] - [$intersection_container_type->param_name]) - ) { - continue; - } - } + return true; + } - return false; + foreach ($intersection_input_type->as->getAtomicTypes() as $input_as_atomic) { + if ($input_as_atomic->equals($intersection_container_type, false)) { + return true; + } } } + } - if (!$intersection_container_type instanceof TTemplateParam - || $intersection_input_type instanceof TTemplateParam + if ($intersection_container_type->param_name === $intersection_input_type->param_name + && $intersection_container_type->defining_class === $intersection_input_type->defining_class + ) { + return true; + } + + if ($intersection_container_type->param_name !== $intersection_input_type->param_name + || ($intersection_container_type->defining_class + !== $intersection_input_type->defining_class + && strpos($intersection_input_type->defining_class, 'fn-') !== 0 + && strpos($intersection_container_type->defining_class, 'fn-') !== 0) + ) { + if (strpos($intersection_input_type->defining_class, 'fn-') === 0 + || strpos($intersection_container_type->defining_class, 'fn-') === 0 ) { - if ($intersection_container_type_lower === $intersection_input_type_lower) { - if ($container_was_static - && !$input_was_static - && !$intersection_input_type instanceof TTemplateParam - ) { - if ($atomic_comparison_result) { - $atomic_comparison_result->type_coerced = true; - } - - continue; - } + return false; + } - continue 2; - } + $input_class_storage = $codebase->classlike_storage_provider->get( + $intersection_input_type->defining_class + ); - if ($intersection_input_type_lower === 'generator' - && in_array($intersection_container_type_lower, ['iterator', 'traversable', 'iterable'], true) - ) { - continue 2; - } + if (isset($input_class_storage->template_extended_params + [$intersection_container_type->defining_class] + [$intersection_container_type->param_name]) + ) { + return true; + } + } - if ($intersection_container_type_lower === 'iterable') { - if ($intersection_input_type_lower === 'traversable' - || ($codebase->classlikes->classExists($intersection_input_type_lower) - && $codebase->classlikes->classImplements( - $intersection_input_type_lower, - 'Traversable' - )) - || ($codebase->classlikes->interfaceExists($intersection_input_type_lower) - && $codebase->classlikes->interfaceExtends( - $intersection_input_type_lower, - 'Traversable' - )) - ) { - continue 2; - } - } + return false; + } - if ($intersection_input_type_lower === 'traversable' - && $intersection_container_type_lower === 'iterable' - ) { - continue 2; - } + if ($intersection_container_type instanceof TTemplateParam + || $intersection_container_type_lower === null + ) { + return false; + } - $input_type_is_interface = $codebase->interfaceExists($intersection_input_type_lower); - $container_type_is_interface = $codebase->interfaceExists($intersection_container_type_lower); + if ($intersection_input_type instanceof TTemplateParam) { + $intersection_container_type = clone $intersection_container_type; - if ($allow_interface_equality - && $container_type_is_interface - && ($input_type_is_interface || !isset($intersection_container_types[$intersection_input_key])) - ) { - continue 2; - } + if ($intersection_container_type instanceof TNamedObject) { + // this is extra check is redundant since we're comparing to a template as type + $intersection_container_type->was_static = false; + } - if (($codebase->classExists($intersection_input_type_lower) - || $codebase->classlikes->enumExists($intersection_input_type_lower)) - && $codebase->classOrInterfaceExists($intersection_container_type_lower) - && $codebase->classExtendsOrImplements( - $intersection_input_type_lower, - $intersection_container_type_lower - ) - ) { - if ($container_was_static && !$input_was_static) { - if ($atomic_comparison_result) { - $atomic_comparison_result->type_coerced = true; - } + return UnionTypeComparator::isContainedBy( + $codebase, + $intersection_input_type->as, + new Union([$intersection_container_type]), + false, + false, + $atomic_comparison_result, + $allow_interface_equality + ); + } - continue; - } + $input_was_static = false; - continue 2; - } + if ($intersection_input_type instanceof TIterable) { + $intersection_input_type_lower = 'iterable'; + } elseif ($intersection_input_type instanceof TObjectWithProperties) { + $intersection_input_type_lower = 'object'; + } else { + $input_was_static = $intersection_input_type->was_static; - if ($input_type_is_interface - && $codebase->interfaceExtends( - $intersection_input_type_lower, - $intersection_container_type_lower - ) - ) { - continue 2; - } + $intersection_input_type_lower = strtolower( + $codebase->classlikes->getUnAliasedName( + $intersection_input_type->value + ) + ); + } + + if ($intersection_container_type_lower === $intersection_input_type_lower) { + if ($container_was_static && !$input_was_static) { + if ($atomic_comparison_result) { + $atomic_comparison_result->type_coerced = true; } - if (ExpressionAnalyzer::isMock($intersection_input_type_lower)) { - return true; + return false; + } + + return true; + } + + if ($intersection_input_type_lower === 'generator' + && in_array($intersection_container_type_lower, ['iterator', 'traversable', 'iterable'], true) + ) { + return true; + } + + if ($intersection_container_type_lower === 'iterable') { + if ($intersection_input_type_lower === 'traversable' + || ($codebase->classlikes->classExists($intersection_input_type_lower) + && $codebase->classlikes->classImplements( + $intersection_input_type_lower, + 'Traversable' + )) + || ($codebase->classlikes->interfaceExists($intersection_input_type_lower) + && $codebase->classlikes->interfaceExtends( + $intersection_input_type_lower, + 'Traversable' + )) + ) { + return true; + } + } + + if ($intersection_input_type_lower === 'traversable' + && $intersection_container_type_lower === 'iterable' + ) { + return true; + } + + $input_type_is_interface = $codebase->interfaceExists($intersection_input_type_lower); + $container_type_is_interface = $codebase->interfaceExists($intersection_container_type_lower); + + if ($allow_interface_equality + && $container_type_is_interface + && $input_type_is_interface + ) { + return true; + } + + if (($codebase->classExists($intersection_input_type_lower) + || $codebase->classlikes->enumExists($intersection_input_type_lower)) + && $codebase->classOrInterfaceExists($intersection_container_type_lower) + && $codebase->classExtendsOrImplements( + $intersection_input_type_lower, + $intersection_container_type_lower + ) + ) { + if ($container_was_static && !$input_was_static) { + if ($atomic_comparison_result) { + $atomic_comparison_result->type_coerced = true; } + + return false; } - return false; + return true; } - return true; + if ($input_type_is_interface + && $codebase->interfaceExtends( + $intersection_input_type_lower, + $intersection_container_type_lower + ) + ) { + return true; + } + + if (ExpressionAnalyzer::isMock($intersection_input_type_lower)) { + return true; + } + + return false; } } diff --git a/src/Psalm/Type.php b/src/Psalm/Type.php index 1d31caaac0a..8ba33ed81d5 100644 --- a/src/Psalm/Type.php +++ b/src/Psalm/Type.php @@ -704,6 +704,13 @@ private static function intersectAtomicTypes( $wider_type = $type_2_atomic; $intersection_performed = true; } + + if ($intersection_atomic + && !self::hasIntersection($type_1_atomic) + && !self::hasIntersection($type_2_atomic) + ) { + return $intersection_atomic; + } } if (static::mayHaveIntersection($type_1_atomic) @@ -763,4 +770,13 @@ private static function mayHaveIntersection(Atomic $type): bool || $type instanceof TTemplateParam || $type instanceof TObjectWithProperties; } + + private static function hasIntersection(Atomic $type): bool + { + return ($type instanceof TIterable + || $type instanceof TNamedObject + || $type instanceof TTemplateParam + || $type instanceof TObjectWithProperties + ) && $type->extra_types; + } } diff --git a/tests/Template/FunctionTemplateTest.php b/tests/Template/FunctionTemplateTest.php index f831dfc85b5..b2db707fcc9 100644 --- a/tests/Template/FunctionTemplateTest.php +++ b/tests/Template/FunctionTemplateTest.php @@ -2070,7 +2070,7 @@ interface Baz {} function returnsTemplatedIntersection(object $t) { return $t; }', - 'error_message' => 'InvalidReturnStatement', + 'error_message' => 'LessSpecificReturnStatement', ], 'returnIntersectionWhenTemplateIsExpectedBackward' => [ ' 'InvalidReturnStatement', + 'error_message' => 'LessSpecificReturnStatement', ], 'bottomTypeInClosureShouldClash' => [ '