1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-26 20:34:47 +01:00

feat: allow unions for key-of/value-of

Add tests for TValueOfArray.
This commit is contained in:
Patrick Remy 2022-01-20 22:41:33 +01:00
parent 2880d046ce
commit 1f28d025c3
No known key found for this signature in database
GPG Key ID: FE25C0B14C0500CD
10 changed files with 334 additions and 105 deletions

View File

@ -295,8 +295,7 @@ 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;
}

View File

@ -239,7 +239,6 @@ class TemplateInferredTypeReplacer
: 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);

View File

@ -887,9 +887,14 @@ class TypeExpander
bool $evaluate_class_constants,
bool $throw_on_unresolvable_constant
): array {
$type_param = $return_type->type;
// 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 ($evaluate_class_constants && $type_param instanceof TClassConstant) {
if ($type_param->fq_classlike_name === 'self' && $self_class) {
$type_param->fq_classlike_name = $self_class;
}
@ -900,8 +905,9 @@ class TypeExpander
throw new UnresolvableConstantException($type_param->fq_classlike_name, $type_param->const_name);
}
$constant_type = null;
try {
$type_param = $codebase->classlikes->getClassConstantType(
$constant_type = $codebase->classlikes->getClassConstantType(
$type_param->fq_classlike_name,
$type_param->const_name,
ReflectionProperty::IS_PRIVATE
@ -909,38 +915,44 @@ class TypeExpander
} catch (CircularReferenceException $e) {
return [$return_type];
}
if (!$type_param) {
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($return_type->type->fq_classlike_name, $return_type->type->const_name);
throw new UnresolvableConstantException($type_param->fq_classlike_name, $type_param->const_name);
} else {
return [$return_type];
}
}
$type_atomics = array_merge(
$type_atomics,
$constant_type->getAtomicTypes()
);
}
if (!$type_param instanceof Union) {
$type_param = new Union([$type_param]);
}
// Types inside union are checked on instantiation and above on
// expansion, currently there is no Union<T> for typing here.
/** @var list<TList|TKeyedArray|TArray> $type_atomics */
// 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;
}
foreach ($type_atomics as $type_atomic) {
// 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) {
} elseif ($type_atomic instanceof TKeyedArray) {
$type_atomic = $type_atomic->getGenericArrayType();
}
@ -958,7 +970,7 @@ class TypeExpander
}
}
if (empty($new_return_types)) {
if ($new_return_types === []) {
return [$return_type];
}
return $new_return_types;

View File

@ -696,40 +696,25 @@ class TypeParser
);
}
$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 (!TKeyOfArray::isViableTemplateType($param_union_types[0])) {
if (!TKeyOfArray::isViableTemplateType($generic_params[0])) {
throw new TypeParseTreeException(
'Untemplated key-of param ' . $param_name . ' should be a class constant or an array'
);
}
return new TKeyOfArray($param_union_types[0]);
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 TArray
&& !$param_union_types[0] instanceof TList
&& !$param_union_types[0] instanceof TKeyedArray
&& !$param_union_types[0] instanceof TClassConstant) {
if (!TValueOfArray::isViableTemplateType($generic_params[0])) {
throw new TypeParseTreeException(
'Untemplated value-of param ' . $param_name . ' should be a class constant or an array'
);
}
return new TValueOfArray($param_union_types[0]);
return new TValueOfArray($generic_params[0]);
}
if ($generic_type_value === 'int-mask') {

View File

@ -3,21 +3,17 @@
namespace Psalm\Type\Atomic;
use Psalm\Type\Atomic;
use Psalm\Type\Union;
/**
* Represents an offset of an array.
*
* @psalm-type ArrayLikeTemplateType = TClassConstant|TKeyedArray|TList|TArray
*/
class TKeyOfArray extends TArrayKey
{
/** @var ArrayLikeTemplateType */
/** @var Union */
public $type;
/**
* @param ArrayLikeTemplateType $type
*/
public function __construct(Atomic $type)
public function __construct(Union $type)
{
$this->type = $type;
}
@ -49,14 +45,17 @@ class TKeyOfArray extends TArrayKey
return 'mixed';
}
/**
* @psalm-assert-if-true ArrayLikeTemplateType $template_type
*/
public static function isViableTemplateType(Atomic $template_type): bool
public static function isViableTemplateType(Union $template_type): bool
{
return $template_type instanceof TArray
|| $template_type instanceof TClassConstant
|| $template_type instanceof TKeyedArray
|| $template_type instanceof TList;
foreach ($template_type->getAtomicTypes() as $type) {
if (!$type instanceof TArray
&& !$type instanceof TClassConstant
&& !$type instanceof TKeyedArray
&& !$type instanceof TList
) {
return false;
}
}
return true;
}
}

View File

@ -3,19 +3,17 @@
namespace Psalm\Type\Atomic;
use Psalm\Type\Atomic;
use Psalm\Type\Union;
/**
* Represents a value of an array.
*/
class TValueOfArray extends Atomic
{
/** @var TClassConstant|TKeyedArray|TList|TArray */
/** @var Union */
public $type;
/**
* @param TClassConstant|TKeyedArray|TList|TArray $type
*/
public function __construct(Atomic $type)
public function __construct(Union $type)
{
$this->type = $type;
}
@ -46,4 +44,18 @@ class TValueOfArray extends Atomic
{
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;
}
}

View File

