diff --git a/README.md b/README.md index e608a77..3e753ac 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,6 @@ [![Linux Build Status](https://travis-ci.org/felixfbecker/php-language-server.svg?branch=master)](https://travis-ci.org/felixfbecker/php-language-server) [![Windows Build status](https://ci.appveyor.com/api/projects/status/2sp5ll052wdjqmdm/branch/master?svg=true)](https://ci.appveyor.com/project/felixfbecker/php-language-server/branch/master) [![Coverage](https://codecov.io/gh/felixfbecker/php-language-server/branch/master/graph/badge.svg)](https://codecov.io/gh/felixfbecker/php-language-server) -[![Dependency Status](https://gemnasium.com/badges/github.com/felixfbecker/php-language-server.svg)](https://gemnasium.com/github.com/felixfbecker/php-language-server) [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) [![Minimum PHP Version](https://img.shields.io/badge/php-%3E%3D%207.0-8892BF.svg)](https://php.net/) [![License](https://img.shields.io/packagist/l/felixfbecker/language-server.svg)](https://github.com/felixfbecker/php-language-server/blob/master/LICENSE.txt) diff --git a/benchmarks/completion.php b/benchmarks/completion.php new file mode 100644 index 0000000..2e736d7 --- /dev/null +++ b/benchmarks/completion.php @@ -0,0 +1,89 @@ +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; diff --git a/Performance.php b/benchmarks/parsing.php similarity index 77% rename from Performance.php rename to benchmarks/parsing.php index 4d76d38..516de40 100644 --- a/Performance.php +++ b/benchmarks/parsing.php @@ -1,23 +1,31 @@ setLogger($logger); +$xdebugHandler->check(); +unset($xdebugHandler); + $totalSize = 0; $frameworks = ["drupal", "wordpress", "php-language-server", "tolerant-php-parser", "math-php", "symfony", "codeigniter", "cakephp"]; foreach($frameworks as $framework) { - $iterator = new RecursiveDirectoryIterator(__DIR__ . "/validation/frameworks/$framework"); + $iterator = new RecursiveDirectoryIterator(__DIR__ . "/../validation/frameworks/$framework"); $testProviderArray = array(); foreach (new RecursiveIteratorIterator($iterator) as $file) { @@ -37,8 +45,8 @@ foreach($frameworks as $framework) { if (filesize($testCaseFile) > 10000) { continue; } - if ($idx % 1000 === 0) { - echo "$idx\n"; + if ($idx % 500 === 0) { + echo $idx . '/' . count($testProviderArray) . PHP_EOL; } $fileContents = file_get_contents($testCaseFile); diff --git a/composer.json b/composer.json index 085f189..ffa5971 100644 --- a/composer.json +++ b/composer.json @@ -24,6 +24,7 @@ "php": "^7.0", "composer/xdebug-handler": "^1.0", "felixfbecker/advanced-json-rpc": "^3.0.0", + "felixfbecker/language-server-protocol": "^1.0.1", "jetbrains/phpstorm-stubs": "dev-master", "microsoft/tolerant-php-parser": "0.0.*", "netresearch/jsonmapper": "^1.0", diff --git a/fixtures/completion/used_namespace.php b/fixtures/completion/used_namespace.php new file mode 100644 index 0000000..40a7e50 --- /dev/null +++ b/fixtures/completion/used_namespace.php @@ -0,0 +1,10 @@ +undef_prop = 1; + +$b = new B; +$b->undef_prop = 1; + +$d = new D; +$d->undef_prop = 1; diff --git a/fixtures/symbols.php b/fixtures/symbols.php index 4e9b0d4..7f5c2fb 100644 --- a/fixtures/symbols.php +++ b/fixtures/symbols.php @@ -103,3 +103,8 @@ class Example { public function __construct() {} public function __destruct() {} } + +namespace TestNamespace\InnerNamespace; + +class InnerClass { +} diff --git a/package-lock.json b/package-lock.json index fd26f34..fae7b60 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,8 +1,6 @@ { - "name": "php-language-server", - "version": "0.0.0-development", - "lockfileVersion": 1, "requires": true, + "lockfileVersion": 1, "dependencies": { "@gimenete/type-writer": { "version": "0.1.3", diff --git a/src/Client/TextDocument.php b/src/Client/TextDocument.php index 1e65995..03211fa 100644 --- a/src/Client/TextDocument.php +++ b/src/Client/TextDocument.php @@ -4,7 +4,7 @@ declare(strict_types = 1); namespace LanguageServer\Client; use LanguageServer\ClientHandler; -use LanguageServer\Protocol\{Diagnostic, TextDocumentItem, TextDocumentIdentifier}; +use LanguageServerProtocol\{Diagnostic, TextDocumentItem, TextDocumentIdentifier}; use Sabre\Event\Promise; use JsonMapper; diff --git a/src/Client/Workspace.php b/src/Client/Workspace.php index 901e386..8c31f1f 100644 --- a/src/Client/Workspace.php +++ b/src/Client/Workspace.php @@ -4,7 +4,7 @@ declare(strict_types = 1); namespace LanguageServer\Client; use LanguageServer\ClientHandler; -use LanguageServer\Protocol\TextDocumentIdentifier; +use LanguageServerProtocol\TextDocumentIdentifier; use Sabre\Event\Promise; use JsonMapper; diff --git a/src/ClientHandler.php b/src/ClientHandler.php index 9fe921b..c98cbc6 100644 --- a/src/ClientHandler.php +++ b/src/ClientHandler.php @@ -41,12 +41,12 @@ class ClientHandler { $id = $this->idGenerator->generate(); return $this->protocolWriter->write( - new Protocol\Message( + new Message( new AdvancedJsonRpc\Request($id, $method, (object)$params) ) )->then(function () use ($id) { $promise = new Promise; - $listener = function (Protocol\Message $msg) use ($id, $promise, &$listener) { + $listener = function (Message $msg) use ($id, $promise, &$listener) { if (AdvancedJsonRpc\Response::isResponse($msg->body) && $msg->body->id === $id) { // Received a response $this->protocolReader->removeListener('message', $listener); @@ -72,7 +72,7 @@ class ClientHandler public function notify(string $method, $params): Promise { return $this->protocolWriter->write( - new Protocol\Message( + new Message( new AdvancedJsonRpc\Notification($method, (object)$params) ) ); diff --git a/src/CompletionProvider.php b/src/CompletionProvider.php index d8fefa8..e7dfcf1 100644 --- a/src/CompletionProvider.php +++ b/src/CompletionProvider.php @@ -4,7 +4,8 @@ declare(strict_types = 1); namespace LanguageServer; use LanguageServer\Index\ReadableIndex; -use LanguageServer\Protocol\{ +use LanguageServer\Factory\CompletionItemFactory; +use LanguageServerProtocol\{ TextEdit, Range, Position, @@ -16,7 +17,15 @@ use LanguageServer\Protocol\{ }; use Microsoft\PhpParser; use Microsoft\PhpParser\Node; +use Microsoft\PhpParser\ResolvedName; use Generator; +use function LanguageServer\FqnUtilities\{ + nameConcat, + nameGetFirstPart, + nameGetParent, + nameStartsWith, + nameWithoutFirstPart +}; class CompletionProvider { @@ -143,8 +152,11 @@ class CompletionProvider * @param CompletionContext $context The completion context * @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. $node = $doc->getNodeAtPosition($pos); @@ -236,17 +248,15 @@ class CompletionProvider $this->definitionResolver->resolveExpressionNodeToType($node->dereferencableExpression) ); - // Add the object access operator to only get members of all parents - $prefixes = []; - foreach ($this->expandParentFqns($fqns) as $prefix) { - $prefixes[] = $prefix . '->'; - } - - // Collect all definitions that match any of the prefixes - foreach ($this->index->getDefinitions() as $fqn => $def) { - foreach ($prefixes as $prefix) { - if (substr($fqn, 0, strlen($prefix)) === $prefix && $def->isMember) { - $list->items[] = CompletionItem::fromDefinition($def); + // The FQNs of the symbol and its parents (eg the implemented interfaces) + foreach ($this->expandParentFqns($fqns) as $parentFqn) { + // Add the object access operator to only get members of all parents + $prefix = $parentFqn . '->'; + $prefixLen = strlen($prefix); + // Collect fqn definitions + foreach ($this->index->getChildDefinitionsForFqn($parentFqn) as $fqn => $def) { + if (substr($fqn, 0, $prefixLen) === $prefix && $def->isMember) { + $list->items[] = CompletionItemFactory::fromDefinition($def); } } } @@ -269,17 +279,15 @@ class CompletionProvider $classType = $this->definitionResolver->resolveExpressionNodeToType($scoped->scopeResolutionQualifier) ); - // Append :: operator to only get static members of all parents - $prefixes = []; - foreach ($this->expandParentFqns($fqns) as $prefix) { - $prefixes[] = $prefix . '::'; - } - - // 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) { - $list->items[] = CompletionItem::fromDefinition($def); + // 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 + $prefix = strtolower($parentFqn . '::'); + $prefixLen = strlen($prefix); + // Collect fqn definitions + foreach ($this->index->getChildDefinitionsForFqn($parentFqn) as $fqn => $def) { + if (substr(strtolower($fqn), 0, $prefixLen) === $prefix && $def->isMember) { + $list->items[] = CompletionItemFactory::fromDefinition($def); } } } @@ -296,114 +304,278 @@ class CompletionProvider // my_func| // MY_CONS| // MyCla| + // \MyCla| // The name Node under the cursor $nameNode = isset($creation) ? $creation->classTypeDesignator : $node; - /** The typed name */ - $prefix = $nameNode instanceof Node\QualifiedName - ? (string)PhpParser\ResolvedName::buildName($nameNode->nameParts, $nameNode->getFileContents()) - : $nameNode->getText($node->getFileContents()); - $prefixLen = strlen($prefix); + if ($nameNode instanceof Node\QualifiedName) { + /** @var string The typed name. */ + $prefix = (string)PhpParser\ResolvedName::buildName($nameNode->nameParts, $nameNode->getFileContents()); + } else { + $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(); + /** @var string The current namespace without a leading backslash. */ + $currentNamespace = $namespaceNode === null ? '' : $namespaceNode->name->getText(); - /** @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); + /** @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(); } - // Get the namespace use statements - // TODO: use function statements, use const statements + /** @var bool Whether we are in a new expression */ + $isCreation = isset($creation); - /** @var string[] $aliases A map from local alias to fully qualified name */ - list($aliases,,) = $node->getImportTablesForCurrentScope(); + /** @var array Import (use) tables */ + $importTables = $node->getImportTablesForCurrentScope(); - foreach ($aliases as $alias => $name) { - $aliases[$alias] = (string)$name; + 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); } - // 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[] = CompletionItem::fromDefinition($def); - } + $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); } } - // 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) { + } + return $list; + } - $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 = CompletionItem::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; - } - } + 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; } } - 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; + } + } } /** @@ -472,8 +644,9 @@ class CompletionProvider } } - if ($level instanceof Node\Expression\AnonymousFunctionCreationExpression && $level->anonymousFunctionUseClause !== null && - $level->anonymousFunctionUseClause->useVariableNameList !== null) { + if ($level instanceof Node\Expression\AnonymousFunctionCreationExpression + && $level->anonymousFunctionUseClause !== null + && $level->anonymousFunctionUseClause->useVariableNameList !== null) { foreach ($level->anonymousFunctionUseClause->useVariableNameList->getValues() as $use) { $useName = $use->getName(); if (empty($namePrefix) || strpos($useName, $namePrefix) !== false) { diff --git a/src/ContentRetriever/ClientContentRetriever.php b/src/ContentRetriever/ClientContentRetriever.php index b88042c..d83aad1 100644 --- a/src/ContentRetriever/ClientContentRetriever.php +++ b/src/ContentRetriever/ClientContentRetriever.php @@ -4,7 +4,7 @@ declare(strict_types = 1); namespace LanguageServer\ContentRetriever; use LanguageServer\LanguageClient; -use LanguageServer\Protocol\{TextDocumentIdentifier, TextDocumentItem}; +use LanguageServerProtocol\{TextDocumentIdentifier, TextDocumentItem}; use Sabre\Event\Promise; /** diff --git a/src/Definition.php b/src/Definition.php index 2c92eb9..d81594d 100644 --- a/src/Definition.php +++ b/src/Definition.php @@ -5,7 +5,7 @@ namespace LanguageServer; use LanguageServer\Index\ReadableIndex; use phpDocumentor\Reflection\{Types, Type, TypeResolver}; -use LanguageServer\Protocol\SymbolInformation; +use LanguageServerProtocol\SymbolInformation; use Generator; /** diff --git a/src/DefinitionResolver.php b/src/DefinitionResolver.php index 990c196..adddf77 100644 --- a/src/DefinitionResolver.php +++ b/src/DefinitionResolver.php @@ -4,7 +4,8 @@ declare(strict_types = 1); namespace LanguageServer; use LanguageServer\Index\ReadableIndex; -use LanguageServer\Protocol\SymbolInformation; +use LanguageServer\Factory\SymbolInformationFactory; +use LanguageServerProtocol\SymbolInformation; use Microsoft\PhpParser; use Microsoft\PhpParser\Node; use Microsoft\PhpParser\FunctionLike; @@ -36,7 +37,7 @@ class DefinitionResolver private $docBlockFactory; /** - * Creates SignatureInformation + * Creates SignatureInformation instances * * @var SignatureInformationFactory */ @@ -233,7 +234,7 @@ class DefinitionResolver } } - $def->symbolInformation = SymbolInformation::fromNode($node, $fqn); + $def->symbolInformation = SymbolInformationFactory::fromNode($node, $fqn); if ($def->symbolInformation !== null) { $def->type = $this->getTypeFromNode($node); @@ -437,6 +438,7 @@ class DefinitionResolver // Find the right class that implements the member $implementorFqns = [$classFqn]; + $visitedFqns = []; while ($implementorFqn = array_shift($implementorFqns)) { // If the member FQN exists, return it @@ -449,10 +451,15 @@ class DefinitionResolver if ($implementorDef === null) { break; } + // Note the FQN as visited + $visitedFqns[] = $implementorFqn; // Repeat for parent class if ($implementorDef->extends) { foreach ($implementorDef->extends as $extends) { - $implementorFqns[] = $extends; + // Don't add the parent FQN if it's already been visited + if (!\in_array($extends, $visitedFqns)) { + $implementorFqns[] = $extends; + } } } } @@ -1232,7 +1239,13 @@ class DefinitionResolver if ( $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: diff --git a/src/Factory/CompletionItemFactory.php b/src/Factory/CompletionItemFactory.php new file mode 100644 index 0000000..ce00fa6 --- /dev/null +++ b/src/Factory/CompletionItemFactory.php @@ -0,0 +1,36 @@ +label = $def->symbolInformation->name; + $item->kind = CompletionItemKind::fromSymbolKind($def->symbolInformation->kind); + if ($def->type) { + $item->detail = (string)$def->type; + } else if ($def->symbolInformation->containerName) { + $item->detail = $def->symbolInformation->containerName; + } + if ($def->documentation) { + $item->documentation = $def->documentation; + } + if ($def->isStatic && $def->symbolInformation->kind === SymbolKind::PROPERTY) { + $item->insertText = '$' . $def->symbolInformation->name; + } + return $item; + } +} diff --git a/src/Factory/LocationFactory.php b/src/Factory/LocationFactory.php new file mode 100644 index 0000000..3ccc80b --- /dev/null +++ b/src/Factory/LocationFactory.php @@ -0,0 +1,32 @@ +getStart(), + $node->getWidth(), + $node->getFileContents() + ); + + return new Location($node->getUri(), new Range( + new Position($range->start->line, $range->start->character), + new Position($range->end->line, $range->end->character) + )); + } +} diff --git a/src/Factory/RangeFactory.php b/src/Factory/RangeFactory.php new file mode 100644 index 0000000..b438db5 --- /dev/null +++ b/src/Factory/RangeFactory.php @@ -0,0 +1,31 @@ +getStart(), + $node->getWidth(), + $node->getFileContents() + ); + + return new Range( + new Position($range->start->line, $range->start->character), + new Position($range->end->line, $range->end->character) + ); + } +} diff --git a/src/Protocol/SymbolInformation.php b/src/Factory/SymbolInformationFactory.php similarity index 74% rename from src/Protocol/SymbolInformation.php rename to src/Factory/SymbolInformationFactory.php index 499e417..b03d951 100644 --- a/src/Protocol/SymbolInformation.php +++ b/src/Factory/SymbolInformationFactory.php @@ -1,44 +1,16 @@ kind = SymbolKind::CLASS_; } else if ($node instanceof Node\Statement\TraitDeclaration) { @@ -98,7 +70,7 @@ class SymbolInformation $symbol->name = $node->getName(); } else if (isset($node->name)) { if ($node->name instanceof Node\QualifiedName) { - $symbol->name = (string)PhpParser\ResolvedName::buildName($node->name->nameParts, $node->getFileContents()); + $symbol->name = (string)ResolvedName::buildName($node->name->nameParts, $node->getFileContents()); } else { $symbol->name = ltrim((string)$node->name->getText($node->getFileContents()), "$"); } @@ -108,7 +80,7 @@ class SymbolInformation return null; } - $symbol->location = Location::fromNode($node); + $symbol->location = LocationFactory::fromNode($node); if ($fqn !== null) { $parts = preg_split('/(::|->|\\\\)/', $fqn); array_pop($parts); @@ -116,18 +88,4 @@ class SymbolInformation } return $symbol; } - - /** - * @param string $name - * @param int $kind - * @param Location $location - * @param string $containerName - */ - public function __construct($name = null, $kind = null, $location = null, $containerName = null) - { - $this->name = $name; - $this->kind = $kind; - $this->location = $location; - $this->containerName = $containerName; - } } diff --git a/src/FqnUtilities.php b/src/FqnUtilities.php index fcd6027..57e3776 100644 --- a/src/FqnUtilities.php +++ b/src/FqnUtilities.php @@ -28,3 +28,91 @@ function getFqnsFromType($type): array } 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; +} diff --git a/src/Index/AbstractAggregateIndex.php b/src/Index/AbstractAggregateIndex.php index 5377c3a..8c8c95a 100644 --- a/src/Index/AbstractAggregateIndex.php +++ b/src/Index/AbstractAggregateIndex.php @@ -99,20 +99,29 @@ abstract class AbstractAggregateIndex implements ReadableIndex } /** - * Returns an associative array [string => Definition] that maps fully qualified symbol names - * to Definitions + * Returns a Generator providing an associative array [string => Definition] + * 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 ($index->getDefinitions() as $fqn => $def) { - $defs[$fqn] = $def; - } + yield from $index->getDefinitions(); + } + } + + /** + * 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 - * @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 ($index->getReferenceUris($fqn) as $ref) { - $refs[] = $ref; - } + yield from $index->getReferenceUris($fqn); } - return $refs; } } diff --git a/src/Index/Index.php b/src/Index/Index.php index 6f95a4b..0d61f9c 100644 --- a/src/Index/Index.php +++ b/src/Index/Index.php @@ -15,14 +15,26 @@ class Index implements ReadableIndex, \Serializable 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 = []; /** - * 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[][] */ @@ -84,14 +96,46 @@ class Index implements ReadableIndex, \Serializable } /** - * Returns an associative array [string => Definition] that maps fully qualified symbol names - * to Definitions + * Returns a Generator providing an associative array [string => Definition] + * 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) { - if (isset($this->definitions[$fqn])) { - return $this->definitions[$fqn]; + $parts = $this->splitFqn($fqn); + $result = $this->getIndexValue($parts, $this->definitions); + + if ($result instanceof Definition) { + return $result; } + if ($globalFallback) { $parts = explode('\\', $fqn); $fqn = end($parts); + return $this->getDefinition($fqn); } } @@ -122,7 +171,9 @@ class Index implements ReadableIndex, \Serializable */ 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'); } @@ -135,19 +186,23 @@ class Index implements ReadableIndex, \Serializable */ 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]); } /** - * 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 - * @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) { $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) { $this->$prop = $val; } @@ -215,10 +279,164 @@ class Index implements ReadableIndex, \Serializable public function serialize() { return serialize([ - 'definitions' => $this->definitions, + 'definitions' => iterator_to_array($this->getDefinitions()), 'references' => $this->references, 'complete' => $this->complete, '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); + } + } } diff --git a/src/Index/ReadableIndex.php b/src/Index/ReadableIndex.php index 67b20b6..505bb9a 100644 --- a/src/Index/ReadableIndex.php +++ b/src/Index/ReadableIndex.php @@ -30,12 +30,20 @@ interface ReadableIndex extends EmitterInterface public function isStaticComplete(): bool; /** - * Returns an associative array [string => Definition] that maps fully qualified symbol names - * to Definitions + * Returns a Generator providing an associative array [string => Definition] + * 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 @@ -47,10 +55,10 @@ interface ReadableIndex extends EmitterInterface 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 - * @return string[] + * @return \Generator yields string */ - public function getReferenceUris(string $fqn): array; + public function getReferenceUris(string $fqn): \Generator; } diff --git a/src/Indexer.php b/src/Indexer.php index 85d1787..7ebff3f 100644 --- a/src/Indexer.php +++ b/src/Indexer.php @@ -6,7 +6,7 @@ namespace LanguageServer; use LanguageServer\Cache\Cache; use LanguageServer\FilesFinder\FilesFinder; use LanguageServer\Index\{DependenciesIndex, Index}; -use LanguageServer\Protocol\MessageType; +use LanguageServerProtocol\MessageType; use Webmozart\PathUtil\Path; use Sabre\Event\Promise; use function Sabre\Event\coroutine; @@ -16,7 +16,7 @@ class Indexer /** * @var int The prefix for every cache item */ - const CACHE_VERSION = 2; + const CACHE_VERSION = 3; /** * @var FilesFinder diff --git a/src/LanguageServer.php b/src/LanguageServer.php index 46281f5..a30992b 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -3,15 +3,15 @@ declare(strict_types = 1); namespace LanguageServer; -use LanguageServer\Protocol\{ +use LanguageServerProtocol\{ ServerCapabilities, ClientCapabilities, TextDocumentSyncKind, - Message, InitializeResult, CompletionOptions, SignatureHelpOptions }; +use LanguageServer\Message; use LanguageServer\FilesFinder\{FilesFinder, ClientFilesFinder, FileSystemFilesFinder}; use LanguageServer\ContentRetriever\{ContentRetriever, ClientContentRetriever, FileSystemContentRetriever}; use LanguageServer\Index\{DependenciesIndex, GlobalIndex, Index, ProjectIndex, StubsIndex}; @@ -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. * @return Promise */ - 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) { if ($capabilities->xfilesProvider) { diff --git a/src/Protocol/Message.php b/src/Message.php similarity index 96% rename from src/Protocol/Message.php rename to src/Message.php index 8df5d0e..fb51110 100644 --- a/src/Protocol/Message.php +++ b/src/Message.php @@ -1,9 +1,10 @@ triggerKind = $triggerKind; - $this->triggerCharacter = $triggerCharacter; - } -} diff --git a/src/Protocol/CompletionItem.php b/src/Protocol/CompletionItem.php deleted file mode 100644 index 64bc69d..0000000 --- a/src/Protocol/CompletionItem.php +++ /dev/null @@ -1,164 +0,0 @@ -label = $label; - $this->kind = $kind; - $this->detail = $detail; - $this->documentation = $documentation; - $this->sortText = $sortText; - $this->filterText = $filterText; - $this->insertText = $insertText; - $this->textEdit = $textEdit; - $this->additionalTextEdits = $additionalTextEdits; - $this->command = $command; - $this->data = $data; - } - - /** - * Creates a CompletionItem for a Definition - * - * @param Definition $def - * @return self - */ - public static function fromDefinition(Definition $def): self - { - $item = new CompletionItem; - $item->label = $def->symbolInformation->name; - $item->kind = CompletionItemKind::fromSymbolKind($def->symbolInformation->kind); - if ($def->type) { - $item->detail = (string)$def->type; - } else if ($def->symbolInformation->containerName) { - $item->detail = $def->symbolInformation->containerName; - } - if ($def->documentation) { - $item->documentation = $def->documentation; - } - if ($def->isStatic && $def->symbolInformation->kind === SymbolKind::PROPERTY) { - $item->insertText = '$' . $def->symbolInformation->name; - } - return $item; - } -} diff --git a/src/Protocol/CompletionItemKind.php b/src/Protocol/CompletionItemKind.php deleted file mode 100644 index 046f8ef..0000000 --- a/src/Protocol/CompletionItemKind.php +++ /dev/null @@ -1,70 +0,0 @@ -items = $items; - $this->isIncomplete = $isIncomplete; - } -} diff --git a/src/Protocol/CompletionOptions.php b/src/Protocol/CompletionOptions.php deleted file mode 100644 index f668ca0..0000000 --- a/src/Protocol/CompletionOptions.php +++ /dev/null @@ -1,24 +0,0 @@ -attributes = $attributes ?? new \stdClass; - $this->hints = $hints; - } -} diff --git a/src/Protocol/Diagnostic.php b/src/Protocol/Diagnostic.php deleted file mode 100644 index 7bf9895..0000000 --- a/src/Protocol/Diagnostic.php +++ /dev/null @@ -1,63 +0,0 @@ -message = $message; - $this->range = $range; - $this->code = $code; - $this->severity = $severity; - $this->source = $source; - } -} diff --git a/src/Protocol/DiagnosticSeverity.php b/src/Protocol/DiagnosticSeverity.php deleted file mode 100644 index e91da8f..0000000 --- a/src/Protocol/DiagnosticSeverity.php +++ /dev/null @@ -1,26 +0,0 @@ -uri = $uri; - $this->type = $type; - } -} diff --git a/src/Protocol/FormattingOptions.php b/src/Protocol/FormattingOptions.php deleted file mode 100644 index b672ddf..0000000 --- a/src/Protocol/FormattingOptions.php +++ /dev/null @@ -1,25 +0,0 @@ -contents = $contents; - $this->range = $range; - } -} diff --git a/src/Protocol/InitializeResult.php b/src/Protocol/InitializeResult.php deleted file mode 100644 index 4a18e82..0000000 --- a/src/Protocol/InitializeResult.php +++ /dev/null @@ -1,21 +0,0 @@ -capabilities = $capabilities ?? new ServerCapabilities(); - } -} diff --git a/src/Protocol/Location.php b/src/Protocol/Location.php deleted file mode 100644 index 50fedfa..0000000 --- a/src/Protocol/Location.php +++ /dev/null @@ -1,43 +0,0 @@ -getStart(), $node->getWidth(), $node->getFileContents()); - return new self($node->getUri(), new Range( - new Position($range->start->line, $range->start->character), - new Position($range->end->line, $range->end->character) - )); - } - - public function __construct(string $uri = null, Range $range = null) - { - $this->uri = $uri; - $this->range = $range; - } -} diff --git a/src/Protocol/MarkedString.php b/src/Protocol/MarkedString.php deleted file mode 100644 index 0799f14..0000000 --- a/src/Protocol/MarkedString.php +++ /dev/null @@ -1,22 +0,0 @@ -language = $language; - $this->value = $value; - } -} diff --git a/src/Protocol/MessageActionItem.php b/src/Protocol/MessageActionItem.php deleted file mode 100644 index 59dafe5..0000000 --- a/src/Protocol/MessageActionItem.php +++ /dev/null @@ -1,13 +0,0 @@ -name = $name; - } -} diff --git a/src/Protocol/ParameterInformation.php b/src/Protocol/ParameterInformation.php deleted file mode 100644 index fa9b7bf..0000000 --- a/src/Protocol/ParameterInformation.php +++ /dev/null @@ -1,39 +0,0 @@ -label = $label; - $this->documentation = $documentation; - } -} diff --git a/src/Protocol/Position.php b/src/Protocol/Position.php deleted file mode 100644 index f47afe2..0000000 --- a/src/Protocol/Position.php +++ /dev/null @@ -1,65 +0,0 @@ -line = $line; - $this->character = $character; - } - - /** - * Compares this position to another position - * Returns - * - 0 if the positions match - * - a negative number if $this is before $position - * - a positive number otherwise - * - * @param Position $position - * @return int - */ - public function compare(Position $position): int - { - if ($this->line === $position->line && $this->character === $position->character) { - return 0; - } - - if ($this->line !== $position->line) { - return $this->line - $position->line; - } - - return $this->character - $position->character; - } - - /** - * Returns the offset of the position in a string - * - * @param string $content - * @return int - */ - public function toOffset(string $content): int - { - $lines = explode("\n", $content); - $slice = array_slice($lines, 0, $this->line); - return array_sum(array_map('strlen', $slice)) + count($slice) + $this->character; - } -} diff --git a/src/Protocol/Range.php b/src/Protocol/Range.php deleted file mode 100644 index 1e19a5a..0000000 --- a/src/Protocol/Range.php +++ /dev/null @@ -1,59 +0,0 @@ -getStart(), $node->getWidth(), $node->getFileContents()); - - return new self( - new Position($range->start->line, $range->start->character), - new Position($range->end->line, $range->end->character) - ); - } - - public function __construct(Position $start = null, Position $end = null) - { - $this->start = $start; - $this->end = $end; - } - - /** - * Checks if a position is within the range - * - * @param Position $position - * @return bool - */ - public function includes(Position $position): bool - { - return $this->start->compare($position) <= 0 && $this->end->compare($position) >= 0; - } -} diff --git a/src/Protocol/ReferenceContext.php b/src/Protocol/ReferenceContext.php deleted file mode 100644 index bd546d5..0000000 --- a/src/Protocol/ReferenceContext.php +++ /dev/null @@ -1,13 +0,0 @@ -reference = $reference; - $this->symbol = $symbol; - } -} diff --git a/src/Protocol/ServerCapabilities.php b/src/Protocol/ServerCapabilities.php deleted file mode 100644 index 1893a51..0000000 --- a/src/Protocol/ServerCapabilities.php +++ /dev/null @@ -1,132 +0,0 @@ -signatures = $signatures; - $this->activeSignature = $activeSignature; - $this->activeParameter = $activeParameter; - } -} diff --git a/src/Protocol/SignatureHelpOptions.php b/src/Protocol/SignatureHelpOptions.php deleted file mode 100644 index 25b0e37..0000000 --- a/src/Protocol/SignatureHelpOptions.php +++ /dev/null @@ -1,16 +0,0 @@ -label = $label; - $this->parameters = $parameters; - $this->documentation = $documentation; - } -} diff --git a/src/Protocol/SymbolDescriptor.php b/src/Protocol/SymbolDescriptor.php deleted file mode 100644 index 4116864..0000000 --- a/src/Protocol/SymbolDescriptor.php +++ /dev/null @@ -1,34 +0,0 @@ -fqsen = $fqsen; - $this->package = $package; - } -} diff --git a/src/Protocol/SymbolKind.php b/src/Protocol/SymbolKind.php deleted file mode 100644 index e8a44e2..0000000 --- a/src/Protocol/SymbolKind.php +++ /dev/null @@ -1,28 +0,0 @@ -symbol = $symbol; - $this->location = $location; - } -} diff --git a/src/Protocol/TextDocumentContentChangeEvent.php b/src/Protocol/TextDocumentContentChangeEvent.php deleted file mode 100644 index 9cfa08b..0000000 --- a/src/Protocol/TextDocumentContentChangeEvent.php +++ /dev/null @@ -1,31 +0,0 @@ -uri = $uri; - } -} diff --git a/src/Protocol/TextDocumentItem.php b/src/Protocol/TextDocumentItem.php deleted file mode 100644 index 0c86664..0000000 --- a/src/Protocol/TextDocumentItem.php +++ /dev/null @@ -1,38 +0,0 @@ -range = $range; - $this->newText = $newText; - } -} diff --git a/src/Protocol/VersionedTextDocumentIdentifier.php b/src/Protocol/VersionedTextDocumentIdentifier.php deleted file mode 100644 index ba74e42..0000000 --- a/src/Protocol/VersionedTextDocumentIdentifier.php +++ /dev/null @@ -1,13 +0,0 @@ - */ public function documentSymbol(TextDocumentIdentifier $textDocument): Promise @@ -127,7 +129,7 @@ class TextDocument * document's truth is now managed by the client and the server must not try to read the document's truth using the * document's uri. * - * @param \LanguageServer\Protocol\TextDocumentItem $textDocument The document that was opened. + * @param \LanguageServerProtocol\TextDocumentItem $textDocument The document that was opened. * @return void */ public function didOpen(TextDocumentItem $textDocument) @@ -141,8 +143,8 @@ class TextDocument /** * The document change notification is sent from the client to the server to signal changes to a text document. * - * @param \LanguageServer\Protocol\VersionedTextDocumentIdentifier $textDocument - * @param \LanguageServer\Protocol\TextDocumentContentChangeEvent[] $contentChanges + * @param \LanguageServerProtocol\VersionedTextDocumentIdentifier $textDocument + * @param \LanguageServerProtocol\TextDocumentContentChangeEvent[] $contentChanges * @return void */ public function didChange(VersionedTextDocumentIdentifier $textDocument, array $contentChanges) @@ -157,7 +159,7 @@ class TextDocument * The document's truth now exists where the document's uri points to (e.g. if the document's uri is a file uri the * truth now exists on disk). * - * @param \LanguageServer\Protocol\TextDocumentIdentifier $textDocument The document that was closed + * @param \LanguageServerProtocol\TextDocumentIdentifier $textDocument The document that was closed * @return void */ public function didClose(TextDocumentIdentifier $textDocument) @@ -208,7 +210,7 @@ class TextDocument if ($descendantNode instanceof Node\Expression\Variable && $descendantNode->getName() === $node->getName() ) { - $locations[] = Location::fromNode($descendantNode); + $locations[] = LocationFactory::fromNode($descendantNode); } } } else { @@ -225,15 +227,16 @@ class TextDocument return []; } } - $refDocuments = yield Promise\all(array_map( - [$this->documentLoader, 'getOrLoad'], - $this->index->getReferenceUris($fqn) - )); + $refDocumentPromises = []; + foreach ($this->index->getReferenceUris($fqn) as $uri) { + $refDocumentPromises[] = $this->documentLoader->getOrLoad($uri); + } + $refDocuments = yield Promise\all($refDocumentPromises); foreach ($refDocuments as $document) { $refs = $document->getReferenceNodesByFqn($fqn); if ($refs !== null) { foreach ($refs as $ref) { - $locations[] = Location::fromNode($ref); + $locations[] = LocationFactory::fromNode($ref); } } } @@ -332,7 +335,7 @@ class TextDocument } yield waitForEvent($this->index, 'definition-added'); } - $range = Range::fromNode($node); + $range = RangeFactory::fromNode($node); if ($def === null) { return new Hover([], $range); } diff --git a/src/Server/Workspace.php b/src/Server/Workspace.php index 548197b..fd93a9e 100644 --- a/src/Server/Workspace.php +++ b/src/Server/Workspace.php @@ -5,7 +5,8 @@ namespace LanguageServer\Server; use LanguageServer\{LanguageClient, PhpDocumentLoader}; use LanguageServer\Index\{ProjectIndex, DependenciesIndex, Index}; -use LanguageServer\Protocol\{ +use LanguageServer\Factory\LocationFactory; +use LanguageServerProtocol\{ FileChangeType, FileEvent, SymbolInformation, @@ -150,7 +151,7 @@ class Workspace $doc = yield $this->documentLoader->getOrLoad($uri); foreach ($doc->getReferenceNodesByFqn($fqn) as $node) { $refInfo = new ReferenceInformation; - $refInfo->reference = Location::fromNode($node); + $refInfo->reference = LocationFactory::fromNode($node); $refInfo->symbol = $query; $refInfos[] = $refInfo; } diff --git a/src/SignatureHelpProvider.php b/src/SignatureHelpProvider.php index 92775be..66afd5f 100644 --- a/src/SignatureHelpProvider.php +++ b/src/SignatureHelpProvider.php @@ -4,7 +4,7 @@ declare(strict_types = 1); namespace LanguageServer; use LanguageServer\Index\ReadableIndex; -use LanguageServer\Protocol\{ +use LanguageServerProtocol\{ Position, SignatureHelp }; diff --git a/src/SignatureInformationFactory.php b/src/SignatureInformationFactory.php index 6b8a1f0..86402b6 100644 --- a/src/SignatureInformationFactory.php +++ b/src/SignatureInformationFactory.php @@ -3,7 +3,7 @@ declare(strict_types = 1); namespace LanguageServer; -use LanguageServer\Protocol\{SignatureInformation, ParameterInformation}; +use LanguageServerProtocol\{SignatureInformation, ParameterInformation}; use Microsoft\PhpParser\FunctionLike; class SignatureInformationFactory diff --git a/src/TreeAnalyzer.php b/src/TreeAnalyzer.php index 03ad539..6b48470 100644 --- a/src/TreeAnalyzer.php +++ b/src/TreeAnalyzer.php @@ -3,7 +3,8 @@ declare(strict_types = 1); namespace LanguageServer; -use LanguageServer\Protocol\{Diagnostic, DiagnosticSeverity, Range, Position}; +use LanguageServer\Factory\RangeFactory; +use LanguageServerProtocol\{Diagnostic, DiagnosticSeverity, Range, Position}; use phpDocumentor\Reflection\DocBlockFactory; use Microsoft\PhpParser; use Microsoft\PhpParser\Node; @@ -100,7 +101,7 @@ class TreeAnalyzer if ($method && $method->isStatic()) { $this->diagnostics[] = new Diagnostic( "\$this can not be used in static methods.", - Range::fromNode($node), + RangeFactory::fromNode($node), null, DiagnosticSeverity::ERROR, 'php' diff --git a/src/utils.php b/src/utils.php index 97e091d..af47a96 100644 --- a/src/utils.php +++ b/src/utils.php @@ -38,7 +38,7 @@ function pathToUri(string $filepath): string function uriToPath(string $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"); } $filepath = urldecode($fragments['path']); diff --git a/tests/ClientHandlerTest.php b/tests/ClientHandlerTest.php index 214b018..19114d9 100644 --- a/tests/ClientHandlerTest.php +++ b/tests/ClientHandlerTest.php @@ -5,7 +5,7 @@ namespace LanguageServer\Tests; use PHPUnit\Framework\TestCase; use LanguageServer\ClientHandler; -use LanguageServer\Protocol\Message; +use LanguageServer\Message; use AdvancedJsonRpc; use Sabre\Event\Loop; diff --git a/tests/Diagnostics/InvalidThisUsageTest.php b/tests/Diagnostics/InvalidThisUsageTest.php index b7e8196..566c1c0 100644 --- a/tests/Diagnostics/InvalidThisUsageTest.php +++ b/tests/Diagnostics/InvalidThisUsageTest.php @@ -9,7 +9,7 @@ use LanguageServer\{ DefinitionResolver, TreeAnalyzer }; use LanguageServer\Index\{Index}; -use LanguageServer\Protocol\{ +use LanguageServerProtocol\{ Diagnostic, DiagnosticSeverity, Position, Range }; use function LanguageServer\pathToUri; diff --git a/tests/LanguageServerTest.php b/tests/LanguageServerTest.php index 52963e6..10ca3f9 100644 --- a/tests/LanguageServerTest.php +++ b/tests/LanguageServerTest.php @@ -5,8 +5,8 @@ namespace LanguageServer\Tests; use PHPUnit\Framework\TestCase; use LanguageServer\LanguageServer; -use LanguageServer\Protocol\{ - Message, +use LanguageServer\Message; +use LanguageServerProtocol\{ ClientCapabilities, TextDocumentSyncKind, MessageType, diff --git a/tests/MockProtocolStream.php b/tests/MockProtocolStream.php index b2a489d..9287053 100644 --- a/tests/MockProtocolStream.php +++ b/tests/MockProtocolStream.php @@ -4,7 +4,7 @@ declare(strict_types = 1); namespace LanguageServer\Tests; use LanguageServer\{ProtocolReader, ProtocolWriter}; -use LanguageServer\Protocol\Message; +use LanguageServer\Message; use Sabre\Event\{Loop, Emitter, Promise}; /** diff --git a/tests/NodeVisitor/DefinitionCollectorTest.php b/tests/NodeVisitor/DefinitionCollectorTest.php index a092b62..c27822c 100644 --- a/tests/NodeVisitor/DefinitionCollectorTest.php +++ b/tests/NodeVisitor/DefinitionCollectorTest.php @@ -35,7 +35,9 @@ class DefinitionCollectorTest extends TestCase 'TestNamespace\\ChildClass', 'TestNamespace\\Example', 'TestNamespace\\Example->__construct()', - 'TestNamespace\\Example->__destruct()' + 'TestNamespace\\Example->__destruct()', + 'TestNamespace\\InnerNamespace', + 'TestNamespace\\InnerNamespace\\InnerClass', ], array_keys($defNodes)); $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\MethodDeclaration::class, $defNodes['TestNamespace\\Example->__construct()']); $this->assertInstanceOf(Node\MethodDeclaration::class, $defNodes['TestNamespace\\Example->__destruct()']); + $this->assertInstanceOf(Node\Statement\ClassDeclaration::class, $defNodes['TestNamespace\\InnerNamespace\\InnerClass']); } public function testDoesNotCollectReferences() diff --git a/tests/PhpDocumentTest.php b/tests/PhpDocumentTest.php index ae1b5cd..710e651 100644 --- a/tests/PhpDocumentTest.php +++ b/tests/PhpDocumentTest.php @@ -9,7 +9,7 @@ use LanguageServer\{ use LanguageServer\Index\{ Index }; -use LanguageServer\Protocol\{ +use LanguageServerProtocol\{ Position }; use Microsoft\PhpParser; diff --git a/tests/ProtocolStreamReaderTest.php b/tests/ProtocolStreamReaderTest.php index 69eea37..a0dcad5 100644 --- a/tests/ProtocolStreamReaderTest.php +++ b/tests/ProtocolStreamReaderTest.php @@ -5,7 +5,7 @@ namespace LanguageServer\Tests; use PHPUnit\Framework\TestCase; use LanguageServer\{LanguageServer, ProtocolStreamReader, ProtocolStreamWriter}; -use LanguageServer\Protocol\Message; +use LanguageServer\Message; use AdvancedJsonRpc\{Request as RequestBody, Response as ResponseBody}; use Sabre\Event\Loop; diff --git a/tests/ProtocolStreamWriterTest.php b/tests/ProtocolStreamWriterTest.php index b67bdcb..8ac94e7 100644 --- a/tests/ProtocolStreamWriterTest.php +++ b/tests/ProtocolStreamWriterTest.php @@ -5,7 +5,7 @@ namespace LanguageServer\Tests; use PHPUnit\Framework\TestCase; use LanguageServer\ProtocolStreamWriter; -use LanguageServer\Protocol\Message; +use LanguageServer\Message; use AdvancedJsonRpc\{Request as RequestBody}; use Sabre\Event\Loop; diff --git a/tests/Server/ServerTestCase.php b/tests/Server/ServerTestCase.php index 45d949f..3331aef 100644 --- a/tests/Server/ServerTestCase.php +++ b/tests/Server/ServerTestCase.php @@ -10,7 +10,7 @@ use LanguageServer\{ }; use LanguageServer\Index\{ProjectIndex, DependenciesIndex, Index}; use LanguageServer\ContentRetriever\FileSystemContentRetriever; -use LanguageServer\Protocol\{Position, Location, Range}; +use LanguageServerProtocol\{Position, Location, Range}; use function LanguageServer\pathToUri; abstract class ServerTestCase extends TestCase @@ -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\\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::__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 = [ diff --git a/tests/Server/TextDocument/CompletionTest.php b/tests/Server/TextDocument/CompletionTest.php index 2b4e9ff..516d3b4 100644 --- a/tests/Server/TextDocument/CompletionTest.php +++ b/tests/Server/TextDocument/CompletionTest.php @@ -10,7 +10,7 @@ use LanguageServer\{ }; use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex}; use LanguageServer\ContentRetriever\FileSystemContentRetriever; -use LanguageServer\Protocol\{ +use LanguageServerProtocol\{ TextDocumentIdentifier, TextEdit, Range, @@ -47,6 +47,9 @@ class CompletionTest extends TestCase $this->textDocument = new Server\TextDocument($this->loader, $definitionResolver, $client, $projectIndex); } + /** + * Tests completion at `$obj->t|` + */ public function testPropertyAndMethodWithPrefix() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/property_with_prefix.php'); @@ -71,6 +74,9 @@ class CompletionTest extends TestCase ], true), $items); } + /** + * Tests completion at `public function a() { tes| }` + */ public function testGlobalFunctionInsideNamespaceAndClass() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/inside_namespace_and_method.php'); @@ -92,6 +98,9 @@ class CompletionTest extends TestCase ], true), $items); } + /** + * Tests completion at `$obj->|` + */ public function testPropertyAndMethodWithoutPrefix() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/property.php'); @@ -116,6 +125,9 @@ class CompletionTest extends TestCase ], true), $items); } + /** + * Tests completion at `$|` when variables are defined + */ public function testVariable() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/variable.php'); @@ -148,6 +160,9 @@ class CompletionTest extends TestCase ], true), $items); } + /** + * Tests completion at `$p|` when variables are defined + */ public function testVariableWithPrefix() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/variable_with_prefix.php'); @@ -170,6 +185,9 @@ class CompletionTest extends TestCase ], true), $items); } + /** + * Tests completion at `new|` when in a namespace and have used variables. + */ public function testNewInNamespace() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/used_new.php'); @@ -218,27 +236,12 @@ class CompletionTest extends TestCase null, '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); } + /** + * Tests completion at `TestC|` with `use TestNamespace\TestClass` + */ public function testUsedClass() { $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" . '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" . - 'sint. Officia culpa pariatur laborum nostrud cupidatat consequat mollit.' + 'sint. Officia culpa pariatur laborum nostrud cupidatat consequat mollit.', + null, + null, + 'TestClass' ) ], 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() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/static_property_with_prefix.php'); @@ -283,6 +349,9 @@ class CompletionTest extends TestCase ], true), $items); } + /** + * Tests completion at `TestClass::|` + */ public function testStaticWithoutPrefix() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/static.php'); @@ -316,6 +385,9 @@ class CompletionTest extends TestCase ], true), $items); } + /** + * Tests completion at `TestClass::st|` + */ public function testStaticMethodWithPrefix() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/static_method_with_prefix.php'); @@ -325,21 +397,6 @@ class CompletionTest extends TestCase new Position(2, 13) )->wait(); $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( 'staticTestMethod', CompletionItemKind::METHOD, @@ -349,6 +406,9 @@ class CompletionTest extends TestCase ], true), $items); } + /** + * Tests completion at `TestClass::TE` at the root level. + */ public function testClassConstWithPrefix() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/class_const_with_prefix.php'); @@ -363,25 +423,13 @@ class CompletionTest extends TestCase 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( - 'staticTestMethod', - CompletionItemKind::METHOD, - 'mixed', - 'Do magna consequat veniam minim proident eiusmod incididunt aute proident.' ) ], true), $items); } + /** + * Test completion at `\TestC|` in a namespace + */ public function testFullyQualifiedClass() { $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" . '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" . - 'sint. Officia culpa pariatur laborum nostrud cupidatat consequat mollit.', - null, - null, - 'TestClass' + 'sint. Officia culpa pariatur laborum nostrud cupidatat consequat mollit.' ) ], 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() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/keywords.php'); @@ -422,6 +474,9 @@ class CompletionTest extends TestCase ], true), $items); } + /** + * Tests completion in an empty file + */ public function testHtmlWithoutPrefix() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html.php'); @@ -444,6 +499,9 @@ class CompletionTest extends TestCase ], true), $items); } + /** + * Tests completion in `<|` when not within `assertEquals(new CompletionList([], true), $items); } + /** + * Tests completion in `<|` when not within `assertEquals(new CompletionList([], true), $items); } + /** + * Tests completion at `<|` when not within `assertCompletionsListSubset(new CompletionList([ new CompletionItem( 'SomeNamespace', - CompletionItemKind::MODULE, - null, - null, - null, - null, - 'SomeNamespace' + CompletionItemKind::MODULE ) ], 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'); $this->loader->open($completionUri, file_get_contents($completionUri)); @@ -776,6 +844,16 @@ class CompletionTest extends TestCase $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() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/this.php'); @@ -812,6 +890,9 @@ class CompletionTest extends TestCase ], true), $items); } + /** + * Tests completion at `$this->m|` + */ public function testThisWithPrefix() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/this_with_prefix.php'); @@ -821,18 +902,6 @@ class CompletionTest extends TestCase new Position(12, 16) )->wait(); $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( 'foo', CompletionItemKind::PROPERTY, @@ -856,10 +925,25 @@ class CompletionTest extends TestCase CompletionItemKind::METHOD, 'mixed', // Return type of the method 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); } + /** + * Tests completion at `$this->foo()->q|` + */ public function testThisReturnValue() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/this_return_value.php'); @@ -869,11 +953,6 @@ class CompletionTest extends TestCase new Position(17, 23) )->wait(); $this->assertEquals(new CompletionList([ - new CompletionItem( - 'foo', - CompletionItemKind::METHOD, - '$this' // Return type of the method - ), new CompletionItem( 'bar', CompletionItemKind::METHOD, @@ -883,7 +962,12 @@ class CompletionTest extends TestCase 'qux', CompletionItemKind::METHOD, 'mixed' // Return type of the method - ) + ), + new CompletionItem( + 'foo', + CompletionItemKind::METHOD, + '$this' // Return type of the method + ), ], true), $items); } } diff --git a/tests/Server/TextDocument/Definition/GlobalFallbackTest.php b/tests/Server/TextDocument/Definition/GlobalFallbackTest.php index 4e45f9e..cca6d4d 100644 --- a/tests/Server/TextDocument/Definition/GlobalFallbackTest.php +++ b/tests/Server/TextDocument/Definition/GlobalFallbackTest.php @@ -10,7 +10,7 @@ use LanguageServer\{ }; use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex}; use LanguageServer\ContentRetriever\FileSystemContentRetriever; -use LanguageServer\Protocol\{TextDocumentIdentifier, Position, Range, Location}; +use LanguageServerProtocol\{TextDocumentIdentifier, Position, Range, Location}; class GlobalFallbackTest extends ServerTestCase { diff --git a/tests/Server/TextDocument/Definition/GlobalTest.php b/tests/Server/TextDocument/Definition/GlobalTest.php index 2a80873..fdfb3b2 100644 --- a/tests/Server/TextDocument/Definition/GlobalTest.php +++ b/tests/Server/TextDocument/Definition/GlobalTest.php @@ -4,7 +4,7 @@ declare(strict_types = 1); namespace LanguageServer\Tests\Server\TextDocument\Definition; use LanguageServer\Tests\Server\ServerTestCase; -use LanguageServer\Protocol\{TextDocumentIdentifier, Position, Location, Range}; +use LanguageServerProtocol\{TextDocumentIdentifier, Position, Location, Range}; use function LanguageServer\pathToUri; class GlobalTest extends ServerTestCase diff --git a/tests/Server/TextDocument/Definition/NamespacedTest.php b/tests/Server/TextDocument/Definition/NamespacedTest.php index 395881b..0f9a582 100644 --- a/tests/Server/TextDocument/Definition/NamespacedTest.php +++ b/tests/Server/TextDocument/Definition/NamespacedTest.php @@ -3,7 +3,7 @@ declare(strict_types = 1); namespace LanguageServer\Tests\Server\TextDocument\Definition; -use LanguageServer\Protocol\{TextDocumentIdentifier, Location}; +use LanguageServerProtocol\{TextDocumentIdentifier, Location}; use function LanguageServer\pathToUri; class NamespacedTest extends GlobalTest diff --git a/tests/Server/TextDocument/DidChangeTest.php b/tests/Server/TextDocument/DidChangeTest.php index 4d26ed8..0f92ed7 100644 --- a/tests/Server/TextDocument/DidChangeTest.php +++ b/tests/Server/TextDocument/DidChangeTest.php @@ -10,7 +10,7 @@ use LanguageServer\{ }; use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex}; -use LanguageServer\Protocol\{ +use LanguageServerProtocol\{ VersionedTextDocumentIdentifier, TextDocumentContentChangeEvent, Range, diff --git a/tests/Server/TextDocument/DidCloseTest.php b/tests/Server/TextDocument/DidCloseTest.php index 42daec5..22eb534 100644 --- a/tests/Server/TextDocument/DidCloseTest.php +++ b/tests/Server/TextDocument/DidCloseTest.php @@ -10,7 +10,7 @@ use LanguageServer\{ }; use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex}; -use LanguageServer\Protocol\{TextDocumentItem, TextDocumentIdentifier}; +use LanguageServerProtocol\{TextDocumentItem, TextDocumentIdentifier}; class DidCloseTest extends TestCase { diff --git a/tests/Server/TextDocument/DocumentSymbolTest.php b/tests/Server/TextDocument/DocumentSymbolTest.php index 155e4a2..1267fd0 100644 --- a/tests/Server/TextDocument/DocumentSymbolTest.php +++ b/tests/Server/TextDocument/DocumentSymbolTest.php @@ -6,7 +6,7 @@ namespace LanguageServer\Tests\Server\TextDocument; use LanguageServer\Tests\Server\ServerTestCase; use LanguageServer\Tests\MockProtocolStream; use LanguageServer\{Server, LanguageClient, Project}; -use LanguageServer\Protocol\{TextDocumentIdentifier, SymbolInformation, SymbolKind, Position, Location, Range}; +use LanguageServerProtocol\{TextDocumentIdentifier, SymbolInformation, SymbolKind, Position, Location, Range}; use function LanguageServer\pathToUri; class DocumentSymbolTest extends ServerTestCase @@ -32,7 +32,9 @@ class DocumentSymbolTest extends ServerTestCase new SymbolInformation('ChildClass', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\ChildClass'), 'TestNamespace'), new SymbolInformation('Example', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\Example'), 'TestNamespace'), new SymbolInformation('__construct', SymbolKind::CONSTRUCTOR, $this->getDefinitionLocation('TestNamespace\\Example::__construct'), 'TestNamespace\\Example'), - new SymbolInformation('__destruct', SymbolKind::CONSTRUCTOR, $this->getDefinitionLocation('TestNamespace\\Example::__destruct'), 'TestNamespace\\Example') + new SymbolInformation('__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); // @codingStandardsIgnoreEnd } diff --git a/tests/Server/TextDocument/HoverTest.php b/tests/Server/TextDocument/HoverTest.php index 2ba49e1..42b69c8 100644 --- a/tests/Server/TextDocument/HoverTest.php +++ b/tests/Server/TextDocument/HoverTest.php @@ -6,7 +6,7 @@ namespace LanguageServer\Tests\Server\TextDocument; use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\Server\ServerTestCase; use LanguageServer\{Server, LanguageClient, Project}; -use LanguageServer\Protocol\{TextDocumentIdentifier, Position, Range, Hover, MarkedString}; +use LanguageServerProtocol\{TextDocumentIdentifier, Position, Range, Hover, MarkedString}; use function LanguageServer\pathToUri; class HoverTest extends ServerTestCase diff --git a/tests/Server/TextDocument/ParseErrorsTest.php b/tests/Server/TextDocument/ParseErrorsTest.php index 4428523..09cdc98 100644 --- a/tests/Server/TextDocument/ParseErrorsTest.php +++ b/tests/Server/TextDocument/ParseErrorsTest.php @@ -10,7 +10,7 @@ use LanguageServer\{ }; use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex}; use LanguageServer\ContentRetriever\FileSystemContentRetriever; -use LanguageServer\Protocol\{TextDocumentItem, DiagnosticSeverity}; +use LanguageServerProtocol\{TextDocumentItem, DiagnosticSeverity}; use Sabre\Event\Promise; use JsonMapper; diff --git a/tests/Server/TextDocument/References/GlobalFallbackTest.php b/tests/Server/TextDocument/References/GlobalFallbackTest.php index abfefce..b1cbdac 100644 --- a/tests/Server/TextDocument/References/GlobalFallbackTest.php +++ b/tests/Server/TextDocument/References/GlobalFallbackTest.php @@ -10,7 +10,7 @@ use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\Index\{ DependenciesIndex, Index, ProjectIndex }; -use LanguageServer\Protocol\{ +use LanguageServerProtocol\{ Location, Position, Range, ReferenceContext, TextDocumentIdentifier }; use LanguageServer\Tests\MockProtocolStream; diff --git a/tests/Server/TextDocument/References/GlobalTest.php b/tests/Server/TextDocument/References/GlobalTest.php index 105dfef..d2550c8 100644 --- a/tests/Server/TextDocument/References/GlobalTest.php +++ b/tests/Server/TextDocument/References/GlobalTest.php @@ -3,7 +3,7 @@ declare(strict_types = 1); namespace LanguageServer\Tests\Server\TextDocument\References; -use LanguageServer\Protocol\{TextDocumentIdentifier, Position, ReferenceContext, Location, Range}; +use LanguageServerProtocol\{TextDocumentIdentifier, Position, ReferenceContext, Location, Range}; use LanguageServer\Tests\Server\ServerTestCase; use function LanguageServer\pathToUri; diff --git a/tests/Server/TextDocument/References/NamespacedTest.php b/tests/Server/TextDocument/References/NamespacedTest.php index 1ee1ab4..b2bb9bb 100644 --- a/tests/Server/TextDocument/References/NamespacedTest.php +++ b/tests/Server/TextDocument/References/NamespacedTest.php @@ -3,7 +3,7 @@ declare(strict_types = 1); namespace LanguageServer\Tests\Server\TextDocument\References; -use LanguageServer\Protocol\{TextDocumentIdentifier, Position, ReferenceContext, Location, Range}; +use LanguageServerProtocol\{TextDocumentIdentifier, Position, ReferenceContext, Location, Range}; use function LanguageServer\pathToUri; class NamespacedTest extends GlobalTest diff --git a/tests/Server/TextDocument/SignatureHelpTest.php b/tests/Server/TextDocument/SignatureHelpTest.php index 2004aae..83dcb2c 100644 --- a/tests/Server/TextDocument/SignatureHelpTest.php +++ b/tests/Server/TextDocument/SignatureHelpTest.php @@ -10,7 +10,7 @@ use LanguageServer\{ }; use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex}; use LanguageServer\ContentRetriever\FileSystemContentRetriever; -use LanguageServer\Protocol\{ +use LanguageServerProtocol\{ TextDocumentIdentifier, TextEdit, Range, diff --git a/tests/Server/Workspace/DidChangeWatchedFilesTest.php b/tests/Server/Workspace/DidChangeWatchedFilesTest.php index 1074c58..1f57acd 100644 --- a/tests/Server/Workspace/DidChangeWatchedFilesTest.php +++ b/tests/Server/Workspace/DidChangeWatchedFilesTest.php @@ -6,7 +6,8 @@ namespace LanguageServer\Tests\Server\Workspace; use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\{DefinitionResolver, LanguageClient, PhpDocumentLoader, Server}; use LanguageServer\Index\{DependenciesIndex, Index, ProjectIndex}; -use LanguageServer\Protocol\{FileChangeType, FileEvent, Message}; +use LanguageServerProtocol\{FileChangeType, FileEvent}; +use LanguageServer\Message; use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\Server\ServerTestCase; use LanguageServer\Server\Workspace; diff --git a/tests/Server/Workspace/SymbolTest.php b/tests/Server/Workspace/SymbolTest.php index 765841b..74fc92e 100644 --- a/tests/Server/Workspace/SymbolTest.php +++ b/tests/Server/Workspace/SymbolTest.php @@ -6,7 +6,7 @@ namespace LanguageServer\Tests\Server\Workspace; use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\Server\ServerTestCase; use LanguageServer\{Server, Client, LanguageClient, Project, PhpDocument}; -use LanguageServer\Protocol\{ +use LanguageServerProtocol\{ TextDocumentItem, TextDocumentIdentifier, SymbolInformation, @@ -30,7 +30,7 @@ class SymbolTest extends ServerTestCase // @codingStandardsIgnoreStart $this->assertEquals([ - new SymbolInformation('TestNamespace', SymbolKind::NAMESPACE, new Location($referencesUri, new Range(new Position(2, 0), new Position(2, 24))), ''), + new SymbolInformation('TestNamespace', SymbolKind::NAMESPACE, new Location($referencesUri, new Range(new Position(2, 0), new Position(2, 24))), ''), // Namespaced new SymbolInformation('TEST_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TestNamespace\\TEST_CONST'), 'TestNamespace'), new SymbolInformation('TestClass', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\TestClass'), 'TestNamespace'), @@ -46,6 +46,8 @@ class SymbolTest extends ServerTestCase new SymbolInformation('Example', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\Example'), 'TestNamespace'), new SymbolInformation('__construct', SymbolKind::CONSTRUCTOR, $this->getDefinitionLocation('TestNamespace\\Example::__construct'), 'TestNamespace\\Example'), new SymbolInformation('__destruct', SymbolKind::CONSTRUCTOR, $this->getDefinitionLocation('TestNamespace\\Example::__destruct'), 'TestNamespace\\Example'), + new SymbolInformation('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'), // Global new SymbolInformation('TEST_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TEST_CONST'), ''), diff --git a/tests/Validation/ValidationTest.php b/tests/Validation/ValidationTest.php index 9057c9d..2c3efaf 100644 --- a/tests/Validation/ValidationTest.php +++ b/tests/Validation/ValidationTest.php @@ -13,7 +13,7 @@ use phpDocumentor\Reflection\DocBlock; use phpDocumentor\Reflection\DocBlockFactory; use PHPUnit\Framework\TestCase; use LanguageServer\ClientHandler; -use LanguageServer\Protocol\Message; +use LanguageServer\Message; use AdvancedJsonRpc; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; @@ -64,7 +64,7 @@ class ValidationTest extends TestCase try { $this->assertEquals($expectedValues['definitions'], $actualValues['definitions']); - $this->assertEquals((array)$expectedValues['references'], (array)$actualValues['references'], 'references don\'t match.'); + $this->assertEquals((array)$expectedValues['references'], (array)$actualValues['references'], sprintf('references match in "%s"', $outputFile)); } catch (\Throwable $e) { $outputFile = getExpectedValuesFile($testCaseFile); file_put_contents($outputFile, json_encode($actualValues, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)); diff --git a/tests/Validation/cases/namespaces5.php b/tests/Validation/cases/namespaces5.php index 85ee4b5..d4a37b2 100644 --- a/tests/Validation/cases/namespaces5.php +++ b/tests/Validation/cases/namespaces5.php @@ -1,3 +1,3 @@