Repository: phpactor/phpactor (php)

Issue:
completion: parameter suggestions should take into account type of parameter
e.g. 

```
function foo(LoggerInterface $logger) {}
```

only instances of `LoggerInterface` should be suggested for `foo(<>` or `foo(new <>` (in addition to variables etc)

Pull request description:
Gh 2419 completion param context
This PR will narrow completion suggestions to the parameter being completed.

Fixes #2419 

Note this is a relatively risky change in a butterfly-flaps-its-wings-and-everything-is-broken kind of way - so I'll merge it after releasing.

Before:

![image](https://github.com/phpactor/phpactor/assets/530801/b056fe71-7dc3-4f50-8002-87c475a594d8)

After:

![image](https://github.com/phpactor/phpactor/assets/530801/670a483e-97e6-4394-afb1-1f4a113e4937)

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 228d69b7..cea98a46 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,21 @@
 Changelog
 =========
 
+## master
+
+Features:
+
+  - Completion suggestions filtered by accepting type #2456
+
+Imporvements:
+
+  - Skip parent parameters on complete constructor #2471 @mamazu
+
+Bug fixes:
+
+  - Fix `$this` undefined var false positive in anon. class #2469 @mamazu
+  - Fix `$argv` undefined var false positives #2468 @mamazu
+
 ## 2023-12-03
 
 Bug fixes:
diff --git a/lib/Completion/Bridge/TolerantParser/ReferenceFinder/ExpressionNameCompletor.php b/lib/Completion/Bridge/TolerantParser/ReferenceFinder/ExpressionNameCompletor.php
index a7749e02..4e0fbc00 100644
--- a/lib/Completion/Bridge/TolerantParser/ReferenceFinder/ExpressionNameCompletor.php
+++ b/lib/Completion/Bridge/TolerantParser/ReferenceFinder/ExpressionNameCompletor.php
@@ -5,6 +5,7 @@ namespace Phpactor\Completion\Bridge\TolerantParser\ReferenceFinder;
 use Generator;
 use Microsoft\PhpParser\Node;
 use Microsoft\PhpParser\Node\Expression\ObjectCreationExpression;
+use Microsoft\PhpParser\Node\Expression\ScopedPropertyAccessExpression;
 use Microsoft\PhpParser\Node\QualifiedName;
 use Phpactor\Completion\Bridge\TolerantParser\CompletionContext;
 use Phpactor\Completion\Bridge\TolerantParser\TolerantCompletor;
@@ -38,10 +39,7 @@ class ExpressionNameCompletor extends CoreNameSearcherCompletor implements Toler
             return true;
         }
 
-        $name = $node->__toString();
-        if ($node instanceof QualifiedName && NameUtil::isQualified($name)) {
-            $name = NameUtil::toFullyQualified((string)$node->getResolvedName());
-        }
+        $name = $this->resolveName($node);
 
         $suggestions = $this->completeName($name, $source->uri(), $node);
 
@@ -89,4 +87,17 @@ class ExpressionNameCompletor extends CoreNameSearcherCompletor implements Toler
 
         return !($parent instanceof ObjectCreationExpression);
     }
+
+    private function resolveName(Node $node): string
+    {
+        if ($node instanceof ScopedPropertyAccessExpression) {
+            $token = $node->memberName;
+            return (string)$token->getText($node->getFileContents());
+        }
+        $name = $node instanceof QualifiedName ? $node->__toString() : '';
+        if ($node instanceof QualifiedName && NameUtil::isQualified($name)) {
+            $name = NameUtil::toFullyQualified((string)$node->getResolvedName());
+        }
+        return $name ?: '';
+    }
 }
