Skip to content

Commit

Permalink
in_array returns false in strict mode if types are incompatibles
Browse files Browse the repository at this point in the history
see #5552
  • Loading branch information
mathroc committed Dec 12, 2021
1 parent 2a570fb commit 2374d24
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 31 deletions.
1 change: 1 addition & 0 deletions src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php
Expand Up @@ -75,6 +75,7 @@ public function __construct()
$this->registerClass(ReturnTypeProvider\MinMaxReturnTypeProvider::class);
$this->registerClass(ReturnTypeProvider\TriggerErrorReturnTypeProvider::class);
$this->registerClass(ReturnTypeProvider\RandReturnTypeProvider::class);
$this->registerClass(ReturnTypeProvider\InArrayReturnTypeProvider::class);
}

/**
Expand Down
@@ -0,0 +1,81 @@
<?php
namespace Psalm\Internal\Provider\ReturnTypeProvider;

use PhpParser\Node\Expr\ConstFetch;
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
use Psalm\Plugin\EventHandler\Event\FunctionReturnTypeProviderEvent;
use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface;
use Psalm\Type;
use Psalm\Type\Atomic\TArray;
use Psalm\Type\Atomic\TKeyedArray;
use Psalm\Type\Atomic\TList;

use function strtolower;

class InArrayReturnTypeProvider implements FunctionReturnTypeProviderInterface
{
/**
* @return array<lowercase-string>
*/
public static function getFunctionIds(): array
{
return ['in_array'];
}

