1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-30 04:39:00 +01:00

Merge pull request #10385 from issidorov/finding-methods-and-properties-for-auto-completion-feature

Finding methods and properties for auto completion feature
This commit is contained in:
orklah 2023-11-17 20:41:11 +01:00 committed by GitHub
commit 44a0d4ce9f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 673 additions and 38 deletions

View File

@ -18,6 +18,7 @@ use PhpParser\Node\Arg;
use Psalm\CodeLocation\Raw;
use Psalm\Exception\UnanalyzedFileException;
use Psalm\Exception\UnpopulatedClasslikeException;
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
use Psalm\Internal\Analyzer\FunctionLikeAnalyzer;
use Psalm\Internal\Analyzer\NamespaceAnalyzer;
use Psalm\Internal\Analyzer\ProjectAnalyzer;
@ -69,7 +70,6 @@ use ReflectionType;
use UnexpectedValueException;
use function array_combine;
use function array_merge;
use function array_pop;
use function array_reverse;
use function array_values;
@ -1868,13 +1868,26 @@ final class Codebase
}
/**
* @param list<int> $allow_visibilities
* @param list<string> $ignore_fq_class_names
* @return list<CompletionItem>
*/
public function getCompletionItemsForClassishThing(
string $type_string,
string $gap,
bool $snippets_supported = false
bool $snippets_supported = false,
array $allow_visibilities = null,
array $ignore_fq_class_names = []
): array {
if ($allow_visibilities === null) {
$allow_visibilities = [
ClassLikeAnalyzer::VISIBILITY_PUBLIC,
ClassLikeAnalyzer::VISIBILITY_PROTECTED,
ClassLikeAnalyzer::VISIBILITY_PRIVATE,
];
}
$allow_visibilities[] = null;
$completion_items = [];
$type = Type::parseString($type_string);
@ -1884,12 +1897,25 @@ final class Codebase
try {
$class_storage = $this->classlike_storage_provider->get($atomic_type->value);
$methods = array_merge(
$class_storage->methods,
$class_storage->pseudo_methods,
$class_storage->pseudo_static_methods,
);
foreach ($methods as $method_storage) {
$method_storages = [];
foreach ($class_storage->declaring_method_ids as $declaring_method_id) {
try {
$method_storages[] = $this->methods->getStorage($declaring_method_id);
} catch (UnexpectedValueException $e) {
error_log($e->getMessage());
}
}
if ($gap === '->') {
$method_storages += $class_storage->pseudo_methods;
}
if ($gap === '::') {
$method_storages += $class_storage->pseudo_static_methods;
}
foreach ($method_storages as $method_storage) {
if (!in_array($method_storage->visibility, $allow_visibilities)) {
continue;
}
if ($method_storage->is_static || $gap === '->') {
$completion_item = new CompletionItem(
$method_storage->cased_name,
@ -1918,43 +1944,51 @@ final class Codebase
}
}
$pseudo_property_types = [];
foreach ($class_storage->pseudo_property_get_types as $property_name => $type) {
$pseudo_property_types[$property_name] = new CompletionItem(
str_replace('$', '', $property_name),
CompletionItemKind::PROPERTY,
$type->__toString(),
null,
'1', //sort text
str_replace('$', '', $property_name),
($gap === '::' ? '$' : '') .
if ($gap === '->') {
$pseudo_property_types = [];
foreach ($class_storage->pseudo_property_get_types as $property_name => $type) {
$pseudo_property_types[$property_name] = new CompletionItem(
str_replace('$', '', $property_name),
);
}
foreach ($class_storage->pseudo_property_set_types as $property_name => $type) {
$pseudo_property_types[$property_name] = new CompletionItem(
str_replace('$', '', $property_name),
CompletionItemKind::PROPERTY,
$type->__toString(),
null,
'1',
str_replace('$', '', $property_name),
($gap === '::' ? '$' : '') .
CompletionItemKind::PROPERTY,
$type->__toString(),
null,
'1', //sort text
str_replace('$', '', $property_name),
);
str_replace('$', '', $property_name),
);
}
foreach ($class_storage->pseudo_property_set_types as $property_name => $type) {
$pseudo_property_types[$property_name] = new CompletionItem(
str_replace('$', '', $property_name),
CompletionItemKind::PROPERTY,
$type->__toString(),
null,
'1',
str_replace('$', '', $property_name),
str_replace('$', '', $property_name),
);
}
$completion_items = [...$completion_items, ...array_values($pseudo_property_types)];
}
$completion_items = [...$completion_items, ...array_values($pseudo_property_types)];
foreach ($class_storage->declaring_property_ids as $property_name => $declaring_class) {
$property_storage = $this->properties->getStorage(
$declaring_class . '::$' . $property_name,
);
try {
$property_storage = $this->properties->getStorage(
$declaring_class . '::$' . $property_name,
);
} catch (UnexpectedValueException $e) {
error_log($e->getMessage());
continue;
}
if ($property_storage->is_static || $gap === '->') {
if (!in_array($property_storage->visibility, $allow_visibilities)) {
continue;
}
if ($property_storage->is_static === ($gap === '::')) {
$completion_items[] = new CompletionItem(
'$' . $property_name,
$property_name,
CompletionItemKind::PROPERTY,
$property_storage->getInfo(),
$property_storage->description,
@ -1976,6 +2010,22 @@ final class Codebase
$const_name,
);
}
if ($gap === '->') {
foreach ($class_storage->namedMixins as $mixin) {
if (in_array($mixin->value, $ignore_fq_class_names)) {
continue;
}
$mixin_completion_items = $this->getCompletionItemsForClassishThing(
$mixin->value,
$gap,
$snippets_supported,
[ClassLikeAnalyzer::VISIBILITY_PUBLIC],
[$type_string, ...$ignore_fq_class_names],
);
$completion_items = [...$completion_items, ...$mixin_completion_items];
}
}
} catch (Exception $e) {
error_log($e->getMessage());
continue;

View File

@ -450,6 +450,8 @@ final class Populator
$storage->pseudo_property_get_types += $trait_storage->pseudo_property_get_types;
$storage->pseudo_property_set_types += $trait_storage->pseudo_property_set_types;
$storage->pseudo_static_methods += $trait_storage->pseudo_static_methods;
$storage->pseudo_methods += $trait_storage->pseudo_methods;
$storage->declaring_pseudo_method_ids += $trait_storage->declaring_pseudo_method_ids;
}
@ -560,6 +562,8 @@ final class Populator
$parent_storage->dependent_classlikes[strtolower($storage->name)] = true;
$storage->pseudo_static_methods += $parent_storage->pseudo_static_methods;
$storage->pseudo_methods += $parent_storage->pseudo_methods;
$storage->declaring_pseudo_method_ids += $parent_storage->declaring_pseudo_method_ids;
}

View File

@ -0,0 +1,581 @@
<?php
declare(strict_types=1);
namespace Psalm\Tests\Internal\Codebase;
use Psalm\Codebase;
use Psalm\Context;
use Psalm\Internal\Analyzer\ProjectAnalyzer;
use Psalm\Internal\Provider\FakeFileProvider;
use Psalm\Internal\Provider\Providers;
use Psalm\Tests\Internal\Provider\FakeFileReferenceCacheProvider;
use Psalm\Tests\Internal\Provider\ParserInstanceCacheProvider;
use Psalm\Tests\Internal\Provider\ProjectCacheProvider;
use Psalm\Tests\TestCase;
use Psalm\Tests\TestConfig;
use function array_map;
/**
* Fat tests for method `getCompletionItemsForClassishThing` of class `Psalm\Codebase`.
*/
final class MethodGetCompletionItemsForClassishThingTest extends TestCase
{
private Codebase $codebase;
public function setUp(): void
{
parent::setUp();
$this->file_provider = new FakeFileProvider();
$config = new TestConfig();
$providers = new Providers(
$this->file_provider,
new ParserInstanceCacheProvider(),
null,
null,
new FakeFileReferenceCacheProvider(),
new ProjectCacheProvider(),
);
$this->codebase = new Codebase($config, $providers);
$this->project_analyzer = new ProjectAnalyzer(
$config,
$providers,
null,
[],
1,
null,
$this->codebase,
);
$this->project_analyzer->setPhpVersion('7.3', 'tests');
$this->project_analyzer->getCodebase()->store_node_types = true;
$this->codebase->config->throw_exception = false;
}
/**
* @return list<string>
*/
protected function getCompletionLabels(string $content, string $class_name, string $gap): array
{
$this->addFile('somefile.php', $content);
$this->analyzeFile('somefile.php', new Context());
$items = $this->codebase->getCompletionItemsForClassishThing($class_name, $gap, true);
return array_map(fn($item) => $item->label, $items);
}
/**
* @return iterable<array-key, array{0: string}>
*/
public function providerGaps(): iterable
{
return [
'object-gap' => ['->'],
'static-gap' => ['::'],
];
}
/**
* @dataProvider providerGaps
*/
public function testSimpleOnceClass(string $gap): void
{
$content = <<<'EOF'
<?php
namespace B;
/**
* @property int $magicObjProp1
* @property-read string $magicObjProp2
* @method int magicObjMethod()
* @method static string magicStaticMethod()
*/
class A {
public $publicObjProp;
protected $protectedObjProp;
private $privateObjProp;
public static $publicStaticProp;
protected static $protectedStaticProp;
private static $privateStaticProp;
public function publicObjMethod() {}
protected function protectedObjMethod() {}
private function privateObjMethod() {}
public static function publicStaticMethod() {}
protected static function protectedStaticMethod() {}
private static function privateStaticMethod() {}
}
EOF;
$actual_labels = $this->getCompletionLabels($content, 'B\A', $gap);
$expected_labels = [
'->' => [
'magicObjProp1',
'magicObjProp2',
'magicObjMethod',
'publicObjProp',
'protectedObjProp',
'privateObjProp',
'publicObjMethod',
'protectedObjMethod',
'privateObjMethod',
'publicStaticMethod',
'protectedStaticMethod',
'privateStaticMethod',
],
'::' => [
'magicStaticMethod',
'publicStaticProp',
'protectedStaticProp',
'privateStaticProp',
'publicStaticMethod',
'protectedStaticMethod',
'privateStaticMethod',
],
];
$this->assertEqualsCanonicalizing($expected_labels[$gap], $actual_labels);
}
/**
* @dataProvider providerGaps
*/
public function testAbstractClass(string $gap): void
{
$content = <<<'EOF'
<?php
namespace B;
/**
* @property int $magicObjProp1
* @property-read string $magicObjProp2
* @method int magicObjMethod()
* @method static string magicStaticMethod()
*/
abstract class A {
public $publicObjProp;
protected $protectedObjProp;
private $privateObjProp;
public static $publicStaticProp;
protected static $protectedStaticProp;
private static $privateStaticProp;
abstract public function abstractPublicMethod();
abstract protected function abstractProtectedMethod();
public function publicObjMethod() {}
protected function protectedObjMethod() {}
private function privateObjMethod() {}
public static function publicStaticMethod() {}
protected static function protectedStaticMethod() {}
private static function privateStaticMethod() {}
}
EOF;
$actual_labels = $this->getCompletionLabels($content, 'B\A', $gap);
$expected_labels = [
'->' => [
'magicObjProp1',
'magicObjProp2',
'magicObjMethod',
'publicObjProp',
'protectedObjProp',
'privateObjProp',
'abstractPublicMethod',
'abstractProtectedMethod',
'publicObjMethod',
'protectedObjMethod',
'privateObjMethod',
'publicStaticMethod',
'protectedStaticMethod',
'privateStaticMethod',
],
'::' => [
'magicStaticMethod',
'publicStaticProp',
'protectedStaticProp',
'privateStaticProp',
'publicStaticMethod',
'protectedStaticMethod',
'privateStaticMethod',
],
];
$this->assertEqualsCanonicalizing($expected_labels[$gap], $actual_labels);
}
/**
* @dataProvider providerGaps
*/
public function testUseTrait(string $gap): void
{
$content = <<<'EOF'
<?php
namespace B;
/**
* @property int $magicObjProp1
* @property-read string $magicObjProp2
* @method int magicObjMethod()
* @method static string magicStaticMethod()
*/
trait C {
public $publicObjProp;
protected $protectedObjProp;
private $privateObjProp;
public static $publicStaticProp;
protected static $protectedStaticProp;
private static $privateStaticProp;
abstract public function abstractPublicMethod();
abstract protected function abstractProtectedMethod();
public function publicObjMethod() {}
protected function protectedObjMethod() {}
private function privateObjMethod() {}
public static function publicStaticMethod() {}
protected static function protectedStaticMethod() {}
private static function privateStaticMethod() {}
}
class A {
use C;
}
EOF;
$actual_labels = $this->getCompletionLabels($content, 'B\A', $gap);
$expected_labels = [
'->' => [
'magicObjProp1',
'magicObjProp2',
'magicObjMethod',
'publicObjProp',
'protectedObjProp',
'privateObjProp',
'abstractPublicMethod',
'abstractProtectedMethod',
'publicObjMethod',
'protectedObjMethod',
'privateObjMethod',
'publicStaticMethod',
'protectedStaticMethod',
'privateStaticMethod',
],
'::' => [
'magicStaticMethod',
'publicStaticProp',
'protectedStaticProp',
'privateStaticProp',
'publicStaticMethod',
'protectedStaticMethod',
'privateStaticMethod',
],
];
$this->assertEqualsCanonicalizing($expected_labels[$gap], $actual_labels);
}
/**
* @dataProvider providerGaps
*/
public function testUseTraitWithAbstractClass(string $gap): void
{
$content = <<<'EOF'
<?php
namespace B;
/**
* @property int $magicObjProp1
* @property-read string $magicObjProp2
* @method int magicObjMethod()
* @method static string magicStaticMethod()
*/
trait C {
public $publicObjProp;
protected $protectedObjProp;
private $privateObjProp;
public static $publicStaticProp;
protected static $protectedStaticProp;
private static $privateStaticProp;
abstract public function abstractPublicMethod();
abstract protected function abstractProtectedMethod();
public function publicObjMethod() {}
protected function protectedObjMethod() {}
private function privateObjMethod() {}
public static function publicStaticMethod() {}
protected static function protectedStaticMethod() {}
private static function privateStaticMethod() {}
}
abstract class A {
use C;
}
EOF;
$actual_labels = $this->getCompletionLabels($content, 'B\A', $gap);
$expected_labels = [
'->' => [
'magicObjProp1',
'magicObjProp2',
'magicObjMethod',
'publicObjProp',
'protectedObjProp',
'privateObjProp',
'abstractPublicMethod',
'abstractProtectedMethod',
'publicObjMethod',
'protectedObjMethod',
'privateObjMethod',
'publicStaticMethod',
'protectedStaticMethod',
'privateStaticMethod',
],
'::' => [
'magicStaticMethod',
'publicStaticProp',
'protectedStaticProp',
'privateStaticProp',
'publicStaticMethod',
'protectedStaticMethod',
'privateStaticMethod',
],
];
$this->assertEqualsCanonicalizing($expected_labels[$gap], $actual_labels);
}
/**
* @dataProvider providerGaps
*/
public function testClassWithExtends(string $gap): void
{
$content = <<<'EOF'
<?php
namespace B;
/**
* @property int $magicObjProp1
* @property-read string $magicObjProp2
* @method int magicObjMethod()
* @method static string magicStaticMethod()
*/
class C {
public $publicObjProp;
protected $protectedObjProp;
private $privateObjProp;
public static $publicStaticProp;
protected static $protectedStaticProp;
private static $privateStaticProp;
public function publicObjMethod() {}
protected function protectedObjMethod() {}
private function privateObjMethod() {}
public static function publicStaticMethod() {}
protected static function protectedStaticMethod() {}
private static function privateStaticMethod() {}
}
class A extends C {
}
EOF;
$actual_labels = $this->getCompletionLabels($content, 'B\A', $gap);
$expected_labels = [
'->' => [
'magicObjProp1',
'magicObjProp2',
'magicObjMethod',
'publicObjProp',
'protectedObjProp',
'publicObjMethod',
'protectedObjMethod',
'publicStaticMethod',
'protectedStaticMethod',
],
'::' => [
'magicStaticMethod',
'publicStaticProp',
'protectedStaticProp',
'publicStaticMethod',
'protectedStaticMethod',
],
];
$this->assertEqualsCanonicalizing($expected_labels[$gap], $actual_labels);
}
/**
* @dataProvider providerGaps
*/
public function testAstractClassWithInterface(string $gap): void
{
$content = <<<'EOF'
<?php
namespace B;
interface C {
public function publicObjMethod();
protected function protectedObjMethod();
}
abstract class A implements C {
abstract public function publicObjMethod();
abstract protected function protectedObjMethod();
}
EOF;
$actual_labels = $this->getCompletionLabels($content, 'B\A', $gap);
$expected_labels = [
'->' => [
'publicObjMethod',
'protectedObjMethod',
],
'::' => [],
];
$this->assertEqualsCanonicalizing($expected_labels[$gap], $actual_labels);
}
/**
* @dataProvider providerGaps
*/
public function testClassWithAnnotationMixin(string $gap): void
{
$content = <<<'EOF'
<?php
namespace B;
/**
* @property int $magicObjProp1
* @property-read string $magicObjProp2
* @method int magicObjMethod()
* @method static string magicStaticMethod()
*/
class C {
public $publicObjProp;
protected $protectedObjProp;
private $privateObjProp;
public static $publicStaticProp;
protected static $protectedStaticProp;
private static $privateStaticProp;
public function publicObjMethod() {}
protected function protectedObjMethod() {}
private function privateObjMethod() {}
public static function publicStaticMethod() {}
protected static function protectedStaticMethod() {}
private static function privateStaticMethod() {}
}
/**
* @mixin C
*/
class A {
}
EOF;
$actual_labels = $this->getCompletionLabels($content, 'B\A', $gap);
$expected_labels = [
'->' => [
'magicObjProp1',
'magicObjProp2',
'magicObjMethod',
'publicObjProp',
'publicObjMethod',
'publicStaticMethod',
],
'::' => [],
];
$this->assertEqualsCanonicalizing($expected_labels[$gap], $actual_labels);
}
public function testResolveCollisionWithMixin(): void
{
$content = <<<'EOF'
<?php
namespace B;
/** @mixin A */
class C {
public $myObjProp;
}
/** @mixin C */
class A {}
EOF;
$actual_labels = $this->getCompletionLabels($content, 'B\A', '->');
$expected_labels = [
'myObjProp',
];
$this->assertEqualsCanonicalizing($expected_labels, $actual_labels);
}
}