From 0be474ec2c0fc8076f27100a53196ef619fc08d1 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Sat, 30 Mar 2024 20:13:23 +0100 Subject: [PATCH] Fixes --- .../Expression/Call/ArgumentAnalyzer.php | 2 + .../Call/ArrayFunctionArgumentsAnalyzer.php | 4 +- .../ArrayFilterParamsProvider.php | 4 +- .../ArrayChunkReturnTypeProvider.php | 6 +- .../ArrayColumnReturnTypeProvider.php | 43 +++++++++--- .../ArrayMapReturnTypeProvider.php | 2 +- .../FilterInputReturnTypeProvider.php | 70 ++++++++++--------- .../ReturnTypeProvider/FilterUtils.php | 37 +++++----- src/Psalm/Type.php | 20 ++++++ src/Psalm/Type/UnionTrait.php | 8 +-- 10 files changed, 123 insertions(+), 73 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php index e687818a467..b7ae69c9c76 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php @@ -61,6 +61,7 @@ use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Union; +use function assert; use function count; use function explode; use function implode; @@ -508,6 +509,7 @@ private static function checkFunctionLikeTypeMatches( } elseif ($unpacked_atomic_array instanceof TClassStringMap) { $arg_value_type = Type::getMixed(); } else { + assert($unpacked_atomic_array instanceof TArray); if (!$allow_named_args && !$unpacked_atomic_array->type_params[0]->isInt()) { $arg_key_allowed = false; } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php index 4d54c0d0e65..6490eb645bd 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php @@ -20,7 +20,6 @@ use Psalm\Internal\Type\Comparator\UnionTypeComparator; use Psalm\Internal\Type\TemplateResult; use Psalm\Internal\Type\TemplateStandinTypeReplacer; -use Psalm\Internal\Type\TypeCombiner; use Psalm\Internal\Type\TypeExpander; use Psalm\Issue\ArgumentTypeCoercion; use Psalm\Issue\InvalidArgument; @@ -337,6 +336,7 @@ public static function handleSplice( ) === false) { return false; } + $codebase = $statements_analyzer->getCodebase(); $array_types = []; $max_array_size = null; @@ -555,7 +555,7 @@ public static function handleSplice( } } - $by_ref_type = TypeCombiner::combine([$array_type, ...$replacement_arg_type->getArrayValueTypes()]); + $by_ref_type = Type::combineUnionTypeArray([new Union([$array_type]), ...$replacement_arg_type->getArrayValueTypes()], $codebase); AssignmentAnalyzer::assignByRefParam( $statements_analyzer, diff --git a/src/Psalm/Internal/Provider/ParamsProvider/ArrayFilterParamsProvider.php b/src/Psalm/Internal/Provider/ParamsProvider/ArrayFilterParamsProvider.php index 3407bc1e025..46762d9540a 100644 --- a/src/Psalm/Internal/Provider/ParamsProvider/ArrayFilterParamsProvider.php +++ b/src/Psalm/Internal/Provider/ParamsProvider/ArrayFilterParamsProvider.php @@ -103,8 +103,8 @@ public static function getFunctionParams(FunctionParamsProviderEvent $event): ?a $key_type = Type::getArrayKey(); $inner_type = Type::getMixed(); } else { - $inner_type = Type::combineUnionTypeArray($first_arg_type->getArrayValueTypes(), $codebase); - $key_type = Type::combineUnionTypeArray($first_arg_type->getArrayKeyTypes(), $codebase); + $inner_type = $first_arg_type->getArrayValueType($codebase); + $key_type = $first_arg_type->getArrayKeyType($codebase); } $has_both = false; diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayChunkReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayChunkReturnTypeProvider.php index b555b354df6..679be25732a 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayChunkReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayChunkReturnTypeProvider.php @@ -42,10 +42,10 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev new Union([ $preserve_keys ? new TNonEmptyArray([ - Type::combineUnionTypeArray($array_arg_type->getArrayKeyTypes(), $codebase), - Type::combineUnionTypeArray($array_arg_type->getArrayValueTypes(), $codebase), + $array_arg_type->getArrayKeyType($codebase), + $array_arg_type->getArrayValueType($codebase), ]) - : Type::getNonEmptyListAtomic(Type::combineUnionTypeArray($array_arg_type->getArrayValueTypes(), $codebase)), + : Type::getNonEmptyListAtomic($array_arg_type->getArrayValueType($codebase)), ]), ); } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayColumnReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayColumnReturnTypeProvider.php index d51714d8cb4..8aeb43369b3 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayColumnReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayColumnReturnTypeProvider.php @@ -74,19 +74,42 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev } } + $r = []; + foreach ($statements_source->node_data->getType($call_args[0]->value)?->getAtomicTypes() ?? [] as $t) { + $r []= self::handleInner( + $statements_source, + $context, + $code_location, + $t, + $key_column_name, + $key_column_name_is_null, + $value_column_name, + $value_column_name_is_null, + $third_arg_type, + ); + } + return new Union($r); + } + private static function handleInner( + StatementsAnalyzer $statements_source, + Context $context, + CodeLocation $code_location, + ?Atomic $input_array, + string|int|null $key_column_name, + bool $key_column_name_is_null, + string|int|null $value_column_name, + bool $value_column_name_is_null, + ?Union $third_arg_type, + ): Atomic { $row_type = $row_shape = null; $input_array_not_empty = false; // calculate row shape - if (($first_arg_type = $statements_source->node_data->getType($call_args[0]->value)) - && $first_arg_type->isSingle() - && $first_arg_type->hasArray() - ) { - $input_array = $first_arg_type->getArray(); + if ($input_array) { if ($input_array instanceof TKeyedArray && !$input_array->fallback_params && ($value_column_name !== null || $value_column_name_is_null) - && !($third_arg_type && !$key_column_name) + && !($third_arg_type && $key_column_name === null) ) { $properties = []; $ok = true; @@ -167,14 +190,14 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev } if ($ok) { if (!$properties) { - return Type::getEmptyArray(); + return Type::getEmptyArrayAtomic(); } - return new Union([new TKeyedArray( + return new TKeyedArray( $properties, null, $input_array->fallback_params, $is_list, - )]); + ); } } @@ -228,7 +251,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev : Type::getListAtomic($result_element_type ?? Type::getMixed()); } - return new Union([$type]); + return $type; } /** diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php index ecfd447c5d8..b920c181304 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php @@ -149,7 +149,7 @@ static function (array $sub) use ($null) { if (count($call_args) === 2) { $generic_key_type = $array_arg_type - ? Type::combineUnionTypeArray($array_arg_union_type->getArrayKeyTypes(), $codebase) + ? $array_arg_union_type->getArrayKeyType($codebase) : Type::getArrayKey(); } else { $generic_key_type = Type::getInt(); diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterInputReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterInputReturnTypeProvider.php index b3bc3ad48b4..fdfcfed5c73 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterInputReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterInputReturnTypeProvider.php @@ -6,6 +6,7 @@ use Psalm\Internal\Analyzer\Statements\Expression\Fetch\VariableFetchAnalyzer; use Psalm\Internal\Analyzer\StatementsAnalyzer; +use Psalm\Internal\Type\TypeCombiner; use Psalm\Plugin\EventHandler\Event\FunctionReturnTypeProviderEvent; use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface; use Psalm\Type; @@ -209,45 +210,46 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev // @todo eventually this needs to be changed when we fully support filter_has_var $global_type = VariableFetchAnalyzer::getGlobalType($global_name, $codebase->analysis_php_version_id); - $input_type = null; - if ($global_type->isArray() && $global_type->getArray() instanceof TKeyedArray) { - $array_instance = $global_type->getArray(); - if ($second_arg_type->isSingleStringLiteral()) { - $key = $second_arg_type->getSingleStringLiteral()->value; + $res = []; + foreach ($global_type->getArrays() as $array_atomic) { + $input_type = null; + if ($array_atomic instanceof TKeyedArray) { + if ($second_arg_type->isSingleStringLiteral()) { + $key = $second_arg_type->getSingleStringLiteral()->value; - if (isset($array_instance->properties[ $key ])) { - $input_type = $array_instance->properties[ $key ]; + if (isset($array_atomic->properties[ $key ])) { + $input_type = $array_atomic->properties[ $key ]; + } } - } - if ($input_type === null) { - $input_type = $array_instance->getGenericValueType(); + if ($input_type === null) { + $input_type = $array_atomic->getGenericValueType(); + $input_type = $input_type->setPossiblyUndefined(true); + } + } elseif ($array_atomic instanceof TArray) { + [$_, $input_type] = $array_atomic->type_params; $input_type = $input_type->setPossiblyUndefined(true); + } else { + // this is impossible + throw new UnexpectedValueException('This should not happen'); } - } elseif ($global_type->isArray() - && ($array_atomic = $global_type->getArray()) - && $array_atomic instanceof TArray) { - [$_, $input_type] = $array_atomic->type_params; - $input_type = $input_type->setPossiblyUndefined(true); - } else { - // this is impossible - throw new UnexpectedValueException('This should not happen'); - } - return FilterUtils::getReturnType( - $filter_int_used, - $flags_int_used, - $input_type, - $fails_type, - $not_set_type, - $statements_analyzer, - $code_location, - $codebase, - $function_id, - $has_range, - $min_range, - $max_range, - $regexp, - ); + $res []= FilterUtils::getReturnType( + $filter_int_used, + $flags_int_used, + $input_type, + $fails_type, + $not_set_type, + $statements_analyzer, + $code_location, + $codebase, + $function_id, + $has_range, + $min_range, + $max_range, + $regexp, + ); + } + return TypeCombiner::combine($res, $codebase); } } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php index 0cade67528d..68f0507e747 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php @@ -149,7 +149,7 @@ public static function getFilterArgValueOrError( return $filter_int_used; } - /** @return array{flags_int_used: int, options: TKeyedArray|null}|Union|null */ + /** @return array{flags_int_used: int, options: array|null}|Union|null */ public static function getOptionsArgValueOrError( Arg $options_arg, StatementsAnalyzer $statements_analyzer, @@ -170,9 +170,10 @@ public static function getOptionsArgValueOrError( 'options' => null, ); - $atomic_type = $options_arg_type->getArray(); - if ($atomic_type instanceof TKeyedArray) { - $redundant_keys = array_diff(array_keys($atomic_type->properties), array('flags', 'options')); + $properties = Type::mergeKeyedArrayProperties($options_arg_type, $codebase); + + if ($properties) { + $redundant_keys = array_diff(array_keys($properties), array('flags', 'options')); if ($redundant_keys !== array()) { // reported as it's usually an oversight/misunderstanding of how the function works // it's silently ignored by the function though @@ -186,10 +187,10 @@ public static function getOptionsArgValueOrError( ); } - if (isset($atomic_type->properties['options'])) { + if (isset($properties['options'])) { if ($filter_int_used === FILTER_CALLBACK) { $only_callables = true; - foreach ($atomic_type->properties['options']->getAtomicTypes() as $option_atomic) { + foreach ($properties['options']->getAtomicTypes() as $option_atomic) { if ($option_atomic->isCallableType()) { continue; } @@ -205,7 +206,7 @@ public static function getOptionsArgValueOrError( $only_callables = false; } - if ($atomic_type->properties['options']->possibly_undefined) { + if ($properties['options']->possibly_undefined) { $only_callables = false; } @@ -230,7 +231,7 @@ public static function getOptionsArgValueOrError( ); } - if (! $atomic_type->properties['options']->isArray()) { + if (! $properties['options']->isArray()) { // silently ignored by the function, but this usually indicates a bug IssueBuffer::maybeAdd( new InvalidArgument( @@ -241,8 +242,7 @@ public static function getOptionsArgValueOrError( ), $statements_analyzer->getSuppressedIssues(), ); - } elseif (($options_array = $atomic_type->properties['options']->getArray()) - && $options_array instanceof TKeyedArray) { + } elseif ($options_array = Type::mergeKeyedArrayProperties($properties['options'], $codebase)) { $defaults['options'] = $options_array; } else { // cannot infer a 100% correct specific return type @@ -250,10 +250,10 @@ public static function getOptionsArgValueOrError( } } - if (isset($atomic_type->properties['flags'])) { - if ($atomic_type->properties['flags']->isSingleIntLiteral()) { - $defaults['flags_int_used'] = $atomic_type->properties['flags']->getSingleIntLiteral()->value; - } elseif ($atomic_type->properties['flags']->isInt()) { + if (isset($properties['flags'])) { + if ($properties['flags']->isSingleIntLiteral()) { + $defaults['flags_int_used'] = $properties['flags']->getSingleIntLiteral()->value; + } elseif ($properties['flags']->isInt()) { // cannot infer a 100% correct specific return type $return_null = true; } else { @@ -494,11 +494,14 @@ public static function checkRedundantFlags( return null; } - /** @return array{Union|null, float|int|null, float|int|null, bool, non-falsy-string|true|null} */ + /** + * @param ?array $options + * @return array{Union|null, float|int|null, float|int|null, bool, non-falsy-string|true|null} + */ public static function getOptions( int $filter_int_used, int $flags_int_used, - ?TKeyedArray $options, + ?array $options, StatementsAnalyzer $statements_analyzer, CodeLocation $code_location, Codebase $codebase, @@ -515,7 +518,7 @@ public static function getOptions( } $all_filters = self::getFilters($codebase); - foreach ($options->properties as $option => $option_value) { + foreach ($options as $option => $option_value) { if (! isset($all_filters[ $filter_int_used ]['options'][ $option ])) { IssueBuffer::maybeAdd( new RedundantFlag( diff --git a/src/Psalm/Type.php b/src/Psalm/Type.php index 8d2393d0dc7..348b2824f2c 100644 --- a/src/Psalm/Type.php +++ b/src/Psalm/Type.php @@ -71,6 +71,26 @@ abstract class Type { + /** + * @internal + * @return array + */ + public static function mergeKeyedArrayProperties(Union $union, Codebase $codebase): array + { + $properties = []; + foreach ($union->getArrays() as $atomic_type) { + if (!$atomic_type instanceof TKeyedArray) { + continue; + } + foreach ($atomic_type->properties as $key => $val) { + if (isset($properties[$key])) { + $val = self::combineUnionTypes($properties[$key], $val, $codebase); + } + $properties[$key] = $val; + } + } + return $properties; + } /** * Parses a string type representation * diff --git a/src/Psalm/Type/UnionTrait.php b/src/Psalm/Type/UnionTrait.php index 32bcd80f476..8a1fdda2483 100644 --- a/src/Psalm/Type/UnionTrait.php +++ b/src/Psalm/Type/UnionTrait.php @@ -7,7 +7,6 @@ use InvalidArgumentException; use Psalm\CodeLocation; use Psalm\Codebase; -use Psalm\Internal\Type\TypeCombiner; use Psalm\Internal\TypeVisitor\CanContainObjectTypeVisitor; use Psalm\Internal\TypeVisitor\ClasslikeReplacer; use Psalm\Internal\TypeVisitor\ContainsClassLikeVisitor; @@ -17,6 +16,7 @@ use Psalm\Internal\TypeVisitor\TypeScanner; use Psalm\StatementsSource; use Psalm\Storage\FileStorage; +use Psalm\Type; use Psalm\Type\Atomic\ArrayInterface; use Psalm\Type\Atomic\TCallable; use Psalm\Type\Atomic\TClassString; @@ -412,7 +412,7 @@ public function hasArray(): bool } /** - * @return list + * @return list */ public function getArrays(): array { @@ -456,12 +456,12 @@ public function getArrayValueTypes(): array public function getArrayKeyType(Codebase $codebase): Union { - return TypeCombiner::combine($this->getArrayKeyTypes(), $codebase); + return Type::combineUnionTypeArray($this->getArrayKeyTypes(), $codebase); } public function getArrayValueType(Codebase $codebase): Union { - return TypeCombiner::combine($this->getArrayValueTypes(), $codebase); + return Type::combineUnionTypeArray($this->getArrayValueTypes(), $codebase); } /**