Skip to content
This repository was archived by the owner on Mar 6, 2022. It is now read-only.

Commit cebd325

Browse files
committed
refactor to use ResolveAliasSuggestionCompletor decorator
1 parent 3eae721 commit cebd325

File tree

7 files changed

+419
-249
lines changed

7 files changed

+419
-249
lines changed

lib/Bridge/TolerantParser/ChainTolerantCompletor.php

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@
1212

1313
class ChainTolerantCompletor implements Completor
1414
{
15-
use ResolveAliasSuggestionsTrait;
16-
1715
/**
1816
* @var Parser
1917
*/
@@ -38,7 +36,8 @@ public function complete(TextDocument $source, ByteOffset $byteOffset): Generato
3836
$truncatedSource = $this->truncateSource((string) $source, $byteOffset->toInt());
3937

4038
$node = $this->parser->parseSourceFile($truncatedSource)->getDescendantNodeAtPosition(
41-
// the parser requires the byte offset, not the char offset
39+
// use strlen because the parser requires the byte offset, not the char offset
40+
// But we need to recalculate it because we removed trailing spaces when truncating
4241
strlen($truncatedSource)
4342
);
4443

@@ -55,16 +54,9 @@ public function complete(TextDocument $source, ByteOffset $byteOffset): Generato
5554
continue;
5655
}
5756

58-
$importTable = $this->getClassImportTablesForNode($completionNode);
5957
$suggestions = $tolerantCompletor->complete($completionNode, $source, $byteOffset);
6058

61-
foreach ($suggestions as $suggestion) {
62-
// Trick to avoid any BC break when converting to an array
63-
// https://www.php.net/manual/fr/language.generators.syntax.php#control-structures.yield.from
64-
foreach ($this->resolveAliasSuggestions($importTable, $suggestion) as $resolvedSuggestion) {
65-
yield $resolvedSuggestion;
66-
}
67-
}
59+
yield from $suggestions;
6860

