1
0
mirror of https://github.com/danog/psalm.git synced 2025-01-10 15:09:04 +01:00
psalm/src/Psalm/Internal/Cli/LanguageServer.php

327 lines
10 KiB
PHP
Raw Normal View History

<?php
namespace Psalm\Internal\Cli;
use Psalm\Config;
use Psalm\Internal\Analyzer\ProjectAnalyzer;
use Psalm\Internal\CliUtils;
use Psalm\Internal\Composer;
use Psalm\Internal\ErrorHandler;
2021-12-03 20:11:20 +01:00
use Psalm\Internal\Fork\PsalmRestarter;
use Psalm\Internal\IncludeCollector;
2021-12-03 20:11:20 +01:00
use Psalm\Internal\Provider\ClassLikeStorageCacheProvider;
use Psalm\Internal\Provider\FileProvider;
use Psalm\Internal\Provider\FileReferenceCacheProvider;
use Psalm\Internal\Provider\FileStorageCacheProvider;
use Psalm\Internal\Provider\ParserCacheProvider;
use Psalm\Internal\Provider\ProjectCacheProvider;
use Psalm\Internal\Provider\Providers;
use Psalm\Report;
use function array_key_exists;
use function array_map;
use function array_search;
use function array_slice;
use function chdir;
use function error_log;
use function fwrite;
use function gc_disable;
use function getcwd;
use function getopt;
use function implode;
use function in_array;
use function ini_set;
use function is_array;
use function is_string;
use function preg_replace;
use function realpath;
use function setlocale;
2021-09-26 23:41:26 +02:00
use function strpos;
use function strtolower;
use function substr;
use const DIRECTORY_SEPARATOR;
use const LC_CTYPE;
use const PHP_EOL;
use const STDERR;
// phpcs:disable PSR1.Files.SideEffects
require_once __DIR__ . '/../ErrorHandler.php';
require_once __DIR__ . '/../CliUtils.php';
require_once __DIR__ . '/../Composer.php';
require_once __DIR__ . '/../IncludeCollector.php';
2022-01-03 07:55:32 +01:00
/**
* @internal
*/
final class LanguageServer
{
/** @param array<int,string> $argv */
public static function run(array $argv): void
{
gc_disable();
ErrorHandler::install();
$valid_short_options = [
'h',
'v',
'c:',
'r:',
];
$valid_long_options = [
'clear-cache',
'config:',
'find-dead-code',
'help',
'root:',
'use-ini-defaults',
'version',
'tcp:',
'tcp-server',
'disable-on-change::',
2021-11-10 18:04:47 +01:00
'enable-autocomplete::',
'use-extended-diagnostic-codes',
'verbose'
];
$args = array_slice($argv, 1);
$psalm_proxy = array_search('--language-server', $args, true);
if ($psalm_proxy !== false) {
unset($args[$psalm_proxy]);
}
array_map(
2021-10-04 00:03:06 +02:00
function (string $arg) use ($valid_long_options): void {
2021-09-26 22:51:44 +02:00
if (strpos($arg, '--') === 0 && $arg !== '--') {
$arg_name = preg_replace('/=.*$/', '', substr($arg, 2));
if (!in_array($arg_name, $valid_long_options, true)
&& !in_array($arg_name . ':', $valid_long_options, true)
&& !in_array($arg_name . '::', $valid_long_options, true)
) {
fwrite(
STDERR,
'Unrecognised argument "--' . $arg_name . '"' . PHP_EOL
. 'Type --help to see a list of supported arguments' . PHP_EOL
);
error_log('Bad argument');
exit(1);
}
}
},
$args
);
// get options from command line
$options = getopt(implode('', $valid_short_options), $valid_long_options);
if (!array_key_exists('use-ini-defaults', $options)) {
ini_set('display_errors', '1');
ini_set('display_startup_errors', '1');
ini_set('memory_limit', (string) (8 * 1_024 * 1_024 * 1_024));
}
if (array_key_exists('help', $options)) {
$options['h'] = false;
}
if (array_key_exists('version', $options)) {
$options['v'] = false;
}
if (isset($options['config'])) {
$options['c'] = $options['config'];
}
if (isset($options['c']) && is_array($options['c'])) {
fwrite(STDERR, 'Too many config files provided' . PHP_EOL);
exit(1);
}
if (array_key_exists('h', $options)) {
echo <<<HELP
Usage:
psalm-language-server [options]
Options:
-h, --help
Display this help message
-v, --version
Display the Psalm version
-c, --config=psalm.xml
Path to a psalm.xml configuration file. Run psalm --init to create one.
-r, --root
If running Psalm globally you'll need to specify a project root. Defaults to cwd
--find-dead-code
Look for dead code
--clear-cache
Clears all cache files that the language server uses for this specific project
--use-ini-defaults
Use PHP-provided ini defaults for memory and error display
--tcp=url
Use TCP mode (by default Psalm uses STDIO)
--tcp-server
Use TCP in server mode (default is client)
--disable-on-change[=line-number-threshold]
If added, the language server will not respond to onChange events.
You can also specify a line count over which Psalm will not run on-change events.
--enable-autocomplete[=BOOL]
Enables or disables autocomplete on methods and properties. Default is true.
--use-extended-diagnostic-codes
Enables sending help uri links with the code in diagnostic messages.
--verbose
Will send log messages to the client with information.
HELP;
exit;
}
if (getcwd() === false) {
fwrite(STDERR, 'Cannot get current working directory' . PHP_EOL);
exit(1);
}
if (isset($options['root'])) {
$options['r'] = $options['root'];
}
$current_dir = (string)getcwd() . DIRECTORY_SEPARATOR;
if (isset($options['r']) && is_string($options['r'])) {
$root_path = realpath($options['r']);
if (!$root_path) {
fwrite(
STDERR,
'Could not locate root directory ' . $current_dir . DIRECTORY_SEPARATOR . $options['r'] . PHP_EOL
);
exit(1);
}
$current_dir = $root_path . DIRECTORY_SEPARATOR;
}
$vendor_dir = CliUtils::getVendorDir($current_dir);
$include_collector = new IncludeCollector();
$first_autoloader = $include_collector->runAndCollect(
// we ignore the FQN because of a hack in scoper.inc that needs full path
// phpcs:ignore SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly.ReferenceViaFullyQualifiedName
function () use ($current_dir, $options, $vendor_dir): ?\Composer\Autoload\ClassLoader {
return CliUtils::requireAutoloaders($current_dir, isset($options['r']), $vendor_dir);
}
);
if (array_key_exists('v', $options)) {
echo 'Psalm ' . PSALM_VERSION . PHP_EOL;
exit;
}
2021-12-03 20:11:20 +01:00
$ini_handler = new PsalmRestarter('PSALM');
$ini_handler->disableExtension('grpc');
// If Xdebug is enabled, restart without it
$ini_handler->check();
setlocale(LC_CTYPE, 'C');
$path_to_config = CliUtils::getPathToConfig($options);
if (isset($options['tcp'])) {
if (!is_string($options['tcp'])) {
fwrite(STDERR, 'tcp url should be a string' . PHP_EOL);
exit(1);
}
}
$find_unused_code = isset($options['find-dead-code']) ? 'auto' : null;
$config = CliUtils::initializeConfig(
$path_to_config,
$current_dir,
2021-12-03 20:11:20 +01:00
Report::TYPE_CONSOLE,
$first_autoloader
);
$config->setIncludeCollector($include_collector);
if ($config->resolve_from_config_file) {
$current_dir = $config->base_dir;
chdir($current_dir);
}
$config->setServerMode();
if (isset($options['clear-cache'])) {
$cache_directory = $config->getCacheDirectory();
if ($cache_directory !== null) {
Config::removeCacheDirectory($cache_directory);
}
echo 'Cache directory deleted' . PHP_EOL;
exit;
}
2021-12-03 20:11:20 +01:00
$providers = new Providers(
new FileProvider,
new ParserCacheProvider($config),
new FileStorageCacheProvider($config),
new ClassLikeStorageCacheProvider($config),
new FileReferenceCacheProvider($config),
new ProjectCacheProvider(Composer::getLockFilePath($current_dir))
);
$project_analyzer = new ProjectAnalyzer(
$config,
$providers
);
if ($config->find_unused_variables) {
$project_analyzer->getCodebase()->reportUnusedVariables();
}
if ($config->find_unused_code) {
$find_unused_code = 'auto';
}
if (isset($options['disable-on-change'])) {
$project_analyzer->onchange_line_limit = (int) $options['disable-on-change'];
}
$project_analyzer->provide_completion = !isset($options['enable-autocomplete'])
|| !is_string($options['enable-autocomplete'])
|| strtolower($options['enable-autocomplete']) !== 'false';
if ($find_unused_code) {
$project_analyzer->getCodebase()->reportUnusedCode($find_unused_code);
}
if (isset($options['use-extended-diagnostic-codes'])) {
$project_analyzer->language_server_use_extended_diagnostic_codes = true;
}
if (isset($options['verbose'])) {
$project_analyzer->language_server_verbose = true;
}
2021-09-26 21:49:00 +02:00
$project_analyzer->server($options['tcp'] ?? null, isset($options['tcp-server']));
}
}