From 2880d046cec4823270d864391f0be16c99ac7b10 Mon Sep 17 00:00:00 2001 From: Patrick Remy Date: Sun, 16 Jan 2022 21:45:58 +0100 Subject: [PATCH 01/11] feat: make key-of/value-of usable with non-const arrays --- .../plugins/plugins_type_system.md | 6 +- .../Stubs/Generator/StubsGenerator.php | 1 - .../Type/Comparator/ScalarTypeComparator.php | 34 +++- .../Type/TemplateInferredTypeReplacer.php | 17 ++ src/Psalm/Internal/Type/TypeExpander.php | 146 +++++++++----- src/Psalm/Internal/Type/TypeParser.php | 31 ++- src/Psalm/Type/Atomic/TIntMaskOf.php | 4 +- src/Psalm/Type/Atomic/TKeyOfArray.php | 62 ++++++ src/Psalm/Type/Atomic/TKeyOfClassConstant.php | 94 --------- src/Psalm/Type/Atomic/TTemplateKeyOf.php | 2 +- src/Psalm/Type/Atomic/TValueOfArray.php | 49 +++++ .../Type/Atomic/TValueOfClassConstant.php | 69 ------- stubs/CoreGenericFunctions.phpstub | 15 +- tests/ArrayKeysTest.php | 137 +++++++++++++ tests/KeyOfArrayTest.php | 189 ++++++++++++++++++ tests/Template/KeyOfTemplateTest.php | 110 ++++++++++ 16 files changed, 711 insertions(+), 255 deletions(-) create mode 100644 src/Psalm/Type/Atomic/TKeyOfArray.php delete mode 100644 src/Psalm/Type/Atomic/TKeyOfClassConstant.php create mode 100644 src/Psalm/Type/Atomic/TValueOfArray.php delete mode 100644 src/Psalm/Type/Atomic/TValueOfClassConstant.php create mode 100644 tests/ArrayKeysTest.php create mode 100644 tests/KeyOfArrayTest.php create mode 100644 tests/Template/KeyOfTemplateTest.php diff --git a/docs/running_psalm/plugins/plugins_type_system.md b/docs/running_psalm/plugins/plugins_type_system.md index c59e4c1a465..7676bc2f398 100644 --- a/docs/running_psalm/plugins/plugins_type_system.md +++ b/docs/running_psalm/plugins/plugins_type_system.md @@ -49,9 +49,9 @@ The classes are as follows: `TIntMaskOf` - as above, but used with with a reference to constants in code`int-mask` 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`). -`TValueOfClassConstant` - Represents a value of a class constant array. +`TValueOfArray` - Represents a value of an array (e.g. `value-of`). `TTemplateIndexedAccess` - To be documented @@ -277,5 +277,3 @@ Another way of creating these instances is to use the class `Psalm\Type` which i ``` You can find how Psalm would represent a given type as objects, by specifying the type as an input to this function, and calling `var_dump` on the result. - - diff --git a/src/Psalm/Internal/Stubs/Generator/StubsGenerator.php b/src/Psalm/Internal/Stubs/Generator/StubsGenerator.php index 1f6076d75be..e65c343324a 100644 --- a/src/Psalm/Internal/Stubs/Generator/StubsGenerator.php +++ b/src/Psalm/Internal/Stubs/Generator/StubsGenerator.php @@ -7,7 +7,6 @@ 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; diff --git a/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php b/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php index 2f192757fcd..e96b7d55917 100644 --- a/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php @@ -253,18 +253,16 @@ public static function isContainedBy( return true; } - if ($container_type_part instanceof TArrayKey - && $input_type_part instanceof TNumeric - ) { - return true; - } + if ($container_type_part instanceof TTemplateKeyOf) { + if (!$input_type_part instanceof TTemplateKeyOf) { + return false; + } - if ($container_type_part instanceof TArrayKey - && ($input_type_part instanceof TInt - || $input_type_part instanceof TString - || $input_type_part instanceof TTemplateKeyOf) - ) { - return true; + return UnionTypeComparator::isContainedBy( + $codebase, + $input_type_part->as, + $container_type_part->as + ); } if ($input_type_part instanceof TTemplateKeyOf) { @@ -289,6 +287,20 @@ public static function isContainedBy( return true; } + if ($container_type_part instanceof TArrayKey + && $input_type_part instanceof TNumeric + ) { + return true; + } + + if ($container_type_part instanceof TArrayKey + && ($input_type_part instanceof TInt + || $input_type_part instanceof TString + || $input_type_part instanceof TTemplateKeyOf) + ) { + return true; + } + if ($input_type_part instanceof TArrayKey && ($container_type_part instanceof TInt || $container_type_part instanceof TString) ) { diff --git a/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php index c800b1682da..956d1861ce0 100644 --- a/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php @@ -10,6 +10,7 @@ use Psalm\Type\Atomic\TConditional; use Psalm\Type\Atomic\TInt; use Psalm\Type\Atomic\TIterable; +use Psalm\Type\Atomic\TKeyOfArray; use Psalm\Type\Atomic\TKeyedArray; use Psalm\Type\Atomic\TLiteralInt; use Psalm\Type\Atomic\TLiteralString; @@ -18,6 +19,7 @@ use Psalm\Type\Atomic\TObject; use Psalm\Type\Atomic\TObjectWithProperties; use Psalm\Type\Atomic\TTemplateIndexedAccess; +use Psalm\Type\Atomic\TTemplateKeyOf; use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Atomic\TTemplateParamClass; use Psalm\Type\Union; @@ -228,6 +230,21 @@ public static function replace( } else { $new_types[] = new TMixed(); } + } elseif ($atomic_type instanceof TTemplateKeyOf) { + $template_type = isset($inferred_lower_bounds[$atomic_type->param_name][$atomic_type->defining_class]) + ? clone TemplateStandinTypeReplacer::getMostSpecificTypeFromBounds( + $inferred_lower_bounds[$atomic_type->param_name][$atomic_type->defining_class], + $codebase + ) + : null; + + if ($template_type) { + $template_type = $template_type->getSingleAtomic(); + if (TKeyOfArray::isViableTemplateType($template_type)) { + $keys_to_unset[] = $key; + $new_types[] = new TKeyOfArray(clone $template_type); + } + } } elseif ($atomic_type instanceof TConditional && $codebase ) { diff --git a/src/Psalm/Internal/Type/TypeExpander.php b/src/Psalm/Internal/Type/TypeExpander.php index 97c1a8854a8..5b6827738f1 100644 --- a/src/Psalm/Internal/Type/TypeExpander.php +++ b/src/Psalm/Internal/Type/TypeExpander.php @@ -18,7 +18,7 @@ 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\TObjectWithProperties; use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Atomic\TTypeAlias; -use Psalm\Type\Atomic\TValueOfClassConstant; +use Psalm\Type\Atomic\TValueOfArray; use Psalm\Type\Atomic\TVoid; use Psalm\Type\Union; use ReflectionProperty; @@ -350,52 +350,16 @@ public static function expandAtomic( return [$return_type]; } - if ($return_type instanceof TKeyOfClassConstant - || $return_type instanceof TValueOfClassConstant + if ($return_type instanceof TKeyOfArray + || $return_type instanceof TValueOfArray ) { - if ($return_type->fq_classlike_name === 'self' && $self_class) { - $return_type->fq_classlike_name = $self_class; - } - - if ($evaluate_class_constants) { - if ($throw_on_unresolvable_constant - && !$codebase->classOrInterfaceExists($return_type->fq_classlike_name) - ) { - throw new UnresolvableConstantException($return_type->fq_classlike_name, $return_type->const_name); - } - - try { - $class_constant_type = $codebase->classlikes->getClassConstantType( - $return_type->fq_classlike_name, - $return_type->const_name, - ReflectionProperty::IS_PRIVATE - ); - } catch (CircularReferenceException $e) { - $class_constant_type = null; - } - - if ($class_constant_type) { - foreach ($class_constant_type->getAtomicTypes() as $const_type_atomic) { - if ($const_type_atomic instanceof TKeyedArray - || $const_type_atomic instanceof TArray - ) { - if ($const_type_atomic instanceof TKeyedArray) { - $const_type_atomic = $const_type_atomic->getGenericArrayType(); - } - - if ($return_type instanceof TKeyOfClassConstant) { - return array_values($const_type_atomic->type_params[0]->getAtomicTypes()); - } - - return array_values($const_type_atomic->type_params[1]->getAtomicTypes()); - } - } - } elseif ($throw_on_unresolvable_constant) { - throw new UnresolvableConstantException($return_type->fq_classlike_name, $return_type->const_name); - } - } - - return [$return_type]; + return self::expandKeyOfValueOfArray( + $codebase, + $return_type, + $self_class, + $evaluate_class_constants, + $throw_on_unresolvable_constant + ); } if ($return_type instanceof TIntMask) { @@ -911,4 +875,92 @@ private static function expandConditional( return [$return_type]; } + + /** + * @param TKeyOfArray|TValueOfArray $return_type + * @return non-empty-list + */ + private static function expandKeyOfValueOfArray( + Codebase $codebase, + $return_type, + ?string $self_class, + bool $evaluate_class_constants, + bool $throw_on_unresolvable_constant + ): array { + $type_param = $return_type->type; + + if ($evaluate_class_constants && $type_param instanceof TClassConstant) { + if ($type_param->fq_classlike_name === 'self' && $self_class) { + $type_param->fq_classlike_name = $self_class; + } + + if ($throw_on_unresolvable_constant + && !$codebase->classOrInterfaceExists($type_param->fq_classlike_name) + ) { + throw new UnresolvableConstantException($type_param->fq_classlike_name, $type_param->const_name); + } + + try { + $type_param = $codebase->classlikes->getClassConstantType( + $type_param->fq_classlike_name, + $type_param->const_name, + ReflectionProperty::IS_PRIVATE + ); + } catch (CircularReferenceException $e) { + return [$return_type]; + } + if (!$type_param) { + if ($throw_on_unresolvable_constant) { + throw new UnresolvableConstantException($return_type->type->fq_classlike_name, $return_type->type->const_name); + } else { + return [$return_type]; + } + } + } + + if (!$type_param instanceof Union) { + $type_param = new Union([$type_param]); + } + + // Merge keys/values of provided array types + $new_return_types = []; + foreach ($type_param->getAtomicTypes() as $type_atomic) { + // Abort if any type of the param's union is invalid + if (!$type_atomic instanceof TKeyedArray + && !$type_atomic instanceof TArray + && !$type_atomic instanceof TList + ) { + break; + } + + // Transform all types to TArray if needed + if ($type_atomic instanceof TList) { + $type_atomic = new TArray([ + new Union([new TInt()]), + $type_atomic->type_param + ]); + } + if ($type_atomic instanceof TKeyedArray) { + $type_atomic = $type_atomic->getGenericArrayType(); + } + + // Add key-of/value-of type to return types list + if ($return_type instanceof TKeyOfArray) { + $new_return_types = array_merge( + $new_return_types, + array_values($type_atomic->type_params[0]->getAtomicTypes()) + ) ; + } else { + $new_return_types = array_merge( + $new_return_types, + array_values($type_atomic->type_params[1]->getAtomicTypes()) + ) ; + } + } + + if (empty($new_return_types)) { + return [$return_type]; + } + return $new_return_types; + } } diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index 9e2fc4dba8b..3740c904c1e 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -41,7 +41,7 @@ use Psalm\Type\Atomic\TIntMaskOf; use Psalm\Type\Atomic\TIntRange; use Psalm\Type\Atomic\TIterable; -use Psalm\Type\Atomic\TKeyOfClassConstant; +use Psalm\Type\Atomic\TKeyOfArray; use Psalm\Type\Atomic\TKeyedArray; use Psalm\Type\Atomic\TList; use Psalm\Type\Atomic\TLiteralClassString; @@ -60,7 +60,7 @@ use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Atomic\TTemplateParamClass; use Psalm\Type\Atomic\TTypeAlias; -use Psalm\Type\Atomic\TValueOfClassConstant; +use Psalm\Type\Atomic\TValueOfArray; use Psalm\Type\TypeNode; use Psalm\Type\Union; @@ -692,7 +692,7 @@ private static function getTypeFromGenericTree( return new TTemplateKeyOf( $param_name, $defining_class, - $template_type_map[$param_name][$defining_class] + $generic_params[0] ); } @@ -702,16 +702,13 @@ private static function getTypeFromGenericTree( throw new TypeParseTreeException('Union types are not allowed in key-of type'); } - if (!$param_union_types[0] instanceof TClassConstant) { + if (!TKeyOfArray::isViableTemplateType($param_union_types[0])) { throw new TypeParseTreeException( - 'Untemplated key-of param ' . $param_name . ' should be a class constant' + 'Untemplated key-of param ' . $param_name . ' should be a class constant or an array' ); } - return new TKeyOfClassConstant( - $param_union_types[0]->fq_classlike_name, - $param_union_types[0]->const_name - ); + return new TKeyOfArray($param_union_types[0]); } if ($generic_type_value === 'value-of') { @@ -723,16 +720,16 @@ private static function getTypeFromGenericTree( throw new TypeParseTreeException('Union types are not allowed in value-of type'); } - if (!$param_union_types[0] instanceof TClassConstant) { + if (!$param_union_types[0] instanceof TArray + && !$param_union_types[0] instanceof TList + && !$param_union_types[0] instanceof TKeyedArray + && !$param_union_types[0] instanceof TClassConstant) { throw new TypeParseTreeException( - 'Untemplated value-of param ' . $param_name . ' should be a class constant' + 'Untemplated value-of param ' . $param_name . ' should be a class constant or an array' ); } - return new TValueOfClassConstant( - $param_union_types[0]->fq_classlike_name, - $param_union_types[0]->const_name - ); + return new TValueOfArray($param_union_types[0]); } if ($generic_type_value === 'int-mask') { @@ -803,8 +800,8 @@ private static function getTypeFromGenericTree( $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' diff --git a/src/Psalm/Type/Atomic/TIntMaskOf.php b/src/Psalm/Type/Atomic/TIntMaskOf.php index ec71060ff78..86cb53b83ef 100644 --- a/src/Psalm/Type/Atomic/TIntMaskOf.php +++ b/src/Psalm/Type/Atomic/TIntMaskOf.php @@ -11,11 +11,11 @@ */ 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) { diff --git a/src/Psalm/Type/Atomic/TKeyOfArray.php b/src/Psalm/Type/Atomic/TKeyOfArray.php new file mode 100644 index 00000000000..93cd98de1c6 --- /dev/null +++ b/src/Psalm/Type/Atomic/TKeyOfArray.php @@ -0,0 +1,62 @@ +type = $type; + } + + public function getKey(bool $include_extra = true): string + { + return 'key-of<' . $this->type . '>'; + } + + /** + * @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; + } + + public function getAssertionString(): string + { + return 'mixed'; + } + + /** + * @psalm-assert-if-true ArrayLikeTemplateType $template_type + */ + public static function isViableTemplateType(Atomic $template_type): bool + { + return $template_type instanceof TArray + || $template_type instanceof TClassConstant + || $template_type instanceof TKeyedArray + || $template_type instanceof TList; + } +} diff --git a/src/Psalm/Type/Atomic/TKeyOfClassConstant.php b/src/Psalm/Type/Atomic/TKeyOfClassConstant.php deleted file mode 100644 index 5834db3e25c..00000000000 --- a/src/Psalm/Type/Atomic/TKeyOfClassConstant.php +++ /dev/null @@ -1,94 +0,0 @@ -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 $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 $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-ofconst_name . '>'; - } - - if ($this->fq_classlike_name === $this_class) { - return 'key-ofconst_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'; - } -} diff --git a/src/Psalm/Type/Atomic/TTemplateKeyOf.php b/src/Psalm/Type/Atomic/TTemplateKeyOf.php index 8aaed4356d7..1352d8721c9 100644 --- a/src/Psalm/Type/Atomic/TTemplateKeyOf.php +++ b/src/Psalm/Type/Atomic/TTemplateKeyOf.php @@ -5,7 +5,7 @@ use Psalm\Type\Union; /** - * Represents the type used when using TKeyOfClassConstant when the type of the class constant array is a template + * Represents the type used when using TKeyOfArray when the type of the class constant array is a template */ class TTemplateKeyOf extends TArrayKey { diff --git a/src/Psalm/Type/Atomic/TValueOfArray.php b/src/Psalm/Type/Atomic/TValueOfArray.php new file mode 100644 index 00000000000..ad76824e2c2 --- /dev/null +++ b/src/Psalm/Type/Atomic/TValueOfArray.php @@ -0,0 +1,49 @@ +type = $type; + } + + public function getKey(bool $include_extra = true): string + { + return 'value-of<' . $this->type . '>'; + } + + /** + * @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; + } + + public function getAssertionString(): string + { + return 'mixed'; + } +} diff --git a/src/Psalm/Type/Atomic/TValueOfClassConstant.php b/src/Psalm/Type/Atomic/TValueOfClassConstant.php deleted file mode 100644 index 6f4afc9c5e0..00000000000 --- a/src/Psalm/Type/Atomic/TValueOfClassConstant.php +++ /dev/null @@ -1,69 +0,0 @@ -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 $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 $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-ofconst_name . '>'; - } - - return 'value-of<' - . Type::getStringFromFQCLN($this->fq_classlike_name, $namespace, $aliased_classes, $this_class) - . '>::' . $this->const_name . '>'; - } - - public function getAssertionString(): string - { - return 'mixed'; - } -} diff --git a/stubs/CoreGenericFunctions.phpstub b/stubs/CoreGenericFunctions.phpstub index 24352529233..94fa149bd9e 100644 --- a/stubs/CoreGenericFunctions.phpstub +++ b/stubs/CoreGenericFunctions.phpstub @@ -1,13 +1,12 @@ + * @psalm-template TArray as array * * @param TArray $array * @param mixed $search_value * @param bool $strict * - * @return (TArray is non-empty-array ? non-empty-list : list) + * @return (TArray is non-empty-array ? non-empty-list> : list>) * @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 + * @psalm-template TArray as array * * @param TArray $array * - * @return (TArray is array ? null : (TArray is non-empty-array ? TKey : TKey|null)) + * @return (TArray is array ? null : (TArray is non-empty-array ? key-of : key-of|null)) * @psalm-pure */ function array_key_first($array) @@ -158,12 +156,11 @@ function array_key_first($array) } /** - * @psalm-template TKey as array-key - * @psalm-template TArray as array + * @psalm-template TArray as array * * @param TArray $array * - * @return (TArray is array ? null : (TArray is non-empty-array ? TKey : TKey|null)) + * @return (TArray is array ? null : (TArray is non-empty-array ? key-of : key-of|null)) * @psalm-pure */ function array_key_last($array) diff --git a/tests/ArrayKeysTest.php b/tests/ArrayKeysTest.php new file mode 100644 index 00000000000..39545743d95 --- /dev/null +++ b/tests/ArrayKeysTest.php @@ -0,0 +1,137 @@ +,ignored_issues?:list,php_version?:string}> + */ + public function providerValidCodeParse(): iterable + { + return [ + 'arrayKeysOfEmptyArrayReturnsListOfEmpty' => [ + 'code' => ' [ + '$keys' => 'list', + ], + ], + 'arrayKeysOfKeyedArrayReturnsNonEmptyListOfStrings' => [ + 'code' => ' \'bar\']); + ', + 'assertions' => [ + '$keys' => 'non-empty-list', + ], + ], + 'arrayKeysOfListReturnsNonEmptyListOfInts' => [ + 'code' => ' [ + '$keys' => 'non-empty-list', + ], + ], + 'arrayKeysOfKeyedStringIntArrayReturnsNonEmptyListOfIntsOrStrings' => [ + 'code' => ' \'bar\', 42]); + ', + 'assertions' => [ + '$keys' => 'non-empty-list', + ], + ], + 'arrayKeysOfArrayConformsToArrayKeys' => [ + 'code' => ' + */ + function getKeys(array $array) { + return array_keys($array); + } + ' + ], + 'arrayKeysOfKeyedArrayConformsToCorrectLiteralStringList' => [ + 'code' => ' + */ + function getKeys() { + return array_keys([\'foo\' => 42, \'bar\' => 42]); + } + ' + ], + 'arrayKeysOfLiteralListConformsToCorrectLiteralOffsets' => [ + 'code' => ' + */ + function getKeys() { + return array_keys([\'foo\', \'bar\']); + } + ' + ], + 'arrayKeyFirstOfLiteralListConformsToCorrectLiteralOffsets' => [ + 'code' => ' [ + 'code' => ',php_version?:string}> + */ + public function providerInvalidCodeParse(): iterable + { + return [ + 'arrayKeysOfStringArrayDoesntConformsToIntList' => [ + 'code' => ' $array + * @return list + */ + function getKeys(array $array) { + return array_keys($array); + } + ', + 'error_message' => 'InvalidReturnStatement' + ], + 'arrayKeysOfStringKeyedArrayDoesntConformToIntList' => [ + 'code' => ' + */ + function getKeys() { + return array_keys([\'foo\' => 42, \'bar\' => 42]); + } + ', + 'error_message' => 'InvalidReturnStatement' + ] + ]; + } +} diff --git a/tests/KeyOfArrayTest.php b/tests/KeyOfArrayTest.php new file mode 100644 index 00000000000..3893e9e495f --- /dev/null +++ b/tests/KeyOfArrayTest.php @@ -0,0 +1,189 @@ +,ignored_issues?:list,php_version?:string}> + */ + public function providerValidCodeParse(): iterable + { + return [ + 'keyOfListClassConstant' => [ + 'code' => ' */ + public function getKey() { + return 0; + } + } + ' + ], + 'keyOfAssociativeArrayClassConstant' => [ + 'code' => ' 42 + ]; + /** @return key-of */ + public function getKey() { + return \'bar\'; + } + } + ' + ], + 'allKeysOfAssociativeArrayPossible' => [ + 'code' => ' 42, + \'adams\' => 43, + ]; + /** @return key-of */ + public function getKey(bool $adams) { + if ($adams) { + return \'adams\'; + } + return \'bar\'; + } + } + ' + ], + 'keyOfAsArray' => [ + 'code' => ' 42, + \'adams\' => 43, + ]; + /** @return key-of[] */ + public function getKey(bool $adams) { + return array_keys(self::FOO); + } + } + ' + ], + 'keyOfArrayLiteral' => [ + 'code' => '> + */ + function getKey() { + return 32; + } + ' + ], + 'keyOfUnionArrayLiteral' => [ + 'code' => '|array> + */ + function getKey(bool $asFloat) { + if ($asFloat) { + return 42.0; + } + return 42; + } + ' + ], + 'keyOfListArrayLiteral' => [ + 'code' => '> + */ + function getKey() { + return 42; + } + ' + ], + 'keyOfStringArrayConformsToString' => [ + 'code' => '>[] */ + $keys2 = [\'foo\']; + return $keys2[0]; + } + ' + ], + ]; + } + + /** + * @return iterable,php_version?:string}> + */ + public function providerInvalidCodeParse(): iterable + { + return [ + 'onlyDefinedKeysOfAssociativeArray' => [ + 'code' => ' 42 + ]; + /** @return key-of */ + public function getKey(bool $adams) { + return \'adams\'; + } + } + ', + 'error_message' => 'InvalidReturnStatement' + ], + 'keyOfArrayLiteral' => [ + 'code' => '> + */ + public function getKey() { + return \'foo\'; + } + } + ', + 'error_message' => 'InvalidReturnStatement' + ], + 'onlyIntAllowedForKeyOfList' => [ + 'code' => '> + */ + public function getKey() { + return \'42\'; + } + } + ', + 'error_message' => 'InvalidReturnStatement' + ], + 'noStringAllowedInKeyOfIntFloatArray' => [ + 'code' => '|array> + */ + function getKey(bool $asFloat) { + if ($asFloat) { + return 42.0; + } + return \'42\'; + } + ', + 'error_message' => 'InvalidReturnStatement' + ], + ]; + } +} diff --git a/tests/Template/KeyOfTemplateTest.php b/tests/Template/KeyOfTemplateTest.php new file mode 100644 index 00000000000..b856ff1dab0 --- /dev/null +++ b/tests/Template/KeyOfTemplateTest.php @@ -0,0 +1,110 @@ +,ignored_issues?:list,php_version?:string}> + */ + public function providerValidCodeParse(): iterable + { + return [ + 'acceptsArrayKeysFn' => [ + 'code' => '[] + */ + function getKey($array) { + return array_keys($array); + } + ' + ], + 'acceptsArrayKeyFirstFn' => [ + 'code' => '|null + */ + function getKey($array) { + return array_key_first($array); + } + ' + ], + 'acceptsArrayKeyLastFn' => [ + 'code' => '|null + */ + function getKey($array) { + return array_key_last($array); + } + ' + ], + // Currently not works! + // 'acceptsIfArrayKeyExistsFn' => [ + // 'code' => '|null + // */ + // function getKey(string $key, $array) { + // if (array_key_exists($key, $array)) { + // return $key; + // } + // return null; + // } + // ' + // ], + ]; + } + + /** + * @return iterable,php_version?:string}> + */ + public function providerInvalidCodeParse(): iterable + { + return [ + 'keyOfTemplateNotIncludesString' => [ + 'code' => ' + */ + function getKey($array) { + return \'foo\'; + } + ', + 'error_message' => 'InvalidReturnStatement' + ], + 'keyOfTemplateNotIncludesInt' => [ + 'code' => ' + */ + function getKey($array) { + return 0; + } + ', + 'error_message' => 'InvalidReturnStatement' + ], + ]; + } +} From 1f28d025c388b421b9bf7c665e8a0cc878590fe1 Mon Sep 17 00:00:00 2001 From: Patrick Remy Date: Thu, 20 Jan 2022 22:41:33 +0100 Subject: [PATCH 02/11] feat: allow unions for key-of/value-of Add tests for TValueOfArray. --- .../Type/Comparator/ScalarTypeComparator.php | 3 +- .../Type/TemplateInferredTypeReplacer.php | 1 - src/Psalm/Internal/Type/TypeExpander.php | 52 +++-- src/Psalm/Internal/Type/TypeParser.php | 23 +- src/Psalm/Type/Atomic/TKeyOfArray.php | 29 ++- src/Psalm/Type/Atomic/TValueOfArray.php | 22 +- tests/ArrayKeysTest.php | 18 +- tests/KeyOfArrayTest.php | 58 +++-- tests/Template/KeyOfTemplateTest.php | 33 ++- tests/ValueOfArrayTest.php | 200 ++++++++++++++++++ 10 files changed, 334 insertions(+), 105 deletions(-) create mode 100644 tests/ValueOfArrayTest.php diff --git a/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php b/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php index e96b7d55917..3e87325cf82 100644 --- a/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php @@ -295,8 +295,7 @@ public static function isContainedBy( 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; } diff --git a/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php index 956d1861ce0..40f6130ee54 100644 --- a/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php @@ -239,7 +239,6 @@ public static function replace( : 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); diff --git a/src/Psalm/Internal/Type/TypeExpander.php b/src/Psalm/Internal/Type/TypeExpander.php index 5b6827738f1..e80b2a39476 100644 --- a/src/Psalm/Internal/Type/TypeExpander.php +++ b/src/Psalm/Internal/Type/TypeExpander.php @@ -887,9 +887,14 @@ private static function expandKeyOfValueOfArray( 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 @@ private static function expandKeyOfValueOfArray( 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 @@ private static function expandKeyOfValueOfArray( } 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]; } } - } - if (!$type_param instanceof Union) { - $type_param = new Union([$type_param]); + $type_atomics = array_merge( + $type_atomics, + $constant_type->getAtomicTypes() + ); } + // Types inside union are checked on instantiation and above on + // expansion, currently there is no Union for typing here. + /** @var list $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 @@ private static function expandKeyOfValueOfArray( } } - if (empty($new_return_types)) { + if ($new_return_types === []) { return [$return_type]; } return $new_return_types; diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index 3740c904c1e..a8116a9bdd2 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -696,40 +696,25 @@ private static function getTypeFromGenericTree( ); } - $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') { diff --git a/src/Psalm/Type/Atomic/TKeyOfArray.php b/src/Psalm/Type/Atomic/TKeyOfArray.php index 93cd98de1c6..b4f59c70a12 100644 --- a/src/Psalm/Type/Atomic/TKeyOfArray.php +++ b/src/Psalm/Type/Atomic/TKeyOfArray.php @@ -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 @@ public function getAssertionString(): string 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; } } diff --git a/src/Psalm/Type/Atomic/TValueOfArray.php b/src/Psalm/Type/Atomic/TValueOfArray.php index ad76824e2c2..d8f3480ce56 100644 --- a/src/Psalm/Type/Atomic/TValueOfArray.php +++ b/src/Psalm/Type/Atomic/TValueOfArray.php @@ -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 @@ 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; + } } diff --git a/tests/ArrayKeysTest.php b/tests/ArrayKeysTest.php index 39545743d95..0e2185e4c36 100644 --- a/tests/ArrayKeysTest.php +++ b/tests/ArrayKeysTest.php @@ -28,7 +28,7 @@ public function providerValidCodeParse(): iterable ], 'arrayKeysOfKeyedArrayReturnsNonEmptyListOfStrings' => [ 'code' => ' \'bar\']); + $keys = array_keys(["foo" => "bar"]); ', 'assertions' => [ '$keys' => 'non-empty-list', @@ -36,7 +36,7 @@ public function providerValidCodeParse(): iterable ], 'arrayKeysOfListReturnsNonEmptyListOfInts' => [ 'code' => ' [ '$keys' => 'non-empty-list', @@ -44,7 +44,7 @@ public function providerValidCodeParse(): iterable ], 'arrayKeysOfKeyedStringIntArrayReturnsNonEmptyListOfIntsOrStrings' => [ 'code' => ' \'bar\', 42]); + $keys = array_keys(["foo" => "bar", 42]); ', 'assertions' => [ '$keys' => 'non-empty-list', @@ -63,10 +63,10 @@ function getKeys(array $array) { 'arrayKeysOfKeyedArrayConformsToCorrectLiteralStringList' => [ 'code' => ' + * @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 @@ function getKeys() { * @return non-empty-list<0|1> */ function getKeys() { - return array_keys([\'foo\', \'bar\']); + return array_keys(["foo", "bar"]); } ' ], @@ -86,7 +86,7 @@ function getKeys() { * @return 0|1 */ function getKey() { - return array_key_first([\'foo\', \'bar\']); + return array_key_first(["foo", "bar"]); } ' ], @@ -96,7 +96,7 @@ function getKey() { * @return 0|1 */ function getKey() { - return array_key_last([\'foo\', \'bar\']); + return array_key_last(["foo", "bar"]); } ' ], @@ -127,7 +127,7 @@ function getKeys(array $array) { * @return list */ function getKeys() { - return array_keys([\'foo\' => 42, \'bar\' => 42]); + return array_keys(["foo" => 42, "bar" => 42]); } ', 'error_message' => 'InvalidReturnStatement' diff --git a/tests/KeyOfArrayTest.php b/tests/KeyOfArrayTest.php index 3893e9e495f..e69f940551f 100644 --- a/tests/KeyOfArrayTest.php +++ b/tests/KeyOfArrayTest.php @@ -22,7 +22,7 @@ public function providerValidCodeParse(): iterable 'code' => ' */ public function getKey() { @@ -35,11 +35,11 @@ public function getKey() { 'code' => ' 42 + "bar" => 42 ]; /** @return key-of */ public function getKey() { - return \'bar\'; + return "bar"; } } ' @@ -48,15 +48,15 @@ public function getKey() { 'code' => ' 42, - \'adams\' => 43, + "bar" => 42, + "adams" => 43, ]; /** @return key-of */ public function getKey(bool $adams) { if ($adams) { - return \'adams\'; + return "adams"; } - return \'bar\'; + return "bar"; } } ' @@ -66,11 +66,11 @@ public function getKey(bool $adams) { class A { /** @var array */ const FOO = [ - \'bar\' => 42, - \'adams\' => 43, + "bar" => 42, + "adams" => 43, ]; /** @return key-of[] */ - public function getKey(bool $adams) { + public function getKey() { return array_keys(self::FOO); } } @@ -99,6 +99,19 @@ function getKey(bool $asFloat) { } ' ], + 'keyOfUnionListAndKeyedArray' => [ + 'code' => '|array{a: int, b: int}> + */ + function getKey(bool $asInt) { + if ($asInt) { + return 42; + } + return "a"; + } + ', + ], 'keyOfListArrayLiteral' => [ 'code' => '>[] */ - $keys2 = [\'foo\']; + $keys2 = ["foo"]; return $keys2[0]; } ' @@ -134,11 +147,11 @@ public function providerInvalidCodeParse(): iterable 'code' => ' 42 + "bar" => 42 ]; /** @return key-of */ - public function getKey(bool $adams) { - return \'adams\'; + public function getKey() { + return "adams"; } } ', @@ -151,7 +164,7 @@ class A { * @return key-of> */ public function getKey() { - return \'foo\'; + return "foo"; } } ', @@ -164,7 +177,7 @@ class A { * @return key-of> */ public function getKey() { - return \'42\'; + return "42"; } } ', @@ -179,7 +192,18 @@ function getKey(bool $asFloat) { if ($asFloat) { return 42.0; } - return \'42\'; + return "42"; + } + ', + 'error_message' => 'InvalidReturnStatement' + ], + 'noLiteralCAllowedInKeyOfUnionListAndKeyedArray' => [ + 'code' => '|array{a: int, b: int}> + */ + function getKey() { + return "c"; } ', 'error_message' => 'InvalidReturnStatement' diff --git a/tests/Template/KeyOfTemplateTest.php b/tests/Template/KeyOfTemplateTest.php index b856ff1dab0..9aec719e4d7 100644 --- a/tests/Template/KeyOfTemplateTest.php +++ b/tests/Template/KeyOfTemplateTest.php @@ -54,22 +54,21 @@ function getKey($array) { } ' ], - // Currently not works! - // 'acceptsIfArrayKeyExistsFn' => [ - // 'code' => '|null - // */ - // function getKey(string $key, $array) { - // if (array_key_exists($key, $array)) { - // return $key; - // } - // return null; - // } - // ' - // ], + 'SKIP-acceptsIfArrayKeyExistsFn' => [ + 'code' => '|null + */ + function getKey(string $key, $array) { + if (array_key_exists($key, $array)) { + return $key; + } + return null; + } + ' + ], ]; } @@ -87,7 +86,7 @@ public function providerInvalidCodeParse(): iterable * @return key-of */ function getKey($array) { - return \'foo\'; + return "foo"; } ', 'error_message' => 'InvalidReturnStatement' diff --git a/tests/ValueOfArrayTest.php b/tests/ValueOfArrayTest.php new file mode 100644 index 00000000000..e04c70569c8 --- /dev/null +++ b/tests/ValueOfArrayTest.php @@ -0,0 +1,200 @@ +,ignored_issues?:list,php_version?:string}> + */ + public function providerValidCodeParse(): iterable + { + return [ + 'valueOfListClassConstant' => [ + 'code' => ' */ + public function getKey() { + return "bar"; + } + } + ' + ], + 'valueOfAssociativeArrayClassConstant' => [ + 'code' => ' 42 + ]; + /** @return value-of */ + public function getValue() { + return 42; + } + } + ' + ], + 'allValuesOfAssociativeArrayPossible' => [ + 'code' => ' 42, + "adams" => 43, + ]; + /** @return value-of */ + public function getValue(bool $adams) { + if ($adams) { + return 42; + } + return 43; + } + } + ' + ], + 'valueOfAsArray' => [ + 'code' => ' 42, + "adams" => 43, + ]; + /** @return value-of[] */ + public function getValues() { + return array_values(self::FOO); + } + } + ' + ], + 'valueOfArrayLiteral' => [ + 'code' => '> + */ + function getKey() { + return "42"; + } + ' + ], + 'valueOfUnionArrayLiteral' => [ + 'code' => '|array> + */ + function getValue(bool $asFloat) { + if ($asFloat) { + return 42.0; + } + return 42; + } + ' + ], + 'valueOfStringArrayConformsToString' => [ + 'code' => '>[] */ + $keys2 = ["foo"]; + return $keys2[0]; + } + ' + ], + 'acceptLiteralIntInValueOfUnionLiteralInts' => [ + 'code' => '|array{0: 3, 1: 4}> + */ + function getValue(int $i) { + if ($i >= 0 && $i <= 4) { + return $i; + } + return 0; + } + ', + ], + ]; + } + + /** + * @return iterable,php_version?:string}> + */ + public function providerInvalidCodeParse(): iterable + { + return [ + 'onlyDefinedValuesOfConstantList' => [ + 'code' => ' */ + public function getValue() { + return "adams"; + } + } + ', + 'error_message' => 'InvalidReturnStatement' + ], + 'noIntForValueOfStringArrayLiteral' => [ + 'code' => '> + */ + public function getValue() { + return 42; + } + } + ', + 'error_message' => 'InvalidReturnStatement' + ], + 'noStringForValueOfIntList' => [ + 'code' => '> + */ + public function getValue() { + return "42"; + } + } + ', + 'error_message' => 'InvalidReturnStatement' + ], + 'noOtherStringAllowedForValueOfKeyedArray' => [ + 'code' => ' + */ + function getValue() { + return "adams"; + } + ', + 'error_message' => 'InvalidReturnStatement' + ], + 'noOtherIntAllowedInValueOfUnionLiteralInts' => [ + 'code' => '|array{0: 3, 1: 4}> + */ + function getValue() { + return 5; + } + ', + 'error_message' => 'InvalidReturnStatement' + ], + ]; + } +} From ed87465f717e771ecf567176bfb9bf6cbc6b82aa Mon Sep 17 00:00:00 2001 From: Patrick Remy Date: Thu, 20 Jan 2022 22:47:32 +0100 Subject: [PATCH 03/11] tests: correct 'SKIPPED' prefix for skipped test --- tests/Template/KeyOfTemplateTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Template/KeyOfTemplateTest.php b/tests/Template/KeyOfTemplateTest.php index 9aec719e4d7..38f81fede1f 100644 --- a/tests/Template/KeyOfTemplateTest.php +++ b/tests/Template/KeyOfTemplateTest.php @@ -54,7 +54,7 @@ function getKey($array) { } ' ], - 'SKIP-acceptsIfArrayKeyExistsFn' => [ + 'SKIPPED-acceptsIfArrayKeyExistsFn' => [ 'code' => ' Date: Thu, 20 Jan 2022 22:56:34 +0100 Subject: [PATCH 04/11] style: add use statement for `array_push` --- src/Psalm/Internal/Type/TypeExpander.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Psalm/Internal/Type/TypeExpander.php b/src/Psalm/Internal/Type/TypeExpander.php index e80b2a39476..693a9722557 100644 --- a/src/Psalm/Internal/Type/TypeExpander.php +++ b/src/Psalm/Internal/Type/TypeExpander.php @@ -38,6 +38,7 @@ 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; From ce1feb1ebffb955ff063d2c8e546b1777d0b5708 Mon Sep 17 00:00:00 2001 From: Patrick Remy Date: Thu, 20 Jan 2022 23:06:20 +0100 Subject: [PATCH 05/11] style: remove unused variable declaration --- src/Psalm/Internal/Type/TypeExpander.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Psalm/Internal/Type/TypeExpander.php b/src/Psalm/Internal/Type/TypeExpander.php index 693a9722557..3b371382456 100644 --- a/src/Psalm/Internal/Type/TypeExpander.php +++ b/src/Psalm/Internal/Type/TypeExpander.php @@ -906,7 +906,6 @@ private static function expandKeyOfValueOfArray( throw new UnresolvableConstantException($type_param->fq_classlike_name, $type_param->const_name); } - $constant_type = null; try { $constant_type = $codebase->classlikes->getClassConstantType( $type_param->fq_classlike_name, From dff8869685bc7688e1caa142f61d5cef210da506 Mon Sep 17 00:00:00 2001 From: Patrick Remy Date: Thu, 20 Jan 2022 23:09:48 +0100 Subject: [PATCH 06/11] style: remove unused use statement --- src/Psalm/Type/Atomic/TKeyOfArray.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Psalm/Type/Atomic/TKeyOfArray.php b/src/Psalm/Type/Atomic/TKeyOfArray.php index b4f59c70a12..2f035081d22 100644 --- a/src/Psalm/Type/Atomic/TKeyOfArray.php +++ b/src/Psalm/Type/Atomic/TKeyOfArray.php @@ -2,7 +2,6 @@ namespace Psalm\Type\Atomic; -use Psalm\Type\Atomic; use Psalm\Type\Union; /** From 8cd5ccd076f1cc1ea4c86c5080fea5b492d32a08 Mon Sep 17 00:00:00 2001 From: Patrick Remy Date: Tue, 25 Jan 2022 22:08:44 +0100 Subject: [PATCH 07/11] feat: make `value-of` capable for template types --- .../plugins/plugins_type_system.md | 4 +- .../Provider/FunctionReturnTypeProvider.php | 2 - .../ArrayValuesReturnTypeProvider.php | 91 ------------------- .../Type/Comparator/AtomicTypeComparator.php | 43 +++++++++ .../Type/Comparator/ScalarTypeComparator.php | 36 +++++--- .../Type/TemplateInferredTypeReplacer.php | 59 +++++++++--- .../Type/TemplateStandinTypeReplacer.php | 71 +++++++++------ src/Psalm/Internal/Type/TypeParser.php | 22 ++++- src/Psalm/Type/Atomic/TTemplateKeyOf.php | 2 +- src/Psalm/Type/Atomic/TTemplateValueOf.php | 80 ++++++++++++++++ stubs/CoreGenericFunctions.phpstub | 12 +++ tests/Template/ValueOfTemplateTest.php | 85 +++++++++++++++++ tests/TypeParseTest.php | 8 ++ 13 files changed, 361 insertions(+), 154 deletions(-) delete mode 100644 src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayValuesReturnTypeProvider.php create mode 100644 src/Psalm/Type/Atomic/TTemplateValueOf.php create mode 100644 tests/Template/ValueOfTemplateTest.php diff --git a/docs/running_psalm/plugins/plugins_type_system.md b/docs/running_psalm/plugins/plugins_type_system.md index 7676bc2f398..0589309d30d 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 f23167ce9de..277996aa340 100644 --- a/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php @@ -21,7 +21,6 @@ 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 @@ public function __construct() $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 3ec26808007..00000000000 --- 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 4d0f1e5f80d..87a9dc5d66c 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\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 @@ public static function isContainedBy( 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 3e87325cf82..f556af6e940 100644 --- a/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php @@ -4,6 +4,7 @@ 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\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 @@ public static function isContainedBy( 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 40f6130ee54..925b79b6bfd 100644 --- a/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php @@ -6,6 +6,7 @@ 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\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 @@ public static function replace( } 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 @@ public static function replace( )->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 9d86881d75f..926d85a0b5c 100644 --- a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php @@ -29,6 +29,7 @@ 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 @@ private static function handleAtomicStandin( return [$atomic_type]; } - if ($atomic_type instanceof TTemplateKeyOf) { - if ($replace) { - $atomic_types = []; - - $include_first = true; + if ($atomic_type instanceof TTemplateKeyOf + || $atomic_type instanceof TTemplateValueOf) { + if (!$replace) { + return [$atomic_type]; + } - 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]; + $atomic_types = []; - if ($template_type->isSingle()) { - $template_type = $template_type->getSingleAtomic(); + $include_first = true; - 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]; - } + 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]; - $include_first = false; + foreach ($template_type->getAtomicTypes() as $template_atomic) { + if (!$template_atomic instanceof TKeyedArray + && !$template_atomic instanceof TArray + && !$template_atomic instanceof TList + ) { + return [$atomic_type]; + } - foreach ($key_type->getAtomicTypes() as $key_atomic_type) { - $atomic_types[] = $key_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]; } } - } - if ($include_first) { - $atomic_types[] = $atomic_type; + $include_first = false; + + foreach ($template_atomic->getAtomicTypes() as $key_atomic_type) { + $atomic_types[] = $key_atomic_type; + } } + } - return $atomic_types; + if ($include_first) { + $atomic_types[] = $atomic_type; } - return [$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 a8116a9bdd2..cb3ec407715 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -59,12 +59,14 @@ 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 @@ private static function getTypeFromGenericTree( 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 @@ private static function getTypeFromGenericTree( 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 @@ private static function getTypeFromGenericTree( 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 1352d8721c9..f8abbfcce98 100644 --- a/src/Psalm/Type/Atomic/TTemplateKeyOf.php +++ b/src/Psalm/Type/Atomic/TTemplateKeyOf.php @@ -5,7 +5,7 @@ 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 00000000000..10b4770d9d7 --- /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 94fa149bd9e..17302b0d03e 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 00000000000..50bbb60dc07 --- /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 00186b2b0b3..36efc50a5a3 100644 --- a/tests/TypeParseTest.php +++ b/tests/TypeParseTest.php @@ -762,6 +762,14 @@ public function testKeyOfTemplate(): void ); } + public function testValueOfTemplate(): void + { + $this->assertSame( + 'value-of', + (string)Type::parseString('value-of', null, ['T' => ['' => Type::getArray()]]) + ); + } + public function testIndexedAccess(): void { $this->assertSame( From f87e429e5812e489697fad8304325b78f97a2c01 Mon Sep 17 00:00:00 2001 From: Patrick Remy Date: Thu, 27 Jan 2022 21:59:26 +0100 Subject: [PATCH 08/11] fix: infer key-of/value-of type if still templated Not in all cases the TemplateParam gets replaced before type checking, in these cases, use the defined `as` type. Refactor to extract key/value type of array union to method. --- .../Type/Comparator/AtomicTypeComparator.php | 37 +++++++--------- .../Type/Comparator/ScalarTypeComparator.php | 43 +++++++------------ src/Psalm/Internal/Type/TypeExpander.php | 43 +++++-------------- src/Psalm/Type/Atomic/TKeyOfArray.php | 36 ++++++++++++++++ src/Psalm/Type/Atomic/TValueOfArray.php | 35 +++++++++++++++ tests/Template/KeyOfTemplateTest.php | 26 +++++++++++ tests/Template/ValueOfTemplateTest.php | 26 +++++++++++ 7 files changed, 164 insertions(+), 82 deletions(-) diff --git a/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php b/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php index 87a9dc5d66c..96c0a99b4f9 100644 --- a/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php @@ -34,6 +34,7 @@ use Psalm\Type\Atomic\TString; use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Atomic\TTemplateValueOf; +use Psalm\Type\Atomic\TValueOfArray; use function array_merge; use function array_values; @@ -369,29 +370,21 @@ public static function isContainedBy( } 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(); - } + $array_value_type = TValueOfArray::getArrayValueType($input_type_part->as); + if ($array_value_type === null) { + return false; + } - 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; - } + 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; } } diff --git a/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php b/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php index f556af6e940..871eb05a1cd 100644 --- a/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php @@ -4,9 +4,7 @@ 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; use Psalm\Type\Atomic\TBool; use Psalm\Type\Atomic\TCallableString; @@ -19,8 +17,7 @@ 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\TKeyOfArray; use Psalm\Type\Atomic\TLiteralClassString; use Psalm\Type\Atomic\TLiteralFloat; use Psalm\Type\Atomic\TLiteralInt; @@ -269,30 +266,22 @@ public static function isContainedBy( } 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) { - $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(); - } + $array_key_type = TKeyOfArray::getArrayKeyType($input_type_part->as); + if ($array_key_type === null) { + return false; + } - /** @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; - } + /** @var Scalar $array_key_atomic */ + 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; } } diff --git a/src/Psalm/Internal/Type/TypeExpander.php b/src/Psalm/Internal/Type/TypeExpander.php index 3b371382456..7b6ddea2dd9 100644 --- a/src/Psalm/Internal/Type/TypeExpander.php +++ b/src/Psalm/Internal/Type/TypeExpander.php @@ -935,44 +935,21 @@ private static function expandKeyOfValueOfArray( $type_atomics = array_merge( $type_atomics, - $constant_type->getAtomicTypes() + array_values($constant_type->getAtomicTypes()) ); } - - // Types inside union are checked on instantiation and above on - // expansion, currently there is no Union for typing here. - /** @var list $type_atomics */ - - // Merge keys/values of provided array types - $new_return_types = []; - 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 - ]); - } elseif ($type_atomic instanceof TKeyedArray) { - $type_atomic = $type_atomic->getGenericArrayType(); - } - - // Add key-of/value-of type to return types list - if ($return_type instanceof TKeyOfArray) { - $new_return_types = array_merge( - $new_return_types, - array_values($type_atomic->type_params[0]->getAtomicTypes()) - ) ; - } else { - $new_return_types = array_merge( - $new_return_types, - array_values($type_atomic->type_params[1]->getAtomicTypes()) - ) ; - } + if ($type_atomics === []) { + return [$return_type]; } - if ($new_return_types === []) { + 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 $new_return_types; + return array_values($new_return_types->getAtomicTypes()); } } diff --git a/src/Psalm/Type/Atomic/TKeyOfArray.php b/src/Psalm/Type/Atomic/TKeyOfArray.php index 2f035081d22..8bc250d20f3 100644 --- a/src/Psalm/Type/Atomic/TKeyOfArray.php +++ b/src/Psalm/Type/Atomic/TKeyOfArray.php @@ -2,8 +2,12 @@ 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. */ @@ -57,4 +61,36 @@ public static function isViableTemplateType(Union $template_type): bool } return true; } + + public static function getArrayKeyType(Union $type): ?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) { + $array_key_atomics = static::getArrayKeyType($atomic_type->as); + 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); + } } diff --git a/src/Psalm/Type/Atomic/TValueOfArray.php b/src/Psalm/Type/Atomic/TValueOfArray.php index d8f3480ce56..390bb12c281 100644 --- a/src/Psalm/Type/Atomic/TValueOfArray.php +++ b/src/Psalm/Type/Atomic/TValueOfArray.php @@ -5,6 +5,9 @@ use Psalm\Type\Atomic; use Psalm\Type\Union; +use function array_merge; +use function array_values; + /** * Represents a value of an array. */ @@ -58,4 +61,36 @@ public static function isViableTemplateType(Union $template_type): bool } return true; } + + public static function getArrayValueType(Union $type): ?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) { + $array_value_atomics = static::getArrayValueType($atomic_type->as); + 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); + } } diff --git a/tests/Template/KeyOfTemplateTest.php b/tests/Template/KeyOfTemplateTest.php index 38f81fede1f..81e659f26c2 100644 --- a/tests/Template/KeyOfTemplateTest.php +++ b/tests/Template/KeyOfTemplateTest.php @@ -69,6 +69,18 @@ function getKey(string $key, $array) { } ' ], + 'keyOfUnreplacedTemplateParam' => [ + 'code' => ' + */ + abstract class Foo { + /** + * @return key-of + */ + abstract public function getRandomKey(): string; + }', + ], ]; } @@ -104,6 +116,20 @@ function getKey($array) { ', 'error_message' => 'InvalidReturnStatement' ], + 'keyOfUnresolvedTemplateParamIsStillChecked' => [ + 'code' => ' + */ + abstract class Foo { + /** + * @return key-of + */ + abstract public function getRandomKey(): string; + } + ', + 'error_message' => 'MismatchingDocblockReturnType' + ], ]; } } diff --git a/tests/Template/ValueOfTemplateTest.php b/tests/Template/ValueOfTemplateTest.php index 50bbb60dc07..b4eea45a616 100644 --- a/tests/Template/ValueOfTemplateTest.php +++ b/tests/Template/ValueOfTemplateTest.php @@ -45,6 +45,18 @@ function getValue(string $value, $array) { } ' ], + 'valueOfUnreplacedTemplateParam' => [ + 'code' => ' + */ + abstract class Foo { + /** + * @return value-of + */ + abstract public function getRandomValue(): bool; + }', + ], ]; } @@ -80,6 +92,20 @@ function getValue($array) { ', 'error_message' => 'InvalidReturnStatement' ], + 'valueOfUnresolvedTemplateParamIsStillChecked' => [ + 'code' => ' + */ + abstract class Foo { + /** + * @return value-of + */ + abstract public function getRandomValue(): string; + } + ', + 'error_message' => 'MismatchingDocblockReturnType' + ], ]; } } From 51cab704c4626b8ee55472d82542ea4b031b8960 Mon Sep 17 00:00:00 2001 From: Patrick Remy Date: Fri, 28 Jan 2022 00:04:21 +0100 Subject: [PATCH 09/11] fix: key-of/value-of handle nested template params --- .../Type/Comparator/AtomicTypeComparator.php | 67 ++++++++++--------- .../Type/Comparator/ScalarTypeComparator.php | 5 +- src/Psalm/Type/Atomic/TKeyOfArray.php | 16 +++-- src/Psalm/Type/Atomic/TValueOfArray.php | 19 ++++-- tests/Template/KeyOfTemplateTest.php | 15 ++++- tests/Template/ValueOfTemplateTest.php | 15 ++++- 6 files changed, 92 insertions(+), 45 deletions(-) diff --git a/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php b/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php index 96c0a99b4f9..982a1b08255 100644 --- a/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php @@ -326,37 +326,6 @@ public static function isContainedBy( return true; } - if ($container_type_part instanceof TTemplateParam && $input_type_part instanceof TTemplateParam) { - return UnionTypeComparator::isContainedBy( - $codebase, - $input_type_part->as, - $container_type_part->as, - false, - false, - $atomic_comparison_result, - $allow_interface_equality - ); - } - - if ($container_type_part instanceof TTemplateParam) { - foreach ($container_type_part->as->getAtomicTypes() as $container_as_type_part) { - if (self::isContainedBy( - $codebase, - $input_type_part, - $container_as_type_part, - $allow_interface_equality, - $allow_float_int_equality, - $atomic_comparison_result - )) { - if ($allow_interface_equality) { - return true; - } - } - } - - return false; - } - if ($container_type_part instanceof TTemplateValueOf) { if (!$input_type_part instanceof TTemplateValueOf) { return false; @@ -370,7 +339,10 @@ public static function isContainedBy( } if ($input_type_part instanceof TTemplateValueOf) { - $array_value_type = TValueOfArray::getArrayValueType($input_type_part->as); + $array_value_type = TValueOfArray::getArrayValueType( + $input_type_part->as, + $container_type_part instanceof TTemplateParam + ); if ($array_value_type === null) { return false; } @@ -391,6 +363,37 @@ public static function isContainedBy( return true; } + if ($container_type_part instanceof TTemplateParam && $input_type_part instanceof TTemplateParam) { + return UnionTypeComparator::isContainedBy( + $codebase, + $input_type_part->as, + $container_type_part->as, + false, + false, + $atomic_comparison_result, + $allow_interface_equality + ); + } + + if ($container_type_part instanceof TTemplateParam) { + foreach ($container_type_part->as->getAtomicTypes() as $container_as_type_part) { + if (self::isContainedBy( + $codebase, + $input_type_part, + $container_as_type_part, + $allow_interface_equality, + $allow_float_int_equality, + $atomic_comparison_result + )) { + if ($allow_interface_equality) { + return true; + } + } + } + + return false; + } + 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 871eb05a1cd..7351828e7e7 100644 --- a/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php @@ -266,7 +266,10 @@ public static function isContainedBy( } if ($input_type_part instanceof TTemplateKeyOf) { - $array_key_type = TKeyOfArray::getArrayKeyType($input_type_part->as); + $array_key_type = TKeyOfArray::getArrayKeyType( + $input_type_part->as, + $container_type_part instanceof TTemplateParam + ); if ($array_key_type === null) { return false; } diff --git a/src/Psalm/Type/Atomic/TKeyOfArray.php b/src/Psalm/Type/Atomic/TKeyOfArray.php index 8bc250d20f3..e321df82f7c 100644 --- a/src/Psalm/Type/Atomic/TKeyOfArray.php +++ b/src/Psalm/Type/Atomic/TKeyOfArray.php @@ -62,8 +62,10 @@ public static function isViableTemplateType(Union $template_type): bool return true; } - public static function getArrayKeyType(Union $type): ?Union - { + public static function getArrayKeyType( + Union $type, + bool $keep_template_params = false + ): ?Union { $key_types = []; foreach ($type->getAtomicTypes() as $atomic_type) { @@ -74,9 +76,13 @@ public static function getArrayKeyType(Union $type): ?Union } elseif ($atomic_type instanceof TKeyedArray) { $array_key_atomics = $atomic_type->getGenericKeyType(); } elseif ($atomic_type instanceof TTemplateParam) { - $array_key_atomics = static::getArrayKeyType($atomic_type->as); - if ($array_key_atomics === null) { - continue; + if ($keep_template_params) { + $array_key_atomics = new Union([$atomic_type]); + } else { + $array_key_atomics = static::getArrayKeyType($atomic_type->as); + if ($array_key_atomics === null) { + continue; + } } } else { continue; diff --git a/src/Psalm/Type/Atomic/TValueOfArray.php b/src/Psalm/Type/Atomic/TValueOfArray.php index 390bb12c281..7b10f16ee59 100644 --- a/src/Psalm/Type/Atomic/TValueOfArray.php +++ b/src/Psalm/Type/Atomic/TValueOfArray.php @@ -62,8 +62,10 @@ public static function isViableTemplateType(Union $template_type): bool return true; } - public static function getArrayValueType(Union $type): ?Union - { + public static function getArrayValueType( + Union $type, + bool $keep_template_params = false + ): ?Union { $value_types = []; foreach ($type->getAtomicTypes() as $atomic_type) { @@ -74,9 +76,16 @@ public static function getArrayValueType(Union $type): ?Union } elseif ($atomic_type instanceof TKeyedArray) { $array_value_atomics = $atomic_type->getGenericValueType(); } elseif ($atomic_type instanceof TTemplateParam) { - $array_value_atomics = static::getArrayValueType($atomic_type->as); - if ($array_value_atomics === null) { - continue; + 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; diff --git a/tests/Template/KeyOfTemplateTest.php b/tests/Template/KeyOfTemplateTest.php index 81e659f26c2..a3eff1603b5 100644 --- a/tests/Template/KeyOfTemplateTest.php +++ b/tests/Template/KeyOfTemplateTest.php @@ -79,7 +79,20 @@ abstract class Foo { * @return key-of */ abstract public function getRandomKey(): string; - }', + } + ', + ], + 'SKIPPED-keyOfNestedTemplates' => [ + 'code' => ' + * @param TArray $array + * @return list + */ + function toListOfKeys(array $array): array { + return array_keys($array); + }' ], ]; } diff --git a/tests/Template/ValueOfTemplateTest.php b/tests/Template/ValueOfTemplateTest.php index b4eea45a616..96d0d4dcacf 100644 --- a/tests/Template/ValueOfTemplateTest.php +++ b/tests/Template/ValueOfTemplateTest.php @@ -55,7 +55,20 @@ abstract class Foo { * @return value-of */ abstract public function getRandomValue(): bool; - }', + } + ', + ], + 'valueOfNestedTemplates' => [ + 'code' => ' + * @param TArray $array + * @return list + */ + function toList(array $array): array { + return array_values($array); + }' ], ]; } From 51d4bf556a11b4fc224c748b7da457bf2f33c889 Mon Sep 17 00:00:00 2001 From: Patrick Remy Date: Mon, 31 Jan 2022 20:43:28 +0100 Subject: [PATCH 10/11] fix: replace template args in key-of/value-of Extend TTemplateKeyOf from Atomic instead of Scalar, to ensure it is only compared in AtomicTypeComparator to reduce duplicate code. --- .../Type/Comparator/AtomicTypeComparator.php | 41 +++++++++++++++++-- .../Type/Comparator/ScalarTypeComparator.php | 40 ------------------ src/Psalm/Type/Atomic/TKeyOfArray.php | 5 ++- src/Psalm/Type/Atomic/TTemplateKeyOf.php | 36 +++++++++++++++- src/Psalm/Type/Atomic/TTemplateValueOf.php | 16 +++++++- tests/Template/KeyOfTemplateTest.php | 2 +- 6 files changed, 91 insertions(+), 49 deletions(-) diff --git a/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php b/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php index 982a1b08255..f2eed358f4b 100644 --- a/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php @@ -19,6 +19,7 @@ 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,6 +33,7 @@ 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; @@ -326,6 +328,40 @@ public static function isContainedBy( 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; @@ -339,10 +375,7 @@ public static function isContainedBy( } if ($input_type_part instanceof TTemplateValueOf) { - $array_value_type = TValueOfArray::getArrayValueType( - $input_type_part->as, - $container_type_part instanceof TTemplateParam - ); + $array_value_type = TValueOfArray::getArrayValueType($input_type_part->as); if ($array_value_type === null) { return false; } diff --git a/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php b/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php index 7351828e7e7..fe80125a861 100644 --- a/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php @@ -17,7 +17,6 @@ use Psalm\Type\Atomic\TFloat; use Psalm\Type\Atomic\TInt; use Psalm\Type\Atomic\TIntRange; -use Psalm\Type\Atomic\TKeyOfArray; use Psalm\Type\Atomic\TLiteralClassString; use Psalm\Type\Atomic\TLiteralFloat; use Psalm\Type\Atomic\TLiteralInt; @@ -35,7 +34,6 @@ 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; @@ -253,44 +251,6 @@ public static function isContainedBy( 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, - $container_type_part instanceof TTemplateParam - ); - if ($array_key_type === null) { - return false; - } - - /** @var Scalar $array_key_atomic */ - 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 TArrayKey && $input_type_part instanceof TNumeric ) { diff --git a/src/Psalm/Type/Atomic/TKeyOfArray.php b/src/Psalm/Type/Atomic/TKeyOfArray.php index e321df82f7c..1839a370bb5 100644 --- a/src/Psalm/Type/Atomic/TKeyOfArray.php +++ b/src/Psalm/Type/Atomic/TKeyOfArray.php @@ -79,7 +79,10 @@ public static function getArrayKeyType( if ($keep_template_params) { $array_key_atomics = new Union([$atomic_type]); } else { - $array_key_atomics = static::getArrayKeyType($atomic_type->as); + $array_key_atomics = static::getArrayKeyType( + $atomic_type->as, + $keep_template_params + ); if ($array_key_atomics === null) { continue; } diff --git a/src/Psalm/Type/Atomic/TTemplateKeyOf.php b/src/Psalm/Type/Atomic/TTemplateKeyOf.php index f8abbfcce98..8803f6586c6 100644 --- a/src/Psalm/Type/Atomic/TTemplateKeyOf.php +++ b/src/Psalm/Type/Atomic/TTemplateKeyOf.php @@ -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 TKeyOfArray when the type of the array is a template */ -class TTemplateKeyOf extends TArrayKey +class TTemplateKeyOf extends Atomic { /** * @var string @@ -45,7 +49,7 @@ public function getId(bool $exact = true, bool $nested = false): string 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 @@ public function toNamespacedString( ): string { return 'key-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; + } + + public function replaceTemplateTypesWithArgTypes( + TemplateResult $template_result, + ?Codebase $codebase + ): void { + TemplateInferredTypeReplacer::replace( + $this->as, + $template_result, + $codebase + ); + } } diff --git a/src/Psalm/Type/Atomic/TTemplateValueOf.php b/src/Psalm/Type/Atomic/TTemplateValueOf.php index 10b4770d9d7..02a520a6e17 100644 --- a/src/Psalm/Type/Atomic/TTemplateValueOf.php +++ b/src/Psalm/Type/Atomic/TTemplateValueOf.php @@ -2,6 +2,9 @@ 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; @@ -46,7 +49,7 @@ public function getId(bool $exact = true, bool $nested = false): string return 'value-of<' . $this->param_name . '>'; } - return 'value-of<' . $this->param_name . ':' . $this->defining_class . ' as ' . $this->as->getId($exact) . '>'; + return 'value-of<' . $this->as->getId($exact) . '>'; } /** @@ -77,4 +80,15 @@ 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 + ); + } } diff --git a/tests/Template/KeyOfTemplateTest.php b/tests/Template/KeyOfTemplateTest.php index a3eff1603b5..f5d9ded0236 100644 --- a/tests/Template/KeyOfTemplateTest.php +++ b/tests/Template/KeyOfTemplateTest.php @@ -82,7 +82,7 @@ abstract public function getRandomKey(): string; } ', ], - 'SKIPPED-keyOfNestedTemplates' => [ + 'keyOfNestedTemplates' => [ 'code' => ' Date: Mon, 31 Jan 2022 21:30:04 +0100 Subject: [PATCH 11/11] docs: add key-of/value-of BC to `UPGRADING` --- UPGRADING.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/UPGRADING.md b/UPGRADING.md index 8dafd844e3b..ac7e6211b0c 100644 --- a/UPGRADING.md +++ b/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 @@ -154,6 +154,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