1
0
Fork 0

Merge branch 'master' into fix/eol

pull/682/head
Gabriel Noé González 2018-11-11 15:31:05 +01:00 committed by GitHub
commit 680d2dbdbe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 975 additions and 247 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

@ -0,0 +1,20 @@
<?php
namespace RecursiveTest;
class A extends A {}
class B extends C {}
class C extends B {}
class D extends E {}
class E extends F {}
class F extends D {}
$a = new A;
$a->undef_prop = 1;
$b = new B;
$b->undef_prop = 1;
$d = new D;
$d->undef_prop = 1;

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 {
}

4
package-lock.json generated
View File

@ -1,8 +1,6 @@
{ {
"name": "php-language-server",
"version": "0.0.0-development",
"lockfileVersion": 1,
"requires": true, "requires": true,
"lockfileVersion": 1,
"dependencies": { "dependencies": {
"@gimenete/type-writer": { "@gimenete/type-writer": {
"version": "0.1.3", "version": "0.1.3",

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);
@ -236,7 +247,6 @@ class CompletionProvider
$fqns = FqnUtilities\getFqnsFromType( $fqns = FqnUtilities\getFqnsFromType(
$this->definitionResolver->resolveExpressionNodeToType($node->dereferencableExpression) $this->definitionResolver->resolveExpressionNodeToType($node->dereferencableExpression)
); );
$isInMethodDeclaration = null !== $node->getFirstAncestor(\Microsoft\PhpParser\Node\MethodDeclaration::class); $isInMethodDeclaration = null !== $node->getFirstAncestor(\Microsoft\PhpParser\Node\MethodDeclaration::class);
// Add the object access operator to only get members of all parents // Add the object access operator to only get members of all parents
$prefixes = []; $prefixes = [];
@ -272,19 +282,14 @@ class CompletionProvider
$fqns = FqnUtilities\getFqnsFromType( $fqns = FqnUtilities\getFqnsFromType(
$classType = $this->definitionResolver->resolveExpressionNodeToType($scoped->scopeResolutionQualifier) $classType = $this->definitionResolver->resolveExpressionNodeToType($scoped->scopeResolutionQualifier)
); );
$isInMethodDeclaration = null !== $node->getFirstAncestor(\Microsoft\PhpParser\Node\MethodDeclaration::class); // The FQNs of the symbol and its parents (eg the implemented interfaces)
foreach ($this->expandParentFqns($fqns) as $parentFqn) {
// Append :: operator to only get static members of all parents // Append :: operator to only get static members of all parents
$prefixes = []; $prefix = strtolower($parentFqn . '::');
foreach ($this->expandParentFqns($fqns) as $prefix) { $prefixLen = strlen($prefix);
$prefixes[] = $prefix . '::'; // Collect fqn definitions
} foreach ($this->index->getChildDefinitionsForFqn($parentFqn) as $fqn => $def) {
if (substr(strtolower($fqn), 0, $prefixLen) === $prefix && $def->isMember) {
// Collect all definitions that match any of the prefixes
foreach ($this->index->getDefinitions() as $fqn => $def) {
foreach ($prefixes as $prefix) {
if (substr(strtolower($fqn), 0, strlen($prefix)) === strtolower($prefix) &&
$def->isMember &&
$def->isVisible($prefix, $prefixes[0], $isInMethodDeclaration)) {
$list->items[] = CompletionItemFactory::fromDefinition($def); $list->items[] = CompletionItemFactory::fromDefinition($def);
} }
} }
@ -302,115 +307,279 @@ 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());
$prefixLen = strlen($prefix);
/** 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();
/** @var string The name of the namespace */
$namespacedPrefix = null;
if ($namespaceNode) {
$namespacedPrefix = (string)PhpParser\ResolvedName::buildName($namespaceNode->name->nameParts, $node->getFileContents()) . '\\' . $prefix;
$namespacedPrefixLen = strlen($namespacedPrefix);
}
// Get the namespace use statements
// TODO: use function statements, use const statements
/** @var string[] $aliases A map from local alias to fully qualified name */
list($aliases,,) = $node->getImportTablesForCurrentScope();
foreach ($aliases as $alias => $name) {
$aliases[$alias] = (string)$name;
}
// If there is a prefix that does not start with a slash, suggest `use`d symbols
if ($prefix && !$isFullyQualified) {
foreach ($aliases as $alias => $fqn) {
// Suggest symbols that have been `use`d and match the prefix
if (substr($alias, 0, $prefixLen) === $prefix && ($def = $this->index->getDefinition($fqn))) {
$list->items[] = CompletionItemFactory::fromDefinition($def);
}
}
}
// Suggest global symbols that either
// - start with the current namespace + prefix, if the Name node is not fully qualified
// - start with just the prefix, if the Name node is fully qualified
foreach ($this->index->getDefinitions() as $fqn => $def) {
$fqnStartsWithPrefix = substr($fqn, 0, $prefixLen) === $prefix;
if (
// Exclude methods, properties etc.
!$def->isMember
&& (
!$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
|| (
$namespaceNode
&& !$isFullyQualified
&& 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 { } else {
// Insert the FQN without leading backlash $prefix = $nameNode->getText($node->getFileContents());
$item->insertText = $fqn;
} }
// Don't insert the parenthesis for functions
// TODO return a snippet and put the cursor inside $namespaceNode = $node->getNamespaceDefinition();
if (substr($item->insertText, -2) === '()') { /** @var string The current namespace without a leading backslash. */
$currentNamespace = $namespaceNode === null ? '' : $namespaceNode->name->getText();
/** @var bool Whether the prefix is qualified (contains at least one backslash) */
$isFullyQualified = false;
/** @var bool Whether the prefix is qualified (contains at least one backslash) */
$isQualified = false;
if ($nameNode instanceof Node\QualifiedName) {
$isFullyQualified = $nameNode->isFullyQualifiedName();
$isQualified = $nameNode->isQualifiedName();
}
/** @var bool Whether we are in a new expression */
$isCreation = isset($creation);
/** @var array Import (use) tables */
$importTables = $node->getImportTablesForCurrentScope();
if ($isFullyQualified) {
// \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);
}
$list->items = array_values(iterator_to_array($items));
foreach ($list->items as $item) {
// Remove ()
if (is_string($item->insertText) && substr($item->insertText, strlen($item->insertText) - 2) === '()') {
$item->insertText = substr($item->insertText, 0, -2); $item->insertText = substr($item->insertText, 0, -2);
} }
$list->items[] = $item; }
}
return $list;
}
private function getPartiallyQualifiedCompletions(
string $prefix,
string $currentNamespace,
array $importTables,
bool $requireCanBeInstantiated
): \Generator {
// If the first part of the partially qualified name matches a namespace alias,
// only definitions below that alias can be completed.
list($namespaceAliases,,) = $importTables;
$prefixFirstPart = nameGetFirstPart($prefix);
$foundAlias = $foundAliasFqn = null;
foreach ($namespaceAliases as $alias => $aliasFqn) {
if (strcasecmp($prefixFirstPart, $alias) === 0) {
$foundAlias = $alias;
$foundAliasFqn = (string)$aliasFqn;
break;
} }
} }
// If not a class instantiation, also suggest keywords if ($foundAlias !== null) {
if (!isset($creation)) { 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) { foreach (self::KEYWORDS as $keyword) {
if (substr($keyword, 0, $prefixLen) === $prefix) { if (nameStartsWith($keyword, $prefix)) {
$item = new CompletionItem($keyword, CompletionItemKind::KEYWORD); $item = new CompletionItem($keyword, CompletionItemKind::KEYWORD);
$item->insertText = $keyword; $item->insertText = $keyword;
$list->items[] = $item; yield $keyword => $item;
} }
} }
} }
}
return $list;
}
/** /**
* Yields FQNs from an array along with the FQNs of all parent classes * Yields FQNs from an array along with the FQNs of all parent classes
@ -478,8 +647,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

@ -438,6 +438,7 @@ class DefinitionResolver
// Find the right class that implements the member // Find the right class that implements the member
$implementorFqns = [$classFqn]; $implementorFqns = [$classFqn];
$visitedFqns = [];
while ($implementorFqn = array_shift($implementorFqns)) { while ($implementorFqn = array_shift($implementorFqns)) {
// If the member FQN exists, return it // If the member FQN exists, return it
@ -450,13 +451,18 @@ class DefinitionResolver
if ($implementorDef === null) { if ($implementorDef === null) {
break; break;
} }
// Note the FQN as visited
$visitedFqns[] = $implementorFqn;
// Repeat for parent class // Repeat for parent class
if ($implementorDef->extends) { if ($implementorDef->extends) {
foreach ($implementorDef->extends as $extends) { foreach ($implementorDef->extends as $extends) {
// Don't add the parent FQN if it's already been visited
if (!\in_array($extends, $visitedFqns)) {
$implementorFqns[] = $extends; $implementorFqns[] = $extends;
} }
} }
} }
}
return $classFqn . $memberSuffix; return $classFqn . $memberSuffix;
} }
@ -1233,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;
} }
} }
return $defs;
/**
* 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);
}
} }
/** /**
@ -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

@ -16,7 +16,7 @@ class Indexer
/** /**
* @var int The prefix for every cache item * @var int The prefix for every cache item
*/ */
const CACHE_VERSION = 2; const CACHE_VERSION = 3;
/** /**
* @var FilesFinder * @var FilesFinder

View File

@ -165,8 +165,11 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
* @param int|null $processId The process Id of the parent process that started the server. Is null if the process has not been started by another process. If the parent process is not alive then the server should exit (see exit notification) its process. * @param int|null $processId The process Id of the parent process that started the server. Is null if the process has not been started by another process. If the parent process is not alive then the server should exit (see exit notification) its process.
* @return Promise <InitializeResult> * @return Promise <InitializeResult>
*/ */
public function initialize(ClientCapabilities $capabilities, string $rootPath = null, int $processId = null): Promise public function initialize(ClientCapabilities $capabilities, string $rootPath = null, int $processId = null, string $rootUri = null): Promise
{ {
if ($rootPath === null) {
$rootPath = uriToPath($rootUri);
}
return coroutine(function () use ($capabilities, $rootPath, $processId) { return coroutine(function () use ($capabilities, $rootPath, $processId) {
if ($capabilities->xfilesProvider) { if ($capabilities->xfilesProvider) {

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

@ -38,7 +38,7 @@ function pathToUri(string $filepath): string
function uriToPath(string $uri) function uriToPath(string $uri)
{ {
$fragments = parse_url($uri); $fragments = parse_url($uri);
if ($fragments === null || !isset($fragments['scheme']) || $fragments['scheme'] !== 'file') { if ($fragments === false || !isset($fragments['scheme']) || $fragments['scheme'] !== 'file') {
throw new InvalidArgumentException("Not a valid file URI: $uri"); throw new InvalidArgumentException("Not a valid file URI: $uri");
} }
$filepath = urldecode($fragments['path']); $filepath = urldecode($fragments['path']);

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

@ -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'), ''),