1
0
Fork 0
php-language-server/src/Index/Index.php

520 lines
15 KiB
PHP
Raw Normal View History

2016-12-13 00:51:02 +00:00
<?php
declare(strict_types = 1);
namespace LanguageServer\Index;
use LanguageServer\Definition;
2017-01-25 00:38:11 +00:00
use Sabre\Event\EmitterTrait;
2016-12-13 00:51:02 +00:00
/**
* Represents the index of a project or dependency
* Serializable for caching
*/
2017-02-03 23:20:38 +00:00
class Index implements ReadableIndex, \Serializable
2016-12-13 00:51:02 +00:00
{
2017-01-25 00:38:11 +00:00
use EmitterTrait;
2016-12-13 00:51:02 +00:00
/**
2017-11-13 20:26:43 +00:00
* An associative array that maps splitted fully qualified symbol names
* to member definitions, eg :
2017-11-13 20:26:43 +00:00
* [
* 'Psr' => [
* '\Log' => [
* '\LoggerInterface' => [
* '->log()' => $definition,
* ],
2017-08-10 08:01:18 +00:00
* ],
2017-11-13 20:26:43 +00:00
* ],
* ]
2016-12-13 00:51:02 +00:00
*
* @var array
2016-12-13 00:51:02 +00:00
*/
private $memberDefinitions = [];
/**
* An associative array that maps splitted fully qualified symbol names
* to non member definitions, eg :
* [
* 'Psr' => [
* '\Log' => [
* '\LoggerInterface' => $definition,
* ],
* ],
* ]
*
* @var array
*/
private $nonMemberDefinitions = [];
2016-12-13 00:51:02 +00:00
/**
2017-08-10 08:01:18 +00:00
* An associative array that maps fully qualified symbol names
* to arrays of document URIs that reference the symbol
2016-12-13 00:51:02 +00:00
*
* @var string[][]
*/
private $references = [];
2017-01-25 00:38:11 +00:00
/**
* @var bool
*/
private $complete = false;
/**
* @var bool
*/
private $staticComplete = false;
/**
* Marks this index as complete
*
* @return void
*/
public function setComplete()
{
if (!$this->isStaticComplete()) {
$this->setStaticComplete();
}
$this->complete = true;
$this->emit('complete');
}
/**
* Marks this index as complete for static definitions and references
*
* @return void
*/
public function setStaticComplete()
{
$this->staticComplete = true;
$this->emit('static-complete');
}
/**
* Returns true if this index is complete
*
* @return bool
*/
public function isComplete(): bool
{
return $this->complete;
}
/**
* Returns true if this index is complete
*
* @return bool
*/
public function isStaticComplete(): bool
{
return $this->staticComplete;
}
2016-12-13 00:51:02 +00:00
/**
* Returns a Generator providing an associative array [string => Definition]
* that maps fully qualified symbol names to Definitions (global or not)
2016-12-13 00:51:02 +00:00
*
* @return \Generator providing Definition[]
2016-12-13 00:51:02 +00:00
*/
public function getDefinitions(): \Generator
2016-12-13 00:51:02 +00:00
{
2017-11-13 20:26:43 +00:00
// foreach ($this->fqnDefinitions as $fqnDefinition) {
// foreach ($fqnDefinition as $fqn => $definition) {
// yield $fqn => $definition;
// }
// }
2016-12-13 00:51:02 +00:00
yield from $this->yieldDefinitionsRecursively($this->memberDefinitions);
yield from $this->yieldDefinitionsRecursively($this->nonMemberDefinitions);
}
/**
2017-10-05 18:45:57 +00:00
* Returns a Generator providing the Definitions that are in the given FQN
*
2017-10-05 18:45:57 +00:00
* @param string $fqn
* @return \Generator providing Definitions[]
*/
2017-10-05 18:45:57 +00:00
public function getDefinitionsForFqn(string $fqn): \Generator
{
2017-11-13 20:26:43 +00:00
// foreach ($this->fqnDefinitions[$fqn] ?? [] as $symbolFqn => $definition) {
// yield $symbolFqn => $definition;
// }
$parts = $this->splitFqn($fqn);
$result = $this->getIndexValue($parts, $this->memberDefinitions);
2017-11-13 20:26:43 +00:00
if ($result instanceof Definition) {
yield $fqn => $result;
2017-11-13 20:26:43 +00:00
} elseif (is_array($result)) {
yield from $this->yieldDefinitionsRecursively($result, $fqn);
} else {
$result = $this->getIndexValue($parts, $this->nonMemberDefinitions);
if ($result instanceof Definition) {
yield $fqn => $result;
} elseif (is_array($result)) {
yield from $this->yieldDefinitionsRecursively($result, $fqn);
}
}
}
2016-12-13 00:51:02 +00:00
/**
* Returns the Definition object by a specific FQN
*
* @param string $fqn
* @param bool $globalFallback Whether to fallback to global if the namespaced FQN was not found
* @return Definition|null
*/
public function getDefinition(string $fqn, bool $globalFallback = false)
{
2017-11-13 20:26:43 +00:00
// $namespacedFqn = $this->extractNamespacedFqn($fqn);
// $definitions = $this->fqnDefinitions[$namespacedFqn] ?? [];
2017-11-13 20:26:43 +00:00
// if (isset($definitions[$fqn])) {
// return $definitions[$fqn];
// }
2017-11-13 20:26:43 +00:00
// if ($globalFallback) {
// $parts = explode('\\', $fqn);
// $fqn = end($parts);
// return $this->getDefinition($fqn);
// }
$parts = $this->splitFqn($fqn);
$result = $this->getIndexValue($parts, $this->memberDefinitions);
2017-11-13 20:26:43 +00:00
if ($result instanceof Definition) {
return $result;
}
$result = $this->getIndexValue($parts, $this->nonMemberDefinitions);
return $result instanceof Definition
? $result
: null
;
2016-12-13 00:51:02 +00:00
}
/**
* Registers a definition
*
* @param string $fqn The fully qualified name of the symbol
2017-04-17 15:03:08 +00:00
* @param Definition $definition The Definition object
2016-12-13 00:51:02 +00:00
* @return void
*/
public function setDefinition(string $fqn, Definition $definition)
{
2017-11-13 20:26:43 +00:00
// $namespacedFqn = $this->extractNamespacedFqn($fqn);
// if (!isset($this->fqnDefinitions[$namespacedFqn])) {
// $this->fqnDefinitions[$namespacedFqn] = [];
// }
// $this->fqnDefinitions[$namespacedFqn][$fqn] = $definition;
$parts = $this->splitFqn($fqn);
if ($definition->isMember) {
$this->indexDefinition(0, $parts, $this->memberDefinitions, $definition);
} else {
$this->indexDefinition(0, $parts, $this->nonMemberDefinitions, $definition);
}
2017-01-25 00:38:11 +00:00
$this->emit('definition-added');
2016-12-13 00:51:02 +00:00
}
/**
* Unsets the Definition for a specific symbol
* and removes all references pointing to that symbol
*
* @param string $fqn The fully qualified name of the symbol
* @return void
*/
public function removeDefinition(string $fqn)
{
2017-11-13 20:26:43 +00:00
// $namespacedFqn = $this->extractNamespacedFqn($fqn);
// if (isset($this->fqnDefinitions[$namespacedFqn])) {
// unset($this->fqnDefinitions[$namespacedFqn][$fqn]);
2017-11-13 20:26:43 +00:00
// if (empty($this->fqnDefinitions[$namespacedFqn])) {
// unset($this->fqnDefinitions[$namespacedFqn]);
// }
// }
$parts = $this->splitFqn($fqn);
if (true !== $this->removeIndexedDefinition(0, $parts, $this->memberDefinitions)) {
$this->removeIndexedDefinition(0, $parts, $this->nonMemberDefinitions);
}
2016-12-13 00:51:02 +00:00
unset($this->references[$fqn]);
}
/**
2017-08-09 22:06:53 +00:00
* Returns a Generator providing all URIs in this index that reference a symbol
2016-12-13 00:51:02 +00:00
*
* @param string $fqn The fully qualified name of the symbol
2017-08-09 22:06:53 +00:00
* @return \Generator providing string[]
2016-12-13 00:51:02 +00:00
*/
2017-08-09 22:06:53 +00:00
public function getReferenceUris(string $fqn): \Generator
2016-12-13 00:51:02 +00:00
{
2017-08-10 20:03:16 +00:00
foreach ($this->references[$fqn] ?? [] as $uri) {
2017-08-09 22:06:53 +00:00
yield $uri;
}
2016-12-13 00:51:02 +00:00
}
/**
* For test use.
* Returns all references, keyed by fqn.
*
* @return string[][]
*/
public function getReferences(): array
{
return $this->references;
}
2016-12-13 00:51:02 +00:00
/**
* Adds a document URI as a referencee of a specific symbol
*
* @param string $fqn The fully qualified name of the symbol
* @return void
*/
public function addReferenceUri(string $fqn, string $uri)
{
if (!isset($this->references[$fqn])) {
$this->references[$fqn] = [];
}
// TODO: use DS\Set instead of searching array
if (array_search($uri, $this->references[$fqn], true) === false) {
$this->references[$fqn][] = $uri;
}
}
/**
* Removes a document URI as the container for a specific symbol
*
* @param string $fqn The fully qualified name of the symbol
* @param string $uri The URI
* @return void
*/
public function removeReferenceUri(string $fqn, string $uri)
{
if (!isset($this->references[$fqn])) {
return;
}
$index = array_search($fqn, $this->references[$fqn], true);
if ($index === false) {
return;
}
array_splice($this->references[$fqn], $index, 1);
}
2017-02-03 23:20:38 +00:00
/**
* @param string $serialized
* @return void
*/
public function unserialize($serialized)
{
$data = unserialize($serialized);
2017-02-03 23:20:38 +00:00
foreach ($data as $prop => $val) {
$this->$prop = $val;
}
}
/**
* @param string $serialized
* @return string
*/
public function serialize()
{
return serialize([
'memberDefinitions' => $this->memberDefinitions,
'nonMemberDefinitions' => $this->nonMemberDefinitions,
2017-02-03 23:20:38 +00:00
'references' => $this->references,
'complete' => $this->complete,
'staticComplete' => $this->staticComplete
]);
}
/**
2017-11-13 20:26:43 +00:00
* @param string $fqn The symbol FQN
* @return string The namespaced FQN extracted from the given symbol FQN
*/
// 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
*
2017-11-13 20:26:43 +00:00
* @param array &$storage
* @param string $prefix (optional)
* @return \Generator
*/
private function yieldDefinitionsRecursively(array &$storage, string $prefix = ''): \Generator
{
2017-11-13 20:26:43 +00:00
foreach ($storage as $key => $value) {
if (!is_array($value)) {
yield sprintf('%s%s', $prefix, $key) => $value;
2017-11-13 20:26:43 +00:00
} else {
yield from $this->yieldDefinitionsRecursively($value, sprintf('%s%s', $prefix, $key));
2017-11-13 20:26:43 +00:00
}
}
}
/**
2017-11-13 20:26:43 +00:00
* 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
*/
2017-11-13 20:26:43 +00:00
private function splitFqn(string $fqn): array
{
2017-11-13 20:26:43 +00:00
// split fqn at backslashes
$parts = explode('\\', $fqn);
// write back the backslach prefix to the first part if it was present
if ('' === $parts[0]) {
if (count($parts) > 1) {
$parts = array_slice($parts, 1);
}
2017-11-13 20:26:43 +00:00
$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) {
2017-11-13 20:26:43 +00:00
$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;
}
}
2017-11-13 20:26:43 +00:00
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];
}
if (!is_array($storage[$part]) && count($parts) > 0) {
// we're looking for a member definition in the non member index,
// no matches can be found.
return null;
}
return $this->getIndexValue($parts, $storage[$part]);
2017-11-13 20:26:43 +00:00
}
/**
* 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] = [];
}
if (!is_array($storage[$part])) {
// it's a non member definition, we can't add it to the member
// definitions index
return;
}
2017-11-13 20:26:43 +00:00
$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
* @return boolean|null True when the definition has been found and removed, null otherwise.
2017-11-13 20:26:43 +00:00
*/
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 true;
2017-11-13 20:26:43 +00:00
}
array_pop($parts);
// parse again the definition tree to see if the parent
// can be removed too if it has no more children
return $this->removeIndexedDefinition(0, $parts, $rootStorage, $rootStorage);
2017-11-13 20:26:43 +00:00
}
} else {
return $this->removeIndexedDefinition($level + 1, $parts, $storage[$part], $rootStorage);
2017-11-13 20:26:43 +00:00
}
}
2016-12-13 00:51:02 +00:00
}