mirror of
https://github.com/danog/psalm.git
synced 2024-11-30 04:39:00 +01:00
Add server mode support with error reporting only
This commit is contained in:
parent
47e987ccf8
commit
54fdda651b
@ -11,11 +11,18 @@
|
||||
],
|
||||
"require": {
|
||||
"php": "^7.0",
|
||||
"nikic/PHP-Parser": "^4.0",
|
||||
"nikic/PHP-Parser": "dev-master",
|
||||
"openlss/lib-array2xml": "^0.0.10||^0.5.1",
|
||||
"muglug/package-versions-56": "1.2.4",
|
||||
"php-cs-fixer/diff": "^1.2",
|
||||
"composer/xdebug-handler": "^1.1"
|
||||
"composer/xdebug-handler": "^1.1",
|
||||
"felixfbecker/language-server-protocol": "^1.2",
|
||||
"felixfbecker/advanced-json-rpc": "^3.0.3",
|
||||
"netresearch/jsonmapper": "^1.0",
|
||||
"sabre/event": "^5.0.1",
|
||||
"sabre/uri": "^2.0",
|
||||
"webmozart/glob": "^4.1",
|
||||
"webmozart/path-util": "^2.3"
|
||||
},
|
||||
"bin": ["psalm", "psalter"],
|
||||
"autoload": {
|
||||
@ -31,6 +38,8 @@
|
||||
"config": {
|
||||
"optimize-autoloader": true
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^6.0 || ^7.0",
|
||||
"squizlabs/php_codesniffer": "^3.0",
|
||||
|
@ -15,5 +15,9 @@
|
||||
<exclude-pattern>tests/stubs/</exclude-pattern>
|
||||
<rule ref="Generic.Files.LineLength">
|
||||
<exclude-pattern>tests</exclude-pattern>
|
||||
</rule>
|
||||
</rule>
|
||||
|
||||
<rule ref="PSR2.Namespaces.UseDeclaration">
|
||||
<exclude-pattern>*</exclude-pattern>
|
||||
</rule>
|
||||
</ruleset>
|
||||
|
@ -18,6 +18,7 @@
|
||||
<exclude>
|
||||
<directory suffix=".php">src/Psalm/Issue/</directory>
|
||||
<directory suffix=".php">src/Psalm/Stubs/</directory>
|
||||
<directory suffix=".php">src/Psalm/LanguageServer/</directory>
|
||||
<file>src/command_functions.php</file>
|
||||
<file>src/psalm.php</file>
|
||||
<file>src/psalter.php</file>
|
||||
|
@ -80,6 +80,7 @@
|
||||
|
||||
<PossiblyUnusedProperty>
|
||||
<errorLevel type="info">
|
||||
<file name="src/Psalm/LanguageServer/LanguageClient.php" />
|
||||
<file name="src/Psalm/Storage/FunctionLikeStorage.php" />
|
||||
</errorLevel>
|
||||
</PossiblyUnusedProperty>
|
||||
@ -90,6 +91,8 @@
|
||||
<errorLevel type="suppress">
|
||||
<directory name="tests" />
|
||||
<file name="src/Psalm/Plugin.php" />
|
||||
<file name="src/Psalm/LanguageServer/Client/TextDocument.php" />
|
||||
<file name="src/Psalm/LanguageServer/Server/TextDocument.php" />
|
||||
<referencedMethod name="Psalm\Codebase::getParentInterfaces" />
|
||||
<referencedMethod name="Psalm\Codebase::getMethodParams" />
|
||||
<referencedMethod name="Psalm\Codebase::getMethodReturnType" />
|
||||
|
@ -79,7 +79,6 @@ class FileChecker extends SourceChecker implements StatementsSource
|
||||
public $project_checker;
|
||||
|
||||
/**
|
||||
* @param ProjectChecker $project_checker
|
||||
* @param string $file_path
|
||||
* @param string $file_name
|
||||
*/
|
||||
@ -95,13 +94,16 @@ class FileChecker extends SourceChecker implements StatementsSource
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function analyze(Context $file_context = null, $preserve_checkers = false, Context $global_context = null)
|
||||
{
|
||||
public function analyze(
|
||||
Context $file_context = null,
|
||||
$preserve_checkers = false,
|
||||
Context $global_context = null
|
||||
) {
|
||||
$codebase = $this->project_checker->codebase;
|
||||
|
||||
$file_storage = $codebase->file_storage_provider->get($this->file_path);
|
||||
|
||||
if (!$file_storage->deep_scan) {
|
||||
if (!$file_storage->deep_scan && !$codebase->server_mode) {
|
||||
throw new UnpreparedAnalysisException('File ' . $this->file_path . ' has not been properly scanned');
|
||||
}
|
||||
|
||||
@ -120,7 +122,11 @@ class FileChecker extends SourceChecker implements StatementsSource
|
||||
|
||||
$this->context->is_global = true;
|
||||
|
||||
$stmts = $codebase->getStatementsForFile($this->file_path);
|
||||
try {
|
||||
$stmts = $codebase->getStatementsForFile($this->file_path);
|
||||
} catch (PhpParser\Error $e) {
|
||||
return;
|
||||
}
|
||||
|
||||
$statements_checker = new StatementsChecker($this);
|
||||
|
||||
@ -138,6 +144,7 @@ class FileChecker extends SourceChecker implements StatementsSource
|
||||
}
|
||||
|
||||
// check any leftover classes not already evaluated
|
||||
|
||||
foreach ($this->class_checkers_to_analyze as $class_checker) {
|
||||
$class_checker->analyze(null, $this->context);
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
<?php
|
||||
namespace Psalm\Checker;
|
||||
|
||||
use JsonRPC\Server;
|
||||
use Psalm\Codebase;
|
||||
use Psalm\Config;
|
||||
use Psalm\Context;
|
||||
use Psalm\LanguageServer\{LanguageServer, ProtocolStreamReader, ProtocolStreamWriter};
|
||||
use Psalm\Provider\ClassLikeStorageCacheProvider;
|
||||
use Psalm\Provider\ClassLikeStorageProvider;
|
||||
use Psalm\Provider\FileProvider;
|
||||
@ -228,6 +230,121 @@ class ProjectChecker
|
||||
self::$instance = $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $base_dir
|
||||
* @param string|null $address
|
||||
* @param bool $server_mode
|
||||
* @return void
|
||||
*/
|
||||
public function server($address = '127.0.0.1:12345', $server_mode = true)
|
||||
{
|
||||
$this->file_reference_provider->loadReferenceCache();
|
||||
$this->codebase->enterServerMode();
|
||||
|
||||
$cwd = getcwd();
|
||||
|
||||
if (!$cwd) {
|
||||
throw new \InvalidArgumentException('Cannot work with empty cwd');
|
||||
}
|
||||
|
||||
$cpu_count = self::getCpuCount();
|
||||
|
||||
// let's not go crazy
|
||||
$usable_cpus = $cpu_count - 2;
|
||||
|
||||
if ($usable_cpus > 1) {
|
||||
$this->threads = $usable_cpus;
|
||||
}
|
||||
|
||||
$this->config->initializePlugins($this);
|
||||
|
||||
foreach ($this->config->getProjectDirectories() as $dir_name) {
|
||||
$this->checkDirWithConfig($dir_name, $this->config);
|
||||
}
|
||||
|
||||
$this->output_format = self::TYPE_JSON;
|
||||
|
||||
@cli_set_process_title('Psalm PHP Language Server');
|
||||
|
||||
if (!$server_mode && $address) {
|
||||
// Connect to a TCP server
|
||||
$socket = stream_socket_client('tcp://' . $address, $errno, $errstr);
|
||||
if ($socket === false) {
|
||||
fwrite(STDERR, "Could not connect to language client. Error $errno\n$errstr");
|
||||
exit(1);
|
||||
}
|
||||
stream_set_blocking($socket, false);
|
||||
new LanguageServer(
|
||||
new ProtocolStreamReader($socket),
|
||||
new ProtocolStreamWriter($socket),
|
||||
$this
|
||||
);
|
||||
Loop\run();
|
||||
} elseif ($server_mode && $address) {
|
||||
// Run a TCP Server
|
||||
$tcpServer = stream_socket_server('tcp://' . $address, $errno, $errstr);
|
||||
if ($tcpServer === false) {
|
||||
fwrite(STDERR, "Could not listen on $address. Error $errno\n$errstr");
|
||||
exit(1);
|
||||
}
|
||||
fwrite(STDOUT, "Server listening on $address\n");
|
||||
if (!extension_loaded('pcntl')) {
|
||||
fwrite(STDERR, "PCNTL is not available. Only a single connection will be accepted\n");
|
||||
}
|
||||
while ($socket = stream_socket_accept($tcpServer, -1)) {
|
||||
fwrite(STDOUT, "Connection accepted\n");
|
||||
stream_set_blocking($socket, false);
|
||||
if (extension_loaded('pcntl')) {
|
||||
// If PCNTL is available, fork a child process for the connection
|
||||
// An exit notification will only terminate the child process
|
||||
$pid = pcntl_fork();
|
||||
if ($pid === -1) {
|
||||
fwrite(STDERR, "Could not fork\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if ($pid === 0) {
|
||||
// Child process
|
||||
$reader = new ProtocolStreamReader($socket);
|
||||
$reader->on(
|
||||
'close',
|
||||
/** @return void */
|
||||
function () {
|
||||
fwrite(STDOUT, "Connection closed\n");
|
||||
}
|
||||
);
|
||||
new LanguageServer(
|
||||
$reader,
|
||||
new ProtocolStreamWriter($socket),
|
||||
$this
|
||||
);
|
||||
Loop\run();
|
||||
// Just for safety
|
||||
exit(0);
|
||||
}
|
||||
} else {
|
||||
// If PCNTL is not available, we only accept one connection.
|
||||
// An exit notification will terminate the server
|
||||
new LanguageServer(
|
||||
new ProtocolStreamReader($socket),
|
||||
new ProtocolStreamWriter($socket),
|
||||
$this
|
||||
);
|
||||
Loop\run();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Use STDIO
|
||||
stream_set_blocking(STDIN, false);
|
||||
new LanguageServer(
|
||||
new ProtocolStreamReader(STDIN),
|
||||
new ProtocolStreamWriter(STDOUT),
|
||||
$this
|
||||
);
|
||||
Loop\run();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return self
|
||||
*/
|
||||
@ -743,4 +860,60 @@ class ProjectChecker
|
||||
|
||||
$file_checker->getMethodMutations($appearing_method_id, $this_context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapted from https://gist.github.com/divinity76/01ef9ca99c111565a72d3a8a6e42f7fb
|
||||
* returns number of cpu cores
|
||||
* Copyleft 2018, license: WTFPL
|
||||
* @throws \RuntimeException
|
||||
* @throws \LogicException
|
||||
* @return int
|
||||
* @psalm-suppress ForbiddenCode
|
||||
*/
|
||||
private function getCpuCount(): int
|
||||
{
|
||||
if (defined('PHP_WINDOWS_VERSION_MAJOR')) {
|
||||
/*
|
||||
$str = trim((string) shell_exec('wmic cpu get NumberOfCores 2>&1'));
|
||||
if (!preg_match('/(\d+)/', $str, $matches)) {
|
||||
throw new \RuntimeException('wmic failed to get number of cpu cores on windows!');
|
||||
}
|
||||
return ((int) $matches [1]);
|
||||
*/
|
||||
return 1;
|
||||
}
|
||||
|
||||
$has_nproc = trim((string) @shell_exec('command -v nproc'));
|
||||
if ($has_nproc) {
|
||||
$ret = @shell_exec('nproc');
|
||||
if (is_string($ret)) {
|
||||
$ret = trim($ret);
|
||||
/** @var int|false */
|
||||
$tmp = filter_var($ret, FILTER_VALIDATE_INT);
|
||||
if (is_int($tmp)) {
|
||||
return $tmp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$ret = @shell_exec('sysctl -n hw.ncpu');
|
||||
if (is_string($ret)) {
|
||||
$ret = trim($ret);
|
||||
/** @var int|false */
|
||||
$tmp = filter_var($ret, FILTER_VALIDATE_INT);
|
||||
if (is_int($tmp)) {
|
||||
return $tmp;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_readable('/proc/cpuinfo')) {
|
||||
$cpuinfo = file_get_contents('/proc/cpuinfo');
|
||||
$count = substr_count($cpuinfo, 'processor');
|
||||
if ($count > 0) {
|
||||
return $count;
|
||||
}
|
||||
}
|
||||
|
||||
throw new \LogicException('failed to detect number of CPUs!');
|
||||
}
|
||||
}
|
||||
|
@ -136,6 +136,11 @@ class Codebase
|
||||
*/
|
||||
public $populator;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public $server_mode = false;
|
||||
|
||||
/**
|
||||
* @param bool $collect_references
|
||||
* @param bool $debug_output
|
||||
@ -272,6 +277,12 @@ class Codebase
|
||||
$this->populator->populateCodebase($this);
|
||||
}
|
||||
|
||||
/** @return void */
|
||||
public function enterServerMode()
|
||||
{
|
||||
$this->server_mode = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
@ -771,7 +782,7 @@ class Codebase
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function invalidateInformationForFile($file_path)
|
||||
private function invalidateInformationForFile(string $file_path)
|
||||
{
|
||||
$this->scanner->removeFile($file_path);
|
||||
|
||||
|
@ -493,6 +493,19 @@ class IssueBuffer
|
||||
self::$console_issues = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{severity: string, line_from: int, line_to: int, type: string, message: string,
|
||||
* file_name: string, file_path: string, snippet: string, from: int, to: int, snippet_from: int, snippet_to: int,
|
||||
* column_from: int, column_to: int}>
|
||||
*/
|
||||
public static function clear()
|
||||
{
|
||||
$current_data = self::$issues_data;
|
||||
self::$issues_data = [];
|
||||
self::$emitted = [];
|
||||
return $current_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
|
69
src/Psalm/LanguageServer/Client/TextDocument.php
Normal file
69
src/Psalm/LanguageServer/Client/TextDocument.php
Normal file
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Psalm\LanguageServer\Client;
|
||||
|
||||
use Psalm\LanguageServer\ClientHandler;
|
||||
use LanguageServerProtocol\{Diagnostic, TextDocumentItem, TextDocumentIdentifier};
|
||||
use Sabre\Event\Promise;
|
||||
use JsonMapper;
|
||||
|
||||
/**
|
||||
* Provides method handlers for all textDocument/* methods
|
||||
*/
|
||||
class TextDocument
|
||||
{
|
||||
/**
|
||||
* @var ClientHandler
|
||||
*/
|
||||
private $handler;
|
||||
|
||||
/**
|
||||
* @var JsonMapper
|
||||
*/
|
||||
private $mapper;
|
||||
|
||||
public function __construct(ClientHandler $handler, JsonMapper $mapper)
|
||||
{
|
||||
$this->handler = $handler;
|
||||
$this->mapper = $mapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Diagnostics notification are sent from the server to the client to signal results of validation runs.
|
||||
*
|
||||
* @param string $uri
|
||||
* @param Diagnostic[] $diagnostics
|
||||
* @return Promise <void>
|
||||
*/
|
||||
public function publishDiagnostics(string $uri, array $diagnostics): Promise
|
||||
{
|
||||
return $this->handler->notify('textDocument/publishDiagnostics', [
|
||||
'uri' => $uri,
|
||||
'diagnostics' => $diagnostics
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The content request is sent from a server to a client
|
||||
* to request the current content of a text document identified by the URI
|
||||
*
|
||||
* @param TextDocumentIdentifier $textDocument The document to get the content for
|
||||
* @return Promise <TextDocumentItem> The document's current content
|
||||
*/
|
||||
public function xcontent(TextDocumentIdentifier $textDocument): Promise
|
||||
{
|
||||
return $this->handler->request(
|
||||
'textDocument/xcontent',
|
||||
['textDocument' => $textDocument]
|
||||
)->then(
|
||||
/**
|
||||
* @param object $result
|
||||
* @return object
|
||||
*/
|
||||
function ($result) {
|
||||
return $this->mapper->map($result, new TextDocumentItem);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
97
src/Psalm/LanguageServer/ClientHandler.php
Normal file
97
src/Psalm/LanguageServer/ClientHandler.php
Normal file
@ -0,0 +1,97 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Psalm\LanguageServer;
|
||||
|
||||
use AdvancedJsonRpc;
|
||||
use Sabre\Event\Promise;
|
||||
|
||||
class ClientHandler
|
||||
{
|
||||
/**
|
||||
* @var ProtocolReader
|
||||
*/
|
||||
public $protocolReader;
|
||||
|
||||
/**
|
||||
* @var ProtocolWriter
|
||||
*/
|
||||
public $protocolWriter;
|
||||
|
||||
/**
|
||||
* @var IdGenerator
|
||||
*/
|
||||
public $idGenerator;
|
||||
|
||||
public function __construct(ProtocolReader $protocolReader, ProtocolWriter $protocolWriter)
|
||||
{
|
||||
$this->protocolReader = $protocolReader;
|
||||
$this->protocolWriter = $protocolWriter;
|
||||
$this->idGenerator = new IdGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to the client and returns a promise that is resolved with the result or rejected with the error
|
||||
*
|
||||
* @param string $method The method to call
|
||||
* @param array|object $params The method parameters
|
||||
* @return Promise <mixed> Resolved with the result of the request or rejected with an error
|
||||
*/
|
||||
public function request(string $method, $params): Promise
|
||||
{
|
||||
$id = $this->idGenerator->generate();
|
||||
return $this->protocolWriter->write(
|
||||
new Message(
|
||||
new AdvancedJsonRpc\Request($id, $method, (object)$params)
|
||||
)
|
||||
)->then(
|
||||
/**
|
||||
* @return Promise
|
||||
*/
|
||||
function () use ($id) {
|
||||
$promise = new Promise;
|
||||
$listener =
|
||||
/**
|
||||
* @param callable $listener
|
||||
* @return void
|
||||
*/
|
||||
function (Message $msg) use ($id, $promise, &$listener) {
|
||||
/**
|
||||
* @psalm-suppress UndefinedPropertyFetch
|
||||
* @psalm-suppress MixedArgument
|
||||
*/
|
||||
if ($msg->body
|
||||
&& AdvancedJsonRpc\Response::isResponse($msg->body)
|
||||
&& $msg->body->id === $id
|
||||
) {
|
||||
// Received a response
|
||||
$this->protocolReader->removeListener('message', $listener);
|
||||
if (AdvancedJsonRpc\SuccessResponse::isSuccessResponse($msg->body)) {
|
||||
$promise->fulfill($msg->body->result);
|
||||
} else {
|
||||
$promise->reject($msg->body->error);
|
||||
}
|
||||
}
|
||||
};
|
||||
$this->protocolReader->on('message', $listener);
|
||||
return $promise;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a notification to the client
|
||||
*
|
||||
* @param string $method The method to call
|
||||
* @param array|object $params The method parameters
|
||||
* @return Promise <null> Will be resolved as soon as the notification has been sent
|
||||
*/
|
||||
public function notify(string $method, $params): Promise
|
||||
{
|
||||
return $this->protocolWriter->write(
|
||||
new Message(
|
||||
new AdvancedJsonRpc\Notification($method, (object)$params)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
25
src/Psalm/LanguageServer/IdGenerator.php
Normal file
25
src/Psalm/LanguageServer/IdGenerator.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Psalm\LanguageServer;
|
||||
|
||||
/**
|
||||
* Generates unique, incremental IDs for use as request IDs
|
||||
*/
|
||||
class IdGenerator
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $counter = 1;
|
||||
|
||||
/**
|
||||
* Returns a unique ID
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function generate()
|
||||
{
|
||||
return $this->counter++;
|
||||
}
|
||||
}
|
24
src/Psalm/LanguageServer/LanguageClient.php
Normal file
24
src/Psalm/LanguageServer/LanguageClient.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Psalm\LanguageServer;
|
||||
|
||||
use JsonMapper;
|
||||
|
||||
class LanguageClient
|
||||
{
|
||||
/**
|
||||
* Handles textDocument/* methods
|
||||
*
|
||||
* @var Client\TextDocument
|
||||
*/
|
||||
public $textDocument;
|
||||
|
||||
public function __construct(ProtocolReader $reader, ProtocolWriter $writer)
|
||||
{
|
||||
$handler = new ClientHandler($reader, $writer);
|
||||
$mapper = new JsonMapper;
|
||||
|
||||
$this->textDocument = new Client\TextDocument($handler, $mapper);
|
||||
}
|
||||
}
|
368
src/Psalm/LanguageServer/LanguageServer.php
Normal file
368
src/Psalm/LanguageServer/LanguageServer.php
Normal file
@ -0,0 +1,368 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Psalm\LanguageServer;
|
||||
|
||||
use Psalm\Checker\ProjectChecker;
|
||||
use Psalm\Config;
|
||||
use LanguageServerProtocol\{
|
||||
ServerCapabilities,
|
||||
ClientCapabilities,
|
||||
TextDocumentSyncKind,
|
||||
InitializeResult,
|
||||
CompletionOptions,
|
||||
SignatureHelpOptions
|
||||
};
|
||||
use Psalm\LanguageServer\FilesFinder\{FilesFinder, ClientFilesFinder, FileSystemFilesFinder};
|
||||
use Psalm\LanguageServer\ContentRetriever\{ContentRetriever, ClientContentRetriever, FileSystemContentRetriever};
|
||||
use Psalm\LanguageServer\Index\{DependenciesIndex, GlobalIndex, Index, ProjectIndex, StubsIndex};
|
||||
use Psalm\LanguageServer\Cache\{FileSystemCache, ClientCache};
|
||||
use Psalm\LanguageServer\Server\TextDocument;
|
||||
use LanguageServerProtocol\{Range, Position, Diagnostic, DiagnosticSeverity};
|
||||
use AdvancedJsonRpc;
|
||||
use Sabre\Event\Loop;
|
||||
use Sabre\Event\Promise;
|
||||
use function Sabre\Event\coroutine;
|
||||
use Throwable;
|
||||
use Webmozart\PathUtil\Path;
|
||||
|
||||
class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
||||
{
|
||||
/**
|
||||
* Handles textDocument/* method calls
|
||||
*
|
||||
* @var ?Server\TextDocument
|
||||
*/
|
||||
public $textDocument;
|
||||
|
||||
/**
|
||||
* @var ProtocolReader
|
||||
*/
|
||||
protected $protocolReader;
|
||||
|
||||
/**
|
||||
* @var ProtocolWriter
|
||||
*/
|
||||
protected $protocolWriter;
|
||||
|
||||
/**
|
||||
* @var LanguageClient
|
||||
*/
|
||||
protected $client;
|
||||
|
||||
/**
|
||||
* @var ProjectChecker
|
||||
*/
|
||||
protected $project_checker;
|
||||
|
||||
/**
|
||||
* @param ProtocolReader $reader
|
||||
* @param ProtocolWriter $writer
|
||||
*/
|
||||
public function __construct(
|
||||
ProtocolReader $reader,
|
||||
ProtocolWriter $writer,
|
||||
ProjectChecker $project_checker
|
||||
) {
|
||||
parent::__construct($this, '/');
|
||||
$this->project_checker = $project_checker;
|
||||
|
||||
$this->protocolWriter = $writer;
|
||||
|
||||
$this->protocolReader = $reader;
|
||||
$this->protocolReader->on(
|
||||
'close',
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
function () {
|
||||
$this->shutdown();
|
||||
$this->exit();
|
||||
}
|
||||
);
|
||||
$this->protocolReader->on(
|
||||
'message',
|
||||
/** @return void */
|
||||
function (Message $msg) {
|
||||
coroutine(
|
||||
/** @return \Generator<int, Promise, mixed, void> */
|
||||
function () use ($msg) {
|
||||
if (!$msg->body) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore responses, this is the handler for requests and notifications
|
||||
if (AdvancedJsonRpc\Response::isResponse($msg->body)) {
|
||||
return;
|
||||
}
|
||||
$result = null;
|
||||
$error = null;
|
||||
try {
|
||||
// Invoke the method handler to get a result
|
||||
/**
|
||||
* @var Promise
|
||||
* @psalm-suppress UndefinedClass
|
||||
*/
|
||||
$dispatched = $this->dispatch($msg->body);
|
||||
$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
|
||||
/**
|
||||
* @psalm-suppress UndefinedPropertyFetch
|
||||
* @psalm-suppress MixedArgument
|
||||
*/
|
||||
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);
|
||||
}
|
||||
$this->protocolWriter->write(new Message($responseBody));
|
||||
}
|
||||
}
|
||||
)->otherwise('\Psalm\LanguageServer\LanguageServer::crash');
|
||||
}
|
||||
);
|
||||
|
||||
$this->client = new LanguageClient($reader, $writer);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @return Promise <InitializeResult>
|
||||
* @psalm-suppress PossiblyUnusedMethod
|
||||
*/
|
||||
public function initialize(
|
||||
ClientCapabilities $capabilities,
|
||||
string $rootPath = null,
|
||||
int $processId = null
|
||||
): Promise {
|
||||
return coroutine(
|
||||
/** @return \Generator<int, true, mixed, InitializeResult> */
|
||||
function () use ($capabilities, $rootPath, $processId) {
|
||||
// Eventually, this might block on something. Leave it as a generator.
|
||||
if (false) {
|
||||
yield true;
|
||||
}
|
||||
|
||||
$this->project_checker->codebase->scanFiles($this->project_checker->threads);
|
||||
|
||||
if ($this->textDocument === null) {
|
||||
$this->textDocument = new TextDocument(
|
||||
$this
|
||||
);
|
||||
}
|
||||
|
||||
$serverCapabilities = new ServerCapabilities();
|
||||
|
||||
$serverCapabilities->textDocumentSync = TextDocumentSyncKind::FULL;
|
||||
|
||||
// Support "Find all symbols"
|
||||
$serverCapabilities->documentSymbolProvider = false;
|
||||
// Support "Find all symbols in workspace"
|
||||
$serverCapabilities->workspaceSymbolProvider = false;
|
||||
// Support "Go to definition"
|
||||
$serverCapabilities->definitionProvider = false;
|
||||
// Support "Find all references"
|
||||
$serverCapabilities->referencesProvider = false;
|
||||
// Support "Hover"
|
||||
$serverCapabilities->hoverProvider = false;
|
||||
// Support "Completion"
|
||||
/*
|
||||
$serverCapabilities->completionProvider = new CompletionOptions;
|
||||
$serverCapabilities->completionProvider->resolveProvider = true;
|
||||
$serverCapabilities->completionProvider->triggerCharacters = ['$', '>'];
|
||||
*/
|
||||
|
||||
/*
|
||||
$serverCapabilities->signatureHelpProvider = new SignatureHelpOptions();
|
||||
$serverCapabilities->signatureHelpProvider->triggerCharacters = ['(', ','];
|
||||
*/
|
||||
|
||||
// Support global references
|
||||
$serverCapabilities->xworkspaceReferencesProvider = false;
|
||||
$serverCapabilities->xdefinitionProvider = false;
|
||||
$serverCapabilities->dependenciesProvider = false;
|
||||
|
||||
return new InitializeResult($serverCapabilities);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-suppress PossiblyUnusedMethod
|
||||
* @return void
|
||||
*/
|
||||
public function initialized()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function invalidateFileAndDependents(string $uri)
|
||||
{
|
||||
$file_path = self::uriToPath($uri);
|
||||
$this->project_checker->codebase->reloadFiles($this->project_checker, [$file_path]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function analyzePath(string $file_path)
|
||||
{
|
||||
$codebase = $this->project_checker->codebase;
|
||||
|
||||
$codebase->addFilesToAnalyze([$file_path => $file_path]);
|
||||
$codebase->analyzer->analyzeFiles($this->project_checker, 1, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function emitIssues(string $uri)
|
||||
{
|
||||
$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;
|
||||
}
|
||||
// TODO: copy issue code in 'json' format
|
||||
return new Diagnostic(
|
||||
$description,
|
||||
$range,
|
||||
null,
|
||||
$diagnostic_severity,
|
||||
'Psalm'
|
||||
);
|
||||
},
|
||||
$data
|
||||
);
|
||||
|
||||
$this->client->textDocument->publishDiagnostics($uri, $diagnostics);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function shutdown()
|
||||
{
|
||||
$scanned_files = $this->project_checker->codebase->scanner->getScannedFiles();
|
||||
$this->project_checker->file_reference_provider->updateReferenceCache(
|
||||
$this->project_checker->codebase,
|
||||
$scanned_files
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A notification to ask the server to exit its process.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function exit()
|
||||
{
|
||||
exit(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms URI into file path
|
||||
*
|
||||
* @param string $uri
|
||||
* @return string
|
||||
*/
|
||||
public static function uriToPath(string $uri)
|
||||
{
|
||||
$fragments = parse_url($uri);
|
||||
if ($fragments === false || !isset($fragments['scheme']) || $fragments['scheme'] !== 'file') {
|
||||
throw new \InvalidArgumentException("Not a valid file URI: $uri");
|
||||
}
|
||||
$filepath = urldecode((string) $fragments['path']);
|
||||
if (strpos($filepath, ':') !== false) {
|
||||
if ($filepath[0] === '/') {
|
||||
$filepath = substr($filepath, 1);
|
||||
}
|
||||
$filepath = str_replace('/', '\\', $filepath);
|
||||
}
|
||||
return $filepath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws an exception on the next tick.
|
||||
* Useful for letting a promise crash the process on rejection.
|
||||
*
|
||||
* @param Throwable $err
|
||||
* @return void
|
||||
* @psalm-suppress PossiblyUnusedMethod
|
||||
*/
|
||||
public static function crash(Throwable $err)
|
||||
{
|
||||
Loop\nextTick(
|
||||
/** @return void */
|
||||
function () use ($err) {
|
||||
throw $err;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
65
src/Psalm/LanguageServer/Message.php
Normal file
65
src/Psalm/LanguageServer/Message.php
Normal file
@ -0,0 +1,65 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Psalm\LanguageServer;
|
||||
|
||||
use AdvancedJsonRpc\Message as MessageBody;
|
||||
|
||||
class Message
|
||||
{
|
||||
/**
|
||||
* @var ?\AdvancedJsonRpc\Message
|
||||
*/
|
||||
public $body;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
public $headers;
|
||||
|
||||
/**
|
||||
* Parses a message
|
||||
*
|
||||
* @param string $msg
|
||||
* @return Message
|
||||
* @psalm-suppress UnusedMethod
|
||||
*/
|
||||
public static function parse(string $msg): Message
|
||||
{
|
||||
$obj = new self;
|
||||
$parts = explode("\r\n", $msg);
|
||||
$obj->body = MessageBody::parse(array_pop($parts));
|
||||
foreach ($parts as $line) {
|
||||
if ($line) {
|
||||
$pair = explode(': ', $line);
|
||||
$obj->headers[$pair[0]] = $pair[1];
|
||||
}
|
||||
}
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \AdvancedJsonRpc\Message $body
|
||||
* @param string[] $headers
|
||||
*/
|
||||
public function __construct(MessageBody $body = null, array $headers = [])
|
||||
{
|
||||
$this->body = $body;
|
||||
if (!isset($headers['Content-Type'])) {
|
||||
$headers['Content-Type'] = 'application/vscode-jsonrpc; charset=utf8';
|
||||
}
|
||||
$this->headers = $headers;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
$body = (string)$this->body;
|
||||
$contentLength = strlen($body);
|
||||
$this->headers['Content-Length'] = (string) $contentLength;
|
||||
$headers = '';
|
||||
foreach ($this->headers as $name => $value) {
|
||||
$headers .= "$name: $value\r\n";
|
||||
}
|
||||
return $headers . "\r\n" . $body;
|
||||
}
|
||||
}
|
17
src/Psalm/LanguageServer/ProtocolReader.php
Normal file
17
src/Psalm/LanguageServer/ProtocolReader.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Psalm\LanguageServer;
|
||||
|
||||
use Sabre\Event\EmitterInterface;
|
||||
|
||||
/**
|
||||
* Must emit a "message" event with a Message object as parameter
|
||||
* when a message comes in
|
||||
*
|
||||
* Must emit a "close" event when the stream closes
|
||||
*/
|
||||
interface ProtocolReader extends EmitterInterface
|
||||
{
|
||||
|
||||
}
|
83
src/Psalm/LanguageServer/ProtocolStreamReader.php
Normal file
83
src/Psalm/LanguageServer/ProtocolStreamReader.php
Normal file
@ -0,0 +1,83 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Psalm\LanguageServer;
|
||||
|
||||
use Psalm\LanguageServer\Message;
|
||||
use AdvancedJsonRpc\Message as MessageBody;
|
||||
use Sabre\Event\{Loop, Emitter};
|
||||
|
||||
class ProtocolStreamReader extends Emitter implements ProtocolReader
|
||||
{
|
||||
const PARSE_HEADERS = 1;
|
||||
const PARSE_BODY = 2;
|
||||
|
||||
/** @var resource */
|
||||
private $input;
|
||||
|
||||
/** @var int */
|
||||
private $parsingMode = self::PARSE_HEADERS;
|
||||
|
||||
/** @var string */
|
||||
private $buffer = '';
|
||||
|
||||
/** @var array<string, string> */
|
||||
private $headers = [];
|
||||
|
||||
/** @var ?int */
|
||||
private $contentLength;
|
||||
|
||||
/**
|
||||
* @param resource $input
|
||||
*/
|
||||
public function __construct($input)
|
||||
{
|
||||
$this->input = $input;
|
||||
|
||||
$this->on(
|
||||
'close',
|
||||
/** @return void */
|
||||
function () {
|
||||
Loop\removeReadStream($this->input);
|
||||
}
|
||||
);
|
||||
|
||||
Loop\addReadStream(
|
||||
$this->input,
|
||||
/** @return void */
|
||||
function () {
|
||||
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');
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
92
src/Psalm/LanguageServer/ProtocolStreamWriter.php
Normal file
92
src/Psalm/LanguageServer/ProtocolStreamWriter.php
Normal file
@ -0,0 +1,92 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Psalm\LanguageServer;
|
||||
|
||||
use Psalm\LanguageServer\Message;
|
||||
use Sabre\Event\{
|
||||
Loop,
|
||||
Promise
|
||||
};
|
||||
use RuntimeException;
|
||||
|
||||
class ProtocolStreamWriter implements ProtocolWriter
|
||||
{
|
||||
/**
|
||||
* @var resource $output
|
||||
*/
|
||||
private $output;
|
||||
|
||||
/**
|
||||
* @var array<int, array{message: string, promise: Promise}> $messages
|
||||
*/
|
||||
private $messages = [];
|
||||
|
||||
/**
|
||||
* @param resource $output
|
||||
*/
|
||||
public function __construct($output)
|
||||
{
|
||||
$this->output = $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function write(Message $msg): Promise
|
||||
{
|
||||
// if the message queue is currently empty, register a write handler.
|
||||
if (empty($this->messages)) {
|
||||
Loop\addWriteStream(
|
||||
$this->output,
|
||||
/** @return void */
|
||||
function () {
|
||||
$this->flush();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
$promise = new Promise();
|
||||
$this->messages[] = [
|
||||
'message' => (string)$msg,
|
||||
'promise' => $promise
|
||||
];
|
||||
return $promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes pending messages to the output stream.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function flush()
|
||||
{
|
||||
$keepWriting = true;
|
||||
while ($keepWriting) {
|
||||
$message = $this->messages[0]['message'];
|
||||
$promise = $this->messages[0]['promise'];
|
||||
|
||||
$bytesWritten = @fwrite($this->output, $message);
|
||||
|
||||
if ($bytesWritten > 0) {
|
||||
$message = substr($message, $bytesWritten);
|
||||
}
|
||||
|
||||
// Determine if this message was completely sent
|
||||
if (strlen($message) === 0) {
|
||||
array_shift($this->messages);
|
||||
|
||||
// This was the last message in the queue, remove the write handler.
|
||||
if (count($this->messages) === 0) {
|
||||
Loop\removeWriteStream($this->output);
|
||||
$keepWriting = false;
|
||||
}
|
||||
|
||||
$promise->fulfill();
|
||||
} else {
|
||||
$this->messages[0]['message'] = $message;
|
||||
$keepWriting = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
18
src/Psalm/LanguageServer/ProtocolWriter.php
Normal file
18
src/Psalm/LanguageServer/ProtocolWriter.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Psalm\LanguageServer;
|
||||
|
||||
use Psalm\LanguageServer\Message;
|
||||
use Sabre\Event\Promise;
|
||||
|
||||
interface ProtocolWriter
|
||||
{
|
||||
/**
|
||||
* Sends a Message to the client
|
||||
*
|
||||
* @param Message $msg
|
||||
* @return Promise Resolved when the message has been fully written out to the output stream
|
||||
*/
|
||||
public function write(Message $msg): Promise;
|
||||
}
|
117
src/Psalm/LanguageServer/Server/TextDocument.php
Normal file
117
src/Psalm/LanguageServer/Server/TextDocument.php
Normal file
@ -0,0 +1,117 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Psalm\LanguageServer\Server;
|
||||
|
||||
use PhpParser\PrettyPrinter\Standard as PrettyPrinter;
|
||||
use PhpParser\{
|
||||
Node,
|
||||
NodeTraverser
|
||||
};
|
||||
use Psalm\LanguageServer\{
|
||||
LanguageServer,
|
||||
LanguageClient,
|
||||
PhpDocumentLoader,
|
||||
PhpDocument,
|
||||
DefinitionResolver,
|
||||
CompletionProvider
|
||||
};
|
||||
use Psalm\LanguageServer\NodeVisitor\VariableReferencesCollector;
|
||||
use LanguageServerProtocol\{
|
||||
SymbolLocationInformation,
|
||||
SymbolDescriptor,
|
||||
TextDocumentItem,
|
||||
TextDocumentIdentifier,
|
||||
VersionedTextDocumentIdentifier,
|
||||
Position,
|
||||
Range,
|
||||
FormattingOptions,
|
||||
TextEdit,
|
||||
Location,
|
||||
SymbolInformation,
|
||||
ReferenceContext,
|
||||
Hover,
|
||||
MarkedString,
|
||||
SymbolKind,
|
||||
CompletionItem,
|
||||
CompletionItemKind
|
||||
};
|
||||
use Psalm\Codebase;
|
||||
use Psalm\LanguageServer\Index\ReadableIndex;
|
||||
use Psalm\Checker\FileChecker;
|
||||
use Psalm\Checker\ClassLikeChecker;
|
||||
use Sabre\Event\Promise;
|
||||
use Sabre\Uri;
|
||||
use function Sabre\Event\coroutine;
|
||||
use function Psalm\LanguageServer\{waitForEvent, isVendored};
|
||||
|
||||
/**
|
||||
* Provides method handlers for all textDocument/* methods
|
||||
*/
|
||||
class TextDocument
|
||||
{
|
||||
/**
|
||||
* @var LanguageServer
|
||||
*/
|
||||
protected $server;
|
||||
|
||||
public function __construct(
|
||||
LanguageServer $server
|
||||
) {
|
||||
$this->server = $server;
|
||||
}
|
||||
|
||||
/**
|
||||
* The document open notification is sent from the client to the server to signal newly opened text documents. The
|
||||
* document's truth is now managed by the client and the server must not try to read the document's truth using the
|
||||
* document's uri.
|
||||
*
|
||||
* @param \LanguageServerProtocol\TextDocumentItem $textDocument The document that was opened.
|
||||
* @return void
|
||||
*/
|
||||
public function didOpen(TextDocumentItem $textDocument)
|
||||
{
|
||||
$file_path = LanguageServer::uriToPath($textDocument->uri);
|
||||
|
||||
$this->server->invalidateFileAndDependents($textDocument->uri);
|
||||
|
||||
$this->server->analyzePath($file_path);
|
||||
$this->server->emitIssues($textDocument->uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function didSave(TextDocumentItem $textDocument)
|
||||
{
|
||||
$file_path = LanguageServer::uriToPath($textDocument->uri);
|
||||
|
||||
$this->server->invalidateFileAndDependents($textDocument->uri);
|
||||
|
||||
$this->server->analyzePath($file_path);
|
||||
$this->server->emitIssues($textDocument->uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* The document change notification is sent from the client to the server to signal changes to a text document.
|
||||
*
|
||||
* @param \LanguageServerProtocol\VersionedTextDocumentIdentifier $textDocument
|
||||
* @param \LanguageServerProtocol\TextDocumentContentChangeEvent[] $contentChanges
|
||||
* @return void
|
||||
*/
|
||||
public function didChange(VersionedTextDocumentIdentifier $textDocument, array $contentChanges)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* The document close notification is sent from the client to the server when the document got closed in the client.
|
||||
* The document's truth now exists where the document's uri points to (e.g. if the document's uri is a file uri the
|
||||
* truth now exists on disk).
|
||||
*
|
||||
* @param \LanguageServerProtocol\TextDocumentIdentifier $textDocument The document that was closed
|
||||
* @return void
|
||||
*/
|
||||
public function didClose(TextDocumentIdentifier $textDocument)
|
||||
{
|
||||
}
|
||||
}
|
@ -77,7 +77,7 @@ class ClassLikeStorageCacheProvider
|
||||
$cached_value = $this->loadFromCache($fq_classlike_name_lc, $file_path);
|
||||
|
||||
if (!$cached_value) {
|
||||
throw new \UnexpectedValueException('Should be in cache');
|
||||
throw new \UnexpectedValueException($fq_classlike_name_lc . ' should be in cache');
|
||||
}
|
||||
|
||||
$cache_hash = $this->getCacheHash($file_path, $file_contents);
|
||||
@ -88,7 +88,7 @@ class ClassLikeStorageCacheProvider
|
||||
) {
|
||||
unlink($this->getCacheLocationForClass($fq_classlike_name_lc, $file_path));
|
||||
|
||||
throw new \UnexpectedValueException('Should not be outdated');
|
||||
throw new \UnexpectedValueException($fq_classlike_name_lc . ' should not be outdated');
|
||||
}
|
||||
|
||||
return $cached_value;
|
||||
|
@ -1,6 +1,9 @@
|
||||
<?php
|
||||
namespace Psalm\Provider;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\Checker\ProjectChecker;
|
||||
|
||||
class FileProvider
|
||||
{
|
||||
/**
|
||||
|
@ -20,6 +20,8 @@ use Psalm\Config;
|
||||
* column_from: int,
|
||||
* column_to: int
|
||||
* }
|
||||
*
|
||||
* @psalm-type TaggedCodeType = array<int, array{0: int, 1: string}>
|
||||
*/
|
||||
/**
|
||||
* Used to determine which files reference other files, necessary for using the --diff
|
||||
@ -31,6 +33,7 @@ class FileReferenceCacheProvider
|
||||
const CORRECT_METHODS_CACHE_NAME = 'correct_methods';
|
||||
const CLASS_METHOD_CACHE_NAME = 'class_method_references';
|
||||
const ISSUES_CACHE_NAME = 'issues';
|
||||
const FILE_MAPS_CACHE_NAME = 'file_maps';
|
||||
|
||||
/**
|
||||
* @var Config
|
||||
|
@ -397,10 +397,16 @@ class FileReferenceProvider
|
||||
|
||||
/**
|
||||
* @param string $file_path
|
||||
* @param IssueData $issue
|
||||
* @return void
|
||||
*/
|
||||
public function addIssue($file_path, array $issue)
|
||||
{
|
||||
// don’t save parse errors ever, as they're not responsive to AST diffing
|
||||
if ($issue['type'] === 'ParseError') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isset(self::$issues[$file_path])) {
|
||||
self::$issues[$file_path] = [$issue];
|
||||
} else {
|
||||
|
@ -74,6 +74,7 @@ class FileStorageCacheProvider
|
||||
*/
|
||||
public function getLatestFromCache($file_path, $file_contents)
|
||||
{
|
||||
$file_path = strtolower($file_path);
|
||||
$cached_value = $this->loadFromCache($file_path);
|
||||
|
||||
if (!$cached_value) {
|
||||
|
@ -2,6 +2,7 @@
|
||||
namespace Psalm\Provider;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\Checker\ProjectChecker;
|
||||
|
||||
class StatementsProvider
|
||||
{
|
||||
@ -86,7 +87,9 @@ class StatementsProvider
|
||||
echo 'Parsing ' . $file_path . "\n";
|
||||
}
|
||||
|
||||
return self::parseStatements($file_contents, $file_path) ?: [];
|
||||
$stmts = self::parseStatements($file_contents, $file_path);
|
||||
|
||||
return $stmts ?: [];
|
||||
}
|
||||
|
||||
$file_content_hash = md5($version . $file_contents);
|
||||
@ -275,7 +278,8 @@ class StatementsProvider
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $file_contents
|
||||
* @param string $file_contents
|
||||
* @param bool $server_mode
|
||||
* @param string $file_path
|
||||
*
|
||||
* @return array<int, \PhpParser\Node\Stmt>
|
||||
@ -283,11 +287,11 @@ class StatementsProvider
|
||||
public static function parseStatements($file_contents, $file_path = null)
|
||||
{
|
||||
if (!self::$parser) {
|
||||
$lexer = new PhpParser\Lexer([
|
||||
'usedAttributes' => [
|
||||
'comments', 'startLine', 'startFilePos', 'endFilePos',
|
||||
],
|
||||
]);
|
||||
$attributes = [
|
||||
'comments', 'startLine', 'startFilePos', 'endFilePos',
|
||||
];
|
||||
|
||||
$lexer = new PhpParser\Lexer\Emulative([ 'usedAttributes' => $attributes ]);
|
||||
|
||||
self::$parser = (new PhpParser\ParserFactory())->create(PhpParser\ParserFactory::PREFER_PHP7, $lexer);
|
||||
}
|
||||
@ -323,7 +327,6 @@ class StatementsProvider
|
||||
}
|
||||
}
|
||||
|
||||
/** @var array<int, \PhpParser\Node\Stmt> */
|
||||
self::$node_traverser->traverse($stmts);
|
||||
|
||||
return $stmts;
|
||||
|
@ -3,6 +3,7 @@ namespace Psalm\Storage;
|
||||
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Type;
|
||||
use Psalm\Checker\ClassLikeChecker;
|
||||
|
||||
class FunctionLikeStorage
|
||||
{
|
||||
@ -140,4 +141,36 @@ class FunctionLikeStorage
|
||||
* @var string|null
|
||||
*/
|
||||
public $return_type_description;
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
$symbol_text = 'function ' . $this->cased_name . '(' . implode(
|
||||
', ',
|
||||
array_map(
|
||||
function (FunctionLikeParameter $param) : string {
|
||||
return ($param->type ?: 'mixed') . ' $' . $param->name;
|
||||
},
|
||||
$this->params
|
||||
)
|
||||
) . ') : ' . ($this->return_type ?: 'mixed');
|
||||
|
||||
if (!$this instanceof MethodStorage) {
|
||||
return $symbol_text;
|
||||
}
|
||||
|
||||
switch ($this->visibility) {
|
||||
case ClassLikeChecker::VISIBILITY_PRIVATE:
|
||||
$visibility_text = 'private';
|
||||
break;
|
||||
|
||||
case ClassLikeChecker::VISIBILITY_PROTECTED:
|
||||
$visibility_text = 'protected';
|
||||
break;
|
||||
|
||||
default:
|
||||
$visibility_text = 'public';
|
||||
}
|
||||
|
||||
return $visibility_text . ' ' . $symbol_text;
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ namespace Psalm\Storage;
|
||||
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Type;
|
||||
use Psalm\Checker\ClassLikeChecker;
|
||||
|
||||
class PropertyStorage
|
||||
{
|
||||
|
@ -43,6 +43,7 @@ $valid_long_options = [
|
||||
'use-ini-defaults',
|
||||
'version',
|
||||
'diff-methods',
|
||||
'server',
|
||||
];
|
||||
|
||||
$args = array_slice($argv, 1);
|
||||
@ -190,6 +191,9 @@ Options:
|
||||
--disable-extension=[extension]
|
||||
Used to disable certain extensions while Psalm is running.
|
||||
|
||||
--server=[url]
|
||||
Start Psalm in server mode with optional TCP url (by default it uses stdio)
|
||||
|
||||
HELP;
|
||||
|
||||
exit;
|
||||
@ -426,7 +430,7 @@ $project_checker = new ProjectChecker(
|
||||
!isset($options['show-snippet']) || $options['show-snippet'] !== "false"
|
||||
);
|
||||
|
||||
$project_checker->diff_methods = isset($options['diff-methods']);
|
||||
$project_checker->diff_methods = isset($options['diff-methods']) || isset($options['server']);
|
||||
|
||||
$start_time = microtime(true);
|
||||
|
||||
@ -459,7 +463,9 @@ foreach ($plugins as $plugin_path) {
|
||||
Config::getInstance()->addPluginPath($current_dir . DIRECTORY_SEPARATOR . $plugin_path);
|
||||
}
|
||||
|
||||
if ($paths_to_check === null) {
|
||||
if (isset($options['server'])) {
|
||||
$project_checker->server(is_string($options['server']) ? $options['server'] : null);
|
||||
} elseif ($paths_to_check === null) {
|
||||
$project_checker->check($current_dir, $is_diff);
|
||||
} elseif ($paths_to_check) {
|
||||
$project_checker->checkPaths($paths_to_check);
|
||||
|
Loading…
Reference in New Issue
Block a user