6961
$isComplete = $isComplete && $suggestions->getReturn();
7062
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
<?php
2+
3+
namespace Phpactor\Completion\Bridge\TolerantParser;
4+
5+
use Generator;
6+
use Microsoft\PhpParser\Node;
7+
use Microsoft\PhpParser\Node\SourceFileNode;
8+
use Microsoft\PhpParser\Parser;
9+
use Microsoft\PhpParser\ResolvedName;
10+
use Phpactor\Completion\Core\Completor;
11+
use Phpactor\Completion\Core\Suggestion;
12+
use Phpactor\Completion\Core\Util\OffsetHelper;
13+
use Phpactor\TextDocument\ByteOffset;
14+
use Phpactor\TextDocument\TextDocument;
15+
16+
final class ResolveAliasSuggestionCompletor implements Completor
17+
{
18+
/**
19+
* @var Completor
20+
*/
21+
private $decorated;
22+
23+
/**
24+
* @var Parser
25+
*/
26+
private $parser;
27+
28+
public function __construct(Completor $decorated, Parser $parser = null)
29+
{
30+
$this->decorated = $decorated;
31+
$this->parser = $parser ?: new Parser();
32+
}
33+
34+
/**
35+
* {@inheritDoc}
36+
*/
37+
public function complete(TextDocument $source, ByteOffset $byteOffset): Generator
38+
{
39+
$importTable = $this->getClassImportTableAtPosition($source, $byteOffset);
40+
$suggestions = $this->decorated->complete($source, $byteOffset);
41+
42+
foreach ($suggestions as $suggestion) {
43+
$resolvedSuggestions = $this->resolveAliasSuggestions($importTable, $suggestion);
44+
45+
// Trick to avoid any BC break when converting to an array
46+
// https://www.php.net/manual/fr/language.generators.syntax.php#control-structures.yield.from
47+
foreach ($resolvedSuggestions as $resolvedSuggestion) {
48+
yield $resolvedSuggestion;
49+
}
50+
}
51+
52+
return $suggestions->getReturn();
53+
}
54+
55+
private function truncateSource(string $source, int $byteOffset): string
56+
{
57+
// truncate source at byte offset - we don't want the rest of the source
58+
// file contaminating the completion (for example `$foo($<>\n $bar =
59+
// ` will evaluate the Variable node as an expression node with a
60+
// double variable `$\n $bar = `
61+
$truncatedSource = substr($source, 0, $byteOffset);
62+
63+
// determine the last non-whitespace _character_ offset
64+
$characterOffset = OffsetHelper::lastNonWhitespaceCharacterOffset($truncatedSource);
65+
66+
// truncate the source at the character offset
67+
$truncatedSource = mb_substr($source, 0, $characterOffset);
68+
69+
return $truncatedSource;
70+
}
71+
72+
/**
73+
* Add suggestions when a class is already imported with an alias or when a relative name is abailable.
74+
*
75+
* Will update the suggestion to remove the import_name option if already imported.
76+
* Will add a suggestion if the class is imported under an alias.
77+
* Will add a suggestion if part of the namespace is imported (i.e. ORM\Column is a relative name).
78+
*
79+
* @param ResolvedName[] $importTable
80+
*
81+
* @return Suggestion[]
82+
*/
83+
private function resolveAliasSuggestions(array $importTable, Suggestion $suggestion): array
84+
{
85+
if (Suggestion::TYPE_CLASS !== $suggestion->type()) {
86+
return [$suggestion];
87+
}
88+
89+
$suggestionFqcn = $suggestion->nameImport();
90+
$originalName = $suggestion->name();
91+
$originalSnippet = $suggestion->snippet();
92+
$suggestions = [$suggestion->name() => $suggestion];
93+
94+
foreach ($importTable as $alias => $resolvedName) {
95+
$importFqcn = $resolvedName->getFullyQualifiedNameText();
96+
97+
if (0 !== strpos($suggestionFqcn, $importFqcn)) {
98+
continue;
99+
}
100+
101+
$name = $alias.substr($suggestionFqcn, strlen($importFqcn));
102+
$suggestions[$alias] = $suggestion->withoutNameImport()->withName($name);
103+
104+
if ($originalSnippet && $originalName !== $name) {
105+
$snippet = str_replace($originalName, $name, $originalSnippet);
106+
$suggestions[$alias] = $suggestions[$alias]->withSnippet($snippet);
107+
}
108+
}
109+
110+
return array_values($suggestions);
111+
}
112+
113+
/**
114+
* @return ResolvedName[]
115+
*/
116+
private function getClassImportTableAtPosition(TextDocument $source, ByteOffset $byteOffset): array
117+
{
118+
// We only need the closest node to retrieve the import table
119+
// It's not a big deal if it's not the completed node as long as it has
120+
// the same import table
121+
$node = $this->getClosestNodeAtPosition(
122+
$this->parser->parseSourceFile((string) $source),
123+
$byteOffset->toInt(),
124+
);
125+
126+
try {
127+
[$importTable] = $node->getImportTablesForCurrentScope();
128+
} catch (\Exception $e) {
129+
// If the node does not have an import table (SourceFileNode for example)
130+
$importTable = [];
131+
}
132+
133+
return $importTable;
134+
}
135+
136+
private function getClosestNodeAtPosition(SourceFileNode $sourceFileNode, int $position): Node
137+
{
138+
$lastNode = $sourceFileNode;
139+
/** @var Node $node */
140+
foreach ($sourceFileNode->getDescendantNodes() as $node) {
141+
if ($position < $node->getFullStart()) {
142+
return $lastNode;
143+
}
144+
145+
$lastNode = $node;
146+
}
147+
148+
return $lastNode;
149+
}
150+
}

lib/Bridge/TolerantParser/ResolveAliasSuggestionsTrait.php

Lines changed: 0 additions & 58 deletions
This file was deleted.

