mirror of
https://github.com/danog/psalm.git
synced 2024-11-27 12:55:26 +01:00
Fix #531
Well, it’s basically fixed already, but this adds @TysonAndre’s suggestions (and some of the relevant implementation from Phan)
This commit is contained in:
parent
1ada15f6c6
commit
c2e546facb
@ -135,11 +135,19 @@ class CodeLocation
|
||||
|
||||
$file_contents = $codebase->getFileContents($this->file_path);
|
||||
|
||||
$preview_end = strpos(
|
||||
$file_contents,
|
||||
"\n",
|
||||
$this->single_line ? $this->selection_start : $this->selection_end
|
||||
);
|
||||
$file_length = strlen($file_contents);
|
||||
|
||||
$search_limit = $this->single_line ? $this->selection_start : $this->selection_end;
|
||||
|
||||
if ($search_limit <= $file_length) {
|
||||
$preview_end = strpos(
|
||||
$file_contents,
|
||||
"\n",
|
||||
$search_limit
|
||||
);
|
||||
} else {
|
||||
$preview_end = false;
|
||||
}
|
||||
|
||||
// if the string didn't contain a newline
|
||||
if ($preview_end === false) {
|
||||
|
@ -9,6 +9,7 @@ use LanguageServerProtocol\{
|
||||
ServerCapabilities,
|
||||
ClientCapabilities,
|
||||
TextDocumentSyncKind,
|
||||
TextDocumentSyncOptions,
|
||||
InitializeResult,
|
||||
CompletionOptions,
|
||||
SignatureHelpOptions
|
||||
@ -52,13 +53,23 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
||||
/**
|
||||
* @var LanguageClient
|
||||
*/
|
||||
protected $client;
|
||||
public $client;
|
||||
|
||||
/**
|
||||
* @var ProjectAnalyzer
|
||||
*/
|
||||
protected $project_analyzer;
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected $onsave_paths_to_analyze = [];
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected $onchange_paths_to_analyze = [];
|
||||
|
||||
/**
|
||||
* @param ProtocolReader $reader
|
||||
* @param ProtocolWriter $writer
|
||||
@ -140,6 +151,14 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
||||
}
|
||||
);
|
||||
|
||||
$this->protocolReader->on(
|
||||
'readMessageGroup',
|
||||
/** @return void */
|
||||
function () {
|
||||
$this->doAnalysis();
|
||||
}
|
||||
);
|
||||
|
||||
$this->client = new LanguageClient($reader, $writer);
|
||||
}
|
||||
|
||||
@ -183,7 +202,15 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
||||
|
||||
$serverCapabilities = new ServerCapabilities();
|
||||
|
||||
$serverCapabilities->textDocumentSync = TextDocumentSyncKind::FULL;
|
||||
$textDocumentSyncOptions = new TextDocumentSyncOptions();
|
||||
|
||||
if ($this->project_analyzer->onchange_line_limit === 0) {
|
||||
$textDocumentSyncOptions->change = TextDocumentSyncKind::NONE;
|
||||
} else {
|
||||
$textDocumentSyncOptions->change = TextDocumentSyncKind::FULL;
|
||||
}
|
||||
|
||||
$serverCapabilities->textDocumentSync = $textDocumentSyncOptions;
|
||||
|
||||
// Support "Find all symbols"
|
||||
$serverCapabilities->documentSymbolProvider = false;
|
||||
@ -229,86 +256,117 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function invalidateFileAndDependents(string $uri)
|
||||
public function queueTemporaryFileAnalysis(string $file_path, string $uri)
|
||||
{
|
||||
$file_path = self::uriToPath($uri);
|
||||
$this->project_analyzer->getCodebase()->reloadFiles($this->project_analyzer, [$file_path]);
|
||||
$this->onchange_paths_to_analyze[$file_path] = $uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function analyzePath(string $file_path)
|
||||
public function queueFileAnalysis(string $file_path, string $uri)
|
||||
{
|
||||
$this->onsave_paths_to_analyze[$file_path] = $uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function doAnalysis()
|
||||
{
|
||||
$codebase = $this->project_analyzer->getCodebase();
|
||||
|
||||
$codebase->addFilesToAnalyze([$file_path => $file_path]);
|
||||
$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) {
|
||||
$onsave_paths_to_analyze = array_keys($this->onsave_paths_to_analyze);
|
||||
$codebase->reloadFiles($this->project_analyzer, $onsave_paths_to_analyze);
|
||||
}
|
||||
|
||||
if ($this->onchange_paths_to_analyze) {
|
||||
foreach ($this->onchange_paths_to_analyze as $file_path => $_) {
|
||||
$codebase->invalidateInformationForFile($file_path);
|
||||
$codebase->scanTemporaryFileChanges($file_path);
|
||||
}
|
||||
}
|
||||
|
||||
$all_file_paths_to_analyze = array_keys($all_files_to_analyze);
|
||||
$codebase->analyzer->addFiles(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 = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $uris
|
||||
* @return void
|
||||
*/
|
||||
public function emitIssues(string $uri)
|
||||
public function emitIssues(array $uris)
|
||||
{
|
||||
$data = \Psalm\IssueBuffer::clear();
|
||||
|
||||
$file_path = self::uriToPath($uri);
|
||||
|
||||
$data = array_values(array_filter(
|
||||
$data,
|
||||
function (array $issue_data) use ($file_path) : bool {
|
||||
return $issue_data['file_path'] === $file_path;
|
||||
}
|
||||
));
|
||||
|
||||
$diagnostics = array_map(
|
||||
/**
|
||||
* @param array{
|
||||
* severity: string,
|
||||
* message: string,
|
||||
* line_from: int,
|
||||
* line_to: int,
|
||||
* column_from: int,
|
||||
* column_to: int
|
||||
* } $issue_data
|
||||
*/
|
||||
function (array $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'];
|
||||
// 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;
|
||||
foreach ($uris as $file_path => $uri) {
|
||||
$data = array_values(array_filter(
|
||||
$data,
|
||||
function (array $issue_data) use ($file_path) : bool {
|
||||
return $issue_data['file_path'] === $file_path;
|
||||
}
|
||||
// TODO: copy issue code in 'json' format
|
||||
return new Diagnostic(
|
||||
$description,
|
||||
$range,
|
||||
null,
|
||||
$diagnostic_severity,
|
||||
'Psalm'
|
||||
);
|
||||
},
|
||||
$data
|
||||
);
|
||||
));
|
||||
|
||||
$this->client->textDocument->publishDiagnostics($uri, $diagnostics);
|
||||
$diagnostics = array_map(
|
||||
/**
|
||||
* @param array{
|
||||
* severity: string,
|
||||
* message: string,
|
||||
* line_from: int,
|
||||
* line_to: int,
|
||||
* column_from: int,
|
||||
* column_to: int
|
||||
* } $issue_data
|
||||
*/
|
||||
function (array $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'];
|
||||
// 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;
|
||||
}
|
||||
// TODO: copy issue code in 'json' format
|
||||
return new Diagnostic(
|
||||
$description,
|
||||
$range,
|
||||
null,
|
||||
$diagnostic_severity,
|
||||
'Psalm'
|
||||
);
|
||||
},
|
||||
$data
|
||||
);
|
||||
|
||||
$this->client->textDocument->publishDiagnostics($uri, $diagnostics);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -3,10 +3,15 @@ declare(strict_types = 1);
|
||||
|
||||
namespace Psalm\Internal\LanguageServer;
|
||||
|
||||
use Psalm\Internal\LanguageServer\Message;
|
||||
use AdvancedJsonRpc\Message as MessageBody;
|
||||
use Sabre\Event\{Loop, Emitter};
|
||||
use Exception;
|
||||
use Psalm\Internal\LanguageServer\Message;
|
||||
use Sabre\Event\Emitter;
|
||||
use Sabre\Event\Loop;
|
||||
|
||||
/**
|
||||
* Source: https://github.com/felixfbecker/php-language-server/tree/master/src/ProtocolStreamReader.php
|
||||
*/
|
||||
class ProtocolStreamReader extends Emitter implements ProtocolReader
|
||||
{
|
||||
const PARSE_HEADERS = 1;
|
||||
@ -14,18 +19,22 @@ class ProtocolStreamReader extends Emitter implements ProtocolReader
|
||||
|
||||
/** @var resource */
|
||||
private $input;
|
||||
|
||||
/**
|
||||
* This is checked by ProtocolStreamReader so that it will stop reading from streams in the forked process.
|
||||
* There could be buffered bytes in stdin/over TCP, those would be processed by TCP if it were not for this check.
|
||||
* @var bool
|
||||
*/
|
||||
private $is_accepting_new_requests = true;
|
||||
/** @var int */
|
||||
private $parsingMode = self::PARSE_HEADERS;
|
||||
|
||||
private $parsing_mode = self::PARSE_HEADERS;
|
||||
/** @var string */
|
||||
private $buffer = '';
|
||||
|
||||
/** @var array<string, string> */
|
||||
/** @var string[] */
|
||||
private $headers = [];
|
||||
|
||||
/** @var ?int */
|
||||
private $contentLength;
|
||||
private $content_length = null;
|
||||
/** @var bool */
|
||||
private $did_emit_close = false;
|
||||
|
||||
/**
|
||||
* @param resource $input
|
||||
@ -49,35 +58,84 @@ class ProtocolStreamReader extends Emitter implements ProtocolReader
|
||||
if (feof($this->input)) {
|
||||
// If stream_select reported a status change for this stream,
|
||||
// but the stream is EOF, it means it was closed.
|
||||
$this->emit('close');
|
||||
$this->emitClose();
|
||||
return;
|
||||
}
|
||||
while (($c = fgetc($this->input)) !== false && $c !== '') {
|
||||
$this->buffer .= $c;
|
||||
switch ($this->parsingMode) {
|
||||
case self::PARSE_HEADERS:
|
||||
if ($this->buffer === "\r\n") {
|
||||
$this->parsingMode = self::PARSE_BODY;
|
||||
$this->contentLength = (int)$this->headers['Content-Length'];
|
||||
$this->buffer = '';
|
||||
} elseif (substr($this->buffer, -2) === "\r\n") {
|
||||
$parts = explode(':', $this->buffer);
|
||||
$this->headers[$parts[0]] = trim($parts[1]);
|
||||
$this->buffer = '';
|
||||
}
|
||||
break;
|
||||
case self::PARSE_BODY:
|
||||
if (strlen($this->buffer) === $this->contentLength) {
|
||||
$msg = new Message(MessageBody::parse($this->buffer), $this->headers);
|
||||
$this->emit('message', [$msg]);
|
||||
$this->parsingMode = self::PARSE_HEADERS;
|
||||
$this->headers = [];
|
||||
$this->buffer = '';
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (!$this->is_accepting_new_requests) {
|
||||
// If we fork, don't read any bytes in the input buffer from the worker process.
|
||||
$this->emitClose();
|
||||
return;
|
||||
}
|
||||
$emitted_messages = $this->readMessages();
|
||||
if ($emitted_messages > 0) {
|
||||
$this->emit('readMessageGroup');
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
private function readMessages() : int
|
||||
{
|
||||
$emitted_messages = 0;
|
||||
while (($c = fgetc($this->input)) !== false && $c !== '') {
|
||||
$this->buffer .= $c;
|
||||
switch ($this->parsing_mode) {
|
||||
case self::PARSE_HEADERS:
|
||||
if ($this->buffer === "\r\n") {
|
||||
$this->parsing_mode = self::PARSE_BODY;
|
||||
$this->content_length = (int)$this->headers['Content-Length'];
|
||||
$this->buffer = '';
|
||||
} elseif (substr($this->buffer, -2) === "\r\n") {
|
||||
$parts = explode(':', $this->buffer);
|
||||
$this->headers[$parts[0]] = trim($parts[1]);
|
||||
$this->buffer = '';
|
||||
}
|
||||
break;
|
||||
case self::PARSE_BODY:
|
||||
if (strlen($this->buffer) === $this->content_length) {
|
||||
if (!$this->is_accepting_new_requests) {
|
||||
// If we fork, don't read any bytes in the input buffer from the worker process.
|
||||
$this->emitClose();
|
||||
return $emitted_messages;
|
||||
}
|
||||
// MessageBody::parse can throw an Error, maybe log an error?
|
||||
try {
|
||||
$msg = new Message(MessageBody::parse($this->buffer), $this->headers);
|
||||
} catch (Exception $_) {
|
||||
$msg = null;
|
||||
}
|
||||
if ($msg) {
|
||||
$emitted_messages++;
|
||||
$this->emit('message', [$msg]);
|
||||
/** @psalm-suppress DocblockTypeContradiction */
|
||||
if (!$this->is_accepting_new_requests) {
|
||||
// If we fork, don't read any bytes in the input buffer from the worker process.
|
||||
$this->emitClose();
|
||||
return $emitted_messages;
|
||||
}
|
||||
}
|
||||
$this->parsing_mode = self::PARSE_HEADERS;
|
||||
$this->headers = [];
|
||||
$this->buffer = '';
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $emitted_messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
private function emitClose()
|
||||
{
|
||||
if ($this->did_emit_close) {
|
||||
return;
|
||||
}
|
||||
$this->did_emit_close = true;
|
||||
$this->emit('close');
|
||||
}
|
||||
}
|
||||
|
@ -91,12 +91,9 @@ class TextDocument
|
||||
return;
|
||||
}
|
||||
|
||||
$this->server->invalidateFileAndDependents($textDocument->uri);
|
||||
|
||||
$this->codebase->file_provider->openFile($file_path);
|
||||
|
||||
$this->server->analyzePath($file_path);
|
||||
$this->server->emitIssues($textDocument->uri);
|
||||
$this->server->queueFileAnalysis($file_path, $textDocument->uri);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -112,10 +109,8 @@ class TextDocument
|
||||
|
||||
// reopen file
|
||||
$this->codebase->removeTemporaryFileChanges($file_path);
|
||||
$this->server->invalidateFileAndDependents($textDocument->uri);
|
||||
|
||||
$this->server->analyzePath($file_path);
|
||||
$this->server->emitIssues($textDocument->uri);
|
||||
$this->server->queueFileAnalysis($file_path, $textDocument->uri);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -150,11 +145,7 @@ class TextDocument
|
||||
}
|
||||
|
||||
$this->codebase->addTemporaryFileChanges($file_path, $new_content);
|
||||
$this->codebase->invalidateInformationForFile($file_path);
|
||||
$this->codebase->scanTemporaryFileChanges($file_path);
|
||||
|
||||
$this->server->analyzePath($file_path);
|
||||
$this->server->emitIssues($textDocument->uri);
|
||||
$this->server->queueTemporaryFileAnalysis($file_path, $textDocument->uri);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -170,6 +161,7 @@ class TextDocument
|
||||
$file_path = LanguageServer::uriToPath($textDocument->uri);
|
||||
|
||||
$this->codebase->file_provider->closeFile($file_path);
|
||||
$this->server->client->textDocument->publishDiagnostics($textDocument->uri, []);
|
||||
}
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user