1
0
Fork 0

Merge branch 'master' into master

pull/670/head
Felix Becker 2018-11-11 03:49:48 +01:00 committed by GitHub
commit 3905ca0c50
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 949 additions and 246 deletions

89
benchmarks/completion.php Normal file
View File

@ -0,0 +1,89 @@
<?php
namespace LanguageServer\Tests;
require __DIR__ . '/../vendor/autoload.php';
use Composer\XdebugHandler\XdebugHandler;
use Exception;
use LanguageServer\CompletionProvider;
use LanguageServer\DefinitionResolver;
use LanguageServer\Index\Index;
use LanguageServer\PhpDocument;
use LanguageServer\StderrLogger;
use LanguageServerProtocol\Position;
use Microsoft\PhpParser;
use phpDocumentor\Reflection\DocBlockFactory;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
$logger = new StderrLogger();
$xdebugHandler = new XdebugHandler('PHPLS');
$xdebugHandler->setLogger($logger);
$xdebugHandler->check();
unset($xdebugHandler);
$totalSize = 0;
$framework = "symfony";
$iterator = new RecursiveDirectoryIterator(__DIR__ . "/../validation/frameworks/$framework");
$testProviderArray = array();
foreach (new RecursiveIteratorIterator($iterator) as $file) {
if (strpos((string)$file, ".php") !== false) {
$totalSize += $file->getSize();
$testProviderArray[] = $file->getRealPath();
}
}
if (count($testProviderArray) === 0) {
throw new Exception("ERROR: Validation testsuite frameworks not found - run `git submodule update --init --recursive` to download.");
}
$index = new Index;
$definitionResolver = new DefinitionResolver($index);
$completionProvider = new CompletionProvider($definitionResolver, $index);
$docBlockFactory = DocBlockFactory::createInstance();
$completionFile = realpath(__DIR__ . '/../validation/frameworks/symfony/src/Symfony/Component/HttpFoundation/Request.php');
$parser = new PhpParser\Parser();
$completionDocument = null;
echo "Indexing $framework" . PHP_EOL;
foreach ($testProviderArray as $idx => $testCaseFile) {
if (filesize($testCaseFile) > 100000) {
continue;
}
if ($idx % 100 === 0) {
echo $idx . '/' . count($testProviderArray) . PHP_EOL;
}
$fileContents = file_get_contents($testCaseFile);
try {
$d = new PhpDocument($testCaseFile, $fileContents, $index, $parser, $docBlockFactory, $definitionResolver);
if ($testCaseFile === $completionFile) {
$completionDocument = $d;
}
} catch (\Throwable $e) {
echo $e->getMessage() . PHP_EOL;
continue;
}
}
echo "Getting completion". PHP_EOL;
// Completion in $this->|request = new ParameterBag($request);
$start = microtime(true);
$list = $completionProvider->provideCompletion($completionDocument, new Position(274, 15));
$end = microtime(true);
echo 'Time ($this->|): ' . ($end - $start) . 's' . PHP_EOL;
echo count($list->items) . ' completion items' . PHP_EOL;
// Completion in $this->request = new| ParameterBag($request);
// (this only finds ParameterBag though.)
$start = microtime(true);
$list = $completionProvider->provideCompletion($completionDocument, new Position(274, 28));
$end = microtime(true);
echo 'Time (new|): ' . ($end - $start) . 's' . PHP_EOL;
echo count($list->items) . ' completion items' . PHP_EOL;

View File

