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

feat: make value-of<T> capable for template types

This commit is contained in:
Patrick Remy 2022-01-25 22:08:44 +01:00
parent dff8869685
commit 8cd5ccd076
No known key found for this signature in database
GPG Key ID: FE25C0B14C0500CD
13 changed files with 371 additions and 164 deletions

View File

@ -55,7 +55,9 @@ The classes are as follows:
`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

View File

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

View File

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

View File

@ -33,6 +33,7 @@ use Psalm\Type\Atomic\TObjectWithProperties;
use Psalm\Type\Atomic\TScalar;
use Psalm\Type\Atomic\TString;
use Psalm\Type\Atomic\TTemplateParam;
use Psalm\Type\Atomic\TTemplateValueOf;
use function array_merge;
use function array_values;
@ -355,6 +356,48 @@ class AtomicTypeComparator
return false;
}
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) {
foreach ($input_type_part->as->getAtomicTypes() as $atomic_type) {
/** @var TArray|TList|TKeyedArray $atomic_type */
// Transform all types to TArray if needed
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;
} else {
$array_value_atomics = $atomic_type->getGenericValueType();
}
foreach ($array_value_atomics->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 TConditional) {
$atomic_types = array_merge(
array_values($container_type_part->if_type->getAtomicTypes()),

View File

@ -4,6 +4,7 @@ namespace Psalm\Internal\Type\Comparator;
use Psalm\Codebase;
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
use Psalm\Type;
use Psalm\Type\Atomic\Scalar;
use Psalm\Type\Atomic\TArray;
use Psalm\Type\Atomic\TArrayKey;
@ -18,6 +19,8 @@ use Psalm\Type\Atomic\TFalse;
use Psalm\Type\Atomic\TFloat;
use Psalm\Type\Atomic\TInt;
use Psalm\Type\Atomic\TIntRange;
use Psalm\Type\Atomic\TKeyedArray;
use Psalm\Type\Atomic\TList;
use Psalm\Type\Atomic\TLiteralClassString;
use Psalm\Type\Atomic\TLiteralFloat;
use Psalm\Type\Atomic\TLiteralInt;
@ -267,19 +270,28 @@ class ScalarTypeComparator
if ($input_type_part instanceof TTemplateKeyOf) {
foreach ($input_type_part->as->getAtomicTypes() as $atomic_type) {
/** @var TArray|TList|TKeyedArray $atomic_type */
// Transform all types to TArray if needed
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;
}
$array_key_atomics = $atomic_type->type_params[0];
} elseif ($atomic_type instanceof TList) {
$array_key_atomics = Type::getInt();
} else {
$array_key_atomics = $atomic_type->getGenericKeyType();
}
/** @var Scalar $array_key_atomic */
foreach ($array_key_atomics->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;
}
}
}

View File

@ -6,6 +6,7 @@ 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;
@ -22,6 +23,8 @@ 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;
@ -230,19 +233,18 @@ 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;
} elseif ($atomic_type instanceof TTemplateKeyOf
|| $atomic_type instanceof TTemplateValueOf
) {
$new_type = self::replaceTemplateKeyOfValueOf(
$codebase,
$atomic_type,
$inferred_lower_bounds
);
if ($template_type) {
if (TKeyOfArray::isViableTemplateType($template_type)) {
$keys_to_unset[] = $key;
$new_types[] = new TKeyOfArray(clone $template_type);
}
if ($new_type) {
$keys_to_unset[] = $key;
$new_types[] = $new_type;
}
} elseif ($atomic_type instanceof TConditional
&& $codebase
@ -430,4 +432,37 @@ class TemplateInferredTypeReplacer
)->getAtomicTypes()
);
}
/**
* @param TTemplateKeyOf|TTemplateValueOf $atomic_type
* @param array<string, array<string, non-empty-list<TemplateBound>>> $inferred_lower_bounds
*/
private static function replaceTemplateKeyOfValueOf(
?Codebase $codebase,
Atomic $atomic_type,
array $inferred_lower_bounds
): ?Atomic {
if (!isset($inferred_lower_bounds[$atomic_type->param_name][$atomic_type->defining_class])) {
return null;
}
$template_type = clone TemplateStandinTypeReplacer::getMostSpecificTypeFromBounds(
$inferred_lower_bounds[$atomic_type->param_name][$atomic_type->defining_class],
$codebase
);
if ($atomic_type instanceof TTemplateKeyOf
&& TKeyOfArray::isViableTemplateType($template_type)
) {
return new TKeyOfArray(clone $template_type);
}
if ($atomic_type instanceof TTemplateValueOf
&& TValueOfArray::isViableTemplateType($template_type)
) {
return new TValueOfArray(clone $template_type);
}
return null;
}
}

View File

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

View File

@ -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\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,9 +688,9 @@ 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,
@ -698,7 +700,7 @@ class TypeParser
if (!TKeyOfArray::isViableTemplateType($generic_params[0])) {
throw new TypeParseTreeException(
'Untemplated key-of param ' . $param_name . ' should be a class constant or an array'
'Untemplated key-of param ' . $param_name . ' should be an array'
);
}
@ -708,9 +710,19 @@ class TypeParser
if ($generic_type_value === 'value-of') {
$param_name = $generic_params[0]->getId(false);
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]
);
}
if (!TValueOfArray::isViableTemplateType($generic_params[0])) {
throw new TypeParseTreeException(
'Untemplated value-of param ' . $param_name . ' should be a class constant or an array'
'Untemplated value-of param ' . $param_name . ' should be an array'
);
}

View File

@ -5,7 +5,7 @@ namespace Psalm\Type\Atomic;
use Psalm\Type\Union;
/**
* Represents the type used when using TKeyOfArray 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
{

View File

@ -0,0 +1,80 @@
<?php
namespace Psalm\Type\Atomic;
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->param_name . ':' . $this->defining_class . ' as ' . $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;
}
}

View File

@ -167,6 +167,18 @@ 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
*

View File

@ -0,0 +1,85 @@
<?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;
}
'
],
];
}
/**
* @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'
],
];
}
}

View File

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