1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-26 20:34:47 +01:00

Added new language server options and functionality. (#3161)

* Added new language server options and functionality.

Added new extended diagnostic code information to the language server.
 -- It must be enabled via a command line switch.
Added telemetry data for language server initialization and operation.
Added verbose log messages for language server.
 -- It must be enabled via a command line switch.

* fixed phpcs issues

* fixed failing tests

* changed the language server reported error code to be the help link

Co-authored-by: Anthony Rainer <0@0ze.ro>
This commit is contained in:
Anthony Rainer 2020-04-16 23:47:18 -05:00 committed by GitHub
parent 35d376cbe7
commit 6f36f33630
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 213 additions and 31 deletions

View File

@ -98,6 +98,12 @@ class IssueData
*/
public $error_level;
/**
* @var int
* @readonly
*/
public $shortcode;
/**
* @var string
* @readonly
@ -157,6 +163,7 @@ class IssueData
$this->snippet_to = $snippet_to;
$this->column_from = $column_from;
$this->column_to = $column_to;
$this->shortcode = $shortcode;
$this->error_level = $error_level;
$this->link = 'https://psalm.dev/' . \str_pad((string) $shortcode, 3, "0", \STR_PAD_LEFT);
}

View File

@ -215,6 +215,20 @@ class ProjectAnalyzer
UnnecessaryVarAnnotation::class,
];
/**
* When this is true, the language server will send the diagnostic code with a help link.
*
* @var bool
*/
public $language_server_use_extended_diagnostic_codes = false;
/**
* If this is true then the language server will send log messages to the client with additional information.
*
* @var bool
*/
public $language_server_verbose = false;
/**
* @param array<ReportOptions> $generated_report_options
* @param int $threads
@ -367,7 +381,7 @@ class ProjectAnalyzer
$this->checkDirWithConfig($dir_name, $this->config);
}
@cli_set_process_title('Psalm PHP Language Server');
@cli_set_process_title('Psalm ' . PSALM_VERSION . ' - PHP Language Server');
if (!$socket_server_mode && $address) {
// Connect to a TCP server

View File

@ -2,6 +2,7 @@
declare(strict_types = 1);
namespace Psalm\Internal\LanguageServer;
use Amp\Promise;
use JsonMapper;
/**
@ -16,11 +17,48 @@ class LanguageClient
*/
public $textDocument;
/**
* The client handler
*
* @var ClientHandler
*/
private $handler;
public function __construct(ProtocolReader $reader, ProtocolWriter $writer)
{
$handler = new ClientHandler($reader, $writer);
$this->handler = new ClientHandler($reader, $writer);
$mapper = new JsonMapper;
$this->textDocument = new Client\TextDocument($handler, $mapper);
$this->textDocument = new Client\TextDocument($this->handler, $mapper);
}
/**
* Send a log message to the client.
*
* @param string $message The 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<void>
*/
public function logMessage(string $message, int $type = 4, string $method = 'window/logMessage'): Promise
{
// https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_logMessage
if ($type < 1 || $type > 4) {
$type = 4;
}
return $this->handler->notify(
$method,
[
'type' => $type,
'message' => $message
]
);
}
}

View File