@ -1,23 +1,31 @@
<?php <?php
namespace LanguageServer\Tests; namespace LanguageServer\Tests;
require __DIR__ . '/vendor/autoload.php'; require __DIR__ . '/../vendor/autoload.php';
use Composer\XdebugHandler\XdebugHandler;
use Exception; use Exception;
use LanguageServer\DefinitionResolver;
use LanguageServer\Index\Index; use LanguageServer\Index\Index;
use LanguageServer\PhpDocument; use LanguageServer\PhpDocument;
use LanguageServer\DefinitionResolver; use LanguageServer\StderrLogger;
use Microsoft\PhpParser; use Microsoft\PhpParser;
use phpDocumentor\Reflection\DocBlockFactory; use phpDocumentor\Reflection\DocBlockFactory;
use RecursiveDirectoryIterator; use RecursiveDirectoryIterator;
use RecursiveIteratorIterator; use RecursiveIteratorIterator;
$logger = new StderrLogger();
$xdebugHandler = new XdebugHandler('PHPLS');
$xdebugHandler->setLogger($logger);
$xdebugHandler->check();
unset($xdebugHandler);
$totalSize = 0; $totalSize = 0;
$frameworks = ["drupal", "wordpress", "php-language-server", "tolerant-php-parser", "math-php", "symfony", "codeigniter", "cakephp"]; $frameworks = ["drupal", "wordpress", "php-language-server", "tolerant-php-parser", "math-php", "symfony", "codeigniter", "cakephp"];
foreach($frameworks as $framework) { foreach($frameworks as $framework) {
$iterator = new RecursiveDirectoryIterator(__DIR__ . "/validation/frameworks/$framework"); $iterator = new RecursiveDirectoryIterator(__DIR__ . "/../validation/frameworks/$framework");
$testProviderArray = array(); $testProviderArray = array();
foreach (new RecursiveIteratorIterator($iterator) as $file) { foreach (new RecursiveIteratorIterator($iterator) as $file) {
@ -37,8 +45,8 @@ foreach($frameworks as $framework) {
if (filesize($testCaseFile) > 10000) { if (filesize($testCaseFile) > 10000) {
continue; continue;
} }
if ($idx % 1000 === 0) { if ($idx % 500 === 0) {
echo "$idx\n"; echo $idx . '/' . count($testProviderArray) . PHP_EOL;
} }
$fileContents = file_get_contents($testCaseFile); $fileContents = file_get_contents($testCaseFile);

View File

@ -0,0 +1,10 @@
<?php
namespace Whatever;
use TestNamespace\InnerNamespace as AliasNamespace;
class IDontShowUpInCompletion {}
AliasNamespace\I;
AliasNamespace\;

View File

@ -103,3 +103,8 @@ class Example {
public function __construct() {} public function __construct() {}
public function __destruct() {} public function __destruct() {}
} }
namespace TestNamespace\InnerNamespace;
class InnerClass {
}

View File

@ -17,7 +17,15 @@ use LanguageServerProtocol\{
}; };
use Microsoft\PhpParser; use Microsoft\PhpParser;
use Microsoft\PhpParser\Node; use Microsoft\PhpParser\Node;
use Microsoft\PhpParser\ResolvedName;
use Generator; use Generator;
use function LanguageServer\FqnUtilities\{
nameConcat,
nameGetFirstPart,
nameGetParent,
nameStartsWith,
nameWithoutFirstPart
};
class CompletionProvider class CompletionProvider
{ {
@ -144,8 +152,11 @@ class CompletionProvider
* @param CompletionContext $context The completion context * @param CompletionContext $context The completion context
* @return CompletionList * @return CompletionList
*/ */
public function provideCompletion(PhpDocument $doc, Position $pos, CompletionContext $context = null): CompletionList public function provideCompletion(
{ PhpDocument $doc,
Position $pos,
CompletionContext $context = null
): CompletionList {
// This can be made much more performant if the tree follows specific invariants. // This can be made much more performant if the tree follows specific invariants.
$node = $doc->getNodeAtPosition($pos); $node = $doc->getNodeAtPosition($pos);
@ -237,16 +248,14 @@ class CompletionProvider
$this->definitionResolver->resolveExpressionNodeToType($node->dereferencableExpression) $this->definitionResolver->resolveExpressionNodeToType($node->dereferencableExpression)
); );
// Add the object access operator to only get members of all parents // The FQNs of the symbol and its parents (eg the implemented interfaces)
$prefixes = []; foreach ($this->expandParentFqns($fqns) as $parentFqn) {
foreach ($this->expandParentFqns($fqns) as $prefix) { // Add the object access operator to only get members of all parents
$prefixes[] = $prefix . '->'; $prefix = $parentFqn . '->';
} $prefixLen = strlen($prefix);
// Collect fqn definitions
// Collect all definitions that match any of the prefixes foreach ($this->index->getChildDefinitionsForFqn($parentFqn) as $fqn => $def) {
foreach ($this->index->getDefinitions() as $fqn => $def) { if (substr($fqn, 0, $prefixLen) === $prefix && $def->isMember) {
foreach ($prefixes as $prefix) {
if (substr($fqn, 0, strlen($prefix)) === $prefix && $def->isMember) {
$list->items[] = CompletionItemFactory::fromDefinition($def); $list->items[] = CompletionItemFactory::fromDefinition($def);
} }
} }
@ -270,16 +279,14 @@ class CompletionProvider
$classType = $this->definitionResolver->resolveExpressionNodeToType($scoped->scopeResolutionQualifier) $classType = $this->definitionResolver->resolveExpressionNodeToType($scoped->scopeResolutionQualifier)
); );
// Append :: operator to only get static members of all parents // The FQNs of the symbol and its parents (eg the implemented interfaces)
$prefixes = []; foreach ($this->expandParentFqns($fqns) as $parentFqn) {
foreach ($this->expandParentFqns($fqns) as $prefix) { // Append :: operator to only get static members of all parents
$prefixes[] = $prefix . '::'; $prefix = strtolower($parentFqn . '::');
} $prefixLen = strlen($prefix);
// Collect fqn definitions
// Collect all definitions that match any of the prefixes foreach ($this->index->getChildDefinitionsForFqn($parentFqn) as $fqn => $def) {
foreach ($this->index->getDefinitions() as $fqn => $def) { if (substr(strtolower($fqn), 0, $prefixLen) === $prefix && $def->isMember) {
foreach ($prefixes as $prefix) {
if (substr(strtolower($fqn), 0, strlen($prefix)) === strtolower($prefix) && $def->isMember) {
$list->items[] = CompletionItemFactory::fromDefinition($def); $list->items[] = CompletionItemFactory::fromDefinition($def);
} }
} }
@ -297,114 +304,278 @@ class CompletionProvider
// my_func| // my_func|
// MY_CONS| // MY_CONS|
// MyCla| // MyCla|
// \MyCla|
// The name Node under the cursor // The name Node under the cursor
$nameNode = isset($creation) ? $creation->classTypeDesignator : $node; $nameNode = isset($creation) ? $creation->classTypeDesignator : $node;
/** The typed name */ if ($nameNode instanceof Node\QualifiedName) {
$prefix = $nameNode instanceof Node\QualifiedName /** @var string The typed name. */
? (string)PhpParser\ResolvedName::buildName($nameNode->nameParts, $nameNode->getFileContents()) $prefix = (string)PhpParser\ResolvedName::buildName($nameNode->nameParts, $nameNode->getFileContents());
: $nameNode->getText($node->getFileContents()); } else {
$prefixLen = strlen($prefix); $prefix = $nameNode->getText($node->getFileContents());
}
/** Whether the prefix is qualified (contains at least one backslash) */
$isQualified = $nameNode instanceof Node\QualifiedName && $nameNode->isQualifiedName();
/** Whether the prefix is fully qualified (begins with a backslash) */
$isFullyQualified = $nameNode instanceof Node\QualifiedName && $nameNode->isFullyQualifiedName();
/** The closest NamespaceDefinition Node */
$namespaceNode = $node->getNamespaceDefinition(); $namespaceNode = $node->getNamespaceDefinition();
/** @var string The current namespace without a leading backslash. */
$currentNamespace = $namespaceNode === null ? '' : $namespaceNode->name->getText();
/** @var string The name of the namespace */ /** @var bool Whether the prefix is qualified (contains at least one backslash) */
$namespacedPrefix = null; $isFullyQualified = false;
if ($namespaceNode) {
$namespacedPrefix = (string)PhpParser\ResolvedName::buildName($namespaceNode->name->nameParts, $node->getFileContents()) . '\\' . $prefix; /** @var bool Whether the prefix is qualified (contains at least one backslash) */
$namespacedPrefixLen = strlen($namespacedPrefix); $isQualified = false;
if ($nameNode instanceof Node\QualifiedName) {
$isFullyQualified = $nameNode->isFullyQualifiedName();
$isQualified = $nameNode->isQualifiedName();
} }
// Get the namespace use statements /** @var bool Whether we are in a new expression */
// TODO: use function statements, use const statements $isCreation = isset($creation);
/** @var string[] $aliases A map from local alias to fully qualified name */ /** @var array Import (use) tables */
list($aliases,,) = $node->getImportTablesForCurrentScope(); $importTables = $node->getImportTablesForCurrentScope();
foreach ($aliases as $alias => $name) { if ($isFullyQualified) {
$aliases[$alias] = (string)$name; // \Prefix\Goes\Here| - Only return completions from the root namespace.
/** @var $items \Generator|CompletionItem[] Generator yielding CompletionItems indexed by their FQN */
$items = $this->getCompletionsForFqnPrefix($prefix, $isCreation, false);
} else if ($isQualified) {
// Prefix\Goes\Here|
$items = $this->getPartiallyQualifiedCompletions(
$prefix,
$currentNamespace,
$importTables,
$isCreation
);
} else {
// PrefixGoesHere|
$items = $this->getUnqualifiedCompletions($prefix, $currentNamespace, $importTables, $isCreation);
} }
// If there is a prefix that does not start with a slash, suggest `use`d symbols $list->items = array_values(iterator_to_array($items));
if ($prefix && !$isFullyQualified) { foreach ($list->items as $item) {
foreach ($aliases as $alias => $fqn) { // Remove ()
// Suggest symbols that have been `use`d and match the prefix if (is_string($item->insertText) && substr($item->insertText, strlen($item->insertText) - 2) === '()') {
if (substr($alias, 0, $prefixLen) === $prefix && ($def = $this->index->getDefinition($fqn))) { $item->insertText = substr($item->insertText, 0, -2);
$list->items[] = CompletionItemFactory::fromDefinition($def);
}
} }
} }
// Suggest global symbols that either }
// - start with the current namespace + prefix, if the Name node is not fully qualified return $list;
// - start with just the prefix, if the Name node is fully qualified }
foreach ($this->index->getDefinitions() as $fqn => $def) {
$fqnStartsWithPrefix = substr($fqn, 0, $prefixLen) === $prefix; private function getPartiallyQualifiedCompletions(
string $prefix,
if ( string $currentNamespace,
// Exclude methods, properties etc. array $importTables,
!$def->isMember bool $requireCanBeInstantiated
&& ( ): \Generator {
!$prefix // If the first part of the partially qualified name matches a namespace alias,
|| ( // only definitions below that alias can be completed.
// Either not qualified, but a matching prefix with global fallback list($namespaceAliases,,) = $importTables;
($def->roamed && !$isQualified && $fqnStartsWithPrefix) $prefixFirstPart = nameGetFirstPart($prefix);
// Or not in a namespace or a fully qualified name or AND matching the prefix $foundAlias = $foundAliasFqn = null;
|| ((!$namespaceNode || $isFullyQualified) && $fqnStartsWithPrefix) foreach ($namespaceAliases as $alias => $aliasFqn) {
// Or in a namespace, not fully qualified and matching the prefix + current namespace if (strcasecmp($prefixFirstPart, $alias) === 0) {
|| ( $foundAlias = $alias;
$namespaceNode $foundAliasFqn = (string)$aliasFqn;
&& !$isFullyQualified break;
&& substr($fqn, 0, $namespacedPrefixLen) === $namespacedPrefix
)
)
)
// Only suggest classes for `new`
&& (!isset($creation) || $def->canBeInstantiated)
) {
$item = CompletionItemFactory::fromDefinition($def);
// Find the shortest name to reference the symbol
if ($namespaceNode && ($alias = array_search($fqn, $aliases, true)) !== false) {
// $alias is the name under which this definition is aliased in the current namespace
$item->insertText = $alias;
} else if ($namespaceNode && !($prefix && $isFullyQualified)) {
// Insert the global FQN with leading backslash
$item->insertText = '\\' . $fqn;
} else {
// Insert the FQN without leading backlash
$item->insertText = $fqn;
}
// Don't insert the parenthesis for functions
// TODO return a snippet and put the cursor inside
if (substr($item->insertText, -2) === '()') {
$item->insertText = substr($item->insertText, 0, -2);
}
$list->items[] = $item;
}
}
// If not a class instantiation, also suggest keywords
if (!isset($creation)) {
foreach (self::KEYWORDS as $keyword) {
if (substr($keyword, 0, $prefixLen) === $prefix) {
$item = new CompletionItem($keyword, CompletionItemKind::KEYWORD);
$item->insertText = $keyword;
$list->items[] = $item;
}
}
} }
} }
return $list; if ($foundAlias !== null) {
yield from $this->getCompletionsFromAliasedNamespace(
$prefix,
$foundAlias,
$foundAliasFqn,
$requireCanBeInstantiated
);
} else {
yield from $this->getCompletionsForFqnPrefix(
nameConcat($currentNamespace, $prefix),
$requireCanBeInstantiated,
false
);
}
}
/**
* Yields completions for non-qualified global names.
*
* Yields
* - Aliased classes
* - Completions from current namespace
* - Roamed completions from the global namespace (when not creating and not already in root NS)
* - PHP keywords (when not creating)
*
* @return \Generator|CompletionItem[]
* Yields CompletionItems
*/
private function getUnqualifiedCompletions(
string $prefix,
string $currentNamespace,
array $importTables,
bool $requireCanBeInstantiated
): \Generator {
// Aliases
list($namespaceAliases,,) = $importTables;
// use Foo\Bar
yield from $this->getCompletionsForAliases(
$prefix,
$namespaceAliases,
$requireCanBeInstantiated
);
// Completions from the current namespace
yield from $this->getCompletionsForFqnPrefix(
nameConcat($currentNamespace, $prefix),
$requireCanBeInstantiated,
false
);
if ($currentNamespace !== '' && $prefix === '') {
// Get additional suggestions from the global namespace.
// When completing e.g. for new |, suggest \DateTime
yield from $this->getCompletionsForFqnPrefix('', $requireCanBeInstantiated, true);
}
if (!$requireCanBeInstantiated) {
if ($currentNamespace !== '' && $prefix !== '') {
// Roamed definitions (i.e. global constants and functions). The prefix is checked against '', since
// in that case global completions have already been provided (including non-roamed definitions.)
yield from $this->getRoamedCompletions($prefix);
}
// Lastly and least importantly, suggest keywords.
yield from $this->getCompletionsForKeywords($prefix);
}
}
/**
* Gets completions for prefixes of fully qualified names in their parent namespace.
*
* @param string $prefix Prefix to complete for. Fully qualified.
* @param bool $requireCanBeInstantiated If set, only return classes.
* @param bool $insertFullyQualified If set, return completion with the leading \ inserted.
* @return \Generator|CompletionItem[]
* Yields CompletionItems.
*/
private function getCompletionsForFqnPrefix(
string $prefix,
bool $requireCanBeInstantiated,
bool $insertFullyQualified
): \Generator {
$namespace = nameGetParent($prefix);
foreach ($this->index->getChildDefinitionsForFqn($namespace) as $fqn => $def) {
if ($requireCanBeInstantiated && !$def->canBeInstantiated) {
continue;
}
if (!nameStartsWith($fqn, $prefix)) {
continue;
}
$completion = CompletionItemFactory::fromDefinition($def);
if ($insertFullyQualified) {
$completion->insertText = '\\' . $fqn;
}
yield $fqn => $completion;
}
}
/**
* Gets completions for non-qualified names matching the start of an used class, function, or constant.
*
* @param string $prefix Non-qualified name being completed for
* @param QualifiedName[] $aliases Array of alias FQNs indexed by the alias.
* @return \Generator|CompletionItem[]
* Yields CompletionItems.
*/
private function getCompletionsForAliases(
string $prefix,
array $aliases,
bool $requireCanBeInstantiated
): \Generator {
foreach ($aliases as $alias => $aliasFqn) {
if (!nameStartsWith($alias, $prefix)) {
continue;
}
$definition = $this->index->getDefinition((string)$aliasFqn);
if ($definition) {
if ($requireCanBeInstantiated && !$definition->canBeInstantiated) {
continue;
}
$completionItem = CompletionItemFactory::fromDefinition($definition);
$completionItem->insertText = $alias;
yield (string)$aliasFqn => $completionItem;
}
}
}
/**
* Gets completions for partially qualified names, where the first part is matched by an alias.
*
* @return \Generator|CompletionItem[]
* Yields CompletionItems.
*/
private function getCompletionsFromAliasedNamespace(
string $prefix,
string $alias,
string $aliasFqn,
bool $requireCanBeInstantiated
): \Generator {
$prefixFirstPart = nameGetFirstPart($prefix);
// Matched alias.
$resolvedPrefix = nameConcat($aliasFqn, nameWithoutFirstPart($prefix));
$completionItems = $this->getCompletionsForFqnPrefix(
$resolvedPrefix,
$requireCanBeInstantiated,
false
);
// Convert FQNs in the CompletionItems so they are expressed in terms of the alias.
foreach ($completionItems as $fqn => $completionItem) {
/** @var string $fqn with the leading parts determined by the alias removed. Has the leading backslash. */
$nameWithoutAliasedPart = substr($fqn, strlen($aliasFqn));
$completionItem->insertText = $alias . $nameWithoutAliasedPart;
yield $fqn => $completionItem;
}
}
/**
* Gets completions for globally defined functions and constants (i.e. symbols which may be used anywhere)
*
* @return \Generator|CompletionItem[]
* Yields CompletionItems.
*/
private function getRoamedCompletions(string $prefix): \Generator
{
foreach ($this->index->getChildDefinitionsForFqn('') as $fqn => $def) {
if (!$def->roamed || !nameStartsWith($fqn, $prefix)) {
continue;
}
$completionItem = CompletionItemFactory::fromDefinition($def);
// Second-guessing the user here - do not trust roaming to work. If the same symbol is
// inserted in the current namespace, the code will stop working.
$completionItem->insertText = '\\' . $fqn;
yield $fqn => $completionItem;
}
}
/**
* Completes PHP keywords.
*
* @return \Generator|CompletionItem[]
* Yields CompletionItems.
*/
private function getCompletionsForKeywords(string $prefix): \Generator
{
foreach (self::KEYWORDS as $keyword) {
if (nameStartsWith($keyword, $prefix)) {
$item = new CompletionItem($keyword, CompletionItemKind::KEYWORD);
$item->insertText = $keyword;
yield $keyword => $item;
}
}
} }
/** /**
@ -473,8 +644,9 @@ class CompletionProvider
} }
} }
if ($level instanceof Node\Expression\AnonymousFunctionCreationExpression && $level->anonymousFunctionUseClause !== null && if ($level instanceof Node\Expression\AnonymousFunctionCreationExpression
$level->anonymousFunctionUseClause->useVariableNameList !== null) { && $level->anonymousFunctionUseClause !== null
&& $level->anonymousFunctionUseClause->useVariableNameList !== null) {
foreach ($level->anonymousFunctionUseClause->useVariableNameList->getValues() as $use) { foreach ($level->anonymousFunctionUseClause->useVariableNameList->getValues() as $use) {
$useName = $use->getName(); $useName = $use->getName();
if (empty($namePrefix) || strpos($useName, $namePrefix) !== false) { if (empty($namePrefix) || strpos($useName, $namePrefix) !== false) {

View File

@ -1239,7 +1239,13 @@ class DefinitionResolver
if ( if (
$node instanceof PhpParser\ClassLike $node instanceof PhpParser\ClassLike
) { ) {
return (string) $node->getNamespacedName(); $className = (string)$node->getNamespacedName();
// An (invalid) class declaration without a name will have an empty string as name,
// but should not define an FQN
if ($className === '') {
return null;
}
return $className;
} }
// INPUT OUTPUT: // INPUT OUTPUT:

View File

@ -28,3 +28,91 @@ function getFqnsFromType($type): array
} }
return $fqns; return $fqns;
} }
/**
* Returns parent of an FQN.
*
* getFqnParent('') === ''
* getFqnParent('\\') === ''
* getFqnParent('\A') === ''
* getFqnParent('A') === ''
* getFqnParent('\A\') === '\A' // Empty trailing name is considered a name.
*
* @return string
*/
function nameGetParent(string $name): string
{
if ($name === '') { // Special-case handling for the root namespace.
return '';
}
$parts = explode('\\', $name);
array_pop($parts);
return implode('\\', $parts);
}
/**
* Concatenates two names.
*
* nameConcat('\Foo\Bar', 'Baz') === '\Foo\Bar\Baz'
* nameConcat('\Foo\Bar\\', '\Baz') === '\Foo\Bar\Baz'
* nameConcat('\\', 'Baz') === '\Baz'
* nameConcat('', 'Baz') === 'Baz'
*
* @return string
*/
function nameConcat(string $a, string $b): string
{
if ($a === '') {
return $b;
}
$a = rtrim($a, '\\');
$b = ltrim($b, '\\');
return "$a\\$b";
}
/**
* Returns the first component of $name.
*
* nameGetFirstPart('Foo\Bar') === 'Foo'
* nameGetFirstPart('\Foo\Bar') === 'Foo'
* nameGetFirstPart('') === ''
* nameGetFirstPart('\') === ''
*/
function nameGetFirstPart(string $name): string
{
$parts = explode('\\', $name, 3);
if ($parts[0] === '' && count($parts) > 1) {
return $parts[1];
} else {
return $parts[0];
}
}
/**
* Removes the first component of $name.
*
* nameWithoutFirstPart('Foo\Bar') === 'Bar'
* nameWithoutFirstPart('\Foo\Bar') === 'Bar'
* nameWithoutFirstPart('') === ''
* nameWithoutFirstPart('\') === ''
*/
function nameWithoutFirstPart(string $name): string
{
$parts = explode('\\', $name, 3);
if ($parts[0] === '') {
array_shift($parts);
}
array_shift($parts);
return implode('\\', $parts);
}
/**
* @param string $name Name to match against
* @param string $prefix Prefix $name has to starts with
* @return bool
*/
function nameStartsWith(string $name, string $prefix): bool
{
return strlen($name) >= strlen($prefix)
&& strncmp($name, $prefix, strlen($prefix)) === 0;
}

View File

@ -99,20 +99,29 @@ abstract class AbstractAggregateIndex implements ReadableIndex
} }
/** /**
* Returns an associative array [string => Definition] that maps fully qualified symbol names * Returns a Generator providing an associative array [string => Definition]
* to Definitions * that maps fully qualified symbol names to Definitions (global or not)
* *
* @return Definition[] * @return \Generator yields Definition
*/ */
public function getDefinitions(): array public function getDefinitions(): \Generator
{ {
$defs = [];
foreach ($this->getIndexes() as $index) { foreach ($this->getIndexes() as $index) {
foreach ($index->getDefinitions() as $fqn => $def) { yield from $index->getDefinitions();
$defs[$fqn] = $def; }
} }
/**
* Returns a Generator that yields all the direct child Definitions of a given FQN
*
* @param string $fqn
* @return \Generator yields Definition
*/
public function getChildDefinitionsForFqn(string $fqn): \Generator
{
foreach ($this->getIndexes() as $index) {
yield from $index->getChildDefinitionsForFqn($fqn);
} }
return $defs;
} }
/** /**
@ -132,19 +141,15 @@ abstract class AbstractAggregateIndex implements ReadableIndex
} }
/** /**
* Returns all URIs in this index that reference a symbol * Returns a Generator providing all URIs in this index that reference a symbol
* *
* @param string $fqn The fully qualified name of the symbol * @param string $fqn The fully qualified name of the symbol
* @return string[] * @return \Generator yields string
*/ */
public function getReferenceUris(string $fqn): array public function getReferenceUris(string $fqn): \Generator
{ {
$refs = [];
foreach ($this->getIndexes() as $index) { foreach ($this->getIndexes() as $index) {
foreach ($index->getReferenceUris($fqn) as $ref) { yield from $index->getReferenceUris($fqn);
$refs[] = $ref;
}
} }
return $refs;
} }
} }

