Repository: phpactor/phpactor (php)

Issue:
Support nested array shape
eg:
```php
/** @var array{user:array{name:string,age:int}} $data */
$a['user'][// autocomplete ]
```

Pull request:
Gh 2339: Support for array key completion
Fixes #2339

https://github.com/phpactor/phpactor/assets/530801/0da62373-82bb-4f3c-afc0-867fff12b18c



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b6bf86a5..7aadd564 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,10 @@ Changelog
 
 ## master
 
+Features:
+
+  - Subscript array-shape completion #2339
+
 Improvements:
 
   - Do not promote parameters that are used in parent constructor #2119 @mamazu
diff --git a/lib/Completion/Bridge/TolerantParser/WorseReflection/WorseSubscriptCompletor.php b/lib/Completion/Bridge/TolerantParser/WorseReflection/WorseSubscriptCompletor.php
new file mode 100644
index 00000000..8107ce8f
--- /dev/null
+++ b/lib/Completion/Bridge/TolerantParser/WorseReflection/WorseSubscriptCompletor.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace Phpactor\Completion\Bridge\TolerantParser\WorseReflection;
+
+use Generator;
+use Microsoft\PhpParser\Node;
+use Microsoft\PhpParser\Node\Expression\SubscriptExpression;
+use Phpactor\Completion\Bridge\TolerantParser\TolerantCompletor;
+use Phpactor\Completion\Core\Suggestion;
+use Phpactor\TextDocument\ByteOffset;
+use Phpactor\TextDocument\TextDocument;
+use Phpactor\WorseReflection\Core\Reflector\SourceCodeReflector;
+use Phpactor\WorseReflection\Core\Type\ArrayShapeType;
+
+class WorseSubscriptCompletor implements TolerantCompletor
+{
+    public function __construct(private SourceCodeReflector $reflector)
+    {
+    }
+
+    public function complete(Node $node, TextDocument $source, ByteOffset $offset): Generator
+    {
+        if (false === $this->couldComplete($node, $source, $offset)) {
+            return true;
+        }
+
+        $offset = $this->reflector->reflectOffset($source, $node->getEndPosition());
+        $type = $offset->nodeContext()->type();
+
+        if (!$type instanceof ArrayShapeType) {
+            return true;
+        }
+
+        foreach ($type->keys() as $key) {
+            yield Suggestion::createWithOptions(sprintf('[\'%s\']', (string)$key), [
+                'type' => Suggestion::TYPE_FIELD,
+                'short_description' => $type->typeAtOffset($key)->__toString(),
+            ]);
+        }
+
+        return true;
+    }
+
+    private function couldComplete(Node $node = null, TextDocument $source, ByteOffset $offset): bool
+    {
+        return $node instanceof SubscriptExpression;
+    }
+}
diff --git a/lib/Extension/CompletionWorse/CompletionWorseExtension.php b/lib/Extension/CompletionWorse/CompletionWorseExtension.php
index 30821ca9..a5bbe75e 100644
--- a/lib/Extension/CompletionWorse/CompletionWorseExtension.php
+++ b/lib/Extension/CompletionWorse/CompletionWorseExtension.php
@@ -22,6 +22,7 @@ use Phpactor\Completion\Bridge\TolerantParser\WorseReflection\WorseConstructorCo
 use Phpactor\Completion\Bridge\TolerantParser\WorseReflection\WorseDeclaredClassCompletor;
 use Phpactor\Completion\Bridge\TolerantParser\WorseReflection\WorseSignatureHelper;
 use Phpactor\Completion\Bridge\TolerantParser\WorseReflection\DocblockCompletor;
+use Phpactor\Completion\Bridge\TolerantParser\WorseReflection\WorseSubscriptCompletor;
 use Phpactor\Completion\Bridge\WorseReflection\Formatter\ClassFormatter;
 use Phpactor\Completion\Bridge\WorseReflection\Formatter\ConstantFormatter;
 use Phpactor\Completion\Bridge\WorseReflection\Formatter\EnumCaseFormatter;
@@ -59,6 +60,7 @@ use Phpactor\Extension\WorseReflection\WorseReflectionExtension;
 use Phpactor\MapResolver\Resolver;
 use Phpactor\Container\Container;
 use Phpactor\ReferenceFinder\NameSearcher;
+use Phpactor\WorseReflection\Core\Reflector\SourceCodeReflector;
 use RuntimeException;
 
 class CompletionWorseExtension implements Extension
@@ -345,6 +347,14 @@ class CompletionWorseExtension implements Extension
                     );
                 },
             ],
