1
0
mirror of https://github.com/danog/psalm.git synced 2025-01-21 21:31:13 +01:00

Improve completion for namespaced classes

cc @joehoyle - this mainly allows us to get a correct list when the user starts typing Foo (without the new before it) inside a namespace
This commit is contained in:
Matt Brown 2021-02-14 23:25:13 -05:00
parent 6b53e79505
commit bd6efd7cf2
8 changed files with 203 additions and 36 deletions

View File

@ -1517,6 +1517,12 @@ class Codebase
int $offset,
string $file_path
) : array {
$fq_suggestion = false;
if (($type_string[1] ?? '') === '\\') {
$fq_suggestion = true;
}
$matching_classlike_names = $this->classlikes->getMatchingClassLikeNames($type_string);
$completion_items = [];
@ -1568,6 +1574,7 @@ class Codebase
);
if ($aliases
&& !$fq_suggestion
&& $aliases->namespace
&& $insertion_text === '\\' . $fq_class_name
&& $aliases->namespace_first_stmt_start

View File

@ -2007,7 +2007,11 @@ class ClassAnalyzer extends ClassLikeAnalyzer
$interface_name,
$codebase->classlikes->interfaceExists($fq_interface_name)
? $fq_interface_name
: '*' . implode('\\', $interface_name->parts)
: '*'
. ($interface_name instanceof PhpParser\Node\Name\FullyQualified
? '\\'
: $this->getNamespace() . '-')
. implode('\\', $interface_name->parts)
);
$interface_location = new CodeLocation($this, $interface_name);
@ -2392,7 +2396,11 @@ class ClassAnalyzer extends ClassLikeAnalyzer
$extended_class,
$codebase->classlikes->classExists($parent_fq_class_name)
? $parent_fq_class_name
: '*' . implode('\\', $extended_class->parts)
: '*'
. ($extended_class instanceof PhpParser\Node\Name\FullyQualified
? '\\'
: $this->getNamespace() . '-')
. implode('\\', $extended_class->parts)
);
}

View File

@ -103,7 +103,11 @@ class NewAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\CallAna
$stmt->class,
$codebase->classlikes->classExists($fq_class_name)
? $fq_class_name
: '*' . implode('\\', $stmt->class->parts)
: '*'
. ($stmt->class instanceof PhpParser\Node\Name\FullyQualified
? '\\'
: $statements_analyzer->getNamespace() . '-')
. implode('\\', $stmt->class->parts)
);
}
} elseif ($stmt->class instanceof PhpParser\Node\Stmt\Class_) {

View File

@ -74,7 +74,11 @@ class ConstFetchAnalyzer
$stmt,
$const_type
? $fq_const_name
: '*' . $fq_const_name
: '*'
. ($stmt->name instanceof PhpParser\Node\Name\FullyQualified
? '\\'
: $statements_analyzer->getNamespace() . '-')
. $const_name
);
if ($const_type) {

View File

@ -51,7 +51,11 @@ class InstanceofAnalyzer
$stmt->class,
$codebase->classlikes->classOrInterfaceExists($fq_class_name)
? $fq_class_name
: '*' . implode('\\', $stmt->class->parts)
: '*'
. ($stmt->class instanceof PhpParser\Node\Name\FullyQualified
? '\\'
: $statements_analyzer->getNamespace() . '-')
. implode('\\', $stmt->class->parts)
);
}

View File

@ -239,14 +239,30 @@ class ClassLikes
$stub = substr($stub, 1);
}
$fully_qualified = false;
if ($stub[0] === '\\') {
$fully_qualified = true;
$stub = substr($stub, 1);
} else {
// for any not-fully-qualified class name the bit we care about comes after a dash
[, $stub] = explode('-', $stub);
}
$stub = preg_quote(strtolower($stub));
if ($fully_qualified) {
$stub = '^' . $stub;
} else {
$stub = '(^|\\\)' . $stub;
}
foreach ($this->existing_classes as $fq_classlike_name => $found) {
if (!$found) {
continue;
}
if (preg_match('@(^|\\\)' . $stub . '.*@i', $fq_classlike_name)) {
if (preg_match('@' . $stub . '.*@i', $fq_classlike_name)) {
$matching_classes[] = $fq_classlike_name;
}
}
@ -256,7 +272,7 @@ class ClassLikes
continue;
}
if (preg_match('@(^|\\\)' . $stub . '.*@i', $fq_classlike_name)) {
if (preg_match('@' . $stub . '.*@i', $fq_classlike_name)) {
$matching_classes[] = $fq_classlike_name;
}
}