View File

@ -15,14 +15,26 @@ class Index implements ReadableIndex, \Serializable
use EmitterTrait; use EmitterTrait;
/** /**
* An associative array that maps fully qualified symbol names to Definitions * An associative array that maps splitted fully qualified symbol names
* to 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
* ],
* ],
* ],
* ]
* *
* @var Definition[] * @var array
*/ */
private $definitions = []; private $definitions = [];
/** /**
* An associative array that maps fully qualified symbol names to arrays of document URIs that reference the symbol * An associative array that maps fully qualified symbol names
* to arrays of document URIs that reference the symbol
* *
* @var string[][] * @var string[][]
*/ */
@ -84,14 +96,46 @@ class Index implements ReadableIndex, \Serializable
} }
/** /**
* Returns an associative array [string => Definition] that maps fully qualified symbol names * Returns a Generator providing an associative array [string => Definition]
* to Definitions * that maps fully qualified symbol names to Definitions (global or not)
* *
* @return Definition[] * @return \Generator yields Definition
*/ */
public function getDefinitions(): array public function getDefinitions(): \Generator
{ {
return $this->definitions; yield from $this->yieldDefinitionsRecursively($this->definitions);
}
/**
* Returns a Generator that yields all the direct child Definitions of a given FQN
*
* @param string $fqn
* @return \Generator yields Definition
*/
public function getChildDefinitionsForFqn(string $fqn): \Generator
{
$parts = $this->splitFqn($fqn);
if ('' === end($parts)) {
// we want to return all the definitions in the given FQN, not only
// the one (non member) matching exactly the FQN.
array_pop($parts);
}
$result = $this->getIndexValue($parts, $this->definitions);
if (!$result) {
return;
}
foreach ($result as $name => $item) {
// Don't yield the parent
if ($name === '') {
continue;
}
if ($item instanceof Definition) {
yield $fqn.$name => $item;
} elseif (is_array($item) && isset($item[''])) {
yield $fqn.$name => $item[''];
}
}
} }
/** /**
@ -103,12 +147,17 @@ class Index implements ReadableIndex, \Serializable
*/ */
public function getDefinition(string $fqn, bool $globalFallback = false) public function getDefinition(string $fqn, bool $globalFallback = false)
{ {
if (isset($this->definitions[$fqn])) { $parts = $this->splitFqn($fqn);
return $this->definitions[$fqn]; $result = $this->getIndexValue($parts, $this->definitions);
if ($result instanceof Definition) {
return $result;
} }
if ($globalFallback) { if ($globalFallback) {
$parts = explode('\\', $fqn); $parts = explode('\\', $fqn);
$fqn = end($parts); $fqn = end($parts);
return $this->getDefinition($fqn); return $this->getDefinition($fqn);
} }
} }
@ -122,7 +171,9 @@ class Index implements ReadableIndex, \Serializable
*/ */
public function setDefinition(string $fqn, Definition $definition) public function setDefinition(string $fqn, Definition $definition)
{ {
$this->definitions[$fqn] = $definition; $parts = $this->splitFqn($fqn);
$this->indexDefinition(0, $parts, $this->definitions, $definition);
$this->emit('definition-added'); $this->emit('definition-added');
} }
@ -135,19 +186,23 @@ class Index implements ReadableIndex, \Serializable
*/ */
public function removeDefinition(string $fqn) public function removeDefinition(string $fqn)
{ {
unset($this->definitions[$fqn]); $parts = $this->splitFqn($fqn);
$this->removeIndexedDefinition(0, $parts, $this->definitions, $this->definitions);
unset($this->references[$fqn]); unset($this->references[$fqn]);
} }
/** /**
* Returns all URIs in this index that reference a symbol * Returns a Generator providing all URIs in this index that reference a symbol
* *
* @param string $fqn The fully qualified name of the symbol * @param string $fqn The fully qualified name of the symbol
* @return string[] * @return \Generator yields string
*/ */
public function getReferenceUris(string $fqn): array public function getReferenceUris(string $fqn): \Generator
{ {
return $this->references[$fqn] ?? []; foreach ($this->references[$fqn] ?? [] as $uri) {
yield $uri;
}
} }
/** /**
@ -204,6 +259,15 @@ class Index implements ReadableIndex, \Serializable
public function unserialize($serialized) public function unserialize($serialized)
{ {
$data = unserialize($serialized); $data = unserialize($serialized);
if (isset($data['definitions'])) {
foreach ($data['definitions'] as $fqn => $definition) {
$this->setDefinition($fqn, $definition);
}
unset($data['definitions']);
}
foreach ($data as $prop => $val) { foreach ($data as $prop => $val) {
$this->$prop = $val; $this->$prop = $val;
} }
@ -216,10 +280,164 @@ class Index implements ReadableIndex, \Serializable
public function serialize() public function serialize()
{ {
return serialize([ return serialize([
'definitions' => $this->definitions, 'definitions' => iterator_to_array($this->getDefinitions()),
'references' => $this->references, 'references' => $this->references,
'complete' => $this->complete, 'complete' => $this->complete,
'staticComplete' => $this->staticComplete 'staticComplete' => $this->staticComplete
]); ]);
} }
/**
* Returns a Generator that yields all the Definitions in the given $storage recursively.
* The generator yields key => value pairs, e.g.
* `'Psr\Log\LoggerInterface->log()' => $definition`
*
* @param array &$storage
* @param string $prefix (optional)
* @return \Generator
*/
private function yieldDefinitionsRecursively(array &$storage, string $prefix = ''): \Generator
{
foreach ($storage as $key => $value) {
if (!is_array($value)) {
yield $prefix.$key => $value;
} else {
yield from $this->yieldDefinitionsRecursively($value, $prefix.$key);
}
}
}
/**
* 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 string[]
*/
private function splitFqn(string $fqn): array
{
// split fqn at backslashes
$parts = explode('\\', $fqn);
// write back the backslash prefix to the first part if it was present
if ('' === $parts[0] && count($parts) > 1) {
$parts = array_slice($parts, 1);
$parts[0] = '\\' . $parts[0];
}
// write back the backslashes prefixes for the other parts
for ($i = 1; $i < count($parts); $i++) {
$parts[$i] = '\\' . $parts[$i];
}
// split the last part in 2 parts at the operator
$hasOperator = false;
$lastPart = end($parts);
foreach (['::', '->'] as $operator) {
$endParts = explode($operator, $lastPart);
if (count($endParts) > 1) {
$hasOperator = true;
// replace the last part by its pieces
array_pop($parts);
$parts[] = $endParts[0];
$parts[] = $operator . $endParts[1];
break;
}
}
// The end($parts) === '' holds for the root namespace.
if (!$hasOperator && end($parts) !== '') {
// add an empty part to store the non-member definition to avoid
// definition collisions in the index array, eg
// 'Psr\Log\LoggerInterface' will be stored at
// ['Psr']['\Log']['\LoggerInterface'][''] to be able to also store
// member definitions, ie 'Psr\Log\LoggerInterface->log()' will be
// stored at ['Psr']['\Log']['\LoggerInterface']['->log()']
$parts[] = '';
}
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 string[] $path The splitted FQN
* @param array|Definition &$storage The current level to look for $path.
* @return array|Definition|null
*/
private function getIndexValue(array $path, &$storage)
{
// Empty path returns the object itself.
if (empty($path)) {
return $storage;
}
$part = array_shift($path);
if (!isset($storage[$part])) {
return null;
}
return $this->getIndexValue($path, $storage[$part]);
}
/**
* Recursive function that stores 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 string[] $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);
}
/**
* Recursive function that removes 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 string[] $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, array &$rootStorage)
{
$part = $parts[$level];
if ($level + 1 === count($parts)) {
if (isset($storage[$part])) {
unset($storage[$part]);
if (0 === count($storage)) {
// parse again the definition tree to remove the parent
// when it has no more children
$this->removeIndexedDefinition(0, array_slice($parts, 0, $level), $rootStorage, $rootStorage);
}
}
} else {
$this->removeIndexedDefinition($level + 1, $parts, $storage[$part], $rootStorage);
}
}
} }

View File

@ -30,12 +30,20 @@ interface ReadableIndex extends EmitterInterface
public function isStaticComplete(): bool; public function isStaticComplete(): bool;
/** /**
* Returns an associative array [string => Definition] that maps fully qualified symbol names * Returns a Generator providing an associative array [string => Definition]
* to Definitions * that maps fully qualified symbol names to Definitions (global or not)
* *
* @return Definitions[] * @return \Generator yields Definition
*/ */
public function getDefinitions(): array; public function getDefinitions(): \Generator;
/**
* Returns a Generator that yields all the direct child Definitions of a given FQN
*
* @param string $fqn
* @return \Generator yields Definition
*/
public function getChildDefinitionsForFqn(string $fqn): \Generator;
/** /**
* Returns the Definition object by a specific FQN * Returns the Definition object by a specific FQN
@ -47,10 +55,10 @@ interface ReadableIndex extends EmitterInterface
public function getDefinition(string $fqn, bool $globalFallback = false); public function getDefinition(string $fqn, bool $globalFallback = false);
/** /**
* Returns all URIs in this index that reference a symbol * Returns a Generator that yields all URIs in this index that reference a symbol
* *
* @param string $fqn The fully qualified name of the symbol * @param string $fqn The fully qualified name of the symbol
* @return string[] * @return \Generator yields string
*/ */
public function getReferenceUris(string $fqn): array; public function getReferenceUris(string $fqn): \Generator;
} }

