mirror of
https://github.com/danog/psalm.git
synced 2024-11-26 20:34:47 +01:00
parent
b78f273ccf
commit
068afa09d3
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
||||
|
25
src/Psalm/Internal/Type/ParseTree/TemplateAsTree.php
Normal file
25
src/Psalm/Internal/Type/ParseTree/TemplateAsTree.php
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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 === ':') {
|
||||
|
@ -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');
|
||||
}
|
||||
|
287
src/Psalm/Type/Atomic/TClassStringMap.php
Normal file
287
src/Psalm/Type/Atomic/TClassStringMap.php
Normal 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'
|
||||
)
|
||||
]);
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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
|
||||
*/
|
||||
|
116
tests/Template/ClassStringMapTest.php
Normal file
116
tests/Template/ClassStringMapTest.php
Normal 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'
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
@ -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
|
||||
*/
|
||||
|
Loading…
Reference in New Issue
Block a user