diff --git a/fixtures/completion/property.php b/fixtures/completion/property.php index 17ae95d..06ac2a7 100644 --- a/fixtures/completion/property.php +++ b/fixtures/completion/property.php @@ -1,4 +1,4 @@ diff --git a/fixtures/global_references.php b/fixtures/global_references.php index 0ac117b..c76c934 100644 --- a/fixtures/global_references.php +++ b/fixtures/global_references.php @@ -38,3 +38,6 @@ if ($abc instanceof TestInterface) { // Nested expression $obj->testProperty->testMethod(); TestClass::$staticTestProperty[123]->testProperty; + +$child = new ChildClass; +echo $child->testMethod(); diff --git a/fixtures/global_symbols.php b/fixtures/global_symbols.php index f5c755b..6494848 100644 --- a/fixtures/global_symbols.php +++ b/fixtures/global_symbols.php @@ -96,3 +96,5 @@ new class { $testVariable = 123; } }; + +class ChildClass extends TestClass {} diff --git a/fixtures/references.php b/fixtures/references.php index 4a34698..b98a38e 100644 --- a/fixtures/references.php +++ b/fixtures/references.php @@ -38,3 +38,6 @@ if ($abc instanceof TestInterface) { // Nested expressions $obj->testProperty->testMethod(); TestClass::$staticTestProperty[123]->testProperty; + +$child = new ChildClass; +echo $child->testMethod(); diff --git a/fixtures/symbols.php b/fixtures/symbols.php index 4b0a6d9..d7700bc 100644 --- a/fixtures/symbols.php +++ b/fixtures/symbols.php @@ -96,3 +96,5 @@ new class { $testVariable = 123; } }; + +class ChildClass extends TestClass {} diff --git a/src/CompletionProvider.php b/src/CompletionProvider.php index fb9ebb8..99a5d57 100644 --- a/src/CompletionProvider.php +++ b/src/CompletionProvider.php @@ -145,8 +145,10 @@ class CompletionProvider $this->definitionResolver->resolveExpressionNodeToType($node->var) ); } else { + // Static member reference $prefixes = [$node->class instanceof Node\Name ? (string)$node->class : '']; } + $prefixes = $this->expandParentFqns($prefixes); // If we are just filtering by the class, add the appropiate operator to the prefix // to filter the type of symbol foreach ($prefixes as &$prefix) { @@ -158,6 +160,7 @@ class CompletionProvider $prefix .= '::$'; } } + unset($prefix); foreach ($this->index->getDefinitions() as $fqn => $def) { foreach ($prefixes as $prefix) { @@ -287,6 +290,26 @@ class CompletionProvider return $list; } + /** + * Adds the FQNs of all parent classes to an array of FQNs of classes + * + * @param string[] $fqns + * @return string[] + */ + private function expandParentFqns(array $fqns): array + { + $expanded = $fqns; + foreach ($fqns as $fqn) { + $def = $this->index->getDefinition($fqn); + if ($def) { + foreach ($this->expandParentFqns($def->extends) as $parent) { + $expanded[] = $parent; + } + } + } + return $expanded; + } + /** * Will walk the AST upwards until a function-like node is met * and at each level walk all previous siblings and their children to search for definitions diff --git a/src/Definition.php b/src/Definition.php index b7730f3..d4b59cb 100644 --- a/src/Definition.php +++ b/src/Definition.php @@ -30,6 +30,13 @@ class Definition */ public $fqn; + /** + * For class or interfaces, the FQNs of extended classes and implemented interfaces + * + * @var string[] + */ + public $extends; + /** * Only true for classes, interfaces, traits, functions and non-class constants * This is so methods and properties are not suggested in the global scope diff --git a/src/DefinitionResolver.php b/src/DefinitionResolver.php index 3a104ad..8ff98b5 100644 --- a/src/DefinitionResolver.php +++ b/src/DefinitionResolver.php @@ -115,6 +115,17 @@ class DefinitionResolver || ($node instanceof Node\Stmt\PropertyProperty && $node->getAttribute('parentNode')->isStatic()) ); $def->fqn = $fqn; + if ($node instanceof Node\Stmt\Class_) { + $def->extends = []; + if ($node->extends) { + $def->extends[] = (string)$node->extends; + } + } else if ($node instanceof Node\Stmt\Interface_) { + $def->extends = []; + foreach ($node->extends as $n) { + $def->extends[] = (string)$n; + } + } $def->symbolInformation = SymbolInformation::fromNode($node, $fqn); $def->type = $this->getTypeFromNode($node); $def->declarationLine = $this->getDeclarationLineFromNode($node); @@ -248,7 +259,31 @@ class DefinitionResolver } else { $classFqn = substr((string)$varType->getFqsen(), 1); } - $name = $classFqn . '->' . (string)$node->name; + $memberSuffix = '->' . (string)$node->name; + if ($node instanceof Node\Expr\MethodCall) { + $memberSuffix .= '()'; + } + // Find the right class that implements the member + $implementorFqns = [$classFqn]; + while ($implementorFqn = array_shift($implementorFqns)) { + // If the member FQN exists, return it + if ($this->index->getDefinition($implementorFqn . $memberSuffix)) { + return $implementorFqn . $memberSuffix; + } + // Get Definition of implementor class + $implementorDef = $this->index->getDefinition($implementorFqn); + // If it doesn't exist, return the initial guess + if ($implementorDef === null) { + break; + } + // Repeat for parent class + if ($implementorDef->extends) { + foreach ($implementorDef->extends as $extends) { + $implementorFqns[] = $extends; + } + } + } + return $classFqn . $memberSuffix; } else if ($parent instanceof Node\Expr\FuncCall) { if ($parent->name instanceof Node\Expr) { return null; @@ -290,6 +325,9 @@ class DefinitionResolver } else { return null; } + if (!isset($name)) { + return null; + } if ( $node instanceof Node\Expr\MethodCall || $node instanceof Node\Expr\StaticCall @@ -297,9 +335,6 @@ class DefinitionResolver ) { $name .= '()'; } - if (!isset($name)) { - return null; - } return $name; } diff --git a/tests/NodeVisitor/DefinitionCollectorTest.php b/tests/NodeVisitor/DefinitionCollectorTest.php index 9b60814..f4e053e 100644 --- a/tests/NodeVisitor/DefinitionCollectorTest.php +++ b/tests/NodeVisitor/DefinitionCollectorTest.php @@ -49,7 +49,8 @@ class DefinitionCollectorTest extends TestCase 'TestNamespace\\TestClass->testMethod()', 'TestNamespace\\TestTrait', 'TestNamespace\\TestInterface', - 'TestNamespace\\test_function()' + 'TestNamespace\\test_function()', + 'TestNamespace\\ChildClass' ], array_keys($defNodes)); $this->assertInstanceOf(Node\Const_::class, $defNodes['TestNamespace\\TEST_CONST']); $this->assertInstanceOf(Node\Stmt\Class_::class, $defNodes['TestNamespace\\TestClass']); @@ -61,6 +62,7 @@ class DefinitionCollectorTest extends TestCase $this->assertInstanceOf(Node\Stmt\Trait_::class, $defNodes['TestNamespace\\TestTrait']); $this->assertInstanceOf(Node\Stmt\Interface_::class, $defNodes['TestNamespace\\TestInterface']); $this->assertInstanceOf(Node\Stmt\Function_::class, $defNodes['TestNamespace\\test_function()']); + $this->assertInstanceOf(Node\Stmt\Class_::class, $defNodes['TestNamespace\\ChildClass']); } public function testDoesNotCollectReferences() diff --git a/tests/Server/ServerTestCase.php b/tests/Server/ServerTestCase.php index 602bda0..d46c80e 100644 --- a/tests/Server/ServerTestCase.php +++ b/tests/Server/ServerTestCase.php @@ -71,6 +71,7 @@ abstract class ServerTestCase extends TestCase // Global 'TEST_CONST' => new Location($globalSymbolsUri, new Range(new Position( 9, 6), new Position( 9, 22))), 'TestClass' => new Location($globalSymbolsUri, new Range(new Position(20, 0), new Position(61, 1))), + 'ChildClass' => new Location($globalSymbolsUri, new Range(new Position(99, 0), new Position(99, 37))), 'TestTrait' => new Location($globalSymbolsUri, new Range(new Position(63, 0), new Position(66, 1))), 'TestInterface' => new Location($globalSymbolsUri, new Range(new Position(68, 0), new Position(71, 1))), 'TestClass::TEST_CLASS_CONST' => new Location($globalSymbolsUri, new Range(new Position(27, 10), new Position(27, 32))), @@ -86,6 +87,7 @@ abstract class ServerTestCase extends TestCase 'SecondTestNamespace' => new Location($useUri, new Range(new Position( 2, 0), new Position( 2, 30))), 'TestNamespace\\TEST_CONST' => new Location($symbolsUri, new Range(new Position( 9, 6), new Position( 9, 22))), 'TestNamespace\\TestClass' => new Location($symbolsUri, new Range(new Position(20, 0), new Position(61, 1))), + 'TestNamespace\\ChildClass' => new Location($symbolsUri, new Range(new Position(99, 0), new Position(99, 37))), 'TestNamespace\\TestTrait' => new Location($symbolsUri, new Range(new Position(63, 0), new Position(66, 1))), 'TestNamespace\\TestInterface' => new Location($symbolsUri, new Range(new Position(68, 0), new Position(71, 1))), 'TestNamespace\\TestClass::TEST_CLASS_CONST' => new Location($symbolsUri, new Range(new Position(27, 10), new Position(27, 32))), @@ -104,14 +106,18 @@ abstract class ServerTestCase extends TestCase 0 => new Location($referencesUri, new Range(new Position(29, 5), new Position(29, 15))) ], 'TestNamespace\\TestClass' => [ - 0 => new Location($referencesUri, new Range(new Position( 4, 11), new Position( 4, 20))), // $obj = new TestClass(); - 1 => new Location($referencesUri, new Range(new Position( 7, 0), new Position( 7, 9))), // TestClass::staticTestMethod(); - 2 => new Location($referencesUri, new Range(new Position( 8, 5), new Position( 8, 14))), // echo TestClass::$staticTestProperty; - 3 => new Location($referencesUri, new Range(new Position( 9, 5), new Position( 9, 14))), // TestClass::TEST_CLASS_CONST; - 4 => new Location($referencesUri, new Range(new Position(21, 18), new Position(21, 27))), // function whatever(TestClass $param) - 5 => new Location($referencesUri, new Range(new Position(21, 37), new Position(21, 46))), // function whatever(TestClass $param): TestClass - 6 => new Location($referencesUri, new Range(new Position(39, 0), new Position(39, 9))), // TestClass::$staticTestProperty[123]->testProperty; - 7 => new Location($useUri, new Range(new Position( 4, 4), new Position( 4, 27))), // use TestNamespace\TestClass; + 0 => new Location($symbolsUri , new Range(new Position(99, 25), new Position(99, 34))), // class ChildClass extends TestClass {} + 1 => new Location($referencesUri, new Range(new Position( 4, 11), new Position( 4, 20))), // $obj = new TestClass(); + 2 => new Location($referencesUri, new Range(new Position( 7, 0), new Position( 7, 9))), // TestClass::staticTestMethod(); + 3 => new Location($referencesUri, new Range(new Position( 8, 5), new Position( 8, 14))), // echo TestClass::$staticTestProperty; + 4 => new Location($referencesUri, new Range(new Position( 9, 5), new Position( 9, 14))), // TestClass::TEST_CLASS_CONST; + 5 => new Location($referencesUri, new Range(new Position(21, 18), new Position(21, 27))), // function whatever(TestClass $param) + 6 => new Location($referencesUri, new Range(new Position(21, 37), new Position(21, 46))), // function whatever(TestClass $param): TestClass + 7 => new Location($referencesUri, new Range(new Position(39, 0), new Position(39, 9))), // TestClass::$staticTestProperty[123]->testProperty; + 8 => new Location($useUri, new Range(new Position( 4, 4), new Position( 4, 27))), // use TestNamespace\TestClass; + ], + 'TestNamespace\\TestChild' => [ + 0 => new Location($referencesUri, new Range(new Position(42, 5), new Position(42, 25))), // echo $child->testProperty; ], 'TestNamespace\\TestInterface' => [ 0 => new Location($symbolsUri, new Range(new Position(20, 27), new Position(20, 40))), // class TestClass implements TestInterface @@ -137,7 +143,8 @@ abstract class ServerTestCase extends TestCase ], 'TestNamespace\\TestClass::testMethod()' => [ 0 => new Location($referencesUri, new Range(new Position( 5, 0), new Position( 5, 18))), // $obj->testMethod(); - 1 => new Location($referencesUri, new Range(new Position(38, 0), new Position(38, 32))) // $obj->testProperty->testMethod(); + 1 => new Location($referencesUri, new Range(new Position(38, 0), new Position(38, 32))), // $obj->testProperty->testMethod(); + 2 => new Location($referencesUri, new Range(new Position(42, 5), new Position(42, 25))) // $child->testMethod(); ], 'TestNamespace\\test_function()' => [ 0 => new Location($referencesUri, new Range(new Position(10, 0), new Position(10, 13))), @@ -150,13 +157,17 @@ abstract class ServerTestCase extends TestCase 1 => new Location($globalReferencesUri, new Range(new Position(29, 5), new Position(29, 15))) ], 'TestClass' => [ - 0 => new Location($globalReferencesUri, new Range(new Position( 4, 11), new Position( 4, 20))), // $obj = new TestClass(); - 1 => new Location($globalReferencesUri, new Range(new Position( 7, 0), new Position( 7, 9))), // TestClass::staticTestMethod(); - 2 => new Location($globalReferencesUri, new Range(new Position( 8, 5), new Position( 8, 14))), // echo TestClass::$staticTestProperty; - 3 => new Location($globalReferencesUri, new Range(new Position( 9, 5), new Position( 9, 14))), // TestClass::TEST_CLASS_CONST; - 4 => new Location($globalReferencesUri, new Range(new Position(21, 18), new Position(21, 27))), // function whatever(TestClass $param) - 5 => new Location($globalReferencesUri, new Range(new Position(21, 37), new Position(21, 46))), // function whatever(TestClass $param): TestClass - 6 => new Location($globalReferencesUri, new Range(new Position(39, 0), new Position(39, 9))), // TestClass::$staticTestProperty[123]->testProperty; + 0 => new Location($globalSymbolsUri, new Range(new Position(99, 25), new Position(99, 34))), // class ChildClass extends TestClass {} + 1 => new Location($globalReferencesUri, new Range(new Position( 4, 11), new Position( 4, 20))), // $obj = new TestClass(); + 2 => new Location($globalReferencesUri, new Range(new Position( 7, 0), new Position( 7, 9))), // TestClass::staticTestMethod(); + 3 => new Location($globalReferencesUri, new Range(new Position( 8, 5), new Position( 8, 14))), // echo TestClass::$staticTestProperty; + 4 => new Location($globalReferencesUri, new Range(new Position( 9, 5), new Position( 9, 14))), // TestClass::TEST_CLASS_CONST; + 5 => new Location($globalReferencesUri, new Range(new Position(21, 18), new Position(21, 27))), // function whatever(TestClass $param) + 6 => new Location($globalReferencesUri, new Range(new Position(21, 37), new Position(21, 46))), // function whatever(TestClass $param): TestClass + 7 => new Location($globalReferencesUri, new Range(new Position(39, 0), new Position(39, 9))), // TestClass::$staticTestProperty[123]->testProperty; + ], + 'TestChild' => [ + 0 => new Location($globalReferencesUri, new Range(new Position(42, 5), new Position(42, 25))), // echo $child->testProperty; ], 'TestInterface' => [ 0 => new Location($globalSymbolsUri, new Range(new Position(20, 27), new Position(20, 40))), // class TestClass implements TestInterface @@ -182,7 +193,8 @@ abstract class ServerTestCase extends TestCase ], 'TestClass::testMethod()' => [ 0 => new Location($globalReferencesUri, new Range(new Position( 5, 0), new Position( 5, 18))), // $obj->testMethod(); - 1 => new Location($globalReferencesUri, new Range(new Position(38, 0), new Position(38, 32))) // $obj->testProperty->testMethod(); + 1 => new Location($globalReferencesUri, new Range(new Position(38, 0), new Position(38, 32))), // $obj->testProperty->testMethod(); + 2 => new Location($globalReferencesUri, new Range(new Position(42, 5), new Position(42, 25))) // $child->testMethod(); ], 'test_function()' => [ 0 => new Location($globalReferencesUri, new Range(new Position(10, 0), new Position(10, 13))), diff --git a/tests/Server/TextDocument/CompletionTest.php b/tests/Server/TextDocument/CompletionTest.php index 1e48d7b..15349ed 100644 --- a/tests/Server/TextDocument/CompletionTest.php +++ b/tests/Server/TextDocument/CompletionTest.php @@ -165,6 +165,15 @@ class CompletionTest extends TestCase null, '\TestClass' ), + new CompletionItem( + 'ChildClass', + CompletionItemKind::CLASS_, + null, + null, + null, + null, + '\ChildClass' + ), // Namespaced, `use`d TestClass definition (inserted as TestClass) new CompletionItem( 'TestClass', @@ -175,6 +184,15 @@ class CompletionTest extends TestCase null, 'TestClass' ), + new CompletionItem( + 'ChildClass', + CompletionItemKind::CLASS_, + 'TestNamespace', + null, + null, + null, + '\TestNamespace\ChildClass' + ), ], true), $items); } diff --git a/tests/Server/TextDocument/Definition/GlobalTest.php b/tests/Server/TextDocument/Definition/GlobalTest.php index 2b1e353..b5d7425 100644 --- a/tests/Server/TextDocument/Definition/GlobalTest.php +++ b/tests/Server/TextDocument/Definition/GlobalTest.php @@ -161,6 +161,18 @@ class GlobalTest extends ServerTestCase $this->assertEquals($this->getDefinitionLocation('TestClass::testMethod()'), $result); } + public function testDefinitionForMethodOnChildClass() + { + // $child->testMethod(); + // Get definition for testMethod + $reference = $this->getReferenceLocations('TestClass::testMethod()')[2]; + $result = $this->textDocument->definition( + new TextDocumentIdentifier($reference->uri), + $reference->range->end + )->wait(); + $this->assertEquals($this->getDefinitionLocation('TestClass::testMethod()'), $result); + } + public function testDefinitionForProperties() { // echo $obj->testProperty; diff --git a/tests/Server/TextDocument/DocumentSymbolTest.php b/tests/Server/TextDocument/DocumentSymbolTest.php index b9c937e..89d24ee 100644 --- a/tests/Server/TextDocument/DocumentSymbolTest.php +++ b/tests/Server/TextDocument/DocumentSymbolTest.php @@ -29,6 +29,7 @@ class DocumentSymbolTest extends ServerTestCase new SymbolInformation('TestTrait', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\TestTrait'), 'TestNamespace'), new SymbolInformation('TestInterface', SymbolKind::INTERFACE, $this->getDefinitionLocation('TestNamespace\\TestInterface'), 'TestNamespace'), new SymbolInformation('test_function', SymbolKind::FUNCTION, $this->getDefinitionLocation('TestNamespace\\test_function()'), 'TestNamespace'), + new SymbolInformation('ChildClass', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\ChildClass'), 'TestNamespace'), ], $result); // @codingStandardsIgnoreEnd } diff --git a/tests/Server/Workspace/SymbolTest.php b/tests/Server/Workspace/SymbolTest.php index 33b4cf1..ea80e1b 100644 --- a/tests/Server/Workspace/SymbolTest.php +++ b/tests/Server/Workspace/SymbolTest.php @@ -41,6 +41,7 @@ class SymbolTest extends ServerTestCase new SymbolInformation('TestTrait', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\TestTrait'), 'TestNamespace'), new SymbolInformation('TestInterface', SymbolKind::INTERFACE, $this->getDefinitionLocation('TestNamespace\\TestInterface'), 'TestNamespace'), new SymbolInformation('test_function', SymbolKind::FUNCTION, $this->getDefinitionLocation('TestNamespace\\test_function()'), 'TestNamespace'), + new SymbolInformation('ChildClass', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\ChildClass'), 'TestNamespace'), new SymbolInformation('whatever', SymbolKind::FUNCTION, $this->getDefinitionLocation('TestNamespace\\whatever()'), 'TestNamespace'), // Global new SymbolInformation('TEST_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TEST_CONST'), ''), @@ -53,6 +54,7 @@ class SymbolTest extends ServerTestCase new SymbolInformation('TestTrait', SymbolKind::CLASS_, $this->getDefinitionLocation('TestTrait'), ''), new SymbolInformation('TestInterface', SymbolKind::INTERFACE, $this->getDefinitionLocation('TestInterface'), ''), new SymbolInformation('test_function', SymbolKind::FUNCTION, $this->getDefinitionLocation('test_function()'), ''), + new SymbolInformation('ChildClass', SymbolKind::CLASS_, $this->getDefinitionLocation('ChildClass'), ''), new SymbolInformation('whatever', SymbolKind::FUNCTION, $this->getDefinitionLocation('whatever()'), ''), new SymbolInformation('SecondTestNamespace', SymbolKind::NAMESPACE, $this->getDefinitionLocation('SecondTestNamespace'), '')