Skip to content

Commit bb8faa8

Browse files
authored
Fix "array_rand() - offset might not exists"
1 parent 97289f1 commit bb8faa8

File tree

6 files changed

+155
-0
lines changed

6 files changed

+155
-0
lines changed

src/Analyser/TypeSpecifier.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -687,6 +687,34 @@ public function specifyTypesInCondition(
687687
if ($context->null()) {
688688
$specifiedTypes = $this->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->expr, $context)->setRootExpr($expr);
689689

690+
// infer $arr[$key] after $key = array_rand($arr)
691+
if (
692+
$expr->expr instanceof FuncCall
693+
&& $expr->expr->name instanceof Name
694+
&& in_array($expr->expr->name->toLowerString(), ['array_rand'], true)
695+
&& count($expr->expr->getArgs()) >= 1
696+
) {
697+
$numArg = null;
698+
$arrayArg = $expr->expr->getArgs()[0]->value;
699+
if (count($expr->expr->getArgs()) > 1) {
700+
$numArg = $expr->expr->getArgs()[1]->value;
701+
}
702+
$one = new ConstantIntegerType(1);
703+
$arrayType = $scope->getType($arrayArg);
704+
705+
if (
706+
$arrayType->isArray()->yes()
707+
&& $arrayType->isIterableAtLeastOnce()->yes()
708+
&& ($numArg === null || $one->isSuperTypeOf($scope->getType($numArg))->yes())
709+
) {
710+
$dimFetch = new ArrayDimFetch($arrayArg, $expr->var);
711+
712+
return $specifiedTypes->unionWith(
713+
$this->create($dimFetch, $arrayType->getIterableValueType(), TypeSpecifierContext::createTrue(), $scope),
714+
);
715+
}
716+
}
717+
690718
// infer $arr[$key] after $key = array_key_first/last($arr)
691719
if (
692720
$expr->expr instanceof FuncCall

src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use PHPStan\Rules\RuleErrorBuilder;
1515
use PHPStan\Rules\RuleLevelHelper;
1616
use PHPStan\TrinaryLogic;
17+
use PHPStan\Type\Constant\ConstantIntegerType;
1718
use PHPStan\Type\ErrorType;
1819
use PHPStan\Type\Type;
1920
use PHPStan\Type\VerbosityLevel;
@@ -129,6 +130,33 @@ public function processNode(Node $node, Scope $scope): array
129130
}
130131
}
131132

133+
if (
134+
$node->dim instanceof Node\Expr\FuncCall
135+
&& $node->dim->name instanceof Node\Name
136+
&& $node->dim->name->toLowerString() === 'array_rand'
137+
&& count($node->dim->getArgs()) >= 1
138+
) {
139+
$numArg = null;
140+
$arrayArg = $node->dim->getArgs()[0]->value;
141+
if (count($node->dim->getArgs()) > 1) {
142+
$numArg = $node->dim->getArgs()[1]->value;
143+
}
144+
$one = new ConstantIntegerType(1);
145+
$arrayType = $scope->getType($arrayArg);
146+
147+
if (
148+
$arrayArg instanceof Node\Expr\Variable
149+
&& $node->var instanceof Node\Expr\Variable
150+
&& is_string($arrayArg->name)
151+
&& $arrayArg->name === $node->var->name
152+
&& $arrayType->isArray()->yes()
153+
&& $arrayType->isIterableAtLeastOnce()->yes()
154+
&& ($numArg === null || $one->isSuperTypeOf($scope->getType($numArg))->yes())
155+
) {
156+
return [];
157+
}
158+
}
159+
132160
if (
133161
$node->dim instanceof Node\Expr\BinaryOp\Minus
134162
&& $node->dim->left instanceof Node\Expr\FuncCall

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ private static function findTestFiles(): iterable
6969
}
7070

7171
yield __DIR__ . '/../Rules/Methods/data/bug-6856.php';
72+
yield __DIR__ . '/../Rules/Arrays/data/bug-12981.php';
7273

7374
if (PHP_VERSION_ID < 80000) {
7475
yield __DIR__ . '/data/explode-php74.php';

tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,26 @@ public function testBug12273(): void
116116
]);
117117
}
118118

119+
public function testBug12981(): void
120+
{
121+
$this->analyse([__DIR__ . '/data/bug-12981.php'], [
122+
[
123+
'Invalid array key type array<int, int|string>.',
124+
31,
125+
],
126+
[
127+
'Invalid array key type array<int, int|string>.',
128+
33,
129+
],
130+
[
131+
'Possibly invalid array key type array<int, int|string>|int|string.',
132+
39,
133+
],
134+
[
135+
'Possibly invalid array key type array<int, int|string>|int|string.',
136+
41,
137+
],
138+
]);
139+
}
140+
119141
}

tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -933,6 +933,22 @@ public function testBug12593(): void
933933
$this->analyse([__DIR__ . '/data/bug-12593.php'], []);
934934
}
935935

936+
public function testBug12981(): void
937+
{
938+
$this->reportPossiblyNonexistentGeneralArrayOffset = true;
939+
940+
$this->analyse([__DIR__ . '/data/bug-12981.php'], [
941+
[
942+
'Offset array<int, int|string>|int|string might not exist on non-empty-array<bool|float|int|string>.',
943+
39,
944+
],
945+
[
946+
'Offset array<int, int|string>|int|string might not exist on non-empty-array<bool|float|int|string>.',
947+
41,
948+
],
949+
]);
950+
}
951+
936952
public function testBugObject(): void
937953
{
938954
$this->analyse([__DIR__ . '/data/bug-object.php'], [
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug12981;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class HelloWorld
8+
{
9+
/** @param non-empty-array<scalar> $arr */
10+
public function sayHello(array $arr): void
11+
{
12+
echo $arr[array_rand($arr)];
13+
$randIndex = array_rand($arr);
14+
assertType('bool|float|int|string', $arr[$randIndex]);
15+
echo $arr[$randIndex];
16+
}
17+
18+
/** @param non-empty-array<scalar> $arr */
19+
public function sayHello1(array $arr): void
20+
{
21+
$num = 1;
22+
echo $arr[array_rand($arr, $num)];
23+
$randIndex = array_rand($arr, $num);
24+
echo $arr[$randIndex];
25+
}
26+
27+
/** @param non-empty-array<scalar> $arr */
28+
public function sayHello2(array $arr): void
29+
{
30+
$num = 5;
31+
echo $arr[array_rand($arr, $num)];
32+
$randIndex = array_rand($arr, $num);
33+
echo $arr[$randIndex];
34+
}
35+
36+
/** @param non-empty-array<scalar> $arr */
37+
public function sayHello4(array $arr, int $num): void
38+
{
39+
echo $arr[array_rand($arr, $num)];
40+
$randIndex = array_rand($arr, $num);
41+
echo $arr[$randIndex];
42+
}
43+
44+
public function sayHello5(): void
45+
{
46+
$arr = [
47+
1 => true,
48+
2 => false,
49+
'a' => 'hello',
50+
];
51+
assertType("'hello'|bool", $arr[array_rand($arr)]);
52+
53+
$arr = [
54+
1 => true,
55+
2 => null,
56+
'a' => 'hello',
57+
];
58+
assertType("'hello'|true|null", $arr[array_rand($arr)]);
59+
}
60+
}

0 commit comments

Comments
 (0)