Repository: phpactor/phpactor (php)

Issue:
docblock parser: false positive for missing docblock parsing optional keys(?)
False positive:
```
Method "provideCsiModifierKeyCode" is missing docblock return type: Generator<string,array{string,PhpTui\Term\Event\CodedKeyEvent}|array{string,PhpTui\Term\Event\FunctionKeyEvent}>
```
for:
```
     * @return Generator<array{0:string,1:?Event,2?:bool}>
```

Pull request:
gh-2427: fix array shape optional key parsing
Fixes #2427

- FAiling test for array shape with optional keys
- Support optional key
- FIx


Patch:
diff --git a/lib/DocblockParser/Ast/ArrayKeyValueNode.php b/lib/DocblockParser/Ast/ArrayKeyValueNode.php
index 99f3af5a..8d6590ae 100644
--- a/lib/DocblockParser/Ast/ArrayKeyValueNode.php
+++ b/lib/DocblockParser/Ast/ArrayKeyValueNode.php
@@ -5,12 +5,17 @@ namespace Phpactor\DocblockParser\Ast;
 class ArrayKeyValueNode extends Node
 {
     protected const CHILD_NAMES = [
+        'optional',
         'key',
         'colon',
         'type',
     ];
 
-    public function __construct(public ?Token $key, public ?Token $colon, public ?TypeNode $type)
-    {
+    public function __construct(
+        public ?Token $optional,
+        public ?Token $key,
+        public ?Token $colon,
+        public ?TypeNode $type
+    ) {
     }
 }
diff --git a/lib/DocblockParser/Ast/Tokens.php b/lib/DocblockParser/Ast/Tokens.php
index 4c5ec305..606bf37b 100644
--- a/lib/DocblockParser/Ast/Tokens.php
+++ b/lib/DocblockParser/Ast/Tokens.php
@@ -116,6 +116,17 @@ final class Tokens implements IteratorAggregate
         return false;
     }
 
+    public function ifOneOf(string ...$types): bool
+    {
+        foreach ($types as $type) {
+            if (true === $this->if($type)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
     /**
      * If the current or next non-whitespace node matches,
      * advance internal pointer and return true;
diff --git a/lib/DocblockParser/Parser.php b/lib/DocblockParser/Parser.php
index 18ba5b18..55990620 100644
--- a/lib/DocblockParser/Parser.php
+++ b/lib/DocblockParser/Parser.php
@@ -350,7 +350,7 @@ final class Parser
             assert(!is_null($open));
             $keyValues = [];
             $close = null;
-            if ($this->tokens->if(Token::T_LABEL)) {
+            if ($this->tokens->ifOneOf(Token::T_LABEL, Token::T_INTEGER)) {
                 $keyValues = $this->parseArrayKeyValues();
             }
             if ($this->tokens->if(Token::T_BRACKET_CURLY_CLOSE)) {
@@ -374,6 +374,7 @@ final class Parser
             $type = new ListBracketsNode($type, $list);
         }
 
+
         return $type;
     }
 
@@ -668,7 +669,7 @@ final class Parser
         $key = $colon = $type = null;
 
         if (
-            $this->tokens->if(Token::T_LABEL) &&
+            $this->tokens->ifOneOf(Token::T_LABEL, Token::T_INTEGER) &&
             $this->tokens->peekIs(1, Token::T_COLON)
         ) {
             $key = $this->tokens->chomp();
@@ -676,7 +677,7 @@ final class Parser
         }
 
         if (
-            $this->tokens->if(Token::T_LABEL) &&
+            $this->tokens->ifOneOf(Token::T_LABEL, Token::T_INTEGER) &&
             $this->tokens->peekIs(1, Token::T_NULLABLE) &&
             $this->tokens->peekIs(2, Token::T_COLON)
         ) {
@@ -685,12 +686,16 @@ final class Parser
             $colon = $this->tokens->chomp();
         }
 
+        $optional = null;
+        if ($this->tokens->if(Token::T_NULLABLE)) {
+            $optional = $this->tokens->chomp();
+        }
         $type = null;
-        if ($this->tokens->if(Token::T_LABEL)) {
+        if ($this->tokens->ifOneOf(Token::T_LABEL, Token::T_INTEGER)) {
             $type = $this->parseTypes();
         }
 
-        return new ArrayKeyValueNode($key, $colon, $type);
+        return new ArrayKeyValueNode($optional, $key, $colon, $type);
     }
 
     private function parseConditionalType(): TypeNode


Test patch:
diff --git a/lib/DocblockParser/Tests/Unit/Printer/examples/arrayshape5_opt.test b/lib/DocblockParser/Tests/Unit/Printer/examples/arrayshape5_opt.test
new file mode 100644
index 00000000..bb40c682
--- /dev/null
+++ b/lib/DocblockParser/Tests/Unit/Printer/examples/arrayshape5_opt.test
@@ -0,0 +1,21 @@
+/**
+ * @return Generator<array{0:string,1:?Event,2?:bool}>
+ */
+---
+Docblock: = 
+ ElementList: = /**
+ * 
+  ReturnTag: = @return
+   GenericNode: = 
+    ClassNode: = Generator<
+    TypeList: = 
+     ArrayShapeNode: = {
+      ArrayKeyValueList: = 
+       ArrayKeyValueNode: = 0:
+        ScalarNode: = string,
+       ArrayKeyValueNode: = ?1:
+        ClassNode: = Event,
+       ArrayKeyValueNode: = 2:
+        ScalarNode: = bool}>
+   TextNode: = 
+ */