View File

@ -227,10 +227,11 @@ class TextDocument
return []; return [];
} }
} }
$refDocuments = yield Promise\all(array_map( $refDocumentPromises = [];
[$this->documentLoader, 'getOrLoad'], foreach ($this->index->getReferenceUris($fqn) as $uri) {
$this->index->getReferenceUris($fqn) $refDocumentPromises[] = $this->documentLoader->getOrLoad($uri);
)); }
$refDocuments = yield Promise\all($refDocumentPromises);
foreach ($refDocuments as $document) { foreach ($refDocuments as $document) {
$refs = $document->getReferenceNodesByFqn($fqn); $refs = $document->getReferenceNodesByFqn($fqn);
if ($refs !== null) { if ($refs !== null) {

View File

@ -35,7 +35,9 @@ class DefinitionCollectorTest extends TestCase
'TestNamespace\\ChildClass', 'TestNamespace\\ChildClass',
'TestNamespace\\Example', 'TestNamespace\\Example',
'TestNamespace\\Example->__construct()', 'TestNamespace\\Example->__construct()',
'TestNamespace\\Example->__destruct()' 'TestNamespace\\Example->__destruct()',
'TestNamespace\\InnerNamespace',
'TestNamespace\\InnerNamespace\\InnerClass',
], array_keys($defNodes)); ], array_keys($defNodes));
$this->assertInstanceOf(Node\ConstElement::class, $defNodes['TestNamespace\\TEST_CONST']); $this->assertInstanceOf(Node\ConstElement::class, $defNodes['TestNamespace\\TEST_CONST']);
@ -53,6 +55,7 @@ class DefinitionCollectorTest extends TestCase
$this->assertInstanceOf(Node\Statement\ClassDeclaration::class, $defNodes['TestNamespace\\Example']); $this->assertInstanceOf(Node\Statement\ClassDeclaration::class, $defNodes['TestNamespace\\Example']);
$this->assertInstanceOf(Node\MethodDeclaration::class, $defNodes['TestNamespace\\Example->__construct()']); $this->assertInstanceOf(Node\MethodDeclaration::class, $defNodes['TestNamespace\\Example->__construct()']);
$this->assertInstanceOf(Node\MethodDeclaration::class, $defNodes['TestNamespace\\Example->__destruct()']); $this->assertInstanceOf(Node\MethodDeclaration::class, $defNodes['TestNamespace\\Example->__destruct()']);
$this->assertInstanceOf(Node\Statement\ClassDeclaration::class, $defNodes['TestNamespace\\InnerNamespace\\InnerClass']);
} }
public function testDoesNotCollectReferences() public function testDoesNotCollectReferences()