public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $event): Type\Union
{
$call_args = $event->getCallArgs();

if (!isset($call_args[2])) {
return Type::getBool();
}

$third_arg = $call_args[2]->value;

if (!$third_arg instanceof ConstFetch
|| strtolower($third_arg->name->parts[0]) !== 'true'
|| !isset($call_args[0])
|| !isset($call_args[1])
) {
return Type::getBool();
}

$needle_type = $event->getStatementsSource()->getNodeTypeProvider()->getType($call_args[0]->value);
$haystack_type = $event->getStatementsSource()->getNodeTypeProvider()->getType($call_args[1]->value);

if ($needle_type === null || $haystack_type === null) {
return Type::getBool();
}

/**
* @var TKeyedArray|TArray|TList|null
*/
$array_arg_type = ($types = $haystack_type->getAtomicTypes()) && isset($types['array'])
? $types['array']
: null;

if ($array_arg_type instanceof TKeyedArray) {
$array_arg_type = $array_arg_type->getGenericArrayType();
}

if ($array_arg_type instanceof TList) {
$array_arg_type = new TArray([Type::getInt(), $array_arg_type->type_param]);
}

if (!$array_arg_type instanceof TArray) {
return Type::getBool();
}

$haystack_item_type = $array_arg_type->type_params[1];

if (UnionTypeComparator::canExpressionTypesBeIdentical(
$event->getStatementsSource()->getCodebase(),
$needle_type,
$haystack_item_type
)) {
return Type::getBool();
}

return Type::getFalse();
}
}
5 changes: 0 additions & 5 deletions src/Psalm/Internal/Type/TypeCombiner.php
Expand Up @@ -63,7 +63,6 @@
use function array_values;
use function count;
use function get_class;
use function in_array;
use function is_int;
use function is_numeric;
use function strpos;
Expand Down Expand Up @@ -93,10 +92,6 @@ public static function combine(
bool $allow_mixed_union = true,
int $literal_limit = 500
): Union {
if (in_array(null, $types, true)) {
return Type::getMixed();
}

if (count($types) === 1) {
$union_type = new Union([$types[0]]);

Expand Down
66 changes: 66 additions & 0 deletions tests/ReturnTypeProvider/InArrayTest.php
@@ -0,0 +1,66 @@
<?php

namespace Psalm\Tests\ReturnTypeProvider;

use Psalm\Tests\TestCase;
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;

class InArrayTest extends TestCase
{
use ValidCodeAnalysisTestTrait;

public function providerValidCodeParse(): iterable
{
yield 'inArrayNonStrictCallReturnsBoolWhenTypesAreCompatible' => [
'<?php
/**
* @return string[]
*/
function f(): array {
return ["1"];
}
$ret = in_array("1", f());
',
['$ret' => 'bool'],
];

yield 'inArrayNonStrictCallReturnsBoolWhenTypesAreIncompatible' => [
'<?php
/**
* @return string[]
*/
function f(): array {
return ["1"];
}
$ret = in_array(1, f());
',
['$ret' => 'bool'],
];

yield 'inArrayStrictCallReturnsFalseWhenTypesAreIncompatible' => [
'<?php
/**
* @return string[]
*/
function f(): array {
return ["1"];
}
$ret = in_array(1, f(), true);
',
['$ret' => 'false'],
];

yield 'inArrayStrictCallReturnsBoolWhenTypesAreCompatible' => [
'<?php
/**
* @return string[]
*/
function f(): array {
return ["1"];
}
$ret = in_array("1", f(), true);
',
['$ret' => 'bool'],
];
}
}
35 changes: 9 additions & 26 deletions tests/TypeReconciliation/InArrayTest.php
Expand Up @@ -122,7 +122,7 @@ function assertInArray($x, $y) {
return $x;
}',
'assertions' => [],
'error_level' => ['RedundantConditionGivenDocblockType'],
'error_level' => ['RedundantConditionGivenDocblockType', 'TypeDoesNotContainType'],
],
'assertNegatedInArrayOfNotIntersectingTypeReturnsOriginalType' => [
'<?php
Expand All @@ -139,7 +139,7 @@ function assertInArray($x, $y) {
throw new \Exception();
}',
'assertions' => [],
'error_level' => ['RedundantConditionGivenDocblockType'],
'error_level' => ['RedundantCondition'],
],
'assertAgainstListOfLiteralsAndScalarUnion' => [
'<?php
Expand Down Expand Up @@ -312,7 +312,7 @@ function assertInArray($x, $y) {
return $x;
}',
'error_message' => 'RedundantConditionGivenDocblockType - src' . DIRECTORY_SEPARATOR . 'somefile.php:8:29 - Docblock-defined type int for $x is never string',
'error_message' => 'TypeDoesNotContainType - src' . DIRECTORY_SEPARATOR . 'somefile.php:8:29 - Operand of type false is always false',
],
'assertNegatedInArrayOfNotIntersectingTypeTriggersRedundantCondition' => [
'<?php
Expand All @@ -328,9 +328,9 @@ function assertInArray($x, $y) {
throw new \Exception();
}',
'error_message' => 'RedundantConditionGivenDocblockType - src' . DIRECTORY_SEPARATOR . 'somefile.php:8:30 - Docblock-defined type int for $x is never string',
'error_message' => 'RedundantCondition - src' . DIRECTORY_SEPARATOR . 'somefile.php:8:29 - Operand of type true is always true',
],
'assertInArrayOfNotIntersectingTypeTriggersRedundantCondition' => [
'assertInArrayOfNotIntersectingTypeTriggersTypeDoesNotContainType' => [
'<?php
/**
* @param int $x
Expand All @@ -344,9 +344,9 @@ function assertInArray($x, $y) {
throw new \Exception();
}',
'error_message' => 'RedundantConditionGivenDocblockType - src' . DIRECTORY_SEPARATOR . 'somefile.php:8:29 - Docblock-defined type int for $x is never string',
'error_message' => 'TypeDoesNotContainType - src' . DIRECTORY_SEPARATOR . 'somefile.php:8:29 - Operand of type false is always false',
],
'assertInArrayOfNotIntersectingTypeReturnsTriggersMixedReturnStatement' => [
'assertInArrayOfNotIntersectingTypeReturnsTriggersTypeDoesNotContainType' => [
'<?php
/**
* @param int $x
Expand All @@ -360,7 +360,7 @@ function assertInArray($x, $y) {
throw new \Exception();
}',
'error_message' => 'MixedReturnStatement - src' . DIRECTORY_SEPARATOR . 'somefile.php:9:36 - Could not infer a return type',
'error_message' => 'TypeDoesNotContainType - src' . DIRECTORY_SEPARATOR . 'somefile.php:8:29 - Operand of type false is always false',
'error_levels' => ['RedundantConditionGivenDocblockType'],
],
'assertNegatedInArrayOfNotIntersectingTypeTriggersTypeContradiction' => [
Expand All @@ -377,24 +377,7 @@ function assertInArray($x, $y) {
return $x;
}',
'error_message' => 'RedundantConditionGivenDocblockType - src' . DIRECTORY_SEPARATOR . 'somefile.php:8:30 - Docblock-defined type int for $x is never string',
],
'assertNegatedInArrayOfNotIntersectingTypeTriggersMixedReturnStatement' => [
'<?php
/**
* @param int $x
* @param list<string> $y
* @return string
*/
function assertInArray($x, $y) {
if (!in_array($x, $y, true)) {
throw new \Exception();
}
return $x;
}',
'error_message' => 'MixedReturnStatement - src' . DIRECTORY_SEPARATOR . 'somefile.php:12:32 - Could not infer a return type',
'error_level' => ['RedundantConditionGivenDocblockType'],
'error_message' => 'RedundantCondition - src' . DIRECTORY_SEPARATOR . 'somefile.php:8:29 - Operand of type true is always true',
],
'inArrayDetectType' => [
'<?php
Expand Down

0 comments on commit 2374d24

Please sign in to comment.