1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-26 20:34:47 +01:00

Detect duplicate keys in array shapes

This commit is contained in:
Daniil Gentili 2023-01-25 10:42:05 +01:00
parent e784128902
commit 247d30f74c
2 changed files with 22 additions and 12 deletions

View File

@ -1478,6 +1478,10 @@ class TypeParser
$had_optional = true; $had_optional = true;
} }
if (isset($properties[$property_key])) {
throw new TypeParseTreeException("Duplicate key $property_key detected");
}
$properties[$property_key] = $property_type; $properties[$property_key] = $property_type;
if ($class_string) { if ($class_string) {
$class_strings[$property_key] = true; $class_strings[$property_key] = true;
@ -1485,7 +1489,7 @@ class TypeParser
} }
if ($had_explicit && $had_implicit) { if ($had_explicit && $had_implicit) {
throw new TypeParseTreeException('Cannot mix explicit and implicit keys!'); throw new TypeParseTreeException('Cannot mix explicit and implicit keys');
} }
if ($type === 'object') { if ($type === 'object') {
@ -1500,7 +1504,7 @@ class TypeParser
} }
if ($callable && !$properties) { if ($callable && !$properties) {
throw new TypeParseTreeException('A callable array cannot be empty!'); throw new TypeParseTreeException('A callable array cannot be empty');
} }
if ($type !== 'array' && $type !== 'list') { if ($type !== 'array' && $type !== 'list') {
@ -1508,7 +1512,7 @@ class TypeParser
} }
if ($type === 'list' && !$is_list) { if ($type === 'list' && !$is_list) {
throw new TypeParseTreeException('A list shape cannot describe a non-list!'); throw new TypeParseTreeException('A list shape cannot describe a non-list');
} }
if (!$properties) { if (!$properties) {
@ -1520,7 +1524,7 @@ class TypeParser
$class_strings, $class_strings,
$sealed $sealed
? null ? null
: [$is_list ? Type::getInt() : Type::getArrayKey(), Type::getMixed()], : [$is_list ? Type::getListKey() : Type::getArrayKey(), Type::getMixed()],
$is_list, $is_list,
$from_docblock, $from_docblock,
); );

View File

@ -473,54 +473,60 @@ class TypeParseTest extends TestCase
public function testTKeyedListNonList(): void public function testTKeyedListNonList(): void
{ {
$this->expectExceptionMessage('A list shape cannot describe a non-list!'); $this->expectExceptionMessage('A list shape cannot describe a non-list');
Type::parseString('list{a: 0, b: 1, c: 2}'); Type::parseString('list{a: 0, b: 1, c: 2}');
} }
public function testTKeyedListNonListOptional(): void public function testTKeyedListNonListOptional(): void
{ {
$this->expectExceptionMessage('A list shape cannot describe a non-list!'); $this->expectExceptionMessage('A list shape cannot describe a non-list');
Type::parseString('list{a: 0, b?: 1, c?: 2}'); Type::parseString('list{a: 0, b?: 1, c?: 2}');
} }
public function testTKeyedListNonListOptionalWrongOrder1(): void public function testTKeyedListNonListOptionalWrongOrder1(): void
{ {
$this->expectExceptionMessage('A list shape cannot describe a non-list!'); $this->expectExceptionMessage('A list shape cannot describe a non-list');
Type::parseString('list{0?: 0, 1: 1, 2: 2}'); Type::parseString('list{0?: 0, 1: 1, 2: 2}');
} }
public function testTKeyedListNonListOptionalWrongOrder2(): void public function testTKeyedListNonListOptionalWrongOrder2(): void
{ {
$this->expectExceptionMessage('A list shape cannot describe a non-list!'); $this->expectExceptionMessage('A list shape cannot describe a non-list');
Type::parseString('list{0: 0, 1?: 1, 2: 2}'); Type::parseString('list{0: 0, 1?: 1, 2: 2}');
} }
public function testTKeyedListWrongOrder(): void public function testTKeyedListWrongOrder(): void
{ {
$this->expectExceptionMessage('A list shape cannot describe a non-list!'); $this->expectExceptionMessage('A list shape cannot describe a non-list');
Type::parseString('list{1: 1, 0: 0}'); Type::parseString('list{1: 1, 0: 0}');
} }
public function testTKeyedListNonListKeys(): void public function testTKeyedListNonListKeys(): void
{ {
$this->expectExceptionMessage('A list shape cannot describe a non-list!'); $this->expectExceptionMessage('A list shape cannot describe a non-list');
Type::parseString('list{1: 1, 2: 2}'); Type::parseString('list{1: 1, 2: 2}');
} }
public function testTKeyedListNoExplicitAndImplicitKeys(): void public function testTKeyedListNoExplicitAndImplicitKeys(): void
{ {
$this->expectExceptionMessage('Cannot mix explicit and implicit keys!'); $this->expectExceptionMessage('Cannot mix explicit and implicit keys');
Type::parseString('list{0: 0, 1}'); Type::parseString('list{0: 0, 1}');
} }
public function testTKeyedArrayNoExplicitAndImplicitKeys(): void public function testTKeyedArrayNoExplicitAndImplicitKeys(): void
{ {
$this->expectExceptionMessage('Cannot mix explicit and implicit keys!'); $this->expectExceptionMessage('Cannot mix explicit and implicit keys');
Type::parseString('array{0, test: 1}'); Type::parseString('array{0, test: 1}');
} }
public function testTKeyedArrayNoDuplicateKeys(): void
{
$this->expectExceptionMessage('Duplicate key a detected');
Type::parseString('array{a: int, a: int}');
}
public function testSimpleCallable(): void public function testSimpleCallable(): void
{ {
$this->assertSame( $this->assertSame(