lib/Bridge/TolerantParser/WorseReflection/DoctrineAnnotationCompletor.php

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,19 @@
66
use Microsoft\PhpParser\Node;
77
use Microsoft\PhpParser\Node\SourceFileNode;
88
use Microsoft\PhpParser\Parser;
9-
use Phpactor\Completion\Bridge\TolerantParser\ResolveAliasSuggestionsTrait;
109
use Phpactor\Completion\Core\Completor;
1110
use Phpactor\Completion\Core\Completor\NameSearcherCompletor;
1211
use Phpactor\Completion\Core\Suggestion;
1312
use Phpactor\Completion\Core\Util\OffsetHelper;
1413
use Phpactor\ReferenceFinder\NameSearcher;
14+
use Phpactor\ReferenceFinder\Search\NameSearchResult;
1515
use Phpactor\TextDocument\ByteOffset;
1616
use Phpactor\TextDocument\TextDocument;
1717
use Phpactor\WorseReflection\Core\Exception\NotFound;
1818
use Phpactor\WorseReflection\Reflector;
1919

2020
class DoctrineAnnotationCompletor extends NameSearcherCompletor implements Completor
2121
{
22-
use ResolveAliasSuggestionsTrait;
23-
2422
/**
2523
* @var Reflector
2624
*/
@@ -63,23 +61,26 @@ public function complete(TextDocument $source, ByteOffset $byteOffset): Generato
6361
return true;
6462
}
6563

66-
$importTable = $this->getClassImportTablesForNode($node);
6764
$suggestions = $this->completeName($annotation);
6865

6966
foreach ($suggestions as $suggestion) {
7067
if (!$this->isAnAnnotation($suggestion)) {
7168
continue;
7269
}
7370

74-
$resolvedSuggestions = $this->resolveAliasSuggestions($importTable, $suggestion);
75-
foreach ($resolvedSuggestions as $resolvedSuggestion) {
76-
yield $resolvedSuggestion->withSnippet($resolvedSuggestion->name().'($1)$0');
77-
}
71+
yield $suggestion;
7872
}
7973

8074
return $suggestions->getReturn();
8175
}
8276

77+
protected function createSuggestionOptions(NameSearchResult $result): array
78+
{
79+
return array_merge(parent::createSuggestionOptions($result), [
80+
'snippet' => (string) $result->name()->head() .'($1)$0',
81+
]);
82+
}
83+
8384
private function truncateSource(string $source, int $byteOffset): string
8485
{
8586
// truncate source at byte offset - we don't want the rest of the source

tests/Integration/Bridge/TolerantParser/DoctrineAnnotationCompletorTest.php

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -109,43 +109,6 @@ class Foo {}
109109
]
110110
]];
111111

112-
113-
yield 'in a namespace with an import' => [
114-
<<<'EOT'
115-
<?php
116-
117-
namespace App\Annotation;
118-
119-
/**
120-
* @Annotation
121-
*/
122-
class Entity {}
123-
124-
namespace App;
125-
126-
use App\Annotation as APP;
127-
128-
/**
129-
* @Ent<>
130-
*/
131-
class Foo {}
132-
EOT
133-
, [
134-
[
135-
'type' => Suggestion::TYPE_CLASS,
136-
'name' => 'APP\Entity',
137-
'short_description' => 'App\Annotation\Entity',
138-
'snippet' => 'APP\Entity($1)$0',
139-
'name_import' => null,
140-
], [
141-
'type' => Suggestion::TYPE_CLASS,
142-
'name' => 'Entity',
143-
'short_description' => 'App\Annotation\Entity',
144-
'snippet' => 'Entity($1)$0',
145-
'name_import' => 'App\Annotation\Entity',
146-
]
147-
]];
148-
149112
yield 'annotation on a node in the middle of the AST' => [
150113
<<<'EOT'
151114
<?php

0 commit comments

Comments
 (0)