@ -28,7 +28,7 @@ class ArrayKeysTest extends TestCase
],
'arrayKeysOfKeyedArrayReturnsNonEmptyListOfStrings' => [
'code' => '<?php
$keys = array_keys([\'foo\' => \'bar\']);
$keys = array_keys(["foo" => "bar"]);
',
'assertions' => [
'$keys' => 'non-empty-list<string>',
@ -36,7 +36,7 @@ class ArrayKeysTest extends TestCase
],
'arrayKeysOfListReturnsNonEmptyListOfInts' => [
'code' => '<?php
$keys = array_keys([\'foo\', \'bar\']);
$keys = array_keys(["foo", "bar"]);
',
'assertions' => [
'$keys' => 'non-empty-list<int>',
@ -44,7 +44,7 @@ class ArrayKeysTest extends TestCase
],
'arrayKeysOfKeyedStringIntArrayReturnsNonEmptyListOfIntsOrStrings' => [
'code' => '<?php
$keys = array_keys([\'foo\' => \'bar\', 42]);
$keys = array_keys(["foo" => "bar", 42]);
',
'assertions' => [
'$keys' => 'non-empty-list<int|string>',
@ -63,10 +63,10 @@ class ArrayKeysTest extends TestCase
'arrayKeysOfKeyedArrayConformsToCorrectLiteralStringList' => [
'code' => '<?php
/**
* @return non-empty-list<\'foo\'|\'bar\'>
* @return non-empty-list<"foo"|"bar">
*/
function getKeys() {
return array_keys([\'foo\' => 42, \'bar\' => 42]);
return array_keys(["foo" => 42, "bar" => 42]);
}
'
],
@ -76,7 +76,7 @@ class ArrayKeysTest extends TestCase
* @return non-empty-list<0|1>
*/
function getKeys() {
return array_keys([\'foo\', \'bar\']);
return array_keys(["foo", "bar"]);
}
'
],
@ -86,7 +86,7 @@ class ArrayKeysTest extends TestCase
* @return 0|1
*/
function getKey() {
return array_key_first([\'foo\', \'bar\']);
return array_key_first(["foo", "bar"]);
}
'
],
@ -96,7 +96,7 @@ class ArrayKeysTest extends TestCase
* @return 0|1
*/
function getKey() {
return array_key_last([\'foo\', \'bar\']);
return array_key_last(["foo", "bar"]);
}
'
],
@ -127,7 +127,7 @@ class ArrayKeysTest extends TestCase
* @return list<int>
*/
function getKeys() {
return array_keys([\'foo\' => 42, \'bar\' => 42]);
return array_keys(["foo" => 42, "bar" => 42]);
}
',
'error_message' => 'InvalidReturnStatement'

View File

@ -22,7 +22,7 @@ class KeyOfArrayTest extends TestCase
'code' => '<?php
class A {
const FOO = [
\'bar\'
"bar"
];
/** @return key-of<A::FOO> */
public function getKey() {
@ -35,11 +35,11 @@ class KeyOfArrayTest extends TestCase
'code' => '<?php
class A {
const FOO = [
\'bar\' => 42
"bar" => 42
];
/** @return key-of<A::FOO> */
public function getKey() {
return \'bar\';
return "bar";
}
}
'
@ -48,15 +48,15 @@ class KeyOfArrayTest extends TestCase
'code' => '<?php
class A {
const FOO = [
\'bar\' => 42,
\'adams\' => 43,
"bar" => 42,
"adams" => 43,
];
/** @return key-of<A::FOO> */
public function getKey(bool $adams) {
if ($adams) {
return \'adams\';
return "adams";
}
return \'bar\';
return "bar";
}
}
'
@ -66,11 +66,11 @@ class KeyOfArrayTest extends TestCase
class A {
/** @var array */
const FOO = [
\'bar\' => 42,
\'adams\' => 43,
"bar" => 42,
"adams" => 43,
];
/** @return key-of<self::FOO>[] */
public function getKey(bool $adams) {
public function getKey() {
return array_keys(self::FOO);
}
}
@ -99,6 +99,19 @@ class KeyOfArrayTest extends TestCase
}
'
],
'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
/**
@ -116,7 +129,7 @@ class KeyOfArrayTest extends TestCase
*/
function getKey2() {
/** @var key-of<array<string, string>>[] */
$keys2 = [\'foo\'];
$keys2 = ["foo"];
return $keys2[0];
}
'
@ -134,11 +147,11 @@ class KeyOfArrayTest extends TestCase
'code' => '<?php
class A {
const FOO = [
\'bar\' => 42
"bar" => 42
];
/** @return key-of<A::FOO> */
public function getKey(bool $adams) {
return \'adams\';
public function getKey() {
return "adams";
}
}
',
@ -151,7 +164,7 @@ class KeyOfArrayTest extends TestCase
* @return key-of<array<int, string>>
*/
public function getKey() {
return \'foo\';
return "foo";
}
}
',
@ -164,7 +177,7 @@ class KeyOfArrayTest extends TestCase
* @return key-of<list<string>>
*/
public function getKey() {
return \'42\';
return "42";
}
}
',
@ -179,7 +192,18 @@ class KeyOfArrayTest extends TestCase
if ($asFloat) {
return 42.0;
}
return \'42\';
return "42";
}
',
'error_message' => 'InvalidReturnStatement'
],
'noLiteralCAllowedInKeyOfUnionListAndKeyedArray' => [
'code' => '<?php
/**
* @return key-of<list<int>|array{a: int, b: int}>
*/
function getKey() {
return "c";
}
',
'error_message' => 'InvalidReturnStatement'

View File

@ -54,22 +54,21 @@ class KeyOfTemplateTest extends TestCase
}
'
],
// 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;
// }
// '
// ],
'SKIP-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;
}
'
],
];
}
@ -87,7 +86,7 @@ class KeyOfTemplateTest extends TestCase
* @return key-of<T>
*/
function getKey($array) {
return \'foo\';
return "foo";
}
',
'error_message' => 'InvalidReturnStatement'

200
tests/ValueOfArrayTest.php Normal file
View File

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