View File

@ -107,7 +107,9 @@ abstract class ServerTestCase extends TestCase
'TestNamespace\\whatever()' => new Location($referencesUri, new Range(new Position(21, 0), new Position(23, 1))), 'TestNamespace\\whatever()' => new Location($referencesUri, new Range(new Position(21, 0), new Position(23, 1))),
'TestNamespace\\Example' => new Location($symbolsUri, new Range(new Position(101, 0), new Position(104, 1))), 'TestNamespace\\Example' => new Location($symbolsUri, new Range(new Position(101, 0), new Position(104, 1))),
'TestNamespace\\Example::__construct' => new Location($symbolsUri, new Range(new Position(102, 4), new Position(102, 36))), 'TestNamespace\\Example::__construct' => new Location($symbolsUri, new Range(new Position(102, 4), new Position(102, 36))),
'TestNamespace\\Example::__destruct' => new Location($symbolsUri, new Range(new Position(103, 4), new Position(103, 35))) 'TestNamespace\\Example::__destruct' => new Location($symbolsUri, new Range(new Position(103, 4), new Position(103, 35))),
'TestNamespace\\InnerNamespace' => new Location($symbolsUri, new Range(new Position(106, 0), new Position(106, 39))),
'TestNamespace\\InnerNamespace\\InnerClass' => new Location($symbolsUri, new Range(new Position(108, 0), new Position(109, 1))),
]; ];
$this->referenceLocations = [ $this->referenceLocations = [

View File

@ -47,6 +47,9 @@ class CompletionTest extends TestCase
$this->textDocument = new Server\TextDocument($this->loader, $definitionResolver, $client, $projectIndex); $this->textDocument = new Server\TextDocument($this->loader, $definitionResolver, $client, $projectIndex);
} }
/**
* Tests completion at `$obj->t|`
*/
public function testPropertyAndMethodWithPrefix() public function testPropertyAndMethodWithPrefix()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/property_with_prefix.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/property_with_prefix.php');
@ -71,6 +74,9 @@ class CompletionTest extends TestCase
], true), $items); ], true), $items);
} }
/**
* Tests completion at `public function a() { tes| }`
*/
public function testGlobalFunctionInsideNamespaceAndClass() public function testGlobalFunctionInsideNamespaceAndClass()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/inside_namespace_and_method.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/inside_namespace_and_method.php');
@ -92,6 +98,9 @@ class CompletionTest extends TestCase
], true), $items); ], true), $items);
} }
/**
* Tests completion at `$obj->|`
*/
public function testPropertyAndMethodWithoutPrefix() public function testPropertyAndMethodWithoutPrefix()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/property.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/property.php');
@ -116,6 +125,9 @@ class CompletionTest extends TestCase
], true), $items); ], true), $items);
} }
/**
* Tests completion at `$|` when variables are defined
*/
public function testVariable() public function testVariable()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/variable.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/variable.php');
@ -148,6 +160,9 @@ class CompletionTest extends TestCase
], true), $items); ], true), $items);
} }
/**
* Tests completion at `$p|` when variables are defined
*/
public function testVariableWithPrefix() public function testVariableWithPrefix()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/variable_with_prefix.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/variable_with_prefix.php');
@ -170,6 +185,9 @@ class CompletionTest extends TestCase
], true), $items); ], true), $items);
} }
/**
* Tests completion at `new|` when in a namespace and have used variables.
*/
public function testNewInNamespace() public function testNewInNamespace()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/used_new.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/used_new.php');
@ -218,27 +236,12 @@ class CompletionTest extends TestCase
null, null,
'TestClass' 'TestClass'
), ),
new CompletionItem(
'ChildClass',
CompletionItemKind::CLASS_,
'TestNamespace',
null,
null,
null,
'\TestNamespace\ChildClass'
),
new CompletionItem(
'Example',
CompletionItemKind::CLASS_,
'TestNamespace',
null,
null,
null,
'\TestNamespace\Example'
)
], true), $items); ], true), $items);
} }
/**
* Tests completion at `TestC|` with `use TestNamespace\TestClass`
*/
public function testUsedClass() public function testUsedClass()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/used_class.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/used_class.php');
@ -257,11 +260,74 @@ class CompletionTest extends TestCase
'laboris commodo ad commodo velit mollit qui non officia id. Nulla duis veniam' . "\n" . 'laboris commodo ad commodo velit mollit qui non officia id. Nulla duis veniam' . "\n" .
'veniam officia deserunt et non dolore mollit ea quis eiusmod sit non. Occaecat' . "\n" . 'veniam officia deserunt et non dolore mollit ea quis eiusmod sit non. Occaecat' . "\n" .
'consequat sunt culpa exercitation pariatur id reprehenderit nisi incididunt Lorem' . "\n" . 'consequat sunt culpa exercitation pariatur id reprehenderit nisi incididunt Lorem' . "\n" .
'sint. Officia culpa pariatur laborum nostrud cupidatat consequat mollit.' 'sint. Officia culpa pariatur laborum nostrud cupidatat consequat mollit.',
null,
null,
'TestClass'
) )
], true), $items); ], true), $items);
$this->assertCompletionsListDoesNotContainLabel('OtherClass', $items);
$this->assertCompletionsListDoesNotContainLabel('TestInterface', $items);
} }
/**
* Tests completion at `AliasNamespace\I|` with `use TestNamespace\InnerNamespace as AliasNamespace`
*/
public function testUsedNamespaceWithPrefix()
{
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/used_namespace.php');
$this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri),
new Position(8, 16)
)->wait();
$this->assertEquals(
new CompletionList([
new CompletionItem(
'InnerClass',
CompletionItemKind::CLASS_,
'TestNamespace\\InnerNamespace',
null,
null,
null,
'AliasNamespace\\InnerClass'
)
], true),
$items
);
}
/**
* Tests completion at `AliasNamespace\|` with `use TestNamespace\InnerNamespace as AliasNamespace`
*/
public function testUsedNamespaceWithoutPrefix()
{
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/used_namespace.php');
$this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri),
new Position(9, 15)
)->wait();
$this->assertEquals(
new CompletionList([
new CompletionItem(
'InnerClass',
CompletionItemKind::CLASS_,
'TestNamespace\InnerNamespace',
null,
null,
null,
'AliasNamespace\InnerClass'
),
], true),
$items
);
}
/**
* Tests completion at `TestClass::$st|`
*/
public function testStaticPropertyWithPrefix() public function testStaticPropertyWithPrefix()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/static_property_with_prefix.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/static_property_with_prefix.php');
@ -283,6 +349,9 @@ class CompletionTest extends TestCase
], true), $items); ], true), $items);
} }
/**
* Tests completion at `TestClass::|`
*/
public function testStaticWithoutPrefix() public function testStaticWithoutPrefix()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/static.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/static.php');
@ -316,6 +385,9 @@ class CompletionTest extends TestCase
], true), $items); ], true), $items);
} }
/**
* Tests completion at `TestClass::st|`
*/
public function testStaticMethodWithPrefix() public function testStaticMethodWithPrefix()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/static_method_with_prefix.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/static_method_with_prefix.php');
@ -325,21 +397,6 @@ class CompletionTest extends TestCase
new Position(2, 13) new Position(2, 13)
)->wait(); )->wait();
$this->assertCompletionsListSubset(new CompletionList([ $this->assertCompletionsListSubset(new CompletionList([
new CompletionItem(
'TEST_CLASS_CONST',
CompletionItemKind::VARIABLE,
'int',
'Anim labore veniam consectetur laboris minim quis aute aute esse nulla ad.'
),
new CompletionItem(
'staticTestProperty',
CompletionItemKind::PROPERTY,
'\TestClass[]',
'Lorem excepteur officia sit anim velit veniam enim.',
null,
null,
'$staticTestProperty'
),
new CompletionItem( new CompletionItem(
'staticTestMethod', 'staticTestMethod',
CompletionItemKind::METHOD, CompletionItemKind::METHOD,
@ -349,6 +406,9 @@ class CompletionTest extends TestCase
], true), $items); ], true), $items);
} }
/**
* Tests completion at `TestClass::TE` at the root level.
*/
public function testClassConstWithPrefix() public function testClassConstWithPrefix()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/class_const_with_prefix.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/class_const_with_prefix.php');
@ -363,25 +423,13 @@ class CompletionTest extends TestCase
CompletionItemKind::VARIABLE, CompletionItemKind::VARIABLE,
'int', 'int',
'Anim labore veniam consectetur laboris minim quis aute aute esse nulla ad.' 'Anim labore veniam consectetur laboris minim quis aute aute esse nulla ad.'
),
new CompletionItem(
'staticTestProperty',
CompletionItemKind::PROPERTY,
'\TestClass[]',
'Lorem excepteur officia sit anim velit veniam enim.',
null,
null,
'$staticTestProperty'
),
new CompletionItem(
'staticTestMethod',
CompletionItemKind::METHOD,
'mixed',
'Do magna consequat veniam minim proident eiusmod incididunt aute proident.'
) )
], true), $items); ], true), $items);
} }
/**
* Test completion at `\TestC|` in a namespace
*/
public function testFullyQualifiedClass() public function testFullyQualifiedClass()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/fully_qualified_class.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/fully_qualified_class.php');
@ -400,14 +448,18 @@ class CompletionTest extends TestCase
'laboris commodo ad commodo velit mollit qui non officia id. Nulla duis veniam' . "\n" . 'laboris commodo ad commodo velit mollit qui non officia id. Nulla duis veniam' . "\n" .
'veniam officia deserunt et non dolore mollit ea quis eiusmod sit non. Occaecat' . "\n" . 'veniam officia deserunt et non dolore mollit ea quis eiusmod sit non. Occaecat' . "\n" .
'consequat sunt culpa exercitation pariatur id reprehenderit nisi incididunt Lorem' . "\n" . 'consequat sunt culpa exercitation pariatur id reprehenderit nisi incididunt Lorem' . "\n" .
'sint. Officia culpa pariatur laborum nostrud cupidatat consequat mollit.', 'sint. Officia culpa pariatur laborum nostrud cupidatat consequat mollit.'
null,
null,
'TestClass'
) )
], true), $items); ], true), $items);
// Assert that all results are non-namespaced.
foreach ($items->items as $item) {
$this->assertSame($item->detail, null);
}
} }
/**
* Tests completion at `cl|` at root level
*/
public function testKeywords() public function testKeywords()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/keywords.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/keywords.php');
@ -422,6 +474,9 @@ class CompletionTest extends TestCase
], true), $items); ], true), $items);
} }
/**
* Tests completion in an empty file
*/
public function testHtmlWithoutPrefix() public function testHtmlWithoutPrefix()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html.php');
@ -444,6 +499,9 @@ class CompletionTest extends TestCase
], true), $items); ], true), $items);
} }
/**
* Tests completion in `<|` when not within `<?php` tags
*/
public function testHtmlWontBeProposedWithoutCompletionContext() public function testHtmlWontBeProposedWithoutCompletionContext()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html_with_prefix.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html_with_prefix.php');
@ -456,6 +514,9 @@ class CompletionTest extends TestCase
$this->assertEquals(new CompletionList([], true), $items); $this->assertEquals(new CompletionList([], true), $items);
} }
/**
* Tests completion in `<|` when not within `<?php` tags
*/
public function testHtmlWontBeProposedWithPrefixWithCompletionContext() public function testHtmlWontBeProposedWithPrefixWithCompletionContext()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html_with_prefix.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html_with_prefix.php');
@ -480,6 +541,9 @@ class CompletionTest extends TestCase
], true), $items); ], true), $items);
} }
/**
* Tests completion at `<|` when not within `<?php` tags when triggered by trigger character.
*/
public function testHtmlPrefixShouldNotTriggerCompletion() public function testHtmlPrefixShouldNotTriggerCompletion()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html_no_completion.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html_no_completion.php');
@ -492,6 +556,9 @@ class CompletionTest extends TestCase
$this->assertEquals(new CompletionList([], true), $items); $this->assertEquals(new CompletionList([], true), $items);
} }
/**
* Tests completion at `<|` when not within `<?php` tags when triggered by user input.
*/
public function testHtmlPrefixShouldTriggerCompletionIfManuallyInvoked() public function testHtmlPrefixShouldTriggerCompletionIfManuallyInvoked()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html_no_completion.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html_no_completion.php');
@ -515,6 +582,9 @@ class CompletionTest extends TestCase
], true), $items); ], true), $items);
} }
/**
* Tests completion at `SomeNa|` when namespace `SomeNamespace` is defined
*/
public function testNamespace() public function testNamespace()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/namespace.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/namespace.php');
@ -526,17 +596,15 @@ class CompletionTest extends TestCase
$this->assertCompletionsListSubset(new CompletionList([ $this->assertCompletionsListSubset(new CompletionList([
new CompletionItem( new CompletionItem(
'SomeNamespace', 'SomeNamespace',
CompletionItemKind::MODULE, CompletionItemKind::MODULE
null,
null,
null,
null,
'SomeNamespace'
) )
], true), $items); ], true), $items);
} }
public function testBarePhp() /**
* Tests completion at `echo $ab|` at the root level.
*/
public function testBarePhpVariable()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/bare_php.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/bare_php.php');
$this->loader->open($completionUri, file_get_contents($completionUri)); $this->loader->open($completionUri, file_get_contents($completionUri));
@ -776,6 +844,16 @@ class CompletionTest extends TestCase
$this->assertEquals($subsetList->isIncomplete, $list->isIncomplete); $this->assertEquals($subsetList->isIncomplete, $list->isIncomplete);
} }
private function assertCompletionsListDoesNotContainLabel(string $label, CompletionList $list)
{
foreach ($list->items as $item) {
$this->assertNotSame($label, $item->label, "Completion list should not contain $label.");
}
}
/**
* Tests completion for `$this->|`
*/
public function testThisWithoutPrefix() public function testThisWithoutPrefix()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/this.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/this.php');
@ -812,6 +890,9 @@ class CompletionTest extends TestCase
], true), $items); ], true), $items);
} }
/**
* Tests completion at `$this->m|`
*/
public function testThisWithPrefix() public function testThisWithPrefix()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/this_with_prefix.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/this_with_prefix.php');
@ -821,18 +902,6 @@ class CompletionTest extends TestCase
new Position(12, 16) new Position(12, 16)
)->wait(); )->wait();
$this->assertEquals(new CompletionList([ $this->assertEquals(new CompletionList([
new CompletionItem(
'testProperty',
CompletionItemKind::PROPERTY,
'\TestClass', // Type of the property
'Reprehenderit magna velit mollit ipsum do.'
),
new CompletionItem(
'testMethod',
CompletionItemKind::METHOD,
'\TestClass', // Return type of the method
'Non culpa nostrud mollit esse sunt laboris in irure ullamco cupidatat amet.'
),
new CompletionItem( new CompletionItem(
'foo', 'foo',
CompletionItemKind::PROPERTY, CompletionItemKind::PROPERTY,
@ -856,10 +925,25 @@ class CompletionTest extends TestCase
CompletionItemKind::METHOD, CompletionItemKind::METHOD,
'mixed', // Return type of the method 'mixed', // Return type of the method
null null
) ),
new CompletionItem(
'testProperty',
CompletionItemKind::PROPERTY,
'\TestClass', // Type of the property
'Reprehenderit magna velit mollit ipsum do.'
),
new CompletionItem(
'testMethod',
CompletionItemKind::METHOD,
'\TestClass', // Return type of the method
'Non culpa nostrud mollit esse sunt laboris in irure ullamco cupidatat amet.'
),
], true), $items); ], true), $items);
} }
/**
* Tests completion at `$this->foo()->q|`
*/
public function testThisReturnValue() public function testThisReturnValue()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/this_return_value.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/this_return_value.php');
@ -869,11 +953,6 @@ class CompletionTest extends TestCase
new Position(17, 23) new Position(17, 23)
)->wait(); )->wait();
$this->assertEquals(new CompletionList([ $this->assertEquals(new CompletionList([
new CompletionItem(
'foo',
CompletionItemKind::METHOD,
'$this' // Return type of the method
),
new CompletionItem( new CompletionItem(
'bar', 'bar',
CompletionItemKind::METHOD, CompletionItemKind::METHOD,
@ -883,7 +962,12 @@ class CompletionTest extends TestCase
'qux', 'qux',
CompletionItemKind::METHOD, CompletionItemKind::METHOD,
'mixed' // Return type of the method 'mixed' // Return type of the method
) ),
new CompletionItem(
'foo',
CompletionItemKind::METHOD,
'$this' // Return type of the method
),
], true), $items); ], true), $items);
} }
} }