diff --git a/lib/Completion/Bridge/TolerantParser/TolerantArrayCompletor.php b/lib/Completion/Bridge/TolerantParser/TolerantArrayCompletor.php
new file mode 100644
index 00000000..ca6d82d3
--- /dev/null
+++ b/lib/Completion/Bridge/TolerantParser/TolerantArrayCompletor.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Phpactor\Completion\Bridge\TolerantParser;
+
+use Generator;
+use Microsoft\PhpParser\Node;
+use Phpactor\Completion\Core\Suggestion;
+use Phpactor\TextDocument\ByteOffset;
+use Phpactor\TextDocument\TextDocument;
+
+class TolerantArrayCompletor implements TolerantCompletor
+{
+    /**
+     * @param Suggestion[] $suggestions
+     */
+    public function __construct(private array $suggestions)
+    {
+    }
+
+    public function complete(Node $node, TextDocument $source, ByteOffset $offset): Generator
+    {
+        yield from $this->suggestions;
+
+        return true;
+    }
+}
diff --git a/lib/Completion/Bridge/TolerantParser/WorseReflection/DoctrineAnnotationCompletor.php b/lib/Completion/Bridge/TolerantParser/WorseReflection/DoctrineAnnotationCompletor.php
index bd85fa04..a517f8a0 100644
--- a/lib/Completion/Bridge/TolerantParser/WorseReflection/DoctrineAnnotationCompletor.php
+++ b/lib/Completion/Bridge/TolerantParser/WorseReflection/DoctrineAnnotationCompletor.php
@@ -118,12 +118,12 @@ class DoctrineAnnotationCompletor extends NameSearcherCompletor implements Compl
 
     private function isAnAnnotation(Suggestion $suggestion): bool
     {
-        if (null === $suggestion->classImport()) {
+        if (null === $suggestion->nameImport()) {
             return false;
         }
 
         try {
-            $reflectionClass = $this->reflector->reflectClass($suggestion->classImport());
+            $reflectionClass = $this->reflector->reflectClass($suggestion->nameImport());
             $docblock = $reflectionClass->docblock();
 
             return str_contains($docblock->raw(), '@Annotation');
diff --git a/lib/Completion/Bridge/TolerantParser/WorseReflection/ImportedNameCompletor.php b/lib/Completion/Bridge/TolerantParser/WorseReflection/ImportedNameCompletor.php
index 7f6fa497..b99c94b7 100644
--- a/lib/Completion/Bridge/TolerantParser/WorseReflection/ImportedNameCompletor.php
+++ b/lib/Completion/Bridge/TolerantParser/WorseReflection/ImportedNameCompletor.php
@@ -46,6 +46,7 @@ class ImportedNameCompletor implements TolerantCompletor, TolerantQualifiable
                 [
                     'type' => Suggestion::TYPE_CLASS,
                     'short_description' => sprintf('%s', $resolvedName->__toString()),
+                    'fqn' => $resolvedName->__toString(),
                 ]
             );
         }
diff --git a/lib/Completion/Bridge/WorseReflection/Completor/ContextSensitiveCompletor.php b/lib/Completion/Bridge/WorseReflection/Completor/ContextSensitiveCompletor.php
new file mode 100644
index 00000000..328b797c
--- /dev/null
+++ b/lib/Completion/Bridge/WorseReflection/Completor/ContextSensitiveCompletor.php
@@ -0,0 +1,106 @@
+<?php
+
+namespace Phpactor\Completion\Bridge\WorseReflection\Completor;
+
+use Generator;
+use Microsoft\PhpParser\Node;
+use Microsoft\PhpParser\Node\DelimitedList\ArgumentExpressionList;
+use Microsoft\PhpParser\Node\Expression\ArgumentExpression;
+use Microsoft\PhpParser\Node\Expression\CallExpression;
+use Microsoft\PhpParser\Node\QualifiedName;
+use Phpactor\Completion\Bridge\TolerantParser\TolerantCompletor;
+use Phpactor\Completion\Core\Suggestion;
+use Phpactor\TextDocument\ByteOffset;
+use Phpactor\TextDocument\TextDocument;
+use Phpactor\WorseReflection\Bridge\TolerantParser\Reflection\ReflectionMethodCall;
+use Phpactor\WorseReflection\Core\Exception\NotFound;
+use Phpactor\WorseReflection\Core\Type\ArrayType;
+use Phpactor\WorseReflection\Core\Type\ClassLikeType;
+use Phpactor\WorseReflection\Core\Util\NodeUtil;
+use Phpactor\WorseReflection\Reflector;
+
+class ContextSensitiveCompletor implements TolerantCompletor
+{
+    public function __construct(private TolerantCompletor $inner, private Reflector $reflector)
+    {
+    }
+
+    public function complete(Node $node, TextDocument $source, ByteOffset $offset): Generator
+    {
+        $generator = $this->inner->complete($node, $source, $offset);
+
+        $argumentNb = 0;
+        $callExpression = $node;
+        if ($node instanceof QualifiedName) {
+            $callExpression = null;
+            $argumentExpression = $node->getFirstAncestor(ArgumentExpression::class);
+            if ($argumentExpression instanceof ArgumentExpression) {
+                $list = $argumentExpression->getFirstAncestor(ArgumentExpressionList::class);
+                if (!$list instanceof ArgumentExpressionList) {
+                    return $this->yieldFrom($generator);
+                }
+                $argumentNb = NodeUtil::argumentOffset($list, $argumentExpression) ?? 0;
+                $callExpression = $list->parent;
+            }
+        }
+        if ($node instanceof ArgumentExpressionList) {
+            $argumentNb = count(iterator_to_array($node->getValues()));
+            $callExpression = $node->parent;
+        }
+
+        if (!$callExpression instanceof CallExpression) {
+            return $this->yieldFrom($generator);
+        }
+
+        $callExpression = $this->reflector->reflectNode($source, $callExpression->openParen->getStartPosition());
+        if (!$callExpression instanceof ReflectionMethodCall) {
+            return $this->yieldFrom($generator);
+        }
+        $functionLike = $callExpression->method();
+        $parameters = $functionLike->parameters();
+        $parameter = $parameters->at($argumentNb);
+        if (null === $parameter) {
+            return $this->yieldFrom($generator);
+        }
+
+        $type = $parameter->type();
+        if ($parameter->isVariadic()) {
+            if ($type instanceof ArrayType) {
+                $type = $type->iterableValueType();
+            }
+        }
+        if (!$type instanceof ClassLikeType) {
+            return $this->yieldFrom($generator);
+        }
+
+        foreach ($generator as $suggestion) {
+            $fqn = $suggestion->fqn();
+            if (!$fqn) {
+                yield $suggestion;
+                continue;
+            }
+
+            try {
+                $refection = $this->reflector->reflectClass($fqn);
+            } catch (NotFound $e) {
+                continue;
+            }
+
+            if (!$refection->isInstanceOf($type->name())) {
+                continue;
+            }
+            yield $suggestion;
+        }
+
+        return $generator->getReturn();
+    }
+
+    /**
+     * @param Generator<Suggestion> $generator
+     */
+    private function yieldFrom(Generator $generator): Generator
+    {
+        yield from $generator;
+        return $generator->getReturn();
+    }
+}
diff --git a/lib/Completion/Core/Completor/NameSearcherCompletor.php b/lib/Completion/Core/Completor/NameSearcherCompletor.php
index 76f9132b..984dcf58 100644
--- a/lib/Completion/Core/Completor/NameSearcherCompletor.php
+++ b/lib/Completion/Core/Completor/NameSearcherCompletor.php
@@ -65,9 +65,11 @@ abstract class NameSearcherCompletor
 
         if ($node !== null && $wasQualified) {
             $name = NameUtil::relativeToSearch(ltrim($search, '\\'), $result->name()->__toString());
+            /** @phpstan-ignore-next-line */
             return Suggestion::createWithOptions($name, $options);
         }
 
+        /** @phpstan-ignore-next-line */
         return Suggestion::createWithOptions($result->name()->head(), $options);
     }
 
