mirror of
https://github.com/danog/psalm.git
synced 2024-11-27 04:45:20 +01:00
Simplify object comparison
This commit is contained in:
parent
762ef8dab4
commit
4abbd9cb1b
@ -7,12 +7,10 @@ use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
|
||||
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 @@ class ObjectComparator
|
||||
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;
|
||||
$intersection_input_types = self::getIntersectionTypes($input_type_part);
|
||||
$intersection_container_types = self::getIntersectionTypes($container_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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 @@ class ObjectComparator
|
||||
} 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,158 +54,137 @@ class ObjectComparator
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
$container_type_is_interface = $intersection_container_type_lower
|
||||
&& $codebase->interfaceExists($intersection_container_type_lower);
|
||||
|
||||
$intersection_input_type_lower = null;
|
||||
|
||||
foreach ($intersection_input_type->as->getAtomicTypes() as $g) {
|
||||
if ($g instanceof TNull) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$g instanceof TNamedObject) {
|
||||
continue 2;
|
||||
}
|
||||
|
||||
$intersection_input_type_lower = strtolower($g->value);
|
||||
}
|
||||
|
||||
if ($intersection_input_type_lower === null) {
|
||||
return false;
|
||||
}
|
||||
} 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
|
||||
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])
|
||||
) {
|
||||
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) {
|
||||
$input_class_storage = $codebase->classlike_storage_provider->get(
|
||||
$intersection_input_type->defining_class
|
||||
);
|
||||
$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 (isset($input_class_storage->template_extended_params
|
||||
[$intersection_container_type->defining_class]
|
||||
[$intersection_container_type->param_name])
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!$any_inputs_contained) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TNamedObject|TTemplateParam|TIterable $type_part
|
||||
* @return array<string, TNamedObject|TTemplateParam|TIterable|TObjectWithProperties>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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_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;
|
||||
}
|
||||
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
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
continue;
|
||||
foreach ($intersection_input_type->as->getAtomicTypes() as $input_as_atomic) {
|
||||
if ($input_as_atomic->equals($intersection_container_type, false)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
continue 2;
|
||||
}
|
||||
|
||||
if ($intersection_input_type_lower === 'generator'
|
||||
&& in_array($intersection_container_type_lower, ['iterator', 'traversable', 'iterable'], true)
|
||||
) {
|
||||
continue 2;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
if ($intersection_input_type_lower === 'traversable'
|
||||
&& $intersection_container_type_lower === 'iterable'
|
||||
) {
|
||||
continue 2;
|
||||
}
|
||||
|
||||
$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 || !isset($intersection_container_types[$intersection_input_key]))
|
||||
) {
|
||||
continue 2;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
continue 2;
|
||||
}
|
||||
|
||||
if ($input_type_is_interface
|
||||
&& $codebase->interfaceExtends(
|
||||
$intersection_input_type_lower,
|
||||
$intersection_container_type_lower
|
||||
)
|
||||
) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ExpressionAnalyzer::isMock($intersection_input_type_lower)) {
|
||||
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
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$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])
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -306,6 +192,130 @@ class ObjectComparator
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
if ($intersection_container_type instanceof TTemplateParam
|
||||
|| $intersection_container_type_lower === null
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($intersection_input_type instanceof TTemplateParam) {
|
||||
$intersection_container_type = clone $intersection_container_type;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
return UnionTypeComparator::isContainedBy(
|
||||
$codebase,
|
||||
$intersection_input_type->as,
|
||||
new Union([$intersection_container_type]),
|
||||
false,
|
||||
false,
|
||||
$atomic_comparison_result,
|
||||
$allow_interface_equality
|
||||
);
|
||||
}
|
||||
|
||||
$input_was_static = false;
|
||||
|
||||
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;
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
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 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;
|
||||
}
|
||||
}
|
||||
|
@ -704,6 +704,13 @@ abstract class Type
|
||||
$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 @@ abstract class Type
|
||||
|| $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;
|
||||
}
|
||||
}
|
||||
|
@ -2070,7 +2070,7 @@ class FunctionTemplateTest extends TestCase
|
||||
function returnsTemplatedIntersection(object $t) {
|
||||
return $t;
|
||||
}',
|
||||
'error_message' => 'InvalidReturnStatement',
|
||||
'error_message' => 'LessSpecificReturnStatement',
|
||||
],
|
||||
'returnIntersectionWhenTemplateIsExpectedBackward' => [
|
||||
'<?php
|
||||
@ -2084,7 +2084,7 @@ class FunctionTemplateTest extends TestCase
|
||||
function returnsTemplatedIntersection(object $t) {
|
||||
return $t;
|
||||
}',
|
||||
'error_message' => 'InvalidReturnStatement',
|
||||
'error_message' => 'LessSpecificReturnStatement',
|
||||
],
|
||||
'bottomTypeInClosureShouldClash' => [
|
||||
'<?php
|
||||
|
Loading…
Reference in New Issue
Block a user