@ -179,6 +179,8 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
);
$this->client = new LanguageClient($reader, $writer);
$this->verboseLog("Language server has started.");
}
/**
@ -200,16 +202,25 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
return call(
/** @return \Generator<int, true, mixed, InitializeResult> */
function () use ($capabilities, $rootPath, $processId) {
$this->verboseLog("Initializing...");
$this->clientStatus('initializing');
// Eventually, this might block on something. Leave it as a generator.
/** @psalm-suppress TypeDoesNotContainType */
if (false) {
yield true;
}
$this->verboseLog("Initializing: Getting code base...");
$this->clientStatus('initializing', 'getting code base');
$codebase = $this->project_analyzer->getCodebase();
$this->verboseLog("Initializing: Scanning files...");
$this->clientStatus('initializing', 'scanning files');
$codebase->scanFiles($this->project_analyzer->threads);
$this->verboseLog("Initializing: Registering stub files...");
$this->clientStatus('initializing', 'registering stub files');
$codebase->config->visitStubFiles($codebase, null);
if ($this->textDocument === null) {
@ -257,6 +268,8 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
$serverCapabilities->xdefinitionProvider = false;
$serverCapabilities->dependenciesProvider = false;
$this->verboseLog("Initializing: Complete.");
$this->clientStatus('initialized');
return new InitializeResult($serverCapabilities);
}
);
@ -269,6 +282,7 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
*/
public function initialized()
{
$this->clientStatus('running');
}
/**
@ -292,30 +306,39 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
*/
public function doAnalysis()
{
$codebase = $this->project_analyzer->getCodebase();
$this->clientStatus('analyzing');
$all_files_to_analyze = $this->onchange_paths_to_analyze + $this->onsave_paths_to_analyze;
try {
$codebase = $this->project_analyzer->getCodebase();
if (!$all_files_to_analyze) {
return;
$all_files_to_analyze = $this->onchange_paths_to_analyze + $this->onsave_paths_to_analyze;
if (!$all_files_to_analyze) {
return;
}
if ($this->onsave_paths_to_analyze) {
$codebase->reloadFiles($this->project_analyzer, array_keys($this->onsave_paths_to_analyze));
}
if ($this->onchange_paths_to_analyze) {
$codebase->reloadFiles($this->project_analyzer, array_keys($this->onchange_paths_to_analyze));
}
$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);
$this->onchange_paths_to_analyze = [];
$this->onsave_paths_to_analyze = [];
} finally {
// we are done, so set the status back to running
$this->clientStatus('running');
}
if ($this->onsave_paths_to_analyze) {
$codebase->reloadFiles($this->project_analyzer, array_keys($this->onsave_paths_to_analyze));
}
if ($this->onchange_paths_to_analyze) {
$codebase->reloadFiles($this->project_analyzer, array_keys($this->onchange_paths_to_analyze));
}
$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);
$this->onchange_paths_to_analyze = [];
$this->onsave_paths_to_analyze = [];
}
/**
@ -352,14 +375,35 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
$diagnostic_severity = DiagnosticSeverity::ERROR;
break;
}
// TODO: copy issue code in 'json' format
return new Diagnostic(
$diagnostic = new Diagnostic(
$description,
$range,
null,
$diagnostic_severity,
'Psalm'
);
//$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;
},
$data[$file_path] ?? []
);
@ -377,12 +421,15 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
*/
public function shutdown()
{
$this->clientStatus('closing');
$this->verboseLog("Shutting down...");
$codebase = $this->project_analyzer->getCodebase();
$scanned_files = $codebase->scanner->getScannedFiles();
$codebase->file_reference_provider->updateReferenceCache(
$codebase,
$scanned_files
);
$this->clientStatus('closed');
return new Success(null);
}
@ -396,6 +443,57 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
exit(0);
}
/**
* 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);
}
}
/**
* Transforms an absolute file path into a URI as used by the language server protocol.
*

View File

@ -30,6 +30,8 @@ $valid_long_options = [
'tcp-server',
'disable-on-change::',
'enable-autocomplete',
'use-extended-diagnostic-codes',
'verbose'
];
$args = array_slice($argv, 1);
@ -146,11 +148,22 @@ Options:
--enable-autocomplete[=BOOL]
Enables or disables autocomplete on methods and properties. Default is true.
--use-extended-diagnostic-codes
Enables sending help uri links with the code in diagnostic messages.
--verbose
Will send log messages to the client with information.
HELP;
exit;
}
if (array_key_exists('v', $options)) {
echo 'Psalm ' . PSALM_VERSION . PHP_EOL;
exit;
}
if (getcwd() === false) {
fwrite(STDERR, 'Cannot get current working directory' . PHP_EOL);
exit(1);
@ -180,11 +193,6 @@ $vendor_dir = getVendorDir($current_dir);
$first_autoloader = requireAutoloaders($current_dir, isset($options['r']), $vendor_dir);
if (array_key_exists('v', $options)) {
echo 'Psalm ' . PSALM_VERSION . PHP_EOL;
exit;
}
$ini_handler = new \Psalm\Internal\Fork\PsalmRestarter('PSALM');
$ini_handler->disableExtension('grpc');
@ -250,4 +258,12 @@ if ($find_dead_code) {
$project_analyzer->getCodebase()->reportUnusedCode();
}
if (isset($options['use-extended-diagnostic-codes'])) {
$project_analyzer->language_server_use_extended_diagnostic_codes = true;
}
if (isset($options['verbose'])) {
$project_analyzer->language_server_verbose = true;
}
$project_analyzer->server($options['tcp'] ?? null, isset($options['tcp-server']) ? true : false);

View File

@ -115,6 +115,7 @@ echo $a;';
'column_from' => 10,
'column_to' => 17,
'error_level' => -1,
'shortcode' => 24,
'link' => 'https://psalm.dev/024'
],
[
@ -134,6 +135,7 @@ echo $a;';
'column_from' => 29,
'column_to' => 39,
'error_level' => -2,
'shortcode' => 135,
'link' => 'https://psalm.dev/135'
],
[
@ -153,6 +155,7 @@ echo $a;';
'column_from' => 42,
'column_to' => 49,
'error_level' => 1,
'shortcode' => 47,
'link' => 'https://psalm.dev/047'
],
[
@ -172,6 +175,7 @@ echo $a;';
'column_from' => 6,
'column_to' => 15,
'error_level' => -1,
'shortcode' => 20,
'link' => 'https://psalm.dev/020'
],
[
@ -191,6 +195,7 @@ echo $a;';
'column_from' => 6,
'column_to' => 8,
'error_level' => 3,
'shortcode' => 126,
'link' => 'https://psalm.dev/126'
],
],

View File

@ -125,6 +125,7 @@ echo $a;';
'column_from' => 10,
'column_to' => 17,
'error_level' => -1,
'shortcode' => 24,
'link' => 'https://psalm.dev/024'
],
[
@ -144,6 +145,7 @@ echo $a;';
'column_from' => 42,
'column_to' => 49,
'error_level' => 1,
'shortcode' => 47,
'link' => 'https://psalm.dev/047'
],
[
@ -163,6 +165,7 @@ echo $a;';
'column_from' => 6,
'column_to' => 15,
'error_level' => -1,
'shortcode' => 20,
'link' => 'https://psalm.dev/020'
],
[
@ -182,6 +185,7 @@ echo $a;';
'column_from' => 6,
'column_to' => 8,
'error_level' => 3,
'shortcode' => 126,
'link' => 'https://psalm.dev/126'
],
];