Repository: phpactor/phpactor (php)

Issue:
Complete attribute classes
Given #1998 and previous examples with class constant completion (#1979,  #1985) I started to find how to restrict the suggestion list to `Attribute`s only. This led me to the question which [Completor](https://github.com/phpactor/phpactor/blob/master/lib/Completion/Core/Completor.php) should be used to it: any existing or some new?

Pull request description:
Complete attribute names
#1999 

Patch:
diff --git a/doc/reference/configuration.rst b/doc/reference/configuration.rst
index 997b1490..22a1daf6 100644
--- a/doc/reference/configuration.rst
+++ b/doc/reference/configuration.rst
@@ -493,6 +493,20 @@ Enable or disable the ``class_like`` completor.
 Completion for class like contexts.
 
 
+**Default**: ``true``
+
+.. _param_completion_worse.completor.attribute.enabled:
+
+
+``completion_worse.completor.attribute.enabled``
+"""""""""""""""""""""""""""""""""""""""""""""""""
+
+
+Enable or disable the ``attribute`` completor.
+
+Completion for attribute contexts.
+
+
 **Default**: ``true``
 
 
diff --git a/lib/Completion/Bridge/TolerantParser/CompletionContext.php b/lib/Completion/Bridge/TolerantParser/CompletionContext.php
index 08f81697..5d1fbe80 100644
--- a/lib/Completion/Bridge/TolerantParser/CompletionContext.php
+++ b/lib/Completion/Bridge/TolerantParser/CompletionContext.php
@@ -6,6 +6,8 @@ use Microsoft\PhpParser\ClassLike;
 use Microsoft\PhpParser\MissingToken;
 use Microsoft\PhpParser\Node;
 use Microsoft\PhpParser\Node\ArrayElement;
+use Microsoft\PhpParser\Node\Attribute;
+use Microsoft\PhpParser\Node\AttributeGroup;
 use Microsoft\PhpParser\Node\ClassBaseClause;
 use Microsoft\PhpParser\Node\ClassInterfaceClause;
 use Microsoft\PhpParser\Node\ClassMembersNode;
@@ -55,6 +57,18 @@ class CompletionContext
         ;
     }
 
