From 342b1ef8e903c77fa2a90488a854a252f619de95 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 28 Sep 2025 13:45:20 +0200 Subject: [PATCH 1/8] Fix "array_rand() - offset might not exists" --- src/Analyser/TypeSpecifier.php | 26 ++++++++++++++ .../NonexistentOffsetInArrayDimFetchRule.php | 25 +++++++++++++ ...nexistentOffsetInArrayDimFetchRuleTest.php | 17 +++++++++ tests/PHPStan/Rules/Arrays/data/bug-12981.php | 35 +++++++++++++++++++ 4 files changed, 103 insertions(+) create mode 100644 tests/PHPStan/Rules/Arrays/data/bug-12981.php diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index cc7fb05857..6e7abd9e68 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -687,6 +687,32 @@ public function specifyTypesInCondition( if ($context->null()) { $specifiedTypes = $this->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->expr, $context)->setRootExpr($expr); + // infer $arr[$key] after $arr[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 + ) { + $arrayArg = $expr->expr->getArgs()[0]->value; + $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); + $iterableValueType = $arrayType->getFirstIterableValueType(); + + return $specifiedTypes->unionWith( + $this->create($dimFetch, $iterableValueType, 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..79c3bbc81a 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,30 @@ 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 + ) { + $arrayArg = $node->dim->getArgs()[0]->value; + $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/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 230a908c27..9c6a54d3f7 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -933,6 +933,23 @@ 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 might not exist on non-empty-array.', + 31 + ], + [ + 'Offset array might not exist on non-empty-array.', + 33 + ], + ]); + } + + 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..f6c1d54ef8 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-12981.php @@ -0,0 +1,35 @@ + $arr */ + public function sayHello(array $arr): void + { + echo $arr[array_rand($arr)]; + $randIndex = array_rand($arr); + 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]; + } +} From a3e2972e42cbb8d28456e374d3a7375c5e325adc Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 28 Sep 2025 13:51:30 +0200 Subject: [PATCH 2/8] fix --- src/Analyser/TypeSpecifier.php | 5 ++++- src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php | 7 +++++-- .../Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php | 5 ++--- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 6e7abd9e68..7530458770 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -694,8 +694,11 @@ public function specifyTypesInCondition( && in_array($expr->expr->name->toLowerString(), ['array_rand'], true) && count($expr->expr->getArgs()) >= 1 ) { + $numArg = null; $arrayArg = $expr->expr->getArgs()[0]->value; - $numArg = $expr->expr->getArgs()[1]->value; + if (count($expr->expr->getArgs()) > 1) { + $numArg = $expr->expr->getArgs()[1]->value; + } $one = new ConstantIntegerType(1); $arrayType = $scope->getType($arrayArg); diff --git a/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php index 79c3bbc81a..f8eeddc08a 100644 --- a/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php +++ b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php @@ -136,11 +136,14 @@ public function processNode(Node $node, Scope $scope): array && $node->dim->name->toLowerString() === 'array_rand' && count($node->dim->getArgs()) >= 1 ) { + $numArg = null; $arrayArg = $node->dim->getArgs()[0]->value; - $numArg = $node->dim->getArgs()[1]->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 diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 9c6a54d3f7..a5b0c26899 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -940,16 +940,15 @@ public function testBug12981(): void $this->analyse([__DIR__ . '/data/bug-12981.php'], [ [ 'Offset array might not exist on non-empty-array.', - 31 + 31, ], [ 'Offset array might not exist on non-empty-array.', - 33 + 33, ], ]); } - public function testBugObject(): void { $this->analyse([__DIR__ . '/data/bug-object.php'], [ From 40a218d5e53a0b6cc4a292eb3a2ae3150e971fc1 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 28 Sep 2025 14:00:13 +0200 Subject: [PATCH 3/8] Update TypeSpecifier.php --- src/Analyser/TypeSpecifier.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 7530458770..b52427105c 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -687,7 +687,7 @@ public function specifyTypesInCondition( if ($context->null()) { $specifiedTypes = $this->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->expr, $context)->setRootExpr($expr); - // infer $arr[$key] after $arr[array_rand($arr)] + // infer $arr[$key] after $key = array_rand($arr) if ( $expr->expr instanceof FuncCall && $expr->expr->name instanceof Name From e9602290b6fad607e2db92b8303eee0306ce45a9 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 29 Sep 2025 08:30:52 +0200 Subject: [PATCH 4/8] test --- .../Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php | 8 ++++++++ tests/PHPStan/Rules/Arrays/data/bug-12981.php | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index a5b0c26899..84d9ddd7ab 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -946,6 +946,14 @@ public function testBug12981(): void 'Offset array might not exist on non-empty-array.', 33, ], + [ + 'Offset array|int|string might not exist on non-empty-array.', + 39, + ], + [ + 'Offset array|int|string might not exist on non-empty-array.', + 41, + ], ]); } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-12981.php b/tests/PHPStan/Rules/Arrays/data/bug-12981.php index f6c1d54ef8..f0f29cd0e5 100644 --- a/tests/PHPStan/Rules/Arrays/data/bug-12981.php +++ b/tests/PHPStan/Rules/Arrays/data/bug-12981.php @@ -32,4 +32,12 @@ public function sayHello2(array $arr): void $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]; + } } From 8a1e19fb6b4b8d39a1740e825252ccf7b53239e3 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 2 Oct 2025 09:36:18 +0200 Subject: [PATCH 5/8] fix --- src/Analyser/TypeSpecifier.php | 3 +-- .../PHPStan/Analyser/NodeScopeResolverTest.php | 1 + tests/PHPStan/Rules/Arrays/data/bug-12981.php | 18 ++++++++++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index b52427105c..4ea8a595f5 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -708,10 +708,9 @@ public function specifyTypesInCondition( && ($numArg === null || $one->isSuperTypeOf($scope->getType($numArg))->yes()) ) { $dimFetch = new ArrayDimFetch($arrayArg, $expr->var); - $iterableValueType = $arrayType->getFirstIterableValueType(); return $specifiedTypes->unionWith( - $this->create($dimFetch, $iterableValueType, TypeSpecifierContext::createTrue(), $scope), + $this->create($dimFetch, $arrayType->getIterableValueType(), TypeSpecifierContext::createTrue(), $scope), ); } } 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/data/bug-12981.php b/tests/PHPStan/Rules/Arrays/data/bug-12981.php index f0f29cd0e5..0cc40256f4 100644 --- a/tests/PHPStan/Rules/Arrays/data/bug-12981.php +++ b/tests/PHPStan/Rules/Arrays/data/bug-12981.php @@ -12,6 +12,7 @@ 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]; } @@ -40,4 +41,21 @@ public function sayHello4(array $arr, int $num): void $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)]); + } } From 982967f27d9356bc239f0d0700aa3ff022fd7ce3 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 2 Oct 2025 09:46:17 +0200 Subject: [PATCH 6/8] Update bug-12981.php --- tests/PHPStan/Rules/Arrays/data/bug-12981.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/PHPStan/Rules/Arrays/data/bug-12981.php b/tests/PHPStan/Rules/Arrays/data/bug-12981.php index 0cc40256f4..7cc3168849 100644 --- a/tests/PHPStan/Rules/Arrays/data/bug-12981.php +++ b/tests/PHPStan/Rules/Arrays/data/bug-12981.php @@ -2,7 +2,6 @@ namespace Bug12981; -use function PHPStan\dumpType; use function PHPStan\Testing\assertType; class HelloWorld From be7d2e94071cd91ee321f77e03f986e76e861a2d Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 2 Oct 2025 15:15:52 +0200 Subject: [PATCH 7/8] Update NonexistentOffsetInArrayDimFetchRuleTest.php --- .../Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 84d9ddd7ab..471a1a1b01 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -938,14 +938,6 @@ public function testBug12981(): void $this->reportPossiblyNonexistentGeneralArrayOffset = true; $this->analyse([__DIR__ . '/data/bug-12981.php'], [ - [ - 'Offset array might not exist on non-empty-array.', - 31, - ], - [ - 'Offset array might not exist on non-empty-array.', - 33, - ], [ 'Offset array|int|string might not exist on non-empty-array.', 39, From 2c8a30c1d7a32e611214eafb887272870d5f8ee7 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 2 Oct 2025 15:19:24 +0200 Subject: [PATCH 8/8] Update InvalidKeyInArrayDimFetchRuleTest.php --- .../InvalidKeyInArrayDimFetchRuleTest.php | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) 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, + ], + ]); + } + }