View File

@ -269,10 +269,21 @@ class Functions
$stub = substr($stub, 1);
}
$fully_qualified = false;
if ($stub[0] === '\\') {
$fully_qualified = true;
$stub = substr($stub, 1);
$stub_namespace = '';
} else {
// functions can reference either the current namespace or root-namespaced
// equivalents. We therefore want to make both candidates.
[$stub_namespace, $stub] = explode('-', $stub);
}
/** @var array<lowercase-string, FunctionStorage> */
$matching_functions = [];
$stub_lc = strtolower($stub);
$file_storage = $this->file_storage_provider->get($file_path);
$current_namespace_aliases = null;
@ -290,18 +301,11 @@ class Functions
$stub . '*',
];
if ($current_namespace_aliases) {
// As the stub still include the current namespace in the symbol,
// remove the current namespace to provide the function-only token
// and match against global functions and all imported namespaces.
// "Bar/foo" will become "foo".
if ($current_namespace_aliases->namespace
&& strpos($stub_lc, strtolower($current_namespace_aliases->namespace) . '\\') === 0
) {
$stub = substr($stub, strlen($current_namespace_aliases->namespace) + 1);
$match_function_patterns[] = $stub . '*';
}
if ($stub_namespace) {
$match_function_patterns[] = $stub_namespace . '\\' . $stub . '*';
}
if ($current_namespace_aliases) {
foreach ($current_namespace_aliases->functions as $alias_name => $function_name) {
if (strpos($alias_name, $stub) === 0) {
try {
@ -310,8 +314,11 @@ class Functions
}
}
}
foreach ($current_namespace_aliases->uses as $namespace_name) {
$match_function_patterns[] = $namespace_name . '\\' . $stub . '*';
if (!$fully_qualified) {
foreach ($current_namespace_aliases->uses as $namespace_name) {
$match_function_patterns[] = $namespace_name . '\\' . $stub . '*';
}
}
}

View File

@ -628,7 +628,7 @@ class CompletionTest extends \Psalm\Tests\TestCase
$codebase->scanFiles();
$this->analyzeFile('somefile.php', new Context());
$this->assertSame(['*Ex', 'symbol', 78], $codebase->getCompletionDataAtPosition('somefile.php', new Position(2, 32)));
$this->assertSame(['*-Ex', 'symbol', 78], $codebase->getCompletionDataAtPosition('somefile.php', new Position(2, 32)));
}
public function testCompletionOnNewExceptionWithNamespaceNoUse(): void
@ -655,7 +655,7 @@ class CompletionTest extends \Psalm\Tests\TestCase
$this->assertSame(
[
'*Ex',
'*Bar-Ex',
'symbol',
110,
],
@ -707,7 +707,7 @@ class CompletionTest extends \Psalm\Tests\TestCase
$this->assertSame(
[
'*ArrayO',
'*Bar-ArrayO',
'symbol',
220,
],
@ -727,6 +727,93 @@ class CompletionTest extends \Psalm\Tests\TestCase
$this->assertSame(44, $completion_items[0]->additionalTextEdits[0]->range->end->character);
}
public function testCompletionOnNamespaceWithFullyQualified(): void
{
$codebase = $this->project_analyzer->getCodebase();
$config = $codebase->config;
$config->throw_exception = false;
$this->addFile(
'somefile.php',
'<?php
namespace Bar\Baz\Bat;
class B {
public function foo() : void {
\Ex
}
}'
);
$codebase->file_provider->openFile('somefile.php');
$codebase->scanFiles();
$this->analyzeFile('somefile.php', new Context());
$completion_data = $codebase->getCompletionDataAtPosition('somefile.php', new Position(5, 27));
$this->assertSame(
[
'*\Ex',
'symbol',
150,
],
$completion_data
);
$completion_items = $codebase->getCompletionItemsForPartialSymbol($completion_data[0], $completion_data[2], 'somefile.php');
$this->assertNotEmpty($completion_items);
$this->assertSame('Exception', $completion_items[0]->label);
$this->assertSame('\Exception', $completion_items[0]->insertText);
$this->assertEmpty($completion_items[0]->additionalTextEdits);
}
public function testCompletionOnExceptionWithNamespaceAndUseInClass(): void
{
$codebase = $this->project_analyzer->getCodebase();
$config = $codebase->config;
$config->throw_exception = false;
$this->addFile(
'somefile.php',
'<?php
namespace Bar\Baz\Bat;
class B {
public function foo() : void {
Ex
}
}'
);
$codebase->file_provider->openFile('somefile.php');
$codebase->scanFiles();
$this->analyzeFile('somefile.php', new Context());
$completion_data = $codebase->getCompletionDataAtPosition('somefile.php', new Position(5, 26));
$this->assertSame(
[
'*Bar\Baz\Bat-Ex',
'symbol',
149,
],
$completion_data
);
$completion_items = $codebase->getCompletionItemsForPartialSymbol($completion_data[0], $completion_data[2], 'somefile.php');
$this->assertNotEmpty($completion_items);
$this->assertSame('Exception', $completion_items[0]->label);
$this->assertSame('Exception', $completion_items[0]->insertText);
$this->assertNotNull($completion_items[0]->additionalTextEdits);
$this->assertCount(1, $completion_items[0]->additionalTextEdits);
}
public function testCompletionForFunctionNames(): void
{
$codebase = $this->project_analyzer->getCodebase();
@ -751,12 +838,42 @@ class CompletionTest extends \Psalm\Tests\TestCase
$completion_data = $codebase->getCompletionDataAtPosition('somefile.php', new Position(7, 30));
$this->assertNotNull($completion_data);
$this->assertSame('*Bar\my_function_in', $completion_data[0]);
$this->assertSame('*Bar-my_function_in', $completion_data[0]);
$completion_items = $codebase->getCompletionItemsForPartialSymbol($completion_data[0], $completion_data[2], 'somefile.php');
$this->assertSame(1, count($completion_items));
}
public function testCompletionForNamespacedOverriddenFunctionNames(): void
{
$codebase = $this->project_analyzer->getCodebase();
$config = $codebase->config;
$config->throw_exception = false;
$this->addFile(
'somefile.php',
'<?php
namespace Bar;
function strlen() : void {
}
strlen'
);
$codebase->file_provider->openFile('somefile.php');
$codebase->scanFiles();
$this->analyzeFile('somefile.php', new Context());
$completion_data = $codebase->getCompletionDataAtPosition('somefile.php', new Position(7, 22));
$this->assertNotNull($completion_data);
$this->assertSame('*Bar-strlen', $completion_data[0]);
$completion_items = $codebase->getCompletionItemsForPartialSymbol($completion_data[0], $completion_data[2], 'somefile.php');
$this->assertSame(2, count($completion_items));
}
public function testCompletionForFunctionNamesRespectUsedNamespaces(): void
{
$codebase = $this->project_analyzer->getCodebase();
@ -777,7 +894,7 @@ class CompletionTest extends \Psalm\Tests\TestCase
$completion_data = $codebase->getCompletionDataAtPosition('somefile.php', new Position(3, 25));
$this->assertNotNull($completion_data);
$this->assertSame('*Bar\atleaston', $completion_data[0]);
$this->assertSame('*Bar-atleaston', $completion_data[0]);
$completion_items = $codebase->getCompletionItemsForPartialSymbol($completion_data[0], $completion_data[2], 'somefile.php');
$this->assertSame(1, count($completion_items));
@ -804,7 +921,7 @@ class CompletionTest extends \Psalm\Tests\TestCase
$completion_data = $codebase->getCompletionDataAtPosition('somefile.php', new Position(3, 25));
$this->assertNotNull($completion_data);
$this->assertSame('*Bar\Atleaston', $completion_data[0]);
$this->assertSame('*Bar-Atleaston', $completion_data[0]);
$completion_items = $codebase->getCompletionItemsForPartialSymbol($completion_data[0], $completion_data[2], 'somefile.php');
$this->assertSame(0, count($completion_items));
@ -828,10 +945,10 @@ class CompletionTest extends \Psalm\Tests\TestCase
$codebase->scanFiles();
$this->analyzeFile('somefile.php', new Context());
$functions = $codebase->functions->getMatchingFunctionNames('array_su', 0, 'somefile.php', $codebase);
$functions = $codebase->functions->getMatchingFunctionNames('*-array_su', 0, 'somefile.php', $codebase);
$this->assertSame(1, count($functions));
$functions = $codebase->functions->getMatchingFunctionNames('my_funct', 0, 'somefile.php', $codebase);
$functions = $codebase->functions->getMatchingFunctionNames('*-my_funct', 0, 'somefile.php', $codebase);
$this->assertSame(1, count($functions));
}
@ -850,7 +967,7 @@ class CompletionTest extends \Psalm\Tests\TestCase
$codebase->scanFiles();
$this->analyzeFile('somefile.php', new Context());
$functions = $codebase->functions->getMatchingFunctionNames('urlencod', 0, 'somefile.php', $codebase);
$functions = $codebase->functions->getMatchingFunctionNames('*-urlencod', 0, 'somefile.php', $codebase);
$this->assertSame(1, count($functions));
}
@ -873,7 +990,7 @@ class CompletionTest extends \Psalm\Tests\TestCase
$codebase->scanFiles();
$this->analyzeFile('somefile.php', new Context());
$functions = $codebase->functions->getMatchingFunctionNames('Foo\atleaston', 81, 'somefile.php', $codebase);
$functions = $codebase->functions->getMatchingFunctionNames('*Foo-atleaston', 81, 'somefile.php', $codebase);
$this->assertSame(1, count($functions));
}
@ -896,7 +1013,7 @@ class CompletionTest extends \Psalm\Tests\TestCase
$codebase->scanFiles();
$this->analyzeFile('somefile.php', new Context());
$functions = $codebase->functions->getMatchingFunctionNames('Foo\atleaston', 81, 'somefile.php', $codebase);
$functions = $codebase->functions->getMatchingFunctionNames('*Foo-atleaston', 81, 'somefile.php', $codebase);
$this->assertSame(1, count($functions));
}
@ -919,7 +1036,7 @@ class CompletionTest extends \Psalm\Tests\TestCase
$codebase->scanFiles();
$this->analyzeFile('somefile.php', new Context());
$functions = $codebase->functions->getMatchingFunctionNames('Foo\Atleaston', 81, 'somefile.php', $codebase);
$functions = $codebase->functions->getMatchingFunctionNames('*Foo-Atleaston', 81, 'somefile.php', $codebase);
$this->assertSame(0, count($functions));
}
@ -941,10 +1058,10 @@ class CompletionTest extends \Psalm\Tests\TestCase
$codebase->scanFiles();
$this->analyzeFile('somefile.php', new Context());
$functions = $codebase->functions->getMatchingFunctionNames('*Foo\array_su', 45, 'somefile.php', $codebase);
$functions = $codebase->functions->getMatchingFunctionNames('*Foo-array_su', 45, 'somefile.php', $codebase);
$this->assertSame(1, count($functions));
$functions = $codebase->functions->getMatchingFunctionNames('Foo\my_funct', 45, 'somefile.php', $codebase);
$functions = $codebase->functions->getMatchingFunctionNames('Foo-my_funct', 45, 'somefile.php', $codebase);
$this->assertSame(1, count($functions));
}
@ -978,7 +1095,7 @@ class CompletionTest extends \Psalm\Tests\TestCase
$this->assertSame(
[
'*Ant',
'*Bar-Ant',
'symbol',
267,
],