Skip to content

Commit 0e3727a

Browse files
authored
Improve CompletionProvider (#412)
- Better performance - More documentation - Add field to Definition for global namespace fallback Fixes #380
1 parent 663ccd5 commit 0e3727a

File tree

64 files changed

+358
-91
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+358
-91
lines changed

Diff for: fixtures/completion/inside_namespace_and_method.php

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace MyNamespace;
4+
5+
class SomeClass
6+
{
7+
public function someMethod()
8+
{
9+
tes
10+
}
11+
}

Diff for: phpcs.xml.dist

+2
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,7 @@
77
<exclude name="PSR2.Namespaces.UseDeclaration.MultipleDeclarations"/>
88
<exclude name="PSR2.ControlStructures.ElseIfDeclaration.NotAllowed"/>
99
<exclude name="PSR2.ControlStructures.ControlStructureSpacing.SpacingAfterOpenBrace"/>
10+
<exclude name="Squiz.WhiteSpace.ControlStructureSpacing.SpacingBeforeClose"/>
11+
<exclude name="Squiz.WhiteSpace.ControlStructureSpacing.SpacingAfterOpen"/>
1012
</rule>
1113
</ruleset>

Diff for: src/CompletionProvider.php

+146-85
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ public function provideCompletion(PhpDocument $doc, Position $pos): CompletionLi
128128
// This can be made much more performant if the tree follows specific invariants.
129129
$node = $doc->getNodeAtPosition($pos);
130130

131+
// Get the node at the position under the cursor
131132
$offset = $node === null ? -1 : $pos->toOffset($node->getFileContents());
132133
if (
133134
$node !== null
@@ -148,22 +149,31 @@ public function provideCompletion(PhpDocument $doc, Position $pos): CompletionLi
148149
$node = $node->parent;
149150
}
150151

152+
// Inspect the type of expression under the cursor
153+
151154
if ($node === null || $node instanceof Node\Statement\InlineHtml || $pos == new Position(0, 0)) {
155+
// HTML, beginning of file
156+
157+
// Inside HTML and at the beginning of the file, propose <?php
152158
$item = new CompletionItem('<?php', CompletionItemKind::KEYWORD);
153159
$item->textEdit = new TextEdit(
154160
new Range($pos, $pos),
155161
stripStringOverlap($doc->getRange(new Range(new Position(0, 0), $pos)), '<?php')
156162
);
157163
$list->items[] = $item;
158-
} /*
159-
160-
VARIABLES */
161-
elseif (
162-
$node instanceof Node\Expression\Variable &&
163-
!(
164-
$node->parent instanceof Node\Expression\ScopedPropertyAccessExpression &&
165-
$node->parent->memberName === $node)
164+
165+
} elseif (
166+
$node instanceof Node\Expression\Variable
167+
&& !(
168+
$node->parent instanceof Node\Expression\ScopedPropertyAccessExpression
169+
&& $node->parent->memberName === $node
170+
)
166171
) {
172+
// Variables
173+
//
174+
// $|
175+
// $a|
176+
167177
// Find variables, parameters and use statements in the scope
168178
$namePrefix = $node->getName() ?? '';
169179
foreach ($this->suggestVariablesAtNode($node, $namePrefix) as $var) {
@@ -178,138 +188,189 @@ public function provideCompletion(PhpDocument $doc, Position $pos): CompletionLi
178188
);
179189
$list->items[] = $item;
180190
}
181-
} /*
182191

183-
MEMBER ACCESS EXPRESSIONS
184-
$a->c#
185-
$a-># */
186-
elseif ($node instanceof Node\Expression\MemberAccessExpression) {
192+
} elseif ($node instanceof Node\Expression\MemberAccessExpression) {
193+
// Member access expressions
194+
//
195+
// $a->c|
196+
// $a->|
197+
198+
// Multiple prefixes for all possible types
187199
$prefixes = FqnUtilities\getFqnsFromType(
188200
$this->definitionResolver->resolveExpressionNodeToType($node->dereferencableExpression)
189201
);
202+
203+
// Include parent classes
190204
$prefixes = $this->expandParentFqns($prefixes);
191205

206+
// Add the object access operator to only get members
192207
foreach ($prefixes as &$prefix) {
193208
$prefix .= '->';
194209
}
195-
196210
unset($prefix);
197211

212+
// Collect all definitions that match any of the prefixes
198213
foreach ($this->index->getDefinitions() as $fqn => $def) {
199214
foreach ($prefixes as $prefix) {
200215
if (substr($fqn, 0, strlen($prefix)) === $prefix && !$def->isGlobal) {
201216
$list->items[] = CompletionItem::fromDefinition($def);
202217
}
203218
}
204219
}
205-
} /*
206-
207-
SCOPED PROPERTY ACCESS EXPRESSIONS
208-
A\B\C::$a#
209-
A\B\C::#
210-
A\B\C::$#
211-
A\B\C::foo#
212-
TODO: $a::# */
213-
elseif (
220+
221+
} elseif (
214222
($scoped = $node->parent) instanceof Node\Expression\ScopedPropertyAccessExpression ||
215223
($scoped = $node) instanceof Node\Expression\ScopedPropertyAccessExpression
216224
) {
225+
// Static class members and constants
226+
//
227+
// A\B\C::$a|
228+
// A\B\C::|
229+
// A\B\C::$|
230+
// A\B\C::foo|
231+
//
232+
// TODO: $a::|
233+
234+
// Resolve all possible types to FQNs
217235
$prefixes = FqnUtilities\getFqnsFromType(
218236
$classType = $this->definitionResolver->resolveExpressionNodeToType($scoped->scopeResolutionQualifier)
219237
);
220238

239+
// Add parent classes
221240
$prefixes = $this->expandParentFqns($prefixes);
222241

242+
// Append :: operator to only get static members
223243
foreach ($prefixes as &$prefix) {
224244
$prefix .= '::';
225245
}
226-
227246
unset($prefix);
228247

248+
// Collect all definitions that match any of the prefixes
229249
foreach ($this->index->getDefinitions() as $fqn => $def) {
230250
foreach ($prefixes as $prefix) {
231251
if (substr(strtolower($fqn), 0, strlen($prefix)) === strtolower($prefix) && !$def->isGlobal) {
232252
$list->items[] = CompletionItem::fromDefinition($def);
233253
}
234254
}
235255
}
236-
} elseif (ParserHelpers\isConstantFetch($node) ||
237-
($creation = $node->parent) instanceof Node\Expression\ObjectCreationExpression ||
238-
(($creation = $node) instanceof Node\Expression\ObjectCreationExpression)) {
239-
$class = isset($creation) ? $creation->classTypeDesignator : $node;
240256

241-
$prefix = $class instanceof Node\QualifiedName
242-
? (string)PhpParser\ResolvedName::buildName($class->nameParts, $class->getFileContents())
243-
: $class->getText($node->getFileContents());
257+
} elseif (
258+
ParserHelpers\isConstantFetch($node)
259+
// Creation gets set in case of an instantiation (`new` expression)
260+
|| ($creation = $node->parent) instanceof Node\Expression\ObjectCreationExpression
261+
|| (($creation = $node) instanceof Node\Expression\ObjectCreationExpression)
262+
) {
263+
// Class instantiations, function calls, constant fetches, class names
264+
//
265+
// new MyCl|
266+
// my_func|
267+
// MY_CONS|
268+
// MyCla|
269+
270+
// The name Node under the cursor
271+
$nameNode = isset($creation) ? $creation->classTypeDesignator : $node;
272+
273+
/** The typed name */
274+
$prefix = $nameNode instanceof Node\QualifiedName
275+
? (string)PhpParser\ResolvedName::buildName($nameNode->nameParts, $nameNode->getFileContents())
276+
: $nameNode->getText($node->getFileContents());
277+
$prefixLen = strlen($prefix);
278+
279+
/** Whether the prefix is qualified (contains at least one backslash) */
280+
$isQualified = $nameNode instanceof Node\QualifiedName && $nameNode->isQualifiedName();
281+
282+
/** Whether the prefix is fully qualified (begins with a backslash) */
283+
$isFullyQualified = $nameNode instanceof Node\QualifiedName && $nameNode->isFullyQualifiedName();
284+
285+
/** The closest NamespaceDefinition Node */
286+
$namespaceNode = $node->getNamespaceDefinition();
287+
288+
/** @var string The name of the namespace */
289+
$namespacedPrefix = null;
290+
if ($namespaceNode) {
291+
$namespacedPrefix = (string)PhpParser\ResolvedName::buildName($namespaceNode->name->nameParts, $node->getFileContents()) . '\\' . $prefix;
292+
$namespacedPrefixLen = strlen($namespacedPrefix);
293+
}
294+
295+
// Get the namespace use statements
296+
// TODO: use function statements, use const statements
244297

245-
$namespaceDefinition = $node->getNamespaceDefinition();
298+
/** @var string[] $aliases A map from local alias to fully qualified name */
299+
list($aliases,,) = $node->getImportTablesForCurrentScope();
246300

247-
list($namespaceImportTable,,) = $node->getImportTablesForCurrentScope();
248-
foreach ($namespaceImportTable as $alias => $name) {
249-
$namespaceImportTable[$alias] = (string)$name;
301+
foreach ($aliases as $alias => $name) {
302+
$aliases[$alias] = (string)$name;
250303
}
251304

252-
foreach ($this->index->getDefinitions() as $fqn => $def) {
253-
$fqnStartsWithPrefix = substr($fqn, 0, strlen($prefix)) === $prefix;
254-
$fqnContainsPrefix = empty($prefix) || strpos($fqn, $prefix) !== false;
255-
if (($def->canBeInstantiated || ($def->isGlobal && !isset($creation))) && $fqnContainsPrefix) {
256-
if ($namespaceDefinition !== null && $namespaceDefinition->name !== null) {
257-
$namespacePrefix = (string)PhpParser\ResolvedName::buildName($namespaceDefinition->name->nameParts, $node->getFileContents());
258-
259-
$isAliased = false;
260-
261-
$isNotFullyQualified = !($class instanceof Node\QualifiedName) || !$class->isFullyQualifiedName();
262-
if ($isNotFullyQualified) {
263-
foreach ($namespaceImportTable as $alias => $name) {
264-
if (substr($fqn, 0, strlen($name)) === $name) {
265-
$fqn = $alias;
266-
$isAliased = true;
267-
break;
268-
}
269-
}
270-
}
271-
272-
$prefixWithNamespace = $namespacePrefix . "\\" . $prefix;
273-
$fqnMatchesPrefixWithNamespace = substr($fqn, 0, strlen($prefixWithNamespace)) === $prefixWithNamespace;
274-
$isFullyQualifiedAndPrefixMatches = !$isNotFullyQualified && ($fqnStartsWithPrefix || $fqnMatchesPrefixWithNamespace);
275-
if (!$isFullyQualifiedAndPrefixMatches && !$isAliased) {
276-
if (!array_search($fqn, array_values($namespaceImportTable))) {
277-
if (empty($prefix)) {
278-
$fqn = '\\' . $fqn;
279-
} elseif ($fqnMatchesPrefixWithNamespace) {
280-
$fqn = substr($fqn, strlen($namespacePrefix) + 1);
281-
} else {
282-
continue;
283-
}
284-
} else {
285-
continue;
286-
}
287-
}
288-
} elseif ($fqnStartsWithPrefix && $class instanceof Node\QualifiedName && $class->isFullyQualifiedName()) {
289-
$fqn = '\\' . $fqn;
305+
// If there is a prefix that does not start with a slash, suggest `use`d symbols
306+
if ($prefix && !$isFullyQualified) {
307+
foreach ($aliases as $alias => $fqn) {
308+
// Suggest symbols that have been `use`d and match the prefix
309+
if (substr($alias, 0, $prefixLen) === $prefix && ($def = $this->index->getDefinition($fqn))) {
310+
$list->items[] = CompletionItem::fromDefinition($def);
290311
}
312+
}
313+
}
291314

292-
$item = CompletionItem::fromDefinition($def);
315+
// Suggest global symbols that either
316+
// - start with the current namespace + prefix, if the Name node is not fully qualified
317+
// - start with just the prefix, if the Name node is fully qualified
318+
foreach ($this->index->getDefinitions() as $fqn => $def) {
293319

294-
$item->insertText = $fqn;
320+
$fqnStartsWithPrefix = substr($fqn, 0, $prefixLen) === $prefix;
321+
322+
if (
323+
// Exclude methods, properties etc.
324+
$def->isGlobal
325+
&& (
326+
!$prefix
327+
|| (
328+
// Either not qualified, but a matching prefix with global fallback
329+
($def->roamed && !$isQualified && $fqnStartsWithPrefix)
330+
// Or not in a namespace or a fully qualified name or AND matching the prefix
331+
|| ((!$namespaceNode || $isFullyQualified) && $fqnStartsWithPrefix)
332+
// Or in a namespace, not fully qualified and matching the prefix + current namespace
333+
|| (
334+
$namespaceNode
335+
&& !$isFullyQualified
336+
&& substr($fqn, 0, $namespacedPrefixLen) === $namespacedPrefix
337+
)
338+
)
339+
)
340+
// Only suggest classes for `new`
341+
&& (!isset($creation) || $def->canBeInstantiated)
342+
) {
343+
$item = CompletionItem::fromDefinition($def);
344+
// Find the shortest name to reference the symbol
345+
if ($namespaceNode && ($alias = array_search($fqn, $aliases, true)) !== false) {
346+
// $alias is the name under which this definition is aliased in the current namespace
347+
$item->insertText = $alias;
348+
} else if ($namespaceNode && !($prefix && $isFullyQualified)) {
349+
// Insert the global FQN with leading backslash
350+
$item->insertText = '\\' . $fqn;
351+
} else {
352+
// Insert the FQN without leading backlash
353+
$item->insertText = $fqn;
354+
}
355+
// Don't insert the parenthesis for functions
356+
// TODO return a snippet and put the cursor inside
357+
if (substr($item->insertText, -2) === '()') {
358+
$item->insertText = substr($item->insertText, 0, -2);
359+
}
295360
$list->items[] = $item;
296361
}
297362
}
298363

364+
// If not a class instantiation, also suggest keywords
299365
if (!isset($creation)) {
300366
foreach (self::KEYWORDS as $keyword) {
301-
$item = new CompletionItem($keyword, CompletionItemKind::KEYWORD);
302-
$item->insertText = $keyword . ' ';
303-
$list->items[] = $item;
367+
if (substr($keyword, 0, $prefixLen) === $prefix) {
368+
$item = new CompletionItem($keyword, CompletionItemKind::KEYWORD);
369+
$item->insertText = $keyword;
370+
$list->items[] = $item;
371+
}
304372
}
305373
}
306-
} elseif (ParserHelpers\isConstantFetch($node)) {
307-
$prefix = (string) ($node->getResolvedName() ?? PhpParser\ResolvedName::buildName($node->nameParts, $node->getFileContents()));
308-
foreach (self::KEYWORDS as $keyword) {
309-
$item = new CompletionItem($keyword, CompletionItemKind::KEYWORD);
310-
$item->insertText = $keyword . ' ';
311-
$list->items[] = $item;
312-
}
313374
}
314375

315376
return $list;

Diff for: src/Definition.php

+7
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ class Definition
4444
*/
4545
public $isGlobal;
4646

47+
/**
48+
* True if this definition is affected by global namespace fallback (global function or global constant)
49+
*
50+
* @var bool
51+
*/
52+
public $roamed;
53+
4754
/**
4855
* False for instance methods and properties
4956
*

Diff for: src/DefinitionResolver.php

+10
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,16 @@ public function createDefinitionFromNode(Node $node, string $fqn = null): Defini
193193
($node instanceof Node\ConstElement && $node->parent->parent instanceof Node\Statement\ConstDeclaration)
194194
);
195195

196+
// Definition is affected by global namespace fallback if it is a global constant or a global function
197+
$def->roamed = (
198+
$fqn !== null
199+
&& strpos($fqn, '\\') === false
200+
&& (
201+
($node instanceof Node\ConstElement && $node->parent->parent instanceof Node\Statement\ConstDeclaration)
202+
|| $node instanceof Node\Statement\FunctionDeclaration
203+
)
204+
);
205+
196206
// Static methods and static property declarations
197207
$def->isStatic = (
198208
($node instanceof Node\MethodDeclaration && $node->isStatic()) ||

0 commit comments

Comments
 (0)