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": {
|
"require": {
|
||||||
"php": "^7.0",
|
"php": "^7.0",
|
||||||
"nikic/PHP-Parser": "^4.0",
|
"nikic/PHP-Parser": "dev-master",
|
||||||
"openlss/lib-array2xml": "^0.0.10||^0.5.1",
|
"openlss/lib-array2xml": "^0.0.10||^0.5.1",
|
||||||
"muglug/package-versions-56": "1.2.4",
|
"muglug/package-versions-56": "1.2.4",
|
||||||
"php-cs-fixer/diff": "^1.2",
|
"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"],
|
"bin": ["psalm", "psalter"],
|
||||||
"autoload": {
|
"autoload": {
|
||||||
@ -31,6 +38,8 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"optimize-autoloader": true
|
"optimize-autoloader": true
|
||||||
},
|
},
|
||||||
|
"minimum-stability": "dev",
|
||||||
|
"prefer-stable": true,
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpunit/phpunit": "^6.0 || ^7.0",
|
"phpunit/phpunit": "^6.0 || ^7.0",
|
||||||
"squizlabs/php_codesniffer": "^3.0",
|
"squizlabs/php_codesniffer": "^3.0",
|
||||||
|
@ -15,5 +15,9 @@
|
|||||||
<exclude-pattern>tests/stubs/</exclude-pattern>
|
<exclude-pattern>tests/stubs/</exclude-pattern>
|
||||||
<rule ref="Generic.Files.LineLength">
|
<rule ref="Generic.Files.LineLength">
|
||||||
<exclude-pattern>tests</exclude-pattern>
|
<exclude-pattern>tests</exclude-pattern>
|
||||||
</rule>
|
</rule>
|
||||||
|
|
||||||
|
<rule ref="PSR2.Namespaces.UseDeclaration">
|
||||||
|
<exclude-pattern>*</exclude-pattern>
|
||||||
|
</rule>
|
||||||
</ruleset>
|
</ruleset>
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
<exclude>
|
<exclude>
|
||||||
<directory suffix=".php">src/Psalm/Issue/</directory>
|
<directory suffix=".php">src/Psalm/Issue/</directory>
|
||||||
<directory suffix=".php">src/Psalm/Stubs/</directory>
|
<directory suffix=".php">src/Psalm/Stubs/</directory>
|
||||||
|
<directory suffix=".php">src/Psalm/LanguageServer/</directory>
|
||||||
<file>src/command_functions.php</file>
|
<file>src/command_functions.php</file>
|
||||||
<file>src/psalm.php</file>
|
<file>src/psalm.php</file>
|
||||||
<file>src/psalter.php</file>
|
<file>src/psalter.php</file>
|
||||||
|
@ -80,6 +80,7 @@
|
|||||||
|
|
||||||
<PossiblyUnusedProperty>
|
<PossiblyUnusedProperty>
|
||||||
<errorLevel type="info">
|
<errorLevel type="info">
|
||||||
|
<file name="src/Psalm/LanguageServer/LanguageClient.php" />
|
||||||
<file name="src/Psalm/Storage/FunctionLikeStorage.php" />
|
<file name="src/Psalm/Storage/FunctionLikeStorage.php" />
|
||||||
</errorLevel>
|
</errorLevel>
|
||||||
</PossiblyUnusedProperty>
|
</PossiblyUnusedProperty>
|
||||||
@ -90,6 +91,8 @@
|
|||||||
<errorLevel type="suppress">
|
<errorLevel type="suppress">
|
||||||
<directory name="tests" />
|
<directory name="tests" />
|
||||||
<file name="src/Psalm/Plugin.php" />
|
<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::getParentInterfaces" />
|
||||||
<referencedMethod name="Psalm\Codebase::getMethodParams" />
|
<referencedMethod name="Psalm\Codebase::getMethodParams" />
|
||||||
<referencedMethod name="Psalm\Codebase::getMethodReturnType" />
|
<referencedMethod name="Psalm\Codebase::getMethodReturnType" />
|
||||||
|
@ -79,7 +79,6 @@ class FileChecker extends SourceChecker implements StatementsSource
|
|||||||
public $project_checker;
|
public $project_checker;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param ProjectChecker $project_checker
|
|
||||||
* @param string $file_path
|
* @param string $file_path
|
||||||
* @param string $file_name
|
* @param string $file_name
|
||||||
*/
|
*/
|
||||||
@ -95,13 +94,16 @@ class FileChecker extends SourceChecker implements StatementsSource
|
|||||||
*
|
*
|
||||||
* @return void
|
* @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;
|
$codebase = $this->project_checker->codebase;
|
||||||
|
|
||||||
$file_storage = $codebase->file_storage_provider->get($this->file_path);
|
$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');
|
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;
|
$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);
|
$statements_checker = new StatementsChecker($this);
|
||||||
|
|
||||||
@ -138,6 +144,7 @@ class FileChecker extends SourceChecker implements StatementsSource
|
|||||||
}
|
}
|
||||||
|
|
||||||
// check any leftover classes not already evaluated
|
// check any leftover classes not already evaluated
|
||||||
|
|
||||||
foreach ($this->class_checkers_to_analyze as $class_checker) {
|
foreach ($this->class_checkers_to_analyze as $class_checker) {
|
||||||
$class_checker->analyze(null, $this->context);
|
$class_checker->analyze(null, $this->context);
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace Psalm\Checker;
|
namespace Psalm\Checker;
|
||||||
|
|
||||||
|
use JsonRPC\Server;
|
||||||
use Psalm\Codebase;
|
use Psalm\Codebase;
|
||||||
use Psalm\Config;
|
use Psalm\Config;
|
||||||
use Psalm\Context;
|
use Psalm\Context;
|
||||||
|
use Psalm\LanguageServer\{LanguageServer, ProtocolStreamReader, ProtocolStreamWriter};
|
||||||
use Psalm\Provider\ClassLikeStorageCacheProvider;
|
use Psalm\Provider\ClassLikeStorageCacheProvider;
|
||||||
use Psalm\Provider\ClassLikeStorageProvider;
|
use Psalm\Provider\ClassLikeStorageProvider;
|
||||||
use Psalm\Provider\FileProvider;
|
use Psalm\Provider\FileProvider;
|
||||||
@ -228,6 +230,121 @@ class ProjectChecker
|
|||||||
self::$instance = $this;
|
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
|
* @return self
|
||||||
*/
|
*/
|
||||||
@ -743,4 +860,60 @@ class ProjectChecker
|
|||||||
|
|
||||||
$file_checker->getMethodMutations($appearing_method_id, $this_context);
|
$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;
|
public $populator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public $server_mode = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param bool $collect_references
|
* @param bool $collect_references
|
||||||
* @param bool $debug_output
|
* @param bool $debug_output
|
||||||
@ -272,6 +277,12 @@ class Codebase
|
|||||||
$this->populator->populateCodebase($this);
|
$this->populator->populateCodebase($this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return void */
|
||||||
|
public function enterServerMode()
|
||||||
|
{
|
||||||
|
$this->server_mode = true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
@ -771,7 +782,7 @@ class Codebase
|
|||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
private function invalidateInformationForFile($file_path)
|
private function invalidateInformationForFile(string $file_path)
|
||||||
{
|
{
|
||||||
$this->scanner->removeFile($file_path);
|
$this->scanner->removeFile($file_path);
|
||||||
|
|
||||||
|
@ -493,6 +493,19 @@ class IssueBuffer
|
|||||||
self::$console_issues = [];
|
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
|
* @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);
|
$cached_value = $this->loadFromCache($fq_classlike_name_lc, $file_path);
|
||||||
|
|
||||||
if (!$cached_value) {
|
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);
|
$cache_hash = $this->getCacheHash($file_path, $file_contents);
|
||||||
@ -88,7 +88,7 @@ class ClassLikeStorageCacheProvider
|
|||||||
) {
|
) {
|
||||||
unlink($this->getCacheLocationForClass($fq_classlike_name_lc, $file_path));
|
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;
|
return $cached_value;
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace Psalm\Provider;
|
namespace Psalm\Provider;
|
||||||
|
|
||||||
|
use PhpParser;
|
||||||
|
use Psalm\Checker\ProjectChecker;
|
||||||
|
|
||||||
class FileProvider
|
class FileProvider
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
@ -20,6 +20,8 @@ use Psalm\Config;
|
|||||||
* column_from: int,
|
* column_from: int,
|
||||||
* column_to: 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
|
* 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 CORRECT_METHODS_CACHE_NAME = 'correct_methods';
|
||||||
const CLASS_METHOD_CACHE_NAME = 'class_method_references';
|
const CLASS_METHOD_CACHE_NAME = 'class_method_references';
|
||||||
const ISSUES_CACHE_NAME = 'issues';
|
const ISSUES_CACHE_NAME = 'issues';
|
||||||
|
const FILE_MAPS_CACHE_NAME = 'file_maps';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Config
|
* @var Config
|
||||||
|
@ -397,10 +397,16 @@ class FileReferenceProvider
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $file_path
|
* @param string $file_path
|
||||||
|
* @param IssueData $issue
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function addIssue($file_path, array $issue)
|
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])) {
|
if (!isset(self::$issues[$file_path])) {
|
||||||
self::$issues[$file_path] = [$issue];
|
self::$issues[$file_path] = [$issue];
|
||||||
} else {
|
} else {
|
||||||
|
@ -74,6 +74,7 @@ class FileStorageCacheProvider
|
|||||||
*/
|
*/
|
||||||
public function getLatestFromCache($file_path, $file_contents)
|
public function getLatestFromCache($file_path, $file_contents)
|
||||||
{
|
{
|
||||||
|
$file_path = strtolower($file_path);
|
||||||
$cached_value = $this->loadFromCache($file_path);
|
$cached_value = $this->loadFromCache($file_path);
|
||||||
|
|
||||||
if (!$cached_value) {
|
if (!$cached_value) {
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
namespace Psalm\Provider;
|
namespace Psalm\Provider;
|
||||||
|
|
||||||
use PhpParser;
|
use PhpParser;
|
||||||
|
use Psalm\Checker\ProjectChecker;
|
||||||
|
|
||||||
class StatementsProvider
|
class StatementsProvider
|
||||||
{
|
{
|
||||||
@ -86,7 +87,9 @@ class StatementsProvider
|
|||||||
echo 'Parsing ' . $file_path . "\n";
|
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);
|
$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
|
* @param string $file_path
|
||||||
*
|
*
|
||||||
* @return array<int, \PhpParser\Node\Stmt>
|
* @return array<int, \PhpParser\Node\Stmt>
|
||||||
@ -283,11 +287,11 @@ class StatementsProvider
|
|||||||
public static function parseStatements($file_contents, $file_path = null)
|
public static function parseStatements($file_contents, $file_path = null)
|
||||||
{
|
{
|
||||||
if (!self::$parser) {
|
if (!self::$parser) {
|
||||||
$lexer = new PhpParser\Lexer([
|
$attributes = [
|
||||||
'usedAttributes' => [
|
'comments', 'startLine', 'startFilePos', 'endFilePos',
|
||||||
'comments', 'startLine', 'startFilePos', 'endFilePos',
|
];
|
||||||
],
|
|
||||||
]);
|
$lexer = new PhpParser\Lexer\Emulative([ 'usedAttributes' => $attributes ]);
|
||||||
|
|
||||||
self::$parser = (new PhpParser\ParserFactory())->create(PhpParser\ParserFactory::PREFER_PHP7, $lexer);
|
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);
|
self::$node_traverser->traverse($stmts);
|
||||||
|
|
||||||
return $stmts;
|
return $stmts;
|
||||||
|
@ -3,6 +3,7 @@ namespace Psalm\Storage;
|
|||||||
|
|
||||||
use Psalm\CodeLocation;
|
use Psalm\CodeLocation;
|
||||||
use Psalm\Type;
|
use Psalm\Type;
|
||||||
|
use Psalm\Checker\ClassLikeChecker;
|
||||||
|
|
||||||
class FunctionLikeStorage
|
class FunctionLikeStorage
|
||||||
{
|
{
|
||||||
@ -140,4 +141,36 @@ class FunctionLikeStorage
|
|||||||
* @var string|null
|
* @var string|null
|
||||||
*/
|
*/
|
||||||
public $return_type_description;
|
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\CodeLocation;
|
||||||
use Psalm\Type;
|
use Psalm\Type;
|
||||||
|
use Psalm\Checker\ClassLikeChecker;
|
||||||
|
|
||||||
class PropertyStorage
|
class PropertyStorage
|
||||||
{
|
{
|
||||||
|
@ -43,6 +43,7 @@ $valid_long_options = [
|
|||||||
'use-ini-defaults',
|
'use-ini-defaults',
|
||||||
'version',
|
'version',
|
||||||
'diff-methods',
|
'diff-methods',
|
||||||
|
'server',
|
||||||
];
|
];
|
||||||
|
|
||||||
$args = array_slice($argv, 1);
|
$args = array_slice($argv, 1);
|
||||||
@ -190,6 +191,9 @@ Options:
|
|||||||
--disable-extension=[extension]
|
--disable-extension=[extension]
|
||||||
Used to disable certain extensions while Psalm is running.
|
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;
|
HELP;
|
||||||
|
|
||||||
exit;
|
exit;
|
||||||
@ -426,7 +430,7 @@ $project_checker = new ProjectChecker(
|
|||||||
!isset($options['show-snippet']) || $options['show-snippet'] !== "false"
|
!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);
|
$start_time = microtime(true);
|
||||||
|
|
||||||
@ -459,7 +463,9 @@ foreach ($plugins as $plugin_path) {
|
|||||||
Config::getInstance()->addPluginPath($current_dir . DIRECTORY_SEPARATOR . $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);
|
$project_checker->check($current_dir, $is_diff);
|
||||||
} elseif ($paths_to_check) {
|
} elseif ($paths_to_check) {
|
||||||
$project_checker->checkPaths($paths_to_check);
|
$project_checker->checkPaths($paths_to_check);
|
||||||
|
Loading…
Reference in New Issue
Block a user