Repository: phpactor/phpactor (php)

Issue:
Auto-completion not working for Enum instances
Hello! Thanks for all the work getting traits to work in Enums!

However, I noticed that auto-completion for Enum properties and methods does not work outside of the Enum (and the auto-completion that does occur inside the Enum is incorrect). Note that Enums are stateless and therefore are unable to have properties, but all Enums have a `name` property and BackedEnums have a `value` property.

Inside the Enum, auto-completion sort of works:
![image](https://github.com/phpactor/phpactor/assets/4176520/34074754-9237-449b-832e-cbd65fb24d51)
but it's not showing properties and it is showing static methods (`cases`, `from`, etc.) which don't make sense on an Enum instance.

Outside the Enum, there's no auto-completion suggestions from the LS.
![image](https://github.com/phpactor/phpactor/assets/4176520/7275c724-b223-46eb-aade-ca10df0b6c52)


```php
<?php

namespace Phpactor\Enums;

enum ScheduleType: int
{
    case ProductionDay = 1;
    case Break         = 2;
    case Unscheduled   = 3;

    public function label(): string
    {
        return match ($this) {
            self::ProductionDay => 'Production Day',
            default => $this->name,
        };
    }
}

class Test
{
    public function testing(): void
    {
        $a = ScheduleType::ProductionDay;
    }
}
```


Thanks!

Pull request description:
Fix enum case completion
Fixes #2284 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b466e992..02cb9dd6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,7 @@ Improvements:
 
 Bug fixes:
 
+  - Fix enum case completion #2284
   - Fix error handling for responses from language client #2283
   - Do not show named parameters after string literal argument #2259
   - Fix "instanceof" behavior for statically reflected classes #2273
diff --git a/doc/reference/configuration.rst b/doc/reference/configuration.rst
index edb47c7f..7da74d1f 100644
--- a/doc/reference/configuration.rst
+++ b/doc/reference/configuration.rst
@@ -1334,6 +1334,19 @@ Amount of time (in milliseconds) to wait before responding to a shutdown notific
 **Default**: ``200``
 
 
+.. _param_language_server.phpactor_bin:
+
+
+``language_server.phpactor_bin``
+""""""""""""""""""""""""""""""""
+
+
+Internal use only - name path to Phpactor binary
+
+
+**Default**: ``"\/phpactor\/lib\/Extension\/LanguageServer\/..\/..\/..\/bin\/phpactor"``
+
+
 .. _param_language_server.self_destruct_timeout:
 
 
diff --git a/lib/Completion/Bridge/TolerantParser/WorseReflection/WorseClassMemberCompletor.php b/lib/Completion/Bridge/TolerantParser/WorseReflection/WorseClassMemberCompletor.php
index 53242c9a..1faa9da4 100644
--- a/lib/Completion/Bridge/TolerantParser/WorseReflection/WorseClassMemberCompletor.php
+++ b/lib/Completion/Bridge/TolerantParser/WorseReflection/WorseClassMemberCompletor.php
@@ -20,7 +20,7 @@ use Phpactor\WorseReflection\Core\Reflection\ReflectionClass;
 use Phpactor\WorseReflection\Core\Reflection\ReflectionEnum;
 use Phpactor\WorseReflection\Core\Reflection\ReflectionProperty;
 use Phpactor\WorseReflection\Core\Type;
-use Phpactor\WorseReflection\Core\Type\ClassType;
+use Phpactor\WorseReflection\Core\Type\ClassLikeType;
 use Phpactor\WorseReflection\Reflector;
 use Phpactor\Completion\Core\Suggestion;
 use Phpactor\WorseReflection\Core\Reflection\ReflectionInterface;
@@ -111,7 +111,7 @@ class WorseClassMemberCompletor implements TolerantCompletor, TolerantQualifiabl
             return;
         }
 
-        if (!$type instanceof ClassType) {
+        if (!$type instanceof ClassLikeType) {
             return;
         }
 
diff --git a/lib/WorseReflection/Bridge/TolerantParser/Reflection/ReflectionEnumCase.php b/lib/WorseReflection/Bridge/TolerantParser/Reflection/ReflectionEnumCase.php
index 5e66ca69..d4676097 100644
--- a/lib/WorseReflection/Bridge/TolerantParser/Reflection/ReflectionEnumCase.php
+++ b/lib/WorseReflection/Bridge/TolerantParser/Reflection/ReflectionEnumCase.php
@@ -49,7 +49,7 @@ class ReflectionEnumCase extends AbstractReflectionClassMember implements CoreRe
     public function type(): Type
     {
         if ($this->class()->isBacked()) {
-            return TypeFactory::enumBackedCaseType($this->class()->type(), $this->name(), $this->value());
+            return TypeFactory::enumBackedCaseType($this->serviceLocator()->reflector(), $this->class()->type(), $this->name(), $this->value());
         }
         return TypeFactory::enumCaseType($this->serviceLocator()->reflector(), $this->class()->type(), $this->name());
     }
diff --git a/lib/WorseReflection/Core/Type/EnumBackedCaseType.php b/lib/WorseReflection/Core/Type/EnumBackedCaseType.php
index 409d30cc..c6f01637 100644
--- a/lib/WorseReflection/Core/Type/EnumBackedCaseType.php
+++ b/lib/WorseReflection/Core/Type/EnumBackedCaseType.php
@@ -3,19 +3,21 @@
 namespace Phpactor\WorseReflection\Core\Type;
 
 use Phpactor\WorseReflection\Core\ClassName;
+use Phpactor\WorseReflection\Core\Exception\NotFound;
 use Phpactor\WorseReflection\Core\Reflection\Collection\ReflectionMemberCollection;
+use Phpactor\WorseReflection\Core\Reflector\ClassReflector;
 use Phpactor\WorseReflection\Core\Trinary;
 use Phpactor\WorseReflection\Core\Type;
 
 class EnumBackedCaseType extends Type implements ClassLikeType
 {
-    public function __construct(public ClassType $enumType, public string $name, public Type $value)
+    public function __construct(private ClassReflector $reflector, public ClassType $enumType, public string $name, public Type $value)
     {
     }
 
     public function __toString(): string
     {
-        return sprintf('%s::%s', $this->enumType, $this->name);
+        return sprintf('enum(%s::%s)', $this->enumType, $this->name);
     }
 
     public function toPhpString(): string
@@ -35,6 +37,13 @@ class EnumBackedCaseType extends Type implements ClassLikeType
 
     public function members(): ReflectionMemberCollection
     {
-        return $this->enumType->members();
+        $members = $this->enumType->members();
+        try {
+            $case = $this->reflector->reflectClass('BackedEnumCase');
+        } catch (NotFound) {
+            return $members;
+        }
+        /** @phpstan-ignore-next-line */
+        return $members->merge($case->members()->properties());
     }
 }
diff --git a/lib/WorseReflection/Core/Type/EnumCaseType.php b/lib/WorseReflection/Core/Type/EnumCaseType.php
index bf9ddaab..fcaa4ccf 100644
--- a/lib/WorseReflection/Core/Type/EnumCaseType.php
+++ b/lib/WorseReflection/Core/Type/EnumCaseType.php
@@ -16,7 +16,7 @@ class EnumCaseType extends ReflectedClassType implements ClassLikeType
 
     public function __toString(): string
     {
-        return sprintf('%s::%s', $this->enumType, $this->caseName);
+        return sprintf('enum(%s::%s)', $this->enumType, $this->caseName);
     }
 
     public function toPhpString(): string
diff --git a/lib/WorseReflection/Core/TypeFactory.php b/lib/WorseReflection/Core/TypeFactory.php
index a746b165..653a74f0 100644
--- a/lib/WorseReflection/Core/TypeFactory.php
+++ b/lib/WorseReflection/Core/TypeFactory.php
@@ -365,9 +365,9 @@ class TypeFactory
         return new EnumCaseType($reflector, $enumType, $name);
     }
 
-    public static function enumBackedCaseType(ClassType $enumType, string $name, Type $value): EnumBackedCaseType
+    public static function enumBackedCaseType(Reflector $reflector, ClassType $enumType, string $name, Type $value): EnumBackedCaseType
     {
-        return new EnumBackedCaseType($enumType, $name, $value);
+        return new EnumBackedCaseType($reflector, $enumType, $name, $value);
     }
 
     public static function intRange(Type $lower, Type $upper): IntRangeType


Test patch:
diff --git a/lib/Completion/Tests/Integration/Bridge/TolerantParser/WorseReflection/WorseClassMemberCompletorTest.php b/lib/Completion/Tests/Integration/Bridge/TolerantParser/WorseReflection/WorseClassMemberCompletorTest.php
index e519be41..f5ea0af2 100644
--- a/lib/Completion/Tests/Integration/Bridge/TolerantParser/WorseReflection/WorseClassMemberCompletorTest.php
+++ b/lib/Completion/Tests/Integration/Bridge/TolerantParser/WorseReflection/WorseClassMemberCompletorTest.php
@@ -857,6 +857,47 @@ class WorseClassMemberCompletorTest extends TolerantCompletorTestCase
                 ]
             ];
 