+            'subscript' => [
+                'Completion for subscript (array access from array shapes)',
+                function (Container $container) {
+                    return new WorseSubscriptCompletor(
+                        $container->expect(WorseReflectionExtension::SERVICE_REFLECTOR, SourceCodeReflector::class),
+                    );
+                },
+            ],
             'declared_function' => [
                 'Completion for functions defined in the Phpactor runtime',
                 function (Container $container) {
diff --git a/lib/Extension/LanguageServerCompletion/Handler/CompletionHandler.php b/lib/Extension/LanguageServerCompletion/Handler/CompletionHandler.php
index 5c17a46c..fd1ab128 100644
--- a/lib/Extension/LanguageServerCompletion/Handler/CompletionHandler.php
+++ b/lib/Extension/LanguageServerCompletion/Handler/CompletionHandler.php
@@ -147,6 +147,7 @@ class CompletionHandler implements Handler, CanRegisterCapabilities
             ':',
             '>',
             '$',
+            '[',
             '@',
             '(',
             '\'',
diff --git a/lib/WorseReflection/Core/Type/ArrayShapeType.php b/lib/WorseReflection/Core/Type/ArrayShapeType.php
index bc830d59..16bb7c92 100644
--- a/lib/WorseReflection/Core/Type/ArrayShapeType.php
+++ b/lib/WorseReflection/Core/Type/ArrayShapeType.php
@@ -62,6 +62,14 @@ class ArrayShapeType extends ArrayType implements Generalizable, ArrayAccessType
         return new MissingType();
     }
 
+    /**
+     * @return array-key[]
+     */
+    public function keys(): array
+    {
+        return array_keys($this->typeMap);
+    }
+
     public function map(Closure $mapper): Type
     {
         return new self(


Test patch:
diff --git a/lib/Completion/Tests/Integration/Bridge/TolerantParser/WorseReflection/WorseSubscriptCompletorTest.php b/lib/Completion/Tests/Integration/Bridge/TolerantParser/WorseReflection/WorseSubscriptCompletorTest.php
new file mode 100644
index 00000000..3a8e0ae3
--- /dev/null
+++ b/lib/Completion/Tests/Integration/Bridge/TolerantParser/WorseReflection/WorseSubscriptCompletorTest.php
@@ -0,0 +1,81 @@
+<?php
+
+namespace Phpactor\Completion\Tests\Integration\Bridge\TolerantParser\WorseReflection;
+
+use Phpactor\Completion\Bridge\TolerantParser\TolerantCompletor;
+use Phpactor\Completion\Bridge\TolerantParser\WorseReflection\WorseSubscriptCompletor;
+use Phpactor\Completion\Core\Suggestion;
+use Phpactor\Completion\Tests\Integration\Bridge\TolerantParser\TolerantCompletorTestCase;
+use Generator;
+use Phpactor\TextDocument\TextDocument;
+use Phpactor\WorseReflection\ReflectorBuilder;
+
+class WorseSubscriptCompletorTest extends TolerantCompletorTestCase
+{
+    /**
+     * @dataProvider provideComplete
+     * @param array<int,mixed> $expected
+     */
+    public function testComplete(string $source, array $expected): void
+    {
+        $this->assertComplete($source, $expected);
+    }
+
+    /**
+     * @dataProvider provideCouldNotComplete
+     */
+    public function testCouldNotComplete(string $source): void
+    {
+        $this->assertCouldNotComplete($source);
+    }
+
+    /**
+     * @return Generator<string,array{string}>
+     */
+    public function provideCouldNotComplete(): Generator
+    {
+        yield 'empty string' => [ '<?php  <>' ];
+        yield 'function call' => [ '<?php echo<>' ];
+        yield 'variable with space' => [ '<?php $foo <>' ];
+        yield 'static variable' => ['<?php Foobar::$<>'];
+    }
+
+    /**
+     * @return Generator<array{string,array<int,array<string,string>>}>
+     */
+    public function provideComplete(): Generator
+    {
+        yield 'variable' => [
+            '<?php $foo<>', []
+        ];
+
+        yield 'subscript with no type' => [
+            '<?php $foo[<>', []
+        ];
+        yield 'subscript with type' => [
+            '<?php /** @var array{foo:string} $foo */$foo[<>', [
+                [
+                    'type' => Suggestion::TYPE_FIELD,
+                    'name' => '[\'foo\']',
+                    'short_description' => 'string',
+                ]
+            ]
+        ];
+        yield 'nested subscript with type' => [
+            '<?php /** @var array{foo:array{one:int}} $foo */$foo[\'foo\'][<>', [
+                [
+                    'type' => Suggestion::TYPE_FIELD,
+                    'name' => '[\'one\']',
+                    'short_description' => 'int',
+                ]
+            ]
+        ];
+    }
+
+
+    protected function createTolerantCompletor(TextDocument $source): TolerantCompletor
+    {
+        $reflector = ReflectorBuilder::create()->addSource($source)->build();
+        return new WorseSubscriptCompletor($reflector);
+    }
+}