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:
parent
9168cef2d4
commit
2880d046ce
@ -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.
|
||||
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
) {
|
||||
|
@ -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
|
||||
) {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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'
|
||||
|
@ -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)
|
||||
{
|
||||
|
62
src/Psalm/Type/Atomic/TKeyOfArray.php
Normal file
62
src/Psalm/Type/Atomic/TKeyOfArray.php
Normal 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;
|
||||
}
|
||||
}
|
@ -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';
|
||||
}
|
||||
}
|
@ -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
|
||||
{
|
||||
|
49
src/Psalm/Type/Atomic/TValueOfArray.php
Normal file
49
src/Psalm/Type/Atomic/TValueOfArray.php
Normal 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';
|
||||
}
|
||||
}
|
@ -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';
|
||||
}
|
||||
}
|
@ -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
137
tests/ArrayKeysTest.php
Normal 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
189
tests/KeyOfArrayTest.php
Normal 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'
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
110
tests/Template/KeyOfTemplateTest.php
Normal file
110
tests/Template/KeyOfTemplateTest.php
Normal 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'
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user