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:
commit
2e01e9b7cd
11
UPGRADING.md
11
UPGRADING.md
@ -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
|
||||
|
@ -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.
|
||||
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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]);
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
) {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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 = [];
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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'
|
||||
|
@ -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)
|
||||
{
|
||||
|
105
src/Psalm/Type/Atomic/TKeyOfArray.php
Normal file
105
src/Psalm/Type/Atomic/TKeyOfArray.php
Normal 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);
|
||||
}
|
||||
}
|
@ -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';
|
||||
}
|
||||
}
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
94
src/Psalm/Type/Atomic/TTemplateValueOf.php
Normal file
94
src/Psalm/Type/Atomic/TTemplateValueOf.php
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
105
src/Psalm/Type/Atomic/TValueOfArray.php
Normal file
105
src/Psalm/Type/Atomic/TValueOfArray.php
Normal 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);
|
||||
}
|
||||
}
|
@ -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,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
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'
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
213
tests/KeyOfArrayTest.php
Normal file
213
tests/KeyOfArrayTest.php
Normal 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'
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
148
tests/Template/KeyOfTemplateTest.php
Normal file
148
tests/Template/KeyOfTemplateTest.php
Normal 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'
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
124
tests/Template/ValueOfTemplateTest.php
Normal file
124
tests/Template/ValueOfTemplateTest.php
Normal 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'
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
@ -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
200
tests/ValueOfArrayTest.php
Normal 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'
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user