diff --git a/docs/running_psalm/plugins/plugins_type_system.md b/docs/running_psalm/plugins/plugins_type_system.md index c59e4c1a4..7676bc2f3 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 1f6076d75..e65c34332 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\ClassLikeStorageProvider; use Psalm\Internal\Provider\FileStorageProvider; use Psalm\Storage\FunctionLikeStorage; use Psalm\Type\Atomic\TArray; -use Psalm\Type\Atomic\TAssertionFalsy; use Psalm\Type\Atomic\TEnumCase; use Psalm\Type\Atomic\TFalse; use Psalm\Type\Atomic\TIterable; diff --git a/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php b/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php index 2f192757f..e96b7d559 100644 --- a/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php @@ -253,18 +253,16 @@ class ScalarTypeComparator 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 @@ class ScalarTypeComparator 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 c800b1682..956d1861c 100644 --- a/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php @@ -10,6 +10,7 @@ use Psalm\Type\Atomic\TClassString; use Psalm\Type\Atomic\TConditional; use Psalm\Type\Atomic\TInt; use Psalm\Type\Atomic\TIterable; +use Psalm\Type\Atomic\TKeyOfArray; use Psalm\Type\Atomic\TKeyedArray; use Psalm\Type\Atomic\TLiteralInt; use Psalm\Type\Atomic\TLiteralString; @@ -18,6 +19,7 @@ use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TObject; use Psalm\Type\Atomic\TObjectWithProperties; use Psalm\Type\Atomic\TTemplateIndexedAccess; +use Psalm\Type\Atomic\TTemplateKeyOf; use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Atomic\TTemplateParamClass; use Psalm\Type\Union; @@ -228,6 +230,21 @@ class TemplateInferredTypeReplacer } else { $new_types[] = new TMixed(); } + } elseif ($atomic_type instanceof TTemplateKeyOf) { + $template_type = isset($inferred_lower_bounds[$atomic_type->param_name][$atomic_type->defining_class]) + ? clone TemplateStandinTypeReplacer::getMostSpecificTypeFromBounds( + $inferred_lower_bounds[$atomic_type->param_name][$atomic_type->defining_class], + $codebase + ) + : null; + + 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 97c1a8854..5b6827738 100644 --- a/src/Psalm/Internal/Type/TypeExpander.php +++ b/src/Psalm/Internal/Type/TypeExpander.php @@ -18,7 +18,7 @@ use Psalm\Type\Atomic\TInt; use Psalm\Type\Atomic\TIntMask; use Psalm\Type\Atomic\TIntMaskOf; use Psalm\Type\Atomic\TIterable; -use Psalm\Type\Atomic\TKeyOfClassConstant; +use Psalm\Type\Atomic\TKeyOfArray; use Psalm\Type\Atomic\TKeyedArray; use Psalm\Type\Atomic\TList; use Psalm\Type\Atomic\TLiteralClassString; @@ -29,7 +29,7 @@ use Psalm\Type\Atomic\TNull; use Psalm\Type\Atomic\TObjectWithProperties; use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Atomic\TTypeAlias; -use Psalm\Type\Atomic\TValueOfClassConstant; +use Psalm\Type\Atomic\TValueOfArray; use Psalm\Type\Atomic\TVoid; use Psalm\Type\Union; use ReflectionProperty; @@ -350,52 +350,16 @@ class TypeExpander return [$return_type]; } - if ($return_type instanceof TKeyOfClassConstant - || $return_type instanceof TValueOfClassConstant + if ($return_type instanceof TKeyOfArray + || $return_type instanceof TValueOfArray ) { - if ($return_type->fq_classlike_name === 'self' && $self_class) { - $return_type->fq_classlike_name = $self_class; - } - - if ($evaluate_class_constants) { - if ($throw_on_unresolvable_constant - && !$codebase->classOrInterfaceExists($return_type->fq_classlike_name) - ) { - throw new UnresolvableConstantException($return_type->fq_classlike_name, $return_type->const_name); - } - - try { - $class_constant_type = $codebase->classlikes->getClassConstantType( - $return_type->fq_classlike_name, - $return_type->const_name, - ReflectionProperty::IS_PRIVATE - ); - } catch (CircularReferenceException $e) { - $class_constant_type = null; - } - - if ($class_constant_type) { - foreach ($class_constant_type->getAtomicTypes() as $const_type_atomic) { - if ($const_type_atomic instanceof TKeyedArray - || $const_type_atomic instanceof TArray - ) { - if ($const_type_atomic instanceof TKeyedArray) { - $const_type_atomic = $const_type_atomic->getGenericArrayType(); - } - - if ($return_type instanceof TKeyOfClassConstant) { - return array_values($const_type_atomic->type_params[0]->getAtomicTypes()); - } - - return array_values($const_type_atomic->type_params[1]->getAtomicTypes()); - } - } - } elseif ($throw_on_unresolvable_constant) { - throw new UnresolvableConstantException($return_type->fq_classlike_name, $return_type->const_name); - } - } - - return [$return_type]; + return self::expandKeyOfValueOfArray( + $codebase, + $return_type, + $self_class, + $evaluate_class_constants, + $throw_on_unresolvable_constant + ); } if ($return_type instanceof TIntMask) { @@ -911,4 +875,92 @@ class TypeExpander 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 9e2fc4dba..3740c904c 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -41,7 +41,7 @@ use Psalm\Type\Atomic\TIntMask; use Psalm\Type\Atomic\TIntMaskOf; use Psalm\Type\Atomic\TIntRange; use Psalm\Type\Atomic\TIterable; -use Psalm\Type\Atomic\TKeyOfClassConstant; +use Psalm\Type\Atomic\TKeyOfArray; use Psalm\Type\Atomic\TKeyedArray; use Psalm\Type\Atomic\TList; use Psalm\Type\Atomic\TLiteralClassString; @@ -60,7 +60,7 @@ use Psalm\Type\Atomic\TTemplateKeyOf; 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 @@ class TypeParser return new TTemplateKeyOf( $param_name, $defining_class, - $template_type_map[$param_name][$defining_class] + $generic_params[0] ); } @@ -702,16 +702,13 @@ class TypeParser 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 @@ class TypeParser 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 @@ class TypeParser $param_type = $param_union_types[0]; if (!$param_type instanceof TClassConstant - && !$param_type instanceof TValueOfClassConstant - && !$param_type instanceof TKeyOfClassConstant + && !$param_type instanceof TValueOfArray + && !$param_type instanceof TKeyOfArray ) { throw new TypeParseTreeException( 'Invalid reference passed to int-mask-of' diff --git a/src/Psalm/Type/Atomic/TIntMaskOf.php b/src/Psalm/Type/Atomic/TIntMaskOf.php index ec71060ff..86cb53b83 100644 --- a/src/Psalm/Type/Atomic/TIntMaskOf.php +++ b/src/Psalm/Type/Atomic/TIntMaskOf.php @@ -11,11 +11,11 @@ use Psalm\Type\Atomic; */ class TIntMaskOf extends TInt { - /** @var TClassConstant|TKeyOfClassConstant|TValueOfClassConstant */ + /** @var TClassConstant|TKeyOfArray|TValueOfArray */ public $value; /** - * @param TClassConstant|TKeyOfClassConstant|TValueOfClassConstant $value + * @param TClassConstant|TKeyOfArray|TValueOfArray $value */ public function __construct(Atomic $value) { diff --git a/src/Psalm/Type/Atomic/TKeyOfArray.php b/src/Psalm/Type/Atomic/TKeyOfArray.php new file mode 100644 index 000000000..93cd98de1 --- /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 5834db3e2..000000000 --- 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 8aaed4356..1352d8721 100644 --- a/src/Psalm/Type/Atomic/TTemplateKeyOf.php +++ b/src/Psalm/Type/Atomic/TTemplateKeyOf.php @@ -5,7 +5,7 @@ namespace Psalm\Type\Atomic; use Psalm\Type\Union; /** - * Represents the type used when using 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 000000000..ad76824e2 --- /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 6f4afc9c5..000000000 --- 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 243525292..94fa149bd 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 000000000..39545743d --- /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 000000000..3893e9e49 --- /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 000000000..b856ff1da --- /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' + ], + ]; + } +}