From fe427cdbd8ddcbcbcd4f6870fe778954ed4c7d59 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Mon, 7 Oct 2024 21:43:06 +0200 Subject: [PATCH 1/5] Add ArrayChangeKeyCaseFunctionReturnTypeExtension --- conf/config.neon | 5 + ...gumentBasedFunctionReturnTypeExtension.php | 1 - ...angeKeyCaseFunctionReturnTypeExtension.php | 126 ++++++++++++++++++ .../Analyser/LegacyNodeScopeResolverTest.php | 2 +- .../Analyser/nsrt/array-change-key-case.php | 77 +++++++++++ 5 files changed, 209 insertions(+), 2 deletions(-) create mode 100644 src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php create mode 100644 tests/PHPStan/Analyser/nsrt/array-change-key-case.php diff --git a/conf/config.neon b/conf/config.neon index aa096cc686e..33d190a6473 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -1160,6 +1160,11 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\ArrayChangeKeyCaseFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\ArrayIntersectKeyFunctionReturnTypeExtension tags: diff --git a/src/Type/Php/ArgumentBasedFunctionReturnTypeExtension.php b/src/Type/Php/ArgumentBasedFunctionReturnTypeExtension.php index b2508600f43..6e3c75b9a15 100644 --- a/src/Type/Php/ArgumentBasedFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArgumentBasedFunctionReturnTypeExtension.php @@ -18,7 +18,6 @@ final class ArgumentBasedFunctionReturnTypeExtension implements DynamicFunctionR private const FUNCTION_NAMES = [ 'array_unique' => 0, - 'array_change_key_case' => 0, 'array_diff_assoc' => 0, 'array_diff_key' => 0, 'array_diff_uassoc' => 0, diff --git a/src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php b/src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php new file mode 100644 index 00000000000..1bef4cadfd4 --- /dev/null +++ b/src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php @@ -0,0 +1,126 @@ +getName() === 'array_change_key_case'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (!isset($functionCall->getArgs()[0])) { + return null; + } + + $arrayType = $scope->getType($functionCall->getArgs()[0]->value); + if (!isset($functionCall->getArgs()[1])) { + $case = CASE_LOWER; + } else { + $caseType = $scope->getType($functionCall->getArgs()[1]->value); + $scalarValues = $caseType->getConstantScalarValues(); + if (count($scalarValues) === 1) { + $case = $scalarValues[0]; + } else { + $case = null; + } + } + + $constantArrays = $arrayType->getConstantArrays(); + if (count($constantArrays) > 0) { + $arrayTypes = []; + foreach ($constantArrays as $constantArray) { + $newConstantArrayBuilder = ConstantArrayTypeBuilder::createEmpty(); + foreach ($constantArray->getKeyTypes() as $i => $keyType) { + $valueType = $constantArray->getOffsetValueType($keyType); + if ($keyType->isString()->yes()) { + if (!isset($case)) { + $keyType = TypeCombinator::union( + new ConstantStringType(strtolower((string) $keyType->getValue())), + new ConstantStringType(strtoupper((string) $keyType->getValue())), + ); + } elseif ($case === CASE_LOWER) { + $keyType = new ConstantStringType(strtolower((string) $keyType->getValue())); + } else { + $keyType = new ConstantStringType(strtoupper((string) $keyType->getValue())); + } + } + + $newConstantArrayBuilder->setOffsetValueType( + $keyType, + $valueType, + $constantArray->isOptionalKey($i), + ); + } + $newConstantArrayType = $newConstantArrayBuilder->getArray(); + if ($constantArray->isList()->yes()) { + $newConstantArrayType = AccessoryArrayListType::intersectWith($newConstantArrayType); + } + $arrayTypes[] = $newConstantArrayType; + } + + $newArrayType = TypeCombinator::union(...$arrayTypes); + } else { + $keysType = $arrayType->getIterableKeyType(); + + $keysType = TypeTraverser::map($keysType, static function (Type $type, callable $traverse) use ($case): Type { + if ($type instanceof UnionType) { + return $traverse($type); + } + + if ($type->isString()->yes()) { + if ($case === CASE_LOWER) { + return TypeCombinator::intersect($type, new AccessoryLowercaseStringType()); + } elseif ($type->isLowercaseString()->yes()) { + return TypeCombinator::intersect( + new StringType(), + ...array_filter( + TypeUtils::getAccessoryTypes($type), + static fn (Type $accessory): bool => !$accessory instanceof AccessoryLowercaseStringType, + ), + ); + } + } + + return $type; + }); + + $newArrayType = TypeCombinator::intersect(new ArrayType( + $keysType, + $arrayType->getIterableValueType(), + ), ...TypeUtils::getAccessoryTypes($arrayType)); + } + + if ($arrayType->isIterableAtLeastOnce()->yes()) { + $newArrayType = TypeCombinator::intersect($newArrayType, new NonEmptyArrayType()); + } + + return $newArrayType; + } + +} diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index aea1961e324..a2e1124280e 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -4577,7 +4577,7 @@ public function dataArrayFunctions(): array '$reducedToInt', ], [ - 'array<0|1|2, 1|2|3>', + 'array{1, 2, 3}', 'array_change_key_case($integers)', ], [ diff --git a/tests/PHPStan/Analyser/nsrt/array-change-key-case.php b/tests/PHPStan/Analyser/nsrt/array-change-key-case.php new file mode 100644 index 00000000000..962ff17b667 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-change-key-case.php @@ -0,0 +1,77 @@ + $arr1 + * @param array $arr2 + * @param array $arr3 + * @param array $arr4 + * @param array $arr5 + * @param array $arr6 + * @param array{foo: 1, bar?: 2} $arr7 + * @param list $list + * @param non-empty-array $nonEmpty + */ + public function sayHello( + array $arr1, + array $arr2, + array $arr3, + array $arr4, + array $arr5, + array $arr6, + array $arr7, + array $list, + array $nonEmpty, + int $case + ): void { + assertType('array', array_change_key_case($arr1)); + assertType('array', array_change_key_case($arr1, CASE_LOWER)); + assertType('array', array_change_key_case($arr1, CASE_UPPER)); + assertType('array', array_change_key_case($arr1, $case)); + + assertType('array', array_change_key_case($arr2)); + assertType('array', array_change_key_case($arr2, CASE_LOWER)); + assertType('array', array_change_key_case($arr2, CASE_UPPER)); + assertType('array', array_change_key_case($arr2, $case)); + + assertType('array', array_change_key_case($arr3)); + assertType('array', array_change_key_case($arr3, CASE_LOWER)); + assertType('array', array_change_key_case($arr3, CASE_UPPER)); + assertType('array', array_change_key_case($arr3, $case)); + + assertType('array', array_change_key_case($arr4)); + assertType('array', array_change_key_case($arr4, CASE_LOWER)); + assertType('array', array_change_key_case($arr4, CASE_UPPER)); + assertType('array', array_change_key_case($arr4, $case)); + + assertType('array', array_change_key_case($arr5)); + assertType('array', array_change_key_case($arr5, CASE_LOWER)); + assertType('array', array_change_key_case($arr5, CASE_UPPER)); + assertType('array', array_change_key_case($arr5, $case)); + + assertType('array', array_change_key_case($arr6)); + assertType('array', array_change_key_case($arr6, CASE_LOWER)); + assertType('array', array_change_key_case($arr6, CASE_UPPER)); + assertType('array', array_change_key_case($arr6, $case)); + + assertType('array{foo: 1, bar?: 2}', array_change_key_case($arr7)); + assertType('array{foo: 1, bar?: 2}', array_change_key_case($arr7, CASE_LOWER)); + assertType('array{FOO: 1, BAR?: 2}', array_change_key_case($arr7, CASE_UPPER)); + assertType("non-empty-array<'BAR'|'bar'|'FOO'|'foo', 1|2>", array_change_key_case($arr7, $case)); + + assertType('list', array_change_key_case($list)); + assertType('list', array_change_key_case($list, CASE_LOWER)); + assertType('list', array_change_key_case($list, CASE_UPPER)); + assertType('list', array_change_key_case($list, $case)); + + assertType('non-empty-array', array_change_key_case($nonEmpty)); + assertType('non-empty-array', array_change_key_case($nonEmpty, CASE_LOWER)); + assertType('non-empty-array', array_change_key_case($nonEmpty, CASE_UPPER)); + assertType('non-empty-array', array_change_key_case($nonEmpty, $case)); + } +} From 7133400077e5afbff2aee3e233a2600f9ab2fb70 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Mon, 7 Oct 2024 23:15:50 +0200 Subject: [PATCH 2/5] Rework --- ...angeKeyCaseFunctionReturnTypeExtension.php | 54 ++++++++++++------- .../Analyser/nsrt/array-change-key-case.php | 23 +++++--- 2 files changed, 50 insertions(+), 27 deletions(-) diff --git a/src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php b/src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php index 1bef4cadfd4..455297a64f4 100644 --- a/src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php @@ -7,6 +7,9 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; +use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; +use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; @@ -58,17 +61,8 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $newConstantArrayBuilder = ConstantArrayTypeBuilder::createEmpty(); foreach ($constantArray->getKeyTypes() as $i => $keyType) { $valueType = $constantArray->getOffsetValueType($keyType); - if ($keyType->isString()->yes()) { - if (!isset($case)) { - $keyType = TypeCombinator::union( - new ConstantStringType(strtolower((string) $keyType->getValue())), - new ConstantStringType(strtoupper((string) $keyType->getValue())), - ); - } elseif ($case === CASE_LOWER) { - $keyType = new ConstantStringType(strtolower((string) $keyType->getValue())); - } else { - $keyType = new ConstantStringType(strtoupper((string) $keyType->getValue())); - } + if ($keyType instanceof ConstantStringType) { + $keyType = $this->mapConstantString($keyType, $case); } $newConstantArrayBuilder->setOffsetValueType( @@ -88,22 +82,30 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } else { $keysType = $arrayType->getIterableKeyType(); - $keysType = TypeTraverser::map($keysType, static function (Type $type, callable $traverse) use ($case): Type { + $keysType = TypeTraverser::map($keysType, function (Type $type, callable $traverse) use ($case): Type { if ($type instanceof UnionType) { return $traverse($type); } + if ($type instanceof ConstantStringType) { + return $this->mapConstantString($type, $case); + } + if ($type->isString()->yes()) { if ($case === CASE_LOWER) { return TypeCombinator::intersect($type, new AccessoryLowercaseStringType()); } elseif ($type->isLowercaseString()->yes()) { - return TypeCombinator::intersect( - new StringType(), - ...array_filter( - TypeUtils::getAccessoryTypes($type), - static fn (Type $accessory): bool => !$accessory instanceof AccessoryLowercaseStringType, - ), - ); + $types = [new StringType()]; + if ($type->isNonFalsyString()->yes()) { + $types[] = new AccessoryNonFalsyStringType(); + } elseif ($type->isNonEmptyString()->yes()) { + $types[] = new AccessoryNonEmptyStringType(); + } + if ($type->isNumericString()->yes()) { + $types[] = new AccessoryNumericStringType(); + } + + return TypeCombinator::intersect(...$types); } } @@ -123,4 +125,18 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return $newArrayType; } + private function mapConstantString(ConstantStringType $type, ?int $case): Type + { + if ($case === CASE_LOWER) { + return new ConstantStringType(strtolower($type->getValue())); + } elseif ($case === CASE_UPPER) { + return new ConstantStringType(strtoupper($type->getValue())); + } else { + return TypeCombinator::union( + new ConstantStringType(strtolower($type->getValue())), + new ConstantStringType(strtoupper($type->getValue())), + ); + } + } + } diff --git a/tests/PHPStan/Analyser/nsrt/array-change-key-case.php b/tests/PHPStan/Analyser/nsrt/array-change-key-case.php index 962ff17b667..fa14c5b9f98 100644 --- a/tests/PHPStan/Analyser/nsrt/array-change-key-case.php +++ b/tests/PHPStan/Analyser/nsrt/array-change-key-case.php @@ -7,15 +7,16 @@ class HelloWorld { /** - * @param array $arr1 - * @param array $arr2 - * @param array $arr3 - * @param array $arr4 - * @param array $arr5 + * @param array $arr1 + * @param array $arr2 + * @param array $arr3 + * @param array $arr4 + * @param array $arr5 * @param array $arr6 - * @param array{foo: 1, bar?: 2} $arr7 - * @param list $list - * @param non-empty-array $nonEmpty + * @param array{foo: 1, bar?: 2} $arr7 + * @param array<'foo'|'bar', string> $arr8 + * @param list $list + * @param non-empty-array $nonEmpty */ public function sayHello( array $arr1, @@ -25,6 +26,7 @@ public function sayHello( array $arr5, array $arr6, array $arr7, + array $arr8, array $list, array $nonEmpty, int $case @@ -64,6 +66,11 @@ public function sayHello( assertType('array{FOO: 1, BAR?: 2}', array_change_key_case($arr7, CASE_UPPER)); assertType("non-empty-array<'BAR'|'bar'|'FOO'|'foo', 1|2>", array_change_key_case($arr7, $case)); + assertType("array<'bar'|'foo', string>", array_change_key_case($arr8)); + assertType("array<'bar'|'foo', string>", array_change_key_case($arr8, CASE_LOWER)); + assertType("array<'BAR'|'FOO', string>", array_change_key_case($arr8, CASE_UPPER)); + assertType("array<'BAR'|'bar'|'FOO'|'foo', string>", array_change_key_case($arr8, $case)); + assertType('list', array_change_key_case($list)); assertType('list', array_change_key_case($list, CASE_LOWER)); assertType('list', array_change_key_case($list, CASE_UPPER)); From 40c0b2cbc52a93ae1f7c7b8d8dbae4bf5ffb7e7d Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Mon, 7 Oct 2024 23:18:52 +0200 Subject: [PATCH 3/5] Add test --- .../Rules/Functions/ReturnTypeRuleTest.php | 7 +++++ .../Rules/Functions/data/bug-10960.php | 26 +++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 tests/PHPStan/Rules/Functions/data/bug-10960.php diff --git a/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php index 28d766512f0..d8a9be4200f 100644 --- a/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php @@ -283,6 +283,13 @@ public function testBug10732(): void $this->analyse([__DIR__ . '/data/bug-10732.php'], []); } + public function testBug10960(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-10960.php'], []); + } + public function testBug11518(): void { $this->checkExplicitMixed = true; diff --git a/tests/PHPStan/Rules/Functions/data/bug-10960.php b/tests/PHPStan/Rules/Functions/data/bug-10960.php new file mode 100644 index 00000000000..4b64fae830c --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-10960.php @@ -0,0 +1,26 @@ + 'bar']); +lowerCaseKey(['FOO' => 'bar']); From 61bdd7953a3419b1241d69bd92a3fe4c70d3bc80 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Mon, 7 Oct 2024 23:27:39 +0200 Subject: [PATCH 4/5] Fix --- ...angeKeyCaseFunctionReturnTypeExtension.php | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php b/src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php index 455297a64f4..8dc831103e8 100644 --- a/src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php @@ -21,11 +21,12 @@ use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; -use function array_filter; +use function array_map; use function count; use function strtolower; use function strtoupper; use const CASE_LOWER; +use const CASE_UPPER; final class ArrayChangeKeyCaseFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -48,7 +49,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $caseType = $scope->getType($functionCall->getArgs()[1]->value); $scalarValues = $caseType->getConstantScalarValues(); if (count($scalarValues) === 1) { - $case = $scalarValues[0]; + $case = (int) $scalarValues[0]; } else { $case = null; } @@ -61,8 +62,15 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $newConstantArrayBuilder = ConstantArrayTypeBuilder::createEmpty(); foreach ($constantArray->getKeyTypes() as $i => $keyType) { $valueType = $constantArray->getOffsetValueType($keyType); - if ($keyType instanceof ConstantStringType) { - $keyType = $this->mapConstantString($keyType, $case); + + $constantStrings = $keyType->getConstantStrings(); + if (count($constantStrings) > 0) { + $keyType = TypeCombinator::union( + ...array_map( + fn (ConstantStringType $type): Type => $this->mapConstantString($type, $case), + $constantStrings, + ), + ); } $newConstantArrayBuilder->setOffsetValueType( @@ -87,8 +95,14 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return $traverse($type); } - if ($type instanceof ConstantStringType) { - return $this->mapConstantString($type, $case); + $constantStrings = $type->getConstantStrings(); + if (count($constantStrings) > 0) { + return TypeCombinator::union( + ...array_map( + fn (ConstantStringType $type): Type => $this->mapConstantString($type, $case), + $constantStrings, + ), + ); } if ($type->isString()->yes()) { @@ -131,12 +145,12 @@ private function mapConstantString(ConstantStringType $type, ?int $case): Type return new ConstantStringType(strtolower($type->getValue())); } elseif ($case === CASE_UPPER) { return new ConstantStringType(strtoupper($type->getValue())); - } else { - return TypeCombinator::union( - new ConstantStringType(strtolower($type->getValue())), - new ConstantStringType(strtoupper($type->getValue())), - ); } + + return TypeCombinator::union( + new ConstantStringType(strtolower($type->getValue())), + new ConstantStringType(strtoupper($type->getValue())), + ); } } From cdfc170aba145d4c92aaf653f8ea933afb2de8bc Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Tue, 8 Oct 2024 08:39:39 +0200 Subject: [PATCH 5/5] Rework --- ...angeKeyCaseFunctionReturnTypeExtension.php | 25 +++++++------- .../Analyser/nsrt/array-change-key-case.php | 34 +++++++++++++------ 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php b/src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php index 8dc831103e8..9ca6f45be37 100644 --- a/src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php @@ -106,21 +106,20 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } if ($type->isString()->yes()) { + $types = [new StringType()]; + if ($type->isNonFalsyString()->yes()) { + $types[] = new AccessoryNonFalsyStringType(); + } elseif ($type->isNonEmptyString()->yes()) { + $types[] = new AccessoryNonEmptyStringType(); + } + if ($type->isNumericString()->yes()) { + $types[] = new AccessoryNumericStringType(); + } if ($case === CASE_LOWER) { - return TypeCombinator::intersect($type, new AccessoryLowercaseStringType()); - } elseif ($type->isLowercaseString()->yes()) { - $types = [new StringType()]; - if ($type->isNonFalsyString()->yes()) { - $types[] = new AccessoryNonFalsyStringType(); - } elseif ($type->isNonEmptyString()->yes()) { - $types[] = new AccessoryNonEmptyStringType(); - } - if ($type->isNumericString()->yes()) { - $types[] = new AccessoryNumericStringType(); - } - - return TypeCombinator::intersect(...$types); + $types[] = new AccessoryLowercaseStringType(); } + + return TypeCombinator::intersect(...$types); } return $type; diff --git a/tests/PHPStan/Analyser/nsrt/array-change-key-case.php b/tests/PHPStan/Analyser/nsrt/array-change-key-case.php index fa14c5b9f98..080f155aaac 100644 --- a/tests/PHPStan/Analyser/nsrt/array-change-key-case.php +++ b/tests/PHPStan/Analyser/nsrt/array-change-key-case.php @@ -13,8 +13,10 @@ class HelloWorld * @param array $arr4 * @param array $arr5 * @param array $arr6 - * @param array{foo: 1, bar?: 2} $arr7 - * @param array<'foo'|'bar', string> $arr8 + * @param array $arr7 + * @param array $arr8 + * @param array{foo: 1, bar?: 2} $arr9 + * @param array<'foo'|'bar', string> $arr10 * @param list $list * @param non-empty-array $nonEmpty */ @@ -27,6 +29,8 @@ public function sayHello( array $arr6, array $arr7, array $arr8, + array $arr9, + array $arr10, array $list, array $nonEmpty, int $case @@ -61,15 +65,25 @@ public function sayHello( assertType('array', array_change_key_case($arr6, CASE_UPPER)); assertType('array', array_change_key_case($arr6, $case)); - assertType('array{foo: 1, bar?: 2}', array_change_key_case($arr7)); - assertType('array{foo: 1, bar?: 2}', array_change_key_case($arr7, CASE_LOWER)); - assertType('array{FOO: 1, BAR?: 2}', array_change_key_case($arr7, CASE_UPPER)); - assertType("non-empty-array<'BAR'|'bar'|'FOO'|'foo', 1|2>", array_change_key_case($arr7, $case)); + assertType('array', array_change_key_case($arr7)); + assertType('array', array_change_key_case($arr7, CASE_LOWER)); + assertType('array', array_change_key_case($arr7, CASE_UPPER)); + assertType('array', array_change_key_case($arr7, $case)); - assertType("array<'bar'|'foo', string>", array_change_key_case($arr8)); - assertType("array<'bar'|'foo', string>", array_change_key_case($arr8, CASE_LOWER)); - assertType("array<'BAR'|'FOO', string>", array_change_key_case($arr8, CASE_UPPER)); - assertType("array<'BAR'|'bar'|'FOO'|'foo', string>", array_change_key_case($arr8, $case)); + assertType('array', array_change_key_case($arr8)); + assertType('array', array_change_key_case($arr8, CASE_LOWER)); + assertType('array', array_change_key_case($arr8, CASE_UPPER)); + assertType('array', array_change_key_case($arr8, $case)); + + assertType('array{foo: 1, bar?: 2}', array_change_key_case($arr9)); + assertType('array{foo: 1, bar?: 2}', array_change_key_case($arr9, CASE_LOWER)); + assertType('array{FOO: 1, BAR?: 2}', array_change_key_case($arr9, CASE_UPPER)); + assertType("non-empty-array<'BAR'|'bar'|'FOO'|'foo', 1|2>", array_change_key_case($arr9, $case)); + + assertType("array<'bar'|'foo', string>", array_change_key_case($arr10)); + assertType("array<'bar'|'foo', string>", array_change_key_case($arr10, CASE_LOWER)); + assertType("array<'BAR'|'FOO', string>", array_change_key_case($arr10, CASE_UPPER)); + assertType("array<'BAR'|'bar'|'FOO'|'foo', string>", array_change_key_case($arr10, $case)); assertType('list', array_change_key_case($list)); assertType('list', array_change_key_case($list, CASE_LOWER));