diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index cc7fb05857..4ea8a595f5 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -687,6 +687,34 @@ public function specifyTypesInCondition( if ($context->null()) { $specifiedTypes = $this->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->expr, $context)->setRootExpr($expr); + // infer $arr[$key] after $key = array_rand($arr) + if ( + $expr->expr instanceof FuncCall + && $expr->expr->name instanceof Name + && in_array($expr->expr->name->toLowerString(), ['array_rand'], true) + && count($expr->expr->getArgs()) >= 1 + ) { + $numArg = null; + $arrayArg = $expr->expr->getArgs()[0]->value; + if (count($expr->expr->getArgs()) > 1) { + $numArg = $expr->expr->getArgs()[1]->value; + } + $one = new ConstantIntegerType(1); + $arrayType = $scope->getType($arrayArg); + + if ( + $arrayType->isArray()->yes() + && $arrayType->isIterableAtLeastOnce()->yes() + && ($numArg === null || $one->isSuperTypeOf($scope->getType($numArg))->yes()) + ) { + $dimFetch = new ArrayDimFetch($arrayArg, $expr->var); + + return $specifiedTypes->unionWith( + $this->create($dimFetch, $arrayType->getIterableValueType(), TypeSpecifierContext::createTrue(), $scope), + ); + } + } + // infer $arr[$key] after $key = array_key_first/last($arr) if ( $expr->expr instanceof FuncCall diff --git a/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php index f624f1a097..f8eeddc08a 100644 --- a/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php +++ b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php @@ -14,6 +14,7 @@ use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\TrinaryLogic; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\ErrorType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; @@ -129,6 +130,33 @@ public function processNode(Node $node, Scope $scope): array } } + if ( + $node->dim instanceof Node\Expr\FuncCall + && $node->dim->name instanceof Node\Name + && $node->dim->name->toLowerString() === 'array_rand' + && count($node->dim->getArgs()) >= 1 + ) { + $numArg = null; + $arrayArg = $node->dim->getArgs()[0]->value; + if (count($node->dim->getArgs()) > 1) { + $numArg = $node->dim->getArgs()[1]->value; + } + $one = new ConstantIntegerType(1); + $arrayType = $scope->getType($arrayArg); + + if ( + $arrayArg instanceof Node\Expr\Variable + && $node->var instanceof Node\Expr\Variable + && is_string($arrayArg->name) + && $arrayArg->name === $node->var->name + && $arrayType->isArray()->yes() + && $arrayType->isIterableAtLeastOnce()->yes() + && ($numArg === null || $one->isSuperTypeOf($scope->getType($numArg))->yes()) + ) { + return []; + } + } + if ( $node->dim instanceof Node\Expr\BinaryOp\Minus && $node->dim->left instanceof Node\Expr\FuncCall diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 9f7165c275..ddc8c4d332 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -69,6 +69,7 @@ private static function findTestFiles(): iterable } yield __DIR__ . '/../Rules/Methods/data/bug-6856.php'; + yield __DIR__ . '/../Rules/Arrays/data/bug-12981.php'; if (PHP_VERSION_ID < 80000) { yield __DIR__ . '/data/explode-php74.php'; diff --git a/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php index 22716afcd4..3bcdb20815 100644 --- a/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php @@ -116,4 +116,26 @@ public function testBug12273(): void ]); } + public function testBug12981(): void + { + $this->analyse([__DIR__ . '/data/bug-12981.php'], [ + [ + 'Invalid array key type array.', + 31, + ], + [ + 'Invalid array key type array.', + 33, + ], + [ + 'Possibly invalid array key type array|int|string.', + 39, + ], + [ + 'Possibly invalid array key type array|int|string.', + 41, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 230a908c27..471a1a1b01 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -933,6 +933,22 @@ public function testBug12593(): void $this->analyse([__DIR__ . '/data/bug-12593.php'], []); } + public function testBug12981(): void + { + $this->reportPossiblyNonexistentGeneralArrayOffset = true; + + $this->analyse([__DIR__ . '/data/bug-12981.php'], [ + [ + 'Offset array|int|string might not exist on non-empty-array.', + 39, + ], + [ + 'Offset array|int|string might not exist on non-empty-array.', + 41, + ], + ]); + } + public function testBugObject(): void { $this->analyse([__DIR__ . '/data/bug-object.php'], [ diff --git a/tests/PHPStan/Rules/Arrays/data/bug-12981.php b/tests/PHPStan/Rules/Arrays/data/bug-12981.php new file mode 100644 index 0000000000..7cc3168849 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-12981.php @@ -0,0 +1,60 @@ + $arr */ + public function sayHello(array $arr): void + { + echo $arr[array_rand($arr)]; + $randIndex = array_rand($arr); + assertType('bool|float|int|string', $arr[$randIndex]); + echo $arr[$randIndex]; + } + + /** @param non-empty-array $arr */ + public function sayHello1(array $arr): void + { + $num = 1; + echo $arr[array_rand($arr, $num)]; + $randIndex = array_rand($arr, $num); + echo $arr[$randIndex]; + } + + /** @param non-empty-array $arr */ + public function sayHello2(array $arr): void + { + $num = 5; + echo $arr[array_rand($arr, $num)]; + $randIndex = array_rand($arr, $num); + echo $arr[$randIndex]; + } + + /** @param non-empty-array $arr */ + public function sayHello4(array $arr, int $num): void + { + echo $arr[array_rand($arr, $num)]; + $randIndex = array_rand($arr, $num); + echo $arr[$randIndex]; + } + + public function sayHello5(): void + { + $arr = [ + 1 => true, + 2 => false, + 'a' => 'hello', + ]; + assertType("'hello'|bool", $arr[array_rand($arr)]); + + $arr = [ + 1 => true, + 2 => null, + 'a' => 'hello', + ]; + assertType("'hello'|true|null", $arr[array_rand($arr)]); + } +}