diff --git a/.travis.yml b/.travis.yml index ca1b4ec..caefbec 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,7 @@ install: - composer install script: + - vendor/bin/phpcs -n - vendor/bin/phpunit --coverage-clover=coverage.xml after_success: diff --git a/README.md b/README.md index b14373d..4d3f0f5 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,109 @@ [![License](https://img.shields.io/packagist/l/felixfbecker/language-server.svg)](https://github.com/felixfbecker/php-language-server/blob/master/LICENSE.txt) [![Gitter](https://badges.gitter.im/felixfbecker/php-language-server.svg)](https://gitter.im/felixfbecker/php-language-server?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) -A pure PHP implementation of the [Language Server Protocol](https://github.com/Microsoft/language-server-protocol). +A pure PHP implementation of the open [Language Server Protocol](https://github.com/Microsoft/language-server-protocol). +Provides static code analysis for PHP for any IDE. -![Find all symbols demo](images/documentSymbol.gif) +Uses the great [PHP-Parser](https://github.com/nikic/PHP-Parser), +[phpDocumentor's DocBlock reflection](https://github.com/phpDocumentor/ReflectionDocBlock) +and an [event loop](http://sabre.io/event/loop/) for concurrency. + +## Features + +### [Go To Definition](https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#goto-definition-request) +![Go To Definition demo](images/definition.gif) + +### [Find References](https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#find-references-request) +![Find References demo](images/references.png) + +### [Hover](https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#hover-request) +![Hover class demo](images/hoverClass.png) + +![Hover parameter demo](images/hoverParam.png) + +A hover request returns a declaration line (marked with language `php`) and the summary of the docblock. +For Parameters, it will return the `@param` tag. + +### [Document Symbols](https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#document-symbols-request) +![Document Symbols demo](images/documentSymbol.gif) + +### [Workspace Symbols](https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#workspace-symbols-request) +![Workspace Symbols demo](images/workspaceSymbol.gif) + +The query is matched case-insensitively against the fully qualified name of the symbol. +Non-Standard: An empty query will return _all_ symbols found in the workspace. + +### [Document Formatting](https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#document-formatting-request) +![Document Formatting demo](images/formatDocument.gif) + +### Error reporting through [Publish Diagnostics](https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#publishdiagnostics-notification) +![Error reporting demo](images/publishDiagnostics.png) + +PHP parse errors are reported as errors, parse errors of docblocks are reported as warnings. + +### What is considered a definition? + +Globally searchable definitions are: + - classes + - interfaces + - traits + - properties + - methods + - class constants + - constants with `const` keyword + +Definitions resolved just-in-time when needed: + - variable assignments + - parameters + - closure `use` statements + +Not supported yet: + - constants with `define()` + +Namespaces are not considerd a declaration by design because they only make up a part of the fully qualified name +and don't map to one unique declaration. + +### What is considered a reference? + +Definitions/references/hover currently work for + - class instantiations + - static method calls + - class constant access + - static property access + - parameter type hints + - return type hints + - method calls, if the variable was assigned to a new object in the same scope + - property access, if the variable was assigned to a new object in the same scope + - variables + - parameters + - imported closure variables (`use`) + - `use` statements for classes, constants and functions + - class-like after `implements`/`extends` + - function calls + - constant access + - `instanceof` checks + +They do not work yet for: + - Reassigned variables + - Nested access/calls on return values or properties + +## Performance + +Upon initialization, the server will recursively scan the project directory for PHP files, parse them and add all definitions +and references to an in-memory index. +The time this takes depends on the project size. +At the time of writing, this project contains 78 files + 1560 files in dependencies which take 97s to parse +and consume 76 MB on a Surface Pro 3. +The language server is fully operational while indexing and can respond to requests with the definitions already indexed. +Follow-up requests will be almost instant because the index is kept in memory. + +## Versioning + +This project follows [semver](http://semver.org/) for the protocol communication and command line parameters, +e.g. a major version increase of the LSP will result in a major version increase of the PHP LS. +New features like request implementations will result in a new minor version. +Everything else will be a patch release. +All classes are considered internal and are not subject to semver. ## Used by - [vscode-php-intellisense](https://github.com/felixfbecker/vscode-php-intellisense) @@ -26,20 +126,27 @@ to install dependencies. Run the tests with - vendor/bin/phpunit --bootstrap vendor/autoload.php tests + vendor/bin/phpunit + +Lint with + + vendor/bin/phpcs ## Command line arguments -###### --tcp=host:port (optional) +### `--tcp=host:port` (optional) Causes the server to use a tcp connection for communicating with the language client instead of using STDIN/STDOUT. The server will try to connect to the specified address. +Strongly recommended on Windows because of blocking STDIO. Example: php bin/php-language-server.php --tcp=127.0.0.1:12345 -###### --memory-limit=integer (optional) -Sets memory limit for language server. Equivalent to [memory-limit](http://php.net/manual/en/ini.core.php#ini.memory-limit) *php.ini* directive. By default there is no memory limit. +### `--memory-limit=integer` (optional) +Sets memory limit for language server. +Equivalent to [memory-limit](http://php.net/manual/en/ini.core.php#ini.memory-limit) php.ini directive. +By default there is no memory limit. Example: diff --git a/bin/php-language-server.php b/bin/php-language-server.php index 47e180b..49bd29a 100644 --- a/bin/php-language-server.php +++ b/bin/php-language-server.php @@ -17,7 +17,7 @@ foreach ([__DIR__ . '/../../../autoload.php', __DIR__ . '/../autoload.php', __DI ErrorHandler::register(); -cli_set_process_title('PHP Language Server'); +@cli_set_process_title('PHP Language Server'); if (!empty($options['tcp'])) { $address = $options['tcp']; diff --git a/composer.json b/composer.json index 5f52bca..6d9159b 100644 --- a/composer.json +++ b/composer.json @@ -24,9 +24,9 @@ "bin": ["bin/php-language-server.php"], "require": { "php": ">=7.0", - "nikic/php-parser": "dev-master#90834bff8eaf7b7f893253f312e73d8f532341ca", + "nikic/php-parser": "dev-master#c0f0edf0441f0ddcff0604407b7600a40993faf2", "phpdocumentor/reflection-docblock": "^3.0", - "sabre/event": "^4.0", + "sabre/event": "^5.0", "felixfbecker/advanced-json-rpc": "^2.0", "squizlabs/php_codesniffer" : "^2.7", "symfony/debug": "^3.1" diff --git a/fixtures/format.php b/fixtures/format.php index f300b67..bd35640 100644 --- a/fixtures/format.php +++ b/fixtures/format.php @@ -13,7 +13,7 @@ class TestClass { $testVariable = 123; - if ( empty($testParameter)){ + if (empty($testParameter)){ echo 'Empty'; } } diff --git a/fixtures/global_references.php b/fixtures/global_references.php index 8f88247..8346923 100644 --- a/fixtures/global_references.php +++ b/fixtures/global_references.php @@ -28,3 +28,9 @@ $fn = function() use ($var) { }; echo TEST_CONST; + +use function test_function; + +if ($abc instanceof TestInterface) { + +} diff --git a/fixtures/references.php b/fixtures/references.php index 3f58d05..ca27443 100644 --- a/fixtures/references.php +++ b/fixtures/references.php @@ -28,3 +28,9 @@ $fn = function() use ($var) { }; echo TEST_CONST; + +use function TestNamespace\test_function; + +if ($abc instanceof TestInterface) { + +} diff --git a/images/definition.gif b/images/definition.gif new file mode 100644 index 0000000..f283b3c Binary files /dev/null and b/images/definition.gif differ diff --git a/images/formatDocument.gif b/images/formatDocument.gif new file mode 100644 index 0000000..d8e49fe Binary files /dev/null and b/images/formatDocument.gif differ diff --git a/images/hoverClass.png b/images/hoverClass.png new file mode 100644 index 0000000..29dfcc3 Binary files /dev/null and b/images/hoverClass.png differ diff --git a/images/hoverParam.png b/images/hoverParam.png new file mode 100644 index 0000000..26171a9 Binary files /dev/null and b/images/hoverParam.png differ diff --git a/images/publishDiagnostics.png b/images/publishDiagnostics.png new file mode 100644 index 0000000..4499b70 Binary files /dev/null and b/images/publishDiagnostics.png differ diff --git a/images/references.png b/images/references.png new file mode 100644 index 0000000..15e50ea Binary files /dev/null and b/images/references.png differ diff --git a/images/workspaceSymbol.gif b/images/workspaceSymbol.gif new file mode 100644 index 0000000..a3f0fca Binary files /dev/null and b/images/workspaceSymbol.gif differ diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 0000000..e67104b --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,10 @@ + + + src + tests + + + + + + diff --git a/src/Formatter.php b/src/Formatter.php index 5f4744f..37e3f4c 100644 --- a/src/Formatter.php +++ b/src/Formatter.php @@ -4,8 +4,8 @@ declare(strict_types = 1); namespace LanguageServer; use LanguageServer\Protocol\ { - TextEdit, - Range, + TextEdit, + Range, Position }; use PHP_CodeSniffer; @@ -42,9 +42,9 @@ abstract class Formatter /** * Calculate position of last character. - * + * * @param string $content document as string - * + * * @return \LanguageServer\Protocol\Position */ private static function calculateEndPosition(string $content): Position @@ -54,10 +54,10 @@ abstract class Formatter } /** - * Search for PHP_CodeSniffer configuration file at given directory or its parents. - * If no configuration found then PSR2 standard is loaded by default. + * Search for PHP_CodeSniffer configuration file at given directory or its parents. + * If no configuration found then PSR2 standard is loaded by default. * - * @param string $path path to file or directory + * @param string $path path to file or directory * @return string[] */ private static function findConfiguration(string $path) @@ -85,5 +85,4 @@ abstract class Formatter $standard = PHP_CodeSniffer::getConfigData('default_standard') ?? 'PSR2'; return explode(',', $standard); } - } diff --git a/src/Fqn.php b/src/Fqn.php index a361a7d..f5ef00d 100644 --- a/src/Fqn.php +++ b/src/Fqn.php @@ -35,6 +35,7 @@ function getReferencedFqn(Node $node) || $parent instanceof Node\Expr\StaticCall || $parent instanceof Node\Expr\ClassConstFetch || $parent instanceof Node\Expr\StaticPropertyFetch + || $parent instanceof Node\Expr\Instanceof_ ) ) { // For extends, implements, type hints and classes of classes of static calls use the name directly @@ -45,6 +46,8 @@ function getReferencedFqn(Node $node) $grandParent = $parent->getAttribute('parentNode'); if ($grandParent instanceof Node\Stmt\GroupUse) { $name = $grandParent->prefix . '\\' . $name; + } else if ($grandParent instanceof Node\Stmt\Use_ && $grandParent->type === Node\Stmt\Use_::TYPE_FUNCTION) { + $name .= '()'; } // Only the name node should be considered a reference, not the New_ node itself } else if ($parent instanceof Node\Expr\New_) { diff --git a/src/LanguageServer.php b/src/LanguageServer.php index 352eda3..42beeac 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -103,7 +103,6 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher // start building project index if ($rootPath !== null) { - $this->restoreCache(); $this->indexProject(); } @@ -139,9 +138,6 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher */ public function shutdown() { - if ($this->rootPath !== null) { - $this->saveCache(); - } } /** @@ -167,7 +163,7 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher $startTime = microtime(true); $fileNum = 0; - $processFile = function() use (&$fileList, &$fileNum, &$processFile, $numTotalFiles, $startTime) { + $processFile = function () use (&$fileList, &$fileNum, &$processFile, $numTotalFiles, $startTime) { if ($fileNum < $numTotalFiles) { $file = $fileList[$fileNum]; $uri = pathToUri($file); @@ -185,69 +181,14 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher } } - if ($fileNum % 1000 === 0) { - $this->saveCache(); - } - Loop\setTimeout($processFile, 0); } else { $duration = (int)(microtime(true) - $startTime); $mem = (int)(memory_get_usage(true) / (1024 * 1024)); $this->client->window->logMessage(MessageType::INFO, "All PHP files parsed in $duration seconds. $mem MiB allocated."); - $this->saveCache(); } }; Loop\setTimeout($processFile, 0); } - - /** - * Restores the definition and reference index from the .phpls cache directory, if available - * - * @return void - */ - public function restoreCache() - { - $cacheDir = $this->rootPath . '/.phpls'; - if (is_dir($cacheDir)) { - if (file_exists($cacheDir . '/symbols')) { - $symbols = unserialize(file_get_contents($cacheDir . '/symbols')); - $count = count($symbols); - $this->project->setSymbols($symbols); - $this->client->window->logMessage(MessageType::INFO, "Restoring $count symbols"); - } - if (file_exists($cacheDir . '/references')) { - $references = unserialize(file_get_contents($cacheDir . '/references')); - $count = array_sum(array_map('count', $references)); - $this->project->setReferenceUris($references); - $this->client->window->logMessage(MessageType::INFO, "Restoring $count references"); - } - } else { - $this->client->window->logMessage(MessageType::INFO, 'No cache found'); - } - } - - /** - * Saves the definition and reference index to the .phpls cache directory - * - * @return void - */ - public function saveCache() - { - // Cache definitions, references - $cacheDir = $this->rootPath . '/.phpls'; - if (!is_dir($cacheDir)) { - mkdir($cacheDir); - } - - $symbols = $this->project->getSymbols(); - $count = count($symbols); - $this->client->window->logMessage(MessageType::INFO, "Saving $count symbols to cache"); - file_put_contents($cacheDir . "/symbols", serialize($symbols)); - - $references = $this->project->getReferenceUris(); - $count = array_sum(array_map('count', $references)); - $this->client->window->logMessage(MessageType::INFO, "Saving $count references to cache"); - file_put_contents($cacheDir . "/references", serialize($references)); - } } diff --git a/src/Parser.php b/src/Parser.php new file mode 100644 index 0000000..d6d6d13 --- /dev/null +++ b/src/Parser.php @@ -0,0 +1,25 @@ + [ + 'comments', + 'startLine', + 'endLine', + 'startFilePos', + 'endFilePos' + ] + ]); + parent::__construct($lexer); + } +} diff --git a/src/PhpDocument.php b/src/PhpDocument.php index ea5d732..cbd22c5 100644 --- a/src/PhpDocument.php +++ b/src/PhpDocument.php @@ -13,7 +13,7 @@ use LanguageServer\NodeVisitor\{ ReferencesCollector, VariableReferencesCollector }; -use PhpParser\{Error, Node, NodeTraverser, Parser}; +use PhpParser\{Error, ErrorHandler, Node, NodeTraverser}; use PhpParser\NodeVisitor\NameResolver; use phpDocumentor\Reflection\DocBlockFactory; use function LanguageServer\Fqn\{getDefinedFqn, getVariableDefinition, getReferencedFqn}; @@ -142,19 +142,12 @@ class PhpDocument $this->completionReporter = new CompletionReporter($this); $stmts = null; - $errors = []; - try { - $stmts = $this->parser->parse($content); - } catch (\PhpParser\Error $e) { - // Lexer can throw errors. e.g for unterminated comments - // unfortunately we don't get a location back - $errors[] = $e; - } - $errors = array_merge($this->parser->getErrors(), $errors); + $errorHandler = new ErrorHandler\Collecting; + $stmts = $this->parser->parse($content, $errorHandler); $diagnostics = []; - foreach ($errors as $error) { + foreach ($errorHandler->getErrors() as $error) { $diagnostics[] = Diagnostic::fromError($error, $this->content, DiagnosticSeverity::ERROR, 'php'); } @@ -163,7 +156,7 @@ class PhpDocument $traverser = new NodeTraverser; // Resolve aliased names to FQNs - $traverser->addVisitor(new NameResolver); + $traverser->addVisitor(new NameResolver($errorHandler)); // Add parentNode, previousSibling, nextSibling attributes $traverser->addVisitor(new ReferencesAdder($this)); diff --git a/src/Project.php b/src/Project.php index 5e8a5f9..0a5349f 100644 --- a/src/Project.php +++ b/src/Project.php @@ -5,7 +5,6 @@ namespace LanguageServer; use LanguageServer\Protocol\SymbolInformation; use phpDocumentor\Reflection\DocBlockFactory; -use PhpParser\{ParserFactory, Lexer}; class Project { @@ -34,7 +33,7 @@ class Project /** * Instance of the PHP parser * - * @var ParserAbstract + * @var Parser */ private $parser; @@ -56,8 +55,7 @@ class Project { $this->client = $client; - $lexer = new Lexer(['usedAttributes' => ['comments', 'startLine', 'endLine', 'startFilePos', 'endFilePos']]); - $this->parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7, $lexer, ['throwOnError' => false]); + $this->parser = new Parser; $this->docBlockFactory = DocBlockFactory::createInstance(); } @@ -178,7 +176,8 @@ class Project * @param string $fqn The fully qualified name of the symbol * @return void */ - public function removeSymbol(string $fqn) { + public function removeSymbol(string $fqn) + { unset($this->symbols[$fqn]); unset($this->references[$fqn]); } @@ -207,7 +206,8 @@ class Project * @param string $uri The URI * @return void */ - public function removeReferenceUri(string $fqn, string $uri) { + public function removeReferenceUri(string $fqn, string $uri) + { if (!isset($this->references[$fqn])) { return; } diff --git a/src/Protocol/CompletionItemKind.php b/src/Protocol/CompletionItemKind.php index 043169d..fad9b47 100644 --- a/src/Protocol/CompletionItemKind.php +++ b/src/Protocol/CompletionItemKind.php @@ -7,7 +7,8 @@ use PhpParser\Node; /** * The kind of a completion entry. */ -abstract class CompletionItemKind { +abstract class CompletionItemKind +{ const TEXT = 1; const METHOD = 2; const FUNCTION = 3; diff --git a/src/Protocol/ReferenceContext.php b/src/Protocol/ReferenceContext.php index f0c3b12..bd546d5 100644 --- a/src/Protocol/ReferenceContext.php +++ b/src/Protocol/ReferenceContext.php @@ -4,10 +4,10 @@ namespace LanguageServer\Protocol; class ReferenceContext { - /** - * Include the declaration of the current symbol. + /** + * Include the declaration of the current symbol. * * @var bool - */ - public $includeDeclaration; + */ + public $includeDeclaration; } diff --git a/src/ProtocolStreamReader.php b/src/ProtocolStreamReader.php index 59af308..2d0e351 100644 --- a/src/ProtocolStreamReader.php +++ b/src/ProtocolStreamReader.php @@ -7,16 +7,13 @@ use LanguageServer\Protocol\Message; use AdvancedJsonRpc\Message as MessageBody; use Sabre\Event\Loop; -abstract class ParsingMode -{ - const HEADERS = 1; - const BODY = 2; -} - class ProtocolStreamReader implements ProtocolReader { + const PARSE_HEADERS = 1; + const PARSE_BODY = 2; + private $input; - private $parsingMode = ParsingMode::HEADERS; + private $parsingMode = self::PARSE_HEADERS; private $buffer = ''; private $headers = []; private $contentLength; @@ -28,13 +25,14 @@ class ProtocolStreamReader implements ProtocolReader public function __construct($input) { $this->input = $input; - Loop\addReadStream($this->input, function() { + + Loop\addReadStream($this->input, function () { while (($c = fgetc($this->input)) !== false && $c !== '') { $this->buffer .= $c; switch ($this->parsingMode) { - case ParsingMode::HEADERS: + case self::PARSE_HEADERS: if ($this->buffer === "\r\n") { - $this->parsingMode = ParsingMode::BODY; + $this->parsingMode = self::PARSE_BODY; $this->contentLength = (int)$this->headers['Content-Length']; $this->buffer = ''; } else if (substr($this->buffer, -2) === "\r\n") { @@ -43,14 +41,14 @@ class ProtocolStreamReader implements ProtocolReader $this->buffer = ''; } break; - case ParsingMode::BODY: + case self::PARSE_BODY: if (strlen($this->buffer) === $this->contentLength) { if (isset($this->listener)) { $msg = new Message(MessageBody::parse($this->buffer), $this->headers); $listener = $this->listener; $listener($msg); } - $this->parsingMode = ParsingMode::HEADERS; + $this->parsingMode = self::PARSE_HEADERS; $this->headers = []; $this->buffer = ''; } diff --git a/src/ProtocolStreamWriter.php b/src/ProtocolStreamWriter.php index 2ac3579..41d7afc 100644 --- a/src/ProtocolStreamWriter.php +++ b/src/ProtocolStreamWriter.php @@ -4,12 +4,24 @@ declare(strict_types = 1); namespace LanguageServer; use LanguageServer\Protocol\Message; +use Sabre\Event\{ + Loop, + Promise +}; use RuntimeException; class ProtocolStreamWriter implements ProtocolWriter { + /** + * @var resource $output + */ private $output; + /** + * @var array $messages + */ + private $messages = []; + /** * @param resource $output */ @@ -22,26 +34,58 @@ class ProtocolStreamWriter implements ProtocolWriter * Sends a Message to the client * * @param Message $msg - * @return void + * @return Promise Resolved when the message has been fully written out to the output stream */ public function write(Message $msg) { - $data = (string)$msg; - $msgSize = strlen($data); - $totalBytesWritten = 0; + // if the message queue is currently empty, register a write handler. + if (empty($this->messages)) { + Loop\addWriteStream($this->output, function () { + $this->flush(); + }); + } - while ($totalBytesWritten < $msgSize) { - error_clear_last(); - $bytesWritten = @fwrite($this->output, substr($data, $totalBytesWritten)); - if ($bytesWritten === false) { - $error = error_get_last(); - if ($error !== null) { - throw new RuntimeException('Could not write message: ' . error_get_last()['message']); - } else { - throw new RuntimeException('Could not write message'); - } + $promise = new Promise(); + $this->messages[] = [ + 'message' => (string)$msg, + 'promise' => $promise + ]; + return $promise; + } + + /** + * Writes pending messages to the output stream. + * + * @return void + */ + private function flush() + { + $keepWriting = true; + while ($keepWriting) { + $message = $this->messages[0]['message']; + $promise = $this->messages[0]['promise']; + + $bytesWritten = @fwrite($this->output, $message); + + if ($bytesWritten > 0) { + $message = substr($message, $bytesWritten); + } + + // Determine if this message was completely sent + if (strlen($message) === 0) { + array_shift($this->messages); + + // This was the last message in the queue, remove the write handler. + if (count($this->messages) === 0) { + Loop\removeWriteStream($this->output); + $keepWriting = false; + } + + $promise->fulfill(); + } else { + $this->messages[0]['message'] = $message; + $keepWriting = false; } - $totalBytesWritten += $bytesWritten; } } } diff --git a/src/utils.php b/src/utils.php index c89c2d3..810fbc1 100644 --- a/src/utils.php +++ b/src/utils.php @@ -12,7 +12,8 @@ use InvalidArgumentException; * @param string $pattern * @return array */ -function findFilesRecursive(string $path, string $pattern): array { +function findFilesRecursive(string $path, string $pattern): array +{ $dir = new \RecursiveDirectoryIterator($path); $ite = new \RecursiveIteratorIterator($dir); $files = new \RegexIterator($ite, $pattern, \RegexIterator::GET_MATCH); @@ -29,7 +30,8 @@ function findFilesRecursive(string $path, string $pattern): array { * @param string $filepath * @return string */ -function pathToUri(string $filepath): string { +function pathToUri(string $filepath): string +{ $filepath = trim(str_replace('\\', '/', $filepath), '/'); $parts = explode('/', $filepath); // Don't %-encode the colon after a Windows drive letter diff --git a/tests/MockProtocolStream.php b/tests/MockProtocolStream.php index ce2c361..a6cf1f4 100644 --- a/tests/MockProtocolStream.php +++ b/tests/MockProtocolStream.php @@ -36,4 +36,3 @@ class MockProtocolStream implements ProtocolReader, ProtocolWriter $this->listener = $listener; } } - diff --git a/tests/NodeVisitor/DefinitionCollectorTest.php b/tests/NodeVisitor/DefinitionCollectorTest.php index 10ec058..1ea7b68 100644 --- a/tests/NodeVisitor/DefinitionCollectorTest.php +++ b/tests/NodeVisitor/DefinitionCollectorTest.php @@ -4,9 +4,9 @@ declare(strict_types = 1); namespace LanguageServer\Tests\Server\TextDocument; use PHPUnit\Framework\TestCase; -use PhpParser\{ParserFactory, NodeTraverser, Node}; +use PhpParser\{NodeTraverser, Node}; use PhpParser\NodeVisitor\NameResolver; -use LanguageServer\{LanguageClient, Project, PhpDocument}; +use LanguageServer\{LanguageClient, Project, PhpDocument, Parser}; use LanguageServer\Tests\MockProtocolStream; use LanguageServer\NodeVisitor\{ReferencesAdder, DefinitionCollector}; use function LanguageServer\pathToUri; @@ -17,7 +17,7 @@ class DefinitionCollectorTest extends TestCase { $client = new LanguageClient(new MockProtocolStream()); $project = new Project($client); - $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7); + $parser = new Parser; $uri = pathToUri(realpath(__DIR__ . '/../../fixtures/symbols.php')); $document = $project->loadDocument($uri); $traverser = new NodeTraverser; @@ -56,7 +56,7 @@ class DefinitionCollectorTest extends TestCase { $client = new LanguageClient(new MockProtocolStream()); $project = new Project($client); - $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7); + $parser = new Parser; $uri = pathToUri(realpath(__DIR__ . '/../../fixtures/references.php')); $document = $project->loadDocument($uri); $traverser = new NodeTraverser; diff --git a/tests/ProtocolStreamWriterTest.php b/tests/ProtocolStreamWriterTest.php index 90ef32d..b481c3b 100644 --- a/tests/ProtocolStreamWriterTest.php +++ b/tests/ProtocolStreamWriterTest.php @@ -7,6 +7,7 @@ use PHPUnit\Framework\TestCase; use LanguageServer\ProtocolStreamWriter; use LanguageServer\Protocol\Message; use AdvancedJsonRpc\{Request as RequestBody}; +use Sabre\Event\Loop; class ProtocolStreamWriterTest extends TestCase { @@ -21,7 +22,14 @@ class ProtocolStreamWriterTest extends TestCase $msg = new Message(new RequestBody(1, 'aMethod', ['arg' => str_repeat('X', 100000)])); $msgString = (string)$msg; - $writer->write($msg); + $promise = $writer->write($msg); + $promise->then(function () { + Loop\stop(); + }, function () { + Loop\stop(); + }); + + Loop\run(); fclose($writeHandle); diff --git a/tests/Server/ServerTestCase.php b/tests/Server/ServerTestCase.php index 6728a9f..cb4ed19 100644 --- a/tests/Server/ServerTestCase.php +++ b/tests/Server/ServerTestCase.php @@ -59,6 +59,7 @@ abstract class ServerTestCase extends TestCase $this->project->loadDocument($globalReferencesUri); $this->project->loadDocument($useUri); + // @codingStandardsIgnoreStart $this->definitionLocations = [ // Global @@ -105,7 +106,8 @@ abstract class ServerTestCase extends TestCase ], 'TestNamespace\\TestInterface' => [ 0 => new Location($symbolsUri, new Range(new Position(20, 27), new Position(20, 40))), // class TestClass implements TestInterface - 1 => new Location($symbolsUri, new Range(new Position(57, 48), new Position(57, 61))) // public function testMethod($testParameter): TestInterface + 1 => new Location($symbolsUri, new Range(new Position(57, 48), new Position(57, 61))), // public function testMethod($testParameter): TestInterface + 2 => new Location($referencesUri, new Range(new Position(33, 20), new Position(33, 33))) // if ($abc instanceof TestInterface) ], 'TestNamespace\\TestClass::TEST_CLASS_CONST' => [ 0 => new Location($symbolsUri, new Range(new Position(48, 13), new Position(48, 35))), // echo self::TEST_CLASS_CONSTANT @@ -125,7 +127,8 @@ abstract class ServerTestCase extends TestCase 0 => new Location($referencesUri, new Range(new Position( 5, 0), new Position( 5, 18))) ], 'TestNamespace\\test_function()' => [ - 0 => new Location($referencesUri, new Range(new Position(10, 0), new Position(10, 13))) + 0 => new Location($referencesUri, new Range(new Position(10, 0), new Position(10, 13))), + 1 => new Location($referencesUri, new Range(new Position(31, 13), new Position(31, 40))) ], // Global @@ -143,7 +146,8 @@ abstract class ServerTestCase extends TestCase ], 'TestInterface' => [ 0 => new Location($globalSymbolsUri, new Range(new Position(20, 27), new Position(20, 40))), // class TestClass implements TestInterface - 1 => new Location($globalSymbolsUri, new Range(new Position(57, 48), new Position(57, 61))) // public function testMethod($testParameter): TestInterface + 1 => new Location($globalSymbolsUri, new Range(new Position(57, 48), new Position(57, 61))), // public function testMethod($testParameter): TestInterface + 2 => new Location($globalReferencesUri, new Range(new Position(33, 20), new Position(33, 33))) // if ($abc instanceof TestInterface) ], 'TestClass::TEST_CLASS_CONST' => [ 0 => new Location($globalSymbolsUri, new Range(new Position(48, 13), new Position(48, 35))), // echo self::TEST_CLASS_CONSTANT @@ -163,9 +167,11 @@ abstract class ServerTestCase extends TestCase 0 => new Location($globalReferencesUri, new Range(new Position( 5, 0), new Position( 5, 18))) ], 'test_function()' => [ - 0 => new Location($globalReferencesUri, new Range(new Position(10, 0), new Position(10, 13))) + 0 => new Location($globalReferencesUri, new Range(new Position(10, 0), new Position(10, 13))), + 1 => new Location($globalReferencesUri, new Range(new Position(31, 13), new Position(31, 40))) ] ]; + // @codingStandardsIgnoreEnd } protected function getDefinitionLocation(string $fqn): Location diff --git a/tests/Server/TextDocument/Definition/GlobalTest.php b/tests/Server/TextDocument/Definition/GlobalTest.php index 9fd325e..ded6f38 100644 --- a/tests/Server/TextDocument/Definition/GlobalTest.php +++ b/tests/Server/TextDocument/Definition/GlobalTest.php @@ -9,13 +9,15 @@ use function LanguageServer\pathToUri; class GlobalTest extends ServerTestCase { - public function testDefinitionFileBeginning() { + public function testDefinitionFileBeginning() + { // |textDocument->definition(new TextDocumentIdentifier(pathToUri(realpath(__DIR__ . '/../../../../fixtures/references.php'))), new Position(0, 0)); $this->assertEquals([], $result); } - public function testDefinitionEmptyResult() { + public function testDefinitionEmptyResult() + { // namespace keyword $result = $this->textDocument->definition(new TextDocumentIdentifier(pathToUri(realpath(__DIR__ . '/../../../../fixtures/references.php'))), new Position(2, 4)); $this->assertEquals([], $result); @@ -200,4 +202,22 @@ class GlobalTest extends ServerTestCase $result = $this->textDocument->definition(new TextDocumentIdentifier($reference->uri), $reference->range->start); $this->assertEquals($this->getDefinitionLocation('test_function()'), $result); } + + public function testDefinitionForUseFunctions() + { + // use function test_function; + // Get definition for test_function + $reference = $this->getReferenceLocations('test_function()')[1]; + $result = $this->textDocument->definition(new TextDocumentIdentifier($reference->uri), $reference->range->start); + $this->assertEquals($this->getDefinitionLocation('test_function()'), $result); + } + + public function testDefinitionForInstanceOf() + { + // if ($abc instanceof TestInterface) { + // Get definition for TestInterface + $reference = $this->getReferenceLocations('TestInterface')[2]; + $result = $this->textDocument->definition(new TextDocumentIdentifier($reference->uri), $reference->range->start); + $this->assertEquals($this->getDefinitionLocation('TestInterface'), $result); + } } diff --git a/tests/Server/TextDocument/DocumentSymbolTest.php b/tests/Server/TextDocument/DocumentSymbolTest.php index 803dcae..7b304d0 100644 --- a/tests/Server/TextDocument/DocumentSymbolTest.php +++ b/tests/Server/TextDocument/DocumentSymbolTest.php @@ -16,6 +16,7 @@ class DocumentSymbolTest extends ServerTestCase // Request symbols $uri = pathToUri(realpath(__DIR__ . '/../../../fixtures/symbols.php')); $result = $this->textDocument->documentSymbol(new TextDocumentIdentifier($uri)); + // @codingStandardsIgnoreStart $this->assertEquals([ new SymbolInformation('TEST_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TestNamespace\\TEST_CONST'), 'TestNamespace'), new SymbolInformation('TestClass', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\TestClass'), 'TestNamespace'), @@ -28,5 +29,6 @@ class DocumentSymbolTest extends ServerTestCase new SymbolInformation('TestInterface', SymbolKind::INTERFACE, $this->getDefinitionLocation('TestNamespace\\TestInterface'), 'TestNamespace'), new SymbolInformation('test_function', SymbolKind::FUNCTION, $this->getDefinitionLocation('TestNamespace\\test_function()'), 'TestNamespace'), ], $result); + // @codingStandardsIgnoreEnd } } diff --git a/tests/Server/TextDocument/ParseErrorsTest.php b/tests/Server/TextDocument/ParseErrorsTest.php index 37ce769..09efec7 100644 --- a/tests/Server/TextDocument/ParseErrorsTest.php +++ b/tests/Server/TextDocument/ParseErrorsTest.php @@ -36,7 +36,8 @@ class ParseErrorsTest extends TestCase $this->textDocument = new Server\TextDocument($project, $client); } - private function openFile($file) { + private function openFile($file) + { $textDocumentItem = new TextDocumentItem(); $textDocumentItem->uri = 'whatever'; $textDocumentItem->languageId = 'php'; diff --git a/tests/Server/TextDocument/References/GlobalTest.php b/tests/Server/TextDocument/References/GlobalTest.php index 284c32b..1ecf1f9 100644 --- a/tests/Server/TextDocument/References/GlobalTest.php +++ b/tests/Server/TextDocument/References/GlobalTest.php @@ -101,6 +101,9 @@ class GlobalTest extends ServerTestCase $referencesUri = pathToUri(realpath(__DIR__ . '/../../../../fixtures/references.php')); $symbolsUri = pathToUri(realpath(__DIR__ . '/../../../../fixtures/symbols.php')); $result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier($symbolsUri), new Position(78, 16)); - $this->assertEquals([new Location($referencesUri, new Range(new Position(10, 0), new Position(10, 13)))], $result); + $this->assertEquals([ + new Location($referencesUri, new Range(new Position(10, 0), new Position(10, 13))), + new Location($referencesUri, new Range(new Position(31, 13), new Position(31, 40))) + ], $result); } } diff --git a/tests/Server/Workspace/SymbolTest.php b/tests/Server/Workspace/SymbolTest.php index b029e77..7086942 100644 --- a/tests/Server/Workspace/SymbolTest.php +++ b/tests/Server/Workspace/SymbolTest.php @@ -16,6 +16,7 @@ class SymbolTest extends ServerTestCase { // Request symbols $result = $this->workspace->symbol(''); + // @codingStandardsIgnoreStart $this->assertEquals([ // Namespaced new SymbolInformation('TEST_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TestNamespace\\TEST_CONST'), 'TestNamespace'), @@ -42,17 +43,20 @@ class SymbolTest extends ServerTestCase new SymbolInformation('test_function', SymbolKind::FUNCTION, $this->getDefinitionLocation('test_function()'), ''), new SymbolInformation('whatever', SymbolKind::FUNCTION, $this->getDefinitionLocation('whatever()'), '') ], $result); + // @codingStandardsIgnoreEnd } public function testQueryFiltersResults() { // Request symbols $result = $this->workspace->symbol('testmethod'); + // @codingStandardsIgnoreStart $this->assertEquals([ new SymbolInformation('staticTestMethod', SymbolKind::METHOD, $this->getDefinitionLocation('TestNamespace\\TestClass::staticTestMethod()'), 'TestNamespace\\TestClass'), new SymbolInformation('testMethod', SymbolKind::METHOD, $this->getDefinitionLocation('TestNamespace\\TestClass::testMethod()'), 'TestNamespace\\TestClass'), new SymbolInformation('staticTestMethod', SymbolKind::METHOD, $this->getDefinitionLocation('TestClass::staticTestMethod()'), 'TestClass'), new SymbolInformation('testMethod', SymbolKind::METHOD, $this->getDefinitionLocation('TestClass::testMethod()'), 'TestClass') ], $result); + // @codingStandardsIgnoreEnd } }