mirror of
https://github.com/danog/psalm.git
synced 2025-01-21 21:31:13 +01:00
Add completions for functions (#5128)
* Add completions for functions Provide autocompletions in the LSP for all global functions and functions from namespaces used in the current context. * Uncomment code * PHPCS * Simplify functions map Co-authored-by: Matthew Brown <github@muglug.com> * Switch to storing lowercase function string in array key * Fix spacing Co-authored-by: Matthew Brown <github@muglug.com>
This commit is contained in:
parent
fa337375ae
commit
4077de2c93
@ -44,6 +44,7 @@ use function substr;
|
||||
use function substr_count;
|
||||
use function array_pop;
|
||||
use function implode;
|
||||
use function array_reverse;
|
||||
|
||||
class Codebase
|
||||
{
|
||||
@ -1611,6 +1612,51 @@ class Codebase
|
||||
);
|
||||
}
|
||||
|
||||
$functions = $this->functions->getMatchingFunctionNames($type_string, $offset, $file_path, $this);
|
||||
|
||||
$namespace_map = [];
|
||||
if ($aliases) {
|
||||
$namespace_map += $aliases->uses_flipped;
|
||||
if ($aliases->namespace) {
|
||||
$namespace_map[$aliases->namespace] = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the map by longest first, so we replace most specific
|
||||
// used namespaces first.
|
||||
ksort($namespace_map);
|
||||
$namespace_map = array_reverse($namespace_map);
|
||||
|
||||
foreach ($functions as $function_lowercase => $function) {
|
||||
// Transform FQFN relative to all uses namespaces
|
||||
$function_name = $function->cased_name;
|
||||
if (!$function_name) {
|
||||
continue;
|
||||
}
|
||||
$in_namespace_map = false;
|
||||
foreach ($namespace_map as $namespace_name => $namespace_alias) {
|
||||
if (strpos($function_lowercase, $namespace_name . '\\') === 0) {
|
||||
$function_name = $namespace_alias . '\\' . substr($function_name, strlen($namespace_name) + 1);
|
||||
$in_namespace_map = true;
|
||||
}
|
||||
}
|
||||
// If the function is not use'd, and it's not a global function
|
||||
// prepend it with a backslash.
|
||||
if (!$in_namespace_map && strpos($function_name, '\\') !== false) {
|
||||
$function_name = '\\' . $function_name;
|
||||
}
|
||||
$completion_items[] = new \LanguageServerProtocol\CompletionItem(
|
||||
$function_name,
|
||||
\LanguageServerProtocol\CompletionItemKind::FUNCTION,
|
||||
$function->getSignature(false),
|
||||
null,
|
||||
null,
|
||||
$function_name,
|
||||
$function_name . '()',
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
return $completion_items;
|
||||
}
|
||||
|
||||
|
@ -39,6 +39,7 @@ use function strlen;
|
||||
use function strrpos;
|
||||
use function strtolower;
|
||||
use function substr;
|
||||
use function preg_quote;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -238,7 +239,7 @@ class ClassLikes
|
||||
$stub = substr($stub, 1);
|
||||
}
|
||||
|
||||
$stub = strtolower($stub);
|
||||
$stub = preg_quote(strtolower($stub));
|
||||
|
||||
foreach ($this->existing_classes as $fq_classlike_name => $found) {
|
||||
if (!$found) {
|
||||
|
@ -18,6 +18,10 @@ use function strtolower;
|
||||
use function substr;
|
||||
use Psalm\Type\Atomic\TNamedObject;
|
||||
use Psalm\Internal\MethodIdentifier;
|
||||
use function strlen;
|
||||
use function rtrim;
|
||||
use function is_bool;
|
||||
use function array_values;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -252,6 +256,94 @@ class Functions
|
||||
return ($namespace ? $namespace . '\\' : '') . $function_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<lowercase-string,FunctionStorage>
|
||||
*/
|
||||
public function getMatchingFunctionNames(
|
||||
string $stub,
|
||||
int $offset,
|
||||
string $file_path,
|
||||
Codebase $codebase
|
||||
) : array {
|
||||
if ($stub[0] === '*') {
|
||||
$stub = substr($stub, 1);
|
||||
}
|
||||
|
||||
/** @var array<lowercase-string, FunctionStorage> */
|
||||
$matching_functions = [];
|
||||
|
||||
$stub = strtolower($stub);
|
||||
$file_storage = $this->file_storage_provider->get($file_path);
|
||||
|
||||
$current_namespace_aliases = null;
|
||||
foreach ($file_storage->namespace_aliases as $namespace_start => $namespace_aliases) {
|
||||
if ($namespace_start < $offset) {
|
||||
$current_namespace_aliases = $namespace_aliases;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// We will search all functions for several patterns. This will
|
||||
// be for all used namespaces, the global namespace and matched
|
||||
// used functions.
|
||||
$match_function_patterns = [
|
||||
$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, strtolower($current_namespace_aliases->namespace) . '\\') === 0
|
||||
) {
|
||||
$stub = substr($stub, strlen($current_namespace_aliases->namespace) + 1);
|
||||
$match_function_patterns[] = $stub . '*';
|
||||
}
|
||||
|
||||
foreach ($current_namespace_aliases->functions as $alias_name => $function_name) {
|
||||
if (strpos($alias_name, $stub) === 0) {
|
||||
try {
|
||||
$match_function_patterns[] = $function_name;
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($current_namespace_aliases->uses as $namespace_name) {
|
||||
$match_function_patterns[] = $namespace_name . '\\' . $stub . '*';
|
||||
}
|
||||
}
|
||||
|
||||
$function_map = $file_storage->functions
|
||||
+ $this->getAllStubbedFunctions()
|
||||
+ $this->reflection->getFunctions()
|
||||
+ $codebase->config->getPredefinedFunctions();
|
||||
|
||||
foreach ($function_map as $function_name => $function) {
|
||||
foreach ($match_function_patterns as $pattern) {
|
||||
if (substr($pattern, -1, 1) === '*') {
|
||||
if (strpos($function_name, rtrim($pattern, '*')) !== 0) {
|
||||
continue;
|
||||
}
|
||||
} elseif ($function_name !== $pattern) {
|
||||
continue;
|
||||
}
|
||||
if (is_bool($function)) {
|
||||
/** @var callable-string $function_name */
|
||||
if ($this->reflection->registerFunction($function_name) === false) {
|
||||
continue;
|
||||
}
|
||||
$function = $this->reflection->getFunctionStorage($function_name);
|
||||
}
|
||||
/** @var lowercase-string $function_name */
|
||||
$matching_functions[$function_name] = $function;
|
||||
}
|
||||
}
|
||||
|
||||
return $matching_functions;
|
||||
}
|
||||
|
||||
public static function isVariadic(Codebase $codebase, string $function_id, string $file_path): bool
|
||||
{
|
||||
$file_storage = $codebase->file_storage_provider->get($file_path);
|
||||
|
@ -517,6 +517,14 @@ class Reflection
|
||||
throw new \UnexpectedValueException('Expecting to have a function for ' . $function_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, FunctionStorage>
|
||||
*/
|
||||
public function getFunctions(): array
|
||||
{
|
||||
return self::$builtin_functions;
|
||||
}
|
||||
|
||||
public static function clearCache() : void
|
||||
{
|
||||
self::$builtin_functions = [];
|
||||
|
@ -8,6 +8,7 @@ use Psalm\Internal\Provider\Providers;
|
||||
use Psalm\Tests\Internal\Provider;
|
||||
use Psalm\Tests\TestConfig;
|
||||
use Psalm\Type;
|
||||
use function count;
|
||||
|
||||
class CompletionTest extends \Psalm\Tests\TestCase
|
||||
{
|
||||
@ -726,6 +727,178 @@ class CompletionTest extends \Psalm\Tests\TestCase
|
||||
$this->assertSame(44, $completion_items[0]->additionalTextEdits[0]->range->end->character);
|
||||
}
|
||||
|
||||
public function testCompletionForFunctionNames(): void
|
||||
{
|
||||
$codebase = $this->project_analyzer->getCodebase();
|
||||
$config = $codebase->config;
|
||||
$config->throw_exception = false;
|
||||
|
||||
$this->addFile(
|
||||
'somefile.php',
|
||||
'<?php
|
||||
namespace Bar;
|
||||
|
||||
function my_function_in_bar() : void {
|
||||
|
||||
}
|
||||
|
||||
my_function_in'
|
||||
);
|
||||
|
||||
$codebase->file_provider->openFile('somefile.php');
|
||||
$codebase->scanFiles();
|
||||
$this->analyzeFile('somefile.php', new Context());
|
||||
|
||||
$completion_data = $codebase->getCompletionDataAtPosition('somefile.php', new Position(7, 30));
|
||||
$this->assertNotNull($completion_data);
|
||||
$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 testCompletionForFunctionNamesRespectUsedNamespaces(): void
|
||||
{
|
||||
$codebase = $this->project_analyzer->getCodebase();
|
||||
$config = $codebase->config;
|
||||
$config->throw_exception = false;
|
||||
|
||||
$this->addFile(
|
||||
'somefile.php',
|
||||
'<?php
|
||||
namespace Bar;
|
||||
use phpunit\framework as phpf;
|
||||
atleaston'
|
||||
);
|
||||
|
||||
$codebase->file_provider->openFile('somefile.php');
|
||||
$codebase->scanFiles();
|
||||
$this->analyzeFile('somefile.php', new Context());
|
||||
|
||||
$completion_data = $codebase->getCompletionDataAtPosition('somefile.php', new Position(3, 25));
|
||||
$this->assertNotNull($completion_data);
|
||||
$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));
|
||||
$this->assertSame('phpf\\atLeastOnce', $completion_items[0]->label);
|
||||
}
|
||||
|
||||
public function testGetMatchingFunctionNames(): void
|
||||
{
|
||||
$codebase = $this->project_analyzer->getCodebase();
|
||||
$config = $codebase->config;
|
||||
$config->throw_exception = false;
|
||||
|
||||
$this->addFile(
|
||||
'somefile.php',
|
||||
'<?php
|
||||
|
||||
function my_function() {
|
||||
}'
|
||||
);
|
||||
|
||||
$codebase->file_provider->openFile('somefile.php');
|
||||
$codebase->scanFiles();
|
||||
$this->analyzeFile('somefile.php', new Context());
|
||||
|
||||
$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);
|
||||
$this->assertSame(1, count($functions));
|
||||
}
|
||||
|
||||
public function testGetMatchingFunctionNamesFromPredefinedFunctions(): void
|
||||
{
|
||||
$codebase = $this->project_analyzer->getCodebase();
|
||||
$config = $codebase->config;
|
||||
$config->throw_exception = false;
|
||||
|
||||
$this->addFile(
|
||||
'somefile.php',
|
||||
'<?php'
|
||||
);
|
||||
|
||||
$codebase->file_provider->openFile('somefile.php');
|
||||
$codebase->scanFiles();
|
||||
$this->analyzeFile('somefile.php', new Context());
|
||||
|
||||
$functions = $codebase->functions->getMatchingFunctionNames('urlencod', 0, 'somefile.php', $codebase);
|
||||
$this->assertSame(1, count($functions));
|
||||
}
|
||||
|
||||
public function testGetMatchingFunctionNamesFromUsedFunction(): void
|
||||
{
|
||||
$codebase = $this->project_analyzer->getCodebase();
|
||||
$config = $codebase->config;
|
||||
$config->throw_exception = false;
|
||||
|
||||
$this->addFile(
|
||||
'somefile.php',
|
||||
'<?php
|
||||
|
||||
namespace Foo;
|
||||
use function phpunit\framework\atleastonce;
|
||||
'
|
||||
);
|
||||
|
||||
$codebase->file_provider->openFile('somefile.php');
|
||||
$codebase->scanFiles();
|
||||
$this->analyzeFile('somefile.php', new Context());
|
||||
|
||||
$functions = $codebase->functions->getMatchingFunctionNames('Foo\atleaston', 81, 'somefile.php', $codebase);
|
||||
$this->assertSame(1, count($functions));
|
||||
}
|
||||
|
||||
public function testGetMatchingFunctionNamesFromUsedNamespace(): void
|
||||
{
|
||||
$codebase = $this->project_analyzer->getCodebase();
|
||||
$config = $codebase->config;
|
||||
$config->throw_exception = false;
|
||||
|
||||
$this->addFile(
|
||||
'somefile.php',
|
||||
'<?php
|
||||
|
||||
namespace Foo;
|
||||
use phpunit\framework;
|
||||
'
|
||||
);
|
||||
|
||||
$codebase->file_provider->openFile('somefile.php');
|
||||
$codebase->scanFiles();
|
||||
$this->analyzeFile('somefile.php', new Context());
|
||||
|
||||
$functions = $codebase->functions->getMatchingFunctionNames('Foo\atleaston', 81, 'somefile.php', $codebase);
|
||||
$this->assertSame(1, count($functions));
|
||||
}
|
||||
|
||||
public function testGetMatchingFunctionNamesWithNamespace(): void
|
||||
{
|
||||
$codebase = $this->project_analyzer->getCodebase();
|
||||
$config = $codebase->config;
|
||||
$config->throw_exception = false;
|
||||
|
||||
$this->addFile(
|
||||
'somefile.php',
|
||||
'<?php
|
||||
namespace Foo;
|
||||
function my_function() {
|
||||
}'
|
||||
);
|
||||
|
||||
$codebase->file_provider->openFile('somefile.php');
|
||||
$codebase->scanFiles();
|
||||
$this->analyzeFile('somefile.php', new Context());
|
||||
|
||||
$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);
|
||||
$this->assertSame(1, count($functions));
|
||||
}
|
||||
|
||||
public function testCompletionOnInstanceofWithNamespaceAndUse(): void
|
||||
{
|
||||
$codebase = $this->project_analyzer->getCodebase();
|
||||
|
Loading…
x
Reference in New Issue
Block a user