1
0
mirror of https://github.com/danog/psalm.git synced 2025-01-22 05:41:20 +01:00

Merge pull request #9694 from Nicelocal/unsealed_array_generic_syntax

Implement unsealed array generic syntax
This commit is contained in:
orklah 2023-04-24 12:50:57 +02:00 committed by GitHub
commit 2b68046115
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 107 additions and 25 deletions

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<files psalm-version="dev-master@"> <files psalm-version="dev-master@88f6be1213950f29d6516eb422cf021b10bae455">
<file src="examples/TemplateChecker.php"> <file src="examples/TemplateChecker.php">
<PossiblyUndefinedIntArrayOffset> <PossiblyUndefinedIntArrayOffset>
<code><![CDATA[$comment_block->tags['variablesfrom'][0]]]></code> <code><![CDATA[$comment_block->tags['variablesfrom'][0]]]></code>
@ -439,11 +439,6 @@
<code>traverse</code> <code>traverse</code>
</ImpureMethodCall> </ImpureMethodCall>
</file> </file>
<file src="src/Psalm/Type.php">
<ImpureStaticProperty>
<code>self::$listKey</code>
</ImpureStaticProperty>
</file>
<file src="src/Psalm/Type/Atomic.php"> <file src="src/Psalm/Type/Atomic.php">
<ImpureMethodCall> <ImpureMethodCall>
<code>classExtendsOrImplements</code> <code>classExtendsOrImplements</code>

View File