diff --git a/lib/Completion/Core/Suggestion.php b/lib/Completion/Core/Suggestion.php
index 0691eee9..45dd79bd 100644
--- a/lib/Completion/Core/Suggestion.php
+++ b/lib/Completion/Core/Suggestion.php
@@ -52,7 +52,8 @@ class Suggestion
         $documentation = null,
         private ?Range $range = null,
         private ?string $snippet = null,
-        private ?int $priority = null
+        private ?int $priority = null,
+        private ?string $fqn = null,
     ) {
         $this->shortDescription = $shortDescription;
         $this->label = $label ?: $name;
@@ -71,6 +72,7 @@ class Suggestion
      *   type?:string|null,
      *   class_import?:string|null,
      *   name_import?:string|null,
+     *   fqn?:string|null,
      *   label?:string|null,
      *   range?:Range|null,
      *   snippet?:string|null,
@@ -85,6 +87,7 @@ class Suggestion
             'type' => null,
             'class_import' => null,
             'name_import' => null,
+            'fqn' => null,
             'label' => null,
             'range' => null,
             'snippet' => null,
@@ -111,6 +114,7 @@ class Suggestion
             $options['range'],
             $options['snippet'],
             $options['priority'],
+            $options['fqn'],
         );
     }
 
@@ -128,6 +132,7 @@ class Suggestion
             'documentation' => $this->documentation(),
             'class_import' => $this->type() === self::TYPE_CLASS && $this->nameImport ? $this->nameImport : null,
             'name_import' => $this->nameImport,
