diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 9f23cf6472..a5dc7bc6a5 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -818,6 +818,60 @@ public function specifyTypesInCondition( $rootExpr, ), ); + } 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(), + ]), + ); + } + + $types = $types->unionWith( + $this->create( + $var->dim, + $narrowedKey, + $context, + false, + $scope, + $rootExpr, + ), + ); + } } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-11716.php b/tests/PHPStan/Analyser/nsrt/bug-11716.php new file mode 100644 index 0000000000..2394e8a175 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11716.php @@ -0,0 +1,214 @@ += 8.0 + +namespace Bug11716; + +use function PHPStan\Testing\assertType; + +class TypeExpression +{ + /** + * @return '&'|'|' + */ + public function parse(string $glue): string + { + $seenGlues = ['|' => false, '&' => false]; + + assertType("array{|: false, &: false}", $seenGlues); + + if ($glue !== '') { + assertType('non-empty-string', $glue); + + \assert(isset($seenGlues[$glue])); + $seenGlues[$glue] = true; + + assertType("'&'|'|'", $glue); + assertType('array{|: bool, &: bool}', $seenGlues); + } else { + assertType("''", $glue); + } + + assertType("''|'&'|'|'", $glue); + assertType("array{|: bool, &: bool}", $seenGlues); + + return array_key_first($seenGlues); + } +} + +/** + * @param array $arr + */ +function narrowKey($mixed, string $s, int $i, array $generalArr, array $arr): void { + if (isset($generalArr[$mixed])) { + assertType('mixed~(array|object|resource)', $mixed); + } else { + assertType('mixed', $mixed); + } + assertType('mixed', $mixed); + + if (isset($generalArr[$i])) { + assertType('int', $i); + } else { + assertType('int', $i); + } + assertType('int', $i); + + if (isset($generalArr[$s])) { + assertType('string', $s); + } else { + assertType('string', $s); + } + assertType('string', $s); + + if (isset($arr[$mixed])) { + assertType('mixed~(array|object|resource)', $mixed); + } else { + assertType('mixed', $mixed); + } + assertType('mixed', $mixed); + + if (isset($arr[$i])) { + assertType('int', $i); + } else { + assertType('int', $i); + } + assertType('int', $i); + + if (isset($arr[$s])) { + assertType('string', $s); + } else { + assertType('string', $s); + } + assertType('string', $s); +} + +/** + * @param array> $arr + */ +function multiDim($mixed, $mixed2, array $arr) { + if (isset($arr[$mixed])) { + assertType('mixed~(array|object|resource)', $mixed); + } else { + assertType('mixed', $mixed); + } + assertType('mixed', $mixed); + + if (isset($arr[$mixed]) && isset($arr[$mixed][$mixed2])) { + assertType('mixed~(array|object|resource)', $mixed); + assertType('mixed~(array|object|resource)', $mixed2); + } else { + assertType('mixed', $mixed); + } + assertType('mixed', $mixed); + + if (isset($arr[$mixed][$mixed2])) { + assertType('mixed~(array|object|resource)', $mixed); + assertType('mixed~(array|object|resource)', $mixed2); + } else { + assertType('mixed', $mixed); + assertType('mixed', $mixed2); + } + assertType('mixed', $mixed); + assertType('mixed', $mixed2); +} + +/** + * @param array $arr + */ +function emptyArrr($mixed, array $arr) +{ + if (count($arr) !== 0) { + return; + } + + assertType('array{}', $arr); + if (isset($arr[$mixed])) { + assertType('mixed', $mixed); + } else { + assertType('mixed', $mixed); + } + assertType('mixed', $mixed); +} + +function emptyString($mixed) +{ + // see https://3v4l.org/XHZdr + $arr = ['' => 1, 'a' => 2]; + if (isset($arr[$mixed])) { + assertType("''|'a'|null", $mixed); + } else { + assertType('mixed', $mixed); // could be mixed~(''|'a'|null) + } + assertType('mixed', $mixed); +} + +function numericString($mixed, int $i, string $s) +{ + $arr = ['1' => 1, '2' => 2]; + if (isset($arr[$mixed])) { + assertType("1|2|'1'|'2'|float|true", $mixed); + } else { + assertType('mixed', $mixed); + } + assertType('mixed', $mixed); + + $arr = ['0' => 1, '2' => 2]; + if (isset($arr[$mixed])) { + assertType("0|2|'0'|'2'|float|false", $mixed); + } else { + assertType('mixed', $mixed); + } + assertType('mixed', $mixed); + + $arr = ['1' => 1, '2' => 2]; + if (isset($arr[$i])) { + assertType("1|2", $i); + } else { + assertType('int', $i); + } + assertType('int', $i); + + $arr = ['1' => 1, '2' => 2, 3 => 3]; + if (isset($arr[$s])) { + assertType("'1'|'2'|'3'", $s); + } else { + assertType('string', $s); + } + assertType('string', $s); + + $arr = ['1' => 1, '2' => 2, 3 => 3]; + if (isset($arr[substr($s, 10)])) { + assertType("string", $s); + assertType("'1'|'2'|'3'", substr($s, 10)); + } else { + assertType('string', $s); + } + assertType('string', $s); +} + +function intKeys($mixed) +{ + $arr = [1 => 1, 2 => 2]; + if (isset($arr[$mixed])) { + assertType("1|2|'1'|'2'|float|true", $mixed); + } else { + assertType('mixed', $mixed); + } + assertType('mixed', $mixed); + + $arr = [0 => 0, 1 => 1, 2 => 2]; + if (isset($arr[$mixed])) { + assertType("0|1|2|'0'|'1'|'2'|bool|float", $mixed); + } else { + assertType('mixed', $mixed); + } + assertType('mixed', $mixed); +} + +function arrayAccess(\ArrayAccess $arr, $mixed) { + if (isset($arr[$mixed])) { + assertType("mixed", $mixed); + } else { + assertType('mixed', $mixed); + } + assertType('mixed', $mixed); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-8559.php b/tests/PHPStan/Analyser/nsrt/bug-8559.php new file mode 100644 index 0000000000..ee68b2fff0 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8559.php @@ -0,0 +1,40 @@ + 1, 'b' => 2]; + + /** + * @phpstan-assert key-of $key + * @return value-of + */ + public static function get(string $key): int + { + assert(isset(self::KEYS[$key])); + assertType("'a'|'b'", $key); + return self::KEYS[$key]; + } + + /** + * @phpstan-assert key-of $key + * @return value-of + */ + public static function get2(string $key): int + { + assert(in_array($key, array_keys(self::KEYS), true)); + assertType("'a'|'b'", $key); + return self::KEYS[$key]; + } +} + +$key = 'x'; +$v = X::get($key); +assertType("*NEVER*", $key); + +$key = 'a'; +$v = X::get($key); +assertType("'a'", $key);