1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-26 20:34:47 +01:00

Add very basic implementation for class-string-map

Fixes #1969
This commit is contained in:
Matthew Brown 2019-12-27 12:49:28 -05:00
parent b78f273ccf
commit 068afa09d3
14 changed files with 818 additions and 37 deletions

View File

@ -295,6 +295,12 @@ class ArrayAssignmentAnalyzer
}
if (!$child_stmts) {
// we need this slight hack as the type we're putting it has to be
// different from the type we're getting out
if ($array_type->isSingle() && $array_type->hasClassStringMap()) {
$assignment_type = $child_stmt_type;
}
$child_stmt_type = $assignment_type;
$statements_analyzer->node_data->setType($child_stmt, $assignment_type);
}
@ -535,11 +541,61 @@ class ArrayAssignmentAnalyzer
&& $child_stmt
&& $parent_var_id
&& ($parent_type = $context->vars_in_scope[$parent_var_id] ?? null)
&& $parent_type->hasList()
) {
$array_atomic_type = new TNonEmptyList(
$current_type
);
if ($parent_type->hasList()) {
$array_atomic_type = new TNonEmptyList(
$current_type
);
} elseif ($parent_type->hasClassStringMap()
&& $current_dim_type
&& $current_dim_type->isTemplatedClassString()
) {
/**
* @var Type\Atomic\TClassStringMap
* @psalm-suppress PossiblyUndefinedStringArrayOffset
*/
$class_string_map = $parent_type->getTypes()['array'];
/**
* @var Type\Atomic\TTemplateParamClass
*/
$offset_type_part = \array_values($current_dim_type->getTypes())[0];
$template_result = new \Psalm\Internal\Type\TemplateResult(
[],
[
$offset_type_part->param_name => [
($offset_type_part->defining_class ?? '') => [
new Type\Union([
new Type\Atomic\TTemplateParam(
$class_string_map->param_name,
$offset_type_part->as_type
? new Type\Union([$offset_type_part->as_type])
: Type::getObject(),
'class-string-map'
)
])
]
]
]
);
$current_type->replaceTemplateTypesWithArgTypes(
$template_result->generic_params,
$codebase
);
$array_atomic_type = new Type\Atomic\TClassStringMap(
$class_string_map->param_name,
$class_string_map->as_type,
$current_type
);
} else {
$array_atomic_type = new TNonEmptyArray([
$array_atomic_key_type,
$current_type,
]);
}
} else {
$array_atomic_type = new TNonEmptyArray([
$array_atomic_key_type,
@ -558,7 +614,12 @@ class ArrayAssignmentAnalyzer
$atomic_root_types = $root_type->getTypes();
if (isset($atomic_root_types['array'])) {
if ($atomic_root_types['array'] instanceof TNonEmptyArray
if ($array_atomic_type instanceof Type\Atomic\TClassStringMap) {
$array_atomic_type = new TNonEmptyArray([
$array_atomic_type->getStandinKeyParam(),
$array_atomic_type->value_param
]);
} elseif ($atomic_root_types['array'] instanceof TNonEmptyArray
|| $atomic_root_types['array'] instanceof TNonEmptyList
) {
$array_atomic_type->count = $atomic_root_types['array']->count;

View File

@ -1289,8 +1289,9 @@ class PropertyAssignmentAnalyzer
if (TypeAnalyzer::canBeContainedBy($codebase, $assignment_value_type, $class_property_type)) {
if (IssueBuffer::accepts(
new PossiblyInvalidPropertyAssignmentValue(
$var_id . ' with declared type \'' . $class_property_type . '\' cannot be assigned type \'' .
$assignment_value_type . '\'',
$var_id . ' with declared type \''
. $class_property_type->getId() . '\' cannot be assigned type \''
. $assignment_value_type->getId() . '\'',
new CodeLocation(
$statements_analyzer->getSource(),
$assignment_value ?: $stmt
@ -1304,8 +1305,9 @@ class PropertyAssignmentAnalyzer
} else {
if (IssueBuffer::accepts(
new InvalidPropertyAssignmentValue(
$var_id . ' with declared type \'' . $class_property_type . '\' cannot be assigned type \'' .
$assignment_value_type . '\'',
$var_id . ' with declared type \'' . $class_property_type->getId()
. '\' cannot be assigned type \''
. $assignment_value_type->getId() . '\'',
new CodeLocation(
$statements_analyzer->getSource(),
$assignment_value ?: $stmt

View File

@ -32,6 +32,7 @@ use Psalm\Type;
use Psalm\Type\Atomic\ObjectLike;
use Psalm\Type\Atomic\TArray;
use Psalm\Type\Atomic\TArrayKey;
use Psalm\Type\Atomic\TClassStringMap;
use Psalm\Type\Atomic\TEmpty;
use Psalm\Type\Atomic\TLiteralInt;
use Psalm\Type\Atomic\TLiteralString;
@ -56,6 +57,7 @@ use function in_array;
use function is_int;
use function preg_match;
use Psalm\Internal\Taint\Source;
use Psalm\Internal\Type\TemplateResult;
/**
* @internal
@ -481,7 +483,11 @@ class ArrayFetchAnalyzer
continue;
}
if ($type instanceof TArray || $type instanceof ObjectLike || $type instanceof TList) {
if ($type instanceof TArray
|| $type instanceof ObjectLike
|| $type instanceof TList
|| $type instanceof TClassStringMap
) {
$has_array_access = true;
if ($in_assignment
@ -719,6 +725,102 @@ class ArrayFetchAnalyzer
$type->type_param
);
}
} elseif ($type instanceof TClassStringMap) {
$offset_type_parts = array_values($offset_type->getTypes());
foreach ($offset_type_parts as $offset_type_part) {
if ($offset_type_part instanceof Type\Atomic\TClassString) {
if ($offset_type_part instanceof Type\Atomic\TTemplateParamClass) {
$template_result_get = new TemplateResult(
[],
[
$type->param_name => [
'class-string-map' => [
new Type\Union([
new TTemplateParam(
$offset_type_part->param_name,
$offset_type_part->as_type
? new Type\Union([$offset_type_part->as_type])
: Type::getObject(),
$offset_type_part->defining_class
)
])
]
]
]
);
$template_result_set = new TemplateResult(
[],
[
$offset_type_part->param_name => [
($offset_type_part->defining_class ?: '') => [
new Type\Union([
new TTemplateParam(
$type->param_name,
$type->as_type
? new Type\Union([$type->as_type])
: Type::getObject(),
'class-string-map'
)
])
]
]
]
);
} else {
$template_result_get = new TemplateResult(
[],
[
$type->param_name => [
'class-string-map' => [
new Type\Union([
$offset_type_part->as_type
?: new Type\Atomic\TObject()
])
]
]
]
);
$template_result_set = new TemplateResult(
[],
[]
);
}
$expected_value_param_get = clone $type->value_param;
$expected_value_param_get->replaceTemplateTypesWithArgTypes(
$template_result_get->generic_params,
$codebase
);
if ($replacement_type) {
$expected_value_param_set = clone $type->value_param;
$replacement_type->replaceTemplateTypesWithArgTypes(
$template_result_set->generic_params,
$codebase
);
$type->value_param = Type::combineUnionTypes(
$replacement_type,
$expected_value_param_set,
$codebase
);
}
if (!$array_access_type) {
$array_access_type = $expected_value_param_get;
} else {
$array_access_type = Type::combineUnionTypes(
$array_access_type,
$expected_value_param_get,
$codebase
);
}
}
}
} else {
$generic_key_type = $type->getGenericKeyType();

View File

@ -12,6 +12,7 @@ use Psalm\Type\Atomic\TArray;
use Psalm\Type\Atomic\TArrayKey;
use Psalm\Type\Atomic\TBool;
use Psalm\Type\Atomic\TClassString;
use Psalm\Type\Atomic\TClassStringMap;
use Psalm\Type\Atomic\TCallable;
use Psalm\Type\Atomic\TCallableString;
use Psalm\Type\Atomic\TEmptyMixed;
@ -2079,6 +2080,12 @@ class TypeAnalyzer
}
}
if ($container_type_part instanceof TList
&& $input_type_part instanceof TClassStringMap
) {
return false;
}
if ($container_type_part instanceof TList
&& $input_type_part instanceof TArray
&& $input_type_part->type_params[1]->isEmpty()
@ -2107,10 +2114,12 @@ class TypeAnalyzer
if (($input_type_part instanceof TArray
|| $input_type_part instanceof ObjectLike
|| $input_type_part instanceof TList)
|| $input_type_part instanceof TList
|| $input_type_part instanceof TClassStringMap)
&& ($container_type_part instanceof TArray
|| $container_type_part instanceof ObjectLike
|| $container_type_part instanceof TList)
|| $container_type_part instanceof TList
|| $container_type_part instanceof TClassStringMap)
) {
if ($container_type_part instanceof ObjectLike) {
$generic_container_type_part = $container_type_part->getGenericArrayType();
@ -2128,8 +2137,7 @@ class TypeAnalyzer
false
);
if (!$input_type_part instanceof ObjectLike
&& !$input_type_part instanceof TList
if ($input_type_part instanceof TArray
&& !$input_type_part->type_params[0]->hasMixed()
&& !($input_type_part->type_params[1]->isEmpty()
&& $container_params_can_be_undefined)
@ -2145,6 +2153,20 @@ class TypeAnalyzer
$input_type_part = $input_type_part->getGenericArrayType();
}
if ($input_type_part instanceof TClassStringMap) {
$input_type_part = new TArray([
$input_type_part->getStandinKeyParam(),
clone $input_type_part->value_param
]);
}
if ($container_type_part instanceof TClassStringMap) {
$container_type_part = new TArray([
$container_type_part->getStandinKeyParam(),
clone $container_type_part->value_param
]);
}
if ($container_type_part instanceof TList) {
$all_types_contain = false;
$atomic_comparison_result->type_coerced = true;

View File

@ -472,6 +472,29 @@ class ParseTree
break;
case 'as':
$current_parent = $current_leaf->parent;
if (!$current_leaf instanceof ParseTree\Value
|| !$current_parent instanceof ParseTree\GenericTree
|| !$next_token
) {
throw new TypeParseTreeException('Unexpected token ' . $type_token[0]);
}
array_pop($current_parent->children);
$current_leaf = new ParseTree\TemplateAsTree(
$current_leaf->value,
$next_token[0],
$current_parent
);
$current_parent->children[] = $current_leaf;
++$i;
break;
default:
$new_parent = !$current_leaf instanceof ParseTree\Root ? $current_leaf : null;

View File

@ -0,0 +1,25 @@
<?php
namespace Psalm\Internal\Type\ParseTree;
/**
* @internal
*/
class TemplateAsTree extends \Psalm\Internal\Type\ParseTree
{
/**
* @var string
*/
public $param_name;
/**
* @var string
*/
public $as;
public function __construct(string $param_name, string $as, ?\Psalm\Internal\Type\ParseTree $parent = null)
{
$this->param_name = $param_name;
$this->as = $as;
$this->parent = $parent;
}
}

View File

@ -122,6 +122,15 @@ class TypeCombination
/** @var ?bool */
private $all_arrays_lists;
/** @var ?bool */
private $all_arrays_class_string_maps;
/** @var array<string, bool> */
private $class_string_map_names = [];
/** @var array<string, ?TNamedObject> */
private $class_string_map_as_types = [];
/**
* Combines types together
* - so `int + string = int|string`
@ -437,7 +446,16 @@ class TypeCombination
}
}
} else {
if ($combination->all_arrays_lists) {
if ($combination->all_arrays_class_string_maps
&& count($combination->class_string_map_as_types) === 1
&& count($combination->class_string_map_names) === 1
) {
$array_type = new Type\Atomic\TClassStringMap(
array_keys($combination->class_string_map_names)[0],
array_values($combination->class_string_map_as_types)[0],
$generic_type_params[1]
);
} elseif ($combination->all_arrays_lists) {
$array_type = new TList($generic_type_params[1]);
} else {
$array_type = new TArray($generic_type_params);
@ -725,6 +743,7 @@ class TypeCombination
if (!$type->type_params[1]->isEmpty()) {
$combination->all_arrays_lists = false;
$combination->all_arrays_class_string_maps = false;
}
} elseif ($type instanceof TList) {
foreach ([Type::getInt(), $type->type_param] as $i => $type_param) {
@ -757,6 +776,29 @@ class TypeCombination
if ($combination->all_arrays_lists !== false) {
$combination->all_arrays_lists = true;
}
$combination->all_arrays_class_string_maps = false;
} elseif ($type instanceof Atomic\TClassStringMap) {
foreach ([$type->getStandinKeyParam(), $type->value_param] as $i => $type_param) {
if (isset($combination->array_type_params[$i])) {
$combination->array_type_params[$i] = Type::combineUnionTypes(
$combination->array_type_params[$i],
$type_param,
$codebase,
$overwrite_empty_array
);
} else {
$combination->array_type_params[$i] = $type_param;
}
}
$combination->array_always_filled = false;
if ($combination->all_arrays_class_string_maps !== false) {
$combination->all_arrays_class_string_maps = true;
$combination->class_string_map_names[$type->param_name] = true;
$combination->class_string_map_as_types[(string) $type->as_type] = $type->as_type;
}
} elseif (($type instanceof TGenericObject && ($type->value === 'Traversable' || $type->value === 'Generator'))
|| ($type instanceof TIterable && $type->has_docblock_params)
|| ($type instanceof TArray && $type_key === 'iterable')
@ -854,6 +896,8 @@ class TypeCombination
} elseif ($combination->all_arrays_lists !== false) {
$combination->all_arrays_lists = true;
}
$combination->all_arrays_class_string_maps = false;
} else {
if ($type instanceof TObject) {
$combination->named_object_types = null;

View File

@ -30,6 +30,7 @@ use Psalm\Type\Atomic\TArrayKey;
use Psalm\Type\Atomic\TBool;
use Psalm\Type\Atomic\TCallable;
use Psalm\Type\Atomic\TClassString;
use Psalm\Type\Atomic\TClassStringMap;
use Psalm\Type\Atomic\TEmpty;
use Psalm\Type\Atomic\TFalse;
use Psalm\Type\Atomic\TFloat;
@ -110,6 +111,7 @@ abstract class Type
'non-empty-countable' => true,
'list' => true,
'non-empty-list' => true,
'class-string-map' => true,
];
/**
@ -230,7 +232,7 @@ abstract class Type
* @param array{int,int}|null $php_version
* @param array<string, array<string, array{Type\Union}>> $template_type_map
*
* @return Atomic|TArray|TGenericObject|ObjectLike|Union
* @return Atomic|Union
*/
public static function getTypeFromTree(
ParseTree $parse_tree,
@ -240,17 +242,23 @@ abstract class Type
if ($parse_tree instanceof ParseTree\GenericTree) {
$generic_type = $parse_tree->value;
$generic_params = array_map(
/**
* @return Union
*/
function (ParseTree $child_tree) use ($template_type_map) {
$tree_type = self::getTypeFromTree($child_tree, null, $template_type_map);
$generic_params = [];
return $tree_type instanceof Union ? $tree_type : new Union([$tree_type]);
},
$parse_tree->children
);
foreach ($parse_tree->children as $i => $child_tree) {
$tree_type = self::getTypeFromTree($child_tree, null, $template_type_map);
if ($generic_type === 'class-string-map'
&& $i === 0
) {
if ($tree_type instanceof TTemplateParam) {
$template_type_map[$tree_type->param_name] = ['class-string-map' => [$tree_type->as]];
} elseif ($tree_type instanceof TNamedObject) {
$template_type_map[$tree_type->value] = ['class-string-map' => [self::getObject()]];
}
}
$generic_params[] = $tree_type instanceof Union ? $tree_type : new Union([$tree_type]);
}
$generic_type_value = self::fixScalarTerms($generic_type);
@ -322,6 +330,44 @@ abstract class Type
return new TClassString($class_name, $param_union_types[0]);
}
if ($generic_type_value === 'class-string-map') {
if (count($generic_params) !== 2) {
throw new TypeParseTreeException(
'There should only be two params for class-string-map, '
. count($generic_params) . ' provided'
);
}
$template_marker_parts = array_values($generic_params[0]->getTypes());
$template_marker = $template_marker_parts[0];
$template_as_type = null;
if ($template_marker instanceof TNamedObject) {
$template_param_name = $template_marker->value;
} elseif ($template_marker instanceof Atomic\TTemplateParam) {
$template_param_name = $template_marker->param_name;
$template_as_type = array_values($template_marker->as->getTypes())[0];
if (!$template_as_type instanceof TNamedObject) {
throw new TypeParseTreeException(
'Unrecognised as type'
);
}
} else {
throw new TypeParseTreeException(
'Unrecognised class-string-map templated param'
);
}
return new TClassStringMap(
$template_param_name,
$template_as_type,
$generic_params[1]
);
}
if ($generic_type_value === 'key-of') {
$param_name = (string) $generic_params[0];
@ -599,16 +645,10 @@ abstract class Type
return $non_nullable_type;
}
if ($non_nullable_type instanceof Atomic) {
return TypeCombination::combineTypes([
new TNull,
$non_nullable_type,
]);
}
throw new \UnexpectedValueException(
'Was expecting an atomic or union type, got ' . get_class($non_nullable_type)
);
return TypeCombination::combineTypes([
new TNull,
$non_nullable_type,
]);
}
if ($parse_tree instanceof ParseTree\MethodTree
@ -661,6 +701,14 @@ abstract class Type
);
}
if ($parse_tree instanceof ParseTree\TemplateAsTree) {
return new Atomic\TTemplateParam(
$parse_tree->param_name,
new Union([new TNamedObject($parse_tree->as)]),
'class-string-map'
);
}
if (!$parse_tree instanceof ParseTree\Value) {
throw new \InvalidArgumentException('Unrecognised parse tree type ' . get_class($parse_tree));
}
@ -814,6 +862,14 @@ abstract class Type
) {
$type_tokens[++$rtc] = [' ', $i - 1];
$type_tokens[++$rtc] = ['', $i];
} elseif ($was_space
&& $char === 'a'
&& ($chars[$i + 1] ?? null) === 's'
&& ($chars[$i + 2] ?? null) === ' '
) {
$type_tokens[++$rtc] = ['as', $i - 1];
$type_tokens[++$rtc] = ['', ++$i];
continue;
} elseif ($was_char) {
$type_tokens[++$rtc] = ['', $i];
}
@ -980,7 +1036,7 @@ abstract class Type
if (in_array(
$string_type_token[0],
[
'<', '>', '|', '?', ',', '{', '}', ':', '::', '[', ']', '(', ')', '&', '=', '...',
'<', '>', '|', '?', ',', '{', '}', ':', '::', '[', ']', '(', ')', '&', '=', '...', 'as',
],
true
)) {
@ -1035,6 +1091,14 @@ abstract class Type
continue;
}
if ($i > 1
&& ($type_tokens[$i - 2][0] === 'class-string-map')
&& ($type_tokens[$i - 1][0] === '<')
) {
$template_type_map[$string_type_token[0]] = true;
continue;
}
if (isset($type_tokens[$i + 1])) {
$next_char = $type_tokens[$i + 1][0];
if ($next_char === ':') {

View File

@ -368,6 +368,7 @@ abstract class Atomic
return $this instanceof TArray
|| $this instanceof ObjectLike
|| $this instanceof TList
|| $this instanceof Atomic\TClassStringMap
|| $this->hasArrayAccessInterface($codebase)
|| ($this instanceof TNamedObject && $this->value === 'SimpleXMLElement');
}

View File

@ -0,0 +1,287 @@
<?php
namespace Psalm\Type\Atomic;
use function get_class;
use Psalm\Codebase;
use Psalm\CodeLocation;
use Psalm\StatementsSource;
use Psalm\Internal\Type\TemplateResult;
use Psalm\Internal\Type\UnionTemplateHandler;
use Psalm\Type;
use Psalm\Type\Atomic;
use Psalm\Type\Union;
/**
* Represents an array where the type of each value
* is a function of its string key value
*/
class TClassStringMap extends \Psalm\Type\Atomic
{
/**
* @var string
*/
public $param_name;
/**
* @var ?TNamedObject
*/
public $as_type;
/**
* @var Union
*/
public $value_param;
const KEY = 'class-string-map';
/**
* Constructs a new instance of a list
*/
public function __construct(string $param_name, ?TNamedObject $as_type, Union $value_param)
{
$this->value_param = $value_param;
$this->param_name = $param_name;
$this->as_type = $as_type;
}
public function __toString()
{
/** @psalm-suppress MixedOperand */
return static::KEY
. '<'
. $this->param_name
. ' as '
. ($this->as_type ? (string) $this->as_type : 'object')
. ', '
. ((string) $this->value_param)
. '>';
}
public function getId()
{
/** @psalm-suppress MixedOperand */
return static::KEY
. '<'
. $this->param_name
. ' as '
. ($this->as_type ? (string) $this->as_type : 'object')
. ', '
. $this->value_param->getId()
. '>';
}
public function __clone()
{
$this->value_param = clone $this->value_param;
}
/**
* @param array<string, string> $aliased_classes
*
* @return string
*/
public function toNamespacedString(
?string $namespace,
array $aliased_classes,
?string $this_class,
bool $use_phpdoc_format
) {
if ($use_phpdoc_format) {
return (new TArray([Type::getString(), $this->value_param]))
->toNamespacedString(
$namespace,
$aliased_classes,
$this_class,
$use_phpdoc_format
);
}
/** @psalm-suppress MixedOperand */
return static::KEY
. '<'
. $this->param_name
. ($this->as_type ? ' as ' . $this->as_type : '')
. ', '
. $this->value_param->toNamespacedString(
$namespace,
$aliased_classes,
$this_class,
$use_phpdoc_format
)
. '>';
}
/**
* @param string|null $namespace
* @param array<string> $aliased_classes
* @param string|null $this_class
* @param int $php_major_version
* @param int $php_minor_version
*
* @return string
*/
public function toPhpString($namespace, array $aliased_classes, $this_class, $php_major_version, $php_minor_version)
{
return 'array';
}
public function canBeFullyExpressedInPhp()
{
return false;
}
/**
* @return string
*/
public function getKey()
{
return 'array';
}
public function setFromDocblock()
{
$this->from_docblock = true;
$this->value_param->from_docblock = true;
}
public function replaceTemplateTypesWithStandins(
TemplateResult $template_result,
Codebase $codebase = null,
Atomic $input_type = null,
?string $calling_class = null,
bool $replace = true,
bool $add_upper_bound = false,
int $depth = 0
) : Atomic {
$map = clone $this;
foreach ([Type::getString(), $map->value_param] as $offset => $type_param) {
$input_type_param = null;
if (($input_type instanceof Atomic\TGenericObject
|| $input_type instanceof Atomic\TIterable
|| $input_type instanceof Atomic\TArray)
&&
isset($input_type->type_params[$offset])
) {
$input_type_param = clone $input_type->type_params[$offset];
} elseif ($input_type instanceof Atomic\ObjectLike) {
if ($offset === 0) {
$input_type_param = $input_type->getGenericKeyType();
} else {
$input_type_param = $input_type->getGenericValueType();
}
} elseif ($input_type instanceof Atomic\TList) {
if ($offset === 0) {
continue;
}
$input_type_param = clone $input_type->type_param;
}
$value_param = UnionTemplateHandler::replaceTemplateTypesWithStandins(
$type_param,
$template_result,
$codebase,
$input_type_param,
$calling_class,
$replace,
$add_upper_bound,
$depth + 1
);
if ($offset === 1) {
$map->value_param = $value_param;
}
}
return $map;
}
/**
* @param array<string, array<string, array{Type\Union, 1?:int}>> $template_types
*
* @return void
*/
public function replaceTemplateTypesWithArgTypes(array $template_types, ?Codebase $codebase)
{
$this->value_param->replaceTemplateTypesWithArgTypes($template_types);
}
/**
* @return list<Type\Atomic\TTemplateParam>
*/
public function getTemplateTypes() : array
{
return $this->value_param->getTemplateTypes();
}
/**
* @return bool
*/
public function equals(Atomic $other_type)
{
if (get_class($other_type) !== static::class) {
return false;
}
if (!$this->value_param->equals($other_type->value_param)) {
return false;
}
return true;
}
/**
* @return string
*/
public function getAssertionString()
{
return $this->getKey();
}
/**
* @param StatementsSource $source
* @param CodeLocation $code_location
* @param array<string> $suppressed_issues
* @param array<string, bool> $phantom_classes
* @param bool $inferred
*
* @return void
*/
public function check(
StatementsSource $source,
CodeLocation $code_location,
array $suppressed_issues,
array $phantom_classes = [],
bool $inferred = true,
bool $prevent_template_covariance = false
) {
if ($this->checked) {
return;
}
$this->value_param->check(
$source,
$code_location,
$suppressed_issues,
$phantom_classes,
$inferred,
$prevent_template_covariance
);
$this->checked = true;
}
public function getStandinKeyParam() : Type\Union
{
return new Type\Union([
new TTemplateParamClass(
$this->param_name,
$this->as_type ? $this->as_type->value : 'object',
$this->as_type,
'class-string-map'
)
]);
}
}

View File

@ -537,6 +537,8 @@ class Reconciler
}
} elseif ($existing_key_type_part instanceof Type\Atomic\TNull) {
$new_base_type_candidate = Type::getNull();
} elseif ($existing_key_type_part instanceof Type\Atomic\TClassStringMap) {
return Type::getMixed();
} elseif (!$existing_key_type_part instanceof Type\Atomic\ObjectLike) {
return Type::getMixed();
} elseif ($array_key[0] === '$' || ($array_key[0] !== '\'' && !\is_numeric($array_key[0]))) {
@ -778,6 +780,7 @@ class Reconciler
|| ($base_atomic_type instanceof Type\Atomic\TArray
&& !$base_atomic_type->type_params[1]->isEmpty())
|| $base_atomic_type instanceof Type\Atomic\TList
|| $base_atomic_type instanceof Type\Atomic\TClassStringMap
) {
$new_base_type = clone $existing_types[$base_key];
@ -811,6 +814,8 @@ class Reconciler
$base_atomic_type->previous_key_type = $previous_key_type;
$base_atomic_type->previous_value_type = $previous_value_type;
} elseif ($base_atomic_type instanceof Type\Atomic\TClassStringMap) {
// do nothing
} else {
$base_atomic_type = clone $base_atomic_type;
$base_atomic_type->properties[$array_key_offset] = clone $result_type;

View File

@ -626,6 +626,27 @@ class Union
return isset($this->types['array']) && $this->types['array'] instanceof Atomic\TList;
}
/**
* @return bool
*/
public function hasClassStringMap()
{
return isset($this->types['array']) && $this->types['array'] instanceof Atomic\TClassStringMap;
}
public function isTemplatedClassString() : bool
{
return $this->isSingle()
&& count(
array_filter(
$this->types,
function ($type) {
return $type instanceof Atomic\TTemplateParamClass;
}
)
) === 1;
}
/**
* @return bool
*/

View File

@ -0,0 +1,116 @@
<?php
namespace Psalm\Tests\Template;
use const DIRECTORY_SEPARATOR;
use Psalm\Tests\TestCase;
use Psalm\Tests\Traits;
class ClassStringMapTest extends TestCase
{
use Traits\ValidCodeAnalysisTestTrait;
use Traits\InvalidCodeAnalysisTestTrait;
/**
* @return iterable<string,array{string,assertions?:array<string,string>,error_levels?:string[]}>
*/
public function providerValidCodeParse()
{
return [
'basicClassStringMap' => [
'<?php
namespace Bar;
class Foo {}
class A {
/** @var class-string-map<T as Foo, T> */
public static array $map = [];
/**
* @template T as Foo
* @param class-string<T> $class
* @return T
*/
public function get(string $class) : Foo {
if (isset(self::$map[$class])) {
return self::$map[$class];
}
self::$map[$class] = new $class();
return self::$map[$class];
}
}',
],
'basicClassStringMapDifferentTemplateName' => [
'<?php
namespace Bar;
class Foo {}
class A {
/** @var class-string-map<T as Foo, T> */
public static array $map = [];
/**
* @template U as Foo
* @param class-string<U> $class
* @return U
*/
public function get(string $class) : Foo {
if (isset(self::$map[$class])) {
return self::$map[$class];
}
self::$map[$class] = new $class();
return self::$map[$class];
}
}',
],
];
}
/**
* @return iterable<string,array{string,error_message:string,2?:string[],3?:bool,4?:string}>
*/
public function providerInvalidCodeParse()
{
return [
'assignInvalidClass' => [
'<?php
namespace Bar;
class Foo {}
class A {
/** @var class-string-map<T, T> */
public static array $map = [];
/**
* @template T
* @param class-string<T> $class
*/
public function get(string $class) : void {
self::$map[$class] = 5;
}
}',
'error_message' => 'InvalidPropertyAssignmentValue'
],
'assignInvalidClassDifferentTemplateName' => [
'<?php
namespace Bar;
class Foo {}
class A {
/** @var class-string-map<T, T> */
public static array $map = [];
/**
* @template U
* @param class-string<U> $class
*/
public function get(string $class) : void {
self::$map[$class] = 5;
}
}',
'error_message' => 'InvalidPropertyAssignmentValue'
],
];
}
}

View File

@ -801,6 +801,14 @@ class TypeParseTest extends TestCase
);
}
public function testClassStringMap() : void
{
$this->assertSame(
'class-string-map<T as Foo, T>',
(string)Type::parseString('class-string-map<T as Foo, T>')
);
}
/**
* @return void
*/