Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix generic object comparison to use template constraint as default. #8069

Merged
merged 2 commits into from Jun 8, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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());
}
Comment on lines +1142 to +1152
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will conflict with #8044 when both are merged to master, this one is correct.

}

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' => [
'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);
',
],
];
}

Expand Down