Repository: phpactor/phpactor (php)

Issue:
Support subtype of array
`list` is a subtype of `array<int, mixed>`.
`list<T>` is a subtype of `array<int, T>`.
https://psalm.dev/docs/annotating_code/type_syntax/array_types/#lists

Pull request:
Gh 1666 support `list`
Fixes #1666 

Patch:
diff --git a/doc/reference/types.rst b/doc/reference/types.rst
index 6acc2bae..e22def36 100644
--- a/doc/reference/types.rst
+++ b/doc/reference/types.rst
@@ -22,7 +22,7 @@ Basic Types
     Boolean          ``bool``            ``*``      ✔
     Float            ``float``           ``*``      ✔
     Int              ``int``             ``*``      ✔
-    Resource         (internal type)     ``*``      ✘
+    Resource         (internal type)     ``*``      ✔
     String           ``string``          ``*``      ✔
     Self             ``self``            ``*``      ✔
     Parent           ``parent``          ``*``      🤷
@@ -68,6 +68,7 @@ Docblock Types
     Float Literal    ``1234.12``                     ✔ 
     String Literal   ``"hello"``                     ✔ 
     Parenthesized    ``(Foo&Bar)|object``            ✔ 
+    List             ``list<string>``                ✔ 
     ===============  ==============================  ========
 
 Integer Types
diff --git a/lib/DocblockParser/Ast/Type/ListBracketsNode.php b/lib/DocblockParser/Ast/Type/ListBracketsNode.php
new file mode 100644
index 00000000..5caa761e
--- /dev/null
+++ b/lib/DocblockParser/Ast/Type/ListBracketsNode.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Phpactor\DocblockParser\Ast\Type;
+
+use Phpactor\DocblockParser\Ast\TypeNode;
+use Phpactor\DocblockParser\Ast\Token;
+
+class ListBracketsNode extends TypeNode
+{
+    protected const CHILD_NAMES = [
+        'type',
+        'listChars',
+    ];
+    
+    public TypeNode $type;
+    
+    public Token $listChars;
+
+    public function __construct(TypeNode $type, Token $listChars)
+    {
+        $this->type = $type;
+        $this->listChars = $listChars;
+    }
+
+    public function type(): TypeNode
+    {
+        return $this->type;
+    }
+
+    public function listChars(): Token
+    {
+        return $this->listChars;
+    }
+}
diff --git a/lib/DocblockParser/Ast/Type/ListNode.php b/lib/DocblockParser/Ast/Type/ListNode.php
index ba3c2638..3ca4577f 100644
--- a/lib/DocblockParser/Ast/Type/ListNode.php
+++ b/lib/DocblockParser/Ast/Type/ListNode.php
@@ -3,32 +3,7 @@
 namespace Phpactor\DocblockParser\Ast\Type;
 
 use Phpactor\DocblockParser\Ast\TypeNode;
-use Phpactor\DocblockParser\Ast\Token;
 
 class ListNode extends TypeNode
 {
-    protected const CHILD_NAMES = [
-        'type',
-        'listChars',
-    ];
-    
-    public TypeNode $type;
-    
-    public Token $listChars;
-
-    public function __construct(TypeNode $type, Token $listChars)
-    {
-        $this->type = $type;
-        $this->listChars = $listChars;
-    }
-
-    public function type(): TypeNode
-    {
-        return $this->type;
-    }
-
-    public function listChars(): Token
-    {
-        return $this->listChars;
-    }
 }
diff --git a/lib/DocblockParser/Parser.php b/lib/DocblockParser/Parser.php
index be489e23..dc5f4915 100644
--- a/lib/DocblockParser/Parser.php
+++ b/lib/DocblockParser/Parser.php
@@ -28,6 +28,7 @@ use Phpactor\DocblockParser\Ast\TypeNode;
 use Phpactor\DocblockParser\Ast\Type\ConstantNode;
 use Phpactor\DocblockParser\Ast\Type\GenericNode;
 use Phpactor\DocblockParser\Ast\Type\IntersectionNode;
+use Phpactor\DocblockParser\Ast\Type\ListBracketsNode;
 use Phpactor\DocblockParser\Ast\Type\ListNode;
 use Phpactor\DocblockParser\Ast\Type\LiteralFloatNode;
 use Phpactor\DocblockParser\Ast\Type\LiteralIntegerNode;
