1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-02 09:37:59 +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\CodeLocation\Raw;
use Psalm\Exception\UnanalyzedFileException; use Psalm\Exception\UnanalyzedFileException;
use Psalm\Exception\UnpopulatedClasslikeException; use Psalm\Exception\UnpopulatedClasslikeException;
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
use Psalm\Internal\Analyzer\FunctionLikeAnalyzer; use Psalm\Internal\Analyzer\FunctionLikeAnalyzer;
use Psalm\Internal\Analyzer\NamespaceAnalyzer; use Psalm\Internal\Analyzer\NamespaceAnalyzer;
use Psalm\Internal\Analyzer\ProjectAnalyzer; use Psalm\Internal\Analyzer\ProjectAnalyzer;
@ -69,7 +70,6 @@ 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;
@ -1868,13 +1868,26 @@ final class Codebase
} }
/** /**
* @param list<int> $allow_visibilities
* @param list<string> $ignore_fq_class_names
* @return list<CompletionItem> * @return list<CompletionItem>
*/ */
public function getCompletionItemsForClassishThing( public function getCompletionItemsForClassishThing(
string $type_string, string $type_string,
string $gap, string $gap,
bool $snippets_supported = false bool $snippets_supported = false,
array $allow_visibilities = null,
array $ignore_fq_class_names = []
): array { ): array {
if ($allow_visibilities === null) {
$allow_visibilities = [
ClassLikeAnalyzer::VISIBILITY_PUBLIC,
ClassLikeAnalyzer::VISIBILITY_PROTECTED,
ClassLikeAnalyzer::VISIBILITY_PRIVATE,
];
}
$allow_visibilities[] = null;
$completion_items = []; $completion_items = [];
$type = Type::parseString($type_string); $type = Type::parseString($type_string);
@ -1884,12 +1897,25 @@ 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);
$methods = array_merge( $method_storages = [];
$class_storage->methods, foreach ($class_storage->declaring_method_ids as $declaring_method_id) {
$class_storage->pseudo_methods, try {
$class_storage->pseudo_static_methods, $method_storages[] = $this->methods->getStorage($declaring_method_id);
); } catch (UnexpectedValueException $e) {
foreach ($methods as $method_storage) { 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 === '->') { if ($method_storage->is_static || $gap === '->') {
$completion_item = new CompletionItem( $completion_item = new CompletionItem(
$method_storage->cased_name, $method_storage->cased_name,
@ -1918,43 +1944,51 @@ final class Codebase
} }
} }
$pseudo_property_types = []; if ($gap === '->') {
foreach ($class_storage->pseudo_property_get_types as $property_name => $type) { $pseudo_property_types = [];
$pseudo_property_types[$property_name] = new CompletionItem( foreach ($class_storage->pseudo_property_get_types as $property_name => $type) {
str_replace('$', '', $property_name), $pseudo_property_types[$property_name] = new CompletionItem(
CompletionItemKind::PROPERTY,
$type->__toString(),
null,
'1', //sort text
str_replace('$', '', $property_name),
($gap === '::' ? '$' : '') .
str_replace('$', '', $property_name), str_replace('$', '', $property_name),
); CompletionItemKind::PROPERTY,
} $type->__toString(),
null,
foreach ($class_storage->pseudo_property_set_types as $property_name => $type) { '1', //sort text
$pseudo_property_types[$property_name] = new CompletionItem(
str_replace('$', '', $property_name),
CompletionItemKind::PROPERTY,
$type->__toString(),
null,
'1',
str_replace('$', '', $property_name),
($gap === '::' ? '$' : '') .
str_replace('$', '', $property_name), 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) { foreach ($class_storage->declaring_property_ids as $property_name => $declaring_class) {
$property_storage = $this->properties->getStorage( try {
$declaring_class . '::$' . $property_name, $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( $completion_items[] = new CompletionItem(
'$' . $property_name, $property_name,
CompletionItemKind::PROPERTY, CompletionItemKind::PROPERTY,
$property_storage->getInfo(), $property_storage->getInfo(),
$property_storage->description, $property_storage->description,
@ -1976,6 +2010,22 @@ final class Codebase
$const_name, $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) { } catch (Exception $e) {
error_log($e->getMessage()); error_log($e->getMessage());
continue; continue;

View File

@ -450,6 +450,8 @@ final class Populator
$storage->pseudo_property_get_types += $trait_storage->pseudo_property_get_types; $storage->pseudo_property_get_types += $trait_storage->pseudo_property_get_types;
$storage->pseudo_property_set_types += $trait_storage->pseudo_property_set_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->pseudo_methods += $trait_storage->pseudo_methods;
$storage->declaring_pseudo_method_ids += $trait_storage->declaring_pseudo_method_ids; $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; $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->pseudo_methods += $parent_storage->pseudo_methods;
$storage->declaring_pseudo_method_ids += $parent_storage->declaring_pseudo_method_ids; $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);
}
}