1
0
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:
Matthew Brown 2022-01-07 18:50:13 -05:00
parent 762ef8dab4
commit 4abbd9cb1b
3 changed files with 264 additions and 238 deletions

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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