Repository: phpactor/phpactor (php)

Issue:
completion doesn't trigger after the \ character
```php
<?php
\print<>
```

Pull request description:
Support completion for absolute names
Fixes #1852

Patch:
diff --git a/lib/Completion/Bridge/TolerantParser/ReferenceFinder/ExpressionNameCompletor.php b/lib/Completion/Bridge/TolerantParser/ReferenceFinder/ExpressionNameCompletor.php
index 9d3b6e62..efac8545 100644
--- a/lib/Completion/Bridge/TolerantParser/ReferenceFinder/ExpressionNameCompletor.php
+++ b/lib/Completion/Bridge/TolerantParser/ReferenceFinder/ExpressionNameCompletor.php
@@ -49,9 +49,10 @@ class ExpressionNameCompletor extends CoreNameSearcherCompletor implements Toler
     protected function createSuggestionOptions(
         NameSearchResult $result,
         ?TextDocumentUri $sourceUri = null,
-        ?Node $node = null
+        ?Node $node = null,
+        bool $wasAbsolute = false
     ): array {
-        $suggestionOptions = parent::createSuggestionOptions($result, $sourceUri, $node);
+        $suggestionOptions = parent::createSuggestionOptions($result, $sourceUri, $node, $wasAbsolute);
 
         if ($this->isNonObjectCreationClassResult($result, $node) ||
             !$this->snippetFormatter->canFormat($result)) {
diff --git a/lib/Completion/Bridge/TolerantParser/TypeSuggestionProvider.php b/lib/Completion/Bridge/TolerantParser/TypeSuggestionProvider.php
index 101bd96e..76019e8f 100644
--- a/lib/Completion/Bridge/TolerantParser/TypeSuggestionProvider.php
+++ b/lib/Completion/Bridge/TolerantParser/TypeSuggestionProvider.php
@@ -52,9 +52,10 @@ class TypeSuggestionProvider
                 continue;
             }
 
+            $wasAbsolute = strpos($search, '\\') === 0;
             yield Suggestion::createWithOptions($result->name()->head(), [
                 'short_description' => $result->name()->__toString(),
-                'name_import' => $result->name()->__toString(),
+                'name_import' => $wasAbsolute ? null : $result->name()->__toString(),
                 'type' => Suggestion::TYPE_CLASS,
                 'priority' => Suggestion::PRIORITY_MEDIUM,
             ]);
diff --git a/lib/Completion/Bridge/TolerantParser/WorseReflection/DoctrineAnnotationCompletor.php b/lib/Completion/Bridge/TolerantParser/WorseReflection/DoctrineAnnotationCompletor.php
index 701266c8..d589f84f 100644
--- a/lib/Completion/Bridge/TolerantParser/WorseReflection/DoctrineAnnotationCompletor.php
+++ b/lib/Completion/Bridge/TolerantParser/WorseReflection/DoctrineAnnotationCompletor.php
@@ -72,9 +72,10 @@ class DoctrineAnnotationCompletor extends NameSearcherCompletor implements Compl
     protected function createSuggestionOptions(
         NameSearchResult $result,
         ?TextDocumentUri $sourceUri = null,
-        ?Node $node = null
+        ?Node $node = null,
+        bool $wasAbsolute = false
     ): array {
-        return array_merge(parent::createSuggestionOptions($result, null, $node), [
+        return array_merge(parent::createSuggestionOptions($result, null, $node, $wasAbsolute), [
             'snippet' => (string) $result->name()->head() .'($1)$0',
         ]);
     }
@@ -131,7 +132,7 @@ class DoctrineAnnotationCompletor extends NameSearcherCompletor implements Compl
     private function extractAnnotation(string $truncatedSource): ?string
     {
         $count = 0;
-        $annotation = preg_replace('/.*@([^@\s\t*]+)$/s', '$1', $truncatedSource, 1, $count);
+        $annotation = preg_replace('/.*@([^\\@\s\t*]+)$/s', '$1', $truncatedSource, 1, $count);
 
         if (0 === $count) {
             return null;
diff --git a/lib/Completion/Core/Completor/NameSearcherCompletor.php b/lib/Completion/Core/Completor/NameSearcherCompletor.php
index c65614b0..e51be617 100644
--- a/lib/Completion/Core/Completor/NameSearcherCompletor.php
+++ b/lib/Completion/Core/Completor/NameSearcherCompletor.php
@@ -24,13 +24,11 @@ abstract class NameSearcherCompletor
         $this->prioritizer = $prioritizer ?: new DefaultResultPrioritizer();
     }
 
-    /**
-     * @return Generator<Suggestion>
-     */
     protected function completeName(string $name, ?TextDocumentUri $sourceUri = null, ?Node $node = null): Generator
     {
         foreach ($this->nameSearcher->search($name) as $result) {
-            $options = $this->createSuggestionOptions($result, $sourceUri, $node);
+            $wasAbsolute = strpos($name, '\\') === 0;
+            $options = $this->createSuggestionOptions($result, $sourceUri, $node, $wasAbsolute);
             yield $this->createSuggestion(
                 $result,
                 $node,
@@ -41,6 +39,9 @@ abstract class NameSearcherCompletor
         return true;
     }
 
+    /**
+     * @param array<string,string> $options
+     */
     protected function createSuggestion(NameSearchResult $result, ?Node $node = null, array $options = []): Suggestion
     {
         $options = array_merge($this->createSuggestionOptions($result, null, $node), $options);
@@ -55,7 +56,7 @@ abstract class NameSearcherCompletor
     /**
      * @return array<string,mixed>
      */
-    protected function createSuggestionOptions(NameSearchResult $result, ?TextDocumentUri $sourceUri = null, ?Node $node = null): array
+    protected function createSuggestionOptions(NameSearchResult $result, ?TextDocumentUri $sourceUri = null, ?Node $node = null, bool $wasAbsolute = false): array
     {
         $options = [
             'short_description' => $result->name()->__toString(),
@@ -65,8 +66,7 @@ abstract class NameSearcherCompletor
             'priority' => $this->prioritizer->priority($result->uri(), $sourceUri)
         ];
 
-
-        if ($node === null || !($node->getParent() instanceof NamespaceUseClause)) {
+        if (!$wasAbsolute && ($node === null || !($node->getParent() instanceof NamespaceUseClause))) {
             $options['class_import'] = $this->classImport($result);
             $options['name_import'] = $result->name()->__toString();
         }
diff --git a/lib/Indexer/Adapter/ReferenceFinder/IndexedNameSearcher.php b/lib/Indexer/Adapter/ReferenceFinder/IndexedNameSearcher.php
index 550ec35f..914bc732 100644
--- a/lib/Indexer/Adapter/ReferenceFinder/IndexedNameSearcher.php
+++ b/lib/Indexer/Adapter/ReferenceFinder/IndexedNameSearcher.php
@@ -27,10 +27,17 @@ class IndexedNameSearcher implements NameSearcher
      */
     public function search(string $name, ?string $type = null): Generator
     {
-        if (false === PhpNameMatcher::isPhpName($name)) {
+        if (false === PhpNameMatcher::isPhpFqn($name)) {
             return;
         }
-        $criteria = Criteria::shortNameBeginsWith($name);
+
+        $fullyQualified = substr($name, 0, 1) === '\\';
+
+        if ($fullyQualified) {
+            $criteria = Criteria::fqnBeginsWith(substr($name, 1));
+        } else {
+            $criteria = Criteria::shortNameBeginsWith($name);
+        }
 
         $typeCriteria = $this->resolveTypeCriteria($type);
 
diff --git a/lib/Indexer/Util/PhpNameMatcher.php b/lib/Indexer/Util/PhpNameMatcher.php
index 2f4efa6d..0692ae2b 100644
--- a/lib/Indexer/Util/PhpNameMatcher.php
+++ b/lib/Indexer/Util/PhpNameMatcher.php
@@ -9,4 +9,9 @@ class PhpNameMatcher
         // https://www.php.net/manual/en/language.oop5.basic.php
         return (bool)preg_match('{^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$}', $name);
     }
+
+    public static function isPhpFqn(string $name): bool
+    {
+        return (bool)preg_match('{^[a-zA-Z_\x80-\xff\\][\\a-zA-Z0-9_\x80-\xff]*$}', $name);
+    }
 }


Test patch:
diff --git a/lib/Completion/Tests/Integration/Bridge/TolerantParser/ReferenceFinder/ExpressionNameCompletorTest.php b/lib/Completion/Tests/Integration/Bridge/TolerantParser/ReferenceFinder/ExpressionNameCompletorTest.php
index cb2203da..573ff04a 100644
--- a/lib/Completion/Tests/Integration/Bridge/TolerantParser/ReferenceFinder/ExpressionNameCompletorTest.php
+++ b/lib/Completion/Tests/Integration/Bridge/TolerantParser/ReferenceFinder/ExpressionNameCompletorTest.php
@@ -55,6 +55,15 @@ class ExpressionNameCompletorTest extends TolerantCompletorTestCase
                 ]
             ]
         ];
+        yield 'absolute class typehint' => [
+            '<?php class Foobar { public function __construct() {} } :int {}; \Fo<>', [
+                [
+                    'type'              => Suggestion::TYPE_CLASS,
+                    'name'              => 'FOO',
+                    'short_description' => 'FOO',
+                ]
+            ]
+        ];
 
         yield 'function' => [
             '<?php function bar(int $foo) {}; ba<>', [
@@ -94,6 +103,9 @@ class ExpressionNameCompletorTest extends TolerantCompletorTestCase
         $searcher->search('FO')->willYield([
             NameSearchResult::create('constant', 'FOO')
         ]);
+        $searcher->search('\Fo')->willYield([
+            NameSearchResult::create('class', 'FOO')
+        ]);
         $searcher->search('Foo')->willYield([
             NameSearchResult::create('class', 'Foobar')
         ]);
diff --git a/lib/Indexer/Tests/Adapter/ReferenceFinder/IndexedNameSearcherTest.php b/lib/Indexer/Tests/Adapter/ReferenceFinder/IndexedNameSearcherTest.php
index f471c4d4..839d5017 100644
--- a/lib/Indexer/Tests/Adapter/ReferenceFinder/IndexedNameSearcherTest.php
+++ b/lib/Indexer/Tests/Adapter/ReferenceFinder/IndexedNameSearcherTest.php
@@ -9,6 +9,20 @@ use Phpactor\ReferenceFinder\Search\NameSearchResult;
 
 class IndexedNameSearcherTest extends IndexTestCase
 {
+    public function testSearcherWithAbsolute(): void
+    {
+        $this->workspace()->put('project/Foobar.php', '<?php class Foobar {}');
+        $this->workspace()->put('project/Barfoo.php', '<?php namespace Bar; class Foobar {}');
+        $this->workspace()->put('project/Barfoo.php', '<?php namespace Foo; class Foobar {}');
+        $agent = $this->indexAgent();
+        $agent->indexer()->getJob()->run();
+        $searcher = new IndexedNameSearcher($agent->search());
+
+        $results = iterator_to_array($searcher->search('\Foo'));
+
+        self::assertCount(2, $results, 'Returns both root class name and namespace match');
+    }
+
     public function testSearcher(): void
     {
         $this->workspace()->put('project/Foobar.php', '<?php class Foobar {}');