Repository: phpactor/phpactor (php)

Issue:
Jump to interface definition from implementation definition
It seems like PhpActor is not able to find the methods (type) definition from an interface.

Interface:
```php
<?php

interface FooInterface
{
    /**
     * @return string
     */
    public function getBar(): string;
}
```

Class:
```php
<?php

class Foo implements FooInterface
{
    public function getBar(): string
    {
        return 'abc';
    }
}
```
FooInterface::getBar() -> go to implementation -> works ✅
Foo::getBar() -> go to (type) definition -> does not work ❌


Let me know if you need any additional info.

Pull request description:
Goto parent member definition
For methods and properties, when going to definition, if the member is defined in a parent class (or interface), goto the parent class/interface.

Fixes #1883 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index edf469b8..72f7b4b1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,9 @@ Improvements:
 
   - [cmp/lsp] inline type information for completion items
 
+Features:
+
+  - [gtd] For member declarations, goto parent member definition if it exists #1886
 
 ## 2022.09.11
 
diff --git a/lib/WorseReferenceFinder/WorseReflectionDefinitionLocator.php b/lib/WorseReferenceFinder/WorseReflectionDefinitionLocator.php
index 0b4ed17d..4082491b 100644
--- a/lib/WorseReferenceFinder/WorseReflectionDefinitionLocator.php
+++ b/lib/WorseReferenceFinder/WorseReflectionDefinitionLocator.php
@@ -11,7 +11,9 @@ use Phpactor\TextDocument\Location;
 use Phpactor\TextDocument\TextDocument;
 use Phpactor\TextDocument\TextDocumentUri;
 use Phpactor\WorseReflection\Core\Cache;
+use Phpactor\WorseReflection\Core\ClassHierarchyResolver;
 use Phpactor\WorseReflection\Core\Exception\NotFound;
+use Phpactor\WorseReflection\Core\Inference\Context\MemberDeclarationContext;
 use Phpactor\WorseReflection\Core\Inference\Symbol;
 use Phpactor\WorseReflection\Core\Inference\NodeContext;
 use Phpactor\WorseReflection\Core\Reflection\ReflectionClass;
@@ -73,6 +75,11 @@ class WorseReflectionDefinitionLocator implements DefinitionLocator
     private function gotoDefinition(TextDocument $document, ReflectionOffset $offset): TypeLocations
     {
         $symbolContext = $offset->symbolContext();
+
+        if ($symbolContext instanceof MemberDeclarationContext) {
+            return $this->gotoMethodDeclaration($symbolContext);
+        }
+
         switch ($symbolContext->symbol()->symbolType()) {
             case Symbol::METHOD:
             case Symbol::PROPERTY:
@@ -245,4 +252,42 @@ class WorseReflectionDefinitionLocator implements DefinitionLocator
 
         return new TypeLocations($locations);
     }
+
+    private function gotoMethodDeclaration(MemberDeclarationContext $symbolContext): TypeLocations
+    {
+        try {
+            $class = $this->reflector->reflectClass($symbolContext->classType()->name());
+        } catch (NotFound $notFound) {
+            return new TypeLocations([]);
+        }
+
+        // find first parent definition or return declaring class
+        $member = (function (string $name) use ($class) {
+            foreach ((new ClassHierarchyResolver())->resolve($class) as $currentClass) {
+                if ($currentClass->ownMembers()->has($name)) {
+                    return $currentClass->ownMembers()->byName($name)->first();
+                }
+            }
+            return null;
+        })($symbolContext->name());
+
+        if (null === $member) {
+            return new TypeLocations([]);
+        }
+
+
+        $path = $member->declaringClass()->sourceCode()->path();
+
+        if (null === $path) {
+            throw new CouldNotLocateDefinition(sprintf(
+                'The source code for class "%s" has no path associated with it.',
+                (string) $member->declaringClass()->name()
+            ));
+        }
+
+        return new TypeLocations([new TypeLocation($symbolContext->classType(), new Location(
+            TextDocumentUri::fromString($path),
+            ByteOffset::fromInt($member->position()->start())
+        ))]);
+    }
 }
