1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-27 12:55:26 +01:00
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:
Matthew Brown 2018-11-17 18:00:28 -05:00
parent 1ada15f6c6
commit c2e546facb
4 changed files with 229 additions and 113 deletions

View File

@ -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) {

View File

@ -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);
}
}
/**

View File

@ -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');
}
}

View File

@ -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, []);
}