diff --git a/docs/running_psalm/plugins/plugins_type_system.md b/docs/running_psalm/plugins/plugins_type_system.md index 7676bc2f3..0589309d3 100644 --- a/docs/running_psalm/plugins/plugins_type_system.md +++ b/docs/running_psalm/plugins/plugins_type_system.md @@ -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 diff --git a/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php b/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php index f23167ce9..277996aa3 100644 --- a/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php @@ -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); diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayValuesReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayValuesReturnTypeProvider.php deleted file mode 100644 index 3ec268080..000000000 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayValuesReturnTypeProvider.php +++ /dev/null @@ -1,91 +0,0 @@ - - */ - 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]); - } -} diff --git a/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php b/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php index 4d0f1e5f8..87a9dc5d6 100644 --- a/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php @@ -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()), diff --git a/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php b/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php index 3e87325cf..f556af6e9 100644 --- a/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php @@ -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; } } } diff --git a/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php index 40f6130ee..925b79b6b 100644 --- a/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php @@ -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>> $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; + } } diff --git a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php index 9d86881d7..926d85a0b 100644 --- a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php @@ -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 = []; diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index a8116a9bd..cb3ec4077 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -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' ); } diff --git a/src/Psalm/Type/Atomic/TTemplateKeyOf.php b/src/Psalm/Type/Atomic/TTemplateKeyOf.php index 1352d8721..f8abbfcce 100644 --- a/src/Psalm/Type/Atomic/TTemplateKeyOf.php +++ b/src/Psalm/Type/Atomic/TTemplateKeyOf.php @@ -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 { diff --git a/src/Psalm/Type/Atomic/TTemplateValueOf.php b/src/Psalm/Type/Atomic/TTemplateValueOf.php new file mode 100644 index 000000000..10b4770d9 --- /dev/null +++ b/src/Psalm/Type/Atomic/TTemplateValueOf.php @@ -0,0 +1,80 @@ +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 $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 $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; + } +} diff --git a/stubs/CoreGenericFunctions.phpstub b/stubs/CoreGenericFunctions.phpstub index 94fa149bd..17302b0d0 100644 --- a/stubs/CoreGenericFunctions.phpstub +++ b/stubs/CoreGenericFunctions.phpstub @@ -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> : list>) + * @psalm-pure + */ +function array_values($array) +{ +} + /** * @psalm-template T * diff --git a/tests/Template/ValueOfTemplateTest.php b/tests/Template/ValueOfTemplateTest.php new file mode 100644 index 000000000..50bbb60dc --- /dev/null +++ b/tests/Template/ValueOfTemplateTest.php @@ -0,0 +1,85 @@ +,ignored_issues?:list,php_version?:string}> + */ + public function providerValidCodeParse(): iterable + { + return [ + 'acceptsArrayValuesFn' => [ + 'code' => '[] + */ + function getValues($array) { + return array_values($array); + } + ' + ], + 'SKIPPED-acceptsIfInArrayFn' => [ + 'code' => '|null + */ + function getValue(string $value, $array) { + if (in_array($value, $array)) { + return $value; + } + return null; + } + ' + ], + ]; + } + + /** + * @return iterable,php_version?:string}> + */ + public function providerInvalidCodeParse(): iterable + { + return [ + 'valueOfTemplateNotIncludesString' => [ + 'code' => ' + */ + function getValue($array) { + return "foo"; + } + ', + 'error_message' => 'InvalidReturnStatement' + ], + 'valueOfTemplateNotIncludesInt' => [ + 'code' => ' + */ + function getValue($array) { + return 0; + } + ', + 'error_message' => 'InvalidReturnStatement' + ], + ]; + } +} diff --git a/tests/TypeParseTest.php b/tests/TypeParseTest.php index 00186b2b0..36efc50a5 100644 --- a/tests/TypeParseTest.php +++ b/tests/TypeParseTest.php @@ -762,6 +762,14 @@ class TypeParseTest extends TestCase ); } + public function testValueOfTemplate(): void + { + $this->assertSame( + 'value-of', + (string)Type::parseString('value-of', null, ['T' => ['' => Type::getArray()]]) + ); + } + public function testIndexedAccess(): void { $this->assertSame(