diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index a5dc7bc6a5..fb8b5985e7 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, $dimType); + 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..2b15a4eb65 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,55 @@ public static function getType(): Type ]); } + public static function narrowOffsetKeyType(Type $varType, Type $keyType): ?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()); + } + + return $narrowedKey; + } elseif ($varIterableKeyType->isInteger()->yes() && $keyType->isString()->yes()) { + return TypeCombinator::intersect($varIterableKeyType->toString(), $keyType); + } + + 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);