mirror of
https://github.com/danog/psalm.git
synced 2024-11-30 04:39:00 +01:00
Merge pull request #10320 from issidorov/completion-by-partial-property-or-method
Fix auto completion by partial property or method
This commit is contained in:
commit
722fe6c38c
@ -69,6 +69,7 @@ use ReflectionType;
|
|||||||
use UnexpectedValueException;
|
use UnexpectedValueException;
|
||||||
|
|
||||||
use function array_combine;
|
use function array_combine;
|
||||||
|
use function array_merge;
|
||||||
use function array_pop;
|
use function array_pop;
|
||||||
use function array_reverse;
|
use function array_reverse;
|
||||||
use function array_values;
|
use function array_values;
|
||||||
@ -1742,6 +1743,9 @@ final class Codebase
|
|||||||
|
|
||||||
$offset = $position->toOffset($file_contents);
|
$offset = $position->toOffset($file_contents);
|
||||||
|
|
||||||
|
$literal_part = $this->getBeginedLiteralPart($file_path, $position);
|
||||||
|
$begin_literal_offset = $offset - strlen($literal_part);
|
||||||
|
|
||||||
[$reference_map, $type_map] = $this->analyzer->getMapsForFile($file_path);
|
[$reference_map, $type_map] = $this->analyzer->getMapsForFile($file_path);
|
||||||
|
|
||||||
if (!$reference_map && !$type_map) {
|
if (!$reference_map && !$type_map) {
|
||||||
@ -1776,7 +1780,7 @@ final class Codebase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($offset - $end_pos === 2 || $offset - $end_pos === 3) {
|
if ($begin_literal_offset - $end_pos === 2) {
|
||||||
$candidate_gap = substr($file_contents, $end_pos, 2);
|
$candidate_gap = substr($file_contents, $end_pos, 2);
|
||||||
|
|
||||||
if ($candidate_gap === '->' || $candidate_gap === '::') {
|
if ($candidate_gap === '->' || $candidate_gap === '::') {
|
||||||
@ -1801,6 +1805,11 @@ final class Codebase
|
|||||||
return [$possible_reference, '::', $offset];
|
return [$possible_reference, '::', $offset];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($offset <= $end_pos && substr($file_contents, $begin_literal_offset - 2, 2) === '::') {
|
||||||
|
$class_name = explode('::', $possible_reference)[0];
|
||||||
|
return [$class_name, '::', $offset];
|
||||||
|
}
|
||||||
|
|
||||||
// Only continue for references that are partial / don't exist.
|
// Only continue for references that are partial / don't exist.
|
||||||
if ($possible_reference[0] !== '*') {
|
if ($possible_reference[0] !== '*') {
|
||||||
continue;
|
continue;
|
||||||
@ -1816,6 +1825,23 @@ final class Codebase
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getBeginedLiteralPart(string $file_path, Position $position): string
|
||||||
|
{
|
||||||
|
$is_open = $this->file_provider->isOpen($file_path);
|
||||||
|
|
||||||
|
if (!$is_open) {
|
||||||
|
throw new UnanalyzedFileException($file_path . ' is not open');
|
||||||
|
}
|
||||||
|
|
||||||
|
$file_contents = $this->getFileContents($file_path);
|
||||||
|
|
||||||
|
$offset = $position->toOffset($file_contents);
|
||||||
|
|
||||||
|
preg_match('/\$?\w+$/', substr($file_contents, 0, $offset), $matches);
|
||||||
|
|
||||||
|
return $matches[0] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
public function getTypeContextAtPosition(string $file_path, Position $position): ?Union
|
public function getTypeContextAtPosition(string $file_path, Position $position): ?Union
|
||||||
{
|
{
|
||||||
$file_contents = $this->getFileContents($file_path);
|
$file_contents = $this->getFileContents($file_path);
|
||||||
@ -1858,9 +1884,12 @@ final class Codebase
|
|||||||
try {
|
try {
|
||||||
$class_storage = $this->classlike_storage_provider->get($atomic_type->value);
|
$class_storage = $this->classlike_storage_provider->get($atomic_type->value);
|
||||||
|
|
||||||
foreach ($class_storage->appearing_method_ids as $declaring_method_id) {
|
$methods = array_merge(
|
||||||
$method_storage = $this->methods->getStorage($declaring_method_id);
|
$class_storage->methods,
|
||||||
|
$class_storage->pseudo_methods,
|
||||||
|
$class_storage->pseudo_static_methods,
|
||||||
|
);
|
||||||
|
foreach ($methods as $method_storage) {
|
||||||
if ($method_storage->is_static || $gap === '->') {
|
if ($method_storage->is_static || $gap === '->') {
|
||||||
$completion_item = new CompletionItem(
|
$completion_item = new CompletionItem(
|
||||||
$method_storage->cased_name,
|
$method_storage->cased_name,
|
||||||
@ -1957,6 +1986,26 @@ final class Codebase
|
|||||||
return $completion_items;
|
return $completion_items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<CompletionItem> $items
|
||||||
|
* @return list<CompletionItem>
|
||||||
|
*/
|
||||||
|
public function filterCompletionItemsByBeginLiteralPart(array $items, string $literal_part): array
|
||||||
|
{
|
||||||
|
if (!$literal_part) {
|
||||||
|
return $items;
|
||||||
|
}
|
||||||
|
|
||||||
|
$res = [];
|
||||||
|
foreach ($items as $item) {
|
||||||
|
if ($item->insertText && strpos($item->insertText, $literal_part) === 0) {
|
||||||
|
$res[] = $item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $res;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return list<CompletionItem>
|
* @return list<CompletionItem>
|
||||||
*/
|
*/
|
||||||
|
@ -297,6 +297,7 @@ class TextDocument
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
$completion_data = $this->codebase->getCompletionDataAtPosition($file_path, $position);
|
$completion_data = $this->codebase->getCompletionDataAtPosition($file_path, $position);
|
||||||
|
$literal_part = $this->codebase->getBeginedLiteralPart($file_path, $position);
|
||||||
if ($completion_data) {
|
if ($completion_data) {
|
||||||
[$recent_type, $gap, $offset] = $completion_data;
|
[$recent_type, $gap, $offset] = $completion_data;
|
||||||
|
|
||||||
@ -305,6 +306,8 @@ class TextDocument
|
|||||||
->textDocument->completion->completionItem->snippetSupport ?? false;
|
->textDocument->completion->completionItem->snippetSupport ?? false;
|
||||||
$completion_items =
|
$completion_items =
|
||||||
$this->codebase->getCompletionItemsForClassishThing($recent_type, $gap, $snippetSupport);
|
$this->codebase->getCompletionItemsForClassishThing($recent_type, $gap, $snippetSupport);
|
||||||
|
$completion_items =
|
||||||
|
$this->codebase->filterCompletionItemsByBeginLiteralPart($completion_items, $literal_part);
|
||||||
} elseif ($gap === '[') {
|
} elseif ($gap === '[') {
|
||||||
$completion_items = $this->codebase->getCompletionItemsForArrayKeys($recent_type);
|
$completion_items = $this->codebase->getCompletionItemsForArrayKeys($recent_type);
|
||||||
} else {
|
} else {
|
||||||
|
@ -15,6 +15,7 @@ use Psalm\Tests\TestCase;
|
|||||||
use Psalm\Tests\TestConfig;
|
use Psalm\Tests\TestConfig;
|
||||||
use Psalm\Type;
|
use Psalm\Type;
|
||||||
|
|
||||||
|
use function array_map;
|
||||||
use function count;
|
use function count;
|
||||||
|
|
||||||
class CompletionTest extends TestCase
|
class CompletionTest extends TestCase
|
||||||
@ -370,7 +371,7 @@ class CompletionTest extends TestCase
|
|||||||
$codebase->scanFiles();
|
$codebase->scanFiles();
|
||||||
$this->analyzeFile('somefile.php', new Context());
|
$this->analyzeFile('somefile.php', new Context());
|
||||||
|
|
||||||
$this->assertNull($codebase->getCompletionDataAtPosition('somefile.php', new Position(16, 41)));
|
$this->assertSame(['B\C', '->', 456], $codebase->getCompletionDataAtPosition('somefile.php', new Position(16, 41)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testCompletionOnTemplatedThisProperty(): void
|
public function testCompletionOnTemplatedThisProperty(): void
|
||||||
@ -725,6 +726,201 @@ class CompletionTest extends TestCase
|
|||||||
$this->assertSame('baz()', $completion_items[1]->insertText);
|
$this->assertSame('baz()', $completion_items[1]->insertText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testObjectPropertyOnAppendToEnd(): void
|
||||||
|
{
|
||||||
|
$codebase = $this->codebase;
|
||||||
|
$config = $codebase->config;
|
||||||
|
$config->throw_exception = false;
|
||||||
|
|
||||||
|
$this->addFile(
|
||||||
|
'somefile.php',
|
||||||
|
'<?php
|
||||||
|
namespace B;
|
||||||
|
|
||||||
|
class A {
|
||||||
|
public $aProp = 123;
|
||||||
|
public $bProp = 234;
|
||||||
|
|
||||||
|
public function bar() {
|
||||||
|
$this->aPr
|
||||||
|
}
|
||||||
|
}',
|
||||||
|
);
|
||||||
|
|
||||||
|
$codebase->file_provider->openFile('somefile.php');
|
||||||
|
$codebase->scanFiles();
|
||||||
|
|
||||||
|
$this->analyzeFile('somefile.php', new Context());
|
||||||
|
|
||||||
|
$position = new Position(8, 34);
|
||||||
|
$completion_data = $codebase->getCompletionDataAtPosition('somefile.php', $position);
|
||||||
|
$literal_part = $codebase->getBeginedLiteralPart('somefile.php', $position);
|
||||||
|
|
||||||
|
$this->assertSame(['B\A&static', '->', 223], $completion_data);
|
||||||
|
|
||||||
|
$completion_items = $codebase->getCompletionItemsForClassishThing($completion_data[0], $completion_data[1], true);
|
||||||
|
$completion_items = $codebase->filterCompletionItemsByBeginLiteralPart($completion_items, $literal_part);
|
||||||
|
$completion_item_texts = array_map(fn($item) => $item->insertText, $completion_items);
|
||||||
|
|
||||||
|
$this->assertSame(['aProp'], $completion_item_texts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testObjectPropertyOnReplaceEndPart(): void
|
||||||
|
{
|
||||||
|
$codebase = $this->codebase;
|
||||||
|
$config = $codebase->config;
|
||||||
|
$config->throw_exception = false;
|
||||||
|
|
||||||
|
$this->addFile(
|
||||||
|
'somefile.php',
|
||||||
|
'<?php
|
||||||
|
namespace B;
|
||||||
|
|
||||||
|
class A {
|
||||||
|
public $aProp1 = 123;
|
||||||
|
public $aProp2 = 234;
|
||||||
|
|
||||||
|
public function bar() {
|
||||||
|
$this->aProp2;
|
||||||
|
}
|
||||||
|
}',
|
||||||
|
);
|
||||||
|
|
||||||
|
$codebase->file_provider->openFile('somefile.php');
|
||||||
|
$codebase->scanFiles();
|
||||||
|
|
||||||
|
$this->analyzeFile('somefile.php', new Context());
|
||||||
|
|
||||||
|
$position = new Position(8, 34);
|
||||||
|
$completion_data = $codebase->getCompletionDataAtPosition('somefile.php', $position);
|
||||||
|
$literal_part = $codebase->getBeginedLiteralPart('somefile.php', $position);
|
||||||
|
|
||||||
|
$this->assertSame(['B\A&static', '->', 225], $completion_data);
|
||||||
|
|
||||||
|
$completion_items = $codebase->getCompletionItemsForClassishThing($completion_data[0], $completion_data[1], true);
|
||||||
|
$completion_items = $codebase->filterCompletionItemsByBeginLiteralPart($completion_items, $literal_part);
|
||||||
|
$completion_item_texts = array_map(fn($item) => $item->insertText, $completion_items);
|
||||||
|
|
||||||
|
$this->assertSame(['aProp1', 'aProp2'], $completion_item_texts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSelfPropertyOnAppendToEnd(): void
|
||||||
|
{
|
||||||
|
$codebase = $this->codebase;
|
||||||
|
$config = $codebase->config;
|
||||||
|
$config->throw_exception = false;
|
||||||
|
|
||||||
|
$this->addFile(
|
||||||
|
'somefile.php',
|
||||||
|
'<?php
|
||||||
|
namespace B;
|
||||||
|
|
||||||
|
class A {
|
||||||
|
public static $aProp = 123;
|
||||||
|
public static $bProp = 234;
|
||||||
|
|
||||||
|
public function bar() {
|
||||||
|
self::$aPr
|
||||||
|
}
|
||||||
|
}',
|
||||||
|
);
|
||||||
|
|
||||||
|
$codebase->file_provider->openFile('somefile.php');
|
||||||
|
$codebase->scanFiles();
|
||||||
|
|
||||||
|
$this->analyzeFile('somefile.php', new Context());
|
||||||
|
|
||||||
|
$position = new Position(8, 34);
|
||||||
|
$completion_data = $codebase->getCompletionDataAtPosition('somefile.php', $position);
|
||||||
|
$literal_part = $codebase->getBeginedLiteralPart('somefile.php', $position);
|
||||||
|
|
||||||
|
$this->assertSame(['B\A', '::', 237], $completion_data);
|
||||||
|
|
||||||
|
$completion_items = $codebase->getCompletionItemsForClassishThing($completion_data[0], $completion_data[1], true);
|
||||||
|
$completion_items = $codebase->filterCompletionItemsByBeginLiteralPart($completion_items, $literal_part);
|
||||||
|
$completion_item_texts = array_map(fn($item) => $item->insertText, $completion_items);
|
||||||
|
|
||||||
|
$this->assertSame(['$aProp'], $completion_item_texts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testStaticPropertyOnAppendToEnd(): void
|
||||||
|
{
|
||||||
|
$codebase = $this->codebase;
|
||||||
|
$config = $codebase->config;
|
||||||
|
$config->throw_exception = false;
|
||||||
|
|
||||||
|
$this->addFile(
|
||||||
|
'somefile.php',
|
||||||
|
'<?php
|
||||||
|
namespace B;
|
||||||
|
|
||||||
|
class A {
|
||||||
|
public static $aProp = 123;
|
||||||
|
public static $bProp = 234;
|
||||||
|
|
||||||
|
public function bar() {
|
||||||
|
static::$aPr
|
||||||
|
}
|
||||||
|
}',
|
||||||
|
);
|
||||||
|
|
||||||
|
$codebase->file_provider->openFile('somefile.php');
|
||||||
|
$codebase->scanFiles();
|
||||||
|
|
||||||
|
$this->analyzeFile('somefile.php', new Context());
|
||||||
|
|
||||||
|
$position = new Position(8, 36);
|
||||||
|
$completion_data = $codebase->getCompletionDataAtPosition('somefile.php', $position);
|
||||||
|
$literal_part = $codebase->getBeginedLiteralPart('somefile.php', $position);
|
||||||
|
|
||||||
|
$this->assertSame(['B\A', '::', 239], $completion_data);
|
||||||
|
|
||||||
|
$completion_items = $codebase->getCompletionItemsForClassishThing($completion_data[0], $completion_data[1], true);
|
||||||
|
$completion_items = $codebase->filterCompletionItemsByBeginLiteralPart($completion_items, $literal_part);
|
||||||
|
$completion_item_texts = array_map(fn($item) => $item->insertText, $completion_items);
|
||||||
|
|
||||||
|
$this->assertSame(['$aProp'], $completion_item_texts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testStaticPropertyOnReplaceEndPart(): void
|
||||||
|
{
|
||||||
|
$codebase = $this->codebase;
|
||||||
|
$config = $codebase->config;
|
||||||
|
$config->throw_exception = false;
|
||||||
|
|
||||||
|
$this->addFile(
|
||||||
|
'somefile.php',
|
||||||
|
'<?php
|
||||||
|
namespace B;
|
||||||
|
|
||||||
|
class A {
|
||||||
|
public static $aProp1 = 123;
|
||||||
|
public static $aProp2 = 234;
|
||||||
|
|
||||||
|
public function bar() {
|
||||||
|
self::$aProp2;
|
||||||
|
}
|
||||||
|
}',
|
||||||
|
);
|
||||||
|
|
||||||
|
$codebase->file_provider->openFile('somefile.php');
|
||||||
|
$codebase->scanFiles();
|
||||||
|
|
||||||
|
$this->analyzeFile('somefile.php', new Context());
|
||||||
|
|
||||||
|
$position = new Position(8, 34);
|
||||||
|
$completion_data = $codebase->getCompletionDataAtPosition('somefile.php', $position);
|
||||||
|
$literal_part = $codebase->getBeginedLiteralPart('somefile.php', $position);
|
||||||
|
|
||||||
|
$this->assertSame(['B\A', '::', 239], $completion_data);
|
||||||
|
|
||||||
|
$completion_items = $codebase->getCompletionItemsForClassishThing($completion_data[0], $completion_data[1], true);
|
||||||
|
$completion_items = $codebase->filterCompletionItemsByBeginLiteralPart($completion_items, $literal_part);
|
||||||
|
$completion_item_texts = array_map(fn($item) => $item->insertText, $completion_items);
|
||||||
|
|
||||||
|
$this->assertSame(['$aProp1', '$aProp2'], $completion_item_texts);
|
||||||
|
}
|
||||||
|
|
||||||
public function testCompletionOnNewExceptionWithoutNamespace(): void
|
public function testCompletionOnNewExceptionWithoutNamespace(): void
|
||||||
{
|
{
|
||||||
$codebase = $this->codebase;
|
$codebase = $this->codebase;
|
||||||
@ -1260,6 +1456,38 @@ class CompletionTest extends TestCase
|
|||||||
$this->assertCount(2, $completion_items);
|
$this->assertCount(2, $completion_items);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testCompletionStaticMethodOnDocBlock(): void
|
||||||
|
{
|
||||||
|
$codebase = $this->codebase;
|
||||||
|
$config = $codebase->config;
|
||||||
|
$config->throw_exception = false;
|
||||||
|
|
||||||
|
$this->addFile(
|
||||||
|
'somefile.php',
|
||||||
|
'<?php
|
||||||
|
namespace Bar;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @method static void foo()
|
||||||
|
*/
|
||||||
|
class Alpha {}
|
||||||
|
Alpha::',
|
||||||
|
);
|
||||||
|
|
||||||
|
$codebase->file_provider->openFile('somefile.php');
|
||||||
|
$codebase->scanFiles();
|
||||||
|
$this->analyzeFile('somefile.php', new Context());
|
||||||
|
|
||||||
|
$position = new Position(7, 23);
|
||||||
|
$completion_data = $codebase->getCompletionDataAtPosition('somefile.php', $position);
|
||||||
|
|
||||||
|
$this->assertSame(['Bar\Alpha', '::', 177], $completion_data);
|
||||||
|
|
||||||
|
$completion_items = $codebase->getCompletionItemsForClassishThing($completion_data[0], $completion_data[1], true);
|
||||||
|
$this->assertCount(1, $completion_items);
|
||||||
|
$this->assertSame('foo()', $completion_items[0]->insertText);
|
||||||
|
}
|
||||||
|
|
||||||
public function testCompletionOnClassInstanceReferenceWithAssignmentAfter(): void
|
public function testCompletionOnClassInstanceReferenceWithAssignmentAfter(): void
|
||||||
{
|
{
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user