Skip to content

Commit 4b02fa3

Browse files
committed
Fix false positive non-existing-offset after array_key_last
1 parent 7c281ba commit 4b02fa3

File tree

3 files changed

+82
-1
lines changed

3 files changed

+82
-1
lines changed

Diff for: src/Analyser/TypeSpecifier.php

+23-1
Original file line numberDiff line numberDiff line change
@@ -662,7 +662,29 @@ public function specifyTypesInCondition(
662662
throw new ShouldNotHappenException();
663663
}
664664
if ($context->null()) {
665-
return $this->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->expr, $context)->setRootExpr($expr);
665+
$specifiedTypes = $this->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->expr, $context)->setRootExpr($expr);
666+
667+
if (
668+
$expr->expr instanceof FuncCall
669+
&& $expr->expr->name instanceof Name
670+
&& $expr->expr->name->toLowerString() === 'array_key_last'
671+
&& count($expr->expr->getArgs()) >= 1
672+
) {
673+
$arrayArg = $expr->expr->getArgs()[0]->value;
674+
$arrayType = $scope->getType($arrayArg);
675+
if (
676+
$arrayType->isArray()->yes()
677+
&& $arrayType->isIterableAtLeastOnce()->yes()
678+
) {
679+
$dimFetch = new ArrayDimFetch($arrayArg, $expr->var);
680+
681+
return $specifiedTypes->unionWith(
682+
$this->create($dimFetch, $arrayType->getLastIterableValueType(), TypeSpecifierContext::createTrue(), $scope),
683+
);
684+
}
685+
}
686+
687+
return $specifiedTypes;
666688
}
667689

668690
return $this->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->var, $context)->setRootExpr($expr);

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

+12
Original file line numberDiff line numberDiff line change
@@ -780,4 +780,16 @@ public function testInternalClassesWithOverloadedOffsetAccessInvalid84(): void
780780
$this->analyse([__DIR__ . '/data/internal-classes-overload-offset-access-invalid-php84.php'], []);
781781
}
782782

783+
public function testArrayDimFetchAfterArrayKeyFirstOrLast(): void
784+
{
785+
$this->reportPossiblyNonexistentGeneralArrayOffset = true;
786+
787+
$this->analyse([__DIR__ . '/data/array-dim-after-array-key-first-or-last.php'], [
788+
[
789+
'Offset null does not exist on array{}.',
790+
17,
791+
],
792+
]);
793+
}
794+
783795
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ArrayDimAfterArrayKeyFirstOrLast;
4+
5+
class HelloWorld
6+
{
7+
/**
8+
* @param list<string> $hellos
9+
*/
10+
public function last(array $hellos): string
11+
{
12+
if ($hellos !== []) {
13+
$lastHelloKey = array_key_last($hellos);
14+
return $hellos[$lastHelloKey];
15+
} else {
16+
$lastHelloKey = array_key_last($hellos);
17+
return $hellos[$lastHelloKey];
18+
}
19+
}
20+
21+
/**
22+
* @param list<string> $hellos
23+
*/
24+
public function first(array $hellos): string
25+
{
26+
if ($hellos !== []) {
27+
$firstHelloKey = array_key_first($hellos);
28+
return $hellos[$firstHelloKey];
29+
}
30+
31+
return 'nothing';
32+
}
33+
34+
/**
35+
* @param array{first: int, middle: float, last: bool} $hellos
36+
*/
37+
public function shape(array $hellos): int|bool
38+
{
39+
$firstHelloKey = array_key_first($hellos);
40+
$lastHelloKey = array_key_last($hellos);
41+
42+
if (rand(0,1)) {
43+
return $hellos[$firstHelloKey];
44+
}
45+
return $hellos[$lastHelloKey];
46+
}
47+
}

0 commit comments

Comments
 (0)