Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions build/ignore-by-php-version.neon.php
Original file line number Diff line number Diff line change
Expand Up @@ -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-php-8.neon';
}
Expand Down
7 changes: 7 additions & 0 deletions build/randomizer.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
parameters:
ignoreErrors:
-
message: '#^Class Random\\Randomizer not found\.$#'
identifier: class.notFound
count: 1
path: ../src/Type/Php/RandomizerMethodReturnTypeExtension.php
8 changes: 7 additions & 1 deletion resources/functionMap_php82delta.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,13 @@
'iterator_count' => ['0|positive-int', 'iterator'=>'iterable'],
'iterator_to_array' => ['array', 'iterator'=>'iterable', 'use_keys='=>'bool'],
'str_split' => ['list<string>', 'str'=>'string', 'split_length='=>'positive-int'],
'Random\Randomizer::pickArrayKeys' => ['non-empty-array<int|string>', '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<int|string>', 'array'=>'non-empty-array', 'num'=>'positive-int'],
],
'old' => [

Expand Down
1 change: 1 addition & 0 deletions resources/functionMap_php83delta.php
Original file line number Diff line number Diff line change
Expand Up @@ -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}'],
Expand Down
46 changes: 32 additions & 14 deletions src/Type/Php/ArrayRandFunctionReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(),
);
}

}
94 changes: 94 additions & 0 deletions src/Type/Php/RandomizerMethodReturnTypeExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;

use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Name\FullyQualified;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\Type;
use Random\Randomizer;
use function array_map;
use function count;
use function in_array;

/**
* Randomizer methods mirror global functions PHPStan already describes:
* shuffleArray() is shuffle(), pickArrayKeys() is array_rand(),
* shuffleBytes() is str_shuffle() and getInt() is random_int().
*/
#[AutowiredService]
final class RandomizerMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension
{

public function getClass(): string
{
return Randomizer::class;
}

public function isMethodSupported(MethodReflection $methodReflection): bool
{
return in_array($methodReflection->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<Expr> $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),
);
}

}
64 changes: 64 additions & 0 deletions src/Type/Php/StrShuffleFunctionReturnTypeExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;

use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Type\Accessory\AccessoryLowercaseStringType;
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
use PHPStan\Type\Accessory\AccessoryUppercaseStringType;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use function count;

#[AutowiredService]
final class StrShuffleFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension
{

public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return $functionReflection->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;
}

}
5 changes: 0 additions & 5 deletions stubs/core.stub
Original file line number Diff line number Diff line change
Expand Up @@ -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<mixed> $result
* @param-out array<int|string, array<mixed>|string> $result
Expand Down
28 changes: 14 additions & 14 deletions tests/PHPStan/Analyser/nsrt/array-functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<int, int>', array_rand([1 => 1, 2 => "b"], 2));
assertType('array<int, string>', array_rand(["a" => 1, "b" => "b"], 2));
assertType('array<int, int|string>', array_rand(["a" => 1, 2 => "b"], 2));
assertType('array<int, int|string>', array_rand([1 => 1, 2 => "2", $mixed => $mixed], 2));
assertType('array<int, int>|int', array_rand([1 => 1, 2 => "b"], $mixed));
assertType('array<int, string>|string', array_rand(["a" => 1, "b" => "b"], $mixed));
assertType('array<int, int|string>|int|string', array_rand(["a" => 1, 2 => "b"], $mixed));
assertType('array<int, int|string>|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<int|string>', 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<int|string>|string', array_rand([1 => 1, 2 => "b", $mixed => $mixed], $mixed));
33 changes: 33 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-15256-83.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php // lint >= 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));
}
Loading
Loading