Skip to content

Commit 1bf2ad3

Browse files
phpstan-botstaabm
andauthored
Fix offset might not exist false-positives
Co-authored-by: Markus Staab <maggus.staab@googlemail.com>
1 parent 945e00c commit 1bf2ad3

File tree

6 files changed

+349
-8
lines changed

6 files changed

+349
-8
lines changed

src/Analyser/TypeSpecifier.php

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,35 @@ public function specifyTypesInCondition(
351351
}
352352
}
353353

354+
// infer $list[$index] after $zeroOrMore < count($list) - N
355+
// infer $list[$index] after $zeroOrMore <= count($list) - N
356+
if (
357+
$context->true()
358+
&& $expr->right instanceof Expr\BinaryOp\Minus
359+
&& $expr->right->left instanceof FuncCall
360+
&& $expr->right->left->name instanceof Name
361+
&& in_array(strtolower((string) $expr->right->left->name), ['count', 'sizeof'], true)
362+
&& count($expr->right->left->getArgs()) >= 1
363+
// constant offsets are handled via HasOffsetType/HasOffsetValueType
364+
&& !$leftType instanceof ConstantIntegerType
365+
&& $leftType->isInteger()->yes()
366+
&& IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($leftType)->yes()
367+
) {
368+
$countArgType = $scope->getType($expr->right->left->getArgs()[0]->value);
369+
$subtractedType = $scope->getType($expr->right->right);
370+
if (
371+
$countArgType->isList()->yes()
372+
&& $this->isNormalCountCall($expr->right->left, $countArgType, $scope)->yes()
373+
&& IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($subtractedType)->yes()
374+
) {
375+
$arrayArg = $expr->right->left->getArgs()[0]->value;
376+
$dimFetch = new ArrayDimFetch($arrayArg, $expr->left);
377+
$result = $result->unionWith(
378+
$this->create($dimFetch, $countArgType->getIterableValueType(), TypeSpecifierContext::createTrue(), $scope)->setRootExpr($expr),
379+
);
380+
}
381+
}
382+
354383
if (
355384
!$context->null()
356385
&& $expr->right instanceof FuncCall
@@ -1211,6 +1240,16 @@ public function specifyTypesInCondition(
12111240
return (new SpecifiedTypes([], []))->setRootExpr($expr);
12121241
}
12131242

1243+
private function isNormalCountCall(FuncCall $countFuncCall, Type $typeToCount, Scope $scope): TrinaryLogic
1244+
{
1245+
if (count($countFuncCall->getArgs()) === 1) {
1246+
return TrinaryLogic::createYes();
1247+
}
1248+
1249+
$mode = $scope->getType($countFuncCall->getArgs()[1]->value);
1250+
return (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->result->or($typeToCount->getIterableValueType()->isArray()->negate());
1251+
}
1252+
12141253
private function specifyTypesForCountFuncCall(
12151254
FuncCall $countFuncCall,
12161255
Type $type,
@@ -1220,18 +1259,11 @@ private function specifyTypesForCountFuncCall(
12201259
Expr $rootExpr,
12211260
): ?SpecifiedTypes
12221261
{
1223-
if (count($countFuncCall->getArgs()) === 1) {
1224-
$isNormalCount = TrinaryLogic::createYes();
1225-
} else {
1226-
$mode = $scope->getType($countFuncCall->getArgs()[1]->value);
1227-
$isNormalCount = (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->result->or($type->getIterableValueType()->isArray()->negate());
1228-
}
1229-
12301262
$isConstantArray = $type->isConstantArray();
12311263
$isList = $type->isList();
12321264
$oneOrMore = IntegerRangeType::fromInterval(1, null);
12331265
if (
1234-
!$isNormalCount->yes()
1266+
!$this->isNormalCountCall($countFuncCall, $type, $scope)->yes()
12351267
|| (!$isConstantArray->yes() && !$isList->yes())
12361268
|| !$oneOrMore->isSuperTypeOf($sizeType)->yes()
12371269
|| $sizeType->isSuperTypeOf($type->getArraySize())->yes()

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ private static function findTestFiles(): iterable
248248
yield __DIR__ . '/../Rules/Classes/data/bug-11591-property-tag.php';
249249
yield __DIR__ . '/../Rules/Classes/data/mixin-trait-use.php';
250250

251+
yield __DIR__ . '/../Rules/Arrays/data/bug-14234.php';
251252
yield __DIR__ . '/../Rules/Arrays/data/bug-11679.php';
252253
yield __DIR__ . '/../Rules/Methods/data/bug-4801.php';
253254
yield __DIR__ . '/../Rules/Arrays/data/narrow-superglobal.php';

tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1161,6 +1161,18 @@ public function testBug13526(): void
11611161
$this->analyse([__DIR__ . '/data/bug-13526.php'], []);
11621162
}
11631163

1164+
public function testBug14215(): void
1165+
{
1166+
$this->reportPossiblyNonexistentGeneralArrayOffset = true;
1167+
1168+
$this->analyse([__DIR__ . '/data/bug-14215.php'], [
1169+
[
1170+
'Offset int might not exist on list<int>.',
1171+
39,
1172+
],
1173+
]);
1174+
}
1175+
11641176
public function testBug13770(): void
11651177
{
11661178
$this->reportPossiblyNonexistentGeneralArrayOffset = true;
@@ -1186,6 +1198,39 @@ public function testBug13770(): void
11861198
'Offset -1|3|6|10 might not exist on list<int>.',
11871199
126,
11881200
],
1201+
[
1202+
'Offset int<0, max> might not exist on list<int>.',
1203+
139,
1204+
],
1205+
[
1206+
'Offset int<0, max> might not exist on array.',
1207+
177,
1208+
],
1209+
[
1210+
'Offset -5|int<0, max> might not exist on list<int>.',
1211+
190,
1212+
],
1213+
[
1214+
'Offset int<0, max> might not exist on list<mixed>.',
1215+
203,
1216+
],
1217+
]);
1218+
}
1219+
1220+
#[RequiresPhp('>= 8.0')]
1221+
public function testBug14234(): void
1222+
{
1223+
$this->reportPossiblyNonexistentGeneralArrayOffset = true;
1224+
1225+
$this->analyse([__DIR__ . '/data/bug-14234.php'], [
1226+
[
1227+
'Offset int<2, max> might not exist on non-empty-array<int<0, max>, string>.',
1228+
49,
1229+
],
1230+
[
1231+
'Offset int<0, max> might not exist on array.',
1232+
60,
1233+
],
11891234
]);
11901235
}
11911236

tests/PHPStan/Rules/Arrays/data/bug-13770.php

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,4 +128,81 @@ public function constantMaybeNegativeIntLessThanCount(array $array, int $index):
128128

129129
return 0;
130130
}
131+
132+
/**
133+
* @param list<int> $array
134+
* @param 0|positive-int $index
135+
*/
136+
public function ZeroOrMoreIntLessThanOrEqualCount(array $array, int $index): int
137+
{
138+
if ($index <= count($array)) {
139+
return $array[$index]; // SHOULD still report - off by one
140+
}
141+
142+
return 0;
143+
}
144+
145+
/**
146+
* @param list<int> $array
147+
* @param 0|positive-int $index
148+
*/
149+
public function ZeroOrMoreIntLessThanOrEqualCountMinusOne(array $array, int $index): int
150+
{
151+
if ($index <= count($array) - 1) {
152+
return $array[$index];
153+
}
154+
155+
return 0;
156+
}
157+
158+
/**
159+
* @param list<int> $array
160+
* @param 0|positive-int $index
161+
*/
162+
public function ZeroOrMoreIntLessThanOrEqualCountMinusFive(array $array, int $index): int
163+
{
164+
if ($index <= count($array) - 5) {
165+
return $array[$index];
166+
}
167+
168+
return 0;
169+
}
170+
171+
/**
172+
* @param 0|positive-int $index
173+
*/
174+
public function errorsBecauseNotList(array $array, int $index): int
175+
{
176+
if ($index <= count($array) - 5) {
177+
return $array[$index];
178+
}
179+
180+
return 0;
181+
}
182+
183+
/**
184+
* @param list<int> $array
185+
* @param -5|0|positive-int $index
186+
*/
187+
public function errorsBecauseMaybeTooSmall(array $array, int $index): int
188+
{
189+
if ($index <= count($array) - 5) {
190+
return $array[$index];
191+
}
192+
193+
return 0;
194+
}
195+
196+
/**
197+
* @param list<mixed> $array
198+
* @param 0|positive-int $index
199+
*/
200+
public function errorsOnRecursiveCount(array $array, int $index): int
201+
{
202+
if ($index <= count($array, COUNT_RECURSIVE) - 5) {
203+
return $array[$index];
204+
}
205+
206+
return 0;
207+
}
131208
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug14215;
4+
5+
class HelloWorld
6+
{
7+
/**
8+
* @param list<int> $array
9+
* @param positive-int $index
10+
*/
11+
public function positiveIntLessThanCountMinusOne(array $array, int $index): int
12+
{
13+
if ($index < count($array) - 1) {
14+
return $array[$index]; // should not report
15+
}
16+
17+
return 0;
18+
}
19+
20+
/**
21+
* @param list<int> $array
22+
* @param positive-int $index
23+
*/
24+
public function positiveIntLessThanOrEqualCountMinusOne(array $array, int $index): int
25+
{
26+
if ($index <= count($array) - 1) {
27+
return $array[$index]; // should not report
28+
}
29+
30+
return 0;
31+
}
32+
33+
/**
34+
* @param list<int> $array
35+
*/
36+
public function intLessThanOrEqualCountMinusOne(array $array, int $index): int
37+
{
38+
if ($index <= count($array) - 1) {
39+
return $array[$index]; // should error report, could be negative int
40+
}
41+
42+
return 0;
43+
}
44+
45+
/**
46+
* @param list<int> $array
47+
* @param int<0, max> $index
48+
*/
49+
public function nonNegativeIntLessThanCountMinusOne(array $array, int $index): int
50+
{
51+
if ($index < count($array) - 1) {
52+
return $array[$index]; // should not report
53+
}
54+
55+
return 0;
56+
}
57+
58+
/**
59+
* @param list<int> $array
60+
* @param int<0, max> $index
61+
*/
62+
public function nonNegativeIntLessThanOrEqualCountMinusOne(array $array, int $index): int
63+
{
64+
if ($index <= count($array) - 1) {
65+
return $array[$index]; // should not report
66+
}
67+
68+
return 0;
69+
}
70+
71+
/**
72+
* @param list<int> $array
73+
* @param positive-int $index
74+
*/
75+
public function positiveIntLessThanCountMinusTwo(array $array, int $index): int
76+
{
77+
if ($index < count($array) - 2) {
78+
return $array[$index]; // should not report
79+
}
80+
81+
return 0;
82+
}
83+
84+
/**
85+
* @param list<int> $array
86+
* @param positive-int $index
87+
*/
88+
public function positiveIntLessThanOrEqualCountMinusTwo(array $array, int $index): int
89+
{
90+
if ($index <= count($array) - 2) {
91+
return $array[$index]; // should not report
92+
}
93+
94+
return 0;
95+
}
96+
97+
/**
98+
* @param list<int> $array
99+
* @param positive-int $index
100+
*/
101+
public function positiveIntLessThanSizeofMinusOne(array $array, int $index): int
102+
{
103+
if ($index < sizeof($array) - 1) {
104+
return $array[$index]; // should not report
105+
}
106+
107+
return 0;
108+
}
109+
110+
/**
111+
* @param list<int> $array
112+
* @param positive-int $index
113+
*/
114+
public function positiveIntGreaterThanCountMinusOneInversed(array $array, int $index): int
115+
{
116+
if (count($array) - 1 > $index) {
117+
return $array[$index]; // should not report
118+
}
119+
120+
return 0;
121+
}
122+
}

0 commit comments

Comments
 (0)