+    public static function attribute(?Node $node): bool
+    {
+        if (null === $node) {
+            return false;
+        }
+
+        return
+            $node instanceof AttributeGroup ||
+            $node instanceof Attribute ||
+            $node->parent instanceof Attribute;
+    }
+
     public static function useImport(?Node $node): bool
     {
         if (null === $node) {
diff --git a/lib/Completion/Bridge/TolerantParser/ReferenceFinder/AttributeCompletor.php b/lib/Completion/Bridge/TolerantParser/ReferenceFinder/AttributeCompletor.php
new file mode 100644
index 00000000..f613b7f4
--- /dev/null
+++ b/lib/Completion/Bridge/TolerantParser/ReferenceFinder/AttributeCompletor.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Phpactor\Completion\Bridge\TolerantParser\ReferenceFinder;
+
+use Generator;
+use Microsoft\PhpParser\Node;
+use Phpactor\Completion\Bridge\TolerantParser\CompletionContext;
+use Phpactor\Completion\Bridge\TolerantParser\TolerantCompletor;
+use Phpactor\Completion\Core\DocumentPrioritizer\DocumentPrioritizer;
+use Phpactor\Completion\Core\Suggestion;
+use Phpactor\ReferenceFinder\NameSearcher;
+use Phpactor\ReferenceFinder\NameSearcherType;
+use Phpactor\TextDocument\ByteOffset;
+use Phpactor\TextDocument\TextDocument;
+
+class AttributeCompletor implements TolerantCompletor
+{
+    public function __construct(
+        private NameSearcher $nameSearcher,
+        private DocumentPrioritizer $prioritizer
+    ) {
+    }
+
+    public function complete(Node $node, TextDocument $source, ByteOffset $offset): Generator
+    {
+        if (!CompletionContext::attribute($node)) {
+            return true;
+        }
+
+        $search = $node->getText();
+        $type = NameSearcherType::CLASS_;
+
+        foreach ($this->nameSearcher->search($search, $type) as $result) {
+            yield Suggestion::createWithOptions($result->name()->head(), [
+                'type' => Suggestion::TYPE_CLASS,
+                'priority' => $this->prioritizer->priority($result->uri(), $source->uri()),
+                'short_description' => sprintf('%s %s', $type, $result->name()->__toString()),
+                'class_import' => $result->name()->__toString(),
+                'name_import' => $result->name()->__toString(),
+            ]);
+        }
+
+        return true;
+    }
+}
diff --git a/lib/Extension/CompletionWorse/CompletionWorseExtension.php b/lib/Extension/CompletionWorse/CompletionWorseExtension.php
index de370e7b..7ed4c0b2 100644
--- a/lib/Extension/CompletionWorse/CompletionWorseExtension.php
+++ b/lib/Extension/CompletionWorse/CompletionWorseExtension.php
@@ -5,6 +5,7 @@ namespace Phpactor\Extension\CompletionWorse;
 use Closure;
 use Phpactor\Completion\Bridge\TolerantParser\DebugTolerantCompletor;
 use Phpactor\Completion\Bridge\TolerantParser\LimitingCompletor;
+use Phpactor\Completion\Bridge\TolerantParser\ReferenceFinder\AttributeCompletor;
 use Phpactor\Completion\Bridge\TolerantParser\ReferenceFinder\ClassLikeCompletor;
 use Phpactor\Completion\Bridge\TolerantParser\ReferenceFinder\ExpressionNameCompletor;
 use Phpactor\Completion\Bridge\TolerantParser\ReferenceFinder\TypeCompletor;
@@ -391,6 +392,15 @@ class CompletionWorseExtension implements Extension
                     ));
                 },
             ],
+            'attribute' => [
+                'Completion for attribute class names',
+                function (Container $container) {
+                    return $this->limitCompletor($container, new AttributeCompletor(
+                        $container->get(NameSearcher::class),
+                        $container->get(DocumentPrioritizer::class)
+                    ));
+                },
+            ],
             'class_like' => [
                 'Completion for class like contexts',
                 function (Container $container) {


Test patch:
diff --git a/lib/Completion/Tests/Unit/Bridge/TolerantParser/CompletionContextTest.php b/lib/Completion/Tests/Unit/Bridge/TolerantParser/CompletionContextTest.php
index 5eace62f..27c0ec82 100644
--- a/lib/Completion/Tests/Unit/Bridge/TolerantParser/CompletionContextTest.php
+++ b/lib/Completion/Tests/Unit/Bridge/TolerantParser/CompletionContextTest.php
@@ -178,6 +178,47 @@ class CompletionContextTest extends TestCase
         ];
     }
 
+    /**
+     * @dataProvider provideAttribute
+     */
+    public function testAttribute(string $source, bool $expected): void
+    {
+        [$source, $offset] = ExtractOffset::fromSource($source);
+        $node = (new Parser())->parseSourceFile($source)->getDescendantNodeAtPosition((int)$offset);
+        self::assertEquals($expected, CompletionContext::attribute($node));
+    }
+
+    /**
+     * @return Generator<string,array{string,bool}>
+     */
+    public function provideAttribute(): Generator
+    {
+        yield 'not attribute' => [
+            '<?php $hello<>',
+            false,
+        ];
+
+        yield 'in not mapped attribute' => [
+            '<?php #[Att<>]',
+            true,
+        ];
+
+        yield 'in not mapped attribute, empty name' => [
+            '<?php #[<>]',
+            true,
+        ];
+
+        yield 'in not mapped attribute, empty name of the second' => [
+            '<?php #[Attribute(), <>]',
+            true,
+        ];
+
+        yield 'in method attribute' => [
+            '<?php class X {#[Att<>] public function x()',
+            true,
+        ];
+    }
+
     /**
      * @dataProvider provideAnonymousUse
      */