Skip to content

Commit 1ce0e72

Browse files
phpstan-botclaude
authored andcommitted
Narrow array to non-empty for array_search, narrow $arr[$key] to needle type for strict array_search
Both array_search and array_find_key now narrow the array to non-empty when the result is not the sentinel value (false/null respectively). For array_search with strict mode (third argument true), $arr[$key] is narrowed to the intersection of the needle type and the value type, since strict comparison guarantees the found value has the same type as the needle. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 09d59ca commit 1ce0e72

2 files changed

Lines changed: 60 additions & 10 deletions

File tree

src/Analyser/TypeSpecifier.php

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -875,15 +875,15 @@ public function specifyTypesInCondition(
875875
$funcName = $expr->expr->name->toLowerString();
876876
$arrayArgIndex = null;
877877
$sentinelType = null;
878-
$narrowToNonEmpty = false;
878+
$needleArgIndex = null;
879879

880880
if ($funcName === 'array_search') {
881881
$arrayArgIndex = 1;
882882
$sentinelType = new ConstantBooleanType(false);
883+
$needleArgIndex = 0;
883884
} elseif ($funcName === 'array_find_key') {
884885
$arrayArgIndex = 0;
885886
$sentinelType = new NullType();
886-
$narrowToNonEmpty = true;
887887
}
888888

889889
if ($arrayArgIndex !== null) {
@@ -892,23 +892,40 @@ public function specifyTypesInCondition(
892892

893893
if ($arrayType->isArray()->yes()) {
894894
if ($context->true()) {
895-
if ($narrowToNonEmpty) {
896-
$specifiedTypes = $specifiedTypes->unionWith(
897-
$this->create($arrayArg, new NonEmptyArrayType(), TypeSpecifierContext::createTrue(), $scope),
898-
);
899-
}
895+
$specifiedTypes = $specifiedTypes->unionWith(
896+
$this->create($arrayArg, new NonEmptyArrayType(), TypeSpecifierContext::createTrue(), $scope),
897+
);
900898

901899
$dimFetch = new ArrayDimFetch($arrayArg, $expr->var);
902900

901+
$dimFetchType = $arrayType->getIterableValueType();
902+
if (
903+
$needleArgIndex !== null
904+
&& count($expr->expr->getArgs()) >= 3
905+
&& $scope->getType($expr->expr->getArgs()[2]->value)->isTrue()->yes()
906+
) {
907+
$needleType = $scope->getType($expr->expr->getArgs()[$needleArgIndex]->value);
908+
$dimFetchType = TypeCombinator::intersect($needleType, $dimFetchType);
909+
}
910+
903911
$specifiedTypes = $specifiedTypes->unionWith(
904-
$this->create($dimFetch, $arrayType->getIterableValueType(), TypeSpecifierContext::createTrue(), $scope),
912+
$this->create($dimFetch, $dimFetchType, TypeSpecifierContext::createTrue(), $scope),
905913
);
906914
} elseif ($expr->var instanceof Expr\Variable && is_string($expr->var->name)) {
907915
$keyType = $scope->getType($expr->expr);
908916
$narrowedKeyType = TypeCombinator::remove($keyType, $sentinelType);
909917
if (!$narrowedKeyType instanceof NeverType) {
918+
$dimFetchType = null;
919+
if (
920+
$needleArgIndex !== null
921+
&& count($expr->expr->getArgs()) >= 3
922+
&& $scope->getType($expr->expr->getArgs()[2]->value)->isTrue()->yes()
923+
) {
924+
$needleType = $scope->getType($expr->expr->getArgs()[$needleArgIndex]->value);
925+
$dimFetchType = TypeCombinator::intersect($needleType, $arrayType->getIterableValueType());
926+
}
910927
$specifiedTypes = $specifiedTypes->unionWith(
911-
$this->createArrayDimFetchConditionalExpressionHolder($expr->var, $arrayArg, $arrayType, $narrowedKeyType),
928+
$this->createArrayDimFetchConditionalExpressionHolder($expr->var, $arrayArg, $arrayType, $narrowedKeyType, $dimFetchType),
912929
);
913930
}
914931
}
@@ -2421,6 +2438,7 @@ private function createArrayDimFetchConditionalExpressionHolder(
24212438
Expr $arrayArg,
24222439
Type $arrayType,
24232440
Type $narrowedKeyType,
2441+
?Type $dimFetchType = null,
24242442
): SpecifiedTypes
24252443
{
24262444
$dimFetch = new ArrayDimFetch($arrayArg, $keyVar);
@@ -2429,7 +2447,7 @@ private function createArrayDimFetchConditionalExpressionHolder(
24292447

24302448
$holder = new ConditionalExpressionHolder(
24312449
[$keyExprString => ExpressionTypeHolder::createYes($keyVar, $narrowedKeyType)],
2432-
ExpressionTypeHolder::createYes($dimFetch, $arrayType->getIterableValueType()),
2450+
ExpressionTypeHolder::createYes($dimFetch, $dimFetchType ?? $arrayType->getIterableValueType()),
24332451
);
24342452

24352453
return (new SpecifiedTypes([], []))->setNewConditionalExpressionHolders([

tests/PHPStan/Analyser/nsrt/array-search-existing.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,35 @@ function arraySearchReversedComparison(array $list, string $s): void
3737
assertType('string', $list[$key]);
3838
}
3939
}
40+
41+
/**
42+
* @param array<string, int|string> $arr
43+
*/
44+
function arraySearchStrictNarrowsToNeedle(array $arr, int $needle): void
45+
{
46+
$key = array_search($needle, $arr, true);
47+
if ($key !== false) {
48+
assertType('int', $arr[$key]);
49+
}
50+
}
51+
52+
/**
53+
* @param array<string, int|string> $arr
54+
*/
55+
function arraySearchLooseKeepsValueType(array $arr, int $needle): void
56+
{
57+
$key = array_search($needle, $arr);
58+
if ($key !== false) {
59+
assertType('int|string', $arr[$key]);
60+
}
61+
}
62+
63+
/**
64+
* @param array<string, int|string> $arr
65+
*/
66+
function arraySearchStrictInlineAssign(array $arr, int $needle): void
67+
{
68+
if (($key = array_search($needle, $arr, true)) !== false) {
69+
assertType('int', $arr[$key]);
70+
}
71+
}

0 commit comments

Comments
 (0)