@ -64,11 +64,14 @@ class ParseTreeCreator
$type_token = $this->type_tokens[$this->t]; $type_token = $this->type_tokens[$this->t];
switch ($type_token[0]) { switch ($type_token[0]) {
case '<':
case '{': case '{':
case ']': case ']':
throw new TypeParseTreeException('Unexpected token ' . $type_token[0]); throw new TypeParseTreeException('Unexpected token ' . $type_token[0]);
case '<':
$this->handleLessThan();
break;
case '[': case '[':
$this->handleOpenSquareBracket(); $this->handleOpenSquareBracket();
break; break;
@ -232,6 +235,29 @@ class ParseTreeCreator
$this->current_leaf = $new_parent_leaf; $this->current_leaf = $new_parent_leaf;
} }
private function handleLessThan(): void
{
if (!$this->current_leaf instanceof FieldEllipsis) {
throw new TypeParseTreeException('Unexpected token <');
}
$current_parent = $this->current_leaf->parent;
if (!$current_parent instanceof KeyedArrayTree) {
throw new TypeParseTreeException('Unexpected token <');
}
array_pop($current_parent->children);
$generic_leaf = new GenericTree(
'',
$current_parent,
);
$current_parent->children []= $generic_leaf;
$this->current_leaf = $generic_leaf;
}
private function handleOpenSquareBracket(): void private function handleOpenSquareBracket(): void
{ {
if ($this->current_leaf instanceof Root) { if ($this->current_leaf instanceof Root) {

View File

@ -688,6 +688,9 @@ class TypeParser
} }
if ($generic_type_value === 'list') { if ($generic_type_value === 'list') {
if (count($generic_params) > 1) {
throw new TypeParseTreeException('Too many template parameters for list');
}
return Type::getListAtomic($generic_params[0], $from_docblock); return Type::getListAtomic($generic_params[0], $from_docblock);
} }
@ -1352,6 +1355,16 @@ class TypeParser
$sealed = true; $sealed = true;
$extra_params = null;
$last_property_branch = end($parse_tree->children);
if ($last_property_branch instanceof GenericTree
&& $last_property_branch->value === ''
) {
$extra_params = $last_property_branch->children;
array_pop($parse_tree->children);
}
foreach ($parse_tree->children as $i => $property_branch) { foreach ($parse_tree->children as $i => $property_branch) {
$class_string = false; $class_string = false;
@ -1472,12 +1485,37 @@ class TypeParser
return new TArray([Type::getNever($from_docblock), Type::getNever($from_docblock)], $from_docblock); return new TArray([Type::getNever($from_docblock), Type::getNever($from_docblock)], $from_docblock);
} }
if ($extra_params) {
if ($is_list && count($extra_params) !== 1) {
throw new TypeParseTreeException('Must have exactly one extra field!');
}
if (!$is_list && count($extra_params) !== 2) {
throw new TypeParseTreeException('Must have exactly two extra fields!');
}
$final_extra_params = $is_list ? [Type::getListKey(true)] : [];
foreach ($extra_params as $child_tree) {
$child_type = self::getTypeFromTree(
$child_tree,
$codebase,
null,
$template_type_map,
$type_aliases,
$from_docblock,
);
if ($child_type instanceof Atomic) {
$child_type = new Union([$child_type]);
}
$final_extra_params []= $child_type;
}
$extra_params = $final_extra_params;
}
return new $class( return new $class(
$properties, $properties,
$class_strings, $class_strings,
$sealed $extra_params ?? ($sealed
? null ? null
: [$is_list ? Type::getListKey() : Type::getArrayKey(), Type::getMixed()], : [$is_list ? Type::getListKey() : Type::getArrayKey(), Type::getMixed()]
),
$is_list, $is_list,
$from_docblock, $from_docblock,
); );
@ -1642,11 +1680,11 @@ class TypeParser
$all_sealed = true; $all_sealed = true;
foreach ($intersection_types as $intersection_type) { foreach ($intersection_types as $intersection_type) {
foreach ($intersection_type->properties as $property => $property_type) {
if ($intersection_type->fallback_params !== null) { if ($intersection_type->fallback_params !== null) {
$all_sealed = false; $all_sealed = false;
} }
foreach ($intersection_type->properties as $property => $property_type) {
if (!array_key_exists($property, $properties)) { if (!array_key_exists($property, $properties)) {
$properties[$property] = $property_type; $properties[$property] = $property_type;
continue; continue;

View File

@ -484,12 +484,17 @@ abstract class Type
} }
private static ?Union $listKey = null; private static ?Union $listKey = null;
private static ?Union $listKeyFromDocblock = null;
/** /**
* @psalm-pure * @psalm-pure
* @psalm-suppress ImpureStaticProperty Used for caching
*/ */
public static function getListKey(): Union public static function getListKey(bool $from_docblock = false): Union
{ {
if ($from_docblock) {
return self::$listKeyFromDocblock ??= new Union([new TIntRange(0, null, true)]);
}
return self::$listKey ??= new Union([new TIntRange(0, null)]); return self::$listKey ??= new Union([new TIntRange(0, null)]);
} }

View File

@ -202,8 +202,11 @@ class TKeyedArray extends Atomic
} }
$params_part = $this->fallback_params !== null $params_part = $this->fallback_params !== null
? ', ...<' . $this->fallback_params[0]->getId($exact) . ', ' ? ', ...<' . ($this->is_list
. $this->fallback_params[1]->getId($exact) . '>' ? $this->fallback_params[1]->getId($exact)
: $this->fallback_params[0]->getId($exact) . ', '
. $this->fallback_params[1]->getId($exact)
) . '>'
: ''; : '';
return $key . '{' . implode(', ', $property_strings) . $params_part . '}'; return $key . '{' . implode(', ', $property_strings) . $params_part . '}';

View File

@ -1540,7 +1540,7 @@ class ArrayAssignmentTest extends TestCase
$x = [...$x, ...$y]; $x = [...$x, ...$y];
', ',
'assertions' => ['$x===' => 'list{int, int, ...<int<0, max>, int>}'], 'assertions' => ['$x===' => 'list{int, int, ...<int>}'],
], ],
'unpackEmptyKeepsCorrectKeys' => [ 'unpackEmptyKeepsCorrectKeys' => [
'code' => '<?php 'code' => '<?php

View File

@ -308,8 +308,8 @@ class ArrayFunctionCallTest extends TestCase
'assertions' => [ 'assertions' => [
// todo: this first type is not entirely correct // todo: this first type is not entirely correct
//'$c===' => "list{int|string, ...<int<0, max>, int|string>}", //'$c===' => "list{int|string, ...<int<0, max>, int|string>}",
'$c===' => "list{string, ...<int<0, max>, int|string>}", '$c===' => "list{string, ...<int|string>}",
'$d===' => "list{string, ...<int<0, max>, int|string>}", '$d===' => "list{string, ...<int|string>}",
], ],
], ],
'arrayMergeEmpty' => [ 'arrayMergeEmpty' => [

View File

@ -624,7 +624,7 @@ class FunctionCallTest extends TestCase
*/ */
$elements = explode(" ", $string, $limit);', $elements = explode(" ", $string, $limit);',
'assertions' => [ 'assertions' => [
'$elements' => 'list{0?: string, 1?: string, 2?: string, ...<int<0, max>, string>}', '$elements' => 'list{0?: string, 1?: string, 2?: string, ...<string>}',
], ],
], ],
'explodeWithDynamicDelimiter' => [ 'explodeWithDynamicDelimiter' => [
@ -680,7 +680,7 @@ class FunctionCallTest extends TestCase
*/ */
$elements = explode($delim, $string, $limit);', $elements = explode($delim, $string, $limit);',
'assertions' => [ 'assertions' => [
'$elements' => 'list{0?: string, 1?: string, 2?: string, ...<int<0, max>, string>}', '$elements' => 'list{0?: string, 1?: string, 2?: string, ...<string>}',
], ],
], ],
'explodeWithDynamicNonEmptyDelimiter' => [ 'explodeWithDynamicNonEmptyDelimiter' => [

View File

@ -29,7 +29,7 @@ class ReturnTypeTest extends TestCase
$result = ret(); $result = ret();
', ',
'assertions' => [ 'assertions' => [
'$result===' => 'list{0?: 0|a, 1?: 0|a, ...<int<0, max>, a>}', '$result===' => 'list{0?: 0|a, 1?: 0|a, ...<a>}',
], ],
], ],
'arrayCombineInv' => [ 'arrayCombineInv' => [
@ -46,7 +46,7 @@ class ReturnTypeTest extends TestCase
$result = ret(); $result = ret();
', ',
'assertions' => [ 'assertions' => [
'$result===' => 'list{0?: 0|a, 1?: 0|a, ...<int<0, max>, a>}', '$result===' => 'list{0?: 0|a, 1?: 0|a, ...<a>}',
], ],
], ],
'arrayCombine2' => [ 'arrayCombine2' => [

View File

@ -105,7 +105,7 @@ class TypeCombinationTest extends TestCase
], ],
], ],
'complexArrayFallback2' => [ 'complexArrayFallback2' => [
'list{0?: 0|a, 1?: 0|a, ...<int<0, max>, a>}', 'list{0?: 0|a, 1?: 0|a, ...<a>}',
[ [
'list<a>', 'list<a>',
'list{0, 0}', 'list{0, 0}',
@ -639,7 +639,7 @@ class TypeCombinationTest extends TestCase
], ],
], ],
'combineNonEmptyListWithTKeyedArrayList' => [ 'combineNonEmptyListWithTKeyedArrayList' => [
'list{null|string, ...<int<0, max>, string>}', 'list{null|string, ...<string>}',
[ [
'non-empty-list<string>', 'non-empty-list<string>',
'array{null}', 'array{null}',

View File

@ -178,6 +178,21 @@ class TypeParseTest extends TestCase
); );
} }
public function testUnsealedArray(): void
{
$this->assertSame('array{a: int, ...<string, string>}', Type::parseString('array{a: int, ...<string, string>}')->getId());
}
public function testUnsealedList(): void
{
$this->assertSame('list{int, ...<string>}', Type::parseString('list{int, ...<string>}')->getId());
}
public function testUnsealedListComplex(): void
{
$this->assertSame('list{array{a: 123}, ...<123>}', Type::parseString('list{0: array{a: 123}, ...<123>}')->getId());
}
public function testIntersectionAfterGeneric(): void public function testIntersectionAfterGeneric(): void
{ {
$this->assertSame('Countable&iterable<mixed, int>&I', (string) Type::parseString('Countable&iterable<int>&I')); $this->assertSame('Countable&iterable<mixed, int>&I', (string) Type::parseString('Countable&iterable<int>&I'));