@@ -297,7 +298,7 @@ final class Parser
 
         if ($this->tokens->current->type === Token::T_LIST) {
             $list = $this->tokens->chomp();
-            return new ListNode($this->createTypeFromToken($type), $list);
+            return new ListBracketsNode($this->createTypeFromToken($type), $list);
         }
 
         if ($this->tokens->current->type === Token::T_BRACKET_ANGLE_OPEN) {
@@ -352,6 +353,9 @@ final class Parser
         if (strtolower($type->value) === 'array') {
             return new ArrayNode();
         }
+        if (strtolower($type->value) === 'list') {
+            return new ListNode();
+        }
         if (in_array($type->value, self::SCALAR_TYPES)) {
             return new ScalarNode($type);
         }
diff --git a/lib/WorseReflection/Bridge/Phpactor/DocblockParser/TypeConverter.php b/lib/WorseReflection/Bridge/Phpactor/DocblockParser/TypeConverter.php
index 1addf050..0c12cc41 100644
--- a/lib/WorseReflection/Bridge/Phpactor/DocblockParser/TypeConverter.php
+++ b/lib/WorseReflection/Bridge/Phpactor/DocblockParser/TypeConverter.php
@@ -4,6 +4,7 @@ namespace Phpactor\WorseReflection\Bridge\Phpactor\DocblockParser;
 
 use Phpactor\DocblockParser\Ast\Type\ArrayShapeNode;
 use Phpactor\DocblockParser\Ast\Type\IntersectionNode;
+use Phpactor\DocblockParser\Ast\Type\ListNode;
 use Phpactor\DocblockParser\Ast\Type\LiteralFloatNode;
 use Phpactor\DocblockParser\Ast\Type\LiteralIntegerNode;
 use Phpactor\DocblockParser\Ast\Type\LiteralStringNode;
@@ -19,7 +20,7 @@ use Phpactor\DocblockParser\Ast\Type\ArrayNode;
 use Phpactor\DocblockParser\Ast\Type\CallableNode;
 use Phpactor\DocblockParser\Ast\Type\ClassNode;
 use Phpactor\DocblockParser\Ast\Type\GenericNode;
-use Phpactor\DocblockParser\Ast\Type\ListNode;
+use Phpactor\DocblockParser\Ast\Type\ListBracketsNode;
 use Phpactor\DocblockParser\Ast\Type\NullNode;
 use Phpactor\DocblockParser\Ast\Type\ScalarNode;
 use Phpactor\DocblockParser\Ast\Type\ThisNode;
@@ -40,6 +41,7 @@ use Phpactor\WorseReflection\Core\Type\IntLiteralType;
 use Phpactor\WorseReflection\Core\Type\IntType;
 use Phpactor\WorseReflection\Core\Type\IntersectionType;
 use Phpactor\WorseReflection\Core\Type\IterablePrimitiveType;
+use Phpactor\WorseReflection\Core\Type\ListType;
 use Phpactor\WorseReflection\Core\Type\MissingType;
 use Phpactor\WorseReflection\Core\Type\MixedType;
 use Phpactor\WorseReflection\Core\Type\NullType;
@@ -76,6 +78,9 @@ class TypeConverter
         if ($type instanceof ListNode) {
             return $this->convertList($type);
         }
+        if ($type instanceof ListBracketsNode) {
+            return $this->convertListBrackets($type);
+        }
         if ($type instanceof ArrayNode) {
             return $this->convertArray($type);
         }
@@ -158,9 +163,15 @@ class TypeConverter
 
     private function convertArray(ArrayNode $type): Type
     {
+        // use missing type so this is rendered as "array" and not "mixed[]"
         return new ArrayType(new ArrayKeyType(), new MissingType());
     }
 
+    private function convertList(ListNode $type): Type
+    {
+        return new ListType(new MixedType());
+    }
+
     private function convertUnion(UnionNode $union): Type
     {
         return new UnionType(...array_map(
@@ -193,7 +204,17 @@ class TypeConverter
                     $this->convert($parameters[1]),
                 );
             }
-            return new MissingType();
+            return new ArrayType(new MissingType());
+        }
+
+        if ($type->type instanceof ListNode) {
+            $parameters = array_values(iterator_to_array($type->parameters()->types()));
+            if (count($parameters) === 1) {
+                return new ListType(
+                    $this->convert($parameters[0])
+                );
+            }
+            return new ListType(new MissingType());
         }
 
         $classType = $this->convert($type->type);
@@ -254,7 +275,7 @@ class TypeConverter
         return $resolved;
     }
 
-    private function convertList(ListNode $type): Type
+    private function convertListBrackets(ListBracketsNode $type): Type
     {
         return new ArrayType($this->convert($type->type));
     }
