From 24640d93e975f2974443e05c8dece9b7f911705c Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 8 Nov 2025 10:52:37 +0100 Subject: [PATCH 1/6] Infer non-empty-array after array_key_first/last() --- src/Analyser/TypeSpecifier.php | 15 +++++++++ tests/PHPStan/Analyser/nsrt/bug-13546.php | 37 +++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-13546.php diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 6698a8a800..bd08d4f364 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -2346,6 +2346,21 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty } } + // array_key_first($a) !== null + // array_key_last($a) !== null + if ( + $unwrappedLeftExpr instanceof FuncCall + && $unwrappedLeftExpr->name instanceof Name + && in_array($unwrappedLeftExpr->name->toLowerString(), ['array_key_first', 'array_key_last'], true) + && isset($unwrappedLeftExpr->getArgs()[0]) + && $rightType->isNull()->yes() + ) { + $argType = $scope->getType($unwrappedLeftExpr->getArgs()[0]->value); + if ($argType->isArray()->yes()) { + return $this->create($unwrappedLeftExpr->getArgs()[0]->value, new NonEmptyArrayType(), $context->negate(), $scope)->setRootExpr($expr); + } + } + // preg_match($a) === $b if ( $context->true() diff --git a/tests/PHPStan/Analyser/nsrt/bug-13546.php b/tests/PHPStan/Analyser/nsrt/bug-13546.php new file mode 100644 index 0000000000..47beb71390 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13546.php @@ -0,0 +1,37 @@ + $array */ +function first(array $array): void +{ + if (array_key_first($array) !== null) { + assertType('non-empty-array', $array); + } else { + assertType('array{}', $array); + } + assertType('array', $array); +} + +/** @param array $array */ +function firstReversed(array $array): void +{ + if (null !== array_key_first($array)) { + assertType('non-empty-array', $array); + } else { + assertType('array{}', $array); + } + assertType('array', $array); +} + +/** @param array $array */ +function last(array $array): void +{ + if (array_key_last($array) !== null) { + assertType('non-empty-array', $array); + } else { + assertType('array{}', $array); + } + assertType('array', $array); +} From e4b9e606574d6af38432f42c37bb48d7ae8fa4b9 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 8 Nov 2025 10:56:15 +0100 Subject: [PATCH 2/6] Update bug-13546.php --- tests/PHPStan/Analyser/nsrt/bug-13546.php | 26 +++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-13546.php b/tests/PHPStan/Analyser/nsrt/bug-13546.php index 47beb71390..17d221d495 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-13546.php +++ b/tests/PHPStan/Analyser/nsrt/bug-13546.php @@ -35,3 +35,29 @@ function last(array $array): void } assertType('array', $array); } + +function maybeArray(array $array, mixed $mixed): void +{ + $arrayOrMixed = rand(0, 1) ? $array : $mixed; + + if (array_key_last($arrayOrMixed) !== null) { + assertType('mixed', $arrayOrMixed); + } else { + assertType('mixed', $arrayOrMixed); + } + assertType('mixed', $arrayOrMixed); +} + +function mixedLast(mixed $mixed): void +{ + if (is_array($mixed)) { + return; + } + + if (array_key_last($mixed) !== null) { + assertType('mixed~array', $mixed); + } else { + assertType('mixed~array', $mixed); + } + assertType('mixed~array', $mixed); +} From 28f6ee14ea91888949acd157f8672b49694da1fa Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 8 Nov 2025 15:30:54 +0100 Subject: [PATCH 3/6] add test --- tests/PHPStan/Analyser/nsrt/bug-13546.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-13546.php b/tests/PHPStan/Analyser/nsrt/bug-13546.php index 17d221d495..588e90d162 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-13546.php +++ b/tests/PHPStan/Analyser/nsrt/bug-13546.php @@ -61,3 +61,14 @@ function mixedLast(mixed $mixed): void } assertType('mixed~array', $mixed); } + +/** @param list $array */ +function firstInCondition(array $array): mixed +{ + if (($key = array_key_first($array)) !== null) { + assertType('list', $array); // could be 'non-empty-list' + return $array[$key]; + } + assertType('list', $array); + return null; +} From 4196932a079319c556ccac6cdd00fb046f18ba78 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 8 Nov 2025 15:33:51 +0100 Subject: [PATCH 4/6] test maybe null --- tests/PHPStan/Analyser/nsrt/bug-13546.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-13546.php b/tests/PHPStan/Analyser/nsrt/bug-13546.php index 588e90d162..c390b717d0 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-13546.php +++ b/tests/PHPStan/Analyser/nsrt/bug-13546.php @@ -72,3 +72,15 @@ function firstInCondition(array $array): mixed assertType('list', $array); return null; } + +/** @param list $array */ +function maybeNull(array $array, ?int $nullOrInt, ?string $nullOrString): void +{ + if (array_key_first($array) !== $nullOrInt) { + assertType('list', $array); + } + if (array_key_first($array) !== $nullOrString) { + assertType('list', $array); + } + assertType('list', $array); +} From f2252dc0a5d129276c774922ad3ff0e92954b4b0 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 8 Nov 2025 15:37:54 +0100 Subject: [PATCH 5/6] fix php 7.4 compat --- tests/PHPStan/Analyser/nsrt/bug-13546.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-13546.php b/tests/PHPStan/Analyser/nsrt/bug-13546.php index c390b717d0..cfa4dd9aba 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-13546.php +++ b/tests/PHPStan/Analyser/nsrt/bug-13546.php @@ -63,7 +63,7 @@ function mixedLast(mixed $mixed): void } /** @param list $array */ -function firstInCondition(array $array): mixed +function firstInCondition(array $array) { if (($key = array_key_first($array)) !== null) { assertType('list', $array); // could be 'non-empty-list' From be0c368c197405508e9df8c4f50e79faceebcbd5 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 8 Nov 2025 15:52:51 +0100 Subject: [PATCH 6/6] Update bug-13546.php --- tests/PHPStan/Analyser/nsrt/bug-13546.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-13546.php b/tests/PHPStan/Analyser/nsrt/bug-13546.php index cfa4dd9aba..63eb5ee889 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-13546.php +++ b/tests/PHPStan/Analyser/nsrt/bug-13546.php @@ -36,7 +36,7 @@ function last(array $array): void assertType('array', $array); } -function maybeArray(array $array, mixed $mixed): void +function maybeArray(array $array, $mixed): void { $arrayOrMixed = rand(0, 1) ? $array : $mixed; @@ -48,7 +48,7 @@ function maybeArray(array $array, mixed $mixed): void assertType('mixed', $arrayOrMixed); } -function mixedLast(mixed $mixed): void +function mixedLast($mixed): void { if (is_array($mixed)) { return;