Repository: phpactor/phpactor (php)

Issue:
Symfony container autocomplete options are not shown from ContainerAwareInterface
My setup is nvim and neoclide/coc.nvim plugin.
I can see suggestions correctly:
```
$container = $this->container;
$container->get(' 
```
I do not see suggestions:
`$this->container->get('`

Example code:

BaseController.php
```
 <?php    
namespace Grounds\Framework;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; 
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
  
class BaseController implements ContainerAwareInterface    
{    
    use ContainerAwareTrait;
    //(...)
}
```
Index.php
```
<?php      
      
namespace Grounds\Controller;     
      
use Grounds\Framework\BaseController;              
use Symfony\Component\HttpFoundation\Response;      
                                                            
class Index extends BaseController                                      
{                                                                   
    public function indexAction(): Response                
    {                                                               
        //does not autocomplete:    
        $this->container->get('validator.post');                                                                             
                                                                                                                             
        //works ok:                                                                                                          
        $container = $this->container;                                                                                       
        $container->get('validator.post');                                                                                   
                                                                                                                            
        //(...)                                                                                                              
                    }      
```
![phpactor_works](https://user-images.githubusercontent.com/12596541/200917381-51192ad4-d284-4b2e-8801-a1769f528250.png)
![phpactor_doesnt_work](https://user-images.githubusercontent.com/12596541/200917384-ec304688-7245-4eed-8e28-166dc9bed1fc.png)

Pull request description:
Gh 1942: Fix container completion on open quote
This fixes the quotation completion for the Symfont DI container, and artificially "fixes" the AST when the last char is an unterminated open quote.

Fixes #1942 

Patch:
diff --git a/lib/Completion/Bridge/TolerantParser/ChainTolerantCompletor.php b/lib/Completion/Bridge/TolerantParser/ChainTolerantCompletor.php
index 33908243..aa4f545a 100644
--- a/lib/Completion/Bridge/TolerantParser/ChainTolerantCompletor.php
+++ b/lib/Completion/Bridge/TolerantParser/ChainTolerantCompletor.php
@@ -71,9 +71,18 @@ class ChainTolerantCompletor implements Completor
         // determine the last non-whitespace _character_ offset
         $characterOffset = OffsetHelper::lastNonWhitespaceCharacterOffset($truncatedSource);
 
+        $lastChar = mb_substr($source, $characterOffset - 1, 1);
+
         // truncate the source at the character offset
         $truncatedSource = mb_substr($source, 0, $characterOffset);
 
+        // the parser will get very confused if there is an unterminated open quote,
+        // if the very last non-whitepsace char is a quote, then add another to
+        // create a string literal.
+        if ($lastChar === '\'' || $lastChar === '"') {
+            $truncatedSource .= $lastChar;
+        }
+
         return $truncatedSource;
     }
 
diff --git a/lib/Extension/Symfony/Completor/SymfonyContainerCompletor.php b/lib/Extension/Symfony/Completor/SymfonyContainerCompletor.php
index 63eeb19b..d98c48bf 100644
--- a/lib/Extension/Symfony/Completor/SymfonyContainerCompletor.php
+++ b/lib/Extension/Symfony/Completor/SymfonyContainerCompletor.php
@@ -6,8 +6,7 @@ use Generator;
 use Microsoft\PhpParser\Node;
 use Microsoft\PhpParser\Node\Expression\CallExpression;
 use Microsoft\PhpParser\Node\Expression\MemberAccessExpression;
-use Microsoft\PhpParser\Node\SourceFileNode;
-use Microsoft\PhpParser\Node\Statement\CompoundStatementNode;
+use Microsoft\PhpParser\Node\StringLiteral;
 use Phpactor\Completion\Bridge\TolerantParser\TolerantCompletor;
 use Phpactor\Completion\Core\Suggestion;
 use Phpactor\Extension\Symfony\Model\SymfonyContainerInspector;
@@ -34,22 +33,17 @@ class SymfonyContainerCompletor implements TolerantCompletor
 
     public function complete(Node $node, TextDocument $source, ByteOffset $offset): Generator
     {
-        if ($node instanceof SourceFileNode || $node instanceof CompoundStatementNode) {
-            $node = NodeUtil::firstDescendantNodeBeforeOffset($node, $offset->toInt());
+        $inQuote = false;
+        if ($node instanceof StringLiteral && $node->parent->parent) {
+            $inQuote = true;
+            $node = $node->getFirstAncestor(CallExpression::class);
         }
 
-        $memberAccessExpression = null;
-        $inQuote = false;
         if (!$node instanceof CallExpression) {
-            if ($node->parent instanceof MemberAccessExpression) {
-                $memberAccessExpression = $node->parent;
-                $inQuote = true;
-            } else {
-                return;
-            }
+            return;
         }
 
-        $memberAccess = $memberAccessExpression ?: ($node instanceof CallExpression ? $node->callableExpression : null);
+        $memberAccess = $node->callableExpression;
 
         if (!$memberAccess instanceof MemberAccessExpression) {
             return;
diff --git a/lib/WorseReflection/Core/Util/NodeUtil.php b/lib/WorseReflection/Core/Util/NodeUtil.php
index 94544390..34034050 100644
--- a/lib/WorseReflection/Core/Util/NodeUtil.php
+++ b/lib/WorseReflection/Core/Util/NodeUtil.php
@@ -179,10 +179,11 @@ class NodeUtil
     {
         $out = [
             sprintf(
-                '%s %d:%d',
+                '%s %d:%d - %s',
                 str_repeat('  ', $level) . $node->getNodeKindName(),
                 $node->getStartPosition(),
-                $node->getEndPosition()
+                $node->getEndPosition(),
+                str_replace("\n", '\\n', $node->getText()),
             )
         ];
 
@@ -291,6 +292,25 @@ class NodeUtil
 
         return $node;
     }
+    public static function lastDescendantNodeBeforeOffsetOfType(Node $node, int $offset, string $fqn): Node
+    {
+        $best = null;
+        $bestPos = null;
+        foreach ($node->getDescendantNodes() as $descendant) {
+            if (!$descendant instanceof $fqn) {
+                continue;
+            }
+            if ($descendant->getEndPosition() > $offset) {
+                continue;
+            }
+            if (null === $bestPos || $descendant->getEndPosition() > $bestPos) {
+                $best = $descendant;
+                $bestPos = $descendant->getEndPosition();
+            }
+        }
+
+        return $best ?? $node;
+    }
 
     public static function previousSibling(Node $node): ?Node
     {


Test patch:
diff --git a/lib/Extension/Symfony/Tests/Integration/Completor/SymfonyContainerCompletorTest.php b/lib/Extension/Symfony/Tests/Integration/Completor/SymfonyContainerCompletorTest.php
index cd538c1e..23c74b5c 100644
--- a/lib/Extension/Symfony/Tests/Integration/Completor/SymfonyContainerCompletorTest.php
+++ b/lib/Extension/Symfony/Tests/Integration/Completor/SymfonyContainerCompletorTest.php
@@ -6,6 +6,7 @@ use Closure;
 use Generator;
 use Microsoft\PhpParser\Parser;
 use PHPUnit\Framework\TestCase;
+use Phpactor\Completion\Bridge\TolerantParser\ChainTolerantCompletor;
 use Phpactor\Completion\Bridge\TolerantParser\TolerantCompletor;
 use Phpactor\Completion\Core\Suggestion;
 use Phpactor\Extension\Symfony\Completor\SymfonyContainerCompletor;
@@ -28,9 +29,8 @@ class SymfonyContainerCompletorTest extends TestCase
     public function testComplete(string $source, array $services, Closure $assertion): void
     {
         [$source, $start] = ExtractOffset::fromSource($source);
-        $node = (new Parser())->parseSourceFile($source)->getDescendantNodeAtPosition((int)$start);
-        $suggestions = iterator_to_array($this->completor($source, $services, [])->complete(
-            $node,
+        $completor = new ChainTolerantCompletor([$this->completor($source, $services, [])]);
+        $suggestions = iterator_to_array($completor->complete(
             TextDocumentBuilder::create($source)->language('php')->build(),
             ByteOffset::fromInt((int)$start)
         ));
@@ -157,7 +157,9 @@ class SymfonyContainerCompletorTest extends TestCase
             ,
             /** @param Suggestion[] $suggestions */
             function (array $suggestions): void {
-                self::assertCount(2, $suggestions);
+                // parser doesnt like this, and we already pass all the
+                // suggestions on the open quote, so irrelevant
+                self::assertCount(0, $suggestions);
             }
         ];
 
@@ -167,7 +169,7 @@ class SymfonyContainerCompletorTest extends TestCase
 
                 use Symfony\Component\DependencyInjection\Container;
                 $container = new Container();
-                $foobar = $container->get('foo<>
+                $foobar = $container->get('<>
 
                 EOT
             ,
@@ -277,9 +279,7 @@ class SymfonyContainerCompletorTest extends TestCase
             ,
             /** @param Suggestion[] $suggestions */
             function (array $suggestions): void {
-                // unfotunately the parser breaks in this case and we are
-                // unable to see that we are on a get() method....
-                self::assertCount(0, $suggestions);
+                self::assertCount(1, $suggestions);
             }
         ];
     }