mirror of
https://github.com/danog/psalm.git
synced 2024-11-29 20:28:59 +01:00
Completions for array keys and type literals (#5105)
* Add completions for known array keys * Use dynamic gap value * Provide completions for known type contexts * Fix formatting * Remove trailing comma * PHPCS fixes * Remove support for literal floats * Fix test for floats
This commit is contained in:
parent
037b24550f
commit
cdc431e940
@ -900,6 +900,41 @@ class Codebase
|
||||
$this->file_storage_provider->remove($file_path);
|
||||
}
|
||||
|
||||
public function getFunctionStorageForSymbol(string $file_path, string $symbol): ?FunctionLikeStorage
|
||||
{
|
||||
if (strpos($symbol, '::')) {
|
||||
$symbol = substr($symbol, 0, -2);
|
||||
/** @psalm-suppress ArgumentTypeCoercion */
|
||||
$method_id = new \Psalm\Internal\MethodIdentifier(...explode('::', $symbol));
|
||||
|
||||
$declaring_method_id = $this->methods->getDeclaringMethodId($method_id);
|
||||
|
||||
if (!$declaring_method_id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$storage = $this->methods->getStorage($declaring_method_id);
|
||||
return $storage;
|
||||
}
|
||||
|
||||
$function_id = strtolower(substr($symbol, 0, -2));
|
||||
$file_storage = $this->file_storage_provider->get($file_path);
|
||||
|
||||
if (isset($file_storage->functions[$function_id])) {
|
||||
$function_storage = $file_storage->functions[$function_id];
|
||||
|
||||
return $function_storage;
|
||||
}
|
||||
|
||||
if (!$function_id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$function = $this->functions->getStorage(null, $function_id);
|
||||
|
||||
return $function;
|
||||
}
|
||||
|
||||
public function getSymbolInformation(string $file_path, string $symbol): ?string
|
||||
{
|
||||
if (\is_numeric($symbol[0])) {
|
||||
@ -1230,7 +1265,7 @@ class Codebase
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: string, 1: '->'|'::'|'symbol', 2: int}|null
|
||||
* @return array{0: string, 1: '->'|'::'|'['|'symbol', 2: int}|null
|
||||
*/
|
||||
public function getCompletionDataAtPosition(string $file_path, Position $position): ?array
|
||||
{
|
||||
@ -1262,6 +1297,21 @@ class Codebase
|
||||
: 0;
|
||||
$end_pos = $end_pos_excluding_whitespace + $num_whitespace_bytes;
|
||||
|
||||
if ($offset - $end_pos === 1) {
|
||||
$candidate_gap = substr($file_contents, $end_pos, 1);
|
||||
|
||||
if ($candidate_gap == '[') {
|
||||
$gap = $candidate_gap;
|
||||
$recent_type = $possible_type;
|
||||
|
||||
if ($recent_type === 'mixed') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [$recent_type, $gap, $offset];
|
||||
}
|
||||
}
|
||||
|
||||
if ($offset - $end_pos === 2 || $offset - $end_pos === 3) {
|
||||
$candidate_gap = substr($file_contents, $end_pos, 2);
|
||||
|
||||
@ -1302,6 +1352,31 @@ class Codebase
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getTypeContextAtPosition(string $file_path, Position $position): ?Type\Union
|
||||
{
|
||||
$file_contents = $this->getFileContents($file_path);
|
||||
$offset = $position->toOffset($file_contents);
|
||||
|
||||
[$reference_map, $type_map, $argument_map] = $this->analyzer->getMapsForFile($file_path);
|
||||
if (!$reference_map && !$type_map && !$argument_map) {
|
||||
return null;
|
||||
}
|
||||
foreach ($argument_map as $start_pos => [$end_pos, $function, $argument_num]) {
|
||||
if ($offset < $start_pos || $offset > $end_pos) {
|
||||
continue;
|
||||
}
|
||||
// First parameter to a function-like
|
||||
$function_storage = $this->getFunctionStorageForSymbol($file_path, $function . '()');
|
||||
if (!$function_storage || !$function_storage->params) {
|
||||
return null;
|
||||
}
|
||||
$parameter = $function_storage->params[$argument_num];
|
||||
return $parameter->type;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<\LanguageServerProtocol\CompletionItem>
|
||||
*/
|
||||
@ -1499,6 +1574,88 @@ class Codebase
|
||||
return $completion_items;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<\LanguageServerProtocol\CompletionItem>
|
||||
*/
|
||||
public function getCompletionItemsForType(Type\Union $type): array
|
||||
{
|
||||
$completion_items = [];
|
||||
foreach ($type->getAtomicTypes() as $atomic_type) {
|
||||
if ($atomic_type instanceof Type\Atomic\TBool) {
|
||||
$bools = (string) $atomic_type === 'bool' ? ['true', 'false'] : [(string) $atomic_type];
|
||||
foreach ($bools as $property_name) {
|
||||
$completion_items[] = new \LanguageServerProtocol\CompletionItem(
|
||||
$property_name,
|
||||
\LanguageServerProtocol\CompletionItemKind::VALUE,
|
||||
'bool',
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
$property_name,
|
||||
);
|
||||
}
|
||||
} elseif ($atomic_type instanceof Type\Atomic\TLiteralString) {
|
||||
$completion_items[] = new \LanguageServerProtocol\CompletionItem(
|
||||
$atomic_type->value,
|
||||
\LanguageServerProtocol\CompletionItemKind::VALUE,
|
||||
$atomic_type->getId(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"'$atomic_type->value'",
|
||||
);
|
||||
} elseif ($atomic_type instanceof Type\Atomic\TLiteralInt) {
|
||||
$completion_items[] = new \LanguageServerProtocol\CompletionItem(
|
||||
(string) $atomic_type->value,
|
||||
\LanguageServerProtocol\CompletionItemKind::VALUE,
|
||||
$atomic_type->getId(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
(string) $atomic_type->value,
|
||||
);
|
||||
} elseif ($atomic_type instanceof Type\Atomic\TScalarClassConstant) {
|
||||
$const = $atomic_type->fq_classlike_name . '::' . $atomic_type->const_name;
|
||||
$completion_items[] = new \LanguageServerProtocol\CompletionItem(
|
||||
$const,
|
||||
\LanguageServerProtocol\CompletionItemKind::VALUE,
|
||||
$atomic_type->getId(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
$const,
|
||||
);
|
||||
}
|
||||
}
|
||||
return $completion_items;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<\LanguageServerProtocol\CompletionItem>
|
||||
*/
|
||||
public function getCompletionItemsForArrayKeys(
|
||||
string $type_string
|
||||
) : array {
|
||||
$completion_items = [];
|
||||
$type = Type::parseString($type_string);
|
||||
foreach ($type->getAtomicTypes() as $atomic_type) {
|
||||
if ($atomic_type instanceof Type\Atomic\TKeyedArray) {
|
||||
foreach ($atomic_type->properties as $property_name => $property) {
|
||||
$completion_items[] = new \LanguageServerProtocol\CompletionItem(
|
||||
(string) $property_name,
|
||||
\LanguageServerProtocol\CompletionItemKind::PROPERTY,
|
||||
(string) $property,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"'$property_name'",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $completion_items;
|
||||
}
|
||||
|
||||
private static function getPositionFromOffset(int $offset, string $file_contents) : Position
|
||||
{
|
||||
$file_contents = substr($file_contents, 0, $offset);
|
||||
|
@ -247,7 +247,7 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
||||
if ($this->project_analyzer->provide_completion) {
|
||||
$serverCapabilities->completionProvider = new CompletionOptions();
|
||||
$serverCapabilities->completionProvider->resolveProvider = false;
|
||||
$serverCapabilities->completionProvider->triggerCharacters = ['$', '>', ':'];
|
||||
$serverCapabilities->completionProvider->triggerCharacters = ['$', '>', ':',"[", "(", ",", " "];
|
||||
}
|
||||
|
||||
$serverCapabilities->signatureHelpProvider = new SignatureHelpOptions(['(', ',']);
|
||||
|
@ -249,18 +249,30 @@ class TextDocument
|
||||
return new Success([]);
|
||||
}
|
||||
|
||||
if (!$completion_data) {
|
||||
$type_context = $this->codebase->getTypeContextAtPosition($file_path, $position);
|
||||
|
||||
if (!$completion_data && !$type_context) {
|
||||
error_log('completion not found at ' . $position->line . ':' . $position->character);
|
||||
|
||||
return new Success([]);
|
||||
}
|
||||
|
||||
if ($completion_data) {
|
||||
[$recent_type, $gap, $offset] = $completion_data;
|
||||
|
||||
if ($gap === '->' || $gap === '::') {
|
||||
$completion_items = $this->codebase->getCompletionItemsForClassishThing($recent_type, $gap);
|
||||
} elseif ($gap === '[') {
|
||||
$completion_items = $this->codebase->getCompletionItemsForArrayKeys($recent_type);
|
||||
} else {
|
||||
$completion_items = $this->codebase->getCompletionItemsForPartialSymbol($recent_type, $offset, $file_path);
|
||||
$completion_items = $this->codebase->getCompletionItemsForPartialSymbol(
|
||||
$recent_type,
|
||||
$offset,
|
||||
$file_path
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$completion_items = $this->codebase->getCompletionItemsForType($type_context);
|
||||
}
|
||||
|
||||
return new Success(new CompletionList($completion_items, false));
|
||||
|
@ -764,7 +764,7 @@ class ParseTreeCreator
|
||||
);
|
||||
} else {
|
||||
throw new TypeParseTreeException(
|
||||
'Bracket must be preceded by “Closure”, “callable”, "pure-callable" or a valid @method name'
|
||||
'Paranthesis must be preceded by “Closure”, “callable”, "pure-callable" or a valid @method name'
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ use Psalm\Internal\Analyzer\ProjectAnalyzer;
|
||||
use Psalm\Internal\Provider\Providers;
|
||||
use Psalm\Tests\Internal\Provider;
|
||||
use Psalm\Tests\TestConfig;
|
||||
use Psalm\Type;
|
||||
|
||||
class CompletionTest extends \Psalm\Tests\TestCase
|
||||
{
|
||||
@ -815,4 +816,114 @@ class CompletionTest extends \Psalm\Tests\TestCase
|
||||
$codebase->scanFiles();
|
||||
$this->analyzeFile('somefile.php', new Context());
|
||||
}
|
||||
|
||||
public function testCompletionOnArrayKey(): void
|
||||
{
|
||||
|
||||
$codebase = $this->project_analyzer->getCodebase();
|
||||
$config = $codebase->config;
|
||||
$config->throw_exception = false;
|
||||
|
||||
$this->addFile(
|
||||
'somefile.php',
|
||||
'<?php
|
||||
$my_array = ["foo" => 1, "bar" => 2];
|
||||
$my_array[]
|
||||
'
|
||||
);
|
||||
|
||||
$codebase->file_provider->openFile('somefile.php');
|
||||
$codebase->scanFiles();
|
||||
$this->analyzeFile('somefile.php', new Context());
|
||||
|
||||
$completion_data = $codebase->getCompletionDataAtPosition('somefile.php', new Position(2, 26));
|
||||
$this->assertSame(
|
||||
[
|
||||
'array{bar: 2, foo: 1}',
|
||||
'[',
|
||||
86,
|
||||
],
|
||||
$completion_data
|
||||
);
|
||||
|
||||
$completion_items = $codebase->getCompletionItemsForArrayKeys($completion_data[0]);
|
||||
|
||||
$this->assertCount(2, $completion_items);
|
||||
}
|
||||
|
||||
public function testTypeContextForFunctionArgument(): void
|
||||
{
|
||||
|
||||
$codebase = $this->project_analyzer->getCodebase();
|
||||
$config = $codebase->config;
|
||||
$config->throw_exception = false;
|
||||
|
||||
$this->addFile(
|
||||
'somefile.php',
|
||||
'<?php
|
||||
namespace Bar;
|
||||
function my_func(string $arg_a, bool $arg_b) : string {
|
||||
}
|
||||
|
||||
my_func()'
|
||||
);
|
||||
|
||||
$codebase->file_provider->openFile('somefile.php');
|
||||
$codebase->scanFiles();
|
||||
$this->analyzeFile('somefile.php', new Context());
|
||||
|
||||
$type = $codebase->getTypeContextAtPosition('somefile.php', new Position(5, 24));
|
||||
$this->assertSame('string', (string) $type);
|
||||
}
|
||||
|
||||
public function testTypeContextForFunctionArgumentWithWhiteSpace(): void
|
||||
{
|
||||
|
||||
$codebase = $this->project_analyzer->getCodebase();
|
||||
$config = $codebase->config;
|
||||
$config->throw_exception = false;
|
||||
|
||||
$this->addFile(
|
||||
'somefile.php',
|
||||
'<?php
|
||||
namespace Bar;
|
||||
function my_func(string $arg_a, bool $arg_b) : string {
|
||||
}
|
||||
|
||||
my_func( "yes", )'
|
||||
);
|
||||
|
||||
$codebase->file_provider->openFile('somefile.php');
|
||||
$codebase->scanFiles();
|
||||
$this->analyzeFile('somefile.php', new Context());
|
||||
|
||||
$type = $codebase->getTypeContextAtPosition('somefile.php', new Position(5, 32));
|
||||
$this->assertSame('bool', (string) $type);
|
||||
}
|
||||
|
||||
public function testCompletionsForType(): void
|
||||
{
|
||||
$codebase = $this->project_analyzer->getCodebase();
|
||||
$config = $codebase->config;
|
||||
$config->throw_exception = false;
|
||||
|
||||
$completion_items = $codebase->getCompletionItemsForType(Type::parseString('bool'));
|
||||
$this->assertCount(2, $completion_items);
|
||||
|
||||
$completion_items = $codebase->getCompletionItemsForType(Type::parseString('true'));
|
||||
$this->assertCount(1, $completion_items);
|
||||
|
||||
$completion_items = $codebase->getCompletionItemsForType(Type::parseString("'yes'|'no'"));
|
||||
$this->assertCount(2, $completion_items);
|
||||
|
||||
$completion_items = $codebase->getCompletionItemsForType(Type::parseString("1|2|3"));
|
||||
$this->assertCount(3, $completion_items);
|
||||
|
||||
// Floats not supported.
|
||||
$completion_items = $codebase->getCompletionItemsForType(Type::parseString("1.0"));
|
||||
$this->assertCount(0, $completion_items);
|
||||
|
||||
$completion_items = $codebase->getCompletionItemsForType(Type::parseString("DateTime::RFC3339"));
|
||||
$this->assertCount(1, $completion_items);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user