diff --git a/lib/WorseReflection/Core/Inference/Context/MemberDeclarationContext.php b/lib/WorseReflection/Core/Inference/Context/MemberDeclarationContext.php
new file mode 100644
index 00000000..3c0377de
--- /dev/null
+++ b/lib/WorseReflection/Core/Inference/Context/MemberDeclarationContext.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Phpactor\WorseReflection\Core\Inference\Context;
+
+use Phpactor\WorseReflection\Core\Inference\NodeContext;
+use Phpactor\WorseReflection\Core\Inference\Symbol;
+use Phpactor\WorseReflection\Core\Type;
+use Phpactor\WorseReflection\Core\Type\ClassType;
+use RuntimeException;
+
+class MemberDeclarationContext extends NodeContext
+{
+    public function __construct(Symbol $symbol, Type $type, Type $containerType)
+    {
+        parent::__construct($symbol, $type, $containerType);
+    }
+
+    public function classType(): ?ClassType
+    {
+        if (!$this->containerType instanceof ClassType) {
+            throw new RuntimeException('Member declaration must have class as a container type');
+        }
+        return $this->containerType;
+    }
+
+    public function name(): string
+    {
+        return $this->symbol->name();
+    }
+}
diff --git a/lib/WorseReflection/Core/Inference/NodeContext.php b/lib/WorseReflection/Core/Inference/NodeContext.php
index 89cd6a01..f750c4ae 100644
--- a/lib/WorseReflection/Core/Inference/NodeContext.php
+++ b/lib/WorseReflection/Core/Inference/NodeContext.php
@@ -7,18 +7,18 @@ use Phpactor\WorseReflection\Core\TypeFactory;
 use Phpactor\WorseReflection\Core\Type\MissingType;
 use Phpactor\WorseReflection\Core\Reflection\ReflectionScope;
 
-final class NodeContext
+class NodeContext
 {
-    private Type $type;
-
-    private Symbol $symbol;
-
-    private TypeAssertions $typeAssertions;
+    protected Symbol $symbol;
 
     /**
      * @var Type
      */
-    private ?Type $containerType = null;
+    protected ?Type $containerType = null;
+
+    protected Type $type;
+
+    private TypeAssertions $typeAssertions;
 
     /**
      * @var string[]
@@ -30,7 +30,7 @@ final class NodeContext
      */
     private ?ReflectionScope $scope = null;
 
-    private function __construct(Symbol $symbol, Type $type, Type $containerType = null, ReflectionScope $scope = null)
+    protected function __construct(Symbol $symbol, Type $type, Type $containerType = null, ReflectionScope $scope = null)
     {
         $this->symbol = $symbol;
         $this->containerType = $containerType;
diff --git a/lib/WorseReflection/Core/Inference/Resolver/MethodDeclarationResolver.php b/lib/WorseReflection/Core/Inference/Resolver/MethodDeclarationResolver.php
index 9509256a..7ff29e4d 100644
--- a/lib/WorseReflection/Core/Inference/Resolver/MethodDeclarationResolver.php
+++ b/lib/WorseReflection/Core/Inference/Resolver/MethodDeclarationResolver.php
@@ -4,12 +4,14 @@ namespace Phpactor\WorseReflection\Core\Inference\Resolver;
 
 use Microsoft\PhpParser\Node;
 use Microsoft\PhpParser\Node\MethodDeclaration;
+use Phpactor\WorseReflection\Core\Inference\Context\MemberDeclarationContext;
 use Phpactor\WorseReflection\Core\Inference\Frame;
 use Phpactor\WorseReflection\Core\Inference\NodeContext;
-use Phpactor\WorseReflection\Core\Inference\NodeContextFactory;
 use Phpactor\WorseReflection\Core\Inference\Resolver;
 use Phpactor\WorseReflection\Core\Inference\Symbol;
 use Phpactor\WorseReflection\Core\Inference\NodeContextResolver;
+use Phpactor\WorseReflection\Core\Position;
+use Phpactor\WorseReflection\Core\TypeFactory;
 use Phpactor\WorseReflection\Core\Util\NodeUtil;
 
 class MethodDeclarationResolver implements Resolver
@@ -22,14 +24,17 @@ class MethodDeclarationResolver implements Resolver
         $classNode = NodeUtil::nodeContainerClassLikeDeclaration($node);
         $classSymbolContext = $resolver->resolveNode($frame, $classNode);
 
-        return NodeContextFactory::create(
-            (string)$node->name->getText($node->getFileContents()),
-            $node->name->getStartPosition(),
-            $node->name->getEndPosition(),
-            [
-                'container_type' => $classSymbolContext->type(),
-                'symbol_type' => Symbol::METHOD,
-            ]
+        return new MemberDeclarationContext(
+            Symbol::fromTypeNameAndPosition(
+                Symbol::METHOD,
+                (string)$node->name->getText($node->getFileContents()),
+                Position::fromStartAndEnd(
+                    $node->name->getStartPosition(),
+                    $node->name->getEndPosition()
+                )
+            ),
+            TypeFactory::unknown(),
+            $classSymbolContext->type()
         );
     }
 }
diff --git a/lib/WorseReflection/Core/Inference/Resolver/VariableResolver.php b/lib/WorseReflection/Core/Inference/Resolver/VariableResolver.php
index db1d0c56..77dba832 100644
--- a/lib/WorseReflection/Core/Inference/Resolver/VariableResolver.php
+++ b/lib/WorseReflection/Core/Inference/Resolver/VariableResolver.php
@@ -7,6 +7,7 @@ use Microsoft\PhpParser\Node\Expression\BracedExpression;
 use Microsoft\PhpParser\Node\Expression\ScopedPropertyAccessExpression;
 use Microsoft\PhpParser\Node\Expression\Variable;
 use Microsoft\PhpParser\Node\PropertyDeclaration;
+use Phpactor\WorseReflection\Core\Inference\Context\MemberDeclarationContext;
 use Phpactor\WorseReflection\Core\Inference\Frame;
 use Phpactor\WorseReflection\Core\Inference\MemberTypeResolver;
 use Phpactor\WorseReflection\Core\Inference\NodeContext;
@@ -88,7 +89,7 @@ class VariableResolver implements Resolver
             return NodeContext::none();
         }
 
