Skip to content

Commit

Permalink
Merge pull request #8069 from AndrolGenhald/bugfix/8068
Browse files Browse the repository at this point in the history
Fix generic object comparison to use template constraint as default.
  • Loading branch information
orklah committed Jun 8, 2022
2 parents 53c3c9b + 271dce8 commit b113d77
Show file tree
Hide file tree
Showing 6 changed files with 76 additions and 63 deletions.
Expand Up @@ -63,7 +63,7 @@

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;
Expand Down Expand Up @@ -570,15 +570,9 @@ private static function propertyFetchCanBeAnalyzed(
$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(
Expand Down Expand Up @@ -1100,15 +1094,9 @@ private static function handleNonExistentProperty(
) {
$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(
Expand Down Expand Up @@ -1200,15 +1188,9 @@ private static function getClassPropertyType(
$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(
Expand Down
40 changes: 5 additions & 35 deletions src/Psalm/Internal/Type/Comparator/GenericTypeComparator.php
Expand Up @@ -4,16 +4,11 @@

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
*/
Expand Down Expand Up @@ -44,38 +39,13 @@ public static function isContainedBy(
$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 = [];
Expand Down
20 changes: 18 additions & 2 deletions src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php
Expand Up @@ -33,6 +33,7 @@
use Psalm\Type\Union;
use Throwable;

use function array_fill;
use function array_keys;
use function array_merge;
use function array_search;
Expand All @@ -42,6 +43,7 @@
use function in_array;
use function reset;
use function strpos;
use function strtolower;
use function substr;
use function usort;

Expand Down Expand Up @@ -1124,7 +1126,7 @@ function (TemplateBound $bound_a, TemplateBound $bound_b) {
}

/**
* @param TGenericObject|TIterable $input_type_part
* @param TGenericObject|TNamedObject|TIterable $input_type_part
* @param TGenericObject|TIterable $container_type_part
* @return list<Union>
*/
Expand All @@ -1134,7 +1136,21 @@ public static function getMappedGenericTypeParams(
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);
Expand Down
18 changes: 18 additions & 0 deletions src/Psalm/Storage/ClassLikeStorage.php
Expand Up @@ -11,6 +11,8 @@
use Psalm\Type\Atomic\TTemplateParam;
use Psalm\Type\Union;

use function array_values;

class ClassLikeStorage implements HasAttributesInterface
{
use CustomMetadataTrait;
Expand Down Expand Up @@ -470,4 +472,20 @@ public function getAttributeStorages(): array
{
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;
}
}
2 changes: 1 addition & 1 deletion tests/Template/ClassTemplateExtendsTest.php
Expand Up @@ -5342,7 +5342,7 @@ public function __construct(DoStuff $stuff) {}
}
new Foo(new DoStuffX());',
'error_message' => 'MixedArgumentTypeCoercion'
'error_message' => 'TooManyTemplateParams'
],
'concreteDefinesSignatureTypesDifferent' => [
'<?php
Expand Down
27 changes: 27 additions & 0 deletions tests/Template/FunctionClassStringTemplateTest.php
Expand Up @@ -700,6 +700,33 @@ function (Bar $bar) use ($class): bool {
interface Bar {}'
],
'classStringSatisfiesTemplateWithConstraint' => [
'<?php
/** @template T of string */
class Foo {}
/** @param class-string<Foo> $_fooClass */
function bar(string $_fooClass): void {}
bar(Foo::class);
',
],
'classStringWithGenericChildSatisfiesGenericParentWithDifferentConstraint' => [
'<?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);
',
],
];
}

Expand Down

0 comments on commit b113d77

Please sign in to comment.