From 9843dd871b5cba9f452c270d55235a914847e207 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 23 Sep 2024 11:54:33 +0200 Subject: [PATCH 1/2] Extract `AllowedArrayKeysTypes::narrowOffsetKeyType()` to ease re-use --- src/Analyser/TypeSpecifier.php | 44 ++--------------- src/Rules/Arrays/AllowedArrayKeysTypes.php | 55 ++++++++++++++++++++++ 2 files changed, 58 insertions(+), 41 deletions(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index a5dc7bc6a5..6a40c74454 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -28,6 +28,7 @@ use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\ResolvedFunctionVariant; +use PHPStan\Rules\Arrays\AllowedArrayKeysTypes; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; @@ -820,47 +821,8 @@ public function specifyTypesInCondition( ); } else { $varType = $scope->getType($var->var); - if ($varType->isArray()->yes() && !$varType->isIterableAtLeastOnce()->no()) { - $varIterableKeyType = $varType->getIterableKeyType(); - - if ($varIterableKeyType->isConstantScalarValue()->yes()) { - $narrowedKey = TypeCombinator::union( - $varIterableKeyType, - TypeCombinator::remove($varIterableKeyType->toString(), new ConstantStringType('')), - ); - - if (!$varType->hasOffsetValueType(new ConstantIntegerType(0))->no()) { - $narrowedKey = TypeCombinator::union( - $narrowedKey, - new ConstantBooleanType(false), - ); - } - - if (!$varType->hasOffsetValueType(new ConstantIntegerType(1))->no()) { - $narrowedKey = TypeCombinator::union( - $narrowedKey, - new ConstantBooleanType(true), - ); - } - - if (!$varType->hasOffsetValueType(new ConstantStringType(''))->no()) { - $narrowedKey = TypeCombinator::addNull($narrowedKey); - } - - if (!$varIterableKeyType->isNumericString()->no() || !$varIterableKeyType->isInteger()->no()) { - $narrowedKey = TypeCombinator::union($narrowedKey, new FloatType()); - } - } else { - $narrowedKey = new MixedType( - false, - new UnionType([ - new ArrayType(new MixedType(), new MixedType()), - new ObjectWithoutClassType(), - new ResourceType(), - ]), - ); - } - + $narrowedKey = AllowedArrayKeysTypes::narrowOffsetKeyType($varType); + if ($narrowedKey !== null) { $types = $types->unionWith( $this->create( $var->dim, diff --git a/src/Rules/Arrays/AllowedArrayKeysTypes.php b/src/Rules/Arrays/AllowedArrayKeysTypes.php index 0bc7c1f4c4..eb7de446ad 100644 --- a/src/Rules/Arrays/AllowedArrayKeysTypes.php +++ b/src/Rules/Arrays/AllowedArrayKeysTypes.php @@ -2,12 +2,20 @@ namespace PHPStan\Rules\Arrays; +use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; +use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\FloatType; use PHPStan\Type\IntegerType; +use PHPStan\Type\MixedType; use PHPStan\Type\NullType; +use PHPStan\Type\ObjectWithoutClassType; +use PHPStan\Type\ResourceType; use PHPStan\Type\StringType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; final class AllowedArrayKeysTypes @@ -24,4 +32,51 @@ public static function getType(): Type ]); } + public static function narrowOffsetKeyType(Type $varType): ?Type { + if (!$varType->isArray()->yes() || $varType->isIterableAtLeastOnce()->no()) { + return null; + } + + $varIterableKeyType = $varType->getIterableKeyType(); + + if ($varIterableKeyType->isConstantScalarValue()->yes()) { + $narrowedKey = TypeCombinator::union( + $varIterableKeyType, + TypeCombinator::remove($varIterableKeyType->toString(), new ConstantStringType('')), + ); + + if (!$varType->hasOffsetValueType(new ConstantIntegerType(0))->no()) { + $narrowedKey = TypeCombinator::union( + $narrowedKey, + new ConstantBooleanType(false), + ); + } + + if (!$varType->hasOffsetValueType(new ConstantIntegerType(1))->no()) { + $narrowedKey = TypeCombinator::union( + $narrowedKey, + new ConstantBooleanType(true), + ); + } + + if (!$varType->hasOffsetValueType(new ConstantStringType(''))->no()) { + $narrowedKey = TypeCombinator::addNull($narrowedKey); + } + + if (!$varIterableKeyType->isNumericString()->no() || !$varIterableKeyType->isInteger()->no()) { + $narrowedKey = TypeCombinator::union($narrowedKey, new FloatType()); + } + } else { + $narrowedKey = new MixedType( + false, + new UnionType([ + new ArrayType(new MixedType(), new MixedType()), + new ObjectWithoutClassType(), + new ResourceType(), + ]), + ); + } + + return $narrowedKey; + } } From 2f2e8429f25c7a93501434c293e08f9778e89f34 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 23 Sep 2024 12:31:44 +0200 Subject: [PATCH 2/2] isset() narrows string-key in int-keyed-array to numeric-string --- src/Analyser/TypeSpecifier.php | 2 +- src/Rules/Arrays/AllowedArrayKeysTypes.php | 26 ++++++++++-------- tests/PHPStan/Analyser/nsrt/bug-11716.php | 32 ++++++++++++++++++---- 3 files changed, 43 insertions(+), 17 deletions(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 6a40c74454..fb8b5985e7 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -821,7 +821,7 @@ public function specifyTypesInCondition( ); } else { $varType = $scope->getType($var->var); - $narrowedKey = AllowedArrayKeysTypes::narrowOffsetKeyType($varType); + $narrowedKey = AllowedArrayKeysTypes::narrowOffsetKeyType($varType, $dimType); if ($narrowedKey !== null) { $types = $types->unionWith( $this->create( diff --git a/src/Rules/Arrays/AllowedArrayKeysTypes.php b/src/Rules/Arrays/AllowedArrayKeysTypes.php index eb7de446ad..2b15a4eb65 100644 --- a/src/Rules/Arrays/AllowedArrayKeysTypes.php +++ b/src/Rules/Arrays/AllowedArrayKeysTypes.php @@ -32,7 +32,8 @@ public static function getType(): Type ]); } - public static function narrowOffsetKeyType(Type $varType): ?Type { + public static function narrowOffsetKeyType(Type $varType, Type $keyType): ?Type + { if (!$varType->isArray()->yes() || $varType->isIterableAtLeastOnce()->no()) { return null; } @@ -66,17 +67,20 @@ public static function narrowOffsetKeyType(Type $varType): ?Type { if (!$varIterableKeyType->isNumericString()->no() || !$varIterableKeyType->isInteger()->no()) { $narrowedKey = TypeCombinator::union($narrowedKey, new FloatType()); } - } else { - $narrowedKey = new MixedType( - false, - new UnionType([ - new ArrayType(new MixedType(), new MixedType()), - new ObjectWithoutClassType(), - new ResourceType(), - ]), - ); + + return $narrowedKey; + } elseif ($varIterableKeyType->isInteger()->yes() && $keyType->isString()->yes()) { + return TypeCombinator::intersect($varIterableKeyType->toString(), $keyType); } - return $narrowedKey; + return new MixedType( + false, + new UnionType([ + new ArrayType(new MixedType(), new MixedType()), + new ObjectWithoutClassType(), + new ResourceType(), + ]), + ); } + } diff --git a/tests/PHPStan/Analyser/nsrt/bug-11716.php b/tests/PHPStan/Analyser/nsrt/bug-11716.php index 2394e8a175..e637669483 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11716.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11716.php @@ -35,9 +35,10 @@ public function parse(string $glue): string } /** - * @param array $arr + * @param array $intKeyedArr + * @param array $stringKeyedArr */ -function narrowKey($mixed, string $s, int $i, array $generalArr, array $arr): void { +function narrowKey($mixed, string $s, int $i, array $generalArr, array $intKeyedArr, array $stringKeyedArr): void { if (isset($generalArr[$mixed])) { assertType('mixed~(array|object|resource)', $mixed); } else { @@ -59,21 +60,42 @@ function narrowKey($mixed, string $s, int $i, array $generalArr, array $arr): vo } assertType('string', $s); - if (isset($arr[$mixed])) { + if (isset($intKeyedArr[$mixed])) { assertType('mixed~(array|object|resource)', $mixed); } else { assertType('mixed', $mixed); } assertType('mixed', $mixed); - if (isset($arr[$i])) { + if (isset($intKeyedArr[$i])) { assertType('int', $i); } else { assertType('int', $i); } assertType('int', $i); - if (isset($arr[$s])) { + if (isset($intKeyedArr[$s])) { + assertType("numeric-string", $s); + } else { + assertType('string', $s); + } + assertType('string', $s); + + if (isset($stringKeyedArr[$mixed])) { + assertType('mixed~(array|object|resource)', $mixed); + } else { + assertType('mixed', $mixed); + } + assertType('mixed', $mixed); + + if (isset($stringKeyedArr[$i])) { + assertType('int', $i); + } else { + assertType('int', $i); + } + assertType('int', $i); + + if (isset($stringKeyedArr[$s])) { assertType('string', $s); } else { assertType('string', $s);