From 80d8fc6fd277e34cdcceb022410273dd4b468629 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 24 May 2025 14:18:54 +0200 Subject: [PATCH 1/2] Fix HasOffsetValueType::searchArray --- src/Type/Accessory/HasOffsetValueType.php | 7 ++++++- tests/PHPStan/Analyser/nsrt/array-search.php | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Type/Accessory/HasOffsetValueType.php b/src/Type/Accessory/HasOffsetValueType.php index b673a3e7eb..6090022576 100644 --- a/src/Type/Accessory/HasOffsetValueType.php +++ b/src/Type/Accessory/HasOffsetValueType.php @@ -15,10 +15,12 @@ use PHPStan\Type\ConstantScalarType; use PHPStan\Type\ErrorType; use PHPStan\Type\IntegerRangeType; +use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\MixedType; use PHPStan\Type\ObjectWithoutClassType; +use PHPStan\Type\StringType; use PHPStan\Type\Traits\MaybeArrayTypeTrait; use PHPStan\Type\Traits\MaybeCallableTypeTrait; use PHPStan\Type\Traits\MaybeIterableTypeTrait; @@ -266,7 +268,10 @@ public function searchArray(Type $needleType): Type $needleType instanceof ConstantScalarType && $this->valueType instanceof ConstantScalarType && $needleType->getValue() === $this->valueType->getValue() ) { - return $this->offsetType; + return new UnionType([ + new IntegerType(), + new StringType(), + ]); } return new MixedType(); diff --git a/tests/PHPStan/Analyser/nsrt/array-search.php b/tests/PHPStan/Analyser/nsrt/array-search.php index f26f6b7536..f11d02cb24 100644 --- a/tests/PHPStan/Analyser/nsrt/array-search.php +++ b/tests/PHPStan/Analyser/nsrt/array-search.php @@ -29,7 +29,7 @@ public function normalArrays(array $arr, string $string): void } if (array_key_exists(17, $arr) && $arr[17] === 'foo') { - assertType('17', array_search('foo', $arr, true)); + assertType('int', array_search('foo', $arr, true)); assertType('int|false', array_search('foo', $arr)); assertType('int|false', array_search($string, $arr, true)); } From b7056396c6e1f4fffbed716e7234bd3b4d9ff713 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 24 May 2025 14:56:39 +0200 Subject: [PATCH 2/2] Improve array_search inference --- src/Type/Accessory/AccessoryArrayListType.php | 2 +- src/Type/Accessory/HasOffsetValueType.php | 8 ++++-- src/Type/Accessory/NonEmptyArrayType.php | 2 +- src/Type/Accessory/OversizedArrayType.php | 2 +- src/Type/ArrayType.php | 6 ++++- src/Type/Constant/ConstantArrayType.php | 27 ++++++++++++------- src/Type/IntersectionType.php | 4 +-- src/Type/MixedType.php | 2 +- src/Type/NeverType.php | 2 +- ...archFunctionDynamicReturnTypeExtension.php | 15 +++-------- src/Type/StaticType.php | 4 +-- src/Type/Traits/LateResolvableTypeTrait.php | 4 +-- src/Type/Traits/MaybeArrayTypeTrait.php | 2 +- src/Type/Traits/NonArrayTypeTrait.php | 2 +- src/Type/Type.php | 2 +- src/Type/UnionType.php | 4 +-- tests/PHPStan/Analyser/nsrt/array-search.php | 16 ++++++++--- tests/PHPStan/Analyser/nsrt/bug-3789.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-7809.php | 4 +-- 19 files changed, 64 insertions(+), 46 deletions(-) diff --git a/src/Type/Accessory/AccessoryArrayListType.php b/src/Type/Accessory/AccessoryArrayListType.php index c2eea34349..bd76a18aa1 100644 --- a/src/Type/Accessory/AccessoryArrayListType.php +++ b/src/Type/Accessory/AccessoryArrayListType.php @@ -241,7 +241,7 @@ public function reverseArray(TrinaryLogic $preserveKeys): Type return new MixedType(); } - public function searchArray(Type $needleType): Type + public function searchArray(Type $needleType, TrinaryLogic $strict): Type { return new MixedType(); } diff --git a/src/Type/Accessory/HasOffsetValueType.php b/src/Type/Accessory/HasOffsetValueType.php index 6090022576..cd6d5d4691 100644 --- a/src/Type/Accessory/HasOffsetValueType.php +++ b/src/Type/Accessory/HasOffsetValueType.php @@ -262,11 +262,15 @@ public function reverseArray(TrinaryLogic $preserveKeys): Type return new NonEmptyArrayType(); } - public function searchArray(Type $needleType): Type + public function searchArray(Type $needleType, TrinaryLogic $strict): Type { if ( $needleType instanceof ConstantScalarType && $this->valueType instanceof ConstantScalarType - && $needleType->getValue() === $this->valueType->getValue() + && ( + $needleType->getValue() === $this->valueType->getValue() + // @phpstan-ignore equal.notAllowed + || ($strict->no() && $needleType->getValue() == $this->valueType->getValue()) // phpcs:ignore + ) ) { return new UnionType([ new IntegerType(), diff --git a/src/Type/Accessory/NonEmptyArrayType.php b/src/Type/Accessory/NonEmptyArrayType.php index 7b9312e6c5..21eb662d69 100644 --- a/src/Type/Accessory/NonEmptyArrayType.php +++ b/src/Type/Accessory/NonEmptyArrayType.php @@ -218,7 +218,7 @@ public function reverseArray(TrinaryLogic $preserveKeys): Type return $this; } - public function searchArray(Type $needleType): Type + public function searchArray(Type $needleType, TrinaryLogic $strict): Type { return new MixedType(); } diff --git a/src/Type/Accessory/OversizedArrayType.php b/src/Type/Accessory/OversizedArrayType.php index 3de317ba0e..a3be218957 100644 --- a/src/Type/Accessory/OversizedArrayType.php +++ b/src/Type/Accessory/OversizedArrayType.php @@ -214,7 +214,7 @@ public function reverseArray(TrinaryLogic $preserveKeys): Type return $this; } - public function searchArray(Type $needleType): Type + public function searchArray(Type $needleType, TrinaryLogic $strict): Type { return new MixedType(); } diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index 38d1e6741b..5ec3f5ae1c 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -601,8 +601,12 @@ public function reverseArray(TrinaryLogic $preserveKeys): Type return $this; } - public function searchArray(Type $needleType): Type + public function searchArray(Type $needleType, TrinaryLogic $strict): Type { + if ($strict->yes() && $this->getIterableValueType()->isSuperTypeOf($needleType)->no()) { + return new ConstantBooleanType(false); + } + return TypeCombinator::union($this->getIterableKeyType(), new ConstantBooleanType(false)); } diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index 16e999bcb3..6591921b60 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -909,22 +909,31 @@ public function reverseArray(TrinaryLogic $preserveKeys): Type return $builder->getArray(); } - public function searchArray(Type $needleType): Type + public function searchArray(Type $needleType, TrinaryLogic $strict): Type { $matches = []; $hasIdenticalValue = false; foreach ($this->valueTypes as $index => $valueType) { - $isNeedleSuperType = $valueType->isSuperTypeOf($needleType); - if ($isNeedleSuperType->no()) { - continue; + if ($strict->yes()) { + $isNeedleSuperType = $valueType->isSuperTypeOf($needleType); + if ($isNeedleSuperType->no()) { + continue; + } } - if ($needleType instanceof ConstantScalarType && $valueType instanceof ConstantScalarType - && $needleType->getValue() === $valueType->getValue() - && !$this->isOptionalKey($index) - ) { - $hasIdenticalValue = true; + if ($needleType instanceof ConstantScalarType && $valueType instanceof ConstantScalarType) { + // @phpstan-ignore equal.notAllowed + $isLooseEqual = $needleType->getValue() == $valueType->getValue(); // phpcs:ignore + if (!$isLooseEqual) { + continue; + } + if ( + ($strict->no() || $needleType->getValue() === $valueType->getValue()) + && !$this->isOptionalKey($index) + ) { + $hasIdenticalValue = true; + } } $matches[] = $this->keyTypes[$index]; diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 52abfdff30..39d8a8c25c 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -819,9 +819,9 @@ public function reverseArray(TrinaryLogic $preserveKeys): Type return $this->intersectTypes(static fn (Type $type): Type => $type->reverseArray($preserveKeys)); } - public function searchArray(Type $needleType): Type + public function searchArray(Type $needleType, TrinaryLogic $strict): Type { - return $this->intersectTypes(static fn (Type $type): Type => $type->searchArray($needleType)); + return $this->intersectTypes(static fn (Type $type): Type => $type->searchArray($needleType, $strict)); } public function shiftArray(): Type diff --git a/src/Type/MixedType.php b/src/Type/MixedType.php index 6ca4e4bad8..31e0e639f8 100644 --- a/src/Type/MixedType.php +++ b/src/Type/MixedType.php @@ -264,7 +264,7 @@ public function reverseArray(TrinaryLogic $preserveKeys): Type return new ArrayType(new MixedType($this->isExplicitMixed), new MixedType($this->isExplicitMixed)); } - public function searchArray(Type $needleType): Type + public function searchArray(Type $needleType, TrinaryLogic $strict): Type { if ($this->isArray()->no()) { return new ErrorType(); diff --git a/src/Type/NeverType.php b/src/Type/NeverType.php index a3d8daf1fe..769b3b1e36 100644 --- a/src/Type/NeverType.php +++ b/src/Type/NeverType.php @@ -336,7 +336,7 @@ public function reverseArray(TrinaryLogic $preserveKeys): Type return new NeverType(); } - public function searchArray(Type $needleType): Type + public function searchArray(Type $needleType, TrinaryLogic $strict): Type { return new NeverType(); } diff --git a/src/Type/Php/ArraySearchFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ArraySearchFunctionDynamicReturnTypeExtension.php index 762e577211..90040a953b 100644 --- a/src/Type/Php/ArraySearchFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArraySearchFunctionDynamicReturnTypeExtension.php @@ -11,7 +11,6 @@ use PHPStan\Type\NeverType; use PHPStan\Type\NullType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; use function count; final class ArraySearchFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension @@ -39,20 +38,14 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } if ($argsCount < 3) { - return TypeCombinator::union($haystackArgType->getIterableKeyType(), new ConstantBooleanType(false)); - } - - $strictArgType = $scope->getType($functionCall->getArgs()[2]->value); - if (!$strictArgType->isTrue()->yes()) { - return TypeCombinator::union($haystackArgType->getIterableKeyType(), new ConstantBooleanType(false)); + $strictArgType = new ConstantBooleanType(false); + } else { + $strictArgType = $scope->getType($functionCall->getArgs()[2]->value); } $needleArgType = $scope->getType($functionCall->getArgs()[0]->value); - if ($haystackArgType->getIterableValueType()->isSuperTypeOf($needleArgType)->no()) { - return new ConstantBooleanType(false); - } - return $haystackArgType->searchArray($needleArgType); + return $haystackArgType->searchArray($needleArgType, $strictArgType->isTrue()); } } diff --git a/src/Type/StaticType.php b/src/Type/StaticType.php index a28eacfab6..1a72ac6e95 100644 --- a/src/Type/StaticType.php +++ b/src/Type/StaticType.php @@ -450,9 +450,9 @@ public function reverseArray(TrinaryLogic $preserveKeys): Type return $this->getStaticObjectType()->reverseArray($preserveKeys); } - public function searchArray(Type $needleType): Type + public function searchArray(Type $needleType, TrinaryLogic $strict): Type { - return $this->getStaticObjectType()->searchArray($needleType); + return $this->getStaticObjectType()->searchArray($needleType, $strict); } public function shiftArray(): Type diff --git a/src/Type/Traits/LateResolvableTypeTrait.php b/src/Type/Traits/LateResolvableTypeTrait.php index e6355875ca..03152742ea 100644 --- a/src/Type/Traits/LateResolvableTypeTrait.php +++ b/src/Type/Traits/LateResolvableTypeTrait.php @@ -298,9 +298,9 @@ public function reverseArray(TrinaryLogic $preserveKeys): Type return $this->resolve()->reverseArray($preserveKeys); } - public function searchArray(Type $needleType): Type + public function searchArray(Type $needleType, TrinaryLogic $strict): Type { - return $this->resolve()->searchArray($needleType); + return $this->resolve()->searchArray($needleType, $strict); } public function shiftArray(): Type diff --git a/src/Type/Traits/MaybeArrayTypeTrait.php b/src/Type/Traits/MaybeArrayTypeTrait.php index afafc91708..701f999a93 100644 --- a/src/Type/Traits/MaybeArrayTypeTrait.php +++ b/src/Type/Traits/MaybeArrayTypeTrait.php @@ -79,7 +79,7 @@ public function reverseArray(TrinaryLogic $preserveKeys): Type return new ErrorType(); } - public function searchArray(Type $needleType): Type + public function searchArray(Type $needleType, TrinaryLogic $strict): Type { return new ErrorType(); } diff --git a/src/Type/Traits/NonArrayTypeTrait.php b/src/Type/Traits/NonArrayTypeTrait.php index 1d1b948242..495f278d0c 100644 --- a/src/Type/Traits/NonArrayTypeTrait.php +++ b/src/Type/Traits/NonArrayTypeTrait.php @@ -79,7 +79,7 @@ public function reverseArray(TrinaryLogic $preserveKeys): Type return new ErrorType(); } - public function searchArray(Type $needleType): Type + public function searchArray(Type $needleType, TrinaryLogic $strict): Type { return new ErrorType(); } diff --git a/src/Type/Type.php b/src/Type/Type.php index 6562ba2005..de85943a0c 100644 --- a/src/Type/Type.php +++ b/src/Type/Type.php @@ -172,7 +172,7 @@ public function popArray(): Type; public function reverseArray(TrinaryLogic $preserveKeys): Type; - public function searchArray(Type $needleType): Type; + public function searchArray(Type $needleType, TrinaryLogic $strict): Type; public function shiftArray(): Type; diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index 14eb812267..b8ff4d4f1c 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -779,9 +779,9 @@ public function reverseArray(TrinaryLogic $preserveKeys): Type return $this->unionTypes(static fn (Type $type): Type => $type->reverseArray($preserveKeys)); } - public function searchArray(Type $needleType): Type + public function searchArray(Type $needleType, TrinaryLogic $strict): Type { - return $this->unionTypes(static fn (Type $type): Type => $type->searchArray($needleType)); + return $this->unionTypes(static fn (Type $type): Type => $type->searchArray($needleType, $strict)); } public function shiftArray(): Type diff --git a/tests/PHPStan/Analyser/nsrt/array-search.php b/tests/PHPStan/Analyser/nsrt/array-search.php index f11d02cb24..680c56844a 100644 --- a/tests/PHPStan/Analyser/nsrt/array-search.php +++ b/tests/PHPStan/Analyser/nsrt/array-search.php @@ -30,7 +30,7 @@ public function normalArrays(array $arr, string $string): void if (array_key_exists(17, $arr) && $arr[17] === 'foo') { assertType('int', array_search('foo', $arr, true)); - assertType('int|false', array_search('foo', $arr)); + assertType('int', array_search('foo', $arr)); assertType('int|false', array_search($string, $arr, true)); } } @@ -39,25 +39,33 @@ public function constantArrays(array $arr, string $string): void { /** @var array{'a', 'b', 'c'} $arr */ assertType('1', array_search('b', $arr, true)); - assertType('0|1|2|false', array_search('b', $arr)); + assertType('1', array_search('b', $arr)); assertType('0|1|2|false', array_search($string, $arr, true)); + assertType('0|1|2|false', array_search($string, $arr, false)); /** @var array{} $arr */ assertType('false', array_search('b', $arr, true)); assertType('false', array_search('b', $arr)); assertType('false', array_search($string, $arr, true)); + assertType('false', array_search($string, $arr, false)); + + /** @var array{1, '1', '2'} $arr */ + assertType('1', array_search('1', $arr, true)); + assertType('0|1', array_search('1', $arr)); + assertType('1|2|false', array_search($string, $arr, true)); + assertType('0|1|2|false', array_search($string, $arr, false)); } public function constantArraysWithOptionalKeys(array $arr, string $string): void { /** @var array{0: 'a', 1?: 'b', 2: 'c'} $arr */ assertType('1|false', array_search('b', $arr, true)); - assertType('0|1|2|false', array_search('b', $arr)); + assertType('1|false', array_search('b', $arr)); assertType('0|1|2|false', array_search($string, $arr, true)); /** @var array{0: 'a', 1?: 'b', 2: 'b'} $arr */ assertType('1|2', array_search('b', $arr, true)); - assertType('0|1|2|false', array_search('b', $arr)); + assertType('1|2', array_search('b', $arr)); assertType('0|1|2|false', array_search($string, $arr, true)); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-3789.php b/tests/PHPStan/Analyser/nsrt/bug-3789.php index 133ce49b50..95934512e3 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-3789.php +++ b/tests/PHPStan/Analyser/nsrt/bug-3789.php @@ -19,5 +19,5 @@ function doFoo(string $needle, array $haystack): void { assertType('0|1|2', array_search('foo', $haystack, true)); assertType('0|1|2|false', array_search($needle, $haystack)); - assertType('0|1|2|false', array_search('foo', $haystack)); + assertType('0|1|2', array_search('foo', $haystack)); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-7809.php b/tests/PHPStan/Analyser/nsrt/bug-7809.php index 2769152110..a6c0c139cb 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-7809.php +++ b/tests/PHPStan/Analyser/nsrt/bug-7809.php @@ -6,7 +6,7 @@ use function PHPStan\Testing\assertType; function foo(bool $strict = false): void { - assertType('0|1|2|false', array_search('c', ['a', 'b', 'c'], $strict)); + assertType('2', array_search('c', ['a', 'b', 'c'], $strict)); } function bar(): void{ @@ -14,5 +14,5 @@ function bar(): void{ } function baz(): void{ - assertType('0|1|2|false', array_search('c', ['a', 'b', 'c'], false)); + assertType('2', array_search('c', ['a', 'b', 'c'], false)); }