mirror of
https://github.com/danog/psalm.git
synced 2024-11-30 04:39:00 +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:
parent
35d376cbe7
commit
6f36f33630
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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);
|
||||
|
@ -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'
|
||||
],
|
||||
],
|
||||
|
@ -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'
|
||||
],
|
||||
];
|
||||
|
Loading…
Reference in New Issue
Block a user