From 39fb4222d57151b8cf4e0e5e4f46ebe4a850dd48 Mon Sep 17 00:00:00 2001 From: Mathieu Rochette Date: Sun, 12 Dec 2021 18:00:02 +0100 Subject: [PATCH] in_array returns false in strict mode if types are incompatibles see #5552 --- .../Provider/FunctionReturnTypeProvider.php | 1 + .../InArrayReturnTypeProvider.php | 82 +++++++++++++++++++ src/Psalm/Internal/Type/TypeCombiner.php | 5 -- tests/ReturnTypeProvider/InArrayTest.php | 66 +++++++++++++++ tests/TypeReconciliation/InArrayTest.php | 47 ++--------- 5 files changed, 156 insertions(+), 45 deletions(-) create mode 100644 src/Psalm/Internal/Provider/ReturnTypeProvider/InArrayReturnTypeProvider.php create mode 100644 tests/ReturnTypeProvider/InArrayTest.php diff --git a/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php b/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php index fc19fc9e623..33db1297a96 100644 --- a/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php @@ -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); } /** diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/InArrayReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/InArrayReturnTypeProvider.php new file mode 100644 index 00000000000..9d219c00950 --- /dev/null +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/InArrayReturnTypeProvider.php @@ -0,0 +1,82 @@ + + */ + public static function getFunctionIds(): array + { + return ['in_array']; + } + + public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $event): Type\Union + { + $call_args = $event->getCallArgs(); + $bool = Type::getBool(); + + if (!isset($call_args[0]) || !isset($call_args[1])) { + return $bool; + } + + $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 $bool; + } + + $false = Type::getFalse(); + $false->from_docblock = $bool->from_docblock = $needle_type->from_docblock || $haystack_type->from_docblock; + + if (!isset($call_args[2])) { + return $bool; + } + + $strict_type = $event->getStatementsSource()->getNodeTypeProvider()->getType($call_args[2]->value); + + if ($strict_type === null || !$strict_type->isTrue()) { + return $bool; + } + + /** + * @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 $bool; + } + + $haystack_item_type = $array_arg_type->type_params[1]; + + if (UnionTypeComparator::canExpressionTypesBeIdentical( + $event->getStatementsSource()->getCodebase(), + $needle_type, + $haystack_item_type + )) { + return $bool; + } + + return $false; + } +} diff --git a/src/Psalm/Internal/Type/TypeCombiner.php b/src/Psalm/Internal/Type/TypeCombiner.php index 689dae3c352..b308759ab05 100644 --- a/src/Psalm/Internal/Type/TypeCombiner.php +++ b/src/Psalm/Internal/Type/TypeCombiner.php @@ -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; @@ -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]]); diff --git a/tests/ReturnTypeProvider/InArrayTest.php b/tests/ReturnTypeProvider/InArrayTest.php new file mode 100644 index 00000000000..2bab0728640 --- /dev/null +++ b/tests/ReturnTypeProvider/InArrayTest.php @@ -0,0 +1,66 @@ + [ + ' 'bool'], + ]; + + yield 'inArrayNonStrictCallReturnsBoolWhenTypesAreIncompatible' => [ + ' 'bool'], + ]; + + yield 'inArrayStrictCallReturnsFalseWhenTypesAreIncompatible' => [ + ' 'false'], + ]; + + yield 'inArrayStrictCallReturnsBoolWhenTypesAreCompatible' => [ + ' 'bool'], + ]; + } +} diff --git a/tests/TypeReconciliation/InArrayTest.php b/tests/TypeReconciliation/InArrayTest.php index 35159301af3..10ac8c19a17 100644 --- a/tests/TypeReconciliation/InArrayTest.php +++ b/tests/TypeReconciliation/InArrayTest.php @@ -122,7 +122,7 @@ function assertInArray($x, $y) { return $x; }', 'assertions' => [], - 'error_level' => ['RedundantConditionGivenDocblockType'], + 'error_level' => ['RedundantConditionGivenDocblockType', 'DocblockTypeContradiction'], ], 'assertNegatedInArrayOfNotIntersectingTypeReturnsOriginalType' => [ ' 'RedundantConditionGivenDocblockType - src' . DIRECTORY_SEPARATOR . 'somefile.php:8:29 - Docblock-defined type int for $x is never string', + 'error_message' => 'DocblockTypeContradiction - src' . DIRECTORY_SEPARATOR . 'somefile.php:8:29 - Operand of type false is always false', ], 'assertNegatedInArrayOfNotIntersectingTypeTriggersRedundantCondition' => [ ' 'RedundantConditionGivenDocblockType - src' . DIRECTORY_SEPARATOR . 'somefile.php:8:30 - Docblock-defined type int for $x is never string', + 'error_message' => 'RedundantConditionGivenDocblockType - src' . DIRECTORY_SEPARATOR . 'somefile.php:8:29 - Operand of type true is always true', ], - 'assertInArrayOfNotIntersectingTypeTriggersRedundantCondition' => [ + 'assertInArrayOfNotIntersectingTypeTriggersDocblockTypeContradiction' => [ ' 'RedundantConditionGivenDocblockType - src' . DIRECTORY_SEPARATOR . 'somefile.php:8:29 - Docblock-defined type int for $x is never string', + 'error_message' => 'DocblockTypeContradiction - src' . DIRECTORY_SEPARATOR . 'somefile.php:8:29 - Operand of type false is always false', ], - 'assertInArrayOfNotIntersectingTypeReturnsTriggersMixedReturnStatement' => [ + 'assertInArrayOfNotIntersectingTypeReturnsTriggersDocblockTypeContradiction' => [ ' 'MixedReturnStatement - src' . DIRECTORY_SEPARATOR . 'somefile.php:9:36 - Could not infer a return type', + 'error_message' => 'DocblockTypeContradiction - src' . DIRECTORY_SEPARATOR . 'somefile.php:8:29 - Operand of type false is always false', 'error_levels' => ['RedundantConditionGivenDocblockType'], ], - 'assertNegatedInArrayOfNotIntersectingTypeTriggersTypeContradiction' => [ - ' $y - * @return string - */ - function assertInArray($x, $y) { - if (!in_array($x, $y, true)) { - throw new \Exception(); - } - - return $x; - }', - 'error_message' => 'RedundantConditionGivenDocblockType - src' . DIRECTORY_SEPARATOR . 'somefile.php:8:30 - Docblock-defined type int for $x is never string', - ], - 'assertNegatedInArrayOfNotIntersectingTypeTriggersMixedReturnStatement' => [ - ' $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'], - ], 'inArrayDetectType' => [ '