diff --git a/src/CompletionProvider.php b/src/CompletionProvider.php index 2f22333..a5f9119 100644 --- a/src/CompletionProvider.php +++ b/src/CompletionProvider.php @@ -221,10 +221,10 @@ class CompletionProvider // The FQNs of the symbol and its parents (eg the implemented interfaces) foreach ($this->expandParentFqns($fqns) as $parentFqn) { // Collect fqn definitions - foreach ($this->index->getDescendantDefinitionsForFqn($parentFqn) as $fqn => $def) { + foreach ($this->index->getDescendantDefinitionsForFqn($parentFqn, true) as $fqn => $def) { // Add the object access operator to only get members of all parents $prefix = $parentFqn . '->'; - if (substr($fqn, 0, strlen($prefix)) === $prefix && $def->isMember) { + if (substr($fqn, 0, strlen($prefix)) === $prefix) { $list->items[] = CompletionItem::fromDefinition($def); } } @@ -251,10 +251,10 @@ class CompletionProvider // The FQNs of the symbol and its parents (eg the implemented interfaces) foreach ($this->expandParentFqns($fqns) as $parentFqn) { // Collect fqn definitions - foreach ($this->index->getDescendantDefinitionsForFqn($parentFqn) as $fqn => $def) { + foreach ($this->index->getDescendantDefinitionsForFqn($parentFqn, true) as $fqn => $def) { // Append :: operator to only get static members of all parents $prefix = strtolower($parentFqn . '::'); - if (substr(strtolower($fqn), 0, strlen($prefix)) === $prefix && $def->isMember) { + if (substr(strtolower($fqn), 0, strlen($prefix)) === $prefix) { $list->items[] = CompletionItem::fromDefinition($def); } } @@ -321,14 +321,12 @@ class CompletionProvider // Suggest global (ie non member) symbols that either // - start with the current namespace + prefix, if the Name node is not fully qualified // - start with just the prefix, if the Name node is fully qualified - foreach ($this->index->getDefinitions() as $fqn => $def) { + foreach ($this->index->getDefinitions(false) as $fqn => $def) { $fqnStartsWithPrefix = substr($fqn, 0, $prefixLen) === $prefix; if ( - // Exclude methods, properties etc. - !$def->isMember - && ( + ( !$prefix || ( // Either not qualified, but a matching prefix with global fallback diff --git a/src/Index/AbstractAggregateIndex.php b/src/Index/AbstractAggregateIndex.php index 90490ab..3458d55 100644 --- a/src/Index/AbstractAggregateIndex.php +++ b/src/Index/AbstractAggregateIndex.php @@ -102,12 +102,13 @@ abstract class AbstractAggregateIndex implements ReadableIndex * Returns a Generator providing an associative array [string => Definition] * that maps fully qualified symbol names to Definitions (global or not) * + * @param boolean|null $member Indicates if we want member or non-member definitions (null for both, default null) * @return \Generator yields Definition */ - public function getDefinitions(): \Generator + public function getDefinitions(bool $member = null): \Generator { foreach ($this->getIndexes() as $index) { - yield from $index->getDefinitions(); + yield from $index->getDefinitions($member); } } @@ -115,12 +116,13 @@ abstract class AbstractAggregateIndex implements ReadableIndex * Returns a Generator that yields all the descendant Definitions of a given FQN * * @param string $fqn + * @param boolean|null $member Indicates if we want member or non-member definitions (null for both, default null) * @return \Generator yields Definition */ - public function getDescendantDefinitionsForFqn(string $fqn): \Generator + public function getDescendantDefinitionsForFqn(string $fqn, bool $member = null): \Generator { foreach ($this->getIndexes() as $index) { - yield from $index->getDescendantDefinitionsForFqn($fqn); + yield from $index->getDescendantDefinitionsForFqn($fqn, $member); } } diff --git a/src/Index/Index.php b/src/Index/Index.php index dcb1b8f..1099fab 100644 --- a/src/Index/Index.php +++ b/src/Index/Index.php @@ -16,13 +16,12 @@ class Index implements ReadableIndex, \Serializable /** * An associative array that maps splitted fully qualified symbol names - * to definitions, eg : + * to non-member definitions, eg : * [ * 'Psr' => [ * '\Log' => [ * '\LoggerInterface' => [ - * '' => $def1, // definition for 'Psr\Log\LoggerInterface' which is non-member - * '->log()' => $def2, // definition for 'Psr\Log\LoggerInterface->log()' which is a member definition + * '' => $definition, * ], * ], * ], @@ -30,7 +29,24 @@ class Index implements ReadableIndex, \Serializable * * @var array */ - private $definitions = []; + private $nonMemberDefinitions = []; + + /** + * An associative array that maps splitted fully qualified symbol names + * to member definitions, eg : + * [ + * 'Psr' => [ + * '\Log' => [ + * '\LoggerInterface' => [ + * '->log()' => $definition, + * ], + * ], + * ], + * ] + * + * @var array + */ + private $memberDefinitions = []; /** * An associative array that maps fully qualified symbol names @@ -99,20 +115,29 @@ class Index implements ReadableIndex, \Serializable * Returns a Generator providing an associative array [string => Definition] * that maps fully qualified symbol names to Definitions (global or not) * + * @param boolean|null $member Indicates if we want member or non-member definitions (null for both, default null) * @return \Generator yields Definition */ - public function getDefinitions(): \Generator + public function getDefinitions(bool $member = null): \Generator { - yield from $this->yieldDefinitionsRecursively($this->definitions); + if (true === $member) { + yield from $this->yieldDefinitionsRecursively($this->memberDefinitions); + } elseif (false === $member) { + yield from $this->yieldDefinitionsRecursively($this->nonMemberDefinitions); + } else { + yield from $this->yieldDefinitionsRecursively($this->memberDefinitions); + yield from $this->yieldDefinitionsRecursively($this->nonMemberDefinitions); + } } /** * Returns a Generator that yields all the descendant Definitions of a given FQN * * @param string $fqn + * @param boolean|null $member Indicates if we want member or non-member definitions (null for both, default null) * @return \Generator yields Definition */ - public function getDescendantDefinitionsForFqn(string $fqn): \Generator + public function getDescendantDefinitionsForFqn(string $fqn, bool $member = null): \Generator { $parts = $this->splitFqn($fqn); if ('' === end($parts)) { @@ -121,12 +146,13 @@ class Index implements ReadableIndex, \Serializable array_pop($parts); } - $result = $this->getIndexValue($parts, $this->definitions); - - if ($result instanceof Definition) { - yield $fqn => $result; - } elseif (is_array($result)) { - yield from $this->yieldDefinitionsRecursively($result, $fqn); + if (true === $member) { + yield from $this->doGetDescendantDefinitionsForFqn($fqn, $parts, $this->memberDefinitions); + } elseif (false === $member) { + yield from $this->doGetDescendantDefinitionsForFqn($fqn, $parts, $this->nonMemberDefinitions); + } else { + yield from $this->doGetDescendantDefinitionsForFqn($fqn, $parts, $this->memberDefinitions); + yield from $this->doGetDescendantDefinitionsForFqn($fqn, $parts, $this->nonMemberDefinitions); } } @@ -140,8 +166,13 @@ class Index implements ReadableIndex, \Serializable public function getDefinition(string $fqn, bool $globalFallback = false) { $parts = $this->splitFqn($fqn); - $result = $this->getIndexValue($parts, $this->definitions); + $result = $this->getIndexValue($parts, $this->memberDefinitions); + if ($result instanceof Definition) { + return $result; + } + + $result = $this->getIndexValue($parts, $this->nonMemberDefinitions); if ($result instanceof Definition) { return $result; } @@ -164,7 +195,12 @@ class Index implements ReadableIndex, \Serializable public function setDefinition(string $fqn, Definition $definition) { $parts = $this->splitFqn($fqn); - $this->indexDefinition(0, $parts, $this->definitions, $definition); + + if ($definition->isMember) { + $this->indexDefinition(0, $parts, $this->memberDefinitions, $definition); + } else { + $this->indexDefinition(0, $parts, $this->nonMemberDefinitions, $definition); + } $this->emit('definition-added'); } @@ -179,7 +215,8 @@ class Index implements ReadableIndex, \Serializable public function removeDefinition(string $fqn) { $parts = $this->splitFqn($fqn); - $this->removeIndexedDefinition(0, $parts, $this->definitions, $this->definitions); + $this->removeIndexedDefinition(0, $parts, $this->memberDefinitions, $this->memberDefinitions); + $this->removeIndexedDefinition(0, $parts, $this->nonMemberDefinitions, $this->nonMemberDefinitions); unset($this->references[$fqn]); } @@ -279,6 +316,26 @@ class Index implements ReadableIndex, \Serializable ]); } + /** + * Returns a Generator that yields all the descendant Definitions of a given FQN + * in the given definition index. + * + * @param string $fqn + * @param string[] $parts The splitted FQN + * @param array &$storage The definitions index to look into + * @return \Generator yields Definition + */ + private function doGetDescendantDefinitionsForFqn(string $fqn, array $parts, array &$storage): \Generator + { + $result = $this->getIndexValue($parts, $storage); + + if ($result instanceof Definition) { + yield $fqn => $result; + } elseif (is_array($result)) { + yield from $this->yieldDefinitionsRecursively($result, $fqn); + } + } + /** * Returns a Generator that yields all the Definitions in the given $storage recursively. * The generator yields key => value pairs, e.g. @@ -431,7 +488,7 @@ class Index implements ReadableIndex, \Serializable $this->removeIndexedDefinition(0, array_slice($parts, 0, $level), $rootStorage, $rootStorage); } } - } else { + } elseif (isset($storage[$part])) { $this->removeIndexedDefinition($level + 1, $parts, $storage[$part], $rootStorage); } } diff --git a/src/Index/ReadableIndex.php b/src/Index/ReadableIndex.php index 90ddcc4..d02f22b 100644 --- a/src/Index/ReadableIndex.php +++ b/src/Index/ReadableIndex.php @@ -33,17 +33,19 @@ interface ReadableIndex extends EmitterInterface * Returns a Generator providing an associative array [string => Definition] * that maps fully qualified symbol names to Definitions (global or not) * + * @param boolean|null $member Indicates if we want member or non-member definitions (null for both, default null) * @return \Generator yields Definition */ - public function getDefinitions(): \Generator; + public function getDefinitions(bool $member = null): \Generator; /** * Returns a Generator that yields all the descendant Definitions of a given FQN * * @param string $fqn + * @param boolean|null $member Indicates if we want member or non-member definitions (null for both, default null) * @return \Generator yields Definition */ - public function getDescendantDefinitionsForFqn(string $fqn): \Generator; + public function getDescendantDefinitionsForFqn(string $fqn, bool $member = null): \Generator; /** * Returns the Definition object by a specific FQN diff --git a/tests/Server/Workspace/SymbolTest.php b/tests/Server/Workspace/SymbolTest.php index 765841b..02de5ab 100644 --- a/tests/Server/Workspace/SymbolTest.php +++ b/tests/Server/Workspace/SymbolTest.php @@ -30,41 +30,40 @@ class SymbolTest extends ServerTestCase // @codingStandardsIgnoreStart $this->assertEquals([ - new SymbolInformation('TestNamespace', SymbolKind::NAMESPACE, new Location($referencesUri, new Range(new Position(2, 0), new Position(2, 24))), ''), - // Namespaced - new SymbolInformation('TEST_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TestNamespace\\TEST_CONST'), 'TestNamespace'), - new SymbolInformation('TestClass', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\TestClass'), 'TestNamespace'), + // member new SymbolInformation('TEST_CLASS_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TestNamespace\\TestClass::TEST_CLASS_CONST'), 'TestNamespace\\TestClass'), new SymbolInformation('staticTestProperty', SymbolKind::PROPERTY, $this->getDefinitionLocation('TestNamespace\\TestClass::staticTestProperty'), 'TestNamespace\\TestClass'), new SymbolInformation('testProperty', SymbolKind::PROPERTY, $this->getDefinitionLocation('TestNamespace\\TestClass::testProperty'), 'TestNamespace\\TestClass'), new SymbolInformation('staticTestMethod', SymbolKind::METHOD, $this->getDefinitionLocation('TestNamespace\\TestClass::staticTestMethod()'), 'TestNamespace\\TestClass'), new SymbolInformation('testMethod', SymbolKind::METHOD, $this->getDefinitionLocation('TestNamespace\\TestClass::testMethod()'), 'TestNamespace\\TestClass'), - 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('Example', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\Example'), 'TestNamespace'), new SymbolInformation('__construct', SymbolKind::CONSTRUCTOR, $this->getDefinitionLocation('TestNamespace\\Example::__construct'), 'TestNamespace\\Example'), new SymbolInformation('__destruct', SymbolKind::CONSTRUCTOR, $this->getDefinitionLocation('TestNamespace\\Example::__destruct'), 'TestNamespace\\Example'), - new SymbolInformation('whatever', SymbolKind::FUNCTION, $this->getDefinitionLocation('TestNamespace\\whatever()'), 'TestNamespace'), - // Global - new SymbolInformation('TEST_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TEST_CONST'), ''), - new SymbolInformation('TestClass', SymbolKind::CLASS_, $this->getDefinitionLocation('TestClass'), ''), new SymbolInformation('TEST_CLASS_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TestClass::TEST_CLASS_CONST'), 'TestClass'), new SymbolInformation('staticTestProperty', SymbolKind::PROPERTY, $this->getDefinitionLocation('TestClass::staticTestProperty'), 'TestClass'), new SymbolInformation('testProperty', SymbolKind::PROPERTY, $this->getDefinitionLocation('TestClass::testProperty'), 'TestClass'), new SymbolInformation('staticTestMethod', SymbolKind::METHOD, $this->getDefinitionLocation('TestClass::staticTestMethod()'), 'TestClass'), new SymbolInformation('testMethod', SymbolKind::METHOD, $this->getDefinitionLocation('TestClass::testMethod()'), 'TestClass'), + // non member + new SymbolInformation('TEST_DEFINE_CONSTANT', SymbolKind::CONSTANT, $this->getDefinitionLocation('TEST_DEFINE_CONSTANT'), ''), + new SymbolInformation('unusedProperty', SymbolKind::PROPERTY, $this->getDefinitionLocation('UnusedClass::unusedProperty'), 'UnusedClass'), + new SymbolInformation('unusedMethod', SymbolKind::METHOD, $this->getDefinitionLocation('UnusedClass::unusedMethod'), 'UnusedClass'), + new SymbolInformation('TestNamespace', SymbolKind::NAMESPACE, new Location($referencesUri, new Range(new Position(2, 0), new Position(2, 24))), ''), + new SymbolInformation('TEST_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TestNamespace\\TEST_CONST'), 'TestNamespace'), + new SymbolInformation('TestClass', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\TestClass'), 'TestNamespace'), + 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('Example', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\Example'), 'TestNamespace'), + new SymbolInformation('whatever', SymbolKind::FUNCTION, $this->getDefinitionLocation('TestNamespace\\whatever()'), 'TestNamespace'), + new SymbolInformation('TEST_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TEST_CONST'), ''), + new SymbolInformation('TestClass', SymbolKind::CLASS_, $this->getDefinitionLocation('TestClass'), ''), 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('TEST_DEFINE_CONSTANT', SymbolKind::CONSTANT, $this->getDefinitionLocation('TEST_DEFINE_CONSTANT'), ''), new SymbolInformation('UnusedClass', SymbolKind::CLASS_, $this->getDefinitionLocation('UnusedClass'), ''), - new SymbolInformation('unusedProperty', SymbolKind::PROPERTY, $this->getDefinitionLocation('UnusedClass::unusedProperty'), 'UnusedClass'), - new SymbolInformation('unusedMethod', SymbolKind::METHOD, $this->getDefinitionLocation('UnusedClass::unusedMethod'), 'UnusedClass'), new SymbolInformation('whatever', SymbolKind::FUNCTION, $this->getDefinitionLocation('whatever()'), ''), - new SymbolInformation('SecondTestNamespace', SymbolKind::NAMESPACE, $this->getDefinitionLocation('SecondTestNamespace'), ''), ], $result); // @codingStandardsIgnoreEnd