-
Notifications
You must be signed in to change notification settings - Fork 588
Describe Random\Randomizer array, bytes and integer methods with dedicated dynamic return type extensions
#6469
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
VincentLanglet
merged 14 commits into
phpstan:2.2.x
from
phpstan-bot:create-pull-request/patch-ctxs42z
Sep 19, 2026
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
e62aa81
Describe `Random\Randomizer` array, bytes and integer methods with de…
phpstan-bot e6c8585
Leave `Randomizer::getBytesFromString()` to the function map
phpstan-bot 2191d73
Describe Randomizer methods through the extensions of the functions t…
phpstan-bot 091c272
Ask the scope for the type of the function calls Randomizer methods m…
phpstan-bot c5e2b1c
Leave strrev() alone, keep the accessory logic inside each extension
phpstan-bot b7a7245
Describe pickArrayKeys() as an array cast of the array_rand() it mirrors
phpstan-bot 4a92ccf
Fix Ci
VincentLanglet 3fcec81
Guard the Randomizer test file behind lint >= 8.2
phpstan-bot 46a7a21
Drop the now-empty str_shuffle() stub
phpstan-bot bd8eb9f
Assert str_shuffle() over an intersection of accessory string types
phpstan-bot a4f9d63
Cover pickArrayKeys() over arrays with numeric string keys
phpstan-bot fc35282
Cover Randomizer::getFloat() and nextFloat()
phpstan-bot 85a6a1d
Cover Randomizer methods against same-named functions and class in th…
phpstan-bot 27f4408
Cover pickArrayKeys() over decimal-int-string and non-decimal-int-str…
phpstan-bot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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), | ||
|
staabm marked this conversation as resolved.
|
||
| array_map(static fn (Expr $argValue): Arg => new Arg($argValue), $argValues), | ||
| ); | ||
| } | ||
|
|
||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| } | ||
|
|
||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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)); | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.