diff --git a/fixtures/completion/property.php b/fixtures/completion/property.php index 17ae95d1..06ac2a78 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 0ac117b5..c76c9347 100644 --- a/fixtures/global_references.php +++ b/fixtures/global_references.php @@ -38,3 +38,6 @@ function whatever(TestClass $param): TestClass { // 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 f5c755bc..6494848a 100644 --- a/fixtures/global_symbols.php +++ b/fixtures/global_symbols.php @@ -96,3 +96,5 @@ public function testMethod($testParameter) $testVariable = 123; } }; + +class ChildClass extends TestClass {} diff --git a/fixtures/references.php b/fixtures/references.php index 4a346987..b98a38ee 100644 --- a/fixtures/references.php +++ b/fixtures/references.php @@ -38,3 +38,6 @@ function whatever(TestClass $param): TestClass { // 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 4b0a6d94..d7700bcb 100644 --- a/fixtures/symbols.php +++ b/fixtures/symbols.php @@ -96,3 +96,5 @@ public function testMethod($testParameter) $testVariable = 123; } }; + +class ChildClass extends TestClass {} diff --git a/src/CompletionProvider.php b/src/CompletionProvider.php index fb9ebb87..99a5d57f 100644 --- a/src/CompletionProvider.php +++ b/src/CompletionProvider.php @@ -145,8 +145,10 @@ public function provideCompletion(PhpDocument $doc, Position $pos): CompletionLi $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 @@ public function provideCompletion(PhpDocument $doc, Position $pos): CompletionLi $prefix .= '::$'; } } + unset($prefix); foreach ($this->index->getDefinitions() as $fqn => $def) { foreach ($prefixes as $prefix) { @@ -287,6 +290,26 @@ public function provideCompletion(PhpDocument $doc, Position $pos): CompletionLi 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 b7730f3a..d4b59cb1 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 3a104ade..8ff98b50 100644 --- a/src/DefinitionResolver.php +++ b/src/DefinitionResolver.php @@ -115,6 +115,17 @@ public function createDefinitionFromNode(Node $node, string $fqn = null): Defini || ($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 @@ public function resolveReferenceNodeToFqn(Node $node) } 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 @@ public function resolveReferenceNodeToFqn(Node $node) } else { return null; } + if (!isset($name)) { + return null; + } if ( $node instanceof Node\Expr\MethodCall || $node instanceof Node\Expr\StaticCall @@ -297,9 +335,6 @@ public function resolveReferenceNodeToFqn(Node $node) ) { $name .= '()'; } - if (!isset($name)) { - return null; - } return $name; } diff --git a/tests/NodeVisitor/DefinitionCollectorTest.php b/tests/NodeVisitor/DefinitionCollectorTest.php index 9b608142..f4e053ec 100644 --- a/tests/NodeVisitor/DefinitionCollectorTest.php +++ b/tests/NodeVisitor/DefinitionCollectorTest.php @@ -49,7 +49,8 @@ public function testCollectsSymbols() '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 @@ public function testCollectsSymbols() $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 602bda0f..d46c80e5 100644 --- a/tests/Server/ServerTestCase.php +++ b/tests/Server/ServerTestCase.php @@ -71,6 +71,7 @@ public function setUp() // 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 @@ public function setUp() '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 @@ public function setUp() 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 @@ public function setUp() ], '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 @@ public function setUp() 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 @@ public function setUp() ], '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 1e48d7b1..15349ed0 100644 --- a/tests/Server/TextDocument/CompletionTest.php +++ b/tests/Server/TextDocument/CompletionTest.php @@ -165,6 +165,15 @@ public function testNewInNamespace() 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 @@ public function testNewInNamespace() 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 2b1e353d..b5d7425e 100644 --- a/tests/Server/TextDocument/Definition/GlobalTest.php +++ b/tests/Server/TextDocument/Definition/GlobalTest.php @@ -161,6 +161,18 @@ public function testDefinitionForMethods() $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 b9c937ef..89d24ee6 100644 --- a/tests/Server/TextDocument/DocumentSymbolTest.php +++ b/tests/Server/TextDocument/DocumentSymbolTest.php @@ -29,6 +29,7 @@ public function test() 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 33b4cf1d..ea80e1b8 100644 --- a/tests/Server/Workspace/SymbolTest.php +++ b/tests/Server/Workspace/SymbolTest.php @@ -41,6 +41,7 @@ public function testEmptyQueryReturnsAllSymbols() 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 @@ public function testEmptyQueryReturnsAllSymbols() 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'), '')