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
Show file tree
Hide file tree
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
Expand Up @@ -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();
}
Expand Down
15 changes: 12 additions & 3 deletions src/Type/Accessory/HasOffsetValueType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion src/Type/Accessory/NonEmptyArrayType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
2 changes: 1 addition & 1 deletion src/Type/Accessory/OversizedArrayType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
6 changes: 5 additions & 1 deletion src/Type/ArrayType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

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

Expand Down
4 changes: 2 additions & 2 deletions src/Type/UnionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 13 additions & 5 deletions tests/PHPStan/Analyser/nsrt/array-search.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
Expand All @@ -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));
}

Expand Down
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/nsrt/bug-3789.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Up @@ -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));
}
Loading