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

feat: make key-of/value-of usable with non-const arrays

This commit is contained in:
Patrick Remy 2022-01-16 21:45:58 +01:00
parent 9168cef2d4
commit 2880d046ce
No known key found for this signature in database
GPG Key ID: FE25C0B14C0500CD
16 changed files with 711 additions and 255 deletions

View File

@ -49,9 +49,9 @@ 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
@ -277,5 +277,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

@ -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

@ -253,18 +253,16 @@ class ScalarTypeComparator
return true;
}
if ($container_type_part instanceof TArrayKey
&& $input_type_part instanceof TNumeric
) {
return true;
}
if ($container_type_part instanceof TTemplateKeyOf) {
if (!$input_type_part instanceof TTemplateKeyOf) {
return false;
}
if ($container_type_part instanceof TArrayKey
&& ($input_type_part instanceof TInt
|| $input_type_part instanceof TString
|| $input_type_part instanceof TTemplateKeyOf)
) {
return true;
return UnionTypeComparator::isContainedBy(
$codebase,
$input_type_part->as,
$container_type_part->as
);
}
if ($input_type_part instanceof TTemplateKeyOf) {
@ -289,6 +287,20 @@ class ScalarTypeComparator
return true;
}
if ($container_type_part instanceof TArrayKey
&& $input_type_part instanceof TNumeric
) {
return true;
}
if ($container_type_part instanceof TArrayKey
&& ($input_type_part instanceof TInt
|| $input_type_part instanceof TString
|| $input_type_part instanceof TTemplateKeyOf)
) {
return true;
}
if ($input_type_part instanceof TArrayKey &&
($container_type_part instanceof TInt || $container_type_part instanceof TString)
) {

View File

@ -10,6 +10,7 @@ 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,6 +19,7 @@ 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\Union;
@ -228,6 +230,21 @@ class TemplateInferredTypeReplacer
} else {
$new_types[] = new TMixed();
}
} elseif ($atomic_type instanceof TTemplateKeyOf) {
$template_type = isset($inferred_lower_bounds[$atomic_type->param_name][$atomic_type->defining_class])
? clone TemplateStandinTypeReplacer::getMostSpecificTypeFromBounds(
$inferred_lower_bounds[$atomic_type->param_name][$atomic_type->defining_class],
$codebase
)
: null;
if ($template_type) {
$template_type = $template_type->getSingleAtomic();
if (TKeyOfArray::isViableTemplateType($template_type)) {
$keys_to_unset[] = $key;
$new_types[] = new TKeyOfArray(clone $template_type);
}
}
} elseif ($atomic_type instanceof TConditional
&& $codebase
) {

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;
@ -350,52 +350,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 +875,92 @@ 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 {
$type_param = $return_type->type;
if ($evaluate_class_constants && $type_param instanceof TClassConstant) {
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 {
$type_param = $codebase->classlikes->getClassConstantType(
$type_param->fq_classlike_name,
$type_param->const_name,
ReflectionProperty::IS_PRIVATE
);
} catch (CircularReferenceException $e) {
return [$return_type];
}
if (!$type_param) {
if ($throw_on_unresolvable_constant) {
throw new UnresolvableConstantException($return_type->type->fq_classlike_name, $return_type->type->const_name);
} else {
return [$return_type];
}
}
}
if (!$type_param instanceof Union) {
$type_param = new Union([$type_param]);
}
// Merge keys/values of provided array types
$new_return_types = [];
foreach ($type_param->getAtomicTypes() as $type_atomic) {
// Abort if any type of the param's union is invalid
if (!$type_atomic instanceof TKeyedArray
&& !$type_atomic instanceof TArray
&& !$type_atomic instanceof TList
) {
break;
}
// Transform all types to TArray if needed
if ($type_atomic instanceof TList) {
$type_atomic = new TArray([
new Union([new TInt()]),
$type_atomic->type_param
]);
}
if ($type_atomic instanceof TKeyedArray) {
$type_atomic = $type_atomic->getGenericArrayType();
}
// Add key-of/value-of type to return types list
if ($return_type instanceof TKeyOfArray) {
$new_return_types = array_merge(
$new_return_types,
array_values($type_atomic->type_params[0]->getAtomicTypes())
) ;
} else {
$new_return_types = array_merge(
$new_return_types,
array_values($type_atomic->type_params[1]->getAtomicTypes())
) ;
}
}
if (empty($new_return_types)) {
return [$return_type];
}
return $new_return_types;
}
}

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;
@ -60,7 +60,7 @@ use Psalm\Type\Atomic\TTemplateKeyOf;
use Psalm\Type\Atomic\TTemplateParam;
use Psalm\Type\Atomic\TTemplateParamClass;
use Psalm\Type\Atomic\TTypeAlias;
use Psalm\Type\Atomic\TValueOfClassConstant;
use Psalm\Type\Atomic\TValueOfArray;
use Psalm\Type\TypeNode;
use Psalm\Type\Union;
@ -692,7 +692,7 @@ class TypeParser
return new TTemplateKeyOf(
$param_name,
$defining_class,
$template_type_map[$param_name][$defining_class]
$generic_params[0]
);
}
@ -702,16 +702,13 @@ class TypeParser
throw new TypeParseTreeException('Union types are not allowed in key-of type');
}
if (!$param_union_types[0] instanceof TClassConstant) {
if (!TKeyOfArray::isViableTemplateType($param_union_types[0])) {
throw new TypeParseTreeException(
'Untemplated key-of param ' . $param_name . ' should be a class constant'
'Untemplated key-of param ' . $param_name . ' should be a class constant or an array'
);
}
return new TKeyOfClassConstant(
$param_union_types[0]->fq_classlike_name,
$param_union_types[0]->const_name
);
return new TKeyOfArray($param_union_types[0]);
}
if ($generic_type_value === 'value-of') {
@ -723,16 +720,16 @@ class TypeParser
throw new TypeParseTreeException('Union types are not allowed in value-of type');
}
if (!$param_union_types[0] instanceof TClassConstant) {
if (!$param_union_types[0] instanceof TArray
&& !$param_union_types[0] instanceof TList
&& !$param_union_types[0] instanceof TKeyedArray
&& !$param_union_types[0] instanceof TClassConstant) {
throw new TypeParseTreeException(
'Untemplated value-of param ' . $param_name . ' should be a class constant'
'Untemplated value-of param ' . $param_name . ' should be a class constant or an array'
);
}
return new TValueOfClassConstant(
$param_union_types[0]->fq_classlike_name,
$param_union_types[0]->const_name
);
return new TValueOfArray($param_union_types[0]);
}
if ($generic_type_value === 'int-mask') {
@ -803,8 +800,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,62 @@
<?php
namespace Psalm\Type\Atomic;
use Psalm\Type\Atomic;
/**
* Represents an offset of an array.
*
* @psalm-type ArrayLikeTemplateType = TClassConstant|TKeyedArray|TList|TArray
*/
class TKeyOfArray extends TArrayKey
{
/** @var ArrayLikeTemplateType */
public $type;
/**
* @param ArrayLikeTemplateType $type
*/
public function __construct(Atomic $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';
}
/**
* @psalm-assert-if-true ArrayLikeTemplateType $template_type
*/
public static function isViableTemplateType(Atomic $template_type): bool
{
return $template_type instanceof TArray
|| $template_type instanceof TClassConstant
|| $template_type instanceof TKeyedArray
|| $template_type instanceof TList;
}
}

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

@ -5,7 +5,7 @@ namespace 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 class constant array is a template
*/
class TTemplateKeyOf extends TArrayKey
{

View File

@ -0,0 +1,49 @@
<?php
namespace Psalm\Type\Atomic;
use Psalm\Type\Atomic;
/**
* Represents a value of an array.
*/
class TValueOfArray extends Atomic
{
/** @var TClassConstant|TKeyedArray|TList|TArray */
public $type;
/**
* @param TClassConstant|TKeyedArray|TList|TArray $type
*/
public function __construct(Atomic $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';
}
}

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,12 +156,11 @@ 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)

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'
]
];
}
}

189
tests/KeyOfArrayTest.php Normal file
View File

@ -0,0 +1,189 @@
<?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(bool $adams) {
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;
}
'
],
'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(bool $adams) {
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'
],
];
}
}

View File

@ -0,0 +1,110 @@
<?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);
}
'
],
// Currently not works!
// '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;
// }
// '
// ],
];
}
/**
* @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'
],
];
}
}