diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php index 42f0b6b9d..9cab7a89b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php @@ -247,7 +247,8 @@ class ArrayAnalyzer $statements_analyzer, $array_creation_info, $item, - $unpacked_array_type + $unpacked_array_type, + $codebase ); if (($data_flow_graph = $statements_analyzer->data_flow_graph) @@ -482,35 +483,42 @@ class ArrayAnalyzer StatementsAnalyzer $statements_analyzer, ArrayCreationInfo $array_creation_info, PhpParser\Node\Expr\ArrayItem $item, - Type\Union $unpacked_array_type + Type\Union $unpacked_array_type, + Codebase $codebase ) : void { foreach ($unpacked_array_type->getAtomicTypes() as $unpacked_atomic_type) { if ($unpacked_atomic_type instanceof Type\Atomic\TKeyedArray) { foreach ($unpacked_atomic_type->properties as $key => $property_value) { if (\is_string($key)) { - if (IssueBuffer::accepts( - new DuplicateArrayKey( - 'String keys are not supported in unpacked arrays', - new CodeLocation($statements_analyzer->getSource(), $item->value) - ), - $statements_analyzer->getSuppressedIssues() - )) { - // fall through - } + if ($codebase->php_major_version < 8 || + ($codebase->php_major_version === 8 && $codebase->php_minor_version < 1) + ) { + if (IssueBuffer::accepts( + new DuplicateArrayKey( + 'String keys are not supported in unpacked arrays', + new CodeLocation($statements_analyzer->getSource(), $item->value) + ), + $statements_analyzer->getSuppressedIssues() + )) { + // fall through + } - return; + return; + } + $new_offset = $key; + $array_creation_info->item_key_atomic_types[] = new Type\Atomic\TLiteralString($new_offset); + } else { + $new_offset = $array_creation_info->int_offset++; + $array_creation_info->item_key_atomic_types[] = new Type\Atomic\TLiteralInt($new_offset); } - $new_int_offset = $array_creation_info->int_offset++; - - $array_creation_info->item_key_atomic_types[] = new Type\Atomic\TLiteralInt($new_int_offset); $array_creation_info->item_value_atomic_types = array_merge( $array_creation_info->item_value_atomic_types, array_values($property_value->getAtomicTypes()) ); - $array_creation_info->array_keys[$new_int_offset] = true; - $array_creation_info->property_types[$new_int_offset] = $property_value; + $array_creation_info->array_keys[$new_offset] = true; + $array_creation_info->property_types[$new_offset] = $property_value; } } else { $codebase = $statements_analyzer->getCodebase(); @@ -529,15 +537,23 @@ class ArrayAnalyzer $array_creation_info->can_create_objectlike = false; if ($unpacked_atomic_type->type_params[0]->hasString()) { - if (IssueBuffer::accepts( - new DuplicateArrayKey( - 'String keys are not supported in unpacked arrays', - new CodeLocation($statements_analyzer->getSource(), $item->value) - ), - $statements_analyzer->getSuppressedIssues() - )) { - // fall through + if ($codebase->php_major_version < 8 || + ($codebase->php_major_version === 8 && $codebase->php_minor_version < 1) + ) { + if (IssueBuffer::accepts( + new DuplicateArrayKey( + 'String keys are not supported in unpacked arrays', + new CodeLocation($statements_analyzer->getSource(), $item->value) + ), + $statements_analyzer->getSuppressedIssues() + )) { + // fall through + } + + return; } + + $array_creation_info->item_key_atomic_types[] = new Type\Atomic\TString(); } elseif ($unpacked_atomic_type->type_params[0]->hasInt()) { $array_creation_info->item_key_atomic_types[] = new Type\Atomic\TInt(); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php index ceb43a8a5..a88386e47 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php @@ -671,20 +671,20 @@ class SimpleTypeInferer if ($unpacked_atomic_type instanceof Type\Atomic\TKeyedArray) { foreach ($unpacked_atomic_type->properties as $key => $property_value) { if (\is_string($key)) { - // string keys are not supported in unpacked arrays - return false; + $new_offset = $key; + $array_creation_info->item_key_atomic_types[] = new Type\Atomic\TLiteralString($new_offset); + } else { + $new_offset = $array_creation_info->int_offset++; + $array_creation_info->item_key_atomic_types[] = new Type\Atomic\TLiteralInt($new_offset); } - $new_int_offset = $array_creation_info->int_offset++; - - $array_creation_info->item_key_atomic_types[] = new Type\Atomic\TLiteralInt($new_int_offset); $array_creation_info->item_value_atomic_types = array_merge( $array_creation_info->item_value_atomic_types, array_values($property_value->getAtomicTypes()) ); - $array_creation_info->array_keys[$new_int_offset] = true; - $array_creation_info->property_types[$new_int_offset] = $property_value; + $array_creation_info->array_keys[$new_offset] = true; + $array_creation_info->property_types[$new_offset] = $property_value; } } elseif ($unpacked_atomic_type instanceof Type\Atomic\TArray) { /** @psalm-suppress PossiblyUndefinedArrayOffset provably true, but Psalm can’t see it */ @@ -694,8 +694,7 @@ class SimpleTypeInferer $array_creation_info->can_create_objectlike = false; if ($unpacked_atomic_type->type_params[0]->hasString()) { - // string keys are not supported in unpacked arrays - return false; + $array_creation_info->item_key_atomic_types[] = new Type\Atomic\TString(); } if ($unpacked_atomic_type->type_params[0]->hasInt()) { diff --git a/tests/ArrayAssignmentTest.php b/tests/ArrayAssignmentTest.php index 6d1bc8d7f..513505f5e 100644 --- a/tests/ArrayAssignmentTest.php +++ b/tests/ArrayAssignmentTest.php @@ -1234,6 +1234,19 @@ class ArrayAssignmentTest extends TestCase '$arr3' => 'array{1: int, 2: int, 3: int, 4: int}', ] ], + 'arraySpreadWithString' => [ + ' 0, + ...["a" => 1], + ...["b" => 2] + ];', + [ + '$x===' => 'array{a: 1, b: 2}', + ], + [], + '8.1' + ], 'listPropertyAssignmentAfterIsset' => [ ' ['$_a===' => 'array{16: 16, 17: 17, 18: 18}'] ], + 'unpackTypedIterableWithStringKeysIntoArray' => [ + ' $data + * @return list + */ + function unpackIterable(iterable $data): array + { + return [...$data]; + }', + [], + [], + '8.1' + ], + 'unpackTypedTraversableWithStringKeysIntoArray' => [ + ' $data + * @return list + */ + function unpackIterable(Traversable $data): array + { + return [...$data]; + }', + [], + [], + '8.1' + ], + 'unpackArrayWithArrayKeyIntoArray' => [ + ' $data + * @return list + */ + function unpackArray(array $data): array + { + return [...$data]; + }', + [], + [], + '8.1' + ], ]; } @@ -1882,45 +1940,6 @@ class ArrayAssignmentTest extends TestCase }', 'error_message' => 'NullableReturnStatement', ], - 'unpackTypedIterableWithStringKeysIntoArray' => [ - ' $data - * @return list - */ - function unpackIterable(iterable $data): array - { - return [...$data]; - }', - 'error_message' => 'DuplicateArrayKey' - ], - 'unpackTypedTraversableWithStringKeysIntoArray' => [ - ' $data - * @return list - */ - function unpackIterable(Traversable $data): array - { - return [...$data]; - }', - 'error_message' => 'DuplicateArrayKey' - ], - 'unpackArrayWithArrayKeyIntoArray' => [ - ' $data - * @return list - */ - function unpackArray(array $data): array - { - return [...$data]; - }', - 'error_message' => 'DuplicateArrayKey', - ], 'ArrayCreateOffsetObject' => [ ' "a"];