Skip to content

Commit 1b0239b

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

File tree

4 files changed

+131
-12
lines changed

4 files changed

+131
-12
lines changed

Diff for: src/Analyser/TypeSpecifier.php

+27-1
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),

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

+20
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
}
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)