From c01030c841a6d36e2dc4c430d863cce1dc8788c8 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Tue, 18 Feb 2025 21:33:10 +0100 Subject: [PATCH 01/17] Fix assertions with empty lists --- psalm-baseline.xml | 13 +++- .../Statements/Expression/ArrayAnalyzer.php | 2 +- .../Assignment/ArrayAssignmentAnalyzer.php | 6 +- .../BinaryOp/ArithmeticOpAnalyzer.php | 2 +- .../Call/ArrayFunctionArgumentsAnalyzer.php | 2 +- .../Call/FunctionCallReturnTypeFetcher.php | 4 +- .../Statements/Expression/CastAnalyzer.php | 4 +- .../Expression/Fetch/ArrayFetchAnalyzer.php | 6 +- .../Fetch/VariableFetchAnalyzer.php | 4 +- .../Expression/SimpleTypeInferer.php | 2 +- .../Analyzer/Statements/UnsetAnalyzer.php | 4 +- .../Codebase/ConstantTypeResolver.php | 6 +- src/Psalm/Internal/Codebase/Methods.php | 2 +- .../ArrayColumnReturnTypeProvider.php | 2 +- .../ArrayCombineReturnTypeProvider.php | 2 +- .../ArrayFillKeysReturnTypeProvider.php | 2 +- .../ArrayFillReturnTypeProvider.php | 2 +- .../ArrayFilterReturnTypeProvider.php | 2 +- .../ArrayMapReturnTypeProvider.php | 6 +- .../ArrayMergeReturnTypeProvider.php | 2 +- .../ArrayReverseReturnTypeProvider.php | 2 +- .../ReturnTypeProvider/FilterUtils.php | 4 +- .../GetObjectVarsReturnTypeProvider.php | 8 +-- .../ImagickPixelColorReturnTypeProvider.php | 6 +- .../ParseUrlReturnTypeProvider.php | 2 +- .../Internal/Type/AssertionReconciler.php | 6 +- .../Type/Comparator/ArrayTypeComparator.php | 3 +- .../Type/SimpleAssertionReconciler.php | 4 +- .../Type/SimpleNegatedAssertionReconciler.php | 2 +- src/Psalm/Internal/Type/TypeCombiner.php | 8 +-- src/Psalm/Internal/Type/TypeExpander.php | 4 +- src/Psalm/Internal/Type/TypeParser.php | 2 +- src/Psalm/Type.php | 8 +-- src/Psalm/Type/Atomic/TKeyedArray.php | 62 +++++++++++++------ src/Psalm/Type/Reconciler.php | 4 +- tests/ArrayAccessTest.php | 15 +++++ 36 files changed, 133 insertions(+), 82 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 14ee0eaec99..1b5640dd16d 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + tags['variablesfrom'][0]]]> @@ -3006,6 +3006,17 @@ + + + + + as_type]]> diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php index 0afd08d1eab..cd601393360 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php @@ -119,7 +119,7 @@ public static function analyze( // if this array looks like an object-like array, let's return that instead if (count($array_creation_info->property_types) !== 0) { - $atomic_type = new TKeyedArray( + $atomic_type = TKeyedArray::make( $array_creation_info->property_types, $array_creation_info->class_strings, $array_creation_info->can_create_objectlike diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php index ec07e8c0715..a9904ea32f8 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php @@ -357,7 +357,7 @@ private static function updateTypeWithKeyValues( $classStrings[$key_value->value] = true; } } - $object_like = new TKeyedArray( + $object_like = TKeyedArray::make( $properties, $classStrings ?: null, ); @@ -609,7 +609,7 @@ private static function updateArrayAssignmentChildType( ); } elseif ($prop_count !== null) { assert($array_atomic_type_list !== null); - $array_atomic_type = new TKeyedArray( + $array_atomic_type = TKeyedArray::make( array_fill( 0, $prop_count, @@ -644,7 +644,7 @@ private static function updateArrayAssignmentChildType( $array_atomic_type_list, ); assert(count($array_atomic_type) > 0); - $array_atomic_type = new TKeyedArray( + $array_atomic_type = TKeyedArray::make( $array_atomic_type, null, null, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php index 4d87d3749d3..f1b3df52037 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php @@ -593,7 +593,7 @@ private static function analyzeOperands( $fallback_params = $left_type_part->fallback_params ?: $right_type_part->fallback_params; } - $new_keyed_array = new TKeyedArray( + $new_keyed_array = TKeyedArray::make( $properties, null, $fallback_params, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php index 687b9e47db8..2e62950919d 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php @@ -291,7 +291,7 @@ public static function handleAddition( $by_ref_type = new Union([$objectlike_list->setProperties($properties)]); } elseif ($array_type instanceof TArray && $array_type->isEmptyArray()) { - $by_ref_type = new Union([new TKeyedArray([ + $by_ref_type = new Union([TKeyedArray::make([ $arg_value_type, ], null, null, true)]); } else { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php index 9aec38e44a9..17099aff13f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php @@ -328,7 +328,7 @@ private static function getReturnTypeFromCallMapWithArgs( if (!$call_args) { switch ($call_map_key) { case 'hrtime': - $keyed_array = new TKeyedArray([ + $keyed_array = TKeyedArray::make([ Type::getInt(), Type::getInt(), ], null, null, true); @@ -407,7 +407,7 @@ private static function getReturnTypeFromCallMapWithArgs( return Type::getInt(true); } - $keyed_array = new TKeyedArray([ + $keyed_array = TKeyedArray::make([ Type::getInt(), Type::getInt(), ], null, null, true); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php index dce9bd1e3a0..6a0c43b056f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php @@ -247,7 +247,7 @@ public static function analyze( foreach ($stmt_expr_type->getAtomicTypes() as $type) { if ($type instanceof Scalar) { - $keyed_array = new TKeyedArray([new Union([$type])], null, null, true); + $keyed_array = TKeyedArray::make([new Union([$type])], null, null, true); $permissible_atomic_types[] = $keyed_array; } elseif ($type instanceof TNull) { $permissible_atomic_types[] = new TArray([Type::getNever(), Type::getNever()]); @@ -258,7 +258,7 @@ public static function analyze( } elseif ($type instanceof TObjectWithProperties) { $array_type = $type->properties === [] ? Type::getArrayAtomic() - : new TKeyedArray( + : TKeyedArray::make( $type->properties, null, [Type::getArrayKey(), Type::getMixed()], diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php index 4186103eecd..63987b157b4 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php @@ -1169,7 +1169,7 @@ private static function handleArrayAccessOnArray( $from_mixed_array = $type->type_params[1]->isMixed(); // ok, type becomes a TKeyedArray - $type = new TKeyedArray( + $type = TKeyedArray::make( [ $single_atomic->value => $from_mixed_array ? Type::getMixed() : Type::getNever(), ], @@ -1179,7 +1179,7 @@ private static function handleArrayAccessOnArray( $from_empty_array ? null : $type->type_params, ); } elseif (!$stmt->dim && $from_empty_array && $replacement_type) { - $type = new TKeyedArray( + $type = TKeyedArray::make( [$replacement_type], null, null, @@ -1730,7 +1730,7 @@ private static function handleArrayAccessOnKeyedArray( if (!$stmt->dim) { if ($type->is_list) { - $type = new TKeyedArray( + $type = TKeyedArray::make( $type->properties, null, [$new_key_type, $generic_params], diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php index 53a6b6542e9..aed43d3c409 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php @@ -791,7 +791,7 @@ private static function getGlobalTypeInner(string $var_id, bool $files_full_path $arr['argc'] = $argc_helper; } - $detailed_type = new TKeyedArray( + $detailed_type = TKeyedArray::make( $arr, null, [Type::getNonEmptyString(), Type::getString()], @@ -814,7 +814,7 @@ private static function getGlobalTypeInner(string $var_id, bool $files_full_path $values['full_path'] = $str; } - $type = new Union([new TKeyedArray($values)]); + $type = new Union([TKeyedArray::make($values)]); $parent = new TArray([Type::getNonEmptyString(), $type]); return new Union([$parent]); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php index bc2875f7a56..3aa70101ad2 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php @@ -590,7 +590,7 @@ private static function inferArrayType( && $array_creation_info->can_create_objectlike && $array_creation_info->property_types ) { - $objectlike = new TKeyedArray( + $objectlike = TKeyedArray::make( $array_creation_info->property_types, $array_creation_info->class_strings, null, diff --git a/src/Psalm/Internal/Analyzer/Statements/UnsetAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/UnsetAnalyzer.php index 51df9a31c27..570fe78ac53 100644 --- a/src/Psalm/Internal/Analyzer/Statements/UnsetAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/UnsetAnalyzer.php @@ -120,7 +120,7 @@ public static function analyze( ; } } else { - $root_types []= new TKeyedArray( + $root_types []= TKeyedArray::make( $properties, null, $atomic_root_type->fallback_params ? [ @@ -135,7 +135,7 @@ public static function analyze( foreach ($atomic_root_type->properties as $key => $type) { $properties[$key] = $type->setPossiblyUndefined(true); } - $root_types []= new TKeyedArray( + $root_types []= TKeyedArray::make( $properties, null, $atomic_root_type->fallback_params, diff --git a/src/Psalm/Internal/Codebase/ConstantTypeResolver.php b/src/Psalm/Internal/Codebase/ConstantTypeResolver.php index 84930c32e4c..11b50a779ec 100644 --- a/src/Psalm/Internal/Codebase/ConstantTypeResolver.php +++ b/src/Psalm/Internal/Codebase/ConstantTypeResolver.php @@ -145,7 +145,7 @@ public static function resolve( } if ($left instanceof TKeyedArray && $right instanceof TKeyedArray) { - $type = new TKeyedArray( + $type = TKeyedArray::make( $left->properties + $right->properties, null, ); @@ -268,7 +268,7 @@ public static function resolve( if (empty($properties)) { $resolved_type = Type::getEmptyArrayAtomic(); } else { - $resolved_type = new TKeyedArray($properties, null, null, $is_list); + $resolved_type = TKeyedArray::make($properties, null, null, $is_list); } return $resolved_type; @@ -381,7 +381,7 @@ public static function getLiteralTypeFromScalarValue(array|string|int|float|bool foreach ($value as $key => $val) { $types[$key] = new Union([self::getLiteralTypeFromScalarValue($val)]); } - return new TKeyedArray($types, null); + return TKeyedArray::make($types, null); } if (is_string($value)) { diff --git a/src/Psalm/Internal/Codebase/Methods.php b/src/Psalm/Internal/Codebase/Methods.php index f4c8f159f9c..d3d0e50c96c 100644 --- a/src/Psalm/Internal/Codebase/Methods.php +++ b/src/Psalm/Internal/Codebase/Methods.php @@ -608,7 +608,7 @@ public function getMethodReturnType( $types[] = new Union([new TEnumCase($original_fq_class_name, $case_name)]); } - $list = new TKeyedArray($types, null, null, true); + $list = TKeyedArray::make($types, null, null, true); return new Union([$list]); } } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayColumnReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayColumnReturnTypeProvider.php index 55e77ad506e..b4e33a9e8b7 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayColumnReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayColumnReturnTypeProvider.php @@ -172,7 +172,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev if (!$properties) { return Type::getEmptyArray(); } - return new Union([new TKeyedArray( + return new Union([TKeyedArray::make( $properties, null, $input_array->fallback_params, diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayCombineReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayCombineReturnTypeProvider.php index a54446ad1e3..07eea161291 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayCombineReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayCombineReturnTypeProvider.php @@ -129,6 +129,6 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev return Type::getEmptyArray(); } - return new Union([new TKeyedArray($result, null, null, $is_list)]); + return new Union([TKeyedArray::make($result, null, null, $is_list)]); } } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFillKeysReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFillKeysReturnTypeProvider.php index 67d2c4e5ca8..1e26383f38a 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFillKeysReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFillKeysReturnTypeProvider.php @@ -82,7 +82,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev $key_k->possibly_undefined, ); } - return new Union([new TKeyedArray( + return new Union([TKeyedArray::make( $result, null, null, diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFillReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFillReturnTypeProvider.php index 46dd6ce26d0..1fc698b56f0 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFillReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFillReturnTypeProvider.php @@ -70,7 +70,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev if (!$result) { return Type::getEmptyArray(); } - return new Union([new TKeyedArray( + return new Union([TKeyedArray::make( $result, null, null, diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php index 6c66f72b02c..54cf54c52bd 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php @@ -118,7 +118,7 @@ static function ($keyed_type) use ($statements_source, $context) { return Type::getEmptyArray(); } - return new Union([new TKeyedArray( + return new Union([TKeyedArray::make( $new_properties, null, $first_arg_array->fallback_params, diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php index 13d3ac25bee..179d8b6e2a5 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php @@ -123,7 +123,7 @@ static function (array $sub) use ($null) { static fn(?Union $t) => $t ?? $null, $sub, ); - return new Union([new TKeyedArray($sub, null, null, true)]); + return new Union([TKeyedArray::make($sub, null, null, true)]); }, $array_arg_types, ); @@ -132,7 +132,7 @@ static function (array $sub) use ($null) { return Type::getEmptyArray(); } - return new Union([new TKeyedArray($array_arg_types, null, null, true)]); + return new Union([TKeyedArray::make($array_arg_types, null, null, true)]); } $array_arg = $call_args[1] ?? null; @@ -234,7 +234,7 @@ static function (array $sub) use ($null) { if ($mapping_return_type && $generic_key_type) { if ($array_arg_atomic_type instanceof TKeyedArray && count($call_args) === 2) { - $atomic_type = new TKeyedArray( + $atomic_type = TKeyedArray::make( array_map( static fn(Union $in): Union => $mapping_return_type->setPossiblyUndefined( $in->possibly_undefined, diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php index b2a8f000bc3..429ff44adcd 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php @@ -263,7 +263,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev && ($generic_property_count < $max_keyed_array_size * 2 || $generic_property_count < 16) ) { - $objectlike = new TKeyedArray( + $objectlike = TKeyedArray::make( $generic_properties, $class_strings ?: null, $all_keyed_arrays || $inner_key_type === null || $inner_value_type === null diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReverseReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReverseReturnTypeProvider.php index 1ea31c98641..4981fc7bc76 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReverseReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReverseReturnTypeProvider.php @@ -75,7 +75,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev : new Union([$first_arg_array->setProperties(array_reverse($first_arg_array->properties))]); } - return new Union([new TKeyedArray( + return new Union([TKeyedArray::make( $first_arg_array->properties, null, $first_arg_array->fallback_params, diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php index 6b52279defa..6a8bb8ecb9b 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php @@ -726,7 +726,7 @@ public static function getReturnType( $fallback_params = [$keys_union, $values_union]; } - $from_array[] = new TKeyedArray( + $from_array[] = TKeyedArray::make( $new, $atomic_type->class_strings, $fallback_params, @@ -1430,7 +1430,7 @@ public static function getReturnType( if (!$in_array_recursion && !self::hasFlag($flags_int_used, FILTER_REQUIRE_ARRAY) && self::hasFlag($flags_int_used, FILTER_FORCE_ARRAY)) { - $return_type = new Union([new TKeyedArray( + $return_type = new Union([TKeyedArray::make( [$return_type], null, null, diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/GetObjectVarsReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/GetObjectVarsReturnTypeProvider.php index 02bc2ae6a5e..e5b8170a5ef 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/GetObjectVarsReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/GetObjectVarsReturnTypeProvider.php @@ -60,7 +60,7 @@ public static function getGetObjectVarsReturnType( $codebase = $statements_source->getCodebase(); $enum_classlike_storage = $codebase->classlike_storage_provider->get($object_type->value); if ($enum_classlike_storage->enum_type === null) { - return new TKeyedArray($properties); + return TKeyedArray::make($properties); } $enum_case_storage = $enum_classlike_storage->enum_cases[$object_type->case_name]; $case_value = $enum_case_storage->getValue($statements_source->getCodebase()->classlikes); @@ -69,14 +69,14 @@ public static function getGetObjectVarsReturnType( $properties['value'] = new Union([$case_value]); } - return new TKeyedArray($properties); + return TKeyedArray::make($properties); } if ($object_type instanceof TObjectWithProperties) { if ([] === $object_type->properties) { return self::$fallback; } - return new TKeyedArray($object_type->properties); + return TKeyedArray::make($object_type->properties); } if ($object_type instanceof TNamedObject) { @@ -140,7 +140,7 @@ public static function getGetObjectVarsReturnType( return self::$fallback; } - return new TKeyedArray( + return TKeyedArray::make( $properties, null, $class_storage->final diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ImagickPixelColorReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ImagickPixelColorReturnTypeProvider.php index 18c80c92d25..0f841e4ece2 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ImagickPixelColorReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ImagickPixelColorReturnTypeProvider.php @@ -60,7 +60,7 @@ public static function getMethodReturnType(MethodReturnTypeProviderEvent $event) $types = []; if (isset($formats[0])) { $types []= new Union([ - new TKeyedArray([ + TKeyedArray::make([ 'r' => Type::getIntRange(0, 255), 'g' => Type::getIntRange(0, 255), 'b' => Type::getIntRange(0, 255), @@ -70,7 +70,7 @@ public static function getMethodReturnType(MethodReturnTypeProviderEvent $event) } if (isset($formats[1])) { $types []= new Union([ - new TKeyedArray([ + TKeyedArray::make([ 'r' => Type::getFloat(), 'g' => Type::getFloat(), 'b' => Type::getFloat(), @@ -80,7 +80,7 @@ public static function getMethodReturnType(MethodReturnTypeProviderEvent $event) } if (isset($formats[2])) { $types []= new Union([ - new TKeyedArray([ + TKeyedArray::make([ 'r' => Type::getIntRange(0, 255), 'g' => Type::getIntRange(0, 255), 'b' => Type::getIntRange(0, 255), diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ParseUrlReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ParseUrlReturnTypeProvider.php index 4b4f461c55f..7f036484b3b 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ParseUrlReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ParseUrlReturnTypeProvider.php @@ -151,7 +151,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev $component_types['port'] = new Union([new TInt()], ['possibly_undefined' => true]); self::$return_type = new Union([ - new TKeyedArray( + TKeyedArray::make( $component_types, null, ), diff --git a/src/Psalm/Internal/Type/AssertionReconciler.php b/src/Psalm/Internal/Type/AssertionReconciler.php index d92fce94df3..49ac8211b17 100644 --- a/src/Psalm/Internal/Type/AssertionReconciler.php +++ b/src/Psalm/Internal/Type/AssertionReconciler.php @@ -639,7 +639,7 @@ private static function filterAtomicWithAnother( return null; } - return new TKeyedArray( + return TKeyedArray::make( $type_2_atomic->properties, null, [Type::getInt(), $type_2_value], @@ -665,7 +665,7 @@ private static function filterAtomicWithAnother( return null; } - return new TKeyedArray( + return TKeyedArray::make( $type_1_atomic->properties, null, [Type::getInt(), $type_1_value], @@ -784,7 +784,7 @@ private static function filterAtomicWithAnother( $fallback_types = [$type_1_atomic->fallback_params[0], $type_2_param]; } - $matching_atomic_type = new TKeyedArray( + $matching_atomic_type = TKeyedArray::make( $type_1_properties, $type_1_atomic->class_strings, $fallback_types, diff --git a/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php b/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php index 8a39b72c976..539cc6ee61f 100644 --- a/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php @@ -69,7 +69,8 @@ public static function isContainedBy( } if ($all_string_int_literals && $properties) { - $input_type_part = new TKeyedArray($properties); + $input_type_part = TKeyedArray::make($properties); + assert($input_type_part instanceof TKeyedArray); return KeyedArrayComparator::isContainedBy( $codebase, diff --git a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php index 3dcf80cce93..5c943a9a0c6 100644 --- a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php @@ -828,7 +828,7 @@ private static function reconcileExactlyCountable( ? $properties[$x]->setPossiblyUndefined(false) : $array_atomic_type->fallback_params[1]; } - $array_atomic_type = new TKeyedArray( + $array_atomic_type = TKeyedArray::make( $properties, null, null, @@ -1929,7 +1929,7 @@ private static function reconcileHasArrayKey( assert(strpos($assertion, '::class') === (strlen($assertion)-7)); [$assertion] = explode('::', $assertion); - $atomic_type = new TKeyedArray( + $atomic_type = TKeyedArray::make( array_merge( $atomic_type->properties, [$assertion => Type::getMixed()], diff --git a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php index f030cf4e39d..1c5fc23eed2 100644 --- a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php @@ -600,7 +600,7 @@ private static function reconcileNotNonEmptyCountable( if (!$properties) { $existing_var_type->addType(Type::getEmptyArrayAtomic()); } else { - $existing_var_type->addType(new TKeyedArray( + $existing_var_type->addType(TKeyedArray::make( $properties, null, null, diff --git a/src/Psalm/Internal/Type/TypeCombiner.php b/src/Psalm/Internal/Type/TypeCombiner.php index a14109f0ee5..c55350dbc33 100644 --- a/src/Psalm/Internal/Type/TypeCombiner.php +++ b/src/Psalm/Internal/Type/TypeCombiner.php @@ -1497,7 +1497,7 @@ private static function handleKeyedArrayEntries( $from_docblock, ); } else { - $objectlike = new TKeyedArray( + $objectlike = TKeyedArray::make( $combination->objectlike_entries, array_filter($combination->objectlike_class_string_keys), $sealed || $fallback_key_type === null || $fallback_value_type === null @@ -1619,7 +1619,7 @@ private static function getArrayTypeFromGenericParams( && $combination->objectlike_sealed && isset($combination->array_type_params[1]) ) { - $array_type = new TKeyedArray( + $array_type = TKeyedArray::make( [$generic_type_params[1]], null, [Type::getInt(), $combination->array_type_params[1]], @@ -1632,7 +1632,7 @@ private static function getArrayTypeFromGenericParams( $properties []= $generic_type_params[1]; } assert($properties !== []); - $array_type = new TKeyedArray( + $array_type = TKeyedArray::make( $properties, null, null, @@ -1649,7 +1649,7 @@ private static function getArrayTypeFromGenericParams( if (!$properties) { $properties []= $generic_type_params[1]->setPossiblyUndefined(true); } - $array_type = new TKeyedArray( + $array_type = TKeyedArray::make( $properties, null, [Type::getListKey(), $generic_type_params[1]], diff --git a/src/Psalm/Internal/Type/TypeExpander.php b/src/Psalm/Internal/Type/TypeExpander.php index bee84a01078..c82dd62debc 100644 --- a/src/Psalm/Internal/Type/TypeExpander.php +++ b/src/Psalm/Internal/Type/TypeExpander.php @@ -497,7 +497,7 @@ public static function expandAtomic( unset($property_type); } if ($changed) { - $return_type = new TKeyedArray( + $return_type = TKeyedArray::make( $properties, $return_type->class_strings, $fallback_params, @@ -987,7 +987,7 @@ private static function expandPropertiesOf( if ($properties === []) { return [$return_type]; } - return [new TKeyedArray( + return [TKeyedArray::make( $properties, null, $all_sealed ? null : [Type::getString(), Type::getMixed()], diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index 6560aeb8709..b7d4b294c6e 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -1787,7 +1787,7 @@ private static function getTypeFromKeyedArrays( $fallback_params = [Type::getArrayKey(), Type::getMixed()]; } - return new TKeyedArray( + return TKeyedArray::make( $properties, null, $fallback_params, diff --git a/src/Psalm/Type.php b/src/Psalm/Type.php index cd9ca3473bf..96d5e4526ba 100644 --- a/src/Psalm/Type.php +++ b/src/Psalm/Type.php @@ -500,9 +500,9 @@ public static function getNonEmptyList(?Union $of = null, bool $from_docblock = /** * @psalm-pure */ - public static function getListAtomic(Union $of, bool $from_docblock = false): TKeyedArray + public static function getListAtomic(Union $of, bool $from_docblock = false): TKeyedArray|TArray { - return new TKeyedArray( + return TKeyedArray::make( [$of->setPossiblyUndefined(true)], null, [self::getListKey(), $of], @@ -514,9 +514,9 @@ public static function getListAtomic(Union $of, bool $from_docblock = false): TK /** * @psalm-pure */ - public static function getNonEmptyListAtomic(Union $of, bool $from_docblock = false): TKeyedArray + public static function getNonEmptyListAtomic(Union $of, bool $from_docblock = false): TKeyedArray|TArray { - return new TKeyedArray( + return TKeyedArray::make( [$of->setPossiblyUndefined(false)], null, [self::getListKey(), $of], diff --git a/src/Psalm/Type/Atomic/TKeyedArray.php b/src/Psalm/Type/Atomic/TKeyedArray.php index 97e2d01660c..7e66802e992 100644 --- a/src/Psalm/Type/Atomic/TKeyedArray.php +++ b/src/Psalm/Type/Atomic/TKeyedArray.php @@ -35,17 +35,6 @@ class TKeyedArray extends Atomic { use UnserializeMemoryUsageSuppressionTrait; - /** - * If the shape has fallback params then they are here - * - * @var array{Union, Union}|null - */ - public ?array $fallback_params = null; - - /** - * @var bool - if this is a list of sequential elements - */ - public bool $is_list = false; /** @var non-empty-lowercase-string */ protected const NAME_ARRAY = 'array'; @@ -54,6 +43,8 @@ class TKeyedArray extends Atomic /** * Constructs a new instance of a generic type + * + * @deprecated Please use make() * * @param non-empty-array $properties * @param array{Union, Union}|null $fallback_params @@ -62,22 +53,54 @@ class TKeyedArray extends Atomic public function __construct( public array $properties, public ?array $class_strings = null, + /** + * If the shape has fallback params then they are here + * + * @var array{Union, Union}|null + */ + public ?array $fallback_params = null, + /** + * @var bool - if this is a list of sequential elements + */ + public bool $is_list = false, + bool $from_docblock = false, + ) { + parent::__construct($from_docblock); + } + + /** + * @psalm-pure + * + * @param non-empty-array $properties + * @param array{Union, Union}|null $fallback_params + * @param array $class_strings + */ + public static function make( + array $properties, + ?array $class_strings = null, ?array $fallback_params = null, bool $is_list = false, bool $from_docblock = false, - ) { + ): self|TArray { if ($is_list && $fallback_params) { $fallback_params[0] = Type::getListKey(); } - $this->fallback_params = $fallback_params; - $this->is_list = $is_list; - if ($this->is_list) { + if (count($properties) === 1 + && $properties[array_key_first($properties)]->isNever() + && ($fallback_params === null || $fallback_params[1]->isNever()) + ) { + $never = $properties[array_key_first($properties)]; + return new TArray([ + $never, $never + ], $from_docblock); + } + if ($is_list) { $last_k = -1; $had_possibly_undefined = false; - ksort($this->properties); - foreach ($this->properties as $k => $v) { + ksort($properties); + foreach ($properties as $k => $v) { if (is_string($k) || $last_k !== ($k-1) || ($had_possibly_undefined && !$v->possibly_undefined)) { - $this->is_list = false; + $is_list = false; break; } if ($v->possibly_undefined) { @@ -86,7 +109,8 @@ public function __construct( $last_k = $k; } } - parent::__construct($from_docblock); + + return new self($properties, $class_strings, $fallback_params, $is_list, $from_docblock); } /** diff --git a/src/Psalm/Type/Reconciler.php b/src/Psalm/Type/Reconciler.php index a8ebe34c543..a1099ea9799 100644 --- a/src/Psalm/Type/Reconciler.php +++ b/src/Psalm/Type/Reconciler.php @@ -1177,7 +1177,7 @@ private static function adjustTKeyedArrayType( $fallback_key_type = $base_atomic_type->type_params[0]; $fallback_value_type = $base_atomic_type->type_params[1]; - $base_atomic_type = new TKeyedArray( + $base_atomic_type = TKeyedArray::make( [ $array_key_offset => $result_type, ], @@ -1208,7 +1208,7 @@ private static function adjustTKeyedArrayType( $base_atomic_type = $base_atomic_type->setProperties($properties); } else { // This should actually be a paradox - $base_atomic_type = new TKeyedArray( + $base_atomic_type = TKeyedArray::make( $properties, null, $base_atomic_type->fallback_params, diff --git a/tests/ArrayAccessTest.php b/tests/ArrayAccessTest.php index 281321261ce..b0a983a0131 100644 --- a/tests/ArrayAccessTest.php +++ b/tests/ArrayAccessTest.php @@ -453,6 +453,21 @@ function foo(array $a, int $b): void { public function providerValidCodeParse(): iterable { return [ + 'allowEmptyList' => [ + 'code' => ' 1 + || empty($a[array_key_first($a)]) + ) { + } + }', + ], 'testBuildList' => [ 'code' => ' Date: Tue, 18 Feb 2025 21:41:02 +0100 Subject: [PATCH 02/17] Fixes --- tests/ArgTest.php | 2 +- tests/ArrayFunctionCallTest.php | 8 ++++---- tests/ArrayKeysTest.php | 2 +- tests/CoreStubsTest.php | 16 ++++++++-------- tests/FunctionCallTest.php | 4 ++-- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/ArgTest.php b/tests/ArgTest.php index df63d74a350..6b3dd13d75f 100644 --- a/tests/ArgTest.php +++ b/tests/ArgTest.php @@ -125,7 +125,7 @@ class Hello {} 'assertions' => [ '$a' => 'array{a: int, b: int}', '$b' => 'non-empty-list', - '$c' => 'list', + '$c' => 'array', ], ], 'arrayModificationFunctions' => [ diff --git a/tests/ArrayFunctionCallTest.php b/tests/ArrayFunctionCallTest.php index 90a0d4b4460..3a327336810 100644 --- a/tests/ArrayFunctionCallTest.php +++ b/tests/ArrayFunctionCallTest.php @@ -2226,7 +2226,7 @@ function getCharPairs(string $line) : array { shuffle($emptyArray);', 'assertions' => [ '$array' => 'non-empty-list', - '$emptyArray' => 'list', + '$emptyArray' => 'array', ], ], 'sort' => [ @@ -2237,7 +2237,7 @@ function getCharPairs(string $line) : array { sort($emptyArray);', 'assertions' => [ '$array' => 'non-empty-list', - '$emptyArray' => 'list', + '$emptyArray' => 'array', ], ], 'rsort' => [ @@ -2248,7 +2248,7 @@ function getCharPairs(string $line) : array { rsort($emptyArray);', 'assertions' => [ '$array' => 'non-empty-list', - '$emptyArray' => 'list', + '$emptyArray' => 'array', ], ], 'usort' => [ @@ -2260,7 +2260,7 @@ function baz (int $a, int $b): int { return $a <=> $b; } usort($emptyArray, "baz");', 'assertions' => [ '$array' => 'non-empty-list', - '$emptyArray' => 'list', + '$emptyArray' => 'array', ], ], 'closureParamConstraintsMet' => [ diff --git a/tests/ArrayKeysTest.php b/tests/ArrayKeysTest.php index c6ea522a53c..1ad87ef00d3 100644 --- a/tests/ArrayKeysTest.php +++ b/tests/ArrayKeysTest.php @@ -22,7 +22,7 @@ public function providerValidCodeParse(): iterable $keys = array_keys([]); ', 'assertions' => [ - '$keys' => 'list', + '$keys' => 'array', ], ], 'arrayKeysOfKeyedArrayReturnsNonEmptyListOfStrings' => [ diff --git a/tests/CoreStubsTest.php b/tests/CoreStubsTest.php index 03c68263b49..282510be765 100644 --- a/tests/CoreStubsTest.php +++ b/tests/CoreStubsTest.php @@ -366,19 +366,19 @@ function after_str_ends_with() $stringPatternMaybeWithNocheckFlagAndMaybeOnlydir = glob( $string , $maybeNocheckFlag | $maybeOnlydirFlag); PHP, 'assertions' => [ - '$emptyPatternNoFlags===' => 'false|list', - '$emptyPatternWithoutNocheckFlag1===' => 'false|list', - '$emptyPatternWithoutNocheckFlag2===' => 'false|list', - '$emptyPatternWithoutNocheckFlag3===' => 'false|list', + '$emptyPatternNoFlags===' => 'false|array', + '$emptyPatternWithoutNocheckFlag1===' => 'false|array', + '$emptyPatternWithoutNocheckFlag2===' => 'false|array', + '$emptyPatternWithoutNocheckFlag3===' => 'false|array', '$emptyPatternWithNocheckFlag1===' => 'false|list{\'\'}', '$emptyPatternWithNocheckFlag2===' => 'false|list{\'\'}', '$emptyPatternWithNocheckFlag3===' => 'false|list{\'\'}', - '$emptyPatternWithNocheckAndOnlydirFlag1===' => 'false|list', - '$emptyPatternWithNocheckAndOnlydirFlag2===' => 'false|list', - '$emptyPatternWithNocheckAndOnlydirFlag3===' => 'false|list', + '$emptyPatternWithNocheckAndOnlydirFlag1===' => 'false|array', + '$emptyPatternWithNocheckAndOnlydirFlag2===' => 'false|array', + '$emptyPatternWithNocheckAndOnlydirFlag3===' => 'false|array', '$emptyPatternWithNocheckFlagAndMaybeOnlydir===' => 'false|list{0?: \'\', ...}', '$emptyPatternMaybeWithNocheckFlag===' => 'false|list{0?: \'\', ...}', - '$emptyPatternMaybeWithNocheckFlagAndOnlydir===' => 'false|list', + '$emptyPatternMaybeWithNocheckFlagAndOnlydir===' => 'false|array', '$emptyPatternMaybeWithNocheckFlagAndMaybeOnlydir===' => 'false|list{0?: \'\', ...}', '$nonEmptyPatternNoFlags===' => 'false|list', diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index 49a52d43b26..43890c3db2a 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -687,7 +687,7 @@ public static function Baz($mixed) : string { /** @var string $string */ $elements = explode(" ", $string, -5);', 'assertions' => [ - '$elements' => 'list', + '$elements' => 'array', ], ], 'explodeWithDynamicLimit' => [ @@ -742,7 +742,7 @@ public static function Baz($mixed) : string { */ $elements = explode($delim, $string, -5);', 'assertions' => [ - '$elements' => 'list', + '$elements' => 'array', ], ], 'explodeWithDynamicDelimiterAndLimit' => [ From 006c6f92841bd3768b811e00f6f57b86323c36d4 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Sat, 1 Mar 2025 15:20:55 +0100 Subject: [PATCH 03/17] Completely remove TCallableKeyedArray --- .../Expression/Call/ArgumentsAnalyzer.php | 5 +- .../Call/FunctionCallReturnTypeFetcher.php | 8 +-- .../Call/HighOrderFunctionArgHandler.php | 2 +- src/Psalm/Internal/PreloaderList.php | 1 - .../Type/Comparator/ArrayTypeComparator.php | 2 + .../Type/Comparator/AtomicTypeComparator.php | 3 +- .../Type/SimpleAssertionReconciler.php | 9 ++- .../Type/SimpleNegatedAssertionReconciler.php | 5 +- src/Psalm/Internal/Type/TypeCombiner.php | 11 ++-- src/Psalm/Internal/Type/TypeParser.php | 37 ++++++----- src/Psalm/Type/Atomic.php | 5 +- src/Psalm/Type/Atomic/TCallableKeyedArray.php | 40 ------------ src/Psalm/Type/Atomic/TKeyedArray.php | 61 ++++++++++++++----- 13 files changed, 90 insertions(+), 99 deletions(-) delete mode 100644 src/Psalm/Type/Atomic/TCallableKeyedArray.php diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index 98a8b1362b5..986928f7793 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -43,7 +43,6 @@ use Psalm\Type; use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TCallable; -use Psalm\Type\Atomic\TCallableKeyedArray; use Psalm\Type\Atomic\TClosure; use Psalm\Type\Atomic\TKeyedArray; use Psalm\Type\Atomic\TLiteralString; @@ -1645,9 +1644,7 @@ private static function checkArgCount( foreach ($arg_value_type->getAtomicTypes() as $atomic_arg_type) { $packed_var_definite_args_tmp = []; - if ($atomic_arg_type instanceof TCallableKeyedArray) { - $packed_var_definite_args_tmp[] = 2; - } elseif ($atomic_arg_type instanceof TKeyedArray) { + if ($atomic_arg_type instanceof TKeyedArray) { if ($atomic_arg_type->fallback_params !== null) { return; } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php index 17099aff13f..f21e32154c5 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php @@ -27,7 +27,6 @@ use Psalm\Type; use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TCallable; -use Psalm\Type\Atomic\TCallableKeyedArray; use Psalm\Type\Atomic\TClassString; use Psalm\Type\Atomic\TClosure; use Psalm\Type\Atomic\TFalse; @@ -364,10 +363,6 @@ private static function getReturnTypeFromCallMapWithArgs( if (count($atomic_types) === 1) { if (isset($atomic_types['array'])) { - if ($atomic_types['array'] instanceof TCallableKeyedArray) { - return Type::getInt(false, 2); - } - if ($atomic_types['array'] instanceof TNonEmptyArray) { return new Union([ $atomic_types['array']->count !== null @@ -377,6 +372,9 @@ private static function getReturnTypeFromCallMapWithArgs( } if ($atomic_types['array'] instanceof TKeyedArray) { + if ($atomic_types['array']->is_callable) { + return Type::getInt(false, 2); + } $min = $atomic_types['array']->getMinCount(); $max = $atomic_types['array']->getMaxCount(); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/HighOrderFunctionArgHandler.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/HighOrderFunctionArgHandler.php index 4822d894c99..b14f8664b15 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/HighOrderFunctionArgHandler.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/HighOrderFunctionArgHandler.php @@ -294,7 +294,7 @@ private static function isSupported(FunctionLikeParameter $container_param): boo } if ($a instanceof Type\Atomic\TCallableString || - $a instanceof Type\Atomic\TCallableKeyedArray + ($a instanceof Type\Atomic\TKeyedArray && $a->is_callable) ) { return false; } diff --git a/src/Psalm/Internal/PreloaderList.php b/src/Psalm/Internal/PreloaderList.php index c4c3ff6d755..956bd90bad6 100644 --- a/src/Psalm/Internal/PreloaderList.php +++ b/src/Psalm/Internal/PreloaderList.php @@ -1688,7 +1688,6 @@ final class PreloaderList { \Psalm\Type\Atomic\TBool::class, \Psalm\Type\Atomic\TCallable::class, \Psalm\Type\Atomic\TCallableInterface::class, - \Psalm\Type\Atomic\TCallableKeyedArray::class, \Psalm\Type\Atomic\TCallableObject::class, \Psalm\Type\Atomic\TCallableString::class, \Psalm\Type\Atomic\TClassConstant::class, diff --git a/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php b/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php index 539cc6ee61f..9fc5a994a51 100644 --- a/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php @@ -15,6 +15,8 @@ use Psalm\Type\Atomic\TNonEmptyArray; use Psalm\Type\Union; +use function assert; + /** * @internal */ diff --git a/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php b/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php index 48369076150..c8c394968b6 100644 --- a/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php @@ -11,7 +11,6 @@ use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TCallable; use Psalm\Type\Atomic\TCallableInterface; -use Psalm\Type\Atomic\TCallableKeyedArray; use Psalm\Type\Atomic\TCallableObject; use Psalm\Type\Atomic\TCallableString; use Psalm\Type\Atomic\TClassStringMap; @@ -175,7 +174,7 @@ public static function isContainedBy( ); } - if ($input_type_part instanceof TCallableKeyedArray + if ($input_type_part instanceof TKeyedArray && $input_type_part->is_callable && $container_type_part instanceof TArray ) { return ArrayTypeComparator::isContainedBy( diff --git a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php index 5c943a9a0c6..8fac92245fe 100644 --- a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php @@ -37,7 +37,6 @@ use Psalm\Type\Atomic\TArrayKey; use Psalm\Type\Atomic\TBool; use Psalm\Type\Atomic\TCallable; -use Psalm\Type\Atomic\TCallableKeyedArray; use Psalm\Type\Atomic\TCallableObject; use Psalm\Type\Atomic\TCallableString; use Psalm\Type\Atomic\TClassConstant; @@ -2279,7 +2278,7 @@ private static function reconcileArray( //a non-empty-array assertion $array_types[] = $type; } elseif ($type instanceof TCallable) { - $array_types[] = new TCallableKeyedArray([ + $array_types[] = TKeyedArray::makeCallable([ new Union([new TClassString, new TObject]), Type::getNonEmptyString(), ]); @@ -2401,7 +2400,7 @@ private static function reconcileList( $redundant = false; } elseif ($type instanceof TCallable) { - $array_types[] = new TCallableKeyedArray([ + $array_types[] = TKeyedArray::makeCallable([ new Union([new TClassString, new TObject]), Type::getNonEmptyString(), ]); @@ -2624,11 +2623,11 @@ private static function reconcileCallable( $callable_types[] = $type; $redundant = false; } elseif ($type instanceof TArray) { - $type = new TCallableKeyedArray($type->type_params); + $type = TKeyedArray::makeCallable($type->type_params); $callable_types[] = $type; $redundant = false; } elseif ($type instanceof TKeyedArray && count($type->properties) === 2) { - $type = new TCallableKeyedArray($type->properties); + $type = TKeyedArray::makeCallable($type->properties); $callable_types[] = $type; $redundant = false; } elseif ($type instanceof TTemplateParam) { diff --git a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php index 1c5fc23eed2..f15ea34b1e2 100644 --- a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php @@ -29,7 +29,6 @@ use Psalm\Type\Atomic\TArrayKey; use Psalm\Type\Atomic\TBool; use Psalm\Type\Atomic\TCallable; -use Psalm\Type\Atomic\TCallableKeyedArray; use Psalm\Type\Atomic\TCallableObject; use Psalm\Type\Atomic\TCallableString; use Psalm\Type\Atomic\TClassString; @@ -1205,7 +1204,7 @@ private static function reconcileObject( $non_object_types[] = $type; } } elseif ($type instanceof TCallable) { - $non_object_types[] = new TCallableKeyedArray([ + $non_object_types[] = TKeyedArray::makeCallable([ new Union([new TClassString, new TObject]), Type::getNonEmptyString(), ]); @@ -1596,7 +1595,7 @@ private static function reconcileString( $non_string_types[] = new TInt(); $redundant = false; } elseif ($type instanceof TCallable) { - $non_string_types[] = new TCallableKeyedArray([ + $non_string_types[] = TKeyedArray::makeCallable([ new Union([new TClassString, new TObject]), Type::getNonEmptyString(), ]); diff --git a/src/Psalm/Internal/Type/TypeCombiner.php b/src/Psalm/Internal/Type/TypeCombiner.php index 85e64fe1eb0..099c995c20d 100644 --- a/src/Psalm/Internal/Type/TypeCombiner.php +++ b/src/Psalm/Internal/Type/TypeCombiner.php @@ -13,7 +13,6 @@ use Psalm\Type\Atomic\TArrayKey; use Psalm\Type\Atomic\TBool; use Psalm\Type\Atomic\TCallable; -use Psalm\Type\Atomic\TCallableKeyedArray; use Psalm\Type\Atomic\TCallableObject; use Psalm\Type\Atomic\TCallableString; use Psalm\Type\Atomic\TClassString; @@ -544,7 +543,7 @@ private static function scrapeTypeProperties( } } - if ($type instanceof TCallableKeyedArray) { + if ($type instanceof TKeyedArray && $type->is_callable) { if (isset($combination->value_types['callable'])) { return null; } @@ -652,7 +651,7 @@ private static function scrapeTypeProperties( } if ($type instanceof TKeyedArray) { - if ($type instanceof TCallableKeyedArray && isset($combination->value_types['callable'])) { + if ($type instanceof TKeyedArray && $type->is_callable && isset($combination->value_types['callable'])) { return null; } @@ -776,7 +775,7 @@ private static function scrapeTypeProperties( $combination->all_arrays_lists = true; } - if ($type instanceof TCallableKeyedArray) { + if ($type instanceof TKeyedArray && $type->is_callable) { if ($combination->all_arrays_callable !== false) { $combination->all_arrays_callable = true; } @@ -1489,7 +1488,7 @@ private static function handleKeyedArrayEntries( ); if ($combination->all_arrays_callable) { - $objectlike = new TCallableKeyedArray( + $objectlike = TKeyedArray::makeCallable( $combination->objectlike_entries, null, $sealed || $fallback_key_type === null || $fallback_value_type === null @@ -1607,7 +1606,7 @@ private static function getArrayTypeFromGenericParams( } if ($combination->all_arrays_callable) { - $array_type = new TCallableKeyedArray($generic_type_params); + $array_type = TKeyedArray::makeCallable($generic_type_params); } elseif ($combination->array_always_filled || ($combination->array_sometimes_filled && $overwrite_empty_array) || ($combination->objectlike_entries diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index b7d4b294c6e..bf897ef84a0 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -33,7 +33,6 @@ use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TArrayKey; use Psalm\Type\Atomic\TCallable; -use Psalm\Type\Atomic\TCallableKeyedArray; use Psalm\Type\Atomic\TCallableObject; use Psalm\Type\Atomic\TClassConstant; use Psalm\Type\Atomic\TClassString; @@ -1397,7 +1396,7 @@ private static function getTypeFromKeyedArrayTree( array $template_type_map, array $type_aliases, bool $from_docblock, - ): TCallableKeyedArray|TKeyedArray|TObjectWithProperties|TArray { + ): TKeyedArray|TObjectWithProperties|TArray { $properties = []; $class_strings = []; @@ -1525,9 +1524,7 @@ private static function getTypeFromKeyedArrayTree( } $callable = str_starts_with($type, 'callable-'); - $class = TKeyedArray::class; if ($callable) { - $class = TCallableKeyedArray::class; $type = substr($type, 9); } @@ -1571,16 +1568,26 @@ private static function getTypeFromKeyedArrayTree( } $extra_params = $final_extra_params; } - return new $class( - $properties, - $class_strings, - $extra_params ?? ($sealed - ? null - : [$is_list ? Type::getListKey() : Type::getArrayKey(), Type::getMixed()] - ), - $is_list, - $from_docblock, - ); + return $callable + ? TKeyedArray::makeCallable( + $properties, + $class_strings, + $extra_params ?? ($sealed + ? null + : [$is_list ? Type::getListKey() : Type::getArrayKey(), Type::getMixed()] + ), + $from_docblock, + ) : TKeyedArray::make( + $properties, + $class_strings, + $extra_params ?? ($sealed + ? null + : [$is_list ? Type::getListKey() : Type::getArrayKey(), Type::getMixed()] + ), + $is_list, + $from_docblock, + ) + ; } /** @@ -1612,7 +1619,7 @@ private static function extractKeyedIntersectionTypes( foreach ($normalized_intersection_types as $intersection_type) { if ($intersection_type instanceof TKeyedArray - && !$intersection_type instanceof TCallableKeyedArray + && !$intersection_type->is_callable ) { $any_array_found = true; diff --git a/src/Psalm/Type/Atomic.php b/src/Psalm/Type/Atomic.php index 8fac11e57e7..95a9d09ce10 100644 --- a/src/Psalm/Type/Atomic.php +++ b/src/Psalm/Type/Atomic.php @@ -20,7 +20,6 @@ use Psalm\Type\Atomic\TArrayKey; use Psalm\Type\Atomic\TBool; use Psalm\Type\Atomic\TCallable; -use Psalm\Type\Atomic\TCallableKeyedArray; use Psalm\Type\Atomic\TCallableObject; use Psalm\Type\Atomic\TCallableString; use Psalm\Type\Atomic\TClassString; @@ -260,7 +259,7 @@ private static function createInner( ); $object = new TObject(true); $string = new TNonEmptyString(true); - return new TCallableKeyedArray([ + return TKeyedArray::makeCallable([ new Union([$classString, $object]), new Union([$string]), ]); @@ -462,7 +461,7 @@ public function isCallableType(): bool return $this instanceof TCallable || $this instanceof TCallableObject || $this instanceof TCallableString - || $this instanceof TCallableKeyedArray + || ($this instanceof TKeyedArray && $this->is_callable) || $this instanceof TClosure; } diff --git a/src/Psalm/Type/Atomic/TCallableKeyedArray.php b/src/Psalm/Type/Atomic/TCallableKeyedArray.php deleted file mode 100644 index 48fe1c30f5e..00000000000 --- a/src/Psalm/Type/Atomic/TCallableKeyedArray.php +++ /dev/null @@ -1,40 +0,0 @@ - $properties - * @param array{Union, Union}|null $fallback_params - * @param array $class_strings - */ - public function __construct( - array $properties, - ?array $class_strings = null, - ?array $fallback_params = null, - bool $from_docblock = false, - ) { - parent::__construct( - $properties, - $class_strings, - $fallback_params, - true, - $from_docblock, - ); - } -} diff --git a/src/Psalm/Type/Atomic/TKeyedArray.php b/src/Psalm/Type/Atomic/TKeyedArray.php index 7e66802e992..b88e5aa93ad 100644 --- a/src/Psalm/Type/Atomic/TKeyedArray.php +++ b/src/Psalm/Type/Atomic/TKeyedArray.php @@ -17,6 +17,7 @@ use Psalm\Type\Union; use function addslashes; +use function array_key_first; use function assert; use function count; use function implode; @@ -32,20 +33,14 @@ * * @psalm-immutable */ -class TKeyedArray extends Atomic +final class TKeyedArray extends Atomic { use UnserializeMemoryUsageSuppressionTrait; - /** @var non-empty-lowercase-string */ - protected const NAME_ARRAY = 'array'; - /** @var non-empty-lowercase-string */ - protected const NAME_LIST = 'list'; - /** * Constructs a new instance of a generic type - * - * @deprecated Please use make() * + * @deprecated Please use make() * @param non-empty-array $properties * @param array{Union, Union}|null $fallback_params * @param array $class_strings @@ -63,6 +58,7 @@ public function __construct( * @var bool - if this is a list of sequential elements */ public bool $is_list = false, + public bool $is_callable = false, bool $from_docblock = false, ) { parent::__construct($from_docblock); @@ -70,7 +66,6 @@ public function __construct( /** * @psalm-pure - * * @param non-empty-array $properties * @param array{Union, Union}|null $fallback_params * @param array $class_strings @@ -91,7 +86,7 @@ public static function make( ) { $never = $properties[array_key_first($properties)]; return new TArray([ - $never, $never + $never, $never, ], $from_docblock); } if ($is_list) { @@ -110,7 +105,45 @@ public static function make( } } - return new self($properties, $class_strings, $fallback_params, $is_list, $from_docblock); + return new self($properties, $class_strings, $fallback_params, $is_list, false, $from_docblock); + } + + /** + * @psalm-pure + * @param non-empty-array $properties + * @param array{Union, Union}|null $fallback_params + * @param array $class_strings + */ + public static function makeCallable( + array $properties, + ?array $class_strings = null, + ?array $fallback_params = null, + bool $from_docblock = false, + ): self|TArray { + if ($fallback_params) { + $fallback_params[0] = Type::getListKey(); + } + if (count($properties) === 1 + && $properties[array_key_first($properties)]->isNever() + && ($fallback_params === null || $fallback_params[1]->isNever()) + ) { + $never = $properties[array_key_first($properties)]; + return new TArray([ + $never, $never, + ], $from_docblock); + } + + return new self($properties, $class_strings, $fallback_params, true, true, $from_docblock); + } + + public function setIsCallable(bool $is_callable): self + { + if ($is_callable === $this->is_callable) { + return $this; + } + $cloned = clone $this; + $cloned->is_callable = $is_callable; + return $cloned; } /** @@ -213,9 +246,9 @@ public function getId(bool $exact = true, bool $nested = false): string } if ($this->is_list) { - $key = static::NAME_LIST; + $key = $this->is_callable ? 'callable-list' : 'list'; } else { - $key = static::NAME_ARRAY; + $key = 'array'; sort($property_strings); } @@ -299,7 +332,7 @@ public function toNamespacedString( $params_part = $this->fallback_params !== null ? ',...' : ''; - return ($this->is_list ? static::NAME_LIST : static::NAME_ARRAY) + return ($this->is_list ? ($this->is_callable ? 'callable-list' : 'list') : 'array') . '{' . implode(', ', $suffixed_properties) . $params_part . '}'; } From b85ef086e10b88b8887146486504e76cc733b78e Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Sat, 1 Mar 2025 15:28:08 +0100 Subject: [PATCH 04/17] Finalize --- UPGRADING.md | 26 +++++++++++++++++++ .../plugins/plugins_type_system.md | 2 +- psalm-baseline.xml | 25 +++++++++--------- src/Psalm/Internal/Type/TypeCombiner.php | 4 +-- src/Psalm/Internal/Type/TypeParser.php | 2 +- src/Psalm/Type/Atomic/TKeyedArray.php | 5 ++-- tests/TypeReconciliation/ReconcilerTest.php | 4 +-- 7 files changed, 48 insertions(+), 20 deletions(-) diff --git a/UPGRADING.md b/UPGRADING.md index db8e1da6017..d19abd2e417 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,3 +1,29 @@ +# Upgrading from Psalm 6 to Psalm 7 + +## Changed + +- [BC] The return type of Psalm\Type::getListAtomic() changed from Psalm\Type\Atomic\TKeyedArray to the non-covariant Psalm\Type\Atomic\TKeyedArray|Psalm\Type\Atomic\TArray + +- [BC] The return type of Psalm\Type::getListAtomic() changed from Psalm\Type\Atomic\TKeyedArray to Psalm\Type\Atomic\TKeyedArray|Psalm\Type\Atomic\TArray + +- [BC] The return type of Psalm\Type::getNonEmptyListAtomic() changed from Psalm\Type\Atomic\TKeyedArray to the non-covariant Psalm\Type\Atomic\TKeyedArray|Psalm\Type\Atomic\TArray + +- [BC] The return type of Psalm\Type::getNonEmptyListAtomic() changed from Psalm\Type\Atomic\TKeyedArray to Psalm\Type\Atomic\TKeyedArray|Psalm\Type\Atomic\TArray + +- [BC] Class Psalm\Type\Atomic\TKeyedArray became final + +- [BC] Class Psalm\Type\Atomic\TKeyedArray can only be created using the new `make` or `makeCallable` factory methods, the constructor was rendered private. + +- [BC] Class Psalm\Type\Atomic\TCallableKeyedArray has been deleted, and replaced with a new `is_callable` flag in Psalm\Type\Atomic\TKeyedArray + +## Removed + +- [BC] Constant Psalm\Type\Atomic\TKeyedArray::NAME_ARRAY was removed + +- [BC] Constant Psalm\Type\Atomic\TKeyedArray::NAME_LIST was removed + +- [BC] Psalm\Type\Atomic\TKeyedArray#__construct() was made private + # Upgrading from Psalm 5 to Psalm 6 ## Changed diff --git a/docs/running_psalm/plugins/plugins_type_system.md b/docs/running_psalm/plugins/plugins_type_system.md index 99e6fa6b807..7faddadce6f 100644 --- a/docs/running_psalm/plugins/plugins_type_system.md +++ b/docs/running_psalm/plugins/plugins_type_system.md @@ -252,7 +252,7 @@ More complex types can be constructed as follows. The following represents an as ``` php new Union([ - new TKeyedArray([ + TKeyedArray::make([ 'key_1' => new Union([new TString()]), 'key_2' => new Union([new TInt()]), 'key_3' => new Union([new TBool()])])]); diff --git a/psalm-baseline.xml b/psalm-baseline.xml index ae8e504c656..3bff6518aed 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + tags['variablesfrom'][0]]]> @@ -2379,6 +2379,18 @@ + + + + + + @@ -2608,17 +2620,6 @@ - - - - - as_type]]> diff --git a/src/Psalm/Internal/Type/TypeCombiner.php b/src/Psalm/Internal/Type/TypeCombiner.php index 099c995c20d..e802d7122c9 100644 --- a/src/Psalm/Internal/Type/TypeCombiner.php +++ b/src/Psalm/Internal/Type/TypeCombiner.php @@ -651,7 +651,7 @@ private static function scrapeTypeProperties( } if ($type instanceof TKeyedArray) { - if ($type instanceof TKeyedArray && $type->is_callable && isset($combination->value_types['callable'])) { + if ($type->is_callable && isset($combination->value_types['callable'])) { return null; } @@ -775,7 +775,7 @@ private static function scrapeTypeProperties( $combination->all_arrays_lists = true; } - if ($type instanceof TKeyedArray && $type->is_callable) { + if ($type->is_callable) { if ($combination->all_arrays_callable !== false) { $combination->all_arrays_callable = true; } diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index bf897ef84a0..e45f9fcd3dc 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -1574,7 +1574,7 @@ private static function getTypeFromKeyedArrayTree( $class_strings, $extra_params ?? ($sealed ? null - : [$is_list ? Type::getListKey() : Type::getArrayKey(), Type::getMixed()] + : [Type::getListKey(), Type::getMixed()] ), $from_docblock, ) : TKeyedArray::make( diff --git a/src/Psalm/Type/Atomic/TKeyedArray.php b/src/Psalm/Type/Atomic/TKeyedArray.php index b88e5aa93ad..1fb3e3dce7e 100644 --- a/src/Psalm/Type/Atomic/TKeyedArray.php +++ b/src/Psalm/Type/Atomic/TKeyedArray.php @@ -31,6 +31,8 @@ /** * Represents an 'object-like array' - an array with known keys. * + * @psalm-api + * * @psalm-immutable */ final class TKeyedArray extends Atomic @@ -40,12 +42,11 @@ final class TKeyedArray extends Atomic /** * Constructs a new instance of a generic type * - * @deprecated Please use make() * @param non-empty-array $properties * @param array{Union, Union}|null $fallback_params * @param array $class_strings */ - public function __construct( + private function __construct( public array $properties, public ?array $class_strings = null, /** diff --git a/tests/TypeReconciliation/ReconcilerTest.php b/tests/TypeReconciliation/ReconcilerTest.php index a156fabc8cc..ce4b246d2bb 100644 --- a/tests/TypeReconciliation/ReconcilerTest.php +++ b/tests/TypeReconciliation/ReconcilerTest.php @@ -163,8 +163,8 @@ public function providerTestReconciliation(): array 'iterableToArray' => ['array', new IsType(new TArray([Type::getArrayKey(), Type::getMixed()])), 'iterable'], 'iterableToTraversable' => ['Traversable', new IsType(new TNamedObject('Traversable')), 'iterable'], 'callableToCallableArray' => ['callable-array{class-string|object, non-empty-string}', new IsType(new TArray([Type::getArrayKey(), Type::getMixed()])), 'callable'], - 'SmallKeyedArrayAndCallable' => ['array{test: string}', new IsType(new TKeyedArray(['test' => Type::getString()])), 'callable'], - 'BigKeyedArrayAndCallable' => ['array{foo: string, test: string, thing: string}', new IsType(new TKeyedArray(['foo' => Type::getString(), 'test' => Type::getString(), 'thing' => Type::getString()])), 'callable'], + 'SmallKeyedArrayAndCallable' => ['array{test: string}', new IsType(TKeyedArray::make(['test' => Type::getString()])), 'callable'], + 'BigKeyedArrayAndCallable' => ['array{foo: string, test: string, thing: string}', new IsType(TKeyedArray::make(['foo' => Type::getString(), 'test' => Type::getString(), 'thing' => Type::getString()])), 'callable'], 'callableOrArrayToCallableArray' => ['array', new IsType(new TArray([Type::getArrayKey(), Type::getMixed()])), 'callable|array'], 'traversableToIntersection' => ['Countable&Traversable', new IsType(new TNamedObject('Traversable')), 'Countable'], 'iterableWithoutParamsToTraversableWithoutParams' => ['Traversable', new IsNotType(new TArray([Type::getArrayKey(), Type::getMixed()])), 'iterable'], From 501ac30c5f18ba9e7ccf10d89f4394c698dd1f58 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Sat, 1 Mar 2025 15:29:02 +0100 Subject: [PATCH 05/17] cs-fix --- src/Psalm/Type/Atomic/TKeyedArray.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Psalm/Type/Atomic/TKeyedArray.php b/src/Psalm/Type/Atomic/TKeyedArray.php index 1fb3e3dce7e..65d1195872d 100644 --- a/src/Psalm/Type/Atomic/TKeyedArray.php +++ b/src/Psalm/Type/Atomic/TKeyedArray.php @@ -32,7 +32,6 @@ * Represents an 'object-like array' - an array with known keys. * * @psalm-api - * * @psalm-immutable */ final class TKeyedArray extends Atomic From 9d017275c661b5f9100371e85d36227d2661794d Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Sat, 1 Mar 2025 15:35:32 +0100 Subject: [PATCH 06/17] fix --- src/Psalm/Internal/Type/TypeCombiner.php | 1 + src/Psalm/Internal/Type/TypeParser.php | 1 + src/Psalm/Type/Atomic/TKeyedArray.php | 7 ++++--- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Internal/Type/TypeCombiner.php b/src/Psalm/Internal/Type/TypeCombiner.php index e802d7122c9..b0a2e1a6954 100644 --- a/src/Psalm/Internal/Type/TypeCombiner.php +++ b/src/Psalm/Internal/Type/TypeCombiner.php @@ -1494,6 +1494,7 @@ private static function handleKeyedArrayEntries( $sealed || $fallback_key_type === null || $fallback_value_type === null ? null : [$fallback_key_type, $fallback_value_type], + (bool)$combination->all_arrays_lists, $from_docblock, ); } else { diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index e45f9fcd3dc..fdc1e361c12 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -1576,6 +1576,7 @@ private static function getTypeFromKeyedArrayTree( ? null : [Type::getListKey(), Type::getMixed()] ), + $is_list, $from_docblock, ) : TKeyedArray::make( $properties, diff --git a/src/Psalm/Type/Atomic/TKeyedArray.php b/src/Psalm/Type/Atomic/TKeyedArray.php index 65d1195872d..b54639d9d4c 100644 --- a/src/Psalm/Type/Atomic/TKeyedArray.php +++ b/src/Psalm/Type/Atomic/TKeyedArray.php @@ -118,6 +118,7 @@ public static function makeCallable( array $properties, ?array $class_strings = null, ?array $fallback_params = null, + bool $is_list = false, bool $from_docblock = false, ): self|TArray { if ($fallback_params) { @@ -133,7 +134,7 @@ public static function makeCallable( ], $from_docblock); } - return new self($properties, $class_strings, $fallback_params, true, true, $from_docblock); + return new self($properties, $class_strings, $fallback_params, $is_list, true, $from_docblock); } public function setIsCallable(bool $is_callable): self @@ -248,7 +249,7 @@ public function getId(bool $exact = true, bool $nested = false): string if ($this->is_list) { $key = $this->is_callable ? 'callable-list' : 'list'; } else { - $key = 'array'; + $key = $this->is_callable ? 'callable-array' : 'array'; sort($property_strings); } @@ -332,7 +333,7 @@ public function toNamespacedString( $params_part = $this->fallback_params !== null ? ',...' : ''; - return ($this->is_list ? ($this->is_callable ? 'callable-list' : 'list') : 'array') + return ($this->is_list ? ($this->is_callable ? 'callable-list' : 'list') : ($this->is_callable ? 'callable-array' : 'array')) . '{' . implode(', ', $suffixed_properties) . $params_part . '}'; } From 8c3dcc157607b8b68baf459452e0494ade864981 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Sat, 1 Mar 2025 15:44:40 +0100 Subject: [PATCH 07/17] Cleanup --- .../Expression/Call/ArgumentAnalyzer.php | 2 +- src/Psalm/Internal/PreloaderList.php | 1 - .../Type/Comparator/AtomicTypeComparator.php | 3 +-- .../Type/Comparator/CallableTypeComparator.php | 15 +++++---------- src/Psalm/Type/Atomic.php | 6 +----- src/Psalm/Type/Atomic/TCallable.php | 8 +++++++- src/Psalm/Type/Atomic/TCallableInterface.php | 9 --------- src/Psalm/Type/Atomic/TCallableObject.php | 8 +++++++- src/Psalm/Type/Atomic/TCallableString.php | 7 ++++++- src/Psalm/Type/Atomic/TClosure.php | 6 ++++++ src/Psalm/Type/Atomic/TKeyedArray.php | 6 ++++++ 11 files changed, 40 insertions(+), 31 deletions(-) delete mode 100644 src/Psalm/Type/Atomic/TCallableInterface.php diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php index a252cdf5c21..8579456b9b5 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php @@ -998,7 +998,7 @@ public static function verifyType( $param_types_without_callable = array_filter( $param_type->getAtomicTypes(), - static fn(Atomic $atomic) => !$atomic instanceof Atomic\TCallableInterface, + static fn(Atomic $atomic) => !$atomic->isCallableType(), ); $param_type_without_callable = [] !== $param_types_without_callable ? new Union($param_types_without_callable) diff --git a/src/Psalm/Internal/PreloaderList.php b/src/Psalm/Internal/PreloaderList.php index 956bd90bad6..105c69de95e 100644 --- a/src/Psalm/Internal/PreloaderList.php +++ b/src/Psalm/Internal/PreloaderList.php @@ -1687,7 +1687,6 @@ final class PreloaderList { \Psalm\Type\Atomic\TArrayKey::class, \Psalm\Type\Atomic\TBool::class, \Psalm\Type\Atomic\TCallable::class, - \Psalm\Type\Atomic\TCallableInterface::class, \Psalm\Type\Atomic\TCallableObject::class, \Psalm\Type\Atomic\TCallableString::class, \Psalm\Type\Atomic\TClassConstant::class, diff --git a/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php b/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php index c8c394968b6..4d88ce33a7f 100644 --- a/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php @@ -10,7 +10,6 @@ use Psalm\Type\Atomic\Scalar; use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TCallable; -use Psalm\Type\Atomic\TCallableInterface; use Psalm\Type\Atomic\TCallableObject; use Psalm\Type\Atomic\TCallableString; use Psalm\Type\Atomic\TClassStringMap; @@ -187,7 +186,7 @@ public static function isContainedBy( } if (($container_type_part instanceof TCallable - && $input_type_part instanceof TCallableInterface + && $input_type_part->isCallableType() ) || ($container_type_part instanceof TClosure && $input_type_part instanceof TClosure) diff --git a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php index 8d79a41037f..102f9995cc1 100644 --- a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php @@ -20,7 +20,6 @@ use Psalm\Type\Atomic; use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TCallable; -use Psalm\Type\Atomic\TCallableInterface; use Psalm\Type\Atomic\TClassString; use Psalm\Type\Atomic\TClosure; use Psalm\Type\Atomic\TKeyedArray; @@ -46,23 +45,19 @@ final class CallableTypeComparator */ public static function isContainedBy( Codebase $codebase, - TClosure|TCallableInterface $input_type_part, + Atomic $input_type_part, Atomic $container_type_part, ?TypeComparisonResult $atomic_comparison_result, ): bool { - if ($container_type_part instanceof TClosure) { - if ($input_type_part instanceof TCallableInterface - && !$input_type_part instanceof TCallable // it has stricter checks below - ) { + if ($input_type_part->isCallableType() + && !$input_type_part instanceof TCallable // it has stricter checks below + ) { + if ($container_type_part instanceof TClosure) { if ($atomic_comparison_result) { $atomic_comparison_result->type_coerced = true; } return false; } - } - if ($input_type_part instanceof TCallableInterface - && !$input_type_part instanceof TCallable // it has stricter checks below - ) { return true; } diff --git a/src/Psalm/Type/Atomic.php b/src/Psalm/Type/Atomic.php index 95a9d09ce10..0d450185078 100644 --- a/src/Psalm/Type/Atomic.php +++ b/src/Psalm/Type/Atomic.php @@ -458,11 +458,7 @@ public function isNamedObjectType(): bool public function isCallableType(): bool { - return $this instanceof TCallable - || $this instanceof TCallableObject - || $this instanceof TCallableString - || ($this instanceof TKeyedArray && $this->is_callable) - || $this instanceof TClosure; + return false; } public function isIterable(Codebase $codebase): bool diff --git a/src/Psalm/Type/Atomic/TCallable.php b/src/Psalm/Type/Atomic/TCallable.php index eafd652a15d..30dc47f19b8 100644 --- a/src/Psalm/Type/Atomic/TCallable.php +++ b/src/Psalm/Type/Atomic/TCallable.php @@ -18,7 +18,7 @@ * * @psalm-immutable */ -final class TCallable extends Atomic implements TCallableInterface +final class TCallable extends Atomic { use UnserializeMemoryUsageSuppressionTrait; use CallableTrait; @@ -124,4 +124,10 @@ protected function getChildNodeKeys(): array { return $this->getCallableChildNodeKeys(); } + + #[Override] + public function isCallableType(): bool + { + return true; + } } diff --git a/src/Psalm/Type/Atomic/TCallableInterface.php b/src/Psalm/Type/Atomic/TCallableInterface.php deleted file mode 100644 index 54e9a3b26db..00000000000 --- a/src/Psalm/Type/Atomic/TCallableInterface.php +++ /dev/null @@ -1,9 +0,0 @@ - $params * @param array $byref_uses diff --git a/src/Psalm/Type/Atomic/TKeyedArray.php b/src/Psalm/Type/Atomic/TKeyedArray.php index b54639d9d4c..47460a17706 100644 --- a/src/Psalm/Type/Atomic/TKeyedArray.php +++ b/src/Psalm/Type/Atomic/TKeyedArray.php @@ -64,6 +64,12 @@ private function __construct( parent::__construct($from_docblock); } + #[Override] + public function isCallableType(): bool + { + return $this->is_callable; + } + /** * @psalm-pure * @param non-empty-array $properties From 2732086da07312ae307ff703879957ade31c4765 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Sat, 1 Mar 2025 18:35:51 +0100 Subject: [PATCH 08/17] Fix --- .../Type/Comparator/CallableTypeComparator.php | 13 +++++++++---- src/Psalm/Type/Atomic/TCallable.php | 1 + src/Psalm/Type/Atomic/TCallableObject.php | 1 + 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php index 102f9995cc1..90cfbfcd76c 100644 --- a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php @@ -20,6 +20,7 @@ use Psalm\Type\Atomic; use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TCallable; +use Psalm\Type\Atomic\TCallableInterface; use Psalm\Type\Atomic\TClassString; use Psalm\Type\Atomic\TClosure; use Psalm\Type\Atomic\TKeyedArray; @@ -49,15 +50,19 @@ public static function isContainedBy( Atomic $container_type_part, ?TypeComparisonResult $atomic_comparison_result, ): bool { - if ($input_type_part->isCallableType() - && !$input_type_part instanceof TCallable // it has stricter checks below - ) { - if ($container_type_part instanceof TClosure) { + if ($container_type_part instanceof TClosure) { + if ($input_type_part->isCallableType() + && !$input_type_part instanceof TCallable // it has stricter checks below + ) { if ($atomic_comparison_result) { $atomic_comparison_result->type_coerced = true; } return false; } + } + if ($input_type_part->isCallableType() + && !$input_type_part instanceof TCallable // it has stricter checks below + ) { return true; } diff --git a/src/Psalm/Type/Atomic/TCallable.php b/src/Psalm/Type/Atomic/TCallable.php index 30dc47f19b8..0212e1ad0d1 100644 --- a/src/Psalm/Type/Atomic/TCallable.php +++ b/src/Psalm/Type/Atomic/TCallable.php @@ -125,6 +125,7 @@ protected function getChildNodeKeys(): array return $this->getCallableChildNodeKeys(); } + /** @return true */ #[Override] public function isCallableType(): bool { diff --git a/src/Psalm/Type/Atomic/TCallableObject.php b/src/Psalm/Type/Atomic/TCallableObject.php index db6c539b2f4..2a91b3ca108 100644 --- a/src/Psalm/Type/Atomic/TCallableObject.php +++ b/src/Psalm/Type/Atomic/TCallableObject.php @@ -15,6 +15,7 @@ final class TCallableObject extends TObject { use HasIntersectionTrait; + /** @return true */ #[Override] public function isCallableType(): bool { From 0235c91d198165a6eceeb2ff512d682b067baa83 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 3 Mar 2025 18:52:01 +0100 Subject: [PATCH 09/17] Cleanup --- tests/TypeParseTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TypeParseTest.php b/tests/TypeParseTest.php index 0d513b8f8e1..9f231b55f65 100644 --- a/tests/TypeParseTest.php +++ b/tests/TypeParseTest.php @@ -489,7 +489,7 @@ public function testTKeyedArrayNonList(): void public function testTKeyedCallableArrayNonList(): void { $this->assertSame( - 'callable-array{class-string, string}', + 'callable-array{0: class-string, 1: string}', (string)Type::parseString('callable-array{0: class-string, 1: string}'), ); } From 7073da142cb0118cea653a89b2a7d95a307c9d8a Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 3 Mar 2025 19:03:52 +0100 Subject: [PATCH 10/17] Cleanup --- src/Psalm/Internal/Type/TypeCombiner.php | 3 --- src/Psalm/Internal/Type/TypeParser.php | 4 ---- src/Psalm/Type/Atomic/TKeyedArray.php | 19 ++----------------- 3 files changed, 2 insertions(+), 24 deletions(-) diff --git a/src/Psalm/Internal/Type/TypeCombiner.php b/src/Psalm/Internal/Type/TypeCombiner.php index b0a2e1a6954..0fdd2af348b 100644 --- a/src/Psalm/Internal/Type/TypeCombiner.php +++ b/src/Psalm/Internal/Type/TypeCombiner.php @@ -1491,9 +1491,6 @@ private static function handleKeyedArrayEntries( $objectlike = TKeyedArray::makeCallable( $combination->objectlike_entries, null, - $sealed || $fallback_key_type === null || $fallback_value_type === null - ? null - : [$fallback_key_type, $fallback_value_type], (bool)$combination->all_arrays_lists, $from_docblock, ); diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index fdc1e361c12..0d3031288fb 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -1572,10 +1572,6 @@ private static function getTypeFromKeyedArrayTree( ? TKeyedArray::makeCallable( $properties, $class_strings, - $extra_params ?? ($sealed - ? null - : [Type::getListKey(), Type::getMixed()] - ), $is_list, $from_docblock, ) : TKeyedArray::make( diff --git a/src/Psalm/Type/Atomic/TKeyedArray.php b/src/Psalm/Type/Atomic/TKeyedArray.php index 47460a17706..c169ff5c84e 100644 --- a/src/Psalm/Type/Atomic/TKeyedArray.php +++ b/src/Psalm/Type/Atomic/TKeyedArray.php @@ -117,30 +117,15 @@ public static function make( /** * @psalm-pure * @param non-empty-array $properties - * @param array{Union, Union}|null $fallback_params * @param array $class_strings */ public static function makeCallable( array $properties, ?array $class_strings = null, - ?array $fallback_params = null, bool $is_list = false, bool $from_docblock = false, - ): self|TArray { - if ($fallback_params) { - $fallback_params[0] = Type::getListKey(); - } - if (count($properties) === 1 - && $properties[array_key_first($properties)]->isNever() - && ($fallback_params === null || $fallback_params[1]->isNever()) - ) { - $never = $properties[array_key_first($properties)]; - return new TArray([ - $never, $never, - ], $from_docblock); - } - - return new self($properties, $class_strings, $fallback_params, $is_list, true, $from_docblock); + ): self { + return new self($properties, $class_strings, null, $is_list, true, $from_docblock); } public function setIsCallable(bool $is_callable): self From 580d343acaa091ea437f70c7c8bd3820f029dca8 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 3 Mar 2025 19:25:11 +0100 Subject: [PATCH 11/17] Fix --- src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php | 3 ++- src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php | 2 ++ tests/TypeReconciliation/ReconcilerTest.php | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php index 6a8bb8ecb9b..59d39427233 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php @@ -19,6 +19,7 @@ use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TArrayKey; use Psalm\Type\Atomic\TBool; +use Psalm\Type\Atomic\TClosure; use Psalm\Type\Atomic\TFalse; use Psalm\Type\Atomic\TFloat; use Psalm\Type\Atomic\TInt; @@ -191,7 +192,7 @@ public static function getOptionsArgValueOrError( if ($filter_int_used === FILTER_CALLBACK) { $only_callables = true; foreach ($atomic_type->properties['options']->getAtomicTypes() as $option_atomic) { - if ($option_atomic->isCallableType()) { + if ($option_atomic->isCallableType() && !$option_atomic instanceof TClosure) { continue; } diff --git a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php index 90cfbfcd76c..5138affbe83 100644 --- a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php @@ -53,6 +53,7 @@ public static function isContainedBy( if ($container_type_part instanceof TClosure) { if ($input_type_part->isCallableType() && !$input_type_part instanceof TCallable // it has stricter checks below + && !$input_type_part instanceof TClosure // it has stricter checks below ) { if ($atomic_comparison_result) { $atomic_comparison_result->type_coerced = true; @@ -62,6 +63,7 @@ public static function isContainedBy( } if ($input_type_part->isCallableType() && !$input_type_part instanceof TCallable // it has stricter checks below + && !$input_type_part instanceof TClosure // it has stricter checks below ) { return true; } diff --git a/tests/TypeReconciliation/ReconcilerTest.php b/tests/TypeReconciliation/ReconcilerTest.php index ce4b246d2bb..ad2b1a4bf6d 100644 --- a/tests/TypeReconciliation/ReconcilerTest.php +++ b/tests/TypeReconciliation/ReconcilerTest.php @@ -162,7 +162,7 @@ public function providerTestReconciliation(): array 'nullableClassStringTruthy' => ['class-string', new Truthy(), 'class-string|null'], 'iterableToArray' => ['array', new IsType(new TArray([Type::getArrayKey(), Type::getMixed()])), 'iterable'], 'iterableToTraversable' => ['Traversable', new IsType(new TNamedObject('Traversable')), 'iterable'], - 'callableToCallableArray' => ['callable-array{class-string|object, non-empty-string}', new IsType(new TArray([Type::getArrayKey(), Type::getMixed()])), 'callable'], + 'callableToCallableArray' => ['callable-array{0: class-string|object, 1: non-empty-string}', new IsType(new TArray([Type::getArrayKey(), Type::getMixed()])), 'callable'], 'SmallKeyedArrayAndCallable' => ['array{test: string}', new IsType(TKeyedArray::make(['test' => Type::getString()])), 'callable'], 'BigKeyedArrayAndCallable' => ['array{foo: string, test: string, thing: string}', new IsType(TKeyedArray::make(['foo' => Type::getString(), 'test' => Type::getString(), 'thing' => Type::getString()])), 'callable'], 'callableOrArrayToCallableArray' => ['array', new IsType(new TArray([Type::getArrayKey(), Type::getMixed()])), 'callable|array'], From d504ad66bc051b1ce3aadcd5727ce8ed486c9f0a Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 3 Mar 2025 20:02:39 +0100 Subject: [PATCH 12/17] Fixes --- psalm-baseline.xml | 21 ++++++-- .../Comparator/CallableTypeComparator.php | 3 +- src/Psalm/Type/Atomic/TKeyedArray.php | 48 ++++++++++++++----- tests/CallableTest.php | 2 +- 4 files changed, 55 insertions(+), 19 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 3bff6518aed..173bc553133 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -2386,10 +2386,6 @@ ? null : [$is_list ? Type::getListKey() : Type::getArrayKey(), Type::getMixed()] )]]> - @@ -2701,11 +2697,28 @@ possibly_undefined]]> + + from_docblock)]]> + from_docblock)]]> + from_docblock)]]> + + + + + + + properties[0]]]> properties[0]]]> diff --git a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php index 5138affbe83..566b6f80990 100644 --- a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php @@ -20,7 +20,6 @@ use Psalm\Type\Atomic; use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TCallable; -use Psalm\Type\Atomic\TCallableInterface; use Psalm\Type\Atomic\TClassString; use Psalm\Type\Atomic\TClosure; use Psalm\Type\Atomic\TKeyedArray; @@ -31,6 +30,7 @@ use UnexpectedValueException; use function array_slice; +use function assert; use function count; use function end; use function strtolower; @@ -67,6 +67,7 @@ public static function isContainedBy( ) { return true; } + assert($input_type_part instanceof TClosure || $input_type_part instanceof TCallable); if ($container_type_part->is_pure && !$input_type_part->is_pure) { if ($atomic_comparison_result) { diff --git a/src/Psalm/Type/Atomic/TKeyedArray.php b/src/Psalm/Type/Atomic/TKeyedArray.php index c169ff5c84e..a35b725619c 100644 --- a/src/Psalm/Type/Atomic/TKeyedArray.php +++ b/src/Psalm/Type/Atomic/TKeyedArray.php @@ -140,13 +140,21 @@ public function setIsCallable(bool $is_callable): self /** * @param non-empty-array $properties - * @return static */ - public function setProperties(array $properties): self + public function setProperties(array $properties): self|TArray { if ($properties === $this->properties) { return $this; } + if (count($properties) === 1 + && $properties[array_key_first($properties)]->isNever() + && ($this->fallback_params === null || $this->fallback_params[1]->isNever()) + ) { + $never = $properties[array_key_first($properties)]; + return new TArray([ + $never, $never, + ], $this->from_docblock); + } $cloned = clone $this; $cloned->properties = $properties; if ($cloned->is_list) { @@ -169,9 +177,6 @@ public function setProperties(array $properties): self return $cloned; } - /** - * @return static - */ public function makeSealed(): self { if ($this->fallback_params === null) { @@ -543,9 +548,6 @@ public function getKey(bool $include_extra = true): string return 'array'; } - /** - * @return static - */ #[Override] public function replaceTemplateTypesWithStandins( TemplateResult $template_result, @@ -558,7 +560,7 @@ public function replaceTemplateTypesWithStandins( bool $replace = true, bool $add_lower_bound = false, int $depth = 0, - ): self { + ): self|TArray { if ($input_type instanceof TKeyedArray && $input_type->is_list && $input_type->isSealed() @@ -581,6 +583,11 @@ public function replaceTemplateTypesWithStandins( ->type_params[1] ->setPossiblyUndefined(!$this->isNonEmpty()); + if ($replaced_list_type->isNever()) { + return new TArray([ + $replaced_list_type, $replaced_list_type, + ], $this->from_docblock); + } $cloned = clone $this; $cloned->properties = [$replaced_list_type]; $cloned->fallback_params = [$this->fallback_params[1], $replaced_list_type]; @@ -647,20 +654,26 @@ public function replaceTemplateTypesWithStandins( return $this; } $cloned = clone $this; + if (count($properties) === 1 + && $properties[array_key_first($properties)]->isNever() + && ($fallback_params === null || $fallback_params[1]->isNever()) + ) { + $never = $properties[array_key_first($properties)]; + return new TArray([ + $never, $never, + ], $this->from_docblock); + } $cloned->properties = $properties; /** @psalm-suppress PropertyTypeCoercion */ $cloned->fallback_params = $fallback_params; return $cloned; } - /** - * @return static - */ #[Override] public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase, - ): self { + ): self|TArray { $properties = $this->properties; foreach ($properties as $offset => $property) { $properties[$offset] = TemplateInferredTypeReplacer::replace( @@ -679,6 +692,15 @@ public function replaceTemplateTypesWithArgTypes( } if ($properties !== $this->properties || $fallback_params !== $this->fallback_params) { $cloned = clone $this; + if (count($properties) === 1 + && $properties[array_key_first($properties)]->isNever() + && ($fallback_params === null || $fallback_params[1]->isNever()) + ) { + $never = $properties[array_key_first($properties)]; + return new TArray([ + $never, $never, + ], $this->from_docblock); + } $cloned->properties = $properties; /** @psalm-suppress PropertyTypeCoercion */ $cloned->fallback_params = $fallback_params; diff --git a/tests/CallableTest.php b/tests/CallableTest.php index 09be6c02ffe..07290709f17 100644 --- a/tests/CallableTest.php +++ b/tests/CallableTest.php @@ -2297,7 +2297,7 @@ function int_int_int_int(Closure $f): void {} ], 'callableArrayTypes' => [ 'code' => ' Date: Mon, 3 Mar 2025 20:58:22 +0100 Subject: [PATCH 13/17] Fixes --- src/Psalm/Internal/Type/TypeTokenizer.php | 1 + src/Psalm/Type/Atomic.php | 16 ++++++++++++++++ tests/TypeCombinationTest.php | 14 ++++++++++++++ 3 files changed, 31 insertions(+) diff --git a/src/Psalm/Internal/Type/TypeTokenizer.php b/src/Psalm/Internal/Type/TypeTokenizer.php index 1b2dafe7031..53463062e2e 100644 --- a/src/Psalm/Internal/Type/TypeTokenizer.php +++ b/src/Psalm/Internal/Type/TypeTokenizer.php @@ -56,6 +56,7 @@ final class TypeTokenizer 'trait-string' => true, 'callable-string' => true, 'callable-array' => true, + 'callable-list' => true, 'callable-object' => true, 'stringable-object' => true, 'pure-callable' => true, diff --git a/src/Psalm/Type/Atomic.php b/src/Psalm/Type/Atomic.php index 0d450185078..eeb036f2809 100644 --- a/src/Psalm/Type/Atomic.php +++ b/src/Psalm/Type/Atomic.php @@ -264,6 +264,22 @@ private static function createInner( new Union([$string]), ]); + case 'callable-list': + $classString = new TClassString( + 'object', + null, + false, + false, + false, + true, + ); + $object = new TObject(true); + $string = new TNonEmptyString(true); + return TKeyedArray::makeCallable([ + new Union([$classString, $object]), + new Union([$string]), + ], null, true); + case 'list': return Type::getListAtomic(Type::getMixed(false, $from_docblock)); diff --git a/tests/TypeCombinationTest.php b/tests/TypeCombinationTest.php index 5c989ffd080..3b2e70ac6de 100644 --- a/tests/TypeCombinationTest.php +++ b/tests/TypeCombinationTest.php @@ -710,6 +710,20 @@ public function providerTestValidTypeCombination(): array 'callable', ], ], + 'combineCallableAndCallableList' => [ + 'callable', + [ + 'callable', + 'callable-list', + ], + ], + 'combineCallableListAndCallable' => [ + 'callable', + [ + 'callable-list', + 'callable', + ], + ], 'combineCallableArrayAndArray' => [ 'array', [ From 470c722561ed130feb8614e5237652f98f2fd6e5 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 3 Mar 2025 20:59:49 +0100 Subject: [PATCH 14/17] cs-fix --- src/Psalm/Type/Atomic/TKeyedArray.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Type/Atomic/TKeyedArray.php b/src/Psalm/Type/Atomic/TKeyedArray.php index a35b725619c..b2163ed1d3d 100644 --- a/src/Psalm/Type/Atomic/TKeyedArray.php +++ b/src/Psalm/Type/Atomic/TKeyedArray.php @@ -329,8 +329,10 @@ public function toNamespacedString( $params_part = $this->fallback_params !== null ? ',...' : ''; - return ($this->is_list ? ($this->is_callable ? 'callable-list' : 'list') : ($this->is_callable ? 'callable-array' : 'array')) - . '{' . implode(', ', $suffixed_properties) . $params_part . '}'; + return ($this->is_list + ? ($this->is_callable ? 'callable-list' : 'list') + : ($this->is_callable ? 'callable-array' : 'array') + ) . '{' . implode(', ', $suffixed_properties) . $params_part . '}'; } /** From d7378140ca2aaee0b56b3917aadbe6e4854157ea Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 3 Mar 2025 21:04:31 +0100 Subject: [PATCH 15/17] Fixes --- tests/CoreStubsTest.php | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/CoreStubsTest.php b/tests/CoreStubsTest.php index 282510be765..e9d25d2e102 100644 --- a/tests/CoreStubsTest.php +++ b/tests/CoreStubsTest.php @@ -366,20 +366,20 @@ function after_str_ends_with() $stringPatternMaybeWithNocheckFlagAndMaybeOnlydir = glob( $string , $maybeNocheckFlag | $maybeOnlydirFlag); PHP, 'assertions' => [ - '$emptyPatternNoFlags===' => 'false|array', - '$emptyPatternWithoutNocheckFlag1===' => 'false|array', - '$emptyPatternWithoutNocheckFlag2===' => 'false|array', - '$emptyPatternWithoutNocheckFlag3===' => 'false|array', + '$emptyPatternNoFlags===' => 'array|false', + '$emptyPatternWithoutNocheckFlag1===' => 'array|false', + '$emptyPatternWithoutNocheckFlag2===' => 'array|false', + '$emptyPatternWithoutNocheckFlag3===' => 'array|false', '$emptyPatternWithNocheckFlag1===' => 'false|list{\'\'}', '$emptyPatternWithNocheckFlag2===' => 'false|list{\'\'}', '$emptyPatternWithNocheckFlag3===' => 'false|list{\'\'}', - '$emptyPatternWithNocheckAndOnlydirFlag1===' => 'false|array', - '$emptyPatternWithNocheckAndOnlydirFlag2===' => 'false|array', - '$emptyPatternWithNocheckAndOnlydirFlag3===' => 'false|array', - '$emptyPatternWithNocheckFlagAndMaybeOnlydir===' => 'false|list{0?: \'\', ...}', - '$emptyPatternMaybeWithNocheckFlag===' => 'false|list{0?: \'\', ...}', - '$emptyPatternMaybeWithNocheckFlagAndOnlydir===' => 'false|array', - '$emptyPatternMaybeWithNocheckFlagAndMaybeOnlydir===' => 'false|list{0?: \'\', ...}', + '$emptyPatternWithNocheckAndOnlydirFlag1===' => 'array|false', + '$emptyPatternWithNocheckAndOnlydirFlag2===' => 'array|false', + '$emptyPatternWithNocheckAndOnlydirFlag3===' => 'array|false', + '$emptyPatternWithNocheckFlagAndMaybeOnlydir===' => 'false|list{0?: \'\'}', + '$emptyPatternMaybeWithNocheckFlag===' => 'false|list{0?: \'\'}', + '$emptyPatternMaybeWithNocheckFlagAndOnlydir===' => 'array|false', + '$emptyPatternMaybeWithNocheckFlagAndMaybeOnlydir===' => 'false|list{0?: \'\'}', '$nonEmptyPatternNoFlags===' => 'false|list', '$nonEmptyPatternWithoutNocheckFlag1===' => 'false|list', From f2a6d9f0ed45a5c820bc2cec7fd4117c160c4b56 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 3 Mar 2025 22:02:02 +0100 Subject: [PATCH 16/17] Fixes --- src/Psalm/Internal/Preloader.php | 12 +++++------- src/Psalm/Progress/DebugProgress.php | 1 + src/Psalm/Progress/LongProgress.php | 5 ++++- src/Psalm/Progress/Phase.php | 1 + src/Psalm/Type/Atomic/TKeyedArray.php | 9 +++++---- 5 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/Psalm/Internal/Preloader.php b/src/Psalm/Internal/Preloader.php index 35ab324812b..7b05763cb3a 100644 --- a/src/Psalm/Internal/Preloader.php +++ b/src/Psalm/Internal/Preloader.php @@ -21,17 +21,15 @@ public static function preload(?Progress $progress = null, bool $hasJit = false) return; } - if ($hasJit) { - $progress?->startPhase(Phase::JIT_COMPILATION); - $progress?->expand(count(PreloaderList::CLASSES)+1); - } + $progress?->startPhase($hasJit ? Phase::JIT_COMPILATION : Phase::PRELOADING); + $progress?->expand(count(PreloaderList::CLASSES)+1); + foreach (PreloaderList::CLASSES as $class) { $progress?->taskDone(0); class_exists($class); } - if ($hasJit) { - $progress?->finish(); - } + + $progress?->finish(); self::$preloaded = true; } } diff --git a/src/Psalm/Progress/DebugProgress.php b/src/Psalm/Progress/DebugProgress.php index 9c2d9587113..0c869cd14b0 100644 --- a/src/Psalm/Progress/DebugProgress.php +++ b/src/Psalm/Progress/DebugProgress.php @@ -33,6 +33,7 @@ public function startPhase(Phase $phase): void Phase::ALTERING => "\nUpdating files...\n", Phase::TAINT_GRAPH_RESOLUTION => "\nResolving taint graph...\n", Phase::JIT_COMPILATION => "\nJIT compilation in progress...\n", + Phase::PRELOADING => "\nPreloading in progress...\n", }); } diff --git a/src/Psalm/Progress/LongProgress.php b/src/Psalm/Progress/LongProgress.php index f55de1f01f2..0f9a0e1be6f 100644 --- a/src/Psalm/Progress/LongProgress.php +++ b/src/Psalm/Progress/LongProgress.php @@ -51,10 +51,12 @@ public function startPhase(Phase $phase): void Phase::ALTERING => "\nUpdating files...\n", Phase::TAINT_GRAPH_RESOLUTION => "\n\nResolving taint graph...\n\n", Phase::JIT_COMPILATION => "JIT compilation in progress...\n\n", + Phase::PRELOADING => "Preloading in progress...\n\n", }); $this->fixed_size = $phase === Phase::ANALYSIS || $phase === Phase::ALTERING - || $phase === Phase::JIT_COMPILATION; + || $phase === Phase::JIT_COMPILATION + || $phase === Phase::PRELOADING; } protected function reportPhaseDuration(?Phase $newPhase = null): void @@ -72,6 +74,7 @@ protected function reportPhaseDuration(?Phase $newPhase = null): void Phase::ALTERING => "\n\nUpdating files took $took seconds.\n", Phase::TAINT_GRAPH_RESOLUTION => "\n\nTaint graph resolution took $took seconds.\n", Phase::JIT_COMPILATION => "JIT compilation took $took seconds.\n\n", + Phase::PRELOADING => "Preloading took $took seconds.\n\n", }); } $this->started = microtime(true); diff --git a/src/Psalm/Progress/Phase.php b/src/Psalm/Progress/Phase.php index 701d0827048..273868568b4 100644 --- a/src/Psalm/Progress/Phase.php +++ b/src/Psalm/Progress/Phase.php @@ -15,4 +15,5 @@ enum Phase case ALTERING; case TAINT_GRAPH_RESOLUTION; case JIT_COMPILATION; + case PRELOADING; } diff --git a/src/Psalm/Type/Atomic/TKeyedArray.php b/src/Psalm/Type/Atomic/TKeyedArray.php index b2163ed1d3d..232919a2eaf 100644 --- a/src/Psalm/Type/Atomic/TKeyedArray.php +++ b/src/Psalm/Type/Atomic/TKeyedArray.php @@ -90,7 +90,7 @@ public static function make( && $properties[array_key_first($properties)]->isNever() && ($fallback_params === null || $fallback_params[1]->isNever()) ) { - $never = $properties[array_key_first($properties)]; + $never = $properties[array_key_first($properties)]->setPossiblyUndefined(false); return new TArray([ $never, $never, ], $from_docblock); @@ -150,7 +150,7 @@ public function setProperties(array $properties): self|TArray && $properties[array_key_first($properties)]->isNever() && ($this->fallback_params === null || $this->fallback_params[1]->isNever()) ) { - $never = $properties[array_key_first($properties)]; + $never = $properties[array_key_first($properties)]->setPossiblyUndefined(false); return new TArray([ $never, $never, ], $this->from_docblock); @@ -586,6 +586,7 @@ public function replaceTemplateTypesWithStandins( ->setPossiblyUndefined(!$this->isNonEmpty()); if ($replaced_list_type->isNever()) { + $replaced_list_type = $replaced_list_type->setPossiblyUndefined(false); return new TArray([ $replaced_list_type, $replaced_list_type, ], $this->from_docblock); @@ -660,7 +661,7 @@ public function replaceTemplateTypesWithStandins( && $properties[array_key_first($properties)]->isNever() && ($fallback_params === null || $fallback_params[1]->isNever()) ) { - $never = $properties[array_key_first($properties)]; + $never = $properties[array_key_first($properties)]->setPossiblyUndefined(false); return new TArray([ $never, $never, ], $this->from_docblock); @@ -698,7 +699,7 @@ public function replaceTemplateTypesWithArgTypes( && $properties[array_key_first($properties)]->isNever() && ($fallback_params === null || $fallback_params[1]->isNever()) ) { - $never = $properties[array_key_first($properties)]; + $never = $properties[array_key_first($properties)]->setPossiblyUndefined(false); return new TArray([ $never, $never, ], $this->from_docblock); From 682ee441bb0eecf343df0eac8af01ff801c84722 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 3 Mar 2025 22:21:44 +0100 Subject: [PATCH 17/17] Bump upgrading --- UPGRADING.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/UPGRADING.md b/UPGRADING.md index 04bd4c410c0..f237259cb1d 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -32,6 +32,8 @@ - [BC] Class Psalm\Type\Atomic\TCallableKeyedArray has been deleted, and replaced with a new `is_callable` flag in Psalm\Type\Atomic\TKeyedArray +- [BC] Class Psalm\Type\Atomic\TCallableInterface has been deleted, use `\Psalm\Type\Atomic::isCallableType()` instead + ## Removed - [BC] Constant Psalm\Type\Atomic\TKeyedArray::NAME_ARRAY was removed