2018-10-17 21:52:26 +02:00
|
|
|
<?php
|
|
|
|
declare(strict_types = 1);
|
2018-11-06 03:57:36 +01:00
|
|
|
namespace Psalm\Internal\LanguageServer;
|
2018-10-17 21:52:26 +02:00
|
|
|
|
|
|
|
use AdvancedJsonRpc;
|
2019-02-06 02:00:13 +01:00
|
|
|
use function Amp\asyncCoroutine;
|
2019-07-05 22:24:00 +02:00
|
|
|
use function Amp\call;
|
|
|
|
use Amp\Promise;
|
2019-09-14 16:14:03 +02:00
|
|
|
use Amp\Success;
|
2019-06-26 22:52:29 +02:00
|
|
|
use function array_combine;
|
|
|
|
use function array_filter;
|
2019-07-05 22:24:00 +02:00
|
|
|
use function array_keys;
|
2019-06-26 22:52:29 +02:00
|
|
|
use function array_map;
|
|
|
|
use function array_shift;
|
|
|
|
use function array_unshift;
|
2019-07-05 22:24:00 +02:00
|
|
|
use function array_values;
|
|
|
|
use function explode;
|
2019-06-26 22:52:29 +02:00
|
|
|
use function implode;
|
2019-07-05 22:24:00 +02:00
|
|
|
use LanguageServerProtocol\ClientCapabilities;
|
|
|
|
use LanguageServerProtocol\CompletionOptions;
|
|
|
|
use LanguageServerProtocol\Diagnostic;
|
|
|
|
use LanguageServerProtocol\DiagnosticSeverity;
|
|
|
|
use LanguageServerProtocol\InitializeResult;
|
|
|
|
use LanguageServerProtocol\Position;
|
|
|
|
use LanguageServerProtocol\Range;
|
|
|
|
use LanguageServerProtocol\ServerCapabilities;
|
|
|
|
use LanguageServerProtocol\SignatureHelpOptions;
|
|
|
|
use LanguageServerProtocol\TextDocumentSyncKind;
|
|
|
|
use LanguageServerProtocol\TextDocumentSyncOptions;
|
|
|
|
use function max;
|
2019-06-26 22:52:29 +02:00
|
|
|
use function parse_url;
|
2020-02-17 00:24:40 +01:00
|
|
|
use Psalm\Internal\Analyzer\IssueData;
|
2019-07-05 22:24:00 +02:00
|
|
|
use Psalm\Internal\Analyzer\ProjectAnalyzer;
|
|
|
|
use Psalm\Internal\LanguageServer\Server\TextDocument;
|
|
|
|
use function rawurlencode;
|
|
|
|
use function str_replace;
|
2019-06-26 22:52:29 +02:00
|
|
|
use function strpos;
|
2019-07-05 22:24:00 +02:00
|
|
|
use function substr;
|
|
|
|
use Throwable;
|
|
|
|
use function trim;
|
|
|
|
use function urldecode;
|
2018-10-17 21:52:26 +02:00
|
|
|
|
2018-12-02 00:37:49 +01:00
|
|
|
/**
|
|
|
|
* @internal
|
|
|
|
*/
|
2018-10-17 21:52:26 +02:00
|
|
|
class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
|
|
|
{
|
|
|
|
/**
|
|
|
|
* Handles textDocument/* method calls
|
|
|
|
*
|
|
|
|
* @var ?Server\TextDocument
|
|
|
|
*/
|
|
|
|
public $textDocument;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var ProtocolReader
|
|
|
|
*/
|
|
|
|
protected $protocolReader;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var ProtocolWriter
|
|
|
|
*/
|
|
|
|
protected $protocolWriter;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var LanguageClient
|
|
|
|
*/
|
2018-11-18 00:00:28 +01:00
|
|
|
public $client;
|
2018-10-17 21:52:26 +02:00
|
|
|
|
|
|
|
/**
|
2018-11-06 03:57:36 +01:00
|
|
|
* @var ProjectAnalyzer
|
2018-10-17 21:52:26 +02:00
|
|
|
*/
|
2018-11-11 18:01:14 +01:00
|
|
|
protected $project_analyzer;
|
2018-10-17 21:52:26 +02:00
|
|
|
|
2018-11-18 00:00:28 +01:00
|
|
|
/**
|
|
|
|
* @var array<string, string>
|
|
|
|
*/
|
|
|
|
protected $onsave_paths_to_analyze = [];
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var array<string, string>
|
|
|
|
*/
|
|
|
|
protected $onchange_paths_to_analyze = [];
|
|
|
|
|
2018-10-17 21:52:26 +02:00
|
|
|
/**
|
|
|
|
* @param ProtocolReader $reader
|
|
|
|
* @param ProtocolWriter $writer
|
|
|
|
*/
|
|
|
|
public function __construct(
|
|
|
|
ProtocolReader $reader,
|
|
|
|
ProtocolWriter $writer,
|
2018-11-11 18:01:14 +01:00
|
|
|
ProjectAnalyzer $project_analyzer
|
2018-10-17 21:52:26 +02:00
|
|
|
) {
|
|
|
|
parent::__construct($this, '/');
|
2018-11-11 18:01:14 +01:00
|
|
|
$this->project_analyzer = $project_analyzer;
|
2018-10-17 21:52:26 +02:00
|
|
|
|
|
|
|
$this->protocolWriter = $writer;
|
|
|
|
|
|
|
|
$this->protocolReader = $reader;
|
|
|
|
$this->protocolReader->on(
|
|
|
|
'close',
|
|
|
|
/**
|
|
|
|
* @return void
|
|
|
|
*/
|
|
|
|
function () {
|
|
|
|
$this->shutdown();
|
|
|
|
$this->exit();
|
|
|
|
}
|
|
|
|
);
|
|
|
|
$this->protocolReader->on(
|
|
|
|
'message',
|
|
|
|
/** @return void */
|
2019-02-06 17:40:18 +01:00
|
|
|
asyncCoroutine(
|
|
|
|
/**
|
2019-02-06 20:17:44 +01:00
|
|
|
* @return \Generator<int, \Amp\Promise, mixed, void>
|
2019-02-06 17:40:18 +01:00
|
|
|
*/
|
|
|
|
function (Message $msg) {
|
|
|
|
if (!$msg->body) {
|
|
|
|
return;
|
|
|
|
}
|
2019-02-06 02:00:13 +01:00
|
|
|
|
2019-02-06 17:40:18 +01:00
|
|
|
// Ignore responses, this is the handler for requests and notifications
|
|
|
|
if (AdvancedJsonRpc\Response::isResponse($msg->body)) {
|
|
|
|
return;
|
|
|
|
}
|
2019-02-06 02:00:13 +01:00
|
|
|
|
2019-07-01 21:54:33 +02:00
|
|
|
/** @psalm-suppress UndefinedPropertyFetch */
|
|
|
|
if ($msg->body->method === 'textDocument/signatureHelp') {
|
|
|
|
$this->doAnalysis();
|
|
|
|
}
|
|
|
|
|
2019-02-06 17:40:18 +01:00
|
|
|
$result = null;
|
|
|
|
$error = null;
|
|
|
|
try {
|
|
|
|
// Invoke the method handler to get a result
|
|
|
|
/**
|
|
|
|
* @var Promise
|
2020-04-04 18:14:53 +02:00
|
|
|
* @psalm-suppress UndefinedDocblockClass
|
2019-02-06 17:40:18 +01:00
|
|
|
*/
|
|
|
|
$dispatched = $this->dispatch($msg->body);
|
2020-04-04 18:15:53 +02:00
|
|
|
/** @psalm-suppress MixedAssignment */
|
2019-02-06 17:40:18 +01:00
|
|
|
$result = yield $dispatched;
|
|
|
|
} catch (AdvancedJsonRpc\Error $e) {
|
|
|
|
// If a ResponseError is thrown, send it back in the Response
|
|
|
|
$error = $e;
|
|
|
|
} catch (Throwable $e) {
|
|
|
|
// If an unexpected error occurred, send back an INTERNAL_ERROR error response
|
|
|
|
$error = new AdvancedJsonRpc\Error(
|
|
|
|
(string) $e,
|
|
|
|
AdvancedJsonRpc\ErrorCode::INTERNAL_ERROR,
|
|
|
|
null,
|
|
|
|
$e
|
|
|
|
);
|
|
|
|
}
|
|
|
|
// Only send a Response for a Request
|
|
|
|
// Notifications do not send Responses
|
2019-02-06 02:00:13 +01:00
|
|
|
/**
|
2019-02-06 17:40:18 +01:00
|
|
|
* @psalm-suppress UndefinedPropertyFetch
|
|
|
|
* @psalm-suppress MixedArgument
|
2019-02-06 02:00:13 +01:00
|
|
|
*/
|
2019-02-06 17:40:18 +01:00
|
|
|
if (AdvancedJsonRpc\Request::isRequest($msg->body)) {
|
|
|
|
if ($error !== null) {
|
|
|
|
$responseBody = new AdvancedJsonRpc\ErrorResponse($msg->body->id, $error);
|
|
|
|
} else {
|
|
|
|
$responseBody = new AdvancedJsonRpc\SuccessResponse($msg->body->id, $result);
|
|
|
|
}
|
|
|
|
yield $this->protocolWriter->write(new Message($responseBody));
|
2018-10-17 21:52:26 +02:00
|
|
|
}
|
2019-02-06 02:00:13 +01:00
|
|
|
}
|
2019-02-06 17:40:18 +01:00
|
|
|
)
|
2018-10-17 21:52:26 +02:00
|
|
|
);
|
|
|
|
|
2018-11-18 00:00:28 +01:00
|
|
|
$this->protocolReader->on(
|
|
|
|
'readMessageGroup',
|
|
|
|
/** @return void */
|
|
|
|
function () {
|
2019-02-06 02:00:13 +01:00
|
|
|
$this->doAnalysis();
|
2018-11-18 00:00:28 +01:00
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2018-10-17 21:52:26 +02:00
|
|
|
$this->client = new LanguageClient($reader, $writer);
|
2020-04-17 06:47:18 +02:00
|
|
|
|
|
|
|
$this->verboseLog("Language server has started.");
|
2018-10-17 21:52:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The initialize request is sent as the first request from the client to the server.
|
|
|
|
*
|
|
|
|
* @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 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.
|
2018-12-14 16:27:39 +01:00
|
|
|
* @psalm-return Promise<InitializeResult>
|
2018-10-17 21:52:26 +02:00
|
|
|
* @psalm-suppress PossiblyUnusedMethod
|
|
|
|
*/
|
|
|
|
public function initialize(
|
|
|
|
ClientCapabilities $capabilities,
|
|
|
|
string $rootPath = null,
|
|
|
|
int $processId = null
|
|
|
|
): Promise {
|
2019-02-06 02:00:13 +01:00
|
|
|
return call(
|
2018-10-17 21:52:26 +02:00
|
|
|
/** @return \Generator<int, true, mixed, InitializeResult> */
|
|
|
|
function () use ($capabilities, $rootPath, $processId) {
|
2020-04-17 06:47:18 +02:00
|
|
|
$this->verboseLog("Initializing...");
|
|
|
|
$this->clientStatus('initializing');
|
|
|
|
|
2018-10-17 21:52:26 +02:00
|
|
|
// Eventually, this might block on something. Leave it as a generator.
|
2020-03-16 03:23:31 +01:00
|
|
|
/** @psalm-suppress TypeDoesNotContainType */
|
2018-10-17 21:52:26 +02:00
|
|
|
if (false) {
|
|
|
|
yield true;
|
|
|
|
}
|
|
|
|
|
2020-04-17 06:47:18 +02:00
|
|
|
$this->verboseLog("Initializing: Getting code base...");
|
|
|
|
$this->clientStatus('initializing', 'getting code base');
|
2018-11-11 18:01:14 +01:00
|
|
|
$codebase = $this->project_analyzer->getCodebase();
|
2018-11-06 03:57:36 +01:00
|
|
|
|
2020-04-17 06:47:18 +02:00
|
|
|
$this->verboseLog("Initializing: Scanning files...");
|
|
|
|
$this->clientStatus('initializing', 'scanning files');
|
2018-11-11 18:01:14 +01:00
|
|
|
$codebase->scanFiles($this->project_analyzer->threads);
|
2018-10-17 21:52:26 +02:00
|
|
|
|
2020-04-17 06:47:18 +02:00
|
|
|
$this->verboseLog("Initializing: Registering stub files...");
|
|
|
|
$this->clientStatus('initializing', 'registering stub files');
|
2019-05-30 16:30:41 +02:00
|
|
|
$codebase->config->visitStubFiles($codebase, null);
|
2018-11-09 06:46:39 +01:00
|
|
|
|
2018-10-17 21:52:26 +02:00
|
|
|
if ($this->textDocument === null) {
|
|
|
|
$this->textDocument = new TextDocument(
|
2018-10-26 06:59:14 +02:00
|
|
|
$this,
|
2018-11-06 03:57:36 +01:00
|
|
|
$codebase,
|
2018-11-11 18:01:14 +01:00
|
|
|
$this->project_analyzer->onchange_line_limit
|
2018-10-17 21:52:26 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
$serverCapabilities = new ServerCapabilities();
|
|
|
|
|
2018-11-18 00:00:28 +01:00
|
|
|
$textDocumentSyncOptions = new TextDocumentSyncOptions();
|
|
|
|
|
|
|
|
if ($this->project_analyzer->onchange_line_limit === 0) {
|
|
|
|
$textDocumentSyncOptions->change = TextDocumentSyncKind::NONE;
|
|
|
|
} else {
|
|
|
|
$textDocumentSyncOptions->change = TextDocumentSyncKind::FULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
$serverCapabilities->textDocumentSync = $textDocumentSyncOptions;
|
2018-10-17 21:52:26 +02:00
|
|
|
|
|
|
|
// Support "Find all symbols"
|
|
|
|
$serverCapabilities->documentSymbolProvider = false;
|
|
|
|
// Support "Find all symbols in workspace"
|
|
|
|
$serverCapabilities->workspaceSymbolProvider = false;
|
|
|
|
// Support "Go to definition"
|
2018-10-26 22:17:15 +02:00
|
|
|
$serverCapabilities->definitionProvider = true;
|
2018-10-17 21:52:26 +02:00
|
|
|
// Support "Find all references"
|
|
|
|
$serverCapabilities->referencesProvider = false;
|
|
|
|
// Support "Hover"
|
2018-10-26 22:17:15 +02:00
|
|
|
$serverCapabilities->hoverProvider = true;
|
2018-10-17 21:52:26 +02:00
|
|
|
// Support "Completion"
|
2018-10-26 22:17:15 +02:00
|
|
|
|
2019-05-09 17:20:13 +02:00
|
|
|
if ($this->project_analyzer->provide_completion) {
|
2019-07-02 00:48:33 +02:00
|
|
|
$serverCapabilities->completionProvider = new CompletionOptions();
|
2019-05-09 17:20:13 +02:00
|
|
|
$serverCapabilities->completionProvider->resolveProvider = false;
|
|
|
|
$serverCapabilities->completionProvider->triggerCharacters = ['$', '>', ':'];
|
|
|
|
}
|
2018-10-17 21:52:26 +02:00
|
|
|
|
2019-07-01 21:54:33 +02:00
|
|
|
$serverCapabilities->signatureHelpProvider = new SignatureHelpOptions(['(', ',']);
|
2018-10-17 21:52:26 +02:00
|
|
|
|
|
|
|
// Support global references
|
|
|
|
$serverCapabilities->xworkspaceReferencesProvider = false;
|
|
|
|
$serverCapabilities->xdefinitionProvider = false;
|
|
|
|
$serverCapabilities->dependenciesProvider = false;
|
|
|
|
|
2020-04-17 06:47:18 +02:00
|
|
|
$this->verboseLog("Initializing: Complete.");
|
|
|
|
$this->clientStatus('initialized');
|
2018-10-17 21:52:26 +02:00
|
|
|
return new InitializeResult($serverCapabilities);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @psalm-suppress PossiblyUnusedMethod
|
2019-07-05 22:24:00 +02:00
|
|
|
*
|
2018-10-17 21:52:26 +02:00
|
|
|
* @return void
|
|
|
|
*/
|
|
|
|
public function initialized()
|
|
|
|
{
|
2020-04-17 06:47:18 +02:00
|
|
|
$this->clientStatus('running');
|
2018-10-17 21:52:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return void
|
|
|
|
*/
|
2018-11-18 00:00:28 +01:00
|
|
|
public function queueTemporaryFileAnalysis(string $file_path, string $uri)
|
|
|
|
{
|
|
|
|
$this->onchange_paths_to_analyze[$file_path] = $uri;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return void
|
|
|
|
*/
|
|
|
|
public function queueFileAnalysis(string $file_path, string $uri)
|
2018-10-17 21:52:26 +02:00
|
|
|
{
|
2018-11-18 00:00:28 +01:00
|
|
|
$this->onsave_paths_to_analyze[$file_path] = $uri;
|
2018-10-17 21:52:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return void
|
|
|
|
*/
|
2018-11-18 00:00:28 +01:00
|
|
|
public function doAnalysis()
|
2018-10-17 21:52:26 +02:00
|
|
|
{
|
2020-04-17 06:47:18 +02:00
|
|
|
$this->clientStatus('analyzing');
|
2018-10-17 21:52:26 +02:00
|
|
|
|
2020-04-17 06:47:18 +02:00
|
|
|
try {
|
|
|
|
$codebase = $this->project_analyzer->getCodebase();
|
2018-11-18 00:00:28 +01:00
|
|
|
|
2020-04-17 06:47:18 +02:00
|
|
|
$all_files_to_analyze = $this->onchange_paths_to_analyze + $this->onsave_paths_to_analyze;
|
2018-11-18 00:00:28 +01:00
|
|
|
|
2020-04-17 06:47:18 +02:00
|
|
|
if (!$all_files_to_analyze) {
|
|
|
|
return;
|
|
|
|
}
|
2018-11-18 00:00:28 +01:00
|
|
|
|
2020-04-17 06:47:18 +02:00
|
|
|
if ($this->onsave_paths_to_analyze) {
|
|
|
|
$codebase->reloadFiles($this->project_analyzer, array_keys($this->onsave_paths_to_analyze));
|
|
|
|
}
|
2018-11-18 00:00:28 +01:00
|
|
|
|
2020-04-17 06:47:18 +02:00
|
|
|
if ($this->onchange_paths_to_analyze) {
|
|
|
|
$codebase->reloadFiles($this->project_analyzer, array_keys($this->onchange_paths_to_analyze));
|
|
|
|
}
|
2019-06-30 03:32:26 +02:00
|
|
|
|
2020-04-17 06:47:18 +02:00
|
|
|
$all_file_paths_to_analyze = array_keys($all_files_to_analyze);
|
|
|
|
$codebase->analyzer->addFilesToAnalyze(
|
|
|
|
array_combine($all_file_paths_to_analyze, $all_file_paths_to_analyze)
|
|
|
|
);
|
|
|
|
$codebase->analyzer->analyzeFiles($this->project_analyzer, 1, false);
|
|
|
|
|
|
|
|
$this->emitIssues($all_files_to_analyze);
|
2018-11-18 00:00:28 +01:00
|
|
|
|
2020-04-17 06:47:18 +02:00
|
|
|
$this->onchange_paths_to_analyze = [];
|
|
|
|
$this->onsave_paths_to_analyze = [];
|
|
|
|
} finally {
|
|
|
|
// we are done, so set the status back to running
|
|
|
|
$this->clientStatus('running');
|
|
|
|
}
|
2018-10-17 21:52:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-11-18 00:00:28 +01:00
|
|
|
* @param array<string, string> $uris
|
2019-07-05 22:24:00 +02:00
|
|
|
*
|
2018-10-17 21:52:26 +02:00
|
|
|
* @return void
|
|
|
|
*/
|
2018-11-18 00:00:28 +01:00
|
|
|
public function emitIssues(array $uris)
|
2018-10-17 21:52:26 +02:00
|
|
|
{
|
|
|
|
$data = \Psalm\IssueBuffer::clear();
|
|
|
|
|
2018-11-18 00:00:28 +01:00
|
|
|
foreach ($uris as $file_path => $uri) {
|
|
|
|
$diagnostics = array_map(
|
2020-02-17 00:24:40 +01:00
|
|
|
function (IssueData $issue_data) use ($file_path) : Diagnostic {
|
|
|
|
//$check_name = $issue->check_name;
|
|
|
|
$description = $issue_data->message;
|
|
|
|
$severity = $issue_data->severity;
|
|
|
|
|
|
|
|
$start_line = max($issue_data->line_from, 1);
|
|
|
|
$end_line = $issue_data->line_to;
|
|
|
|
$start_column = $issue_data->column_from;
|
|
|
|
$end_column = $issue_data->column_to;
|
2018-11-18 00:00:28 +01:00
|
|
|
// Language server has 0 based lines and columns, phan has 1-based lines and columns.
|
|
|
|
$range = new Range(
|
|
|
|
new Position($start_line - 1, $start_column - 1),
|
|
|
|
new Position($end_line - 1, $end_column - 1)
|
|
|
|
);
|
|
|
|
switch ($severity) {
|
|
|
|
case \Psalm\Config::REPORT_INFO:
|
|
|
|
$diagnostic_severity = DiagnosticSeverity::WARNING;
|
|
|
|
break;
|
|
|
|
case \Psalm\Config::REPORT_ERROR:
|
|
|
|
default:
|
|
|
|
$diagnostic_severity = DiagnosticSeverity::ERROR;
|
|
|
|
break;
|
|
|
|
}
|
2020-04-17 06:47:18 +02:00
|
|
|
$diagnostic = new Diagnostic(
|
2018-11-18 00:00:28 +01:00
|
|
|
$description,
|
|
|
|
$range,
|
|
|
|
null,
|
|
|
|
$diagnostic_severity,
|
|
|
|
'Psalm'
|
|
|
|
);
|
2020-04-17 06:47:18 +02:00
|
|
|
|
|
|
|
//$code = 'PS' . \str_pad((string) $issue_data->shortcode, 3, "0", \STR_PAD_LEFT);
|
|
|
|
$code = $issue_data->link;
|
|
|
|
|
|
|
|
if ($this->project_analyzer->language_server_use_extended_diagnostic_codes) {
|
|
|
|
// Added in VSCode 1.43.0 and will be part of the LSP 3.16.0 standard.
|
|
|
|
// Since this new functionality is not backwards compatible, we use a
|
|
|
|
// configuration option so the end user must opt in to it using the cli argument.
|
|
|
|
// https://github.com/microsoft/vscode/blob/1.43.0/src/vs/vscode.d.ts#L4688-L4699
|
|
|
|
|
|
|
|
/** @psalm-suppress InvalidPropertyAssignmentValue */
|
|
|
|
$diagnostic->code = [
|
|
|
|
"value" => $code,
|
|
|
|
"target" => $issue_data->link,
|
|
|
|
];
|
|
|
|
} else {
|
|
|
|
// the Diagnostic constructor only takes `int` for the code, but the property can be
|
|
|
|
// `int` or `string`, so we set the property directly because we want to use a `string`
|
|
|
|
$diagnostic->code = $code;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $diagnostic;
|
2018-11-18 00:00:28 +01:00
|
|
|
},
|
2020-01-22 03:07:44 +01:00
|
|
|
$data[$file_path] ?? []
|
2018-11-18 00:00:28 +01:00
|
|
|
);
|
2018-10-17 21:52:26 +02:00
|
|
|
|
2018-11-18 00:00:28 +01:00
|
|
|
$this->client->textDocument->publishDiagnostics($uri, $diagnostics);
|
|
|
|
}
|
2018-10-17 21:52:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The shutdown request is sent from the client to the server. It asks the server to shut down,
|
|
|
|
* but to not exit (otherwise the response might not be delivered correctly to the client).
|
|
|
|
* There is a separate exit notification that asks the server to exit.
|
|
|
|
*
|
2019-09-14 16:14:03 +02:00
|
|
|
* @psalm-return Promise<null>
|
2018-10-17 21:52:26 +02:00
|
|
|
*/
|
|
|
|
public function shutdown()
|
|
|
|
{
|
2020-04-17 06:47:18 +02:00
|
|
|
$this->clientStatus('closing');
|
|
|
|
$this->verboseLog("Shutting down...");
|
2018-11-11 18:01:14 +01:00
|
|
|
$codebase = $this->project_analyzer->getCodebase();
|
2018-11-06 03:57:36 +01:00
|
|
|
$scanned_files = $codebase->scanner->getScannedFiles();
|
|
|
|
$codebase->file_reference_provider->updateReferenceCache(
|
|
|
|
$codebase,
|
2018-10-17 21:52:26 +02:00
|
|
|
$scanned_files
|
|
|
|
);
|
2020-04-17 06:47:18 +02:00
|
|
|
$this->clientStatus('closed');
|
2019-09-14 16:14:03 +02:00
|
|
|
return new Success(null);
|
2018-10-17 21:52:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A notification to ask the server to exit its process.
|
|
|
|
*
|
|
|
|
* @return void
|
|
|
|
*/
|
|
|
|
public function exit()
|
|
|
|
{
|
|
|
|
exit(0);
|
|
|
|
}
|
|
|
|
|
2020-04-17 06:47:18 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Send log message to the client
|
|
|
|
*
|
|
|
|
* @param string $message The log message to send to the client.
|
|
|
|
* @psalm-param 1|2|3|4 $type
|
|
|
|
* @param integer $type The log type:
|
|
|
|
* - 1 = Error
|
|
|
|
* - 2 = Warning
|
|
|
|
* - 3 = Info
|
|
|
|
* - 4 = Log
|
|
|
|
* @return Promise
|
|
|
|
*/
|
|
|
|
private function verboseLog(string $message, int $type = 4): Promise
|
|
|
|
{
|
|
|
|
if ($this->project_analyzer->language_server_verbose) {
|
|
|
|
try {
|
|
|
|
return $this->client->logMessage(
|
|
|
|
'[Psalm ' .PSALM_VERSION. ' - PHP Language Server] ' . $message,
|
|
|
|
$type
|
|
|
|
);
|
|
|
|
} catch (\Throwable $err) {
|
|
|
|
// do nothing
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return new Success(null);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Send status message to client. This is the same as sending a log message,
|
|
|
|
* except this is meant for parsing by the client to present status updates in a UI.
|
|
|
|
*
|
|
|
|
* @param string $status The log message to send to the client. Should not contain colons `:`.
|
|
|
|
* @param string|null $additional_info This is additional info that the client
|
|
|
|
* can use as part of the display message.
|
|
|
|
* @return Promise
|
|
|
|
*/
|
|
|
|
private function clientStatus(string $status, string $additional_info = null): Promise
|
|
|
|
{
|
|
|
|
try {
|
|
|
|
// here we send a notification to the client using the telemetry notification method
|
|
|
|
return $this->client->logMessage(
|
|
|
|
$status . (!empty($additional_info) ? ': ' . $additional_info : ''),
|
|
|
|
3,
|
|
|
|
'telemetry/event'
|
|
|
|
);
|
|
|
|
} catch (\Throwable $err) {
|
|
|
|
return new Success(null);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-10-26 22:17:15 +02:00
|
|
|
/**
|
|
|
|
* Transforms an absolute file path into a URI as used by the language server protocol.
|
|
|
|
*
|
|
|
|
* @param string $filepath
|
2019-07-05 22:24:00 +02:00
|
|
|
*
|
2018-10-26 22:17:15 +02:00
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public static function pathToUri(string $filepath): string
|
|
|
|
{
|
|
|
|
$filepath = trim(str_replace('\\', '/', $filepath), '/');
|
|
|
|
$parts = explode('/', $filepath);
|
|
|
|
// Don't %-encode the colon after a Windows drive letter
|
|
|
|
$first = array_shift($parts);
|
|
|
|
if (substr($first, -1) !== ':') {
|
|
|
|
$first = rawurlencode($first);
|
|
|
|
}
|
|
|
|
$parts = array_map('rawurlencode', $parts);
|
|
|
|
array_unshift($parts, $first);
|
|
|
|
$filepath = implode('/', $parts);
|
2019-07-05 22:24:00 +02:00
|
|
|
|
2018-10-26 22:17:15 +02:00
|
|
|
return 'file:///' . $filepath;
|
|
|
|
}
|
|
|
|
|
2018-10-17 21:52:26 +02:00
|
|
|
/**
|
|
|
|
* Transforms URI into file path
|
|
|
|
*
|
|
|
|
* @param string $uri
|
2019-07-05 22:24:00 +02:00
|
|
|
*
|
2018-10-17 21:52:26 +02:00
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public static function uriToPath(string $uri)
|
|
|
|
{
|
|
|
|
$fragments = parse_url($uri);
|
2019-10-01 21:44:43 +02:00
|
|
|
if ($fragments === false
|
|
|
|
|| !isset($fragments['scheme'])
|
|
|
|
|| $fragments['scheme'] !== 'file'
|
|
|
|
|| !isset($fragments['path'])
|
|
|
|
) {
|
2018-10-17 21:52:26 +02:00
|
|
|
throw new \InvalidArgumentException("Not a valid file URI: $uri");
|
|
|
|
}
|
2019-10-01 21:44:43 +02:00
|
|
|
|
2018-10-17 21:52:26 +02:00
|
|
|
$filepath = urldecode((string) $fragments['path']);
|
2019-10-01 21:44:43 +02:00
|
|
|
|
2018-10-17 21:52:26 +02:00
|
|
|
if (strpos($filepath, ':') !== false) {
|
|
|
|
if ($filepath[0] === '/') {
|
|
|
|
$filepath = substr($filepath, 1);
|
|
|
|
}
|
|
|
|
$filepath = str_replace('/', '\\', $filepath);
|
|
|
|
}
|
2019-07-05 22:24:00 +02:00
|
|
|
|
2018-10-17 21:52:26 +02:00
|
|
|
return $filepath;
|
|
|
|
}
|
|
|
|
}
|