-        $info = NodeContextFactory::create(
+        $context = NodeContextFactory::create(
             $node->getName(),
             $node->getStartPosition(),
             $node->getEndPosition(),
@@ -97,11 +98,13 @@ class VariableResolver implements Resolver
             ]
         );
 
-        return (new MemberTypeResolver($resolver->reflector()))->propertyType(
+        $context = (new MemberTypeResolver($resolver->reflector()))->propertyType(
             NodeUtil::nodeContainerClassLikeType($resolver->reflector(), $node),
-            $info,
-            $info->symbol()->name()
+            $context,
+            $context->symbol()->name()
         );
+
+        return new MemberDeclarationContext($context->symbol(), $context->type(), $context->containerType());
     }
 
     private function resolveStaticPropertyAccess(NodeContextResolver $resolver, Type $containerType, Variable $node): NodeContext


Test patch:
diff --git a/lib/WorseReferenceFinder/Tests/Unit/WorseReflectionDefinitionLocatorTest.php b/lib/WorseReferenceFinder/Tests/Unit/WorseReflectionDefinitionLocatorTest.php
index 2e98661c..9b1c4835 100644
--- a/lib/WorseReferenceFinder/Tests/Unit/WorseReflectionDefinitionLocatorTest.php
+++ b/lib/WorseReferenceFinder/Tests/Unit/WorseReflectionDefinitionLocatorTest.php
@@ -136,6 +136,67 @@ class WorseReflectionDefinitionLocatorTest extends DefinitionLocatorTestCase
         $this->assertEquals($this->workspace->path('Foobar.php'), (string) $location->first()->location()->uri()->path());
     }
 
+    public function testLocatesMethodDeclaration(): void
+    {
+        $location = $this->locate(<<<'EOT'
+            // File: Foobar.php
+            EOT
+        , '<?php class Foobar { public function b<>ar() {} }');
+
+        $this->assertEquals(
+            $this->workspace->path('somefile.php'),
+            (string) $location->first()->location()->uri()->path()
+        );
+    }
+
+    public function testLocatesMethodDeclarationInParentClass(): void
+    {
+        $location = $this->locate(<<<'EOT'
+            // File: Foobar.php
+            <?php abstract class Foobar { abstract public function bar() {} }
+            // File: Barfoo.php
+            <?php class Barfoo extends Foobar { public function b<>ar() {} }
+            EOT
+        , '<?php class Barfoo extends Foobar { public function b<>ar() {} }');
+
+        $this->assertEquals(
+            $this->workspace->path('Foobar.php'),
+            (string) $location->first()->location()->uri()->path()
+        );
+    }
+
+    public function testLocatesPropertyInParentClass(): void
+    {
+        $location = $this->locate(<<<'EOT'
+            // File: Foobar.php
+            <?php class Foobar { public $bar; }
+            // File: Barfoo.php
+            <?php class Barfoo extends Foobar { public string $bar; }
+            EOT
+        , '<?php class Barfoo extends Foobar { public string $b<>ar; }');
+
+        $this->assertEquals(
+            $this->workspace->path('Foobar.php'),
+            (string) $location->first()->location()->uri()->path()
+        );
+    }
+
+    public function testLocatesMethodInInterface(): void
+    {
+        $location = $this->locate(<<<'EOT'
+            // File: Foobar.php
+            <?php interface Foobar { public function foo(); }
+            // File: Barfoo.php
+            <?php class Barfoo implements Foobar { public function foo() {} }
+            EOT
+        , '<?php class Barfoo implements Foobar { public function f<>oo() }');
+
+        $this->assertEquals(
+            $this->workspace->path('Foobar.php'),
+            (string) $location->first()->location()->uri()->path()
+        );
+    }
+
     public function testLocatesToMethodOnUnionTypeWithOneTypeMissingTheMethod(): void
     {
         $location = $this->locate(<<<'EOT'