1
0
mirror of https://github.com/danog/psalm.git synced 2025-01-21 21:31:13 +01:00

Merge pull request #7396 from Patrick-Remy/feat/key-of-value-of-improvements

feat: make key-of/value-of usable with non-const arrays
This commit is contained in:
orklah 2022-01-31 21:39:01 +01:00 committed by GitHub
commit 2e01e9b7cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1491 additions and 426 deletions

View File

@ -42,7 +42,7 @@
- `Psalm\Type\Atomic\TIntRange`
- `Psalm\Type\Atomic\TIterable`
- `Psalm\Type\Atomic\TKeyedArray`
- `Psalm\Type\Atomic\TKeyOfClassConstant`
- `Psalm\Type\Atomic\TKeyOfArray`
- `Psalm\Type\Atomic\TList`
- `Psalm\Type\Atomic\TLiteralClassString`
- `Psalm\Type\Atomic\TLowercaseString`
@ -64,7 +64,7 @@
- `Psalm\Type\Atomic\TTraitString`
- `Psalm\Type\Atomic\TTrue`
- `Psalm\Type\Atomic\TTypeAlias`
- `Psalm\Type\Atomic\TValueOfClassConstant`
- `Psalm\Type\Atomic\TValueOfArray`
- `Psalm\Type\Atomic\TVoid`
- `Psalm\Type\Union`
@ -92,7 +92,7 @@
- `Psalm\Type\Atomic\TInt`
- `Psalm\Type\Atomic\TIterable`
- `Psalm\Type\Atomic\TKeyedArray`
- `Psalm\Type\Atomic\TKeyOfClassConstant`
- `Psalm\Type\Atomic\TKeyOfArray`
- `Psalm\Type\Atomic\TList`
- `Psalm\Type\Atomic\TLiteralClassString`
- `Psalm\Type\Atomic\TMixed`
@ -109,7 +109,7 @@
- `Psalm\Type\Atomic\TTemplateParam`
- `Psalm\Type\Atomic\TTraitString`
- `Psalm\Type\Atomic\TTypeAlias`
- `Psalm\Type\Atomic\TValueOfClassConstant`
- `Psalm\Type\Atomic\TValueOfArray`
- `Psalm\Type\Atomic\TVoid`
- `Psalm\Type\Union`
- While not a BC break per se, all classes / interfaces / traits / enums under
@ -155,6 +155,9 @@
- [BC] Atomic::getId() has now a first param $exact. Calling the method with false will return a less detailed version of the type in some cases (similarly to what __toString used to return)
- [BC] To remove a variable from the context, Context::remove(). Calling
`unset($context->vars_in_scope[$var_id])` can cause problems when using references.
- [BC] `TKeyOfClassConstant` has been renamed to `TKeyOfArray`.
- [BC] `TValueOfClassConstant` has been renamed to `TValueOfArray`.
- [BC] `TKeyOfTemplate` base class has been changed from `Scalar` to `Atomic`.
## Removed
- [BC] Property `Psalm\Codebase::$php_major_version` was removed, use

View File

