Skip to content

Commit a8d153b

Browse files
staabmondrejmirtes
authored andcommitted
Fix false positive non-existing-offset after count() - 1
1 parent 74648f7 commit a8d153b

File tree

4 files changed

+134
-13
lines changed

4 files changed

+134
-13
lines changed

src/Analyser/TypeSpecifier.php

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

670+
// infer $arr[$key] after $key = array_key_first/last($arr)
670671
if (
671672
$expr->expr instanceof FuncCall
672673
&& $expr->expr->name instanceof Name
673-
&& $expr->expr->name->toLowerString() === 'array_key_last'
674+
&& in_array($expr->expr->name->toLowerString(), ['array_key_first', 'array_key_last'], true)
674675
&& count($expr->expr->getArgs()) >= 1
675676
) {
676677
$arrayArg = $expr->expr->getArgs()[0]->value;
@@ -680,6 +681,33 @@ public function specifyTypesInCondition(
680681
&& $arrayType->isIterableAtLeastOnce()->yes()
681682
) {
682683
$dimFetch = new ArrayDimFetch($arrayArg, $expr->var);
684+
$iterableValueType = $expr->expr->name->toLowerString() === 'array_key_first'
685+
? $arrayType->getFirstIterableValueType()
686+
: $arrayType->getLastIterableValueType();
687+
688+
return $specifiedTypes->unionWith(
689+
$this->create($dimFetch, $iterableValueType, TypeSpecifierContext::createTrue(), $scope),
690+
);
691+
}
692+
}
693+
694+
// infer $list[$count] after $count = count($list) - 1
695+
if (
696+
$expr->expr instanceof Expr\BinaryOp\Minus
697+
&& $expr->expr->left instanceof FuncCall
698+
&& $expr->expr->left->name instanceof Name
699+
&& in_array($expr->expr->left->name->toLowerString(), ['count', 'sizeof'], true)
700+
&& count($expr->expr->left->getArgs()) >= 1
701+
&& $expr->expr->right instanceof Node\Scalar\Int_
702+
&& $expr->expr->right->value === 1
703+
) {
704+
$arrayArg = $expr->expr->left->getArgs()[0]->value;
705+
$arrayType = $scope->getType($arrayArg);
706+
if (
707+
$arrayType->isList()->yes()
708+
&& $arrayType->isIterableAtLeastOnce()->yes()
709+
) {
710+
$dimFetch = new ArrayDimFetch($arrayArg, $expr->var);
683711

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

tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php

+21-1
Original file line numberDiff line numberDiff line change
@@ -792,7 +792,27 @@ public function testArrayDimFetchAfterArrayKeyFirstOrLast(): void
792792
$this->analyse([__DIR__ . '/data/array-dim-after-array-key-first-or-last.php'], [
793793
[
794794
'Offset null does not exist on array{}.',
795-
17,
795+
19,
796+
],
797+
]);
798+
}
799+
800+
public function testArrayDimFetchAfterCount(): void
801+
{
802+
$this->reportPossiblyNonexistentGeneralArrayOffset = true;
803+
804+
$this->analyse([__DIR__ . '/data/array-dim-after-count.php'], [
805+
[
806+
'Offset int<0, max> might not exist on list<string>.',
807+
26,
808+
],
809+
[
810+
'Offset int<-1, max> might not exist on array<string>.',
811+
35,
812+
],
813+
[
814+
'Offset int<0, max> might not exist on non-empty-array<string>.',
815+
42,
796816
],
797817
]);
798818
}
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
}
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)