Skip to content

Commit 32d84e0

Browse files
committed
Fix false positive non-existing-offset after count() - 1
1 parent 4b02fa3 commit 32d84e0

File tree

4 files changed

+134
-13
lines changed

4 files changed

+134
-13
lines changed

Diff for: src/Analyser/TypeSpecifier.php

+29-1
Original file line numberDiff line numberDiff line change
@@ -664,10 +664,11 @@ public function specifyTypesInCondition(
664664
if ($context->null()) {
665665
$specifiedTypes = $this->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->expr, $context)->setRootExpr($expr);
666666

667+
// infer $arr[$key] after $key = array_key_first/last($arr)
667668
if (
668669
$expr->expr instanceof FuncCall
669670
&& $expr->expr->name instanceof Name
670-
&& $expr->expr->name->toLowerString() === 'array_key_last'
671+
&& in_array($expr->expr->name->toLowerString(), ['array_key_first', 'array_key_last'], true)
671672
&& count($expr->expr->getArgs()) >= 1
672673
) {
673674
$arrayArg = $expr->expr->getArgs()[0]->value;
@@ -677,6 +678,33 @@ public function specifyTypesInCondition(
677678
&& $arrayType->isIterableAtLeastOnce()->yes()
678679
) {
679680
$dimFetch = new ArrayDimFetch($arrayArg, $expr->var);
681+
$iterableValueType = $expr->expr->name->toLowerString() === 'array_key_first'
682+
? $arrayType->getFirstIterableValueType()
683+
: $arrayType->getLastIterableValueType();
684+
685+
return $specifiedTypes->unionWith(
686+
$this->create($dimFetch, $iterableValueType, TypeSpecifierContext::createTrue(), $scope),
687+
);
688+
}
689+
}
690+
691+
// infer $list[$count] after $count = count($list) - 1
692+
if (
693+
$expr->expr instanceof Expr\BinaryOp\Minus
694+
&& $expr->expr->left instanceof FuncCall
695+
&& $expr->expr->left->name instanceof Name
696+
&& in_array($expr->expr->left->name->toLowerString(), ['count', 'sizeof'], true)
697+
&& count($expr->expr->left->getArgs()) >= 1
698+
&& $expr->expr->right instanceof Node\Scalar\Int_
699+
&& $expr->expr->right->value === 1
700+
) {
701+
$arrayArg = $expr->expr->left->getArgs()[0]->value;
702+
$arrayType = $scope->getType($arrayArg);
703+
if (
704+
$arrayType->isList()->yes()
705+
&& $arrayType->isIterableAtLeastOnce()->yes()
706+
) {
707+
$dimFetch = new ArrayDimFetch($arrayArg, $expr->var);
680708

681709
return $specifiedTypes->unionWith(
682710
$this->create($dimFetch, $arrayType->getLastIterableValueType(), TypeSpecifierContext::createTrue(), $scope),

Diff for: tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php

+21-1
Original file line numberDiff line numberDiff line change
@@ -787,7 +787,27 @@ public function testArrayDimFetchAfterArrayKeyFirstOrLast(): void
787787
$this->analyse([__DIR__ . '/data/array-dim-after-array-key-first-or-last.php'], [
788788
[
789789
'Offset null does not exist on array{}.',
790-
17,
790+
19,
791+
],
792+
]);
793+
}
794+
795+
public function testArrayDimFetchAfterCount(): void
796+
{
797+
$this->reportPossiblyNonexistentGeneralArrayOffset = true;
798+
799+
$this->analyse([__DIR__ . '/data/array-dim-after-count.php'], [
800+
[
801+
'Offset int<0, max> might not exist on list<string>.',
802+
26,
803+
],
804+
[
805+
'Offset int<-1, max> might not exist on array<string>.',
806+
35,
807+
],
808+
[
809+
'Offset int<0, max> might not exist on non-empty-array<string>.',
810+
42,
791811
],
792812
]);
793813
}
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
<?php declare(strict_types = 1);
1+
<?php // lint >= 8.0
2+
3+
declare(strict_types = 1);
24

35
namespace ArrayDimAfterArrayKeyFirstOrLast;
46

@@ -10,12 +12,25 @@ class HelloWorld
1012
public function last(array $hellos): string
1113
{
1214
if ($hellos !== []) {
13-
$lastHelloKey = array_key_last($hellos);
14-
return $hellos[$lastHelloKey];
15+
$last = array_key_last($hellos);
16+
return $hellos[$last];
1517
} else {
16-
$lastHelloKey = array_key_last($hellos);
17-
return $hellos[$lastHelloKey];
18+
$last = array_key_last($hellos);
19+
return $hellos[$last];
20+
}
21+
}
22+
23+
/**
24+
* @param array<string> $hellos
25+
*/
26+
public function lastOnArray(array $hellos): string
27+
{
28+
if ($hellos !== []) {
29+
$last = array_key_last($hellos);
30+
return $hellos[$last];
1831
}
32+
33+
return 'nothing';
1934
}
2035

2136
/**
@@ -24,8 +39,21 @@ public function last(array $hellos): string
2439
public function first(array $hellos): string
2540
{
2641
if ($hellos !== []) {
27-
$firstHelloKey = array_key_first($hellos);
28-
return $hellos[$firstHelloKey];
42+
$first = array_key_first($hellos);
43+
return $hellos[$first];
44+
}
45+
46+
return 'nothing';
47+
}
48+
49+
/**
50+
* @param array<string> $hellos
51+
*/
52+
public function firstOnArray(array $hellos): string
53+
{
54+
if ($hellos !== []) {
55+
$first = array_key_first($hellos);
56+
return $hellos[$first];
2957
}
3058

3159
return 'nothing';
@@ -36,12 +64,12 @@ public function first(array $hellos): string
3664
*/
3765
public function shape(array $hellos): int|bool
3866
{
39-
$firstHelloKey = array_key_first($hellos);
40-
$lastHelloKey = array_key_last($hellos);
67+
$first = array_key_first($hellos);
68+
$last = array_key_last($hellos);
4169

4270
if (rand(0,1)) {
43-
return $hellos[$firstHelloKey];
71+
return $hellos[$first];
4472
}
45-
return $hellos[$lastHelloKey];
73+
return $hellos[$last];
4674
}
4775
}
+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ArrayDimFetchOnCount;
4+
5+
class HelloWorld
6+
{
7+
/**
8+
* @param list<string> $hellos
9+
*/
10+
public function works(array $hellos): string
11+
{
12+
if ($hellos === []) {
13+
return 'nothing';
14+
}
15+
16+
$count = count($hellos) - 1;
17+
return $hellos[$count];
18+
}
19+
20+
/**
21+
* @param list<string> $hellos
22+
*/
23+
public function offByOne(array $hellos): string
24+
{
25+
$count = count($hellos);
26+
return $hellos[$count];
27+
}
28+
29+
/**
30+
* @param array<string> $hellos
31+
*/
32+
public function maybeInvalid(array $hellos): string
33+
{
34+
$count = count($hellos) - 1;
35+
echo $hellos[$count];
36+
37+
if ($hellos === []) {
38+
return 'nothing';
39+
}
40+
41+
$count = count($hellos) - 1;
42+
return $hellos[$count];
43+
}
44+
45+
}

0 commit comments

Comments
 (0)