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:
commit
2b68046115
@ -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>
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
|
@ -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)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 . '}';
|
||||||
|
@ -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
|
||||||
|
@ -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' => [
|
||||||
|
@ -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' => [
|
||||||
|
@ -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' => [
|
||||||
|
@ -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}',
|
||||||
|
@ -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'));
|
||||||
|
Loading…
x
Reference in New Issue
Block a user