View File

@ -32,7 +32,9 @@ class DocumentSymbolTest extends ServerTestCase
new SymbolInformation('ChildClass', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\ChildClass'), 'TestNamespace'), new SymbolInformation('ChildClass', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\ChildClass'), 'TestNamespace'),
new SymbolInformation('Example', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\Example'), '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('__construct', SymbolKind::CONSTRUCTOR, $this->getDefinitionLocation('TestNamespace\\Example::__construct'), 'TestNamespace\\Example'),
new SymbolInformation('__destruct', SymbolKind::CONSTRUCTOR, $this->getDefinitionLocation('TestNamespace\\Example::__destruct'), 'TestNamespace\\Example') new SymbolInformation('__destruct', SymbolKind::CONSTRUCTOR, $this->getDefinitionLocation('TestNamespace\\Example::__destruct'), 'TestNamespace\\Example'),
new SymbolInformation('TestNamespace\\InnerNamespace', SymbolKind::NAMESPACE, $this->getDefinitionLocation('TestNamespace\\InnerNamespace'), 'TestNamespace'),
new SymbolInformation('InnerClass', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\InnerNamespace\\InnerClass'), 'TestNamespace\\InnerNamespace'),
], $result); ], $result);
// @codingStandardsIgnoreEnd // @codingStandardsIgnoreEnd
} }