+            yield 'enum case' => [
+                <<<'EOT'
+                    <?php
+
+                    enum Enum1: string {
+                        case FOOBAR = 'bar';
+                    }
+
+                    Enum1::FOOBAR-><>
+
+                    EOT
+                , [
+                    [
+                        'type' => Suggestion::TYPE_METHOD,
+                        'name' => 'cases',
+                        'short_description' => 'pub cases(): BackedEnumCase[]',
+                        'snippet' => 'cases()',
+                    ],
+                    [
+                        'type' => Suggestion::TYPE_METHOD,
+                        'name' => 'from',
+                        'short_description' => 'pub from(int|string $value): static(Enum1)',
+                        'snippet' => 'from(${1:\\$value})${0}',
+                    ],
+                    [
+                        'type' => Suggestion::TYPE_PROPERTY,
+                        'name' => 'name',
+                    ],
+                    [
+                        'type' => Suggestion::TYPE_METHOD,
+                        'name' => 'tryFrom',
+                        'short_description' => 'pub tryFrom(int|string $value): static(Enum1)|null',
+                        'snippet' => 'tryFrom(${1:\\$value})${0}',
+                    ],
+                    [
+                        'type' => Suggestion::TYPE_PROPERTY,
+                        'name' => 'value',
+                    ],
+                ]
+            ];
+
             /** See https://github.com/phpactor/phpactor/issues/1612
             yield 'backed enum' => [
                 <<<'EOT'
diff --git a/lib/WorseReflection/Tests/Inference/enum/backed_enum_case.test b/lib/WorseReflection/Tests/Inference/enum/backed_enum_case.test
index 28419350..4f738ff0 100644
--- a/lib/WorseReflection/Tests/Inference/enum/backed_enum_case.test
+++ b/lib/WorseReflection/Tests/Inference/enum/backed_enum_case.test
@@ -1,8 +1,9 @@
 <?php
 
-enum Foo: string 
+
+enum Foo: string  {
    case BAR = 'bar';
 }
 
-// need better generic support here
-wrAssertType('<missing>', Foo::FOO->value);
+wrAssertType('enum(Foo::BAR)', Foo::BAR);
+wrAssertType('int|string', Foo::BAR->value);
diff --git a/lib/WorseReflection/Tests/Inference/enum/enum_case.test b/lib/WorseReflection/Tests/Inference/enum/enum_case.test
index cbf22a1f..169c2004 100644
--- a/lib/WorseReflection/Tests/Inference/enum/enum_case.test
+++ b/lib/WorseReflection/Tests/Inference/enum/enum_case.test
@@ -5,7 +5,7 @@ enum Foo {
    const BAR = 'bar';
 }
 
-wrAssertType('Foo::FOO', Foo::FOO);
+wrAssertType('enum(Foo::FOO)', Foo::FOO);
 wrAssertType('<missing>', Foo::FOO->value);
 wrAssertType('string', Foo::FOO->name);
 wrAssertType('"bar"', Foo::BAR);
diff --git a/lib/WorseReflection/Tests/Integration/Bridge/TolerantParser/Reflection/ReflectionEnumTest.php b/lib/WorseReflection/Tests/Integration/Bridge/TolerantParser/Reflection/ReflectionEnumTest.php
index 325a1e1a..3a41d8b1 100644
--- a/lib/WorseReflection/Tests/Integration/Bridge/TolerantParser/Reflection/ReflectionEnumTest.php
+++ b/lib/WorseReflection/Tests/Integration/Bridge/TolerantParser/Reflection/ReflectionEnumTest.php
@@ -97,7 +97,7 @@ class ReflectionEnumTest extends IntegrationTestCase
             function (ReflectionEnum $class): void {
                 $case = $class->cases()->get('FOOBAR');
                 self::assertEquals('FOOBAR', $case->name());
-                self::assertEquals('Enum1::FOOBAR', $case->type()->__toString());
+                self::assertEquals('enum(Enum1::FOOBAR)', $case->type()->__toString());
                 self::assertInstanceOf(MissingType::class, $case->value());
                 self::assertInstanceOf(EnumCaseType::class, $case->type());
                 self::assertEquals('FOOBAR', $case->name());
@@ -120,7 +120,7 @@ class ReflectionEnumTest extends IntegrationTestCase
                 $case = $class->cases()->get('FOOBAR');
                 self::assertEquals('FOOBAR', $case->name());
                 self::assertEquals('"FOO"', $case->value()->__toString());
-                self::assertEquals('Enum1::FOOBAR', $case->type()->__toString());
+                self::assertEquals('enum(Enum1::FOOBAR)', $case->type()->__toString());
                 self::assertInstanceOf(EnumBackedCaseType::class, $case->type());
                 self::assertTrue($class->isBacked());
                 self::assertEquals('string', $class->backedType());