1
0
Fork 0

Merge remote-tracking branch 'origin/master' into master_188

Conflicts:
	src/LanguageServer.php
pull/189/head
Michal Niewrzal 2016-12-15 09:47:59 +01:00
commit ffb5d76927
43 changed files with 1530 additions and 883 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@
vendor/ vendor/
.phpls/ .phpls/
composer.lock composer.lock
stubs

View File

@ -8,10 +8,11 @@ services:
cache: cache:
directories: directories:
- vendor - $HOME/.composer/cache
install: install:
- composer install - composer install
- composer run-script parse-stubs
script: script:
- vendor/bin/phpcs -n - vendor/bin/phpcs -n

View File

@ -49,6 +49,10 @@ Non-Standard: An empty query will return _all_ symbols found in the workspace.
PHP parse errors are reported as errors, parse errors of docblocks are reported as warnings. PHP parse errors are reported as errors, parse errors of docblocks are reported as warnings.
Errors/Warnings from the `vendor` directory are ignored. Errors/Warnings from the `vendor` directory are ignored.
### Stubs for PHP built-ins
Completion, type resolval etc. will use the standard PHP library and common extensions.
### What is considered a definition? ### What is considered a definition?
Globally searchable definitions are: Globally searchable definitions are:
@ -131,6 +135,11 @@ Simply run
and you will get the latest stable release and all dependencies. and you will get the latest stable release and all dependencies.
Running `composer update` will update the server to the latest non-breaking version. Running `composer update` will update the server to the latest non-breaking version.
After installing the language server and its dependencies,
you must parse the stubs for standard PHP symbols and save the index for fast initialization.
composer run-script --working-dir=vendor/felixfbecker/language-server parse-stubs
## Running ## Running
Start the language server with Start the language server with
@ -178,6 +187,9 @@ Clone the repository and run
composer install composer install
to install dependencies. to install dependencies.
Then parse the stubs with
composer run-script parse-stubs
Run the tests with Run the tests with

View File

