From c859239782b707a31a75fafab468a57049d71bfb Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Sat, 19 Sep 2026 12:53:53 +0200 Subject: [PATCH] Describe `Random\Randomizer` array, bytes and integer methods with dedicated dynamic return type extensions (#6469) Co-authored-by: Claude Opus 5 Co-authored-by: Vincent Langlet --- build/ignore-by-php-version.neon.php | 4 + build/randomizer.neon | 7 + resources/functionMap_php82delta.php | 8 +- resources/functionMap_php83delta.php | 1 + .../ArrayRandFunctionReturnTypeExtension.php | 46 ++++-- .../RandomizerMethodReturnTypeExtension.php | 94 +++++++++++++ .../StrShuffleFunctionReturnTypeExtension.php | 64 +++++++++ stubs/core.stub | 5 - .../PHPStan/Analyser/nsrt/array-functions.php | 28 ++-- tests/PHPStan/Analyser/nsrt/bug-15256-83.php | 33 +++++ .../nsrt/bug-15256-shadowed-functions.php | 95 +++++++++++++ tests/PHPStan/Analyser/nsrt/bug-15256.php | 131 ++++++++++++++++++ tests/PHPStan/Analyser/nsrt/str-shuffle.php | 24 +++- .../InvalidKeyInArrayDimFetchRuleTest.php | 8 +- ...nexistentOffsetInArrayDimFetchRuleTest.php | 4 +- .../PHPStan/Rules/Functions/data/bug-9803.php | 8 +- 16 files changed, 513 insertions(+), 47 deletions(-) create mode 100644 build/randomizer.neon create mode 100644 src/Type/Php/RandomizerMethodReturnTypeExtension.php create mode 100644 src/Type/Php/StrShuffleFunctionReturnTypeExtension.php create mode 100644 tests/PHPStan/Analyser/nsrt/bug-15256-83.php create mode 100644 tests/PHPStan/Analyser/nsrt/bug-15256-shadowed-functions.php create mode 100644 tests/PHPStan/Analyser/nsrt/bug-15256.php diff --git a/build/ignore-by-php-version.neon.php b/build/ignore-by-php-version.neon.php index 1eaaa6863dc..59911622d34 100644 --- a/build/ignore-by-php-version.neon.php +++ b/build/ignore-by-php-version.neon.php @@ -21,6 +21,10 @@ $includes[] = __DIR__ . '/more-enum-adapter-errors.neon'; } +if (PHP_VERSION_ID < 80200) { + $includes[] = __DIR__ . '/randomizer.neon'; +} + if (PHP_VERSION_ID < 80000) { $includes[] = __DIR__ . '/spl-autoload-functions-pre-php-7.neon'; } else { diff --git a/build/randomizer.neon b/build/randomizer.neon new file mode 100644 index 00000000000..102ea874824 --- /dev/null +++ b/build/randomizer.neon @@ -0,0 +1,7 @@ +parameters: + ignoreErrors: + - + message: '#^Class Random\\Randomizer not found\.$#' + identifier: class.notFound + count: 1 + path: ../src/Type/Php/RandomizerMethodReturnTypeExtension.php diff --git a/resources/functionMap_php82delta.php b/resources/functionMap_php82delta.php index 033e6e716b6..afe7fb43253 100644 --- a/resources/functionMap_php82delta.php +++ b/resources/functionMap_php82delta.php @@ -24,7 +24,13 @@ 'iterator_count' => ['0|positive-int', 'iterator'=>'iterable'], 'iterator_to_array' => ['array', 'iterator'=>'iterable', 'use_keys='=>'bool'], 'str_split' => ['list', 'str'=>'string', 'split_length='=>'positive-int'], - 'Random\Randomizer::pickArrayKeys' => ['non-empty-array', 'array'=>'non-empty-array', 'num'=>'positive-int'], + 'Random\Engine\Mt19937::generate' => ['non-empty-string'], + 'Random\Engine\PcgOneseq128XslRr64::generate' => ['non-empty-string'], + 'Random\Engine\Secure::generate' => ['non-empty-string'], + 'Random\Engine\Xoshiro256StarStar::generate' => ['non-empty-string'], + 'Random\Randomizer::getBytes' => ['non-empty-string', 'length'=>'positive-int'], + 'Random\Randomizer::nextInt' => ['int<0, max>'], + 'Random\Randomizer::pickArrayKeys' => ['non-empty-list', 'array'=>'non-empty-array', 'num'=>'positive-int'], ], 'old' => [ diff --git a/resources/functionMap_php83delta.php b/resources/functionMap_php83delta.php index 6b199ec76cd..e96defd162c 100644 --- a/resources/functionMap_php83delta.php +++ b/resources/functionMap_php83delta.php @@ -24,6 +24,7 @@ 'DateInterval::createFromDateString' => ['static', 'modify'=>'string'], 'DateTime::modify' => ['static', 'modify'=>'string'], 'DateTimeImmutable::modify' => ['static', 'modify'=>'string'], + 'Random\Randomizer::getBytesFromString' => ['non-empty-string', 'string'=>'non-empty-string', 'length'=>'positive-int'], 'str_decrement' => ['non-empty-string', 'string'=>'non-empty-string'], 'str_increment' => ['non-falsy-string', 'string'=>'non-empty-string'], 'gc_status' => ['array{running:bool,protected:bool,full:bool,runs:int,collected:int,threshold:int,buffer_size:int,roots:int,application_time:float,collector_time:float,destructor_time:float,free_time:float}'], diff --git a/src/Type/Php/ArrayRandFunctionReturnTypeExtension.php b/src/Type/Php/ArrayRandFunctionReturnTypeExtension.php index 01018174977..b01c2a40022 100644 --- a/src/Type/Php/ArrayRandFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayRandFunctionReturnTypeExtension.php @@ -6,6 +6,8 @@ use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; +use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -35,34 +37,50 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $firstArgType = $scope->getType($args[0]->value); - $isInteger = $firstArgType->getIterableKeyType()->isInteger(); - $isString = $firstArgType->getIterableKeyType()->isString(); - - if ($isInteger->yes()) { - $valueType = new IntegerType(); - } elseif ($isString->yes()) { - $valueType = new StringType(); - } else { - $valueType = new UnionType([new IntegerType(), new StringType()]); - } + $keyType = $this->getPickedKeyType($firstArgType); if ($argsCount < 2) { - return $valueType; + return $keyType; } $secondArgType = $scope->getType($args[1]->value); $one = new ConstantIntegerType(1); if ($one->isSuperTypeOf($secondArgType)->yes()) { - return $valueType; + return $keyType; } + $keysListType = $this->getPickedKeysListType($firstArgType); + $bigger2 = IntegerRangeType::fromInterval(2, null); if ($bigger2->isSuperTypeOf($secondArgType)->yes()) { - return new ArrayType(new IntegerType(), $valueType); + return $keysListType; + } + + return TypeCombinator::union($keyType, $keysListType); + } + + private function getPickedKeyType(Type $arrayType): Type + { + $arrayKeyType = new UnionType([new IntegerType(), new StringType()]); + if ($arrayType->isIterableAtLeastOnce()->no()) { + // Picking out of an empty array always throws, there's no key to describe. + return $arrayKeyType; } - return TypeCombinator::union($valueType, new ArrayType(new IntegerType(), $valueType)); + return TypeCombinator::intersect($arrayType->getIterableKeyType(), $arrayKeyType); + } + + /** + * Picking more than one key returns them re-indexed from zero, keeping their original order. + */ + private function getPickedKeysListType(Type $arrayType): Type + { + return TypeCombinator::intersect( + new ArrayType(new IntegerType(), $this->getPickedKeyType($arrayType)), + new AccessoryArrayListType(), + new NonEmptyArrayType(), + ); } } diff --git a/src/Type/Php/RandomizerMethodReturnTypeExtension.php b/src/Type/Php/RandomizerMethodReturnTypeExtension.php new file mode 100644 index 00000000000..fc17ab82090 --- /dev/null +++ b/src/Type/Php/RandomizerMethodReturnTypeExtension.php @@ -0,0 +1,94 @@ +getName(), [ + 'shuffleArray', + 'pickArrayKeys', + 'shuffleBytes', + 'getInt', + ], true); + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type + { + $args = $methodCall->getArgs(); + if (count($args) < 1) { + return null; + } + + switch ($methodReflection->getName()) { + case 'shuffleArray': + return $scope->getType($args[0]->value)->shuffleArray(); + case 'pickArrayKeys': + if (count($args) < 2) { + return null; + } + + // $num is validated to be between 1 and the size of the array, so unlike + // array_rand() a successful call always returns an array of keys, even + // when a single key is picked - hence the cast to array. + return $scope->getType($this->createFuncCall('array_rand', [ + $args[0]->value, + $args[1]->value, + ]))->toArray(); + case 'shuffleBytes': + return $scope->getType($this->createFuncCall('str_shuffle', [$args[0]->value])); + case 'getInt': + if (count($args) < 2) { + return null; + } + + return $scope->getType($this->createFuncCall('random_int', [ + $args[0]->value, + $args[1]->value, + ])); + } + + return null; + } + + /** + * @param non-empty-string $functionName + * @param list $argValues + */ + private function createFuncCall(string $functionName, array $argValues): FuncCall + { + return new FuncCall( + new FullyQualified($functionName), + array_map(static fn (Expr $argValue): Arg => new Arg($argValue), $argValues), + ); + } + +} diff --git a/src/Type/Php/StrShuffleFunctionReturnTypeExtension.php b/src/Type/Php/StrShuffleFunctionReturnTypeExtension.php new file mode 100644 index 00000000000..f6427fa4f42 --- /dev/null +++ b/src/Type/Php/StrShuffleFunctionReturnTypeExtension.php @@ -0,0 +1,64 @@ +getName() === 'str_shuffle'; + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): ?Type + { + $args = $functionCall->getArgs(); + if (count($args) < 1) { + return null; + } + + // The result contains every byte of the input exactly once, + // so it keeps its emptiness and its casing. + $inputType = $scope->getType($args[0]->value); + $accessoryTypes = []; + if ($inputType->isNonFalsyString()->yes()) { + $accessoryTypes[] = new AccessoryNonFalsyStringType(); + } elseif ($inputType->isNonEmptyString()->yes()) { + $accessoryTypes[] = new AccessoryNonEmptyStringType(); + } + if ($inputType->isLowercaseString()->yes()) { + $accessoryTypes[] = new AccessoryLowercaseStringType(); + } + if ($inputType->isUppercaseString()->yes()) { + $accessoryTypes[] = new AccessoryUppercaseStringType(); + } + + if (count($accessoryTypes) > 0) { + $accessoryTypes[] = new StringType(); + + return new IntersectionType($accessoryTypes); + } + + return null; + } + +} diff --git a/stubs/core.stub b/stubs/core.stub index beee073b9d2..32d59f2b10d 100644 --- a/stubs/core.stub +++ b/stubs/core.stub @@ -132,11 +132,6 @@ function base64_encode(string $string) : string {} */ function bin2hex(string $string): string {} -/** - * @return ($string is non-empty-string ? non-empty-string : string) - */ -function str_shuffle(string $string): string {} - /** * @param array $result * @param-out array|string> $result diff --git a/tests/PHPStan/Analyser/nsrt/array-functions.php b/tests/PHPStan/Analyser/nsrt/array-functions.php index dbbbe0cf76c..b5a640c8cef 100644 --- a/tests/PHPStan/Analyser/nsrt/array-functions.php +++ b/tests/PHPStan/Analyser/nsrt/array-functions.php @@ -351,19 +351,19 @@ assertType('string|null', key($generalStringKeys)); assertType('int|string|null', key($generalIntegerOrStringKeysMixedValues)); assertType('\'foo\'', $poppedFoo); -assertType('int', array_rand([1 => 1, 2 => "2"])); -assertType('string', array_rand(["a" => 1, "b" => "2"])); -assertType('int|string', array_rand(["a" => 1, 2 => "b"])); +assertType('1|2', array_rand([1 => 1, 2 => "2"])); +assertType("'a'|'b'", array_rand(["a" => 1, "b" => "2"])); +assertType("2|'a'", array_rand(["a" => 1, 2 => "b"])); assertType('int|string', array_rand([1 => 1, 2 => "b", $mixed => $mixed])); -assertType('int', array_rand([1 => 1, 2 => "b"], 1)); -assertType('string', array_rand(["a" => 1, "b" => "b"], 1)); -assertType('int|string', array_rand(["a" => 1, 2 => "b"], 1)); +assertType('1|2', array_rand([1 => 1, 2 => "b"], 1)); +assertType("'a'|'b'", array_rand(["a" => 1, "b" => "b"], 1)); +assertType("2|'a'", array_rand(["a" => 1, 2 => "b"], 1)); assertType('int|string', array_rand([1 => 1, 2 => "b", $mixed => $mixed], 1)); -assertType('array', array_rand([1 => 1, 2 => "b"], 2)); -assertType('array', array_rand(["a" => 1, "b" => "b"], 2)); -assertType('array', array_rand(["a" => 1, 2 => "b"], 2)); -assertType('array', array_rand([1 => 1, 2 => "2", $mixed => $mixed], 2)); -assertType('array|int', array_rand([1 => 1, 2 => "b"], $mixed)); -assertType('array|string', array_rand(["a" => 1, "b" => "b"], $mixed)); -assertType('array|int|string', array_rand(["a" => 1, 2 => "b"], $mixed)); -assertType('array|int|string', array_rand([1 => 1, 2 => "b", $mixed => $mixed], $mixed)); +assertType('non-empty-list<1|2>', array_rand([1 => 1, 2 => "b"], 2)); +assertType("non-empty-list<'a'|'b'>", array_rand(["a" => 1, "b" => "b"], 2)); +assertType("non-empty-list<2|'a'>", array_rand(["a" => 1, 2 => "b"], 2)); +assertType('non-empty-list', array_rand([1 => 1, 2 => "2", $mixed => $mixed], 2)); +assertType('1|2|non-empty-list<1|2>', array_rand([1 => 1, 2 => "b"], $mixed)); +assertType("'a'|'b'|non-empty-list<'a'|'b'>", array_rand(["a" => 1, "b" => "b"], $mixed)); +assertType("2|'a'|non-empty-list<2|'a'>", array_rand(["a" => 1, 2 => "b"], $mixed)); +assertType('int|non-empty-list|string', array_rand([1 => 1, 2 => "b", $mixed => $mixed], $mixed)); diff --git a/tests/PHPStan/Analyser/nsrt/bug-15256-83.php b/tests/PHPStan/Analyser/nsrt/bug-15256-83.php new file mode 100644 index 00000000000..660e53dc9bb --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-15256-83.php @@ -0,0 +1,33 @@ += 8.3 + +declare(strict_types = 1); + +namespace Bug15256Php83; + +use Random\IntervalBoundary; +use Random\Randomizer; +use function PHPStan\Testing\assertType; + +/** + * @param non-empty-string $nonEmptyString + * @param lowercase-string $lowercaseString + */ +function getBytesFromString( + Randomizer $randomizer, + string $nonEmptyString, + string $lowercaseString +): void +{ + assertType('non-empty-string', $randomizer->getBytesFromString($nonEmptyString, 5)); + assertType('non-empty-string', $randomizer->getBytesFromString($lowercaseString, 5)); + assertType('non-empty-string', $randomizer->getBytesFromString('abc', 5)); +} + +// PHPStan has no float range types, so there is nothing more precise to say +// about these than the float the class declares. +function floatMethods(Randomizer $randomizer, float $float): void +{ + assertType('float', $randomizer->nextFloat()); + assertType('float', $randomizer->getFloat(0.0, 1.0)); + assertType('float', $randomizer->getFloat($float, $float, IntervalBoundary::ClosedClosed)); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-15256-shadowed-functions.php b/tests/PHPStan/Analyser/nsrt/bug-15256-shadowed-functions.php new file mode 100644 index 00000000000..fbfa80780d3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-15256-shadowed-functions.php @@ -0,0 +1,95 @@ += 8.2 + +declare(strict_types = 1); + +namespace Bug15256Shadowed; + +use function PHPStan\Testing\assertType; + +/** + * @param non-empty-array $array + * @return array{'shadowed'} + */ +function array_rand(array $array, int $num = 1): array +{ + return ['shadowed']; +} + +/** + * @return 'shadowed' + */ +function str_shuffle(string $string): string +{ + return 'shadowed'; +} + +/** + * @return 999 + */ +function random_int(int $min, int $max): int +{ + return 999; +} + +class Randomizer +{ + + /** + * @param array $array + * @return array + */ + public function pickArrayKeys(array $array, int $num): array + { + return []; + } + + public function shuffleBytes(string $bytes): string + { + return $bytes; + } + + public function getInt(int $min, int $max): int + { + return $min; + } + +} + +/** + * The extension builds the calls it borrows types from with fully qualified names, + * so functions of the same name in the current namespace do not take over. + * + * @param non-empty-array $nonEmptyArray + * @param non-empty-string $nonEmptyString + */ +function shadowedFunctions( + \Random\Randomizer $randomizer, + array $nonEmptyArray, + string $nonEmptyString +): void +{ + assertType("array{'shadowed'}", array_rand($nonEmptyArray, 2)); + assertType("'shadowed'", str_shuffle($nonEmptyString)); + assertType('999', random_int(1, 10)); + + assertType('non-empty-list', $randomizer->pickArrayKeys($nonEmptyArray, 2)); + assertType('non-empty-string', $randomizer->shuffleBytes($nonEmptyString)); + assertType('int<1, 10>', $randomizer->getInt(1, 10)); +} + +/** + * A class of the same name in another namespace is not described by the extension. + * + * @param non-empty-array $nonEmptyArray + * @param non-empty-string $nonEmptyString + */ +function shadowedClass( + Randomizer $randomizer, + array $nonEmptyArray, + string $nonEmptyString +): void +{ + assertType('array', $randomizer->pickArrayKeys($nonEmptyArray, 2)); + assertType('string', $randomizer->shuffleBytes($nonEmptyString)); + assertType('int', $randomizer->getInt(1, 10)); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-15256.php b/tests/PHPStan/Analyser/nsrt/bug-15256.php new file mode 100644 index 00000000000..27e839a3ec2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-15256.php @@ -0,0 +1,131 @@ += 8.2 + +declare(strict_types = 1); + +namespace Bug15256; + +use Random\Engine\Mt19937; +use Random\Engine\PcgOneseq128XslRr64; +use Random\Engine\Secure; +use Random\Engine\Xoshiro256StarStar; +use Random\Randomizer; +use function PHPStan\Testing\assertType; + +class Shuffle +{ + + /** + * @template T + * @param T[] $array + * @return list + */ + public static function shuffle(array $array): array + { + $randomizer = new Randomizer(); + assertType('list', $randomizer->shuffleArray($array)); + + return $randomizer->shuffleArray($array); + } + + /** + * @template TKey of array-key + * @param non-empty-array $array + * @return non-empty-list + */ + public static function pick3ArrayKeys(array $array): array + { + $randomizer = new Randomizer(); + assertType('non-empty-list', $randomizer->pickArrayKeys($array, 3)); + + return $randomizer->pickArrayKeys($array, 3); + } + +} + +/** + * @param non-empty-array $nonEmptyArray + * @param list $list + * @param array $mixedKeys + * @param non-empty-array $numericStringKeys + * @param non-empty-array $decimalIntStringKeys + * @param non-empty-array $nonDecimalIntStringKeys + * @param positive-int $num + */ +function arrayMethods( + Randomizer $randomizer, + array $nonEmptyArray, + array $list, + array $mixedKeys, + array $numericStringKeys, + array $decimalIntStringKeys, + array $nonDecimalIntStringKeys, + int $num +): void +{ + assertType('non-empty-list', $randomizer->shuffleArray($nonEmptyArray)); + assertType('list', $randomizer->shuffleArray($list)); + assertType('non-empty-list<1|2>', $randomizer->shuffleArray(['a' => 1, 'b' => 2])); + assertType('array{}', $randomizer->shuffleArray([])); + assertType('list', $randomizer->shuffleArray($mixedKeys)); + + assertType('non-empty-list', $randomizer->pickArrayKeys($nonEmptyArray, 2)); + assertType('non-empty-list>', $randomizer->pickArrayKeys($list, 2)); + assertType('non-empty-list', $randomizer->pickArrayKeys($mixedKeys, 2)); + assertType("non-empty-list<'a'|'b'>", $randomizer->pickArrayKeys(['a' => 1, 'b' => 2], 2)); + + // Numeric string keys are cast to integers, just like array_rand() describes them. + assertType('non-empty-list', $randomizer->pickArrayKeys($numericStringKeys, 2)); + assertType('non-empty-list<1|2>', $randomizer->pickArrayKeys(['1' => 'a', '2' => 'b'], 2)); + assertType("non-empty-list<1|'b'>", $randomizer->pickArrayKeys(['1' => 'a', 'b' => 'b'], 2)); + + // A decimal-int-string key is always cast to an integer, a non-decimal-int-string one never is. + assertType('non-empty-list', $randomizer->pickArrayKeys($decimalIntStringKeys, 2)); + assertType('non-empty-list', $randomizer->pickArrayKeys($nonDecimalIntStringKeys, 2)); + assertType('array{int}', $randomizer->pickArrayKeys($decimalIntStringKeys, 1)); + assertType('array{non-decimal-int-string}', $randomizer->pickArrayKeys($nonDecimalIntStringKeys, 1)); + + // Unlike array_rand(), picking a single key still returns an array. + assertType('array{string}', $randomizer->pickArrayKeys($nonEmptyArray, 1)); + assertType("array{'a'}|array{'b'}", $randomizer->pickArrayKeys(['a' => 1, 'b' => 2], 1)); + assertType('non-empty-list', $randomizer->pickArrayKeys($nonEmptyArray, $num)); +} + +/** + * @param non-empty-string $nonEmptyString + * @param non-falsy-string $nonFalsyString + * @param lowercase-string $lowercaseString + * @param uppercase-string $uppercaseString + */ +function stringMethods( + Randomizer $randomizer, + string $string, + string $nonEmptyString, + string $nonFalsyString, + string $lowercaseString, + string $uppercaseString +): void +{ + assertType('string', $randomizer->shuffleBytes($string)); + assertType('non-empty-string', $randomizer->shuffleBytes($nonEmptyString)); + assertType('non-falsy-string', $randomizer->shuffleBytes($nonFalsyString)); + assertType('lowercase-string', $randomizer->shuffleBytes($lowercaseString)); + assertType('uppercase-string', $randomizer->shuffleBytes($uppercaseString)); + assertType('lowercase-string&non-falsy-string', $randomizer->shuffleBytes('abc')); + + assertType('non-empty-string', $randomizer->getBytes(5)); +} + +function numberMethods(Randomizer $randomizer, int $int): void +{ + assertType('int<1, 10>', $randomizer->getInt(1, 10)); + assertType('int<0, max>', $randomizer->getInt(0, $int)); + assertType('int<0, max>', $randomizer->nextInt()); +} + +function engines(): void +{ + assertType('non-empty-string', (new Mt19937())->generate()); + assertType('non-empty-string', (new PcgOneseq128XslRr64())->generate()); + assertType('non-empty-string', (new Secure())->generate()); + assertType('non-empty-string', (new Xoshiro256StarStar())->generate()); +} diff --git a/tests/PHPStan/Analyser/nsrt/str-shuffle.php b/tests/PHPStan/Analyser/nsrt/str-shuffle.php index 37aa7685252..ab0721361b8 100644 --- a/tests/PHPStan/Analyser/nsrt/str-shuffle.php +++ b/tests/PHPStan/Analyser/nsrt/str-shuffle.php @@ -9,11 +9,29 @@ class X { /** * @param non-empty-string $nonES + * @param non-falsy-string $nonFalsyString + * @param numeric-string $numericString + * @param lowercase-string $lowercaseString + * @param uppercase-string $uppercaseString + * @param uppercase-string&non-empty-string $uppercaseNonEmptyString */ - function doFoo(string $s, $nonES):void { - assertType('non-empty-string', str_shuffle(self::ABC)); - assertType('non-empty-string', str_shuffle('abc')); + function doFoo( + string $s, + $nonES, + string $nonFalsyString, + string $numericString, + string $lowercaseString, + string $uppercaseString, + string $uppercaseNonEmptyString + ):void { + assertType('lowercase-string&non-falsy-string', str_shuffle(self::ABC)); + assertType('lowercase-string&non-falsy-string', str_shuffle('abc')); assertType('string', str_shuffle($s)); assertType('non-empty-string', str_shuffle($nonES)); + assertType('non-falsy-string', str_shuffle($nonFalsyString)); + assertType('non-empty-string', str_shuffle($numericString)); + assertType('lowercase-string', str_shuffle($lowercaseString)); + assertType('uppercase-string', str_shuffle($uppercaseString)); + assertType('non-empty-string&uppercase-string', str_shuffle($uppercaseNonEmptyString)); } } diff --git a/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php index 45911ff6d6a..94a97036425 100644 --- a/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php @@ -157,19 +157,19 @@ public function testBug12981(): void { $this->analyse([__DIR__ . '/data/bug-12981.php'], [ [ - 'Invalid array key type array.', + 'Invalid array key type list<(int|string)>.', 31, ], [ - 'Invalid array key type array.', + 'Invalid array key type list<(int|string)>.', 33, ], [ - 'Possibly invalid array key type array|int|string.', + 'Possibly invalid array key type int|list<(int|string)>|string.', 39, ], [ - 'Possibly invalid array key type array|int|string.', + 'Possibly invalid array key type int|list<(int|string)>|string.', 41, ], ]); diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 7e495b8a32a..2fb885d3652 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -982,11 +982,11 @@ public function testBug12981(): void $this->analyse([__DIR__ . '/data/bug-12981.php'], [ [ - 'Offset array|int|string might not exist on non-empty-array.', + 'Offset int|non-empty-list<(int|string)>|string might not exist on non-empty-array.', 39, ], [ - 'Offset array|int|string might not exist on non-empty-array.', + 'Offset int|non-empty-list<(int|string)>|string might not exist on non-empty-array.', 41, ], ]); diff --git a/tests/PHPStan/Rules/Functions/data/bug-9803.php b/tests/PHPStan/Rules/Functions/data/bug-9803.php index 6e02f6ea991..d7f6a6a8d20 100644 --- a/tests/PHPStan/Rules/Functions/data/bug-9803.php +++ b/tests/PHPStan/Rules/Functions/data/bug-9803.php @@ -13,16 +13,16 @@ function doFoo() { $keys = array(); if ($random == 1) { $keys = array(array_rand($array)); - assertType('array{int}', $keys); + assertType('array{0|1|2|3|4|5|6|7|8|9}', $keys); } else { $keys = array_rand($array, $random); - assertType('array', $keys); + assertType('non-empty-list<0|1|2|3|4|5|6|7|8|9>', $keys); } - assertType('array', $keys); + assertType('non-empty-list<0|1|2|3|4|5|6|7|8|9>', $keys); $theKeys = array_keys($keys); - assertType('list', $theKeys); + assertType('non-empty-list>', $theKeys); }