@ -49,13 +49,15 @@ The classes are as follows:
`TIntMaskOf` - as above, but used with with a reference to constants in code`int-mask<MyClass::CLASS_CONSTANT_*>` will corresponds to `1|2|3|4|5|6|7` if there are three constant 1, 2 and 4
`TKeyOfClassConstant` - Represents an offset of a class constant array.
`TKeyOfArray` - Represents an offset of an array (e.g. `key-of<MyClass::CLASS_CONSTANT>`).
`TValueOfClassConstant` - Represents a value of a class constant array.
`TValueOfArray` - Represents a value of an array (e.g. `value-of<MyClass::CLASS_CONSTANT>`).
`TTemplateIndexedAccess` - To be documented
`TTemplateKeyOf` - Represents the type used when using TKeyOfClassConstant when the type of the class constant array is a template
`TTemplateKeyOf` - Represents the type used when using TKeyOfArray when the type of the array is a template
`TTemplateValueOf` - Represents the type used when using TValueOfArray when the type of the array is a template
`TTypeAlias` - To be documented
@ -277,5 +279,3 @@ Another way of creating these instances is to use the class `Psalm\Type` which i
```
You can find how Psalm would represent a given type as objects, by specifying the type as an input to this function, and calling `var_dump` on the result.

View File

@ -21,7 +21,6 @@ use Psalm\Internal\Provider\ReturnTypeProvider\ArrayReverseReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\ArraySliceReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\ArraySpliceReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\ArrayUniqueReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\ArrayValuesReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\ExplodeReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\FilterVarReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\FirstArgStringReturnTypeProvider;
@ -79,7 +78,6 @@ class FunctionReturnTypeProvider
$this->registerClass(ArraySpliceReturnTypeProvider::class);
$this->registerClass(ArrayReverseReturnTypeProvider::class);
$this->registerClass(ArrayUniqueReturnTypeProvider::class);
$this->registerClass(ArrayValuesReturnTypeProvider::class);
$this->registerClass(ArrayFillReturnTypeProvider::class);
$this->registerClass(FilterVarReturnTypeProvider::class);
$this->registerClass(IteratorToArrayReturnTypeProvider::class);

View File

@ -1,91 +0,0 @@
<?php
namespace Psalm\Internal\Provider\ReturnTypeProvider;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Plugin\EventHandler\Event\FunctionReturnTypeProviderEvent;
use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface;
use Psalm\Type;
use Psalm\Type\Atomic\TArray;
use Psalm\Type\Atomic\TKeyedArray;
use Psalm\Type\Atomic\TList;
use Psalm\Type\Atomic\TNonEmptyArray;
use Psalm\Type\Atomic\TNonEmptyList;
use Psalm\Type\Atomic\TTemplateParam;
use Psalm\Type\Union;
use UnexpectedValueException;
use function array_merge;
use function array_shift;
/**
* @internal
*/
class ArrayValuesReturnTypeProvider implements FunctionReturnTypeProviderInterface
{
/**
* @return array<lowercase-string>
*/
public static function getFunctionIds(): array
{
return ['array_values'];
}
public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $event): Union
{
$statements_source = $event->getStatementsSource();
$call_args = $event->getCallArgs();
if (!$statements_source instanceof StatementsAnalyzer) {
return Type::getMixed();
}
$first_arg = $call_args[0]->value ?? null;
if (!$first_arg) {
return Type::getArray();
}
$first_arg_type = $statements_source->node_data->getType($first_arg);
if (!$first_arg_type) {
return Type::getArray();
}
$atomic_types = $first_arg_type->getAtomicTypes();
$return_atomic_type = null;
while ($atomic_type = array_shift($atomic_types)) {
if ($atomic_type instanceof TTemplateParam) {
$atomic_types = array_merge($atomic_types, $atomic_type->as->getAtomicTypes());
continue;
}
if ($atomic_type instanceof TKeyedArray) {
$atomic_type = $atomic_type->getGenericArrayType();
}
if ($atomic_type instanceof TArray) {
if ($atomic_type instanceof TNonEmptyArray) {
$return_atomic_type = new TNonEmptyList(
clone $atomic_type->type_params[1]
);
} else {
$return_atomic_type = new TList(
clone $atomic_type->type_params[1]
);
}
} elseif ($atomic_type instanceof TList) {
$return_atomic_type = $atomic_type;
} else {
return Type::getArray();
}
}
if (!$return_atomic_type) {
throw new UnexpectedValueException('This should never happen');
}
return new Union([$return_atomic_type]);
}
}

View File

@ -7,7 +7,6 @@ use Psalm\Internal\Provider\ClassLikeStorageProvider;
use Psalm\Internal\Provider\FileStorageProvider;
use Psalm\Storage\FunctionLikeStorage;
use Psalm\Type\Atomic\TArray;
use Psalm\Type\Atomic\TAssertionFalsy;
use Psalm\Type\Atomic\TEnumCase;
use Psalm\Type\Atomic\TFalse;
use Psalm\Type\Atomic\TIterable;

View File

@ -19,6 +19,7 @@ use Psalm\Type\Atomic\TEmptyMixed;
use Psalm\Type\Atomic\TEnumCase;
use Psalm\Type\Atomic\TGenericObject;
use Psalm\Type\Atomic\TIterable;
use Psalm\Type\Atomic\TKeyOfArray;
use Psalm\Type\Atomic\TKeyedArray;
use Psalm\Type\Atomic\TList;
use Psalm\Type\Atomic\TLiteralString;
@ -32,7 +33,10 @@ use Psalm\Type\Atomic\TObject;
use Psalm\Type\Atomic\TObjectWithProperties;
use Psalm\Type\Atomic\TScalar;
use Psalm\Type\Atomic\TString;
use Psalm\Type\Atomic\TTemplateKeyOf;
use Psalm\Type\Atomic\TTemplateParam;
use Psalm\Type\Atomic\TTemplateValueOf;
use Psalm\Type\Atomic\TValueOfArray;
use function array_merge;
use function array_values;
@ -324,6 +328,74 @@ class AtomicTypeComparator
return true;
}
if ($container_type_part instanceof TTemplateKeyOf) {
if (!$input_type_part instanceof TTemplateKeyOf) {
return false;
}
return UnionTypeComparator::isContainedBy(
$codebase,
$input_type_part->as,
$container_type_part->as
);
}
if ($input_type_part instanceof TTemplateKeyOf) {
$array_key_type = TKeyOfArray::getArrayKeyType($input_type_part->as);
if ($array_key_type === null) {
return false;
}
foreach ($array_key_type->getAtomicTypes() as $array_key_atomic) {
if (!self::isContainedBy(
$codebase,
$array_key_atomic,
$container_type_part,
$allow_interface_equality,
$allow_float_int_equality,
$atomic_comparison_result
)) {
return false;
}
}
return true;
}
if ($container_type_part instanceof TTemplateValueOf) {
if (!$input_type_part instanceof TTemplateValueOf) {
return false;
}
return UnionTypeComparator::isContainedBy(
$codebase,
$input_type_part->as,
$container_type_part->as
);
}
if ($input_type_part instanceof TTemplateValueOf) {
$array_value_type = TValueOfArray::getArrayValueType($input_type_part->as);
if ($array_value_type === null) {
return false;
}
foreach ($array_value_type->getAtomicTypes() as $array_value_atomic) {
if (!self::isContainedBy(
$codebase,
$array_value_atomic,
$container_type_part,
$allow_interface_equality,
$allow_float_int_equality,
$atomic_comparison_result
)) {
return false;
}
}
return true;
}
if ($container_type_part instanceof TTemplateParam && $input_type_part instanceof TTemplateParam) {
return UnionTypeComparator::isContainedBy(
$codebase,

View File

@ -5,7 +5,6 @@ namespace Psalm\Internal\Type\Comparator;
use Psalm\Codebase;
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
use Psalm\Type\Atomic\Scalar;
use Psalm\Type\Atomic\TArray;
use Psalm\Type\Atomic\TArrayKey;
use Psalm\Type\Atomic\TBool;
use Psalm\Type\Atomic\TCallableString;
@ -35,7 +34,6 @@ use Psalm\Type\Atomic\TNumericString;
use Psalm\Type\Atomic\TScalar;
use Psalm\Type\Atomic\TSingleLetter;
use Psalm\Type\Atomic\TString;
use Psalm\Type\Atomic\TTemplateKeyOf;
use Psalm\Type\Atomic\TTemplateParam;
use Psalm\Type\Atomic\TTemplateParamClass;
use Psalm\Type\Atomic\TTraitString;
@ -261,34 +259,11 @@ class ScalarTypeComparator
if ($container_type_part instanceof TArrayKey
&& ($input_type_part instanceof TInt
|| $input_type_part instanceof TString
|| $input_type_part instanceof TTemplateKeyOf)
|| $input_type_part instanceof TString)
) {
return true;
}
if ($input_type_part instanceof TTemplateKeyOf) {
foreach ($input_type_part->as->getAtomicTypes() as $atomic_type) {
if ($atomic_type instanceof TArray) {
/** @var Scalar $array_key_atomic */
foreach ($atomic_type->type_params[0]->getAtomicTypes() as $array_key_atomic) {
if (!self::isContainedBy(
$codebase,
$array_key_atomic,
$container_type_part,
$allow_interface_equality,
$allow_float_int_equality,
$atomic_comparison_result
)) {
return false;
}
}
}
}
return true;
}
if ($input_type_part instanceof TArrayKey &&
($container_type_part instanceof TInt || $container_type_part instanceof TString)
) {

View File

@ -6,10 +6,12 @@ use InvalidArgumentException;
use Psalm\Codebase;
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
use Psalm\Type;
use Psalm\Type\Atomic;
use Psalm\Type\Atomic\TClassString;
use Psalm\Type\Atomic\TConditional;
use Psalm\Type\Atomic\TInt;
use Psalm\Type\Atomic\TIterable;
use Psalm\Type\Atomic\TKeyOfArray;
use Psalm\Type\Atomic\TKeyedArray;
use Psalm\Type\Atomic\TLiteralInt;
use Psalm\Type\Atomic\TLiteralString;
@ -18,8 +20,11 @@ use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Atomic\TObject;
use Psalm\Type\Atomic\TObjectWithProperties;
use Psalm\Type\Atomic\TTemplateIndexedAccess;
use Psalm\Type\Atomic\TTemplateKeyOf;
use Psalm\Type\Atomic\TTemplateParam;
use Psalm\Type\Atomic\TTemplateParamClass;
use Psalm\Type\Atomic\TTemplateValueOf;
use Psalm\Type\Atomic\TValueOfArray;
use Psalm\Type\Union;
use UnexpectedValueException;
@ -228,6 +233,19 @@ class TemplateInferredTypeReplacer
} else {
$new_types[] = new TMixed();
}
} elseif ($atomic_type instanceof TTemplateKeyOf
|| $atomic_type instanceof TTemplateValueOf
) {
$new_type = self::replaceTemplateKeyOfValueOf(
$codebase,
$atomic_type,
$inferred_lower_bounds
);
if ($new_type) {
$keys_to_unset[] = $key;
$new_types[] = $new_type;
}
} elseif ($atomic_type instanceof TConditional
&& $codebase
) {
@ -414,4 +432,37 @@ class TemplateInferredTypeReplacer
)->getAtomicTypes()
);
}
/**
* @param TTemplateKeyOf|TTemplateValueOf $atomic_type
* @param array<string, array<string, non-empty-list<TemplateBound>>> $inferred_lower_bounds
*/
private static function replaceTemplateKeyOfValueOf(
?Codebase $codebase,
Atomic $atomic_type,
array $inferred_lower_bounds
): ?Atomic {
if (!isset($inferred_lower_bounds[$atomic_type->param_name][$atomic_type->defining_class])) {
return null;
}
$template_type = clone TemplateStandinTypeReplacer::getMostSpecificTypeFromBounds(
$inferred_lower_bounds[$atomic_type->param_name][$atomic_type->defining_class],
$codebase
);
if ($atomic_type instanceof TTemplateKeyOf
&& TKeyOfArray::isViableTemplateType($template_type)
) {
return new TKeyOfArray(clone $template_type);
}
if ($atomic_type instanceof TTemplateValueOf
&& TValueOfArray::isViableTemplateType($template_type)
) {
return new TValueOfArray(clone $template_type);
}
return null;
}
}

View File

@ -29,6 +29,7 @@ use Psalm\Type\Atomic\TTemplateIndexedAccess;
use Psalm\Type\Atomic\TTemplateKeyOf;
use Psalm\Type\Atomic\TTemplateParam;
use Psalm\Type\Atomic\TTemplateParamClass;
use Psalm\Type\Atomic\TTemplateValueOf;
use Psalm\Type\Union;
use Throwable;
@ -270,48 +271,58 @@ class TemplateStandinTypeReplacer
return [$atomic_type];
}
if ($atomic_type instanceof TTemplateKeyOf) {
if ($replace) {
$atomic_types = [];
$include_first = true;
if (isset($template_result->template_types[$atomic_type->param_name][$atomic_type->defining_class])) {
$template_type
= $template_result->template_types[$atomic_type->param_name][$atomic_type->defining_class];
if ($template_type->isSingle()) {
$template_type = $template_type->getSingleAtomic();
if ($template_type instanceof TKeyedArray
|| $template_type instanceof TArray
|| $template_type instanceof TList
) {
if ($template_type instanceof TKeyedArray) {
$key_type = $template_type->getGenericKeyType();
} elseif ($template_type instanceof TList) {
$key_type = Type::getInt();
} else {
$key_type = clone $template_type->type_params[0];
}
$include_first = false;
foreach ($key_type->getAtomicTypes() as $key_atomic_type) {
$atomic_types[] = $key_atomic_type;
}
}
}
}
if ($include_first) {
$atomic_types[] = $atomic_type;
}
return $atomic_types;
if ($atomic_type instanceof TTemplateKeyOf
|| $atomic_type instanceof TTemplateValueOf) {
if (!$replace) {
return [$atomic_type];
}
return [$atomic_type];
$atomic_types = [];
$include_first = true;
if (isset($template_result->template_types[$atomic_type->param_name][$atomic_type->defining_class])) {
$template_type = $template_result->template_types[$atomic_type->param_name][$atomic_type->defining_class];
foreach ($template_type->getAtomicTypes() as $template_atomic) {
if (!$template_atomic instanceof TKeyedArray
&& !$template_atomic instanceof TArray
&& !$template_atomic instanceof TList
) {
return [$atomic_type];
}
if ($atomic_type instanceof TTemplateKeyOf) {
if ($template_atomic instanceof TKeyedArray) {
$template_atomic = $template_atomic->getGenericKeyType();
} elseif ($template_atomic instanceof TList) {
$template_atomic = Type::getInt();
} else {
$template_atomic = clone $template_atomic->type_params[0];
}
} else {
if ($template_atomic instanceof TKeyedArray) {
$template_atomic = $template_atomic->getGenericValueType();
} elseif ($template_atomic instanceof TList) {
$template_atomic = clone $template_atomic->type_param;
} else {
$template_atomic = clone $template_atomic->type_params[1];
}
}
$include_first = false;
foreach ($template_atomic->getAtomicTypes() as $key_atomic_type) {
$atomic_types[] = $key_atomic_type;
}
}
}
if ($include_first) {
$atomic_types[] = $atomic_type;
}
return $atomic_types;
}
$matching_atomic_types = [];

View File

@ -18,7 +18,7 @@ use Psalm\Type\Atomic\TInt;
use Psalm\Type\Atomic\TIntMask;
use Psalm\Type\Atomic\TIntMaskOf;
use Psalm\Type\Atomic\TIterable;
use Psalm\Type\Atomic\TKeyOfClassConstant;
use Psalm\Type\Atomic\TKeyOfArray;
use Psalm\Type\Atomic\TKeyedArray;
use Psalm\Type\Atomic\TList;
use Psalm\Type\Atomic\TLiteralClassString;
@ -29,7 +29,7 @@ use Psalm\Type\Atomic\TNull;
use Psalm\Type\Atomic\TObjectWithProperties;
use Psalm\Type\Atomic\TTemplateParam;
use Psalm\Type\Atomic\TTypeAlias;
use Psalm\Type\Atomic\TValueOfClassConstant;
use Psalm\Type\Atomic\TValueOfArray;
use Psalm\Type\Atomic\TVoid;
use Psalm\Type\Union;
use ReflectionProperty;
@ -38,6 +38,7 @@ use function array_filter;
use function array_keys;
use function array_map;
use function array_merge;
use function array_push;
use function array_values;
use function count;
use function get_class;
@ -350,52 +351,16 @@ class TypeExpander
return [$return_type];
}
if ($return_type instanceof TKeyOfClassConstant
|| $return_type instanceof TValueOfClassConstant
if ($return_type instanceof TKeyOfArray
|| $return_type instanceof TValueOfArray
) {
if ($return_type->fq_classlike_name === 'self' && $self_class) {
$return_type->fq_classlike_name = $self_class;
}
if ($evaluate_class_constants) {
if ($throw_on_unresolvable_constant
&& !$codebase->classOrInterfaceExists($return_type->fq_classlike_name)
) {
throw new UnresolvableConstantException($return_type->fq_classlike_name, $return_type->const_name);
}
try {
$class_constant_type = $codebase->classlikes->getClassConstantType(
$return_type->fq_classlike_name,
$return_type->const_name,
ReflectionProperty::IS_PRIVATE
);
} catch (CircularReferenceException $e) {
$class_constant_type = null;
}
if ($class_constant_type) {
foreach ($class_constant_type->getAtomicTypes() as $const_type_atomic) {
if ($const_type_atomic instanceof TKeyedArray
|| $const_type_atomic instanceof TArray
) {
if ($const_type_atomic instanceof TKeyedArray) {
$const_type_atomic = $const_type_atomic->getGenericArrayType();
}
if ($return_type instanceof TKeyOfClassConstant) {
return array_values($const_type_atomic->type_params[0]->getAtomicTypes());
}
return array_values($const_type_atomic->type_params[1]->getAtomicTypes());
}
}
} elseif ($throw_on_unresolvable_constant) {
throw new UnresolvableConstantException($return_type->fq_classlike_name, $return_type->const_name);
}
}
return [$return_type];
return self::expandKeyOfValueOfArray(
$codebase,
$return_type,
$self_class,
$evaluate_class_constants,
$throw_on_unresolvable_constant
);
}
if ($return_type instanceof TIntMask) {
@ -911,4 +876,80 @@ class TypeExpander
return [$return_type];
}
/**
* @param TKeyOfArray|TValueOfArray $return_type
* @return non-empty-list<Atomic>
*/
private static function expandKeyOfValueOfArray(
Codebase $codebase,
$return_type,
?string $self_class,
bool $evaluate_class_constants,
bool $throw_on_unresolvable_constant
): array {
// Expand class constants to their atomics
$type_atomics = [];
foreach ($return_type->type->getAtomicTypes() as $type_param) {
if (!$evaluate_class_constants || !$type_param instanceof TClassConstant) {
array_push($type_atomics, $type_param);
continue;
}
if ($type_param->fq_classlike_name === 'self' && $self_class) {
$type_param->fq_classlike_name = $self_class;
}
if ($throw_on_unresolvable_constant
&& !$codebase->classOrInterfaceExists($type_param->fq_classlike_name)
) {
throw new UnresolvableConstantException($type_param->fq_classlike_name, $type_param->const_name);
}
try {
$constant_type = $codebase->classlikes->getClassConstantType(
$type_param->fq_classlike_name,
$type_param->const_name,
ReflectionProperty::IS_PRIVATE
);
} catch (CircularReferenceException $e) {
return [$return_type];
}
if (!$constant_type
|| (
$return_type instanceof TKeyOfArray
&& !TKeyOfArray::isViableTemplateType($constant_type)
)
|| (
$return_type instanceof TValueOfArray
&& !TValueOfArray::isViableTemplateType($constant_type)
)
) {
if ($throw_on_unresolvable_constant) {
throw new UnresolvableConstantException($type_param->fq_classlike_name, $type_param->const_name);
} else {
return [$return_type];
}
}
$type_atomics = array_merge(
$type_atomics,
array_values($constant_type->getAtomicTypes())
);
}
if ($type_atomics === []) {
return [$return_type];
}
if ($return_type instanceof TKeyOfArray) {
$new_return_types = TKeyOfArray::getArrayKeyType(new Union($type_atomics));
} else {
$new_return_types = TValueOfArray::getArrayValueType(new Union($type_atomics));
}
if ($new_return_types === null) {
return [$return_type];
}
return array_values($new_return_types->getAtomicTypes());
}
}

View File

@ -41,7 +41,7 @@ use Psalm\Type\Atomic\TIntMask;
use Psalm\Type\Atomic\TIntMaskOf;
use Psalm\Type\Atomic\TIntRange;
use Psalm\Type\Atomic\TIterable;
use Psalm\Type\Atomic\TKeyOfClassConstant;
use Psalm\Type\Atomic\TKeyOfArray;
use Psalm\Type\Atomic\TKeyedArray;
use Psalm\Type\Atomic\TList;
use Psalm\Type\Atomic\TLiteralClassString;
@ -59,12 +59,14 @@ use Psalm\Type\Atomic\TTemplateIndexedAccess;
use Psalm\Type\Atomic\TTemplateKeyOf;
use Psalm\Type\Atomic\TTemplateParam;
use Psalm\Type\Atomic\TTemplateParamClass;
use Psalm\Type\Atomic\TTemplateValueOf;
use Psalm\Type\Atomic\TTypeAlias;
use Psalm\Type\Atomic\TValueOfClassConstant;
use Psalm\Type\Atomic\TValueOfArray;
use Psalm\Type\TypeNode;
use Psalm\Type\Union;
use function array_key_exists;
use function array_key_first;
use function array_keys;
use function array_map;
use function array_merge;
@ -686,53 +688,45 @@ class TypeParser
if ($generic_type_value === 'key-of') {
$param_name = $generic_params[0]->getId(false);
if (isset($template_type_map[$param_name])) {
$defining_class = array_keys($template_type_map[$param_name])[0];
if (isset($template_type_map[$param_name])
&& ($defining_class = array_key_first($template_type_map[$param_name])) !== null
) {
return new TTemplateKeyOf(
$param_name,
$defining_class,
$template_type_map[$param_name][$defining_class]
$generic_params[0]
);
}
$param_union_types = array_values($generic_params[0]->getAtomicTypes());
if (count($param_union_types) > 1) {
throw new TypeParseTreeException('Union types are not allowed in key-of type');
}
if (!$param_union_types[0] instanceof TClassConstant) {
if (!TKeyOfArray::isViableTemplateType($generic_params[0])) {
throw new TypeParseTreeException(
'Untemplated key-of param ' . $param_name . ' should be a class constant'
'Untemplated key-of param ' . $param_name . ' should be an array'
);
}
return new TKeyOfClassConstant(
$param_union_types[0]->fq_classlike_name,
$param_union_types[0]->const_name
);
return new TKeyOfArray($generic_params[0]);
}
if ($generic_type_value === 'value-of') {
$param_name = $generic_params[0]->getId(false);
$param_union_types = array_values($generic_params[0]->getAtomicTypes());
if (count($param_union_types) > 1) {
throw new TypeParseTreeException('Union types are not allowed in value-of type');
}
if (!$param_union_types[0] instanceof TClassConstant) {
throw new TypeParseTreeException(
'Untemplated value-of param ' . $param_name . ' should be a class constant'
if (isset($template_type_map[$param_name])
&& ($defining_class = array_key_first($template_type_map[$param_name])) !== null
) {
return new TTemplateValueOf(
$param_name,
$defining_class,
$generic_params[0]
);
}
return new TValueOfClassConstant(
$param_union_types[0]->fq_classlike_name,
$param_union_types[0]->const_name
);
if (!TValueOfArray::isViableTemplateType($generic_params[0])) {
throw new TypeParseTreeException(
'Untemplated value-of param ' . $param_name . ' should be an array'
);
}
return new TValueOfArray($generic_params[0]);
}
if ($generic_type_value === 'int-mask') {
@ -803,8 +797,8 @@ class TypeParser
$param_type = $param_union_types[0];
if (!$param_type instanceof TClassConstant
&& !$param_type instanceof TValueOfClassConstant
&& !$param_type instanceof TKeyOfClassConstant
&& !$param_type instanceof TValueOfArray
&& !$param_type instanceof TKeyOfArray
) {
throw new TypeParseTreeException(
'Invalid reference passed to int-mask-of'

View File

@ -11,11 +11,11 @@ use Psalm\Type\Atomic;
*/
class TIntMaskOf extends TInt
{
/** @var TClassConstant|TKeyOfClassConstant|TValueOfClassConstant */
/** @var TClassConstant|TKeyOfArray|TValueOfArray */
public $value;
/**
* @param TClassConstant|TKeyOfClassConstant|TValueOfClassConstant $value
* @param TClassConstant|TKeyOfArray|TValueOfArray $value
*/
public function __construct(Atomic $value)
{

View File

@ -0,0 +1,105 @@
<?php
namespace Psalm\Type\Atomic;
use Psalm\Type;
use Psalm\Type\Union;
use function array_merge;
use function array_values;
/**
* Represents an offset of an array.
*/
class TKeyOfArray extends TArrayKey
{
/** @var Union */
public $type;
public function __construct(Union $type)
{
$this->type = $type;
}
public function getKey(bool $include_extra = true): string
{
return 'key-of<' . $this->type . '>';
}
/**
* @param array<lowercase-string, string> $aliased_classes
*/
public function toPhpString(
?string $namespace,
array $aliased_classes,
?string $this_class,
int $analysis_php_version_id
): ?string {
return null;
}
public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool
{
return false;
}
public function getAssertionString(): string
{
return 'mixed';
}
public static function isViableTemplateType(Union $template_type): bool
{
foreach ($template_type->getAtomicTypes() as $type) {
if (!$type instanceof TArray
&& !$type instanceof TClassConstant
&& !$type instanceof TKeyedArray
&& !$type instanceof TList
) {
return false;
}
}
return true;
}
public static function getArrayKeyType(
Union $type,
bool $keep_template_params = false
): ?Union {
$key_types = [];
foreach ($type->getAtomicTypes() as $atomic_type) {
if ($atomic_type instanceof TArray) {
$array_key_atomics = $atomic_type->type_params[0];
} elseif ($atomic_type instanceof TList) {
$array_key_atomics = Type::getInt();
} elseif ($atomic_type instanceof TKeyedArray) {
$array_key_atomics = $atomic_type->getGenericKeyType();
} elseif ($atomic_type instanceof TTemplateParam) {
if ($keep_template_params) {
$array_key_atomics = new Union([$atomic_type]);
} else {
$array_key_atomics = static::getArrayKeyType(
$atomic_type->as,
$keep_template_params
);
if ($array_key_atomics === null) {
continue;
}
}
} else {
continue;
}
$key_types = array_merge(
$key_types,
array_values($array_key_atomics->getAtomicTypes())
);
}
if ($key_types === []) {
return null;
}
return new Union($key_types);
}
}

View File

@ -1,94 +0,0 @@
<?php
namespace Psalm\Type\Atomic;
use function preg_quote;
use function preg_replace;
use function stripos;
use function strpos;
use function strtolower;
/**
* Represents an offset of a class constant array.
*/
class TKeyOfClassConstant extends Scalar
{
/** @var string */
public $fq_classlike_name;
/** @var string */
public $const_name;
public function __construct(string $fq_classlike_name, string $const_name)
{
$this->fq_classlike_name = $fq_classlike_name;
$this->const_name = $const_name;
}
public function getKey(bool $include_extra = true): string
{
return 'key-of<' . $this->fq_classlike_name . '::' . $this->const_name . '>';
}
/**
* @param array<lowercase-string, string> $aliased_classes
*/
public function toPhpString(
?string $namespace,
array $aliased_classes,
?string $this_class,
int $analysis_php_version_id
): ?string {
return null;
}
public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool
{
return false;
}
/**
* @param array<lowercase-string, string> $aliased_classes
*/
public function toNamespacedString(
?string $namespace,
array $aliased_classes,
?string $this_class,
bool $use_phpdoc_format
): string {
if ($this->fq_classlike_name === 'static') {
return 'key-of<static::' . $this->const_name . '>';
}
if ($this->fq_classlike_name === $this_class) {
return 'key-of<self::' . $this->const_name . '>';
}
if ($namespace && stripos($this->fq_classlike_name, $namespace . '\\') === 0) {
return 'key-of<' . preg_replace(
'/^' . preg_quote($namespace . '\\') . '/i',
'',
$this->fq_classlike_name
) . '::' . $this->const_name . '>';
}
if (!$namespace && strpos($this->fq_classlike_name, '\\') === false) {
return 'key-of<' . $this->fq_classlike_name . '::' . $this->const_name . '>';
}
if (isset($aliased_classes[strtolower($this->fq_classlike_name)])) {
return 'key-of<'
. $aliased_classes[strtolower($this->fq_classlike_name)]
. '::'
. $this->const_name
. '>';
}
return 'key-of<\\' . $this->fq_classlike_name . '::' . $this->const_name . '>';
}
public function getAssertionString(): string
{
return 'mixed';
}
}

View File

@ -2,12 +2,16 @@
namespace Psalm\Type\Atomic;
use Psalm\Codebase;
use Psalm\Internal\Type\TemplateInferredTypeReplacer;
use Psalm\Internal\Type\TemplateResult;
use Psalm\Type\Atomic;
use Psalm\Type\Union;
/**
* Represents the type used when using TKeyOfClassConstant when the type of the class constant array is a template
* Represents the type used when using TKeyOfArray when the type of the array is a template
*/
class TTemplateKeyOf extends TArrayKey
class TTemplateKeyOf extends Atomic
{
/**
* @var string
@ -45,7 +49,7 @@ class TTemplateKeyOf extends TArrayKey
return 'key-of<' . $this->param_name . '>';
}
return 'key-of<' . $this->param_name . ':' . $this->defining_class . ' as ' . $this->as->getId($exact) . '>';
return 'key-of<' . $this->as->getId($exact) . '>';
}
/**
@ -59,4 +63,32 @@ class TTemplateKeyOf extends TArrayKey
): string {
return 'key-of<' . $this->param_name . '>';
}
/**
* @param array<lowercase-string, string> $aliased_classes
*/
public function toPhpString(
?string $namespace,
array $aliased_classes,
?string $this_class,
int $analysis_php_version_id
): ?string {
return null;
}
public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool
{
return false;
}
public function replaceTemplateTypesWithArgTypes(
TemplateResult $template_result,
?Codebase $codebase
): void {
TemplateInferredTypeReplacer::replace(
$this->as,
$template_result,
$codebase
);
}
}

View File

@ -0,0 +1,94 @@
<?php
namespace Psalm\Type\Atomic;
use Psalm\Codebase;
use Psalm\Internal\Type\TemplateInferredTypeReplacer;
use Psalm\Internal\Type\TemplateResult;
use Psalm\Type\Atomic;
use Psalm\Type\Union;
/**
* Represents the type used when using TValueOfArray when the type of the array is a template
*/
class TTemplateValueOf extends Atomic
{
/**
* @var string
*/
public $param_name;
/**
* @var string
*/
public $defining_class;
/**
* @var Union
*/
public $as;
public function __construct(
string $param_name,
string $defining_class,
Union $as
) {
$this->param_name = $param_name;
$this->defining_class = $defining_class;
$this->as = $as;
}
public function getKey(bool $include_extra = true): string
{
return 'value-of<' . $this->param_name . '>';
}
public function getId(bool $exact = true, bool $nested = false): string
{
if (!$exact) {
return 'value-of<' . $this->param_name . '>';
}
return 'value-of<' . $this->as->getId($exact) . '>';
}
/**
* @param array<lowercase-string, string> $aliased_classes
*/
public function toNamespacedString(
?string $namespace,
array $aliased_classes,
?string $this_class,
bool $use_phpdoc_format
): string {
return 'value-of<' . $this->param_name . '>';
}
/**
* @param array<lowercase-string, string> $aliased_classes
*/
public function toPhpString(
?string $namespace,
array $aliased_classes,
?string $this_class,
int $analysis_php_version_id
): ?string {
return null;
}
public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool
{
return false;
}
public function replaceTemplateTypesWithArgTypes(
TemplateResult $template_result,
?Codebase $codebase
): void {
TemplateInferredTypeReplacer::replace(
$this->as,
$template_result,
$codebase
);
}
}

View File

@ -0,0 +1,105 @@
<?php
namespace Psalm\Type\Atomic;
use Psalm\Type\Atomic;
use Psalm\Type\Union;
use function array_merge;
use function array_values;
/**
* Represents a value of an array.
*/
class TValueOfArray extends Atomic
{
/** @var Union */
public $type;
public function __construct(Union $type)
{
$this->type = $type;
}
public function getKey(bool $include_extra = true): string
{
return 'value-of<' . $this->type . '>';
}
/**
* @param array<lowercase-string, string> $aliased_classes
*/
public function toPhpString(
?string $namespace,
array $aliased_classes,
?string $this_class,
int $analysis_php_version_id
): ?string {
return null;
}
public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool
{
return false;
}
public function getAssertionString(): string
{
return 'mixed';
}
public static function isViableTemplateType(Union $template_type): bool
{
foreach ($template_type->getAtomicTypes() as $type) {
if (!$type instanceof TArray
&& !$type instanceof TClassConstant
&& !$type instanceof TKeyedArray
&& !$type instanceof TList
) {
return false;
}
}
return true;
}
public static function getArrayValueType(
Union $type,
bool $keep_template_params = false
): ?Union {
$value_types = [];
foreach ($type->getAtomicTypes() as $atomic_type) {
if ($atomic_type instanceof TArray) {
$array_value_atomics = $atomic_type->type_params[1];
} elseif ($atomic_type instanceof TList) {
$array_value_atomics = $atomic_type->type_param;
} elseif ($atomic_type instanceof TKeyedArray) {
$array_value_atomics = $atomic_type->getGenericValueType();
} elseif ($atomic_type instanceof TTemplateParam) {
if ($keep_template_params) {
$array_value_atomics = new Union([$atomic_type]);
} else {
$array_value_atomics = static::getArrayValueType(
$atomic_type->as,
$keep_template_params
);
if ($array_value_atomics === null) {
continue;
}
}
} else {
continue;
}
$value_types = array_merge(
$value_types,
array_values($array_value_atomics->getAtomicTypes())
);
}
if ($value_types === []) {
return null;
}
return new Union($value_types);
}
}

View File

@ -1,69 +0,0 @@
<?php
namespace Psalm\Type\Atomic;
use Psalm\Type;
use Psalm\Type\Atomic;
/**
* Represents a value of a class constant array.
*/
class TValueOfClassConstant extends Atomic
{
/** @var string */
public $fq_classlike_name;
/** @var string */
public $const_name;
public function __construct(string $fq_classlike_name, string $const_name)
{
$this->fq_classlike_name = $fq_classlike_name;
$this->const_name = $const_name;
}
public function getKey(bool $include_extra = true): string
{
return 'value-of<' . $this->fq_classlike_name . '::' . $this->const_name . '>';
}
/**
* @param array<lowercase-string, string> $aliased_classes
*/
public function toPhpString(
?string $namespace,
array $aliased_classes,
?string $this_class,
int $analysis_php_version_id
): ?string {
return null;
}
public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool
{
return false;
}
/**
* @param array<lowercase-string, string> $aliased_classes
*/
public function toNamespacedString(
?string $namespace,
array $aliased_classes,
?string $this_class,
bool $use_phpdoc_format
): string {
if ($this->fq_classlike_name === 'static') {
return 'value-of<static::' . $this->const_name . '>';
}
return 'value-of<'
. Type::getStringFromFQCLN($this->fq_classlike_name, $namespace, $aliased_classes, $this_class)
. '>::' . $this->const_name . '>';
}
public function getAssertionString(): string
{
return 'mixed';
}
}

View File

@ -1,13 +1,12 @@
<?php
/**
* @psalm-template TKey as array-key
* @psalm-template TArray as array<TKey, mixed>
* @psalm-template TArray as array
*
* @param TArray $array
* @param mixed $search_value
* @param bool $strict
*
* @return (TArray is non-empty-array ? non-empty-list<TKey> : list<TKey>)
* @return (TArray is non-empty-array ? non-empty-list<key-of<TArray>> : list<key-of<TArray>>)
* @psalm-pure
*/
function array_keys(array $array, $search_value = null, bool $strict = false)
@ -145,12 +144,11 @@ function key($array)
}
/**
* @psalm-template TKey as array-key
* @psalm-template TArray as array<TKey, mixed>
* @psalm-template TArray as array
*
* @param TArray $array
*
* @return (TArray is array<never, never> ? null : (TArray is non-empty-array ? TKey : TKey|null))
* @return (TArray is array<never, never> ? null : (TArray is non-empty-array ? key-of<TArray> : key-of<TArray>|null))
* @psalm-pure
*/
function array_key_first($array)
@ -158,18 +156,29 @@ function array_key_first($array)
}
/**
* @psalm-template TKey as array-key
* @psalm-template TArray as array<TKey, mixed>
* @psalm-template TArray as array
*
* @param TArray $array
*
* @return (TArray is array<never, never> ? null : (TArray is non-empty-array ? TKey : TKey|null))
* @return (TArray is array<never, never> ? null : (TArray is non-empty-array ? key-of<TArray> : key-of<TArray>|null))
* @psalm-pure
*/
function array_key_last($array)
{
}
/**
* @psalm-template TArray as array
*
* @param TArray $array
*
* @return (TArray is non-empty-array ? non-empty-list<value-of<TArray>> : list<value-of<TArray>>)
* @psalm-pure
*/
function array_values($array)
{
}
/**
* @psalm-template T
*

137
tests/ArrayKeysTest.php Normal file
View File

@ -0,0 +1,137 @@
<?php
declare(strict_types=1);
namespace Psalm\Tests;
use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait;
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;
class ArrayKeysTest extends TestCase
{
use InvalidCodeAnalysisTestTrait;
use ValidCodeAnalysisTestTrait;
/**
* @return iterable<string,array{code:string,assertions?:array<string,string>,ignored_issues?:list<string>,php_version?:string}>
*/
public function providerValidCodeParse(): iterable
{
return [
'arrayKeysOfEmptyArrayReturnsListOfEmpty' => [
'code' => '<?php
$keys = array_keys([]);
',
'assertions' => [
'$keys' => 'list<never>',
],
],
'arrayKeysOfKeyedArrayReturnsNonEmptyListOfStrings' => [
'code' => '<?php
$keys = array_keys(["foo" => "bar"]);
',
'assertions' => [
'$keys' => 'non-empty-list<string>',
],
],
'arrayKeysOfListReturnsNonEmptyListOfInts' => [
'code' => '<?php
$keys = array_keys(["foo", "bar"]);
',
'assertions' => [
'$keys' => 'non-empty-list<int>',
],
],
'arrayKeysOfKeyedStringIntArrayReturnsNonEmptyListOfIntsOrStrings' => [
'code' => '<?php
$keys = array_keys(["foo" => "bar", 42]);
',
'assertions' => [
'$keys' => 'non-empty-list<int|string>',
],
],
'arrayKeysOfArrayConformsToArrayKeys' => [
'code' => '<?php
/**
* @return list<array-key>
*/
function getKeys(array $array) {
return array_keys($array);
}
'
],
'arrayKeysOfKeyedArrayConformsToCorrectLiteralStringList' => [
'code' => '<?php
/**
* @return non-empty-list<"foo"|"bar">
*/
function getKeys() {
return array_keys(["foo" => 42, "bar" => 42]);
}
'
],
'arrayKeysOfLiteralListConformsToCorrectLiteralOffsets' => [
'code' => '<?php
/**
* @return non-empty-list<0|1>
*/
function getKeys() {
return array_keys(["foo", "bar"]);
}
'
],
'arrayKeyFirstOfLiteralListConformsToCorrectLiteralOffsets' => [
'code' => '<?php
/**
* @return 0|1
*/
function getKey() {
return array_key_first(["foo", "bar"]);
}
'
],
'arrayKeyLastOfLiteralListConformsToCorrectLiteralOffsets' => [
'code' => '<?php
/**
* @return 0|1
*/
function getKey() {
return array_key_last(["foo", "bar"]);
}
'
],
];
}
/**
* @return iterable<string,array{code:string,error_message:string,ignored_issues?:list<string>,php_version?:string}>
*/
public function providerInvalidCodeParse(): iterable
{
return [
'arrayKeysOfStringArrayDoesntConformsToIntList' => [
'code' => '<?php
/**
* @param array<string, mixed> $array
* @return list<int>
*/
function getKeys(array $array) {
return array_keys($array);
}
',
'error_message' => 'InvalidReturnStatement'
],
'arrayKeysOfStringKeyedArrayDoesntConformToIntList' => [
'code' => '<?php
/**
* @return list<int>
*/
function getKeys() {
return array_keys(["foo" => 42, "bar" => 42]);
}
',
'error_message' => 'InvalidReturnStatement'
]
];
}
}

213
tests/KeyOfArrayTest.php Normal file
View File

@ -0,0 +1,213 @@
<?php
declare(strict_types=1);
namespace Psalm\Tests;
use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait;
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;
class KeyOfArrayTest extends TestCase
{
use InvalidCodeAnalysisTestTrait;
use ValidCodeAnalysisTestTrait;
/**
* @return iterable<string,array{code:string,assertions?:array<string,string>,ignored_issues?:list<string>,php_version?:string}>
*/
public function providerValidCodeParse(): iterable
{
return [
'keyOfListClassConstant' => [
'code' => '<?php
class A {
const FOO = [
"bar"
];
/** @return key-of<A::FOO> */
public function getKey() {
return 0;
}
}
'
],
'keyOfAssociativeArrayClassConstant' => [
'code' => '<?php
class A {
const FOO = [
"bar" => 42
];
/** @return key-of<A::FOO> */
public function getKey() {
return "bar";
}
}
'
],
'allKeysOfAssociativeArrayPossible' => [
'code' => '<?php
class A {
const FOO = [
"bar" => 42,
"adams" => 43,
];
/** @return key-of<A::FOO> */
public function getKey(bool $adams) {
if ($adams) {
return "adams";
}
return "bar";
}
}
'
],
'keyOfAsArray' => [
'code' => '<?php
class A {
/** @var array */
const FOO = [
"bar" => 42,
"adams" => 43,
];
/** @return key-of<self::FOO>[] */
public function getKey() {
return array_keys(self::FOO);
}
}
'
],
'keyOfArrayLiteral' => [
'code' => '<?php
/**
* @return key-of<array<int, string>>
*/
function getKey() {
return 32;
}
'
],
'keyOfUnionArrayLiteral' => [
'code' => '<?php
/**
* @return key-of<array<int, string>|array<float, string>>
*/
function getKey(bool $asFloat) {
if ($asFloat) {
return 42.0;
}
return 42;
}
'
],
'keyOfUnionListAndKeyedArray' => [
'code' => '<?php
/**
* @return key-of<list<int>|array{a: int, b: int}>
*/
function getKey(bool $asInt) {
if ($asInt) {
return 42;
}
return "a";
}
',
],
'keyOfListArrayLiteral' => [
'code' => '<?php
/**
* @return key-of<list<string>>
*/
function getKey() {
return 42;
}
'
],
'keyOfStringArrayConformsToString' => [
'code' => '<?php
/**
* @return string
*/
function getKey2() {
/** @var key-of<array<string, string>>[] */
$keys2 = ["foo"];
return $keys2[0];
}
'
],
];
}
/**
* @return iterable<string,array{code:string,error_message:string,ignored_issues?:list<string>,php_version?:string}>
*/
public function providerInvalidCodeParse(): iterable
{
return [
'onlyDefinedKeysOfAssociativeArray' => [
'code' => '<?php
class A {
const FOO = [
"bar" => 42
];
/** @return key-of<A::FOO> */
public function getKey() {
return "adams";
}
}
',
'error_message' => 'InvalidReturnStatement'
],
'keyOfArrayLiteral' => [
'code' => '<?php
class A {
/**
* @return key-of<array<int, string>>
*/
public function getKey() {
return "foo";
}
}
',
'error_message' => 'InvalidReturnStatement'
],
'onlyIntAllowedForKeyOfList' => [
'code' => '<?php
class A {
/**
* @return key-of<list<string>>
*/
public function getKey() {
return "42";
}
}
',
'error_message' => 'InvalidReturnStatement'
],
'noStringAllowedInKeyOfIntFloatArray' => [
'code' => '<?php
/**
* @return key-of<array<int, string>|array<float, string>>
*/
function getKey(bool $asFloat) {
if ($asFloat) {
return 42.0;
}
return "42";
}
',
'error_message' => 'InvalidReturnStatement'
],
'noLiteralCAllowedInKeyOfUnionListAndKeyedArray' => [
'code' => '<?php
/**
* @return key-of<list<int>|array{a: int, b: int}>
*/
function getKey() {
return "c";
}
',
'error_message' => 'InvalidReturnStatement'
],
];
}
}

View File

@ -0,0 +1,148 @@
<?php
declare(strict_types=1);
namespace Psalm\Tests;
use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait;
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;
class KeyOfTemplateTest extends TestCase
{
use InvalidCodeAnalysisTestTrait;
use ValidCodeAnalysisTestTrait;
/**
* @return iterable<string,array{code:string,assertions?:array<string,string>,ignored_issues?:list<string>,php_version?:string}>
*/
public function providerValidCodeParse(): iterable
{
return [
'acceptsArrayKeysFn' => [
'code' => '<?php
/**
* @template T of array
* @param T $array
* @return key-of<T>[]
*/
function getKey($array) {
return array_keys($array);
}
'
],
'acceptsArrayKeyFirstFn' => [
'code' => '<?php
/**
* @template T of array
* @param T $array
* @return key-of<T>|null
*/
function getKey($array) {
return array_key_first($array);
}
'
],
'acceptsArrayKeyLastFn' => [
'code' => '<?php
/**
* @template T of array
* @param T $array
* @return key-of<T>|null
*/
function getKey($array) {
return array_key_last($array);
}
'
],
'SKIPPED-acceptsIfArrayKeyExistsFn' => [
'code' => '<?php
/**
* @template T of array
* @param T $array
* @return key-of<T>|null
*/
function getKey(string $key, $array) {
if (array_key_exists($key, $array)) {
return $key;
}
return null;
}
'
],
'keyOfUnreplacedTemplateParam' => [
'code' => '<?php
/**
* @template T as array<string, bool>
*/
abstract class Foo {
/**
* @return key-of<T>
*/
abstract public function getRandomKey(): string;
}
',
],
'keyOfNestedTemplates' => [
'code' => '<?php
/**
* @template TKey of int
* @template TArray of array<TKey, bool>
* @param TArray $array
* @return list<TKey>
*/
function toListOfKeys(array $array): array {
return array_keys($array);
}'
],
];
}
/**
* @return iterable<string,array{code:string,error_message:string,ignored_issues?:list<string>,php_version?:string}>
*/
public function providerInvalidCodeParse(): iterable
{
return [
'keyOfTemplateNotIncludesString' => [
'code' => '<?php
/**
* @template T of array
* @param T $array
* @return key-of<T>
*/
function getKey($array) {
return "foo";
}
',
'error_message' => 'InvalidReturnStatement'
],
'keyOfTemplateNotIncludesInt' => [
'code' => '<?php
/**
* @template T of array
* @param T $array
* @return key-of<T>
*/
function getKey($array) {
return 0;
}
',
'error_message' => 'InvalidReturnStatement'
],
'keyOfUnresolvedTemplateParamIsStillChecked' => [
'code' => '<?php
/**
* @template T as array<int, bool>
*/
abstract class Foo {
/**
* @return key-of<T>
*/
abstract public function getRandomKey(): string;
}
',
'error_message' => 'MismatchingDocblockReturnType'
],
];
}
}

View File

@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace Psalm\Tests;
use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait;
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;
class ValueOfTemplateTest extends TestCase
{
use InvalidCodeAnalysisTestTrait;
use ValidCodeAnalysisTestTrait;
/**
* @return iterable<string,array{code:string,assertions?:array<string,string>,ignored_issues?:list<string>,php_version?:string}>
*/
public function providerValidCodeParse(): iterable
{
return [
'acceptsArrayValuesFn' => [
'code' => '<?php
/**
* @template T of array
* @param T $array
* @return value-of<T>[]
*/
function getValues($array) {
return array_values($array);
}
'
],
'SKIPPED-acceptsIfInArrayFn' => [
'code' => '<?php
/**
* @template T of array
* @param T $array
* @return value-of<T>|null
*/
function getValue(string $value, $array) {
if (in_array($value, $array)) {
return $value;
}
return null;
}
'
],
'valueOfUnreplacedTemplateParam' => [
'code' => '<?php
/**
* @template T as array<bool>
*/
abstract class Foo {
/**
* @return value-of<T>
*/
abstract public function getRandomValue(): bool;
}
',
],
'valueOfNestedTemplates' => [
'code' => '<?php
/**
* @template TValue
* @template TArray of array<TValue>
* @param TArray $array
* @return list<TValue>
*/
function toList(array $array): array {
return array_values($array);
}'
],
];
}
/**
* @return iterable<string,array{code:string,error_message:string,ignored_issues?:list<string>,php_version?:string}>
*/
public function providerInvalidCodeParse(): iterable
{
return [
'valueOfTemplateNotIncludesString' => [
'code' => '<?php
/**
* @template T of array
* @param T $array
* @return value-of<T>
*/
function getValue($array) {
return "foo";
}
',
'error_message' => 'InvalidReturnStatement'
],
'valueOfTemplateNotIncludesInt' => [
'code' => '<?php
/**
* @template T of array
* @param T $array
* @return value-of<T>
*/
function getValue($array) {
return 0;
}
',
'error_message' => 'InvalidReturnStatement'
],
'valueOfUnresolvedTemplateParamIsStillChecked' => [
'code' => '<?php
/**
* @template T as array<bool>
*/
abstract class Foo {
/**
* @return value-of<T>
*/
abstract public function getRandomValue(): string;
}
',
'error_message' => 'MismatchingDocblockReturnType'
],
];
}
}

View File

@ -762,6 +762,14 @@ class TypeParseTest extends TestCase
);
}
public function testValueOfTemplate(): void
{
$this->assertSame(
'value-of<T>',
(string)Type::parseString('value-of<T>', null, ['T' => ['' => Type::getArray()]])
);
}
public function testIndexedAccess(): void
{
$this->assertSame(

200
tests/ValueOfArrayTest.php Normal file
View File

@ -0,0 +1,200 @@
<?php
declare(strict_types=1);
namespace Psalm\Tests;
use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait;
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;
class ValueOfArrayTest extends TestCase
{
use InvalidCodeAnalysisTestTrait;
use ValidCodeAnalysisTestTrait;
/**
* @return iterable<string,array{code:string,assertions?:array<string,string>,ignored_issues?:list<string>,php_version?:string}>
*/
public function providerValidCodeParse(): iterable
{
return [
'valueOfListClassConstant' => [
'code' => '<?php
class A {
const FOO = [
"bar"
];
/** @return value-of<A::FOO> */
public function getKey() {
return "bar";
}
}
'
],
'valueOfAssociativeArrayClassConstant' => [
'code' => '<?php
class A {
const FOO = [
"bar" => 42
];
/** @return value-of<A::FOO> */
public function getValue() {
return 42;
}
}
'
],
'allValuesOfAssociativeArrayPossible' => [
'code' => '<?php
class A {
const FOO = [
"bar" => 42,
"adams" => 43,
];
/** @return value-of<A::FOO> */
public function getValue(bool $adams) {
if ($adams) {
return 42;
}
return 43;
}
}
'
],
'valueOfAsArray' => [
'code' => '<?php
class A {
/** @var array */
const FOO = [
"bar" => 42,
"adams" => 43,
];
/** @return value-of<self::FOO>[] */
public function getValues() {
return array_values(self::FOO);
}
}
'
],
'valueOfArrayLiteral' => [
'code' => '<?php
/**
* @return value-of<array<int, string>>
*/
function getKey() {
return "42";
}
'
],
'valueOfUnionArrayLiteral' => [
'code' => '<?php
/**
* @return value-of<array<array-key, int>|array<string, float>>
*/
function getValue(bool $asFloat) {
if ($asFloat) {
return 42.0;
}
return 42;
}
'
],
'valueOfStringArrayConformsToString' => [
'code' => '<?php
/**
* @return string
*/
function getKey2() {
/** @var value-of<array<string>>[] */
$keys2 = ["foo"];
return $keys2[0];
}
'
],
'acceptLiteralIntInValueOfUnionLiteralInts' => [
'code' => '<?php
/**
* @return value-of<list<0|1|2>|array{0: 3, 1: 4}>
*/
function getValue(int $i) {
if ($i >= 0 && $i <= 4) {
return $i;
}
return 0;
}
',
],
];
}
/**
* @return iterable<string,array{code:string,error_message:string,ignored_issues?:list<string>,php_version?:string}>
*/
public function providerInvalidCodeParse(): iterable
{
return [
'onlyDefinedValuesOfConstantList' => [
'code' => '<?php
class A {
const FOO = [
"bar"
];
/** @return key-of<A::FOO> */
public function getValue() {
return "adams";
}
}
',
'error_message' => 'InvalidReturnStatement'
],
'noIntForValueOfStringArrayLiteral' => [
'code' => '<?php
class A {
/**
* @return value-of<array<int, string>>
*/
public function getValue() {
return 42;
}
}
',
'error_message' => 'InvalidReturnStatement'
],
'noStringForValueOfIntList' => [
'code' => '<?php
class A {
/**
* @return value-of<list<int>>
*/
public function getValue() {
return "42";
}
}
',
'error_message' => 'InvalidReturnStatement'
],
'noOtherStringAllowedForValueOfKeyedArray' => [
'code' => '<?php
/**
* @return value-of<array{a: "foo", b: "bar"}>
*/
function getValue() {
return "adams";
}
',
'error_message' => 'InvalidReturnStatement'
],
'noOtherIntAllowedInValueOfUnionLiteralInts' => [
'code' => '<?php
/**
* @return value-of<list<0|1|2>|array{0: 3, 1: 4}>
*/
function getValue() {
return 5;
}
',
'error_message' => 'InvalidReturnStatement'
],
];
}
}