@ -22,9 +22,12 @@
"refactor" "refactor"
], ],
"bin": ["bin/php-language-server.php"], "bin": ["bin/php-language-server.php"],
"scripts": {
"parse-stubs": "LanguageServer\\ComposerScripts::parseStubs"
},
"require": { "require": {
"php": ">=7.0", "php": ">=7.0",
"nikic/php-parser": "dev-master#e52ffc4447e034514339a03b450aab9cd625e37c", "nikic/php-parser": "^3.0",
"phpdocumentor/reflection-docblock": "^3.0", "phpdocumentor/reflection-docblock": "^3.0",
"sabre/event": "^5.0", "sabre/event": "^5.0",
"felixfbecker/advanced-json-rpc": "^2.0", "felixfbecker/advanced-json-rpc": "^2.0",
@ -32,8 +35,27 @@
"netresearch/jsonmapper": "^1.0", "netresearch/jsonmapper": "^1.0",
"webmozart/path-util": "^2.3", "webmozart/path-util": "^2.3",
"webmozart/glob": "^4.1", "webmozart/glob": "^4.1",
"sabre/uri": "^2.0" "sabre/uri": "^2.0",
"JetBrains/phpstorm-stubs": "dev-master"
}, },
"repositories": [
{
"type": "package",
"package": {
"name": "JetBrains/phpstorm-stubs",
"version": "dev-master",
"dist": {
"url": "https://github.com/JetBrains/phpstorm-stubs/archive/master.zip",
"type": "zip"
},
"source": {
"url": "https://github.com/JetBrains/phpstorm-stubs",
"type": "git",
"reference": "master"
}
}
}
],
"minimum-stability": "dev", "minimum-stability": "dev",
"prefer-stable": true, "prefer-stable": true,
"autoload": { "autoload": {

View File

@ -2,7 +2,7 @@
namespace TestNamespace; namespace TestNamespace;
$obj = new TestClass(); $obj = new TestClass($a, $b, $c);
$obj->testMethod(); $obj->testMethod();
echo $obj->testProperty; echo $obj->testProperty;
TestClass::staticTestMethod(); TestClass::staticTestMethod();

View File

@ -4,12 +4,11 @@ declare(strict_types = 1);
namespace LanguageServer; namespace LanguageServer;
use PhpParser\Node; use PhpParser\Node;
use phpDocumentor\Reflection\Types; use LanguageServer\Index\ReadableIndex;
use LanguageServer\Protocol\{ use LanguageServer\Protocol\{
TextEdit, TextEdit,
Range, Range,
Position, Position,
SymbolKind,
CompletionList, CompletionList,
CompletionItem, CompletionItem,
CompletionItemKind CompletionItemKind
@ -99,13 +98,18 @@ class CompletionProvider
private $project; private $project;
/** /**
* @param DefinitionResolver $definitionResolver * @var ReadableIndex
* @param Project $project
*/ */
public function __construct(DefinitionResolver $definitionResolver, Project $project) private $index;
/**
* @param DefinitionResolver $definitionResolver
* @param ReadableIndex $index
*/
public function __construct(DefinitionResolver $definitionResolver, ReadableIndex $index)
{ {
$this->definitionResolver = $definitionResolver; $this->definitionResolver = $definitionResolver;
$this->project = $project; $this->index = $index;
} }
/** /**
@ -134,7 +138,6 @@ class CompletionProvider
|| $node instanceof Node\Expr\StaticPropertyFetch || $node instanceof Node\Expr\StaticPropertyFetch
|| $node instanceof Node\Expr\ClassConstFetch || $node instanceof Node\Expr\ClassConstFetch
) { ) {
if (!is_string($node->name)) {
// If the name is an Error node, just filter by the class // If the name is an Error node, just filter by the class
if ($node instanceof Node\Expr\MethodCall || $node instanceof Node\Expr\PropertyFetch) { if ($node instanceof Node\Expr\MethodCall || $node instanceof Node\Expr\PropertyFetch) {
// For instances, resolve the variable type // For instances, resolve the variable type
@ -155,12 +158,8 @@ class CompletionProvider
$prefix .= '::$'; $prefix .= '::$';
} }
} }
} else {
$fqn = $this->definitionResolver->resolveReferenceNodeToFqn($node);
$prefixes = $fqn !== null ? [$fqn] : [];
}
foreach ($this->project->getDefinitions() as $fqn => $def) { foreach ($this->index->getDefinitions() as $fqn => $def) {
foreach ($prefixes as $prefix) { foreach ($prefixes as $prefix) {
if (substr($fqn, 0, strlen($prefix)) === $prefix && !$def->isGlobal) { if (substr($fqn, 0, strlen($prefix)) === $prefix && !$def->isGlobal) {
$list->items[] = CompletionItem::fromDefinition($def); $list->items[] = CompletionItem::fromDefinition($def);
@ -192,7 +191,9 @@ class CompletionProvider
// Get the definition for the used namespace, class-like, function or constant // Get the definition for the used namespace, class-like, function or constant
// And save it under the alias // And save it under the alias
$fqn = (string)Node\Name::concat($stmt->prefix ?? null, $use->name); $fqn = (string)Node\Name::concat($stmt->prefix ?? null, $use->name);
$aliasedDefs[$use->alias] = $this->project->getDefinition($fqn); if ($def = $this->index->getDefinition($fqn)) {
$aliasedDefs[$use->alias] = $def;
}
} }
} else { } else {
// Use statements are always the first statements in a namespace // Use statements are always the first statements in a namespace
@ -213,7 +214,7 @@ class CompletionProvider
// Additionally, suggest global symbols that either // Additionally, suggest global symbols that either
// - start with the current namespace + prefix, if the Name node is not fully qualified // - 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 // - start with just the prefix, if the Name node is fully qualified
foreach ($this->project->getDefinitions() as $fqn => $def) { foreach ($this->index->getDefinitions() as $fqn => $def) {
if ( if (
$def->isGlobal // exclude methods, properties etc. $def->isGlobal // exclude methods, properties etc.
&& ( && (
@ -333,7 +334,7 @@ class CompletionProvider
} }
if ($level instanceof Node\Expr\Closure) { if ($level instanceof Node\Expr\Closure) {
foreach ($level->uses as $use) { foreach ($level->uses as $use) {
if (!isset($vars[$param->name]) && substr($param->name, 0, strlen($namePrefix)) === $namePrefix) { if (!isset($vars[$use->var]) && substr($use->var, 0, strlen($namePrefix)) === $namePrefix) {
$vars[$use->var] = $use; $vars[$use->var] = $use;
} }
} }

57
src/ComposerScripts.php Normal file
View File

@ -0,0 +1,57 @@
<?php
declare(strict_types = 1);
namespace LanguageServer;
use LanguageServer\FilesFinder\FileSystemFilesFinder;
use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Index\StubsIndex;
use phpDocumentor\Reflection\DocBlockFactory;
use Webmozart\PathUtil\Path;
use Sabre\Uri;
use function Sabre\Event\coroutine;
require_once __DIR__ . '/../vendor/sabre/event/lib/coroutine.php';
require_once __DIR__ . '/../vendor/sabre/event/lib/Loop/functions.php';
require_once __DIR__ . '/../vendor/sabre/event/lib/Promise/functions.php';
require_once __DIR__ . '/../vendor/sabre/uri/lib/functions.php';
require_once __DIR__ . '/utils.php';
class ComposerScripts
{
public static function parseStubs()
{
coroutine(function () {
$index = new StubsIndex;
$finder = new FileSystemFilesFinder;
$contentRetriever = new FileSystemContentRetriever;
$docBlockFactory = DocBlockFactory::createInstance();
$parser = new Parser;
$definitionResolver = new DefinitionResolver($index);
$stubsLocation = Path::canonicalize(__DIR__ . '/../vendor/JetBrains/phpstorm-stubs');
$uris = yield $finder->find("$stubsLocation/**/*.php");
foreach ($uris as $uri) {
echo "Parsing $uri\n";
$content = yield $contentRetriever->retrieve($uri);
// Change URI to phpstubs://
$parts = Uri\parse($uri);
$parts['path'] = Path::makeRelative($parts['path'], $stubsLocation);
$parts['scheme'] = 'phpstubs';
$uri = Uri\build($parts);
$document = new PhpDocument($uri, $content, $index, $parser, $docBlockFactory, $definitionResolver);
}
echo "Saving Index\n";
$index->save();
echo "Finished\n";
})->wait();
}
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\ContentRetriever;
use LanguageServer\LanguageClient;
use LanguageServer\Protocol\{TextDocumentIdentifier, TextDocumentItem};
use Sabre\Event\Promise;
/**
* Retrieves file content from the client through a textDocument/xcontent request
*/
class ClientContentRetriever implements ContentRetriever
{
/**
* @param LanguageClient $client
*/
public function __construct(LanguageClient $client)
{
$this->client = $client;
}
/**
* Retrieves the content of a text document identified by the URI through a textDocument/xcontent request
*
* @param string $uri The URI of the document
* @return Promise <string> Resolved with the content as a string
*/
public function retrieve(string $uri): Promise
{
return $this->client->textDocument->xcontent(new TextDocumentIdentifier($uri))
->then(function (TextDocumentItem $textDocument) {
return $textDocument->text;
});
}
}

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\ContentRetriever;
use Sabre\Event\Promise;
/**
* Interface for retrieving the content of a text document
*/
interface ContentRetriever
{
/**
* Retrieves the content of a text document identified by the URI
*
* @param string $uri The URI of the document
* @return Promise <string> Resolved with the content as a string
*/
public function retrieve(string $uri): Promise;
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\ContentRetriever;
use Sabre\Event\Promise;
use function LanguageServer\uriToPath;
/**
* Retrieves document content from the file system
*/
class FileSystemContentRetriever implements ContentRetriever
{
/**
* Retrieves the content of a text document identified by the URI from the file system
*
* @param string $uri The URI of the document
* @return Promise <string> Resolved with the content as a string
*/
public function retrieve(string $uri): Promise
{
return Promise\resolve(file_get_contents(uriToPath($uri)));
}
}

View File

@ -7,15 +7,14 @@ use PhpParser\Node;
use PhpParser\PrettyPrinter\Standard as PrettyPrinter; use PhpParser\PrettyPrinter\Standard as PrettyPrinter;
use phpDocumentor\Reflection\{Types, Type, Fqsen, TypeResolver}; use phpDocumentor\Reflection\{Types, Type, Fqsen, TypeResolver};
use LanguageServer\Protocol\SymbolInformation; use LanguageServer\Protocol\SymbolInformation;
use Sabre\Event\Promise; use LanguageServer\Index\ReadableIndex;
use function Sabre\Event\coroutine;
class DefinitionResolver class DefinitionResolver
{ {
/** /**
* @var \LanguageServer\Project * @var \LanguageServer\Index
*/ */
private $project; private $index;
/** /**
* @var \phpDocumentor\Reflection\TypeResolver * @var \phpDocumentor\Reflection\TypeResolver
@ -27,9 +26,12 @@ class DefinitionResolver
*/ */
private $prettyPrinter; private $prettyPrinter;
public function __construct(Project $project) /**
* @param ReadableIndex $index
*/
public function __construct(ReadableIndex $index)
{ {
$this->project = $project; $this->index = $index;
$this->typeResolver = new TypeResolver; $this->typeResolver = new TypeResolver;
$this->prettyPrinter = new PrettyPrinter; $this->prettyPrinter = new PrettyPrinter;
} }
@ -147,8 +149,8 @@ class DefinitionResolver
// http://php.net/manual/en/language.namespaces.fallback.php // http://php.net/manual/en/language.namespaces.fallback.php
$parent = $node->getAttribute('parentNode'); $parent = $node->getAttribute('parentNode');
$globalFallback = $parent instanceof Node\Expr\ConstFetch || $parent instanceof Node\Expr\FuncCall; $globalFallback = $parent instanceof Node\Expr\ConstFetch || $parent instanceof Node\Expr\FuncCall;
// Return the Definition object from the project index // Return the Definition object from the index index
return $this->project->getDefinition($fqn, $globalFallback); return $this->index->getDefinition($fqn, $globalFallback);
} }
/** /**
@ -193,6 +195,7 @@ class DefinitionResolver
|| $parent instanceof Node\Namespace_ || $parent instanceof Node\Namespace_
|| $parent instanceof Node\Param || $parent instanceof Node\Param
|| $parent instanceof Node\FunctionLike || $parent instanceof Node\FunctionLike
|| $parent instanceof Node\Expr\New_
|| $parent instanceof Node\Expr\StaticCall || $parent instanceof Node\Expr\StaticCall
|| $parent instanceof Node\Expr\ClassConstFetch || $parent instanceof Node\Expr\ClassConstFetch
|| $parent instanceof Node\Expr\StaticPropertyFetch || $parent instanceof Node\Expr\StaticPropertyFetch
@ -210,13 +213,6 @@ class DefinitionResolver
} else if ($grandParent instanceof Node\Stmt\Use_ && $grandParent->type === Node\Stmt\Use_::TYPE_FUNCTION) { } else if ($grandParent instanceof Node\Stmt\Use_ && $grandParent->type === Node\Stmt\Use_::TYPE_FUNCTION) {
$name .= '()'; $name .= '()';
} }
// Only the name node should be considered a reference, not the New_ node itself
} else if ($parent instanceof Node\Expr\New_) {
if (!($parent->class instanceof Node\Name)) {
// Cannot get definition of dynamic calls
return null;
}
$name = (string)$parent->class;
} else if ($node instanceof Node\Expr\MethodCall || $node instanceof Node\Expr\PropertyFetch) { } else if ($node instanceof Node\Expr\MethodCall || $node instanceof Node\Expr\PropertyFetch) {
if ($node->name instanceof Node\Expr) { if ($node->name instanceof Node\Expr) {
// Cannot get definition if right-hand side is expression // Cannot get definition if right-hand side is expression
@ -403,7 +399,7 @@ class DefinitionResolver
return new Types\Mixed; return new Types\Mixed;
} }
$fqn = (string)($expr->getAttribute('namespacedName') ?? $expr->name); $fqn = (string)($expr->getAttribute('namespacedName') ?? $expr->name);
$def = $this->project->getDefinition($fqn, true); $def = $this->index->getDefinition($fqn, true);
if ($def !== null) { if ($def !== null) {
return $def->type; return $def->type;
} }
@ -414,7 +410,7 @@ class DefinitionResolver
} }
// Resolve constant // Resolve constant
$fqn = (string)($expr->getAttribute('namespacedName') ?? $expr->name); $fqn = (string)($expr->getAttribute('namespacedName') ?? $expr->name);
$def = $this->project->getDefinition($fqn, true); $def = $this->index->getDefinition($fqn, true);
if ($def !== null) { if ($def !== null) {
return $def->type; return $def->type;
} }
@ -443,7 +439,7 @@ class DefinitionResolver
if ($expr instanceof Node\Expr\MethodCall) { if ($expr instanceof Node\Expr\MethodCall) {
$fqn .= '()'; $fqn .= '()';
} }
$def = $this->project->getDefinition($fqn); $def = $this->index->getDefinition($fqn);
if ($def !== null) { if ($def !== null) {
return $def->type; return $def->type;
} }
@ -466,7 +462,7 @@ class DefinitionResolver
if ($expr instanceof Node\Expr\StaticCall) { if ($expr instanceof Node\Expr\StaticCall) {
$fqn .= '()'; $fqn .= '()';
} }
$def = $this->project->getDefinition($fqn); $def = $this->index->getDefinition($fqn);
if ($def === null) { if ($def === null) {
return new Types\Mixed; return new Types\Mixed;
} }

View File

@ -0,0 +1,49 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\FilesFinder;
use LanguageServer\LanguageClient;
use Sabre\Event\Promise;
use Sabre\Uri;
use Webmozart\Glob\Glob;
/**
* Retrieves file content from the client through a textDocument/xcontent request
*/
class ClientFilesFinder implements FilesFinder
{
/**
* @var LanguageClient
*/
private $client;
/**
* @param LanguageClient $client
*/
public function __construct(LanguageClient $client)
{
$this->client = $client;
}
/**
* Returns all files in the workspace that match a glob.
* If the client does not support workspace/files, it falls back to searching the file system directly.
*
* @param string $glob
* @return Promise <string[]> The URIs
*/
public function find(string $glob): Promise
{
return $this->client->workspace->xfiles()->then(function (array $textDocuments) use ($glob) {
$uris = [];
foreach ($textDocuments as $textDocument) {
$path = Uri\parse($textDocument->uri)['path'];
if (Glob::match($path, $glob)) {
$uris[] = $textDocument->uri;
}
}
return $uris;
});
}
}

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\FilesFinder;
use Webmozart\Glob\Iterator\GlobIterator;
use Sabre\Event\Promise;
use function Sabre\Event\coroutine;
use function LanguageServer\{pathToUri, timeout};
class FileSystemFilesFinder implements FilesFinder
{
/**
* Returns all files in the workspace that match a glob.
* If the client does not support workspace/xfiles, it falls back to searching the file system directly.
*
* @param string $glob
* @return Promise <string[]>
*/
public function find(string $glob): Promise
{
return coroutine(function () use ($glob) {
$uris = [];
foreach (new GlobIterator($glob) as $path) {
$uris[] = pathToUri($path);
yield timeout();
}
return $uris;
});
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\FilesFinder;
use Sabre\Event\Promise;
/**
* Interface for finding files in the workspace
*/
interface FilesFinder
{
/**
* Returns all files in the workspace that match a glob.
* If the client does not support workspace/xfiles, it falls back to searching the file system directly.
*
* @param string $glob
* @return Promise <string[]>
*/
public function find(string $glob): Promise;
}

View File

@ -0,0 +1,66 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Index;
use LanguageServer\Definition;
abstract class AbstractAggregateIndex implements ReadableIndex
{
/**
* Returns all indexes managed by the aggregate index
*
* @return ReadableIndex[]
*/
abstract protected function getIndexes(): array;
/**
* Returns an associative array [string => Definition] that maps fully qualified symbol names
* to Definitions
*
* @return Definition[]
*/
public function getDefinitions(): array
{
$defs = [];
foreach ($this->getIndexes() as $index) {
foreach ($index->getDefinitions() as $fqn => $def) {
$defs[$fqn] = $def;
}
}
return $defs;
}
/**
* Returns the Definition object by a specific FQN
*
* @param string $fqn
* @param bool $globalFallback Whether to fallback to global if the namespaced FQN was not found
* @return Definition|null
*/
public function getDefinition(string $fqn, bool $globalFallback = false)
{
foreach ($this->getIndexes() as $index) {
if ($def = $index->getDefinition($fqn, $globalFallback)) {
return $def;
}
}
}
/**
* Returns all URIs in this index that reference a symbol
*
* @param string $fqn The fully qualified name of the symbol
* @return string[]
*/
public function getReferenceUris(string $fqn): array
{
$refs = [];
foreach ($this->getIndexes() as $index) {
foreach ($index->getReferenceUris($fqn) as $ref) {
$refs[] = $ref;
}
}
return $refs;
}
}

View File

@ -0,0 +1,52 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Index;
class DependenciesIndex extends AbstractAggregateIndex
{
/**
* Map from package name to index
*
* @var Index[]
*/
protected $indexes = [];
/**
* @return Index[]
*/
protected function getIndexes(): array
{
return $this->indexes;
}
/**
* @param string $packageName
* @return Index
*/
public function getDependencyIndex(string $packageName): Index
{
if (!isset($this->indexes[$packageName])) {
$this->indexes[$packageName] = new Index;
}
return $this->indexes[$packageName];
}
/**
* @param string $packageName
* @return void
*/
public function removeDependencyIndex(string $packageName)
{
unset($this->indexes[$packageName]);
}
/**
* @param string $packageName
* @return bool
*/
public function hasDependencyIndex(string $packageName): bool
{
return isset($this->indexes[$packageName]);
}
}

38
src/Index/GlobalIndex.php Normal file
View File

@ -0,0 +1,38 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Index;
/**
* Aggregates definitions of the project and stubs
*/
class GlobalIndex extends AbstractAggregateIndex
{
/**
* @var Index
*/
private $stubsIndex;
/**
* @var ProjectIndex
*/
private $projectIndex;
/**
* @param StubsIndex $stubsIndex
* @param ProjectIndex $projectIndex
*/
public function __construct(StubsIndex $stubsIndex, ProjectIndex $projectIndex)
{
$this->stubsIndex = $stubsIndex;
$this->projectIndex = $projectIndex;
}
/**
* @return ReadableIndex[]
*/
protected function getIndexes(): array
{
return [$this->stubsIndex, $this->projectIndex];
}
}

129
src/Index/Index.php Normal file
View File

@ -0,0 +1,129 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Index;
use LanguageServer\Definition;
/**
* Represents the index of a project or dependency
* Serializable for caching
*/
class Index implements ReadableIndex
{
/**
* An associative array that maps fully qualified symbol names to Definitions
*
* @var Definition[]
*/
private $definitions = [];
/**
* An associative array that maps fully qualified symbol names to arrays of document URIs that reference the symbol
*
* @var string[][]
*/
private $references = [];
/**
* Returns an associative array [string => Definition] that maps fully qualified symbol names
* to Definitions
*
* @return Definition[]
*/
public function getDefinitions(): array
{
return $this->definitions;
}
/**
* Returns the Definition object by a specific FQN
*
* @param string $fqn
* @param bool $globalFallback Whether to fallback to global if the namespaced FQN was not found
* @return Definition|null
*/
public function getDefinition(string $fqn, bool $globalFallback = false)
{
if (isset($this->definitions[$fqn])) {
return $this->definitions[$fqn];
}
if ($globalFallback) {
$parts = explode('\\', $fqn);
$fqn = end($parts);
return $this->getDefinition($fqn);
}
}
/**
* Registers a definition
*
* @param string $fqn The fully qualified name of the symbol
* @param string $definition The Definition object
* @return void
*/
public function setDefinition(string $fqn, Definition $definition)
{
$this->definitions[$fqn] = $definition;
}
/**
* Unsets the Definition for a specific symbol
* and removes all references pointing to that symbol
*
* @param string $fqn The fully qualified name of the symbol
* @return void
*/
public function removeDefinition(string $fqn)
{
unset($this->definitions[$fqn]);
unset($this->references[$fqn]);
}
/**
* Returns all URIs in this index that reference a symbol
*
* @param string $fqn The fully qualified name of the symbol
* @return string[]
*/
public function getReferenceUris(string $fqn): array
{
return $this->references[$fqn] ?? [];
}
/**
* Adds a document URI as a referencee of a specific symbol
*
* @param string $fqn The fully qualified name of the symbol
* @return void
*/
public function addReferenceUri(string $fqn, string $uri)
{
if (!isset($this->references[$fqn])) {
$this->references[$fqn] = [];
}
// TODO: use DS\Set instead of searching array
if (array_search($uri, $this->references[$fqn], true) === false) {
$this->references[$fqn][] = $uri;
}
}
/**
* Removes a document URI as the container for a specific symbol
*
* @param string $fqn The fully qualified name of the symbol
* @param string $uri The URI
* @return void
*/
public function removeReferenceUri(string $fqn, string $uri)
{
if (!isset($this->references[$fqn])) {
return;
}
$index = array_search($fqn, $this->references[$fqn], true);
if ($index === false) {
return;
}
array_splice($this->references[$fqn], $index, 1);
}
}

View File

@ -0,0 +1,51 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Index;
/**
* A project index manages the source and dependency indexes
*/
class ProjectIndex extends AbstractAggregateIndex
{
/**
* The index for dependencies
*
* @var DependenciesIndex
*/
private $dependenciesIndex;
/**
* The Index for the project source
*
* @var Index
*/
private $sourceIndex;
public function __construct(Index $sourceIndex, DependenciesIndex $dependenciesIndex)
{
$this->sourceIndex = $sourceIndex;
$this->dependenciesIndex = $dependenciesIndex;
}
/**
* @return ReadableIndex[]
*/
protected function getIndexes(): array
{
return [$this->sourceIndex, $this->dependenciesIndex];
}
/**
* @param string $uri
* @return Index
*/
public function getIndexForUri(string $uri): Index
{
if (preg_match('/\/vendor\/(\w+\/\w+)\//', $uri, $matches)) {
$packageName = $matches[0];
return $this->dependenciesIndex->getDependencyIndex($packageName);
}
return $this->sourceIndex;
}
}

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Index;
use LanguageServer\Definition;
/**
* The ReadableIndex interface provides methods to lookup definitions and references
*/
interface ReadableIndex
{
/**
* Returns an associative array [string => Definition] that maps fully qualified symbol names
* to Definitions
*
* @return Definitions[]
*/
public function getDefinitions(): array;
/**
* Returns the Definition object by a specific FQN
*
* @param string $fqn
* @param bool $globalFallback Whether to fallback to global if the namespaced FQN was not found
* @return Definition|null
*/
public function getDefinition(string $fqn, bool $globalFallback = false);
/**
* Returns all URIs in this index that reference a symbol
*
* @param string $fqn The fully qualified name of the symbol
* @return string[]
*/
public function getReferenceUris(string $fqn): array;
}

27
src/Index/StubsIndex.php Normal file
View File

@ -0,0 +1,27 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Index;
class StubsIndex extends Index
{
/**
* Reads the serialized StubsIndex from disk
*
* @return self
*/
public static function read()
{
return unserialize(file_get_contents(__DIR__ . '/../../stubs'));
}
/**
* Serializes and saves the StubsIndex
*
* @return void
*/
public function save()
{
file_put_contents(__DIR__ . '/../../stubs', serialize($this));
}
}

View File

@ -10,16 +10,16 @@ use LanguageServer\Protocol\{
Message, Message,
MessageType, MessageType,
InitializeResult, InitializeResult,
TextDocumentIdentifier,
CompletionOptions CompletionOptions
}; };
use LanguageServer\FilesFinder\{FilesFinder, ClientFilesFinder, FileSystemFilesFinder};
use LanguageServer\ContentRetriever\{ContentRetriever, ClientContentRetriever, FileSystemContentRetriever};
use LanguageServer\Index\{DependenciesIndex, GlobalIndex, Index, ProjectIndex, StubsIndex};
use AdvancedJsonRpc; use AdvancedJsonRpc;
use Sabre\Event\Promise; use Sabre\Event\Promise;
use function Sabre\Event\coroutine; use function Sabre\Event\coroutine;
use Exception; use Exception;
use Throwable; use Throwable;
use Webmozart\Glob\Iterator\GlobIterator;
use Webmozart\Glob\Glob;
use Webmozart\PathUtil\Path; use Webmozart\PathUtil\Path;
use Sabre\Uri; use Sabre\Uri;
use function Sabre\Event\Loop\setTimeout; use function Sabre\Event\Loop\setTimeout;
@ -40,28 +40,44 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
*/ */
public $workspace; public $workspace;
public $telemetry; /**
* @var Server\Window
*/
public $window; public $window;
public $telemetry;
public $completionItem; public $completionItem;
public $codeLens; public $codeLens;
/** /**
* ClientCapabilities * @var ProtocolReader
*/ */
private $clientCapabilities;
private $protocolReader; private $protocolReader;
/**
* @var ProtocolWriter
*/
private $protocolWriter; private $protocolWriter;
/**
* @var LanguageClient
*/
private $client; private $client;
/** /**
* The root project path that was passed to initialize() * @var FilesFinder
*
* @var string
*/ */
private $rootPath; private $filesFinder;
private $project;
/**
* @var ContentRetriever
*/
private $contentRetriever;
/**
* @param PotocolReader $reader
* @param ProtocolWriter $writer
*/
public function __construct(ProtocolReader $reader, ProtocolWriter $writer) public function __construct(ProtocolReader $reader, ProtocolWriter $writer)
{ {
parent::__construct($this, '/'); parent::__construct($this, '/');
@ -87,7 +103,7 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
} catch (Throwable $e) { } catch (Throwable $e) {
// If an unexpected error occured, send back an INTERNAL_ERROR error response // If an unexpected error occured, send back an INTERNAL_ERROR error response
$error = new AdvancedJsonRpc\Error( $error = new AdvancedJsonRpc\Error(
$e->getMessage(), (string)$e,
AdvancedJsonRpc\ErrorCode::INTERNAL_ERROR, AdvancedJsonRpc\ErrorCode::INTERNAL_ERROR,
null, null,
$e $e
@ -115,21 +131,52 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
* @param ClientCapabilities $capabilities The capabilities provided by the client (editor) * @param ClientCapabilities $capabilities The capabilities provided by the client (editor)
* @param string|null $rootPath The rootPath of the workspace. Is null if no folder is open. * @param string|null $rootPath The rootPath of the workspace. Is null if no folder is open.
* @param int|null $processId The process Id of the parent process that started the server. Is null if the process has not been started by another process. If the parent process is not alive then the server should exit (see exit notification) its process. * @param int|null $processId The process Id of the parent process that started the server. Is null if the process has not been started by another process. If the parent process is not alive then the server should exit (see exit notification) its process.
* @return InitializeResult * @return Promise <InitializeResult>
*/ */
public function initialize(ClientCapabilities $capabilities, string $rootPath = null, int $processId = null): InitializeResult public function initialize(ClientCapabilities $capabilities, string $rootPath = null, int $processId = null): Promise
{ {
$this->rootPath = $rootPath; return coroutine(function () use ($capabilities, $rootPath, $processId) {
$this->clientCapabilities = $capabilities;
$this->project = new Project($this->client, $capabilities);
$this->textDocument = new Server\TextDocument($this->project, $this->client);
$this->workspace = new Server\Workspace($this->project, $this->client);
// start building project index if ($capabilities->xfilesProvider) {
if ($rootPath !== null) { $this->filesFinder = new ClientFilesFinder($this->client);
$this->indexProject()->otherwise('\\LanguageServer\\crash'); } else {
$this->filesFinder = new FileSystemFilesFinder;
} }
if ($capabilities->xcontentProvider) {
$this->contentRetriever = new ClientContentRetriever($this->client);
} else {
$this->contentRetriever = new FileSystemContentRetriever;
}
$projectIndex = new ProjectIndex(new Index, new DependenciesIndex);
$stubsIndex = StubsIndex::read();
$globalIndex = new GlobalIndex($stubsIndex, $projectIndex);
// The DefinitionResolver should look in stubs, the project source and dependencies
$definitionResolver = new DefinitionResolver($globalIndex);
$this->documentLoader = new PhpDocumentLoader(
$this->contentRetriever,
$projectIndex,
$definitionResolver
);
if ($rootPath !== null) {
$pattern = Path::makeAbsolute('**/*.php', $rootPath);
$uris = yield $this->filesFinder->find($pattern);
$this->index($uris)->otherwise('\\LanguageServer\\crash');
}
$this->textDocument = new Server\TextDocument(
$this->documentLoader,
$definitionResolver,
$this->client,
$globalIndex
);
// workspace/symbol should only look inside the project source and dependencies
$this->workspace = new Server\Workspace($projectIndex, $this->client);
if (extension_loaded('xdebug')) { if (extension_loaded('xdebug')) {
setTimeout(function () { setTimeout(function () {
$this->client->window->showMessage(MessageType::WARNING, 'You are running PHP Language Server with xdebug enabled. This has a major impact on server performance.'); $this->client->window->showMessage(MessageType::WARNING, 'You are running PHP Language Server with xdebug enabled. This has a major impact on server performance.');
@ -157,6 +204,7 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
$serverCapabilities->completionProvider->triggerCharacters = ['$', '>']; $serverCapabilities->completionProvider->triggerCharacters = ['$', '>'];
return new InitializeResult($serverCapabilities); return new InitializeResult($serverCapabilities);
});
} }
/** /**
@ -182,76 +230,55 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
} }
/** /**
* Parses workspace files, one at a time. * Will read and parse the passed source files in the project and add them to the appropiate indexes
* *
* @return Promise <void> * @return Promise <void>
*/ */
private function indexProject(): Promise private function index(array $uris): Promise
{ {
return coroutine(function () { return coroutine(function () use ($uris) {
$textDocuments = yield $this->findPhpFiles();
$count = count($textDocuments); $count = count($uris);
$startTime = microtime(true); $startTime = microtime(true);
foreach ($textDocuments as $i => $textDocument) { foreach (['Collecting definitions and static references', 'Collecting dynamic references'] as $run) {
$this->client->window->logMessage(MessageType::INFO, $run);
foreach ($uris as $i => $uri) {
if ($this->documentLoader->isOpen($uri)) {
continue;
}
// Give LS to the chance to handle requests while indexing // Give LS to the chance to handle requests while indexing
yield timeout(); yield timeout();
$this->client->window->logMessage( $this->client->window->logMessage(
MessageType::LOG, MessageType::LOG,
"Parsing file $i/$count: {$textDocument->uri}" "Parsing file $i/$count: {$uri}"
); );
try { try {
yield $this->project->loadDocument($textDocument->uri); $document = yield $this->documentLoader->load($uri);
if (!$document->isVendored()) {
$this->client->textDocument->publishDiagnostics($uri, $document->getDiagnostics());
}
} catch (ContentTooLargeException $e) { } catch (ContentTooLargeException $e) {
$this->client->window->logMessage( $this->client->window->logMessage(
MessageType::INFO, MessageType::INFO,
"Ignoring file {$textDocument->uri} because it exceeds size limit of {$e->limit} bytes ({$e->size})" "Ignoring file {$uri} because it exceeds size limit of {$e->limit} bytes ({$e->size})"
); );
} catch (Exception $e) { } catch (Exception $e) {
$this->client->window->logMessage( $this->client->window->logMessage(
MessageType::ERROR, MessageType::ERROR,
"Error parsing file {$textDocument->uri}: " . (string)$e "Error parsing file {$uri}: " . (string)$e
); );
} }
} }
$duration = (int)(microtime(true) - $startTime); $duration = (int)(microtime(true) - $startTime);
$mem = (int)(memory_get_usage(true) / (1024 * 1024)); $mem = (int)(memory_get_usage(true) / (1024 * 1024));
$this->client->window->logMessage( $this->client->window->logMessage(
MessageType::INFO, MessageType::INFO,
"All $count PHP files parsed in $duration seconds. $mem MiB allocated." "All $count PHP files parsed in $duration seconds. $mem MiB allocated."
); );
});
} }
/**
* Returns all PHP files in the workspace.
* If the client does not support workspace/files, it falls back to searching the file system directly.
*
* @return Promise <TextDocumentIdentifier[]>
*/
private function findPhpFiles(): Promise
{
return coroutine(function () {
$textDocuments = [];
$pattern = Path::makeAbsolute('**/*.php', $this->rootPath);
if ($this->clientCapabilities->xfilesProvider) {
// Use xfiles request
foreach (yield $this->client->workspace->xfiles() as $textDocument) {
$path = Uri\parse($textDocument->uri)['path'];
if (Glob::match($path, $pattern)) {
$textDocuments[] = $textDocument;
}
}
} else {
// Use the file system
foreach (new GlobIterator($pattern) as $path) {
$textDocuments[] = new TextDocumentIdentifier(pathToUri($path));
yield timeout();
}
}
return $textDocuments;
}); });
} }
} }

View File

@ -19,6 +19,11 @@ class ReferencesCollector extends NodeVisitorAbstract
*/ */
public $nodes = []; public $nodes = [];
/**
* @var DefinitionResolver
*/
private $definitionResolver;
/** /**
* @param DefinitionResolver $definitionResolver The DefinitionResolver to resolve reference nodes to definitions * @param DefinitionResolver $definitionResolver The DefinitionResolver to resolve reference nodes to definitions
*/ */

View File

@ -10,34 +10,16 @@ use LanguageServer\NodeVisitor\{
DocBlockParser, DocBlockParser,
DefinitionCollector, DefinitionCollector,
ColumnCalculator, ColumnCalculator,
ReferencesCollector, ReferencesCollector
VariableReferencesCollector
}; };
use LanguageServer\Index\Index;
use PhpParser\{Error, ErrorHandler, Node, NodeTraverser}; use PhpParser\{Error, ErrorHandler, Node, NodeTraverser};
use PhpParser\NodeVisitor\NameResolver; use PhpParser\NodeVisitor\NameResolver;
use phpDocumentor\Reflection\DocBlockFactory; use phpDocumentor\Reflection\DocBlockFactory;
use Sabre\Event\Promise;
use function Sabre\Event\coroutine;
use Sabre\Uri; use Sabre\Uri;
class PhpDocument class PhpDocument
{ {
/**
* The LanguageClient instance (to report errors etc)
*
* @var LanguageClient
*/
private $client;
/**
* The Project this document belongs to (to register definitions etc)
*
* @var Project
*/
public $project;
// for whatever reason I get "cannot access private property" error if $project is not public
// https://github.com/felixfbecker/php-language-server/pull/49#issuecomment-252427359
/** /**
* The PHPParser instance * The PHPParser instance
* *
@ -59,6 +41,11 @@ class PhpDocument
*/ */
private $definitionResolver; private $definitionResolver;
/**
* @var Index
*/
private $index;
/** /**
* The URI of the document * The URI of the document
* *
@ -101,26 +88,31 @@ class PhpDocument
*/ */
private $referenceNodes; private $referenceNodes;
/**
* Diagnostics for this document that were collected while parsing
*
* @var Diagnostic[]
*/
private $diagnostics;
/** /**
* @param string $uri The URI of the document * @param string $uri The URI of the document
* @param string $content The content of the document * @param string $content The content of the document
* @param Project $project The Project this document belongs to (to register definitions etc) * @param Index $index The Index to register definitions and references to
* @param LanguageClient $client The LanguageClient instance (to report errors etc)
* @param Parser $parser The PHPParser instance * @param Parser $parser The PHPParser instance
* @param DocBlockFactory $docBlockFactory The DocBlockFactory instance to parse docblocks * @param DocBlockFactory $docBlockFactory The DocBlockFactory instance to parse docblocks
* @param DefinitionResolver $definitionResolver The DefinitionResolver to resolve definitions to symbols in the workspace
*/ */
public function __construct( public function __construct(
string $uri, string $uri,
string $content, string $content,
Project $project, Index $index,
LanguageClient $client,
Parser $parser, Parser $parser,
DocBlockFactory $docBlockFactory, DocBlockFactory $docBlockFactory,
DefinitionResolver $definitionResolver DefinitionResolver $definitionResolver
) { ) {
$this->uri = $uri; $this->uri = $uri;
$this->project = $project; $this->index = $index;
$this->client = $client;
$this->parser = $parser; $this->parser = $parser;
$this->docBlockFactory = $docBlockFactory; $this->docBlockFactory = $docBlockFactory;
$this->definitionResolver = $definitionResolver; $this->definitionResolver = $definitionResolver;
@ -154,9 +146,9 @@ class PhpDocument
$errorHandler = new ErrorHandler\Collecting; $errorHandler = new ErrorHandler\Collecting;
$stmts = $this->parser->parse($content, $errorHandler); $stmts = $this->parser->parse($content, $errorHandler);
$diagnostics = []; $this->diagnostics = [];
foreach ($errorHandler->getErrors() as $error) { foreach ($errorHandler->getErrors() as $error) {
$diagnostics[] = Diagnostic::fromError($error, $this->content, DiagnosticSeverity::ERROR, 'php'); $this->diagnostics[] = Diagnostic::fromError($error, $this->content, DiagnosticSeverity::ERROR, 'php');
} }
// $stmts can be null in case of a fatal parsing error // $stmts can be null in case of a fatal parsing error
@ -180,7 +172,7 @@ class PhpDocument
// Report errors from parsing docblocks // Report errors from parsing docblocks
foreach ($docBlockParser->errors as $error) { foreach ($docBlockParser->errors as $error) {
$diagnostics[] = Diagnostic::fromError($error, $this->content, DiagnosticSeverity::WARNING, 'php'); $this->diagnostics[] = Diagnostic::fromError($error, $this->content, DiagnosticSeverity::WARNING, 'php');
} }
$traverser = new NodeTraverser; $traverser = new NodeTraverser;
@ -198,34 +190,30 @@ class PhpDocument
// Unregister old definitions // Unregister old definitions
if (isset($this->definitions)) { if (isset($this->definitions)) {
foreach ($this->definitions as $fqn => $definition) { foreach ($this->definitions as $fqn => $definition) {
$this->project->removeDefinition($fqn); $this->index->removeDefinition($fqn);
} }
} }
// Register this document on the project for all the symbols defined in it // Register this document on the project for all the symbols defined in it
$this->definitions = $definitionCollector->definitions; $this->definitions = $definitionCollector->definitions;
$this->definitionNodes = $definitionCollector->nodes; $this->definitionNodes = $definitionCollector->nodes;
foreach ($definitionCollector->definitions as $fqn => $definition) { foreach ($definitionCollector->definitions as $fqn => $definition) {
$this->project->setDefinition($fqn, $definition); $this->index->setDefinition($fqn, $definition);
} }
// Unregister old references // Unregister old references
if (isset($this->referenceNodes)) { if (isset($this->referenceNodes)) {
foreach ($this->referenceNodes as $fqn => $node) { foreach ($this->referenceNodes as $fqn => $node) {
$this->project->removeReferenceUri($fqn, $this->uri); $this->index->removeReferenceUri($fqn, $this->uri);
} }
} }
// Register this document on the project for references // Register this document on the project for references
$this->referenceNodes = $referencesCollector->nodes; $this->referenceNodes = $referencesCollector->nodes;
foreach ($referencesCollector->nodes as $fqn => $nodes) { foreach ($referencesCollector->nodes as $fqn => $nodes) {
$this->project->addReferenceUri($fqn, $this->uri); $this->index->addReferenceUri($fqn, $this->uri);
} }
$this->stmts = $stmts; $this->stmts = $stmts;
} }
if (!$this->isVendored()) {
$this->client->textDocument->publishDiagnostics($this->uri, $diagnostics);
}
} }
/** /**
@ -262,6 +250,16 @@ class PhpDocument
return $this->content; return $this->content;
} }
/**
* Returns this document's diagnostics
*
* @return Diagnostic[]
*/
public function getDiagnostics()
{
return $this->diagnostics;
}
/** /**
* Returns the URI of the document * Returns the URI of the document
* *
@ -357,57 +355,4 @@ class PhpDocument
{ {
return isset($this->definitions[$fqn]); return isset($this->definitions[$fqn]);
} }
/**
* Returns the reference nodes for any node
* The references node MAY be in other documents, check the ownerDocument attribute
*
* @param Node $node
* @return Promise <Node[]>
*/
public function getReferenceNodesByNode(Node $node): Promise
{
return coroutine(function () use ($node) {
// Variables always stay in the boundary of the file and need to be searched inside their function scope
// by traversing the AST
if (
$node instanceof Node\Expr\Variable
|| $node instanceof Node\Param
|| $node instanceof Node\Expr\ClosureUse
) {
if ($node->name instanceof Node\Expr) {
return null;
}
// Find function/method/closure scope
$n = $node;
while (isset($n) && !($n instanceof Node\FunctionLike)) {
$n = $n->getAttribute('parentNode');
}
if (!isset($n)) {
$n = $node->getAttribute('ownerDocument');
}
$traverser = new NodeTraverser;
$refCollector = new VariableReferencesCollector($node->name);
$traverser->addVisitor($refCollector);
$traverser->traverse($n->getStmts());
return $refCollector->nodes;
}
// Definition with a global FQN
$fqn = DefinitionResolver::getDefinedFqn($node);
if ($fqn === null) {
return [];
}
$refDocuments = yield $this->project->getReferenceDocuments($fqn);
$nodes = [];
foreach ($refDocuments as $document) {
$refs = $document->getReferenceNodesByFqn($fqn);
if ($refs !== null) {
foreach ($refs as $ref) {
$nodes[] = $ref;
}
}
}
return $nodes;
});
}
} }

178
src/PhpDocumentLoader.php Normal file
View File

@ -0,0 +1,178 @@
<?php
declare(strict_types = 1);
namespace LanguageServer;
use LanguageServer\ContentRetriever\ContentRetriever;
use LanguageServer\Index\ProjectIndex;
use phpDocumentor\Reflection\DocBlockFactory;
use Sabre\Event\Promise;
use function Sabre\Event\coroutine;
/**
* Takes care of loading documents and managing "open" documents
*/
class PhpDocumentLoader
{
/**
* A map from URI => PhpDocument of open documents that should be kept in memory
*
* @var PhpDocument
*/
private $documents = [];
/**
* @var ContentRetriever
*/
private $contentRetriever;
/**
* @var ProjectIndex
*/
private $projectIndex;
/**
* @var Parser
*/
private $parser;
/**
* @var DocBlockFactory
*/
private $docBlockFactory;
/**
* @var DefinitionResolver
*/
private $definitionResolver;
/**
* @param ContentRetriever $contentRetriever
* @param ProjectIndex $project
* @param DefinitionResolver $definitionResolver
*/
public function __construct(
ContentRetriever $contentRetriever,
ProjectIndex $projectIndex,
DefinitionResolver $definitionResolver
) {
$this->contentRetriever = $contentRetriever;
$this->projectIndex = $projectIndex;
$this->definitionResolver = $definitionResolver;
$this->parser = new Parser;
$this->docBlockFactory = DocBlockFactory::createInstance();
}
/**
* Returns the document indicated by uri.
* Returns null if the document if not loaded.
*
* @param string $uri
* @return PhpDocument|null
*/
public function get(string $uri)
{
return $this->documents[$uri] ?? null;
}
/**
* Returns the document indicated by uri.
* If the document is not open, loads it.
*
* @param string $uri
* @return Promise <PhpDocument>
*/
public function getOrLoad(string $uri): Promise
{
return isset($this->documents[$uri]) ? Promise\resolve($this->documents[$uri]) : $this->load($uri);
}
/**
* Loads the document by doing a textDocument/xcontent request to the client.
* If the client does not support textDocument/xcontent, tries to read the file from the file system.
* The document is NOT added to the list of open documents, but definitions are registered.
*
* @param string $uri
* @return Promise <PhpDocument>
*/
public function load(string $uri): Promise
{
return coroutine(function () use ($uri) {
$limit = 150000;
$content = yield $this->contentRetriever->retrieve($uri);
$size = strlen($content);
if ($size > $limit) {
throw new ContentTooLargeException($uri, $size, $limit);
}
if (isset($this->documents[$uri])) {
$document = $this->documents[$uri];
$document->updateContent($content);
} else {
$document = $this->create($uri, $content);
}
return $document;
});
}
/**
* Builds a PhpDocument instance
*
* @param string $uri
* @param string $content
* @return PhpDocument
*/
public function create(string $uri, string $content): PhpDocument
{
return new PhpDocument(
$uri,
$content,
$this->projectIndex->getIndexForUri($uri),
$this->parser,
$this->docBlockFactory,
$this->definitionResolver
);
}
/**
* Ensures a document is loaded and added to the list of open documents.
*
* @param string $uri
* @param string $content
* @return void
*/
public function open(string $uri, string $content)
{
if (isset($this->documents[$uri])) {
$document = $this->documents[$uri];
$document->updateContent($content);
} else {
$document = $this->create($uri, $content);
$this->documents[$uri] = $document;
}
return $document;
}
/**
* Removes the document with the specified URI from the list of open documents
*
* @param string $uri
* @return void
*/
public function close(string $uri)
{
unset($this->documents[$uri]);
}
/**
* Returns true if the document is open (and loaded)
*
* @param string $uri
* @return bool
*/
public function isOpen(string $uri): bool
{
return isset($this->documents[$uri]);
}
}

View File

@ -1,357 +0,0 @@
<?php
declare(strict_types = 1);
namespace LanguageServer;
use LanguageServer\Protocol\{SymbolInformation, TextDocumentIdentifier, ClientCapabilities};
use phpDocumentor\Reflection\DocBlockFactory;
use Sabre\Event\Promise;
use function Sabre\Event\coroutine;
class Project
{
/**
* An associative array [string => PhpDocument]
* that maps URIs to loaded PhpDocuments
*
* @var PhpDocument[]
*/
private $documents = [];
/**
* An associative array that maps fully qualified symbol names to Definitions
*
* @var Definition[]
*/
private $definitions = [];
/**
* An associative array that maps fully qualified symbol names to arrays of document URIs that reference the symbol
*
* @var PhpDocument[][]
*/
private $references = [];
/**
* Instance of the PHP parser
*
* @var Parser
*/
private $parser;
/**
* The DocBlockFactory instance to parse docblocks
*
* @var DocBlockFactory
*/
private $docBlockFactory;
/**
* The DefinitionResolver instance to resolve reference nodes to Definitions
*
* @var DefinitionResolver
*/
private $definitionResolver;
/**
* Reference to the language server client interface
*
* @var LanguageClient
*/
private $client;
/**
* The client's capabilities
*
* @var ClientCapabilities
*/
private $clientCapabilities;
public function __construct(LanguageClient $client, ClientCapabilities $clientCapabilities)
{
$this->client = $client;
$this->clientCapabilities = $clientCapabilities;
$this->parser = new Parser;
$this->docBlockFactory = DocBlockFactory::createInstance();
$this->definitionResolver = new DefinitionResolver($this);
}
/**
* Returns the document indicated by uri.
* Returns null if the document if not loaded.
*
* @param string $uri
* @return PhpDocument|null
*/
public function getDocument(string $uri)
{
return $this->documents[$uri] ?? null;
}
/**
* Returns the document indicated by uri.
* If the document is not open, loads it.
*
* @param string $uri
* @return Promise <PhpDocument>
*/
public function getOrLoadDocument(string $uri)
{
return isset($this->documents[$uri]) ? Promise\resolve($this->documents[$uri]) : $this->loadDocument($uri);
}
/**
* Loads the document by doing a textDocument/xcontent request to the client.
* If the client does not support textDocument/xcontent, tries to read the file from the file system.
* The document is NOT added to the list of open documents, but definitions are registered.
*
* @param string $uri
* @return Promise <PhpDocument>
*/
public function loadDocument(string $uri): Promise
{
return coroutine(function () use ($uri) {
$limit = 150000;
if ($this->clientCapabilities->xcontentProvider) {
$content = (yield $this->client->textDocument->xcontent(new TextDocumentIdentifier($uri)))->text;
$size = strlen($content);
if ($size > $limit) {
throw new ContentTooLargeException($uri, $size, $limit);
}
} else {
$path = uriToPath($uri);
$size = filesize($path);
if ($size > $limit) {
throw new ContentTooLargeException($uri, $size, $limit);
}
$content = file_get_contents($path);
}
if (isset($this->documents[$uri])) {
$document = $this->documents[$uri];
$document->updateContent($content);
} else {
$document = new PhpDocument(
$uri,
$content,
$this,
$this->client,
$this->parser,
$this->docBlockFactory,
$this->definitionResolver
);
}
return $document;
});
}
/**
* Ensures a document is loaded and added to the list of open documents.
*
* @param string $uri
* @param string $content
* @return void
*/
public function openDocument(string $uri, string $content)
{
if (isset($this->documents[$uri])) {
$document = $this->documents[$uri];
$document->updateContent($content);
} else {
$document = new PhpDocument(
$uri,
$content,
$this,
$this->client,
$this->parser,
$this->docBlockFactory,
$this->definitionResolver
);
$this->documents[$uri] = $document;
}
return $document;
}
/**
* Removes the document with the specified URI from the list of open documents
*
* @param string $uri
* @return void
*/
public function closeDocument(string $uri)
{
unset($this->documents[$uri]);
}
/**
* Returns true if the document is open (and loaded)
*
* @param string $uri
* @return bool
*/
public function isDocumentOpen(string $uri): bool
{
return isset($this->documents[$uri]);
}
/**
* Returns an associative array [string => Definition] that maps fully qualified symbol names
* to Definitions
*
* @return Definitions[]
*/
public function getDefinitions()
{
return $this->definitions;
}
/**
* Returns the Definition object by a specific FQN
*
* @param string $fqn
* @param bool $globalFallback Whether to fallback to global if the namespaced FQN was not found
* @return Definition|null
*/
public function getDefinition(string $fqn, $globalFallback = false)
{
if (isset($this->definitions[$fqn])) {
return $this->definitions[$fqn];
} else if ($globalFallback) {
$parts = explode('\\', $fqn);
$fqn = end($parts);
return $this->getDefinition($fqn);
}
}
/**
* Registers a definition
*
* @param string $fqn The fully qualified name of the symbol
* @param string $definition The Definition object
* @return void
*/
public function setDefinition(string $fqn, Definition $definition)
{
$this->definitions[$fqn] = $definition;
}
/**
* Sets the Definition index
*
* @param Definition[] $definitions Map from FQN to Definition
* @return void
*/
public function setDefinitions(array $definitions)
{
$this->definitions = $definitions;
}
/**
* Unsets the Definition for a specific symbol
* and removes all references pointing to that symbol
*
* @param string $fqn The fully qualified name of the symbol
* @return void
*/
public function removeDefinition(string $fqn)
{
unset($this->definitions[$fqn]);
unset($this->references[$fqn]);
}
/**
* Adds a document URI as a referencee of a specific symbol
*
* @param string $fqn The fully qualified name of the symbol
* @return void
*/
public function addReferenceUri(string $fqn, string $uri)
{
if (!isset($this->references[$fqn])) {
$this->references[$fqn] = [];
}
// TODO: use DS\Set instead of searching array
if (array_search($uri, $this->references[$fqn], true) === false) {
$this->references[$fqn][] = $uri;
}
}
/**
* Removes a document URI as the container for a specific symbol
*
* @param string $fqn The fully qualified name of the symbol
* @param string $uri The URI
* @return void
*/
public function removeReferenceUri(string $fqn, string $uri)
{
if (!isset($this->references[$fqn])) {
return;
}
$index = array_search($fqn, $this->references[$fqn], true);
if ($index === false) {
return;
}
array_splice($this->references[$fqn], $index, 1);
}
/**
* Returns all documents that reference a symbol
*
* @param string $fqn The fully qualified name of the symbol
* @return Promise <PhpDocument[]>
*/
public function getReferenceDocuments(string $fqn): Promise
{
if (!isset($this->references[$fqn])) {
return Promise\resolve([]);
}
return Promise\all(array_map([$this, 'getOrLoadDocument'], $this->references[$fqn]));
}
/**
* Returns an associative array [string => string[]] that maps fully qualified symbol names
* to URIs of the document where the symbol is referenced
*
* @return string[][]
*/
public function getReferenceUris()
{
return $this->references;
}
/**
* Sets the reference index
*
* @param string[][] $references an associative array [string => string[]] from FQN to URIs
* @return void
*/
public function setReferenceUris(array $references)
{
$this->references = $references;
}
/**
* Returns the document where a symbol is defined
*
* @param string $fqn The fully qualified name of the symbol
* @return Promise <PhpDocument|null>
*/
public function getDefinitionDocument(string $fqn): Promise
{
if (!isset($this->definitions[$fqn])) {
return Promise\resolve(null);
}
return $this->getOrLoadDocument($this->definitions[$fqn]->symbolInformation->location->uri);
}
/**
* Returns true if the given FQN is defined in the project
*
* @param string $fqn The fully qualified name of the symbol
* @return bool
*/
public function isDefined(string $fqn): bool
{
return isset($this->definitions[$fqn]);
}
}

View File

@ -3,9 +3,10 @@ declare(strict_types = 1);
namespace LanguageServer\Server; namespace LanguageServer\Server;
use LanguageServer\{LanguageClient, Project, PhpDocument, DefinitionResolver, CompletionProvider};
use PhpParser\PrettyPrinter\Standard as PrettyPrinter; use PhpParser\PrettyPrinter\Standard as PrettyPrinter;
use PhpParser\Node; use PhpParser\{Node, NodeTraverser};
use LanguageServer\{LanguageClient, PhpDocumentLoader, PhpDocument, DefinitionResolver, CompletionProvider};
use LanguageServer\NodeVisitor\VariableReferencesCollector;
use LanguageServer\Protocol\{ use LanguageServer\Protocol\{
TextDocumentItem, TextDocumentItem,
TextDocumentIdentifier, TextDocumentIdentifier,
@ -23,7 +24,9 @@ use LanguageServer\Protocol\{
CompletionItem, CompletionItem,
CompletionItemKind CompletionItemKind
}; };
use LanguageServer\Index\ReadableIndex;
use Sabre\Event\Promise; use Sabre\Event\Promise;
use Sabre\Uri;
use function Sabre\Event\coroutine; use function Sabre\Event\coroutine;
/** /**
@ -58,13 +61,29 @@ class TextDocument
*/ */
private $completionProvider; private $completionProvider;
public function __construct(Project $project, LanguageClient $client) /**
{ * @var ReadableIndex
$this->project = $project; */
private $index;
/**
* @param PhpDocumentLoader $documentLoader
* @param DefinitionResolver $definitionResolver
* @param LanguageClient $client
* @param ReadableIndex $index
*/
public function __construct(
PhpDocumentLoader $documentLoader,
DefinitionResolver $definitionResolver,
LanguageClient $client,
ReadableIndex $index
) {
$this->documentLoader = $documentLoader;
$this->client = $client; $this->client = $client;
$this->prettyPrinter = new PrettyPrinter(); $this->prettyPrinter = new PrettyPrinter();
$this->definitionResolver = new DefinitionResolver($project); $this->definitionResolver = $definitionResolver;
$this->completionProvider = new CompletionProvider($this->definitionResolver, $project); $this->completionProvider = new CompletionProvider($this->definitionResolver, $index);
$this->index = $index;
} }
/** /**
@ -76,7 +95,7 @@ class TextDocument
*/ */
public function documentSymbol(TextDocumentIdentifier $textDocument): Promise public function documentSymbol(TextDocumentIdentifier $textDocument): Promise
{ {
return $this->project->getOrLoadDocument($textDocument->uri)->then(function (PhpDocument $document) { return $this->documentLoader->getOrLoad($textDocument->uri)->then(function (PhpDocument $document) {
$symbols = []; $symbols = [];
foreach ($document->getDefinitions() as $fqn => $definition) { foreach ($document->getDefinitions() as $fqn => $definition) {
$symbols[] = $definition->symbolInformation; $symbols[] = $definition->symbolInformation;
@ -95,7 +114,10 @@ class TextDocument
*/ */
public function didOpen(TextDocumentItem $textDocument) public function didOpen(TextDocumentItem $textDocument)
{ {
$this->project->openDocument($textDocument->uri, $textDocument->text); $document = $this->documentLoader->open($textDocument->uri, $textDocument->text);
if (!$document->isVendored()) {
$this->client->textDocument->publishDiagnostics($textDocument->uri, $document->getDiagnostics());
}
} }
/** /**
@ -107,7 +129,9 @@ class TextDocument
*/ */
public function didChange(VersionedTextDocumentIdentifier $textDocument, array $contentChanges) public function didChange(VersionedTextDocumentIdentifier $textDocument, array $contentChanges)
{ {
$this->project->getDocument($textDocument->uri)->updateContent($contentChanges[0]->text); $document = $this->documentLoader->get($textDocument->uri);
$document->updateContent($contentChanges[0]->text);
$this->client->textDocument->publishDiagnostics($textDocument->uri, $document->getDiagnostics());
} }
/** /**
@ -120,7 +144,7 @@ class TextDocument
*/ */
public function didClose(TextDocumentIdentifier $textDocument) public function didClose(TextDocumentIdentifier $textDocument)
{ {
$this->project->closeDocument($textDocument->uri); $this->documentLoader->close($textDocument->uri);
} }
/** /**
@ -132,7 +156,7 @@ class TextDocument
*/ */
public function formatting(TextDocumentIdentifier $textDocument, FormattingOptions $options) public function formatting(TextDocumentIdentifier $textDocument, FormattingOptions $options)
{ {
return $this->project->getOrLoadDocument($textDocument->uri)->then(function (PhpDocument $document) { return $this->documentLoader->getOrLoad($textDocument->uri)->then(function (PhpDocument $document) {
return $document->getFormattedText(); return $document->getFormattedText();
}); });
} }
@ -150,16 +174,59 @@ class TextDocument
Position $position Position $position
): Promise { ): Promise {
return coroutine(function () use ($textDocument, $position) { return coroutine(function () use ($textDocument, $position) {
$document = yield $this->project->getOrLoadDocument($textDocument->uri); $document = yield $this->documentLoader->getOrLoad($textDocument->uri);
$node = $document->getNodeAtPosition($position); $node = $document->getNodeAtPosition($position);
if ($node === null) { if ($node === null) {
return []; return [];
} }
$refs = yield $document->getReferenceNodesByNode($node);
$locations = []; $locations = [];
// Variables always stay in the boundary of the file and need to be searched inside their function scope
// by traversing the AST
if (
$node instanceof Node\Expr\Variable
|| $node instanceof Node\Param
|| $node instanceof Node\Expr\ClosureUse
) {
if ($node->name instanceof Node\Expr) {
return null;
}
// Find function/method/closure scope
$n = $node;
while (isset($n) && !($n instanceof Node\FunctionLike)) {
$n = $n->getAttribute('parentNode');
}
if (!isset($n)) {
$n = $node->getAttribute('ownerDocument');
}
$traverser = new NodeTraverser;
$refCollector = new VariableReferencesCollector($node->name);
$traverser->addVisitor($refCollector);
$traverser->traverse($n->getStmts());
foreach ($refCollector->nodes as $ref) {
$locations[] = Location::fromNode($ref);
}
} else {
// Definition with a global FQN
$fqn = DefinitionResolver::getDefinedFqn($node);
if ($fqn === null) {
$fqn = $this->definitionResolver->resolveReferenceNodeToFqn($node);
if ($fqn === null) {
return [];
}
}
$refDocuments = yield Promise\all(array_map(
[$this->documentLoader, 'getOrLoad'],
$this->index->getReferenceUris($fqn)
));
foreach ($refDocuments as $document) {
$refs = $document->getReferenceNodesByFqn($fqn);
if ($refs !== null) {
foreach ($refs as $ref) { foreach ($refs as $ref) {
$locations[] = Location::fromNode($ref); $locations[] = Location::fromNode($ref);
} }
}
}
}
return $locations; return $locations;
}); });
} }
@ -175,13 +242,17 @@ class TextDocument
public function definition(TextDocumentIdentifier $textDocument, Position $position): Promise public function definition(TextDocumentIdentifier $textDocument, Position $position): Promise
{ {
return coroutine(function () use ($textDocument, $position) { return coroutine(function () use ($textDocument, $position) {
$document = yield $this->project->getOrLoadDocument($textDocument->uri); $document = yield $this->documentLoader->getOrLoad($textDocument->uri);
$node = $document->getNodeAtPosition($position); $node = $document->getNodeAtPosition($position);
if ($node === null) { if ($node === null) {
return []; return [];
} }
$def = $this->definitionResolver->resolveReferenceNodeToDefinition($node); $def = $this->definitionResolver->resolveReferenceNodeToDefinition($node);
if ($def === null || $def->symbolInformation === null) { if (
$def === null
|| $def->symbolInformation === null
|| Uri\parse($def->symbolInformation->location->uri)['scheme'] === 'phpstubs'
) {
return []; return [];
} }
return $def->symbolInformation->location; return $def->symbolInformation->location;
@ -198,15 +269,20 @@ class TextDocument
public function hover(TextDocumentIdentifier $textDocument, Position $position): Promise public function hover(TextDocumentIdentifier $textDocument, Position $position): Promise
{ {
return coroutine(function () use ($textDocument, $position) { return coroutine(function () use ($textDocument, $position) {
$document = yield $this->project->getOrLoadDocument($textDocument->uri); $document = yield $this->documentLoader->getOrLoad($textDocument->uri);
// Find the node under the cursor // Find the node under the cursor
$node = $document->getNodeAtPosition($position); $node = $document->getNodeAtPosition($position);
if ($node === null) { if ($node === null) {
return new Hover([]); return new Hover([]);
} }
$range = Range::fromNode($node); $range = Range::fromNode($node);
if ($definedFqn = DefinitionResolver::getDefinedFqn($node)) {
// Support hover for definitions
$def = $this->index->getDefinition($definedFqn);
} else {
// Get the definition for whatever node is under the cursor // Get the definition for whatever node is under the cursor
$def = $this->definitionResolver->resolveReferenceNodeToDefinition($node); $def = $this->definitionResolver->resolveReferenceNodeToDefinition($node);
}
if ($def === null) { if ($def === null) {
return new Hover([], $range); return new Hover([], $range);
} }
@ -237,7 +313,7 @@ class TextDocument
public function completion(TextDocumentIdentifier $textDocument, Position $position): Promise public function completion(TextDocumentIdentifier $textDocument, Position $position): Promise
{ {
return coroutine(function () use ($textDocument, $position) { return coroutine(function () use ($textDocument, $position) {
$document = yield $this->project->getOrLoadDocument($textDocument->uri); $document = yield $this->documentLoader->getOrLoad($textDocument->uri);
return $this->completionProvider->provideCompletion($document, $position); return $this->completionProvider->provideCompletion($document, $position);
}); });
} }

View File

@ -4,6 +4,7 @@ declare(strict_types = 1);
namespace LanguageServer\Server; namespace LanguageServer\Server;
use LanguageServer\{LanguageClient, Project}; use LanguageServer\{LanguageClient, Project};
use LanguageServer\Index\ProjectIndex;
use LanguageServer\Protocol\SymbolInformation; use LanguageServer\Protocol\SymbolInformation;
/** /**
@ -19,15 +20,18 @@ class Workspace
private $client; private $client;
/** /**
* The current project database * The symbol index for the workspace
* *
* @var Project * @var ProjectIndex
*/ */
private $project; private $index;
public function __construct(Project $project, LanguageClient $client) /**
* @param ProjectIndex $index Index that is searched on a workspace/symbol request
*/
public function __construct(ProjectIndex $index, LanguageClient $client)
{ {
$this->project = $project; $this->index = $index;
$this->client = $client; $this->client = $client;
} }
@ -40,7 +44,7 @@ class Workspace
public function symbol(string $query): array public function symbol(string $query): array
{ {
$symbols = []; $symbols = [];
foreach ($this->project->getDefinitions() as $fqn => $definition) { foreach ($this->index->getDefinitions() as $fqn => $definition) {
if ($query === '' || stripos($fqn, $query) !== false) { if ($query === '' || stripos($fqn, $query) !== false) {
$symbols[] = $definition->symbolInformation; $symbols[] = $definition->symbolInformation;
} }

View File

@ -6,7 +6,16 @@ namespace LanguageServer\Tests;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use LanguageServer\LanguageServer; use LanguageServer\LanguageServer;
use LanguageServer\Protocol\{ use LanguageServer\Protocol\{
Message, ClientCapabilities, TextDocumentSyncKind, MessageType, TextDocumentItem, TextDocumentIdentifier}; Message,
ClientCapabilities,
TextDocumentSyncKind,
MessageType,
TextDocumentItem,
TextDocumentIdentifier,
InitializeResult,
ServerCapabilities,
CompletionOptions
};
use AdvancedJsonRpc; use AdvancedJsonRpc;
use Webmozart\Glob\Glob; use Webmozart\Glob\Glob;
use Webmozart\PathUtil\Path; use Webmozart\PathUtil\Path;
@ -18,41 +27,22 @@ class LanguageServerTest extends TestCase
{ {
public function testInitialize() public function testInitialize()
{ {
$reader = new MockProtocolStream(); $server = new LanguageServer(new MockProtocolStream, new MockProtocolStream);
$writer = new MockProtocolStream(); $result = $server->initialize(new ClientCapabilities, __DIR__, getmypid())->wait();
$server = new LanguageServer($reader, $writer);
$promise = new Promise; $serverCapabilities = new ServerCapabilities();
$writer->once('message', [$promise, 'fulfill']); $serverCapabilities->textDocumentSync = TextDocumentSyncKind::FULL;
$reader->write(new Message(new AdvancedJsonRpc\Request(1, 'initialize', [ $serverCapabilities->documentSymbolProvider = true;
'rootPath' => __DIR__, $serverCapabilities->workspaceSymbolProvider = true;
'processId' => getmypid(), $serverCapabilities->documentFormattingProvider = true;
'capabilities' => new ClientCapabilities() $serverCapabilities->definitionProvider = true;
]))); $serverCapabilities->referencesProvider = true;
$msg = $promise->wait(); $serverCapabilities->hoverProvider = true;
$this->assertNotNull($msg, 'message event should be emitted'); $serverCapabilities->completionProvider = new CompletionOptions;
$this->assertInstanceOf(AdvancedJsonRpc\SuccessResponse::class, $msg->body); $serverCapabilities->completionProvider->resolveProvider = false;
$this->assertEquals((object)[ $serverCapabilities->completionProvider->triggerCharacters = ['$', '>'];
'capabilities' => (object)[
'textDocumentSync' => TextDocumentSyncKind::FULL, $this->assertEquals(new InitializeResult($serverCapabilities), $result);
'documentSymbolProvider' => true,
'hoverProvider' => true,
'completionProvider' => (object)[
'resolveProvider' => false,
'triggerCharacters' => ['$', '>']
],
'signatureHelpProvider' => null,
'definitionProvider' => true,
'referencesProvider' => true,
'documentHighlightProvider' => null,
'workspaceSymbolProvider' => true,
'codeActionProvider' => null,
'codeLensProvider' => null,
'documentFormattingProvider' => true,
'documentRangeFormattingProvider' => null,
'documentOnTypeFormattingProvider' => null,
'renameProvider' => null
]
], $msg->body->result);
} }
public function testIndexingWithDirectFileAccess() public function testIndexingWithDirectFileAccess()
@ -83,7 +73,8 @@ class LanguageServerTest extends TestCase
$rootPath = realpath(__DIR__ . '/../fixtures'); $rootPath = realpath(__DIR__ . '/../fixtures');
$input = new MockProtocolStream; $input = new MockProtocolStream;
$output = new MockProtocolStream; $output = new MockProtocolStream;
$output->on('message', function (Message $msg) use ($promise, $input, $rootPath, &$filesCalled, &$contentCalled) { $run = 1;
$output->on('message', function (Message $msg) use ($promise, $input, $rootPath, &$filesCalled, &$contentCalled, &$run) {
if ($msg->body->method === 'textDocument/xcontent') { if ($msg->body->method === 'textDocument/xcontent') {
// Document content requested // Document content requested
$contentCalled = true; $contentCalled = true;
@ -110,10 +101,13 @@ class LanguageServerTest extends TestCase
$promise->reject(new Exception($msg->body->params->message)); $promise->reject(new Exception($msg->body->params->message));
} }
} else if (strpos($msg->body->params->message, 'All 25 PHP files parsed') !== false) { } else if (strpos($msg->body->params->message, 'All 25 PHP files parsed') !== false) {
// Indexing finished if ($run === 1) {
$run++;
} else {
$promise->fulfill(); $promise->fulfill();
} }
} }
}
}); });
$server = new LanguageServer($input, $output); $server = new LanguageServer($input, $output);
$capabilities = new ClientCapabilities; $capabilities = new ClientCapabilities;

View File

@ -6,8 +6,11 @@ namespace LanguageServer\Tests\Server\TextDocument;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use PhpParser\{NodeTraverser, Node}; use PhpParser\{NodeTraverser, Node};
use PhpParser\NodeVisitor\NameResolver; use PhpParser\NodeVisitor\NameResolver;
use LanguageServer\{LanguageClient, Project, PhpDocument, Parser, DefinitionResolver}; use phpDocumentor\Reflection\DocBlockFactory;
use LanguageServer\{LanguageClient, PhpDocument, PhpDocumentLoader, Parser, DefinitionResolver};
use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Protocol\ClientCapabilities; use LanguageServer\Protocol\ClientCapabilities;
use LanguageServer\Index\{ProjectIndex, Index, DependenciesIndex};
use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\NodeVisitor\{ReferencesAdder, DefinitionCollector}; use LanguageServer\NodeVisitor\{ReferencesAdder, DefinitionCollector};
use function LanguageServer\pathToUri; use function LanguageServer\pathToUri;
@ -16,19 +19,25 @@ class DefinitionCollectorTest extends TestCase
{ {
public function testCollectsSymbols() public function testCollectsSymbols()
{ {
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); $path = realpath(__DIR__ . '/../../fixtures/symbols.php');
$project = new Project($client, new ClientCapabilities); $uri = pathToUri($path);
$parser = new Parser; $parser = new Parser;
$uri = pathToUri(realpath(__DIR__ . '/../../fixtures/symbols.php')); $docBlockFactory = DocBlockFactory::createInstance();
$document = $project->loadDocument($uri)->wait(); $index = new Index;
$definitionResolver = new DefinitionResolver($index);
$content = file_get_contents($path);
$document = new PhpDocument($uri, $content, $index, $parser, $docBlockFactory, $definitionResolver);
$stmts = $parser->parse($content);
$traverser = new NodeTraverser; $traverser = new NodeTraverser;
$traverser->addVisitor(new NameResolver); $traverser->addVisitor(new NameResolver);
$traverser->addVisitor(new ReferencesAdder($document)); $traverser->addVisitor(new ReferencesAdder($document));
$definitionCollector = new DefinitionCollector(new DefinitionResolver($project)); $definitionCollector = new DefinitionCollector($definitionResolver);
$traverser->addVisitor($definitionCollector); $traverser->addVisitor($definitionCollector);
$stmts = $parser->parse(file_get_contents($uri));
$traverser->traverse($stmts); $traverser->traverse($stmts);
$defNodes = $definitionCollector->nodes; $defNodes = $definitionCollector->nodes;
$this->assertEquals([ $this->assertEquals([
'TestNamespace', 'TestNamespace',
'TestNamespace\\TEST_CONST', 'TestNamespace\\TEST_CONST',
@ -56,19 +65,25 @@ class DefinitionCollectorTest extends TestCase
public function testDoesNotCollectReferences() public function testDoesNotCollectReferences()
{ {
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); $path = realpath(__DIR__ . '/../../fixtures/references.php');
$project = new Project($client, new ClientCapabilities); $uri = pathToUri($path);
$parser = new Parser; $parser = new Parser;
$uri = pathToUri(realpath(__DIR__ . '/../../fixtures/references.php')); $docBlockFactory = DocBlockFactory::createInstance();
$document = $project->loadDocument($uri)->wait(); $index = new Index;
$definitionResolver = new DefinitionResolver($index);
$content = file_get_contents($path);
$document = new PhpDocument($uri, $content, $index, $parser, $docBlockFactory, $definitionResolver);
$stmts = $parser->parse($content);
$traverser = new NodeTraverser; $traverser = new NodeTraverser;
$traverser->addVisitor(new NameResolver); $traverser->addVisitor(new NameResolver);
$traverser->addVisitor(new ReferencesAdder($document)); $traverser->addVisitor(new ReferencesAdder($document));
$definitionCollector = new DefinitionCollector(new DefinitionResolver($project)); $definitionCollector = new DefinitionCollector($definitionResolver);
$traverser->addVisitor($definitionCollector); $traverser->addVisitor($definitionCollector);
$stmts = $parser->parse(file_get_contents($uri));
$traverser->traverse($stmts); $traverser->traverse($stmts);
$defNodes = $definitionCollector->nodes; $defNodes = $definitionCollector->nodes;
$this->assertEquals(['TestNamespace', 'TestNamespace\\whatever()'], array_keys($defNodes)); $this->assertEquals(['TestNamespace', 'TestNamespace\\whatever()'], array_keys($defNodes));
$this->assertInstanceOf(Node\Stmt\Namespace_::class, $defNodes['TestNamespace']); $this->assertInstanceOf(Node\Stmt\Namespace_::class, $defNodes['TestNamespace']);
$this->assertInstanceOf(Node\Stmt\Function_::class, $defNodes['TestNamespace\\whatever()']); $this->assertInstanceOf(Node\Stmt\Function_::class, $defNodes['TestNamespace\\whatever()']);

View File

@ -0,0 +1,54 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Tests\Server;
use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, Client, LanguageClient, Project, PhpDocument, PhpDocumentLoader, DefinitionResolver};
use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex};
use LanguageServer\Protocol\{
TextDocumentItem,
TextDocumentIdentifier,
SymbolKind,
DiagnosticSeverity,
FormattingOptions,
ClientCapabilities
};
use AdvancedJsonRpc\{Request as RequestBody, Response as ResponseBody};
use function LanguageServer\pathToUri;
class PhpDocumentLoaderTest extends TestCase
{
/**
* @var PhpDocumentLoader
*/
private $loader;
public function setUp()
{
$projectIndex = new ProjectIndex(new Index, new DependenciesIndex);
$this->loader = new PhpDocumentLoader(
new FileSystemContentRetriever,
$projectIndex,
new DefinitionResolver($projectIndex)
);
}
public function testGetOrLoadLoadsDocument()
{
$document = $this->loader->getOrLoad(pathToUri(__FILE__))->wait();
$this->assertNotNull($document);
$this->assertInstanceOf(PhpDocument::class, $document);
}
public function testGetReturnsOpenedInstance()
{
$document1 = $this->loader->open(pathToUri(__FILE__), file_get_contents(__FILE__));
$document2 = $this->loader->get(pathToUri(__FILE__));
$this->assertSame($document1, $document2);
}
}

View File

@ -4,35 +4,36 @@ declare(strict_types = 1);
namespace LanguageServer\Tests\Server; namespace LanguageServer\Tests\Server;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use phpDocumentor\Reflection\DocBlockFactory;
use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{LanguageClient, Project}; use LanguageServer\{LanguageClient, PhpDocument, DefinitionResolver, Parser};
use LanguageServer\NodeVisitor\NodeAtPositionFinder; use LanguageServer\NodeVisitor\NodeAtPositionFinder;
use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Protocol\{SymbolKind, Position, ClientCapabilities}; use LanguageServer\Protocol\{SymbolKind, Position, ClientCapabilities};
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex};
use PhpParser\Node; use PhpParser\Node;
class PhpDocumentTest extends TestCase class PhpDocumentTest extends TestCase
{ {
/** public function createDocument(string $uri, string $content)
* @var Project $project
*/
private $project;
public function setUp()
{ {
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); $parser = new Parser;
$this->project = new Project($client, new ClientCapabilities); $docBlockFactory = DocBlockFactory::createInstance();
$index = new Index;
$definitionResolver = new DefinitionResolver($index);
return new PhpDocument($uri, $content, $index, $parser, $docBlockFactory, $definitionResolver);
} }
public function testParsesVariableVariables() public function testParsesVariableVariables()
{ {
$document = $this->project->openDocument('whatever', "<?php\n$\$a = 'foo';\n\$bar = 'baz';\n"); $document = $this->createDocument('whatever', "<?php\n$\$a = 'foo';\n\$bar = 'baz';\n");
$this->assertEquals([], $document->getDefinitions()); $this->assertEquals([], $document->getDefinitions());
} }
public function testGetNodeAtPosition() public function testGetNodeAtPosition()
{ {
$document = $this->project->openDocument('whatever', "<?php\n$\$a = new SomeClass;"); $document = $this->createDocument('whatever', "<?php\n$\$a = new SomeClass;");
$node = $document->getNodeAtPosition(new Position(1, 13)); $node = $document->getNodeAtPosition(new Position(1, 13));
$this->assertInstanceOf(Node\Name\FullyQualified::class, $node); $this->assertInstanceOf(Node\Name\FullyQualified::class, $node);
$this->assertEquals('SomeClass', (string)$node); $this->assertEquals('SomeClass', (string)$node);
@ -40,19 +41,19 @@ class PhpDocumentTest extends TestCase
public function testIsVendored() public function testIsVendored()
{ {
$document = $this->project->openDocument('file:///dir/vendor/x.php', "<?php\n$\$a = new SomeClass;"); $document = $this->createDocument('file:///dir/vendor/x.php', "<?php\n$\$a = new SomeClass;");
$this->assertEquals(true, $document->isVendored()); $this->assertEquals(true, $document->isVendored());
$document = $this->project->openDocument('file:///c:/dir/vendor/x.php', "<?php\n$\$a = new SomeClass;"); $document = $this->createDocument('file:///c:/dir/vendor/x.php', "<?php\n$\$a = new SomeClass;");
$this->assertEquals(true, $document->isVendored()); $this->assertEquals(true, $document->isVendored());
$document = $this->project->openDocument('file:///vendor/x.php', "<?php\n$\$a = new SomeClass;"); $document = $this->createDocument('file:///vendor/x.php', "<?php\n$\$a = new SomeClass;");
$this->assertEquals(true, $document->isVendored()); $this->assertEquals(true, $document->isVendored());
$document = $this->project->openDocument('file:///dir/vendor.php', "<?php\n$\$a = new SomeClass;"); $document = $this->createDocument('file:///dir/vendor.php', "<?php\n$\$a = new SomeClass;");
$this->assertEquals(false, $document->isVendored()); $this->assertEquals(false, $document->isVendored());
$document = $this->project->openDocument('file:///dir/x.php', "<?php\n$\$a = new SomeClass;"); $document = $this->createDocument('file:///dir/x.php', "<?php\n$\$a = new SomeClass;");
$this->assertEquals(false, $document->isVendored()); $this->assertEquals(false, $document->isVendored());
} }
} }

View File

@ -1,48 +0,0 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Tests\Server;
use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, Client, LanguageClient, Project, PhpDocument};
use LanguageServer\Protocol\{
TextDocumentItem,
TextDocumentIdentifier,
SymbolKind,
DiagnosticSeverity,
FormattingOptions,
ClientCapabilities
};
use AdvancedJsonRpc\{Request as RequestBody, Response as ResponseBody};
use function LanguageServer\pathToUri;
class ProjectTest extends TestCase
{
/**
* @var Project $project
*/
private $project;
public function setUp()
{
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$this->project = new Project($client, new ClientCapabilities);
}
public function testGetOrLoadDocumentLoadsDocument()
{
$document = $this->project->getOrLoadDocument(pathToUri(__FILE__))->wait();
$this->assertNotNull($document);
$this->assertInstanceOf(PhpDocument::class, $document);
}
public function testGetDocumentReturnsOpenedInstance()
{
$document1 = $this->project->openDocument(pathToUri(__FILE__), file_get_contents(__FILE__));
$document2 = $this->project->getDocument(pathToUri(__FILE__));
$this->assertSame($document1, $document2);
}
}

View File

@ -5,7 +5,9 @@ namespace LanguageServer\Tests\Server;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, LanguageClient, Project}; use LanguageServer\{Server, LanguageClient, PhpDocumentLoader, DefinitionResolver};
use LanguageServer\Index\{ProjectIndex, StubsIndex, GlobalIndex, DependenciesIndex, Index};
use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Protocol\{Position, Location, Range, ClientCapabilities}; use LanguageServer\Protocol\{Position, Location, Range, ClientCapabilities};
use function LanguageServer\pathToUri; use function LanguageServer\pathToUri;
use Sabre\Event\Promise; use Sabre\Event\Promise;
@ -23,9 +25,9 @@ abstract class ServerTestCase extends TestCase
protected $workspace; protected $workspace;
/** /**
* @var Project * @var PhpDocumentLoader
*/ */
protected $project; protected $documentLoader;
/** /**
* Map from FQN to Location of definition * Map from FQN to Location of definition
@ -43,10 +45,13 @@ abstract class ServerTestCase extends TestCase
public function setUp() public function setUp()
{ {
$projectIndex = new ProjectIndex(new Index, new DependenciesIndex);
$definitionResolver = new DefinitionResolver($projectIndex);
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$this->project = new Project($client, new ClientCapabilities); $this->documentLoader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver);
$this->textDocument = new Server\TextDocument($this->project, $client); $this->textDocument = new Server\TextDocument($this->documentLoader, $definitionResolver, $client, $projectIndex);
$this->workspace = new Server\Workspace($this->project, $client); $this->workspace = new Server\Workspace($projectIndex, $client);
$globalSymbolsUri = pathToUri(realpath(__DIR__ . '/../../fixtures/global_symbols.php')); $globalSymbolsUri = pathToUri(realpath(__DIR__ . '/../../fixtures/global_symbols.php'));
$globalReferencesUri = pathToUri(realpath(__DIR__ . '/../../fixtures/global_references.php')); $globalReferencesUri = pathToUri(realpath(__DIR__ . '/../../fixtures/global_references.php'));
@ -54,11 +59,11 @@ abstract class ServerTestCase extends TestCase
$referencesUri = pathToUri(realpath(__DIR__ . '/../../fixtures/references.php')); $referencesUri = pathToUri(realpath(__DIR__ . '/../../fixtures/references.php'));
$useUri = pathToUri(realpath(__DIR__ . '/../../fixtures/use.php')); $useUri = pathToUri(realpath(__DIR__ . '/../../fixtures/use.php'));
$this->project->loadDocument($symbolsUri)->wait(); $this->documentLoader->load($symbolsUri)->wait();
$this->project->loadDocument($referencesUri)->wait(); $this->documentLoader->load($referencesUri)->wait();
$this->project->loadDocument($globalSymbolsUri)->wait(); $this->documentLoader->load($globalSymbolsUri)->wait();
$this->project->loadDocument($globalReferencesUri)->wait(); $this->documentLoader->load($globalReferencesUri)->wait();
$this->project->loadDocument($useUri)->wait(); $this->documentLoader->load($useUri)->wait();
// @codingStandardsIgnoreStart // @codingStandardsIgnoreStart
$this->definitionLocations = [ $this->definitionLocations = [

View File

@ -5,7 +5,9 @@ namespace LanguageServer\Tests\Server\TextDocument;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, LanguageClient, Project, CompletionProvider}; use LanguageServer\{Server, LanguageClient, PhpDocumentLoader, CompletionProvider, DefinitionResolver};
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex, GlobalIndex, StubsIndex};
use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Protocol\{ use LanguageServer\Protocol\{
TextDocumentIdentifier, TextDocumentIdentifier,
TextEdit, TextEdit,
@ -26,23 +28,26 @@ class CompletionTest extends TestCase
private $textDocument; private $textDocument;
/** /**
* @var Project * @var PhpDocumentLoader
*/ */
private $project; private $loader;
public function setUp() public function setUp()
{ {
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$this->project = new Project($client, new ClientCapabilities); $projectIndex = new ProjectIndex(new Index, new DependenciesIndex);
$this->project->loadDocument(pathToUri(__DIR__ . '/../../../fixtures/global_symbols.php'))->wait(); $definitionResolver = new DefinitionResolver($projectIndex);
$this->project->loadDocument(pathToUri(__DIR__ . '/../../../fixtures/symbols.php'))->wait(); $contentRetriever = new FileSystemContentRetriever;
$this->textDocument = new Server\TextDocument($this->project, $client); $this->loader = new PhpDocumentLoader($contentRetriever, $projectIndex, $definitionResolver);
$this->loader->load(pathToUri(__DIR__ . '/../../../fixtures/global_symbols.php'))->wait();
$this->loader->load(pathToUri(__DIR__ . '/../../../fixtures/symbols.php'))->wait();
$this->textDocument = new Server\TextDocument($this->loader, $definitionResolver, $client, $projectIndex);
} }
public function testPropertyAndMethodWithPrefix() public function testPropertyAndMethodWithPrefix()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/property_with_prefix.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/property_with_prefix.php');
$this->project->openDocument($completionUri, file_get_contents($completionUri)); $this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion( $items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(3, 7) new Position(3, 7)
@ -66,7 +71,7 @@ class CompletionTest extends TestCase
public function testPropertyAndMethodWithoutPrefix() public function testPropertyAndMethodWithoutPrefix()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/property.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/property.php');
$this->project->openDocument($completionUri, file_get_contents($completionUri)); $this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion( $items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(3, 6) new Position(3, 6)
@ -90,7 +95,7 @@ class CompletionTest extends TestCase
public function testVariable() public function testVariable()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/variable.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/variable.php');
$this->project->openDocument($completionUri, file_get_contents($completionUri)); $this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion( $items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(8, 5) new Position(8, 5)
@ -122,7 +127,7 @@ class CompletionTest extends TestCase
public function testVariableWithPrefix() public function testVariableWithPrefix()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/variable_with_prefix.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/variable_with_prefix.php');
$this->project->openDocument($completionUri, file_get_contents($completionUri)); $this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion( $items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(8, 6) new Position(8, 6)
@ -144,7 +149,7 @@ class CompletionTest extends TestCase
public function testNewInNamespace() public function testNewInNamespace()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/used_new.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/used_new.php');
$this->project->openDocument($completionUri, file_get_contents($completionUri)); $this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion( $items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(6, 10) new Position(6, 10)
@ -176,7 +181,7 @@ class CompletionTest extends TestCase
public function testUsedClass() public function testUsedClass()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/used_class.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/used_class.php');
$this->project->openDocument($completionUri, file_get_contents($completionUri)); $this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion( $items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(6, 5) new Position(6, 5)
@ -194,7 +199,7 @@ class CompletionTest extends TestCase
public function testStaticPropertyWithPrefix() public function testStaticPropertyWithPrefix()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/static_property_with_prefix.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/static_property_with_prefix.php');
$this->project->openDocument($completionUri, file_get_contents($completionUri)); $this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion( $items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(2, 14) new Position(2, 14)
@ -215,7 +220,7 @@ class CompletionTest extends TestCase
public function testStaticWithoutPrefix() public function testStaticWithoutPrefix()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/static.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/static.php');
$this->project->openDocument($completionUri, file_get_contents($completionUri)); $this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion( $items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(2, 11) new Position(2, 11)
@ -248,25 +253,7 @@ class CompletionTest extends TestCase
public function testStaticMethodWithPrefix() public function testStaticMethodWithPrefix()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/static_method_with_prefix.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/static_method_with_prefix.php');
$this->project->openDocument($completionUri, file_get_contents($completionUri)); $this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri),
new Position(2, 13)
)->wait();
$this->assertEquals(new CompletionList([
new CompletionItem(
'staticTestMethod',
CompletionItemKind::METHOD,
'mixed', // Method return type
'Do magna consequat veniam minim proident eiusmod incididunt aute proident.'
)
], true), $items);
}
public function testClassConstWithPrefix()
{
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/class_const_with_prefix.php');
$this->project->openDocument($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion( $items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(2, 13) new Position(2, 13)
@ -277,6 +264,54 @@ class CompletionTest extends TestCase
CompletionItemKind::VARIABLE, CompletionItemKind::VARIABLE,
'int', 'int',
'Anim labore veniam consectetur laboris minim quis aute aute esse nulla ad.' 'Anim labore veniam consectetur laboris minim quis aute aute esse nulla ad.'
),
new CompletionItem(
'staticTestProperty',
CompletionItemKind::PROPERTY,
'\TestClass[]',
'Lorem excepteur officia sit anim velit veniam enim.',
null,
null,
'$staticTestProperty'
),
new CompletionItem(
'staticTestMethod',
CompletionItemKind::METHOD,
'mixed',
'Do magna consequat veniam minim proident eiusmod incididunt aute proident.'
)
], true), $items);
}
public function testClassConstWithPrefix()
{
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/class_const_with_prefix.php');
$this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri),
new Position(2, 13)
)->wait();
$this->assertEquals(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,
'mixed',
'Do magna consequat veniam minim proident eiusmod incididunt aute proident.'
) )
], true), $items); ], true), $items);
} }
@ -284,7 +319,7 @@ class CompletionTest extends TestCase
public function testFullyQualifiedClass() public function testFullyQualifiedClass()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/fully_qualified_class.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/fully_qualified_class.php');
$this->project->openDocument($completionUri, file_get_contents($completionUri)); $this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion( $items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(6, 6) new Position(6, 6)
@ -305,7 +340,7 @@ class CompletionTest extends TestCase
public function testKeywords() public function testKeywords()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/keywords.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/keywords.php');
$this->project->openDocument($completionUri, file_get_contents($completionUri)); $this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion( $items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(2, 1) new Position(2, 1)
@ -319,7 +354,7 @@ class CompletionTest extends TestCase
public function testHtmlWithoutPrefix() public function testHtmlWithoutPrefix()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html.php');
$this->project->openDocument($completionUri, file_get_contents($completionUri)); $this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion( $items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(0, 0) new Position(0, 0)
@ -341,7 +376,7 @@ class CompletionTest extends TestCase
public function testHtmlWithPrefix() public function testHtmlWithPrefix()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html_with_prefix.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html_with_prefix.php');
$this->project->openDocument($completionUri, file_get_contents($completionUri)); $this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion( $items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(0, 1) new Position(0, 1)
@ -363,7 +398,7 @@ class CompletionTest extends TestCase
public function testNamespace() public function testNamespace()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/namespace.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/namespace.php');
$this->project->openDocument($completionUri, file_get_contents($completionUri)); $this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion( $items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(4, 6) new Position(4, 6)

View File

@ -5,7 +5,9 @@ namespace LanguageServer\Tests\Server\TextDocument\Definition;
use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\Tests\Server\ServerTestCase; use LanguageServer\Tests\Server\ServerTestCase;
use LanguageServer\{Server, LanguageClient, Project}; use LanguageServer\{Server, LanguageClient, PhpDocumentLoader, DefinitionResolver};
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex};
use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Protocol\{TextDocumentIdentifier, Position, Range, Location, ClientCapabilities}; use LanguageServer\Protocol\{TextDocumentIdentifier, Position, Range, Location, ClientCapabilities};
use Sabre\Event\Promise; use Sabre\Event\Promise;
@ -13,11 +15,14 @@ class GlobalFallbackTest extends ServerTestCase
{ {
public function setUp() public function setUp()
{ {
$projectIndex = new ProjectIndex(new Index, new DependenciesIndex);
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$project = new Project($client, new ClientCapabilities); $definitionResolver = new DefinitionResolver($projectIndex);
$this->textDocument = new Server\TextDocument($project, $client); $contentRetriever = new FileSystemContentRetriever;
$project->openDocument('global_fallback', file_get_contents(__DIR__ . '/../../../../fixtures/global_fallback.php')); $loader = new PhpDocumentLoader($contentRetriever, $projectIndex, $definitionResolver);
$project->openDocument('global_symbols', file_get_contents(__DIR__ . '/../../../../fixtures/global_symbols.php')); $this->textDocument = new Server\TextDocument($loader, $definitionResolver, $client, $projectIndex);
$loader->open('global_fallback', file_get_contents(__DIR__ . '/../../../../fixtures/global_fallback.php'));
$loader->open('global_symbols', file_get_contents(__DIR__ . '/../../../../fixtures/global_symbols.php'));
} }
public function testClassDoesNotFallback() public function testClassDoesNotFallback()

View File

@ -5,7 +5,9 @@ namespace LanguageServer\Tests\Server\TextDocument;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, Client, LanguageClient, Project}; use LanguageServer\{Server, Client, LanguageClient, PhpDocumentLoader, DefinitionResolver};
use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex};
use LanguageServer\Protocol\{ use LanguageServer\Protocol\{
TextDocumentIdentifier, TextDocumentIdentifier,
TextDocumentItem, TextDocumentItem,
@ -20,10 +22,12 @@ class DidChangeTest extends TestCase
{ {
public function test() public function test()
{ {
$projectIndex = new ProjectIndex(new Index, new DependenciesIndex);
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$project = new Project($client, new ClientCapabilities); $definitionResolver = new DefinitionResolver($projectIndex);
$textDocument = new Server\TextDocument($project, $client); $loader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver);
$phpDocument = $project->openDocument('whatever', "<?php\necho 'Hello, World'\n"); $textDocument = new Server\TextDocument($loader, $definitionResolver, $client, $projectIndex);
$phpDocument = $loader->open('whatever', "<?php\necho 'Hello, World'\n");
$identifier = new VersionedTextDocumentIdentifier('whatever'); $identifier = new VersionedTextDocumentIdentifier('whatever');
$changeEvent = new TextDocumentContentChangeEvent(); $changeEvent = new TextDocumentContentChangeEvent();

View File

@ -5,7 +5,9 @@ namespace LanguageServer\Tests\Server\TextDocument;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, Client, LanguageClient, Project}; use LanguageServer\{Server, Client, LanguageClient, PhpDocumentLoader, DefinitionResolver};
use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex};
use LanguageServer\Protocol\{TextDocumentItem, TextDocumentIdentifier, ClientCapabilities}; use LanguageServer\Protocol\{TextDocumentItem, TextDocumentIdentifier, ClientCapabilities};
use Exception; use Exception;
@ -13,10 +15,12 @@ class DidCloseTest extends TestCase
{ {
public function test() public function test()
{ {
$projectIndex = new ProjectIndex(new Index, new DependenciesIndex);
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$project = new Project($client, new ClientCapabilities); $definitionResolver = new DefinitionResolver($projectIndex);
$textDocument = new Server\TextDocument($project, $client); $loader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver);
$phpDocument = $project->openDocument('whatever', 'hello world'); $textDocument = new Server\TextDocument($loader, $definitionResolver, $client, $projectIndex);
$phpDocument = $loader->open('whatever', "<?php\necho 'Hello, World'\n");
$textDocumentItem = new TextDocumentItem(); $textDocumentItem = new TextDocumentItem();
$textDocumentItem->uri = 'whatever'; $textDocumentItem->uri = 'whatever';
@ -27,6 +31,6 @@ class DidCloseTest extends TestCase
$textDocument->didClose(new TextDocumentIdentifier($textDocumentItem->uri)); $textDocument->didClose(new TextDocumentIdentifier($textDocumentItem->uri));
$this->assertFalse($project->isDocumentOpen($textDocumentItem->uri)); $this->assertFalse($loader->isOpen($textDocumentItem->uri));
} }
} }

View File

@ -5,7 +5,9 @@ namespace LanguageServer\Tests\Server\TextDocument;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, Client, LanguageClient, Project}; use LanguageServer\{Server, Client, LanguageClient, PhpDocumentLoader, DefinitionResolver};
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex};
use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Protocol\{ use LanguageServer\Protocol\{
TextDocumentIdentifier, TextDocumentIdentifier,
TextDocumentItem, TextDocumentItem,
@ -19,23 +21,14 @@ use function LanguageServer\{pathToUri, uriToPath};
class FormattingTest extends TestCase class FormattingTest extends TestCase
{ {
/**
* @var Server\TextDocument
*/
private $textDocument;
public function setUp()
{
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$project = new Project($client, new ClientCapabilities);
$this->textDocument = new Server\TextDocument($project, $client);
}
public function testFormatting() public function testFormatting()
{ {
$projectIndex = new ProjectIndex(new Index, new DependenciesIndex);
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$project = new Project($client, new ClientCapabilities); $definitionResolver = new DefinitionResolver($projectIndex);
$textDocument = new Server\TextDocument($project, $client); $loader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver);
$textDocument = new Server\TextDocument($loader, $definitionResolver, $client, $projectIndex);
$path = realpath(__DIR__ . '/../../../fixtures/format.php'); $path = realpath(__DIR__ . '/../../../fixtures/format.php');
$uri = pathToUri($path); $uri = pathToUri($path);

View File

@ -26,6 +26,21 @@ class HoverTest extends ServerTestCase
], $reference->range), $result); ], $reference->range), $result);
} }
public function testHoverForClassLikeDefinition()
{
// class TestClass implements TestInterface
// Get hover for TestClass
$definition = $this->getDefinitionLocation('TestClass');
$result = $this->textDocument->hover(
new TextDocumentIdentifier($definition->uri),
$definition->range->start
)->wait();
$this->assertEquals(new Hover([
new MarkedString('php', "<?php\nclass TestClass implements \\TestInterface"),
'Pariatur ut laborum tempor voluptate consequat ea deserunt.'
], $definition->range), $result);
}
public function testHoverForMethod() public function testHoverForMethod()
{ {
// $obj->testMethod(); // $obj->testMethod();

View File

@ -5,7 +5,9 @@ namespace LanguageServer\Tests\Server\TextDocument;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, Client, LanguageClient, Project, ClientHandler}; use LanguageServer\{Server, Client, LanguageClient, ClientHandler, PhpDocumentLoader, DefinitionResolver};
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex};
use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Protocol\{TextDocumentIdentifier, TextDocumentItem, DiagnosticSeverity, ClientCapabilities}; use LanguageServer\Protocol\{TextDocumentIdentifier, TextDocumentItem, DiagnosticSeverity, ClientCapabilities};
use Sabre\Event\Promise; use Sabre\Event\Promise;
use JsonMapper; use JsonMapper;
@ -35,8 +37,10 @@ class ParseErrorsTest extends TestCase
return Promise\resolve(null); return Promise\resolve(null);
} }
}; };
$project = new Project($client, new ClientCapabilities); $projectIndex = new ProjectIndex(new Index, new DependenciesIndex);
$this->textDocument = new Server\TextDocument($project, $client); $definitionResolver = new DefinitionResolver($projectIndex);
$loader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver);
$this->textDocument = new Server\TextDocument($loader, $definitionResolver, $client, $projectIndex);
} }
private function openFile($file) private function openFile($file)

View File

@ -5,7 +5,9 @@ namespace LanguageServer\Tests\Server\TextDocument\References;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, LanguageClient, Project}; use LanguageServer\{Server, LanguageClient, PhpDocumentLoader, DefinitionResolver};
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex};
use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Protocol\{TextDocumentIdentifier, Position, ReferenceContext, Location, Range, ClientCapabilities}; use LanguageServer\Protocol\{TextDocumentIdentifier, Position, ReferenceContext, Location, Range, ClientCapabilities};
use LanguageServer\Tests\Server\ServerTestCase; use LanguageServer\Tests\Server\ServerTestCase;
@ -13,11 +15,13 @@ class GlobalFallbackTest extends ServerTestCase
{ {
public function setUp() public function setUp()
{ {
$projectIndex = new ProjectIndex(new Index, new DependenciesIndex);
$definitionResolver = new DefinitionResolver($projectIndex);
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$project = new Project($client, new ClientCapabilities); $this->documentLoader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver);
$this->textDocument = new Server\TextDocument($project, $client); $this->textDocument = new Server\TextDocument($this->documentLoader, $definitionResolver, $client, $projectIndex);
$project->openDocument('global_fallback', file_get_contents(__DIR__ . '/../../../../fixtures/global_fallback.php')); $this->documentLoader->open('global_fallback', file_get_contents(__DIR__ . '/../../../../fixtures/global_fallback.php'));
$project->openDocument('global_symbols', file_get_contents(__DIR__ . '/../../../../fixtures/global_symbols.php')); $this->documentLoader->open('global_symbols', file_get_contents(__DIR__ . '/../../../../fixtures/global_symbols.php'));
} }
public function testClassDoesNotFallback() public function testClassDoesNotFallback()

View File

@ -146,4 +146,17 @@ class GlobalTest extends ServerTestCase
new Location($referencesUri, new Range(new Position(31, 13), new Position(31, 40))) new Location($referencesUri, new Range(new Position(31, 13), new Position(31, 40)))
], $result); ], $result);
} }
public function testReferencesForReference()
{
// $obj = new TestClass();
// Get references for TestClass
$reference = $this->getReferenceLocations('TestClass')[0];
$result = $this->textDocument->references(
new ReferenceContext,
new TextDocumentIdentifier($reference->uri),
$reference->range->start
)->wait();
$this->assertEquals($this->getReferenceLocations('TestClass'), $result);
}
} }