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

View File

@ -64,11 +64,14 @@ class ParseTreeCreator
$type_token = $this->type_tokens[$this->t];
switch ($type_token[0]) {
case '<':
case '{':
case ']':
throw new TypeParseTreeException('Unexpected token ' . $type_token[0]);
case '<':
$this->handleLessThan();
break;
case '[':
$this->handleOpenSquareBracket();
break;
@ -232,6 +235,29 @@ class ParseTreeCreator
$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
{
if ($this->current_leaf instanceof Root) {

View File

@ -688,6 +688,9 @@ class TypeParser
}
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);
}
@ -1352,6 +1355,16 @@ class TypeParser
$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) {
$class_string = false;
@ -1472,12 +1485,37 @@ class TypeParser
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(
$properties,
$class_strings,
$sealed
$extra_params ?? ($sealed
? null
: [$is_list ? Type::getListKey() : Type::getArrayKey(), Type::getMixed()],
: [$is_list ? Type::getListKey() : Type::getArrayKey(), Type::getMixed()]
),
$is_list,
$from_docblock,
);
@ -1642,11 +1680,11 @@ class TypeParser
$all_sealed = true;
foreach ($intersection_types as $intersection_type) {
foreach ($intersection_type->properties as $property => $property_type) {
if ($intersection_type->fallback_params !== null) {
$all_sealed = false;
}
if ($intersection_type->fallback_params !== null) {
$all_sealed = false;
}
foreach ($intersection_type->properties as $property => $property_type) {
if (!array_key_exists($property, $properties)) {
$properties[$property] = $property_type;
continue;

View File

@ -484,12 +484,17 @@ abstract class Type
}
private static ?Union $listKey = null;
private static ?Union $listKeyFromDocblock = null;
/**
* @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)]);
}

View File

@ -202,8 +202,11 @@ class TKeyedArray extends Atomic
}
$params_part = $this->fallback_params !== null
? ', ...<' . $this->fallback_params[0]->getId($exact) . ', '
. $this->fallback_params[1]->getId($exact) . '>'
? ', ...<' . ($this->is_list
? $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 . '}';

View File

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

View File

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

View File

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

View File

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

View File

@ -105,7 +105,7 @@ class TypeCombinationTest extends TestCase
],
],
'complexArrayFallback2' => [
'list{0?: 0|a, 1?: 0|a, ...<int<0, max>, a>}',
'list{0?: 0|a, 1?: 0|a, ...<a>}',
[
'list<a>',
'list{0, 0}',
@ -639,7 +639,7 @@ class TypeCombinationTest extends TestCase
],
],
'combineNonEmptyListWithTKeyedArrayList' => [
'list{null|string, ...<int<0, max>, string>}',
'list{null|string, ...<string>}',
[
'non-empty-list<string>',
'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
{
$this->assertSame('Countable&iterable<mixed, int>&I', (string) Type::parseString('Countable&iterable<int>&I'));