From d1f85f15b6181b08ea37ea0ca3c9463865198a7b Mon Sep 17 00:00:00 2001 From: Nicolas MURE Date: Mon, 13 Nov 2017 21:26:43 +0100 Subject: [PATCH] use tree representation index --- src/CompletionProvider.php | 32 +-- src/Index/AbstractAggregateIndex.php | 37 ++-- src/Index/Index.php | 294 +++++++++++++++++++-------- src/Index/ReadableIndex.php | 8 - 4 files changed, 245 insertions(+), 126 deletions(-) diff --git a/src/CompletionProvider.php b/src/CompletionProvider.php index f058bac..bf107d5 100644 --- a/src/CompletionProvider.php +++ b/src/CompletionProvider.php @@ -207,7 +207,7 @@ class CompletionProvider foreach ($this->index->getDefinitionsForFqn($parentFqn) 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 && $def->isMember) { $list->items[] = CompletionItem::fromDefinition($def); } } @@ -237,7 +237,7 @@ class CompletionProvider foreach ($this->index->getDefinitionsForFqn($parentFqn) 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 && $def->isMember) { $list->items[] = CompletionItem::fromDefinition($def); } } @@ -301,25 +301,29 @@ class CompletionProvider } } - // Suggest global symbols that either + // 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->getGlobalDefinitions() as $fqn => $def) { + foreach ($this->index->getDefinitions() as $fqn => $def) { $fqnStartsWithPrefix = substr($fqn, 0, $prefixLen) === $prefix; if ( - !$prefix - || ( - // Either not qualified, but a matching prefix with global fallback - ($def->roamed && !$isQualified && $fqnStartsWithPrefix) - // Or not in a namespace or a fully qualified name or AND matching the prefix - || ((!$namespaceNode || $isFullyQualified) && $fqnStartsWithPrefix) - // Or in a namespace, not fully qualified and matching the prefix + current namespace + // Exclude methods, properties etc. + !$def->isMember + && ( + !$prefix || ( - $namespaceNode - && !$isFullyQualified - && substr($fqn, 0, $namespacedPrefixLen) === $namespacedPrefix + // Either not qualified, but a matching prefix with global fallback + ($def->roamed && !$isQualified && $fqnStartsWithPrefix) + // Or not in a namespace or a fully qualified name or AND matching the prefix + || ((!$namespaceNode || $isFullyQualified) && $fqnStartsWithPrefix) + // Or in a namespace, not fully qualified and matching the prefix + current namespace + || ( + $namespaceNode + && !$isFullyQualified + && substr($fqn, 0, $namespacedPrefixLen) === $namespacedPrefix + ) ) ) // Only suggest classes for `new` diff --git a/src/Index/AbstractAggregateIndex.php b/src/Index/AbstractAggregateIndex.php index e4e33b3..8c88659 100644 --- a/src/Index/AbstractAggregateIndex.php +++ b/src/Index/AbstractAggregateIndex.php @@ -107,24 +107,11 @@ abstract class AbstractAggregateIndex implements ReadableIndex public function getDefinitions(): \Generator { foreach ($this->getIndexes() as $index) { - foreach ($index->getDefinitions() as $fqn => $definitions) { - yield $fqn => $definition; - } - } - } + // foreach ($index->getDefinitions() as $fqn => $definition) { + // yield $fqn => $definition; + // } - /** - * Returns a Generator providing an associative array [string => Definition] - * that maps fully qualified symbol names to global Definitions - * - * @return \Generator providing Definitions[] - */ - public function getGlobalDefinitions(): \Generator - { - foreach ($this->getIndexes() as $index) { - foreach ($index->getGlobalDefinitions() as $fqn => $definition) { - yield $fqn => $definition; - } + yield from $index->getDefinitions(); } } @@ -137,9 +124,11 @@ abstract class AbstractAggregateIndex implements ReadableIndex public function getDefinitionsForFqn(string $fqn): \Generator { foreach ($this->getIndexes() as $index) { - foreach ($index->getDefinitionsForFqn($fqn) as $symbolFqn => $definition) { - yield $symbolFqn => $definition; - } + // foreach ($index->getDefinitionsForFqn($fqn) as $symbolFqn => $definition) { + // yield $symbolFqn => $definition; + // } + + yield from $index->getDefinitionsForFqn($fqn); } } @@ -168,9 +157,11 @@ abstract class AbstractAggregateIndex implements ReadableIndex public function getReferenceUris(string $fqn): \Generator { foreach ($this->getIndexes() as $index) { - foreach ($index->getReferenceUris($fqn) as $uri) { - yield $uri; - } + // foreach ($index->getReferenceUris($fqn) as $uri) { + // yield $uri; + // } + + yield from $index->getReferenceUris($fqn); } } } diff --git a/src/Index/Index.php b/src/Index/Index.php index a5532b7..af8eae0 100644 --- a/src/Index/Index.php +++ b/src/Index/Index.php @@ -15,27 +15,21 @@ class Index implements ReadableIndex, \Serializable use EmitterTrait; /** - * An associative array that maps fully qualified names to - * an associative array that maps fully qualified symbol names - * to Definitions, e.g. : - * [ - * 'Psr\Log\LoggerInterface' => [ - * 'Psr\Log\LoggerInterface->log()' => $definition, - * 'Psr\Log\LoggerInterface->debug()' => $definition, + * An associative array that maps splitted fully qualified symbol names + * to definitions, eg : + * [ + * 'Psr' => [ + * '\Log' => [ + * '\LoggerInterface' => [ + * '->log()' => $definition, + * ], * ], - * ] + * ], + * ] * * @var array */ - private $fqnDefinitions = []; - - /** - * An associative array that maps fully qualified symbol names - * to global (ie non member) Definitions - * - * @var Definition[] - */ - private $globalDefinitions = []; + private $definitions = []; /** * An associative array that maps fully qualified symbol names @@ -108,24 +102,13 @@ class Index implements ReadableIndex, \Serializable */ public function getDefinitions(): \Generator { - foreach ($this->fqnDefinitions as $fqnDefinition) { - foreach ($fqnDefinition as $fqn => $definition) { - yield $fqn => $definition; - } - } - } + // foreach ($this->fqnDefinitions as $fqnDefinition) { + // foreach ($fqnDefinition as $fqn => $definition) { + // yield $fqn => $definition; + // } + // } - /** - * Returns a Generator providing an associative array [string => Definition] - * that maps fully qualified symbol names to global Definitions - * - * @return \Generator providing Definitions[] - */ - public function getGlobalDefinitions(): \Generator - { - foreach ($this->globalDefinitions as $fqn => $definition) { - yield $fqn => $definition; - } + yield from $this->generateDefinitionsRecursively($this->definitions); } /** @@ -136,8 +119,17 @@ class Index implements ReadableIndex, \Serializable */ public function getDefinitionsForFqn(string $fqn): \Generator { - foreach ($this->fqnDefinitions[$fqn] ?? [] as $symbolFqn => $definition) { - yield $symbolFqn => $definition; + // foreach ($this->fqnDefinitions[$fqn] ?? [] as $symbolFqn => $definition) { + // yield $symbolFqn => $definition; + // } + + $parts = $this->splitFqn($fqn); + $result = $this->getIndexValue($parts, $this->definitions); + + if ($result instanceof Definition) { + yield $fqn => $definition; + } elseif (is_array($result)) { + yield from $this->generateDefinitionsRecursively($result, $fqn); } } @@ -150,18 +142,23 @@ class Index implements ReadableIndex, \Serializable */ public function getDefinition(string $fqn, bool $globalFallback = false) { - $namespacedFqn = $this->extractNamespacedFqn($fqn); - $definitions = $this->fqnDefinitions[$namespacedFqn] ?? []; + // $namespacedFqn = $this->extractNamespacedFqn($fqn); + // $definitions = $this->fqnDefinitions[$namespacedFqn] ?? []; - if (isset($definitions[$fqn])) { - return $definitions[$fqn]; - } + // if (isset($definitions[$fqn])) { + // return $definitions[$fqn]; + // } - if ($globalFallback) { - $parts = explode('\\', $fqn); - $fqn = end($parts); - return $this->getDefinition($fqn); - } + // if ($globalFallback) { + // $parts = explode('\\', $fqn); + // $fqn = end($parts); + // return $this->getDefinition($fqn); + // } + + $parts = $this->splitFqn($fqn); + $result = $this->getIndexValue($parts, $this->definitions); + + return $result instanceof Definition ?? null; } /** @@ -173,13 +170,16 @@ class Index implements ReadableIndex, \Serializable */ public function setDefinition(string $fqn, Definition $definition) { - $namespacedFqn = $this->extractNamespacedFqn($fqn); - if (!isset($this->fqnDefinitions[$namespacedFqn])) { - $this->fqnDefinitions[$namespacedFqn] = []; - } + // $namespacedFqn = $this->extractNamespacedFqn($fqn); + // if (!isset($this->fqnDefinitions[$namespacedFqn])) { + // $this->fqnDefinitions[$namespacedFqn] = []; + // } + + // $this->fqnDefinitions[$namespacedFqn][$fqn] = $definition; + + $parts = $this->splitFqn($fqn); + $this->indexDefinition(0, $parts, $this->definitions, $definition); - $this->fqnDefinitions[$namespacedFqn][$fqn] = $definition; - $this->setGlobalDefinition($fqn, $definition); $this->emit('definition-added'); } @@ -192,16 +192,18 @@ class Index implements ReadableIndex, \Serializable */ public function removeDefinition(string $fqn) { - $namespacedFqn = $this->extractNamespacedFqn($fqn); - if (isset($this->fqnDefinitions[$namespacedFqn])) { - unset($this->fqnDefinitions[$namespacedFqn][$fqn]); + // $namespacedFqn = $this->extractNamespacedFqn($fqn); + // if (isset($this->fqnDefinitions[$namespacedFqn])) { + // unset($this->fqnDefinitions[$namespacedFqn][$fqn]); - if (empty($this->fqnDefinitions[$namespacedFqn])) { - unset($this->fqnDefinitions[$namespacedFqn]); - } - } + // if (empty($this->fqnDefinitions[$namespacedFqn])) { + // unset($this->fqnDefinitions[$namespacedFqn]); + // } + // } + + $parts = $this->splitFqn($fqn); + $this->removeIndexedDefinition(0, $parts, $this->definitions); - unset($this->globalDefinitions[$fqn]); unset($this->references[$fqn]); } @@ -276,12 +278,6 @@ class Index implements ReadableIndex, \Serializable foreach ($data as $prop => $val) { $this->$prop = $val; } - - foreach ($this->fqnDefinitions as $fqnDefinition) { - foreach ($fqnDefinition as $fqn => $definition) { - $this->setGlobalDefinition($fqn, $definition); - } - } } /** @@ -291,7 +287,7 @@ class Index implements ReadableIndex, \Serializable public function serialize() { return serialize([ - 'fqnDefinitions' => $this->fqnDefinitions, + 'definitions' => $this->definitions, 'references' => $this->references, 'complete' => $this->complete, 'staticComplete' => $this->staticComplete @@ -299,31 +295,167 @@ class Index implements ReadableIndex, \Serializable } /** - * Registers a definition to the global definitions index if it is global - * - * @param string $fqn The fully qualified name of the symbol - * @param Definition $definition The Definition object - * @return void + * @param string $fqn The symbol FQN + * @return string The namespaced FQN extracted from the given symbol FQN */ - private function setGlobalDefinition(string $fqn, Definition $definition) + // private function extractNamespacedFqn(string $fqn): string + // { + // foreach (['::', '->'] as $operator) { + // if (false !== ($pos = strpos($fqn, $operator))) { + // return substr($fqn, 0, $pos); + // } + // } + + // return $fqn; + // } + + /** + * Returns a Genrerator containing all the into the given $storage recursively. + * The generator yields key => value pairs, eg + * 'Psr\Log\LoggerInterface->log()' => $definition + * + * @param array &$storage + * @param string $prefix (optional) + * @return \Generator + */ + private function generateDefinitionsRecursively(array &$storage, string $prefix = ''): \Generator { - if ($definition->isMember) { - $this->globalDefinitions[$fqn] = $definition; + foreach ($storage as $key => $value) { + $prefix .= $key; + if (!is_array($value)) { + yield $prefix => $value; + } else { + yield from generateDefinitionsRecursively($value, $prefix); + } } } /** - * @param string $fqn The symbol FQN - * @return string The namespaced FQN extracted from the given symbol FQN + * Splits the given FQN into an array, eg : + * 'Psr\Log\LoggerInterface->log' will be ['Psr', '\Log', '\LoggerInterface', '->log()'] + * '\Exception->getMessage()' will be ['\Exception', '->getMessage()'] + * 'PHP_VERSION' will be ['PHP_VERSION'] + * + * @param string $fqn + * @return array */ - private function extractNamespacedFqn(string $fqn): string + private function splitFqn(string $fqn): array { + // split fqn at backslashes + $parts = explode('\\', $fqn); + + // write back the backslach prefix to the first part if it was present + if ('' === $parts[0]) { + $parts = array_slice($parts, 1); + $parts[0] = sprintf('\\%s', $parts[0]); + } + + // write back the backslashes prefixes for the other parts + for ($i = 1; $i < count($parts); $i++) { + $parts[$i] = sprintf('\\%s', $parts[$i]); + } + + // split the last part in 2 parts at the operator + $lastPart = end($parts); foreach (['::', '->'] as $operator) { - if (false !== ($pos = strpos($fqn, $operator))) { - return substr($fqn, 0, $pos); + $endParts = explode($operator, $lastPart); + if (count($endParts) > 1) { + // replace the last part by its pieces + array_pop($parts); + $parts[] = $endParts[0]; + $parts[] = sprintf('%s%s', $operator, $endParts[1]); + break; } } - return $fqn; + return $parts; + } + + /** + * Return the values stored in this index under the given $parts array. + * It can be an index node or a Definition if the $parts are precise + * enough. Returns null when nothing is found. + * + * @param array $parts The splitted FQN + * @param array &$storage The array in which to store the $definition + * @return array|Definition|null + */ + private function getIndexValue(array $parts, array &$storage) + { + $part = $parts[0]; + + if (!isset($storage[$part])) { + return null; + } + + $parts = array_slice($parts, 1); + // we've reached the last provided part + if (0 === count($parts)) { + return $storage[$part]; + } + + return getIndexValue($parts, $storage[$part]); + } + + /** + * Recusrive function which store the given definition in the given $storage + * array represented as a tree matching the given $parts. + * + * @param int $level The current level of FQN part + * @param array $parts The splitted FQN + * @param array &$storage The array in which to store the $definition + * @param Definition $definition The Definition to store + */ + private function indexDefinition(int $level, array $parts, array &$storage, Definition $definition) + { + $part = $parts[$level]; + + if ($level + 1 === count($parts)) { + $storage[$part] = $definition; + + return; + } + + if (!isset($storage[$part])) { + $storage[$part] = []; + } + + $this->indexDefinition($level + 1, $parts, $storage[$part], $definition); + } + + /** + * Recusrive function which remove the definition matching the given $parts + * from the given $storage array. + * The function also looks up recursively to remove the parents of the + * definition which no longer has children to avoid to let empty arrays + * in the index. + * + * @param int $level The current level of FQN part + * @param array $parts The splitted FQN + * @param array &$storage The current array in which to remove data + * @param array &$rootStorage The root storage array + */ + private function removeIndexedDefinition(int $level, array $parts, array &$storage, &$rootStorage) + { + $part = $parts[$level]; + + if ($level + 1 === count($parts)) { + if (isset($storage[$part]) && count($storage[$part]) < 2) { + unset($storage[$part]); + + if (0 === $level) { + // we're at root level, no need to check for parents + // w/o children + return; + } + + array_pop($parts); + // parse again the definition tree to see if the parent + // can be removed too if it has no more children + removeIndexedDefinition(0, $parts, $rootStorage, $rootStorage); + } + } else { + removeIndexedDefinition($level + 1, $parts, $storage[$part], $rootStorage); + } } } diff --git a/src/Index/ReadableIndex.php b/src/Index/ReadableIndex.php index 7d3750e..5c54557 100644 --- a/src/Index/ReadableIndex.php +++ b/src/Index/ReadableIndex.php @@ -37,14 +37,6 @@ interface ReadableIndex extends EmitterInterface */ public function getDefinitions(): \Generator; - /** - * Returns a Generator providing an associative array [string => Definition] - * that maps fully qualified symbol names to global Definitions - * - * @return \Generator providing Definitions[] - */ - public function getGlobalDefinitions(): \Generator; - /** * Returns a Generator providing the Definitions that are in the given FQN *