+            'fqn' => $this->fqn,
             'range' => $this->range ? $this->range->toArray() : null,
 
             // removed
@@ -171,9 +176,9 @@ class Suggestion
     }
 
     /**
-     * @deprecated Use nameImport instead
+     * Return the FQN if the name should be imported.
      */
-    public function classImport(): ?string
+    public function nameImport(): ?string
     {
         return $this->type() === self::TYPE_CLASS ? $this->nameImport : null;
     }
@@ -183,7 +188,7 @@ class Suggestion
      */
     public function fqn(): ?string
     {
-        return $this->nameImport;
+        return $this->fqn ?? $this->nameImport;
     }
 
     public function label(): string
diff --git a/lib/Extension/CompletionWorse/CompletionWorseExtension.php b/lib/Extension/CompletionWorse/CompletionWorseExtension.php
index a5bbe75e..ca27d42e 100644
--- a/lib/Extension/CompletionWorse/CompletionWorseExtension.php
+++ b/lib/Extension/CompletionWorse/CompletionWorseExtension.php
@@ -23,6 +23,7 @@ use Phpactor\Completion\Bridge\TolerantParser\WorseReflection\WorseDeclaredClass
 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\Completor\ContextSensitiveCompletor;
 use Phpactor\Completion\Bridge\WorseReflection\Formatter\ClassFormatter;
 use Phpactor\Completion\Bridge\WorseReflection\Formatter\ConstantFormatter;
 use Phpactor\Completion\Bridge\WorseReflection\Formatter\EnumCaseFormatter;
@@ -61,6 +62,7 @@ use Phpactor\MapResolver\Resolver;
 use Phpactor\Container\Container;
 use Phpactor\ReferenceFinder\NameSearcher;
 use Phpactor\WorseReflection\Core\Reflector\SourceCodeReflector;
+use Phpactor\WorseReflection\Reflector;
 use RuntimeException;
 
 class CompletionWorseExtension implements Extension