View File

@ -30,7 +30,7 @@ class SymbolTest extends ServerTestCase
// @codingStandardsIgnoreStart // @codingStandardsIgnoreStart
$this->assertEquals([ $this->assertEquals([
new SymbolInformation('TestNamespace', SymbolKind::NAMESPACE, new Location($referencesUri, new Range(new Position(2, 0), new Position(2, 24))), ''), new SymbolInformation('TestNamespace', SymbolKind::NAMESPACE, new Location($referencesUri, new Range(new Position(2, 0), new Position(2, 24))), ''),
// Namespaced // Namespaced
new SymbolInformation('TEST_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TestNamespace\\TEST_CONST'), 'TestNamespace'), new SymbolInformation('TEST_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TestNamespace\\TEST_CONST'), 'TestNamespace'),
new SymbolInformation('TestClass', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\TestClass'), 'TestNamespace'), new SymbolInformation('TestClass', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\TestClass'), 'TestNamespace'),
@ -46,6 +46,8 @@ class SymbolTest extends ServerTestCase
new SymbolInformation('Example', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\Example'), '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('__construct', SymbolKind::CONSTRUCTOR, $this->getDefinitionLocation('TestNamespace\\Example::__construct'), 'TestNamespace\\Example'),
new SymbolInformation('__destruct', SymbolKind::CONSTRUCTOR, $this->getDefinitionLocation('TestNamespace\\Example::__destruct'), 'TestNamespace\\Example'), new SymbolInformation('__destruct', SymbolKind::CONSTRUCTOR, $this->getDefinitionLocation('TestNamespace\\Example::__destruct'), 'TestNamespace\\Example'),
new SymbolInformation('TestNamespace\\InnerNamespace', SymbolKind::NAMESPACE, $this->getDefinitionLocation('TestNamespace\\InnerNamespace'), 'TestNamespace'),
new SymbolInformation('InnerClass', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\InnerNamespace\\InnerClass'), 'TestNamespace\\InnerNamespace'),
new SymbolInformation('whatever', SymbolKind::FUNCTION, $this->getDefinitionLocation('TestNamespace\\whatever()'), 'TestNamespace'), new SymbolInformation('whatever', SymbolKind::FUNCTION, $this->getDefinitionLocation('TestNamespace\\whatever()'), 'TestNamespace'),
// Global // Global
new SymbolInformation('TEST_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TEST_CONST'), ''), new SymbolInformation('TEST_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TEST_CONST'), ''),