Skip to content

Improve array_search inference #4014

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: 1.12.x
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Type/Accessory/AccessoryArrayListType.php
Original file line number Diff line number Diff line change
@@ -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();
}
15 changes: 12 additions & 3 deletions src/Type/Accessory/HasOffsetValueType.php
Original file line number Diff line number Diff line change
@@ -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;
@@ -260,13 +262,20 @@ 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 $this->offsetType;
return new UnionType([
new IntegerType(),
new StringType(),
]);
}

return new MixedType();
2 changes: 1 addition & 1 deletion src/Type/Accessory/NonEmptyArrayType.php
Original file line number Diff line number Diff line change
@@ -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();
}
2 changes: 1 addition & 1 deletion src/Type/Accessory/OversizedArrayType.php
Original file line number Diff line number Diff line change
@@ -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();
}
6 changes: 5 additions & 1 deletion src/Type/ArrayType.php
Original file line number Diff line number Diff line change
@@ -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));
}

27 changes: 18 additions & 9 deletions src/Type/Constant/ConstantArrayType.php
Original file line number Diff line number Diff line change
@@ -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];
4 changes: 2 additions & 2 deletions src/Type/IntersectionType.php
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion src/Type/MixedType.php
Original file line number Diff line number Diff line change
@@ -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();
2 changes: 1 addition & 1 deletion src/Type/NeverType.php
Original file line number Diff line number Diff line change
@@ -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();
}
15 changes: 4 additions & 11 deletions src/Type/Php/ArraySearchFunctionDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
@@ -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());
}

}
4 changes: 2 additions & 2 deletions src/Type/StaticType.php
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions src/Type/Traits/LateResolvableTypeTrait.php
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion src/Type/Traits/MaybeArrayTypeTrait.php
Original file line number Diff line number Diff line change
@@ -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();
}
2 changes: 1 addition & 1 deletion src/Type/Traits/NonArrayTypeTrait.php
Original file line number Diff line number Diff line change
@@ -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();
}
2 changes: 1 addition & 1 deletion src/Type/Type.php
Original file line number Diff line number Diff line change
@@ -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;

4 changes: 2 additions & 2 deletions src/Type/UnionType.php
Original file line number Diff line number Diff line change
@@ -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
18 changes: 13 additions & 5 deletions tests/PHPStan/Analyser/nsrt/array-search.php
Original file line number Diff line number Diff line change
@@ -29,8 +29,8 @@ 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|false', array_search('foo', $arr));
assertType('int', array_search('foo', $arr, true));
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));
}

2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/nsrt/bug-3789.php
Original file line number Diff line number Diff line change
@@ -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));
}
4 changes: 2 additions & 2 deletions tests/PHPStan/Analyser/nsrt/bug-7809.php
Original file line number Diff line number Diff line change
@@ -6,13 +6,13 @@
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{
assertType('2', array_search('c', ['a', 'b', 'c'], true));
}

function baz(): void{
assertType('0|1|2|false', array_search('c', ['a', 'b', 'c'], false));
assertType('2', array_search('c', ['a', 'b', 'c'], false));
}