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:
parent
6b53e79505
commit
bd6efd7cf2
@ -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
|
||||
|
@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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_) {
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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 . '*';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
],
|
||||
|
Loading…
x
Reference in New Issue
Block a user