diff --git a/lib/WorseReflection/Core/Type/ListType.php b/lib/WorseReflection/Core/Type/ListType.php
new file mode 100644
index 00000000..37168fc5
--- /dev/null
+++ b/lib/WorseReflection/Core/Type/ListType.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace Phpactor\WorseReflection\Core\Type;
+
+use Phpactor\WorseReflection\Core\Type;
+
+class ListType extends ArrayType
+{
+    public function __construct(?Type $valueType = null)
+    {
+        parent::__construct(new IntType(), $valueType ?: new MixedType());
+    }
+}
diff --git a/lib/WorseReflection/Core/TypeFactory.php b/lib/WorseReflection/Core/TypeFactory.php
index f4efa66c..019d3db4 100644
--- a/lib/WorseReflection/Core/TypeFactory.php
+++ b/lib/WorseReflection/Core/TypeFactory.php
@@ -322,6 +322,11 @@ class TypeFactory
         return new ArrayShapeType($typeMap);
     }
 
+    public static function list(?Type $iterabletype = null): ArrayType
+    {
+        return new ArrayType(self::int(), $iterabletype ?: self::mixed());
+    }
+
 
     private static function typeFromString(string $type, Reflector $reflector = null): Type
     {


Test patch:
diff --git a/lib/DocblockParser/Tests/Unit/Ast/NodeTest.php b/lib/DocblockParser/Tests/Unit/Ast/NodeTest.php
index 73c6d128..9d366c11 100644
--- a/lib/DocblockParser/Tests/Unit/Ast/NodeTest.php
+++ b/lib/DocblockParser/Tests/Unit/Ast/NodeTest.php
@@ -10,7 +10,7 @@ use Phpactor\DocblockParser\Ast\Tag\PropertyTag;
 use Phpactor\DocblockParser\Ast\Tag\ReturnTag;
 use Phpactor\DocblockParser\Ast\Type\ClassNode;
 use Phpactor\DocblockParser\Ast\Type\GenericNode;
-use Phpactor\DocblockParser\Ast\Type\ListNode;
+use Phpactor\DocblockParser\Ast\Type\ListBracketsNode;
 use Phpactor\DocblockParser\Ast\Type\ScalarNode;
 use Phpactor\DocblockParser\Ast\Type\UnionNode;
 use Prophecy\Doubler\Generator\Node\MethodNode;
@@ -110,7 +110,7 @@ class NodeTest extends NodeTestCase
         yield 'list' => [
             '@return Foo[]',
             function (ReturnTag $return): void {
-                self::assertInstanceOf(ListNode::class, $return->type);
+                self::assertInstanceOf(ListBracketsNode::class, $return->type);
             }
         ];
         yield 'generic' => [
diff --git a/lib/DocblockParser/Tests/Unit/Printer/examples/generic1.test b/lib/DocblockParser/Tests/Unit/Printer/examples/generic1.test
index 3fd85b39..2919889e 100644
--- a/lib/DocblockParser/Tests/Unit/Printer/examples/generic1.test
+++ b/lib/DocblockParser/Tests/Unit/Printer/examples/generic1.test
@@ -9,7 +9,7 @@ Docblock: =
    GenericNode: = 
     ClassNode: = Foobar<
     TypeList: = 
-     ListNode: = 
+     ListBracketsNode: = 
       ClassNode: = Barfoo[]>
    VariableNode: = $foobar
  */
diff --git a/lib/DocblockParser/Tests/Unit/Printer/examples/generic2.test b/lib/DocblockParser/Tests/Unit/Printer/examples/generic2.test
index 60b522ec..3610fbc3 100644
--- a/lib/DocblockParser/Tests/Unit/Printer/examples/generic2.test
+++ b/lib/DocblockParser/Tests/Unit/Printer/examples/generic2.test
@@ -9,7 +9,7 @@ Docblock: =
    GenericNode: = 
     ClassNode: = Foobar<
     TypeList: = 
-     ListNode: = 
+     ListBracketsNode: = 
       ClassNode: = Barfoo[],
      ScalarNode: = string>
    VariableNode: = $foobar
diff --git a/lib/DocblockParser/Tests/Unit/Printer/examples/generic4.test b/lib/DocblockParser/Tests/Unit/Printer/examples/generic4.test
index 97a8ef56..116157e6 100644
--- a/lib/DocblockParser/Tests/Unit/Printer/examples/generic4.test
+++ b/lib/DocblockParser/Tests/Unit/Printer/examples/generic4.test
@@ -12,9 +12,9 @@ Docblock: =
      GenericNode: = 
       ClassNode: = Barfoo<
       TypeList: = 
-       ListNode: = 
+       ListBracketsNode: = 
         ScalarNode: = int[],
-       ListNode: = 
+       ListBracketsNode: = 
         ScalarNode: = int[]>,
      ScalarNode: = string,
      ClassNode: = Baz>
diff --git a/lib/DocblockParser/Tests/Unit/Printer/examples/list1.test b/lib/DocblockParser/Tests/Unit/Printer/examples/list1.test
index 9ee9cb00..1d9497fa 100644
--- a/lib/DocblockParser/Tests/Unit/Printer/examples/list1.test
+++ b/lib/DocblockParser/Tests/Unit/Printer/examples/list1.test
@@ -7,12 +7,12 @@ Docblock: =
  ElementList: = /**
  * 
   ParamTag: = @param
-   ListNode: = 
+   ListBracketsNode: = 
     ClassNode: = Foobar[]
    VariableNode: = $foobar
  * 
   ParamTag: = @param
-   ListNode: = 
+   ListBracketsNode: = 
     ScalarNode: = string[]
    VariableNode: = $strings
  */
diff --git a/lib/DocblockParser/Tests/Unit/Printer/examples/var1.test b/lib/DocblockParser/Tests/Unit/Printer/examples/var1.test
index 52dbf287..947809a0 100644
--- a/lib/DocblockParser/Tests/Unit/Printer/examples/var1.test
+++ b/lib/DocblockParser/Tests/Unit/Printer/examples/var1.test
@@ -6,6 +6,6 @@ Docblock: =
  ElementList: = /**
  * 
   VarTag: = @var
-   ListNode: = 
+   ListBracketsNode: = 
     ScalarNode: = string[]
  */
diff --git a/lib/WorseReflection/Tests/Inference/type/list.test b/lib/WorseReflection/Tests/Inference/type/list.test
new file mode 100644
index 00000000..36473722
--- /dev/null
+++ b/lib/WorseReflection/Tests/Inference/type/list.test
@@ -0,0 +1,17 @@
+<?php
+
+/**
+ * @param list<string> $foo
+ */
+function listWithArgument(array $foo): void
+{
+    wrAssertType('array<int,string>', $foo);
+}
+
+/**
+ * @param list $foo
+ */
+function listWithoutArgument(array $foo): void
+{
+    wrAssertType('array<int,mixed>', $foo);
+}
diff --git a/lib/WorseReflection/Tests/Unit/Bridge/Phpactor/DocblockParser/DocblockParserFactoryTest.php b/lib/WorseReflection/Tests/Unit/Bridge/Phpactor/DocblockParser/DocblockParserFactoryTest.php
index d319d7c3..578e4406 100644
--- a/lib/WorseReflection/Tests/Unit/Bridge/Phpactor/DocblockParser/DocblockParserFactoryTest.php
+++ b/lib/WorseReflection/Tests/Unit/Bridge/Phpactor/DocblockParser/DocblockParserFactoryTest.php
@@ -7,6 +7,7 @@ use Phpactor\WorseReflection\Bridge\Phpactor\DocblockParser\DocblockParserFactor
 use Phpactor\WorseReflection\Core\DocBlock\DocBlock;
 use Phpactor\WorseReflection\Core\Reflection\ReflectionClassLike;
 use Phpactor\WorseReflection\Core\Type;
+use Phpactor\WorseReflection\Core\TypeFactory;
 use Phpactor\WorseReflection\Core\TypeResolver\ClassLikeTypeResolver;
 use Phpactor\WorseReflection\Core\TypeResolver\PassthroughTypeResolver;
 use Phpactor\WorseReflection\Core\Type\ArrayType;
@@ -42,7 +43,9 @@ class DocblockParserFactoryTest extends IntegrationTestCase
             self::assertEquals($expected, $docblock->returnType()->__toString());
             return;
         }
-        self::assertEquals($expected, $docblock->returnType());
+
+        self::assertInstanceOf(get_class($expected), $docblock->returnType());
+        self::assertEquals($expected->__toString(), $docblock->returnType()->__toString());
     }
 
     /**
@@ -189,6 +192,16 @@ class DocblockParserFactoryTest extends IntegrationTestCase
             '/** @return null|"foo"|123|123.3 */',
             'null|"foo"|123|123.3',
         ];
+
+        yield 'list' => [
+            '/** @return list */',
+            TypeFactory::list(),
+        ];
+
+        yield 'list with type' => [
+            '/** @return list<string> */',
+            TypeFactory::list(TypeFactory::string()),
+        ];
     }
 
     public function testClassConstant(): void