diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 1abb42b684..1e22d530a4 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -747,8 +747,8 @@ public function specifyTypesInCondition( ) { $dimFetch = new ArrayDimFetch($arrayArg, $expr->var); $iterableValueType = $expr->expr->name->toLowerString() === 'array_key_first' - ? $arrayType->getFirstIterableValueType() - : $arrayType->getLastIterableValueType(); + ? $arrayType->getIterableValueType() + : $arrayType->getIterableValueType(); return $specifiedTypes->unionWith( $this->create($dimFetch, $iterableValueType, TypeSpecifierContext::createTrue(), $scope), @@ -775,7 +775,7 @@ public function specifyTypesInCondition( $dimFetch = new ArrayDimFetch($arrayArg, $expr->var); return $specifiedTypes->unionWith( - $this->create($dimFetch, $arrayType->getLastIterableValueType(), TypeSpecifierContext::createTrue(), $scope), + $this->create($dimFetch, $arrayType->getIterableValueType(), TypeSpecifierContext::createTrue(), $scope), ); } } diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index a065b77612..16cb33d2dd 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -690,12 +690,12 @@ public function getIterableKeyType(): Type public function getFirstIterableKeyType(): Type { - return $this->intersectTypes(static fn (Type $type): Type => $type->getFirstIterableKeyType()); + return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableKeyType()); } public function getLastIterableKeyType(): Type { - return $this->intersectTypes(static fn (Type $type): Type => $type->getLastIterableKeyType()); + return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableKeyType()); } public function getIterableValueType(): Type @@ -705,12 +705,12 @@ public function getIterableValueType(): Type public function getFirstIterableValueType(): Type { - return $this->intersectTypes(static fn (Type $type): Type => $type->getFirstIterableValueType()); + return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableValueType()); } public function getLastIterableValueType(): Type { - return $this->intersectTypes(static fn (Type $type): Type => $type->getLastIterableValueType()); + return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableValueType()); } public function isArray(): TrinaryLogic diff --git a/src/Type/Php/ArrayFirstLastDynamicReturnTypeExtension.php b/src/Type/Php/ArrayFirstLastDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..18a7d547ec --- /dev/null +++ b/src/Type/Php/ArrayFirstLastDynamicReturnTypeExtension.php @@ -0,0 +1,49 @@ +getName(), ['array_first', 'array_last'], true); + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + $args = $functionCall->getArgs(); + + if (count($args) < 1) { + return null; + } + + $argType = $scope->getType($args[0]->value); + $iterableAtLeastOnce = $argType->isIterableAtLeastOnce(); + + if ($iterableAtLeastOnce->no()) { + return new NullType(); + } + + $valueType = $argType->getIterableValueType(); + + if ($iterableAtLeastOnce->yes()) { + return $valueType; + } + + return TypeCombinator::union($valueType, new NullType()); + } + +} diff --git a/src/Type/Php/ArrayKeyFirstDynamicReturnTypeExtension.php b/src/Type/Php/ArrayKeyFirstDynamicReturnTypeExtension.php index 64fea966d6..2e926f7554 100644 --- a/src/Type/Php/ArrayKeyFirstDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayKeyFirstDynamicReturnTypeExtension.php @@ -32,7 +32,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return new NullType(); } - $keyType = $argType->getFirstIterableKeyType(); + $keyType = $argType->getIterableKeyType(); if ($iterableAtLeastOnce->yes()) { return $keyType; } diff --git a/src/Type/Php/ArrayKeyLastDynamicReturnTypeExtension.php b/src/Type/Php/ArrayKeyLastDynamicReturnTypeExtension.php index c3865ada3c..a750c9bea5 100644 --- a/src/Type/Php/ArrayKeyLastDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayKeyLastDynamicReturnTypeExtension.php @@ -32,7 +32,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return new NullType(); } - $keyType = $argType->getLastIterableKeyType(); + $keyType = $argType->getIterableKeyType(); if ($iterableAtLeastOnce->yes()) { return $keyType; } diff --git a/src/Type/Php/ArrayPointerFunctionsDynamicReturnTypeExtension.php b/src/Type/Php/ArrayPointerFunctionsDynamicReturnTypeExtension.php index eef5642028..0a73278d0c 100644 --- a/src/Type/Php/ArrayPointerFunctionsDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayPointerFunctionsDynamicReturnTypeExtension.php @@ -45,8 +45,8 @@ public function getTypeFromFunctionCall( } $itemType = $functionReflection->getName() === 'reset' - ? $argType->getFirstIterableValueType() - : $argType->getLastIterableValueType(); + ? $argType->getIterableValueType() + : $argType->getIterableValueType(); if ($iterableAtLeastOnce->yes()) { return $itemType; } diff --git a/src/Type/Php/ArrayPopFunctionReturnTypeExtension.php b/src/Type/Php/ArrayPopFunctionReturnTypeExtension.php index 61bfdf69e6..5a570752ed 100644 --- a/src/Type/Php/ArrayPopFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayPopFunctionReturnTypeExtension.php @@ -32,7 +32,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return new NullType(); } - $itemType = $argType->getLastIterableValueType(); + $itemType = $argType->getIterableValueType(); if ($iterableAtLeastOnce->yes()) { return $itemType; } diff --git a/src/Type/Php/ArrayShiftFunctionReturnTypeExtension.php b/src/Type/Php/ArrayShiftFunctionReturnTypeExtension.php index b961e624e0..704756157d 100644 --- a/src/Type/Php/ArrayShiftFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayShiftFunctionReturnTypeExtension.php @@ -32,7 +32,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return new NullType(); } - $itemType = $argType->getFirstIterableValueType(); + $itemType = $argType->getIterableValueType(); if ($iterableAtLeastOnce->yes()) { return $itemType; } diff --git a/src/Type/StaticType.php b/src/Type/StaticType.php index 1f659eb4f5..21622b5b93 100644 --- a/src/Type/StaticType.php +++ b/src/Type/StaticType.php @@ -401,12 +401,12 @@ public function getIterableKeyType(): Type public function getFirstIterableKeyType(): Type { - return $this->getStaticObjectType()->getFirstIterableKeyType(); + return $this->getStaticObjectType()->getIterableKeyType(); } public function getLastIterableKeyType(): Type { - return $this->getStaticObjectType()->getLastIterableKeyType(); + return $this->getStaticObjectType()->getIterableKeyType(); } public function getIterableValueType(): Type @@ -416,12 +416,12 @@ public function getIterableValueType(): Type public function getFirstIterableValueType(): Type { - return $this->getStaticObjectType()->getFirstIterableValueType(); + return $this->getStaticObjectType()->getIterableValueType(); } public function getLastIterableValueType(): Type { - return $this->getStaticObjectType()->getLastIterableValueType(); + return $this->getStaticObjectType()->getIterableValueType(); } public function isOffsetAccessible(): TrinaryLogic diff --git a/src/Type/Traits/LateResolvableTypeTrait.php b/src/Type/Traits/LateResolvableTypeTrait.php index 6377fb21eb..d35ee72461 100644 --- a/src/Type/Traits/LateResolvableTypeTrait.php +++ b/src/Type/Traits/LateResolvableTypeTrait.php @@ -200,12 +200,12 @@ public function getIterableKeyType(): Type public function getFirstIterableKeyType(): Type { - return $this->resolve()->getFirstIterableKeyType(); + return $this->resolve()->getIterableKeyType(); } public function getLastIterableKeyType(): Type { - return $this->resolve()->getLastIterableKeyType(); + return $this->resolve()->getIterableKeyType(); } public function getIterableValueType(): Type @@ -215,12 +215,12 @@ public function getIterableValueType(): Type public function getFirstIterableValueType(): Type { - return $this->resolve()->getFirstIterableValueType(); + return $this->resolve()->getIterableValueType(); } public function getLastIterableValueType(): Type { - return $this->resolve()->getLastIterableValueType(); + return $this->resolve()->getIterableValueType(); } public function isArray(): TrinaryLogic diff --git a/src/Type/Type.php b/src/Type/Type.php index 9e3480016a..e9cbca0b7a 100644 --- a/src/Type/Type.php +++ b/src/Type/Type.php @@ -117,14 +117,18 @@ public function getArraySize(): Type; public function getIterableKeyType(): Type; + /** @deprecated use getIterableKeyType */ public function getFirstIterableKeyType(): Type; + /** @deprecated use getIterableKeyType */ public function getLastIterableKeyType(): Type; public function getIterableValueType(): Type; + /** @deprecated use getIterableValueType */ public function getFirstIterableValueType(): Type; + /** @deprecated use getIterableValueType */ public function getLastIterableValueType(): Type; public function isArray(): TrinaryLogic; diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index 5d98439a93..4ac858b067 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -933,7 +933,7 @@ private static function optimizeConstantArrays(array $types): array $valueTypes = []; foreach ($results as $result) { $keyTypes[] = $result->getIterableKeyType(); - $valueTypes[] = $result->getLastIterableValueType(); + $valueTypes[] = $result->getIterableValueType(); if ($result->isList()->yes()) { continue; } diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index 77928d2cea..a1855a713b 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -639,12 +639,12 @@ public function getIterableKeyType(): Type public function getFirstIterableKeyType(): Type { - return $this->unionTypes(static fn (Type $type): Type => $type->getFirstIterableKeyType()); + return $this->unionTypes(static fn (Type $type): Type => $type->getIterableKeyType()); } public function getLastIterableKeyType(): Type { - return $this->unionTypes(static fn (Type $type): Type => $type->getLastIterableKeyType()); + return $this->unionTypes(static fn (Type $type): Type => $type->getIterableKeyType()); } public function getIterableValueType(): Type @@ -654,12 +654,12 @@ public function getIterableValueType(): Type public function getFirstIterableValueType(): Type { - return $this->unionTypes(static fn (Type $type): Type => $type->getFirstIterableValueType()); + return $this->unionTypes(static fn (Type $type): Type => $type->getIterableValueType()); } public function getLastIterableValueType(): Type { - return $this->unionTypes(static fn (Type $type): Type => $type->getLastIterableValueType()); + return $this->unionTypes(static fn (Type $type): Type => $type->getIterableValueType()); } public function isArray(): TrinaryLogic diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index 22f21b50a2..41358ea82e 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -4790,7 +4790,7 @@ public static function dataArrayFunctions(): array 'array_map(function (): \stdClass {}, $conditionalKeysArray)', ], [ - 'stdClass', + '\'foo\'|stdClass', 'array_pop($stringKeys)', ], [ @@ -4802,7 +4802,7 @@ public static function dataArrayFunctions(): array 'array_pop($stdClassesWithIsset)', ], [ - '\'foo\'', + '\'foo\'|stdClass', 'array_shift($stringKeys)', ], [ @@ -7169,23 +7169,23 @@ public static function dataArrayPointerFunctions(): array 'reset($emptyConstantArray)', ], [ - '1', + '1|2', 'reset($constantArray)', ], [ - '\'baz\'|\'foo\'', + '\'bar\'|\'baz\'|\'foo\'', 'reset($conditionalArray)', ], [ - '0|1', + '0|1|2', 'reset($constantArrayOptionalKeys1)', ], [ - '0', + '0|1|2', 'reset($constantArrayOptionalKeys2)', ], [ - '0', + '0|1|2', 'reset($constantArrayOptionalKeys3)', ], [ @@ -7205,23 +7205,23 @@ public static function dataArrayPointerFunctions(): array 'end($emptyConstantArray)', ], [ - '2', + '1|2', 'end($constantArray)', ], [ - '\'bar\'|\'baz\'', + '\'bar\'|\'baz\'|\'foo\'', 'end($secondConditionalArray)', ], [ - '2', + '0|1|2', 'end($constantArrayOptionalKeys1)', ], [ - '2', + '0|1|2', 'end($constantArrayOptionalKeys2)', ], [ - '1|2', + '0|1|2', 'end($constantArrayOptionalKeys3)', ], ]; @@ -8644,43 +8644,43 @@ public static function dataPhp73Functions(): array 'array_key_last($emptyArray)', ], [ - '0', + '0|1|2', 'array_key_first($literalArray)', ], [ - '2', + '0|1|2', 'array_key_last($literalArray)', ], [ - '0', + '0|1|2|3', 'array_key_first($anotherLiteralArray)', ], [ - '2|3', + '0|1|2|3', 'array_key_last($anotherLiteralArray)', ], [ - "'a'|'b'", + "'a'|'b'|'c'", 'array_key_first($constantArrayOptionalKeys1)', ], [ - "'c'", + "'a'|'b'|'c'", 'array_key_last($constantArrayOptionalKeys1)', ], [ - "'a'", + "'a'|'b'|'c'", 'array_key_first($constantArrayOptionalKeys2)', ], [ - "'c'", + "'a'|'b'|'c'", 'array_key_last($constantArrayOptionalKeys2)', ], [ - "'a'", + "'a'|'b'|'c'", 'array_key_first($constantArrayOptionalKeys3)', ], [ - "'b'|'c'", + "'a'|'b'|'c'", 'array_key_last($constantArrayOptionalKeys3)', ], [ diff --git a/tests/PHPStan/Analyser/nsrt/array-pop.php b/tests/PHPStan/Analyser/nsrt/array-pop.php index 37a986b0ce..971859d166 100644 --- a/tests/PHPStan/Analyser/nsrt/array-pop.php +++ b/tests/PHPStan/Analyser/nsrt/array-pop.php @@ -32,7 +32,7 @@ public function compoundTypes(array $arr): void public function constantArrays(array $arr): void { /** @var array{a: 0, b: 1, c: 2} $arr */ - assertType('2', array_pop($arr)); + assertType('0|1|2', array_pop($arr)); assertType('array{a: 0, b: 1}', $arr); /** @var array{} $arr */ @@ -43,15 +43,15 @@ public function constantArrays(array $arr): void public function constantArraysWithOptionalKeys(array $arr): void { /** @var array{a?: 0, b: 1, c: 2} $arr */ - assertType('2', array_pop($arr)); + assertType('0|1|2', array_pop($arr)); assertType('array{a?: 0, b: 1}', $arr); /** @var array{a: 0, b?: 1, c: 2} $arr */ - assertType('2', array_pop($arr)); + assertType('0|1|2', array_pop($arr)); assertType('array{a: 0, b?: 1}', $arr); /** @var array{a: 0, b: 1, c?: 2} $arr */ - assertType('1|2', array_pop($arr)); + assertType('0|1|2', array_pop($arr)); assertType('array{a: 0, b?: 1}', $arr); /** @var array{a?: 0, b?: 1, c?: 2} $arr */ diff --git a/tests/PHPStan/Analyser/nsrt/array-shift.php b/tests/PHPStan/Analyser/nsrt/array-shift.php index 2d8ef21d7e..12bbd0a1ba 100644 --- a/tests/PHPStan/Analyser/nsrt/array-shift.php +++ b/tests/PHPStan/Analyser/nsrt/array-shift.php @@ -32,7 +32,7 @@ public function compoundTypes(array $arr): void public function constantArrays(array $arr): void { /** @var array{a: 0, b: 1, c: 2} $arr */ - assertType('0', array_shift($arr)); + assertType('0|1|2', array_shift($arr)); assertType('array{b: 1, c: 2}', $arr); /** @var array{} $arr */ @@ -43,15 +43,15 @@ public function constantArrays(array $arr): void public function constantArraysWithOptionalKeys(array $arr): void { /** @var array{a?: 0, b: 1, c: 2} $arr */ - assertType('0|1', array_shift($arr)); + assertType('0|1|2', array_shift($arr)); assertType('array{b?: 1, c: 2}', $arr); /** @var array{a: 0, b?: 1, c: 2} $arr */ - assertType('0', array_shift($arr)); + assertType('0|1|2', array_shift($arr)); assertType('array{b?: 1, c: 2}', $arr); /** @var array{a: 0, b: 1, c?: 2} $arr */ - assertType('0', array_shift($arr)); + assertType('0|1|2', array_shift($arr)); assertType('array{b: 1, c?: 2}', $arr); /** @var array{a?: 0, b?: 1, c?: 2} $arr */ diff --git a/tests/PHPStan/Analyser/nsrt/array_first_last.php b/tests/PHPStan/Analyser/nsrt/array_first_last.php new file mode 100644 index 0000000000..cf3ba4857e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array_first_last.php @@ -0,0 +1,25 @@ += 8.5 + +namespace ArrayFirstLast; + +use function PHPStan\Testing\assertType; + +/** + * @param string[] $stringArray + * @param non-empty-array $nonEmptyArray + * @param array{1: 'bar', baz: 'foo'} $arrayShape + */ +function doFoo(array $stringArray, array $nonEmptyArray, $mixed, $arrayShape): void +{ + assertType("'a'|'b'|'c'", array_first([1 => 'a', 0 => 'b', 2 => 'c'])); + assertType("'bar'|'foo'", array_first($arrayShape)); + assertType('string|null', array_first($stringArray)); + assertType('string', array_first($nonEmptyArray)); + assertType('mixed', array_first($mixed)); + + assertType("'a'|'b'|'c'", array_last([1 => 'a', 0 => 'b', 2 => 'c'])); + assertType("'bar'|'foo'", array_last($arrayShape)); + assertType('string|null', array_last($stringArray)); + assertType('string', array_last($nonEmptyArray)); + assertType('mixed', array_last($mixed)); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-8084.php b/tests/PHPStan/Analyser/nsrt/bug-8084.php index fe5869e8e2..5879176649 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-8084.php +++ b/tests/PHPStan/Analyser/nsrt/bug-8084.php @@ -14,7 +14,7 @@ class Bug8084 */ public function run(array $arr): void { - assertType('0', array_shift($arr) ?? throw new Exception()); + assertType('0|1', array_shift($arr) ?? throw new Exception()); assertType('1|null', array_shift($arr)); } } diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 597b4000e3..879c86f791 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -852,7 +852,7 @@ public function testArrayDimFetchOnArrayKeyFirsOrLastOrCount(): void $this->analyse([__DIR__ . '/data/array-dim-fetch-on-array-key-first-last.php'], [ [ - 'Offset 0|null might not exist on list.', + 'Offset int<0, max>|null might not exist on list.', 12, ], [