Skip to content

Commit 81e20ec

Browse files
committed
Fix false positive non-existing-offset after count() - 1
1 parent 12550c5 commit 81e20ec

File tree

4 files changed

+128
-11
lines changed

4 files changed

+128
-11
lines changed

src/Analyser/TypeSpecifier.php

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -667,7 +667,7 @@ public function specifyTypesInCondition(
667667
if (
668668
$expr->expr instanceof FuncCall
669669
&& $expr->expr->name instanceof Name
670-
&& $expr->expr->name->toLowerString() === 'array_key_last'
670+
&& in_array($expr->expr->name->toLowerString(), ['array_key_first', 'array_key_last'], true)
671671
&& count($expr->expr->getArgs()) >= 1
672672
) {
673673
$arrayArg = $expr->expr->getArgs()[0]->value;
@@ -677,6 +677,32 @@ public function specifyTypesInCondition(
677677
&& $arrayType->isIterableAtLeastOnce()->yes()
678678
) {
679679
$dimFetch = new ArrayDimFetch($arrayArg, $expr->var);
680+
$iterableValueType = $expr->expr->name->toLowerString() === 'array_key_first'
681+
? $arrayType->getFirstIterableValueType()
682+
: $arrayType->getLastIterableValueType();
683+
684+
return $specifiedTypes->unionWith(
685+
$this->create($dimFetch, $iterableValueType, TypeSpecifierContext::createTrue(), $scope),
686+
);
687+
}
688+
}
689+
690+
if (
691+
$expr->expr instanceof Expr\BinaryOp\Minus
692+
&& $expr->expr->left instanceof FuncCall
693+
&& $expr->expr->left->name instanceof Name
694+
&& $expr->expr->left->name->toLowerString() === 'count'
695+
&& count($expr->expr->left->getArgs()) >= 1
696+
&& $expr->expr->right instanceof Node\Scalar\Int_
697+
&& $expr->expr->right->value === 1
698+
) {
699+
$arrayArg = $expr->expr->left->getArgs()[0]->value;
700+
$arrayType = $scope->getType($arrayArg);
701+
if (
702+
$arrayType->isList()->yes()
703+
&& $arrayType->isIterableAtLeastOnce()->yes()
704+
) {
705+
$dimFetch = new ArrayDimFetch($arrayArg, $expr->var);
680706

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

tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -792,4 +792,24 @@ public function testArrayDimFetchAfterArrayKeyFirstOrLast(): void
792792
]);
793793
}
794794

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,
811+
],
812+
]);
813+
}
814+
795815
}

tests/PHPStan/Rules/Arrays/data/array-dim-after-array-key-first-or-last.php

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,48 @@ class HelloWorld
1010
public function last(array $hellos): string
1111
{
1212
if ($hellos !== []) {
13-
$lastHelloKey = array_key_last($hellos);
14-
return $hellos[$lastHelloKey];
13+
$last = array_key_last($hellos);
14+
return $hellos[$last];
1515
} else {
16-
$lastHelloKey = array_key_last($hellos);
17-
return $hellos[$lastHelloKey];
16+
$last = array_key_last($hellos);
17+
return $hellos[$last];
1818
}
1919
}
2020

21+
/**
22+
* @param array<string> $hellos
23+
*/
24+
public function lastOnArray(array $hellos): string
25+
{
26+
if ($hellos !== []) {
27+
$last = array_key_last($hellos);
28+
return $hellos[$last];
29+
}
30+
31+
return 'nothing';
32+
}
33+
2134
/**
2235
* @param list<string> $hellos
2336
*/
2437
public function first(array $hellos): string
2538
{
2639
if ($hellos !== []) {
27-
$firstHelloKey = array_key_first($hellos);
28-
return $hellos[$firstHelloKey];
40+
$first = array_key_first($hellos);
41+
return $hellos[$first];
42+
}
43+
44+
return 'nothing';
45+
}
46+
47+
/**
48+
* @param array<string> $hellos
49+
*/
50+
public function firstOnArray(array $hellos): string
51+
{
52+
if ($hellos !== []) {
53+
$first = array_key_first($hellos);
54+
return $hellos[$first];
2955
}
3056

3157
return 'nothing';
@@ -36,12 +62,12 @@ public function first(array $hellos): string
3662
*/
3763
public function shape(array $hellos): int|bool
3864
{
39-
$firstHelloKey = array_key_first($hellos);
40-
$lastHelloKey = array_key_last($hellos);
65+
$first = array_key_first($hellos);
66+
$last = array_key_last($hellos);
4167

4268
if (rand(0,1)) {
43-
return $hellos[$firstHelloKey];
69+
return $hellos[$first];
4470
}
45-
return $hellos[$lastHelloKey];
71+
return $hellos[$last];
4672
}
4773
}
Lines changed: 45 additions & 0 deletions
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)