mirror of
https://github.com/danog/psalm.git
synced 2025-01-22 13:51:54 +01:00
Fix generic object comparison to use template constraint as default (fixes #8068).
This commit is contained in:
parent
53c3c9b7c5
commit
af5c191e7b
@ -63,7 +63,7 @@ use Psalm\Type\Union;
|
||||
|
||||
use function array_keys;
|
||||
use function array_search;
|
||||
use function array_values;
|
||||
use function count;
|
||||
use function in_array;
|
||||
use function is_int;
|
||||
use function is_string;
|
||||
@ -570,15 +570,9 @@ class AtomicPropertyFetchAnalyzer
|
||||
$class_storage->parent_class
|
||||
);
|
||||
|
||||
if ($class_storage->template_types) {
|
||||
if (count($template_types = $class_storage->getClassTemplateTypes()) !== 0) {
|
||||
if (!$lhs_type_part instanceof TGenericObject) {
|
||||
$type_params = [];
|
||||
|
||||
foreach ($class_storage->template_types as $type_map) {
|
||||
$type_params[] = clone array_values($type_map)[0];
|
||||
}
|
||||
|
||||
$lhs_type_part = new TGenericObject($lhs_type_part->value, $type_params);
|
||||
$lhs_type_part = new TGenericObject($lhs_type_part->value, $template_types);
|
||||
}
|
||||
|
||||
$stmt_type = self::localizePropertyType(
|
||||
@ -1100,15 +1094,9 @@ class AtomicPropertyFetchAnalyzer
|
||||
) {
|
||||
$stmt_type = clone $class_storage->pseudo_property_get_types['$' . $prop_name];
|
||||
|
||||
if ($class_storage->template_types) {
|
||||
if (count($template_types = $class_storage->getClassTemplateTypes()) !== 0) {
|
||||
if (!$lhs_type_part instanceof TGenericObject) {
|
||||
$type_params = [];
|
||||
|
||||
foreach ($class_storage->template_types as $type_map) {
|
||||
$type_params[] = clone array_values($type_map)[0];
|
||||
}
|
||||
|
||||
$lhs_type_part = new TGenericObject($lhs_type_part->value, $type_params);
|
||||
$lhs_type_part = new TGenericObject($lhs_type_part->value, $template_types);
|
||||
}
|
||||
|
||||
$stmt_type = self::localizePropertyType(
|
||||
@ -1200,15 +1188,9 @@ class AtomicPropertyFetchAnalyzer
|
||||
$declaring_class_storage->parent_class
|
||||
);
|
||||
|
||||
if ($declaring_class_storage->template_types) {
|
||||
if (count($template_types = $declaring_class_storage->getClassTemplateTypes()) !== 0) {
|
||||
if (!$lhs_type_part instanceof TGenericObject) {
|
||||
$type_params = [];
|
||||
|
||||
foreach ($declaring_class_storage->template_types as $type_map) {
|
||||
$type_params[] = clone array_values($type_map)[0];
|
||||
}
|
||||
|
||||
$lhs_type_part = new TGenericObject($lhs_type_part->value, $type_params);
|
||||
$lhs_type_part = new TGenericObject($lhs_type_part->value, $template_types);
|
||||
}
|
||||
|
||||
$class_property_type = self::localizePropertyType(
|
||||
|
@ -4,16 +4,11 @@ namespace Psalm\Internal\Type\Comparator;
|
||||
|
||||
use Psalm\Codebase;
|
||||
use Psalm\Internal\Type\TemplateStandinTypeReplacer;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Atomic;
|
||||
use Psalm\Type\Atomic\TGenericObject;
|
||||
use Psalm\Type\Atomic\TIterable;
|
||||
use Psalm\Type\Atomic\TNamedObject;
|
||||
|
||||
use function array_fill;
|
||||
use function array_values;
|
||||
use function count;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@ -44,38 +39,13 @@ class GenericTypeComparator
|
||||
$container_was_iterable = true;
|
||||
}
|
||||
|
||||
if (!$input_type_part instanceof TGenericObject && !$input_type_part instanceof TIterable) {
|
||||
if ($input_type_part instanceof TNamedObject
|
||||
&& $codebase->classExists($input_type_part->value)
|
||||
) {
|
||||
$class_storage = $codebase->classlike_storage_provider->get($input_type_part->value);
|
||||
|
||||
$container_class = $container_type_part->value;
|
||||
|
||||
// attempt to transform it
|
||||
if (!empty($class_storage->template_extended_params[$container_class])) {
|
||||
$input_type_part = new TGenericObject(
|
||||
$input_type_part->value,
|
||||
array_values($class_storage->template_extended_params[$container_class])
|
||||
);
|
||||
}
|
||||
if (!$input_type_part instanceof TNamedObject && !$input_type_part instanceof TIterable) {
|
||||
if ($atomic_comparison_result) {
|
||||
$atomic_comparison_result->type_coerced = true;
|
||||
$atomic_comparison_result->type_coerced_from_mixed = true;
|
||||
}
|
||||
|
||||
if (!$input_type_part instanceof TGenericObject) {
|
||||
if ($input_type_part instanceof TNamedObject) {
|
||||
$input_type_part = new TGenericObject(
|
||||
$input_type_part->value,
|
||||
array_fill(0, count($container_type_part->type_params), Type::getMixed())
|
||||
);
|
||||
} else {
|
||||
if ($atomic_comparison_result) {
|
||||
$atomic_comparison_result->type_coerced = true;
|
||||
$atomic_comparison_result->type_coerced_from_mixed = true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
$container_type_params_covariant = [];
|
||||
|
@ -33,6 +33,7 @@ use Psalm\Type\Atomic\TTemplateParamClass;
|
||||
use Psalm\Type\Union;
|
||||
use Throwable;
|
||||
|
||||
use function array_fill;
|
||||
use function array_keys;
|
||||
use function array_merge;
|
||||
use function array_search;
|
||||
@ -42,6 +43,7 @@ use function count;
|
||||
use function in_array;
|
||||
use function reset;
|
||||
use function strpos;
|
||||
use function strtolower;
|
||||
use function substr;
|
||||
use function usort;
|
||||
|
||||
@ -1124,7 +1126,7 @@ class TemplateStandinTypeReplacer
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TGenericObject|TIterable $input_type_part
|
||||
* @param TGenericObject|TNamedObject|TIterable $input_type_part
|
||||
* @param TGenericObject|TIterable $container_type_part
|
||||
* @return list<Union>
|
||||
*/
|
||||
@ -1134,7 +1136,21 @@ class TemplateStandinTypeReplacer
|
||||
Atomic $container_type_part,
|
||||
?array &$container_type_params_covariant = null
|
||||
): array {
|
||||
$input_type_params = $input_type_part->type_params;
|
||||
if ($input_type_part instanceof TGenericObject || $input_type_part instanceof TIterable) {
|
||||
$input_type_params = $input_type_part->type_params;
|
||||
} else {
|
||||
$class_storage = $codebase->classlike_storage_provider->get($input_type_part->value);
|
||||
|
||||
$container_class = $container_type_part->value;
|
||||
|
||||
if (strtolower($input_type_part->value) === strtolower($container_type_part->value)) {
|
||||
$input_type_params = $class_storage->getClassTemplateTypes();
|
||||
} elseif (!empty($class_storage->template_extended_params[$container_class])) {
|
||||
$input_type_params = array_values($class_storage->template_extended_params[$container_class]);
|
||||
} else {
|
||||
$input_type_params = array_fill(0, count($class_storage->template_types ?? []), Type::getMixed());
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$input_class_storage = $codebase->classlike_storage_provider->get($input_type_part->value);
|
||||
|
@ -11,6 +11,8 @@ use Psalm\Type\Atomic\TNamedObject;
|
||||
use Psalm\Type\Atomic\TTemplateParam;
|
||||
use Psalm\Type\Union;
|
||||
|
||||
use function array_values;
|
||||
|
||||
class ClassLikeStorage implements HasAttributesInterface
|
||||
{
|
||||
use CustomMetadataTrait;
|
||||
@ -470,4 +472,20 @@ class ClassLikeStorage implements HasAttributesInterface
|
||||
{
|
||||
return $this->attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the template constraint types for the class.
|
||||
*
|
||||
* @return list<Union>
|
||||
*/
|
||||
public function getClassTemplateTypes(): array
|
||||
{
|
||||
$type_params = [];
|
||||
|
||||
foreach ($this->template_types ?? [] as $type_map) {
|
||||
$type_params[] = clone array_values($type_map)[0];
|
||||
}
|
||||
|
||||
return $type_params;
|
||||
}
|
||||
}
|
||||
|
@ -5342,7 +5342,7 @@ class ClassTemplateExtendsTest extends TestCase
|
||||
}
|
||||
|
||||
new Foo(new DoStuffX());',
|
||||
'error_message' => 'MixedArgumentTypeCoercion'
|
||||
'error_message' => 'TooManyTemplateParams'
|
||||
],
|
||||
'concreteDefinesSignatureTypesDifferent' => [
|
||||
'<?php
|
||||
|
@ -700,6 +700,33 @@ class FunctionClassStringTemplateTest extends TestCase
|
||||
|
||||
interface Bar {}'
|
||||
],
|
||||
'classStringSatisfiesTemplateWithConstraint' => [
|
||||
'code' => '<?php
|
||||
/** @template T of string */
|
||||
class Foo {}
|
||||
|
||||
/** @param class-string<Foo> $_fooClass */
|
||||
function bar(string $_fooClass): void {}
|
||||
|
||||
bar(Foo::class);
|
||||
',
|
||||
],
|
||||
'classStringWithGenericChildSatisfiesGenericParentWithDifferentConstraint' => [
|
||||
'code' => '<?php
|
||||
/** @template T of string */
|
||||
class Foo {}
|
||||
/**
|
||||
* @template T of non-empty-string
|
||||
* @extends Foo<T>
|
||||
*/
|
||||
class Bar extends Foo {}
|
||||
|
||||
/** @param class-string<Foo> $_fooClass */
|
||||
function bar(string $_fooClass): void {}
|
||||
|
||||
bar(Bar::class);
|
||||
',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user