@@ -285,8 +287,8 @@ class CompletionWorseExtension implements Extension
             'imported_names' => [
                 'Completion for names imported into the current namespace',
                 function (Container $container) {
-                    return new ImportedNameCompletor(
-                    );
+                    return $this->contextCompletor($container, new ImportedNameCompletor(
+                    ));
                 },
             ],
             'worse_parameter' => [
@@ -383,13 +385,13 @@ class CompletionWorseExtension implements Extension
             'expression_name_search' => [
                 'Completion for class names, constants and functions at expression positions that are located in the index',
                 function (Container $container) {
-                    return $this->limitCompletor($container, new ExpressionNameCompletor(
+                    return $this->contextCompletor($container, $this->limitCompletor($container, new ExpressionNameCompletor(
                         $container->get(NameSearcher::class),
                         new ObjectFormatter(
                             $container->get(self::SERVICE_COMPLETION_WORSE_SNIPPET_FORMATTERS)
                         ),
                         $container->get(DocumentPrioritizer::class)
-                    ));
+                    )));
                 },
             ],
             'use' => [
@@ -420,7 +422,7 @@ class CompletionWorseExtension implements Extension
                 },
             ],
             'type' => [
-                'Completion for types',
+                'Completion for scalar types',
                 function (Container $container) {
                     return $this->limitCompletor($container, new TypeCompletor(
                         $container->get(TypeSuggestionProvider::class)
@@ -467,4 +469,12 @@ class CompletionWorseExtension implements Extension
 
         return new LimitingCompletor($completor, $limit);
     }
+
+    private function contextCompletor(Container $container, TolerantCompletor $tolerantCompletor): TolerantCompletor
+    {
+        return new ContextSensitiveCompletor(
+            $tolerantCompletor,
+            $container->expect(WorseReflectionExtension::SERVICE_REFLECTOR, Reflector::class)
+        );
+    }
 }
diff --git a/lib/Extension/LanguageServerCompletion/Handler/CompletionHandler.php b/lib/Extension/LanguageServerCompletion/Handler/CompletionHandler.php
index c563f33a..86363696 100644
--- a/lib/Extension/LanguageServerCompletion/Handler/CompletionHandler.php
+++ b/lib/Extension/LanguageServerCompletion/Handler/CompletionHandler.php
@@ -198,7 +198,7 @@ class CompletionHandler implements Handler, CanRegisterCapabilities
         Suggestion $suggestion,
         CompletionParams $params
     ): NameImporterResult {
-        $suggestionNameImport = $suggestion->fqn();
+        $suggestionNameImport = $suggestion->nameImport();
 
         if (!$suggestionNameImport) {
             return NameImporterResult::createEmptyResult();
@@ -248,7 +248,7 @@ class CompletionHandler implements Handler, CanRegisterCapabilities
     private function formatShortDescription(Suggestion $suggestion): string
     {
         $prefix = '';
-        if ($suggestion->classImport()) {
+        if ($suggestion->nameImport()) {
             $prefix = '↓ ';
         }
 
diff --git a/lib/WorseReflection/Bridge/TolerantParser/Reflection/AbstractReflectionMethodCall.php b/lib/WorseReflection/Bridge/TolerantParser/Reflection/AbstractReflectionMethodCall.php
index d99331bd..659c1b90 100644
--- a/lib/WorseReflection/Bridge/TolerantParser/Reflection/AbstractReflectionMethodCall.php
+++ b/lib/WorseReflection/Bridge/TolerantParser/Reflection/AbstractReflectionMethodCall.php
@@ -10,6 +10,7 @@ use Phpactor\WorseReflection\Core\Exception\CouldNotResolveNode;
 use Phpactor\WorseReflection\Core\Exception\ItemNotFound;
 use Phpactor\WorseReflection\Core\Reflection\ReflectionClass;
 use Phpactor\WorseReflection\Core\Reflection\ReflectionFunctionLike;
+use Phpactor\WorseReflection\Core\Reflection\ReflectionMethod;
 use Phpactor\WorseReflection\Core\Reflection\ReflectionMethodCall as CoreReflectionMethodCall;
 use Phpactor\TextDocument\ByteOffsetRange;
 use Phpactor\WorseReflection\Core\Reflection\ReflectionClassLike;
@@ -89,11 +90,17 @@ abstract class AbstractReflectionMethodCall implements CoreReflectionMethodCall
     }
 
 
+    public function method(): ReflectionMethod
+    {
+        $class = $this->class();
+        return $class->methods()->get($this->name());
+    }
+
     public function inferredReturnType(): Type
     {
         $return = $this->node->getFirstAncestor(ReturnStatement::class);
         if ($return) {
-            $functionLike = $this->functionLike();
+            $functionLike = $this->containingFunctionLike();
             if (null === $functionLike) {
                 return new MissingType();
             }
@@ -125,7 +132,7 @@ abstract class AbstractReflectionMethodCall implements CoreReflectionMethodCall
         return $this->node->parent;
     }
 
-    private function functionLike(): ?ReflectionFunctionLike
+    private function containingFunctionLike(): ?ReflectionFunctionLike
     {
         $method = $this->node->getFirstAncestor(MethodDeclaration::class);
         if ($method instanceof MethodDeclaration) {
diff --git a/lib/WorseReflection/Core/Inference/NodeReflector.php b/lib/WorseReflection/Core/Inference/NodeReflector.php
index 9e783865..8cde21e6 100644
--- a/lib/WorseReflection/Core/Inference/NodeReflector.php
+++ b/lib/WorseReflection/Core/Inference/NodeReflector.php
@@ -39,6 +39,9 @@ class NodeReflector
         if ($node instanceof ObjectCreationExpression) {
             return $this->reflectObjectCreationExpression($frame, $node);
         }
+        if ($node instanceof CallExpression) {
+            return $this->reflectCallExpression($frame, $node);
+        }
 
         if ($node instanceof MatchExpression) {
             return $this->reflectMatchExpression($frame, $node);
@@ -127,4 +130,20 @@ class NodeReflector
             $node
         );
     }
+
+    private function reflectCallExpression(Frame $frame, CallExpression $node): ReflectionNode
+    {
+        if ($node->callableExpression instanceof MemberAccessExpression) {
+            return new ReflectionMethodCall(
+                $this->services,
+                $frame,
+                $node->callableExpression
+            );
+        }
+
+        throw new CouldNotResolveNode(sprintf(
+            'Did not know how to reflect node of type "%s"',
+            get_class($node)
+        ));
+    }
 }


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 0a62a65b..5557c642 100644
--- a/lib/Completion/Tests/Integration/Bridge/TolerantParser/ReferenceFinder/ExpressionNameCompletorTest.php
+++ b/lib/Completion/Tests/Integration/Bridge/TolerantParser/ReferenceFinder/ExpressionNameCompletorTest.php
@@ -199,6 +199,15 @@ class ExpressionNameCompletorTest extends TolerantCompletorTestCase
                 ],
             ],
         ];
+        yield 'bare call' => [
+            '<?php class Foobar {public function bar(Baz $b) {}} $f = new Foobar; $f->bar(<>', [
+                [
+                    'type'              => Suggestion::TYPE_CLASS,
+                    'name'              => 'EmptyString',
+                    'short_description' => 'EmptyString',
+                ],
+            ],
+        ];
         yield 'php tag' => [
             '<?ph<>', [
             ],
@@ -217,7 +226,7 @@ class ExpressionNameCompletorTest extends TolerantCompletorTestCase
         $searcher->search('sel', null)->willYield([
             NameSearchResult::create('class', 'self')
         ]);
-        $searcher->search('self::F', null)->willYield([
+        $searcher->search('F', null)->willYield([
             NameSearchResult::create('constant', 'FOOCONST')
         ]);
         $searcher->search('\Fo', null)->willYield([
@@ -235,6 +244,9 @@ class ExpressionNameCompletorTest extends TolerantCompletorTestCase
         $searcher->search('b', null)->willYield([
             NameSearchResult::create('class', 'Foo\\Bar'),
         ]);
+        $searcher->search('', null)->willYield([
+            NameSearchResult::create('class', 'EmptyString'),
+        ]);
         $searcher->search('\NS1\Foo\Fo', null)->willYield([
             NameSearchResult::create('class', 'Foobar')
         ]);
diff --git a/lib/Completion/Tests/Unit/Bridge/WorseReflection/Completor/ContextSensitiveCompletorTest.php b/lib/Completion/Tests/Unit/Bridge/WorseReflection/Completor/ContextSensitiveCompletorTest.php
new file mode 100644
index 00000000..141aa98e
--- /dev/null
+++ b/lib/Completion/Tests/Unit/Bridge/WorseReflection/Completor/ContextSensitiveCompletorTest.php
@@ -0,0 +1,194 @@
+<?php
+
+namespace Phpactor\Completion\Tests\Unit\Bridge\WorseReflection\Completor;
+
+use Generator;
+use Microsoft\PhpParser\Parser;
+use PHPUnit\Framework\TestCase;
+use Phpactor\Completion\Bridge\TolerantParser\TolerantArrayCompletor;
+use Phpactor\Completion\Bridge\WorseReflection\Completor\ContextSensitiveCompletor;
+use Phpactor\Completion\Core\Suggestion;
+use Phpactor\TestUtils\ExtractOffset;
+use Phpactor\TextDocument\ByteOffset;
+use Phpactor\TextDocument\TextDocumentBuilder;
+use Phpactor\WorseReflection\ReflectorBuilder;
+
+class ContextSensitiveCompletorTest extends TestCase
+{
+    /**
+     * @dataProvider provideComplete
+     * @param string[] $suggestions
+     * @param string[] $expected
+     */
+    public function testComplete(array $suggestions, string $source, array $expected): void
+    {
+        [$source, $offset] = ExtractOffset::fromSource($source);
+        $node = (new Parser())->parseSourceFile($source);
+        $node = $node->getDescendantNodeAtPosition($offset);
+        $reflector = ReflectorBuilder::create()->addSource($source)->build();
+        $inner = new TolerantArrayCompletor(array_map(
+            fn (string $name) => Suggestion::createWithOptions($name, ['name_import' => $name]),
+            $suggestions
+        ));
+        $suggestions = iterator_to_array((new ContextSensitiveCompletor(
+            $inner,
+            $reflector
+        ))->complete(
+            $node,
+            TextDocumentBuilder::create($source)->build(),
+            ByteOffset::fromInt($offset)
+        ));
+        self::assertEquals($expected, array_map(
+            fn (Suggestion $suggestion) => $suggestion->fqn(),
+            $suggestions
+        ));
+    }
+    /**
+     * @return Generator<string,array{array<int,string>,string,array<int,string>}>
+     */
+    public static function provideComplete(): Generator
+    {
+        yield 'method call' => [
+            [
+                'Bar\Foo',
+                'Bar\Obj',
+            ],
+            <<<'EOT'
+                <?php
+                namespace Bar;
+                class Obj {}
+                class Foo { public function bar(Obj $obj){}}
+
+                $f = new Foo();
+                $f->bar(<>)
+                EOT,
+            [
+                'Bar\Obj',
+            ],
+        ];
+        yield 'namespaced open method call' => [
+            [
+                'Bar\Foo',
+                'Bar\Obj',
+            ],
+            <<<'EOT'
+                <?php
+                namespace Bar;
+                class Obj {}
+                class Foo { public function bar(Obj $obj){}}
+
+                $f = new Foo();
+                $f->bar(<>
+                EOT,
+            [
+                'Bar\Obj',
+            ],
+        ];
+        yield 'no namespace' => [
+            [
+                'Foo',
+                'Obj',
+            ],
+            <<<'EOT'
+                <?php
+                class Obj {}
+                class Foo { public function bar(Obj $obj){}}
+
+                $f = new Foo();
+                $f->bar(<>
+                EOT,
+            [
+                'Obj',
+            ],
+        ];
+        yield 'partial' => [
+            [
+                'Foo',
+                'Obj',
+            ],
+            <<<'EOT'
+                <?php
+                class Obj {}
+                class Foo { public function bar(Obj $obj){}}
+
+                $f = new Foo();
+                $f->bar(O<>
+                EOT,
+            [
+                'Obj',
+            ],
+        ];
+        yield '2nd arg' => [
+            [
+                'Obj',
+                'Baz',
+            ],
+            <<<'EOT'
+                <?php
+                class Obj {}
+                class Baz {}
+                class Foo { public function bar(Obj $obj, Baz $baz){}}
+
+                $f = new Foo();
+                $f->bar(Obj::new(),<>
+                EOT,
+            [
+                'Baz',
+            ],
+        ];
+        yield '2nd arg partial' => [
+            [
+                'Obj',
+                'Baz',
+            ],
+            <<<'EOT'
+                <?php
+                class Obj {}
+                class Baz {}
+                class Foo { public function bar(Obj $obj, Baz $baz){}}
+
+                $f = new Foo();
+                $f->bar(Obj::new(),B<>
+                EOT,
+            [
+                'Baz',
+            ],
+        ];
+        yield 'variadic' => [
+            [
+                'Obj',
+                'Baz',
+            ],
+            <<<'EOT'
+                <?php
+                class Obj {}
+                class Baz {}
+                class Foo { public function bar(Obj $obj, Baz ...$baz){}}
+
+                $f = new Foo();
+                $f->bar(Obj::new(),B<>
+                EOT,
+            [
+                'Baz',
+            ],
+        ];
+        yield 'new' => [
+            [
+                'Obj',
+                'Baz',
+            ],
+            <<<'EOT'
+                <?php
+                class Obj {}
+                class Baz {}
+                class Foo { public function bar(Obj $obj, Baz ...$baz){}}
+
+                $f = new Foo();
+                $f->bar(new O<>
+                EOT,
+            [
+                'Obj',
+            ],
+        ];
+    }
+}
diff --git a/lib/Completion/Tests/Unit/Core/SuggestionTest.php b/lib/Completion/Tests/Unit/Core/SuggestionTest.php
index 25fa9fee..f803e7d6 100644
--- a/lib/Completion/Tests/Unit/Core/SuggestionTest.php
+++ b/lib/Completion/Tests/Unit/Core/SuggestionTest.php
@@ -30,7 +30,7 @@ class SuggestionTest extends TestCase
         $this->assertEquals('hello', $suggestion->name());
         $this->assertEquals('hallo', $suggestion->label());
         $this->assertEquals('Foobar', $suggestion->shortDescription());
-        $this->assertEquals('Namespace\\Foobar', $suggestion->classImport());
+        $this->assertEquals('Namespace\\Foobar', $suggestion->nameImport());
         $this->assertEquals('Namespace\\Foobar', $suggestion->fqn());
     }
 
@@ -49,6 +49,7 @@ class SuggestionTest extends TestCase
             'class_import' => 'Namespace\\Foobar',
             'documentation' => 'foo',
             'label' => 'hallo',
+            'fqn' => null,
             'range' => Range::fromStartAndEnd(1, 2),
             'snippet' => null,
         ]);
@@ -64,6 +65,7 @@ class SuggestionTest extends TestCase
             'info' => '',
             'snippet' => null,
             'name_import' => 'Namespace\\Foobar',
+            'fqn' => null,
         ], $suggestion->toArray());
     }
 }