From 427ff3a018e1d57feb30de9ce6e4cb4da69e4b69 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Mon, 31 Jan 2022 22:15:29 +0000 Subject: [PATCH 01/78] Upgrade LSP to support additional features --- composer.json | 6 +- .../Internal/Analyzer/ProjectAnalyzer.php | 35 ++- src/Psalm/Internal/Cli/LanguageServer.php | 46 +++- .../LanguageServer/Client/TextDocument.php | 43 +-- .../LanguageServer/Client/Workspace.php | 64 +++++ .../LanguageServer/ClientConfiguration.php | 80 ++++++ .../Internal/LanguageServer/ClientHandler.php | 1 - .../LanguageServer/LanguageClient.php | 138 ++++++++-- .../LanguageServer/LanguageServer.php | 248 ++++++++++++------ src/Psalm/Internal/LanguageServer/Message.php | 1 + .../Internal/LanguageServer/Progress.php | 33 +++ .../LanguageServer/Server/TextDocument.php | 168 +++++++----- .../LanguageServer/Server/Workspace.php | 11 + src/Psalm/Internal/Provider/FileProvider.php | 4 +- 14 files changed, 644 insertions(+), 234 deletions(-) create mode 100644 src/Psalm/Internal/LanguageServer/Client/Workspace.php create mode 100644 src/Psalm/Internal/LanguageServer/ClientConfiguration.php create mode 100644 src/Psalm/Internal/LanguageServer/Progress.php diff --git a/composer.json b/composer.json index ef2f427692b..8fae461bf89 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ "composer/xdebug-handler": "^1.1 || ^2.0 || ^3.0", "dnoegel/php-xdg-base-dir": "^0.1.1", "felixfbecker/advanced-json-rpc": "^3.0.3", - "felixfbecker/language-server-protocol": "^1.5", + "felixfbecker/language-server-protocol": "@dev", "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", "nikic/php-parser": "^4.13", "openlss/lib-array2xml": "^1.0", @@ -95,6 +95,10 @@ { "type": "path", "url": "examples/plugins/composer-based/echo-checker" + }, + { + "type": "path", + "url": "/usr/src/php-language-server-protocol" } ], "minimum-stability": "dev", diff --git a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php index 4e99005e19e..1bf7f671bcf 100644 --- a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php @@ -13,6 +13,7 @@ use Psalm\FileManipulation; use Psalm\Internal\Codebase\TaintFlowGraph; use Psalm\Internal\FileManipulation\FileManipulationBuffer; +use Psalm\Internal\LanguageServer\ClientConfiguration; use Psalm\Internal\LanguageServer\LanguageServer; use Psalm\Internal\LanguageServer\ProtocolStreamReader; use Psalm\Internal\LanguageServer\ProtocolStreamWriter; @@ -263,13 +264,6 @@ class ProjectAnalyzer */ public $language_server_use_extended_diagnostic_codes = false; - /** - * If this is true then the language server will send log messages to the client with additional information. - * - * @var bool - */ - public $language_server_verbose = false; - /** * @param array $generated_report_options */ @@ -432,7 +426,8 @@ private function visitAutoloadFiles(): void ); } - public function server(?string $address = '127.0.0.1:12345', bool $socket_server_mode = false): void + //public function server(?string $address = '127.0.0.1:12345', bool $socket_server_mode = false): void + public function server(ClientConfiguration $clientConfiguration): void { $this->visitAutoloadFiles(); $this->codebase->diff_methods = true; @@ -464,9 +459,9 @@ public function server(?string $address = '127.0.0.1:12345', bool $socket_server @cli_set_process_title('Psalm ' . PSALM_VERSION . ' - PHP Language Server'); - if (!$socket_server_mode && $address) { + if (!$clientConfiguration->TCPServerMode && $clientConfiguration->TCPServerAddress) { // Connect to a TCP server - $socket = stream_socket_client('tcp://' . $address, $errno, $errstr); + $socket = stream_socket_client('tcp://' . $clientConfiguration->TCPServerAddress, $errno, $errstr); if ($socket === false) { fwrite(STDERR, "Could not connect to language client. Error $errno\n$errstr"); exit(1); @@ -475,17 +470,18 @@ public function server(?string $address = '127.0.0.1:12345', bool $socket_server new LanguageServer( new ProtocolStreamReader($socket), new ProtocolStreamWriter($socket), - $this + $this, + $clientConfiguration ); Loop::run(); - } elseif ($socket_server_mode && $address) { + } elseif ($clientConfiguration->TCPServerMode && $clientConfiguration->TCPServerAddress) { // Run a TCP Server - $tcpServer = stream_socket_server('tcp://' . $address, $errno, $errstr); + $tcpServer = stream_socket_server('tcp://' . $clientConfiguration->TCPServerAddress, $errno, $errstr); if ($tcpServer === false) { - fwrite(STDERR, "Could not listen on $address. Error $errno\n$errstr"); + fwrite(STDERR, "Could not listen on {$clientConfiguration->TCPServerAddress}. Error $errno\n$errstr"); exit(1); } - fwrite(STDOUT, "Server listening on $address\n"); + fwrite(STDOUT, "Server listening on {$clientConfiguration->TCPServerAddress}\n"); $fork_available = true; if (!extension_loaded('pcntl')) { @@ -527,7 +523,8 @@ function (): void { new LanguageServer( $reader, new ProtocolStreamWriter($socket), - $this + $this, + $clientConfiguration ); // Just for safety exit(0); @@ -538,7 +535,8 @@ function (): void { new LanguageServer( new ProtocolStreamReader($socket), new ProtocolStreamWriter($socket), - $this + $this, + $clientConfiguration ); Loop::run(); } @@ -549,7 +547,8 @@ function (): void { new LanguageServer( new ProtocolStreamReader(STDIN), new ProtocolStreamWriter(STDOUT), - $this + $this, + $clientConfiguration ); Loop::run(); } diff --git a/src/Psalm/Internal/Cli/LanguageServer.php b/src/Psalm/Internal/Cli/LanguageServer.php index e8689ee968c..009b2951be6 100644 --- a/src/Psalm/Internal/Cli/LanguageServer.php +++ b/src/Psalm/Internal/Cli/LanguageServer.php @@ -2,6 +2,7 @@ namespace Psalm\Internal\Cli; +use LanguageServerProtocol\MessageType; use Psalm\Config; use Psalm\Internal\Analyzer\ProjectAnalyzer; use Psalm\Internal\CliUtils; @@ -9,6 +10,7 @@ use Psalm\Internal\ErrorHandler; use Psalm\Internal\Fork\PsalmRestarter; use Psalm\Internal\IncludeCollector; +use Psalm\Internal\LanguageServer\ClientConfiguration; use Psalm\Internal\Provider\ClassLikeStorageCacheProvider; use Psalm\Internal\Provider\FileProvider; use Psalm\Internal\Provider\FileReferenceCacheProvider; @@ -51,12 +53,15 @@ require_once __DIR__ . '/../CliUtils.php'; require_once __DIR__ . '/../Composer.php'; require_once __DIR__ . '/../IncludeCollector.php'; +require_once __DIR__ . '/../LanguageServer/ClientConfiguration.php'; final class LanguageServer { /** @param array $argv */ public static function run(array $argv): void { + $clientConfiguration = new ClientConfiguration(); + gc_disable(); ErrorHandler::install(); $valid_short_options = [ @@ -67,6 +72,7 @@ public static function run(array $argv): void ]; $valid_long_options = [ + 'no-cache', 'clear-cache', 'config:', 'find-dead-code', @@ -162,6 +168,8 @@ function (string $arg) use ($valid_long_options): void { --clear-cache Clears all cache files that the language server uses for this specific project + --no-cache + --use-ini-defaults Use PHP-provided ini defaults for memory and error display @@ -276,14 +284,21 @@ function () use ($current_dir, $options, $vendor_dir): ?\Composer\Autoload\Class exit; } - $providers = new Providers( - new FileProvider, - new ParserCacheProvider($config), - new FileStorageCacheProvider($config), - new ClassLikeStorageCacheProvider($config), - new FileReferenceCacheProvider($config), - new ProjectCacheProvider(Composer::getLockFilePath($current_dir)) - ); + if (isset($options['no-cache']) || isset($options['i'])) { + $providers = new Providers( + new FileProvider + ); + } else { + $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, @@ -302,7 +317,7 @@ function () use ($current_dir, $options, $vendor_dir): ?\Composer\Autoload\Class $project_analyzer->onchange_line_limit = (int) $options['disable-on-change']; } - $project_analyzer->provide_completion = !isset($options['enable-autocomplete']) + $clientConfiguration->provideCompletion = !isset($options['enable-autocomplete']) || !is_string($options['enable-autocomplete']) || strtolower($options['enable-autocomplete']) !== 'false'; @@ -311,13 +326,20 @@ function () use ($current_dir, $options, $vendor_dir): ?\Composer\Autoload\Class } if (isset($options['use-extended-diagnostic-codes'])) { - $project_analyzer->language_server_use_extended_diagnostic_codes = true; + $clientConfiguration->VSCodeExtendedDiagnosticCodes = true; } if (isset($options['verbose'])) { - $project_analyzer->language_server_verbose = true; + $clientConfiguration->logLevel = $options['verbose'] ? MessageType::LOG : MessageType::INFO; } - $project_analyzer->server($options['tcp'] ?? null, isset($options['tcp-server'])); + $clientConfiguration->TCPServerAddress = $options['tcp'] ?? null; + $clientConfiguration->TCPServerMode = isset($options['tcp-server']); + + //Setup Project Analyzer + $project_analyzer->provide_completion = $clientConfiguration->provideCompletion; + + + $project_analyzer->server($clientConfiguration); } } diff --git a/src/Psalm/Internal/LanguageServer/Client/TextDocument.php b/src/Psalm/Internal/LanguageServer/Client/TextDocument.php index 26cd6462fb9..c17196afa66 100644 --- a/src/Psalm/Internal/LanguageServer/Client/TextDocument.php +++ b/src/Psalm/Internal/LanguageServer/Client/TextDocument.php @@ -11,6 +11,7 @@ use LanguageServerProtocol\TextDocumentIdentifier; use LanguageServerProtocol\TextDocumentItem; use Psalm\Internal\LanguageServer\ClientHandler; +use Psalm\Internal\LanguageServer\LanguageServer; use function Amp\call; @@ -29,10 +30,16 @@ class TextDocument */ private $mapper; - public function __construct(ClientHandler $handler, JsonMapper $mapper) + /** + * @var LanguageServer + */ + private $server; + + public function __construct(ClientHandler $handler, JsonMapper $mapper, LanguageServer $server) { $this->handler = $handler; $this->mapper = $mapper; + $this->server = $server; } /** @@ -40,40 +47,14 @@ public function __construct(ClientHandler $handler, JsonMapper $mapper) * * @param Diagnostic[] $diagnostics */ - public function publishDiagnostics(string $uri, array $diagnostics): void + public function publishDiagnostics(string $uri, array $diagnostics, ?int $version=null): void { + $this->server->logDebug("textDocument/publishDiagnostics"); + $this->handler->notify('textDocument/publishDiagnostics', [ 'uri' => $uri, 'diagnostics' => $diagnostics, + 'version' => $version, ]); } - - /** - * 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 The document's current content - */ - public function xcontent(TextDocumentIdentifier $textDocument): Promise - { - return call( - /** - * @return Generator, object, TextDocumentItem> - */ - function () use ($textDocument) { - /** @var Promise */ - $promise = $this->handler->request( - 'textDocument/xcontent', - ['textDocument' => $textDocument] - ); - - $result = yield $promise; - - /** @var TextDocumentItem */ - return $this->mapper->map($result, new TextDocumentItem); - } - ); - } } diff --git a/src/Psalm/Internal/LanguageServer/Client/Workspace.php b/src/Psalm/Internal/LanguageServer/Client/Workspace.php new file mode 100644 index 00000000000..1fbe1d3a942 --- /dev/null +++ b/src/Psalm/Internal/LanguageServer/Client/Workspace.php @@ -0,0 +1,64 @@ +handler = $handler; + $this->mapper = $mapper; + $this->server = $server; + } + + /** + * The workspace/configuration request is sent from the server to the client to + * fetch configuration settings from the client. The request can fetch several + * configuration settings in one roundtrip. The order of the returned configuration + * settings correspond to the order of the passed ConfigurationItems (e.g. the first + * item in the response is the result for the first configuration item in the params). + * + * @param string $section The configuration section asked for. + * @param string|null $scopeUri The scope to get the configuration section for. + */ + public function requestConfiguration(string $section, ?string $scopeUri = null): Promise + { + $this->server->logDebug("workspace/configuration"); + + /** @var Promise */ + return $this->handler->request('workspace/configuration', [ + 'items' => [ + [ + 'section' => $section, + 'scopeUri' => $scopeUri, + ] + ] + ]); + } +} \ No newline at end of file diff --git a/src/Psalm/Internal/LanguageServer/ClientConfiguration.php b/src/Psalm/Internal/LanguageServer/ClientConfiguration.php new file mode 100644 index 00000000000..68a32a96f90 --- /dev/null +++ b/src/Psalm/Internal/LanguageServer/ClientConfiguration.php @@ -0,0 +1,80 @@ +hideWarnings = $hideWarnings; + $this->provideCompletion = $provideCompletion; + } +} \ No newline at end of file diff --git a/src/Psalm/Internal/LanguageServer/ClientHandler.php b/src/Psalm/Internal/LanguageServer/ClientHandler.php index 8c70156556e..475797b0c37 100644 --- a/src/Psalm/Internal/LanguageServer/ClientHandler.php +++ b/src/Psalm/Internal/LanguageServer/ClientHandler.php @@ -69,7 +69,6 @@ function () use ($id, $method, $params): Generator { $listener = function (Message $msg) use ($id, $deferred, &$listener): void { - error_log('request handler'); /** * @psalm-suppress UndefinedPropertyFetch * @psalm-suppress MixedArgument diff --git a/src/Psalm/Internal/LanguageServer/LanguageClient.php b/src/Psalm/Internal/LanguageServer/LanguageClient.php index 9c2ef144e11..6a7b4b330a2 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageClient.php +++ b/src/Psalm/Internal/LanguageServer/LanguageClient.php @@ -5,7 +5,10 @@ namespace Psalm\Internal\LanguageServer; use JsonMapper; +use LanguageServerProtocol\LogMessage; +use LanguageServerProtocol\LogTrace; use Psalm\Internal\LanguageServer\Client\TextDocument as ClientTextDocument; +use Psalm\Internal\LanguageServer\Client\Workspace as ClientWorkspace; /** * @internal @@ -19,6 +22,13 @@ class LanguageClient */ public $textDocument; + /** + * Handles workspace/* methods + * + * @var ClientWorkspace + */ + public $workspace; + /** * The client handler * @@ -26,39 +36,123 @@ class LanguageClient */ private $handler; - public function __construct(ProtocolReader $reader, ProtocolWriter $writer) - { + /** + * The Language Server + * + * @var LanguageServer + */ + private $server; + + /** + * The Client Configuration + * + * @var ClientConfiguration + */ + public $clientConfiguration; + + /** + * The JsonMapper + * + * @var JsonMapper + */ + private $mapper; + + public function __construct( + ProtocolReader $reader, + ProtocolWriter $writer, + LanguageServer $server, + ClientConfiguration $clientConfiguration + ) { $this->handler = new ClientHandler($reader, $writer); - $mapper = new JsonMapper; + $this->mapper = new JsonMapper; + $this->server = $server; - $this->textDocument = new ClientTextDocument($this->handler, $mapper); + $this->textDocument = new ClientTextDocument($this->handler, $this->mapper, $this->server); + $this->workspace = new ClientWorkspace($this->handler, $this->mapper, $this->server); + $this->clientConfiguration = $clientConfiguration; } /** - * Send a log message to the client. - * - * @param string $message The message to send to the client. - * @psalm-param 1|2|3|4 $type - * @param int $type The log type: - * - 1 = Error - * - 2 = Warning - * - 3 = Info - * - 4 = Log + * Request Configuration from Client and save it */ - public function logMessage(string $message, int $type = 4, string $method = 'window/logMessage'): void + public function refreshConfiguration(): void { - // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_logMessage + $capabilities = $this->server->clientCapabilities; + if ($capabilities && $capabilities->workspace && $capabilities->workspace->configuration) { + $this->workspace->requestConfiguration('psalm')->onResolve(function ($error, $value): void { + if ($error) { + $this->server->logError('There was an error getting configuration'); + } else { + $this->mapper->map($value[0], $this->clientConfiguration); + $this->configurationRefreshed(); + } + }); + } + } - if ($type < 1 || $type > 4) { - $type = 4; + /** + * A notification to log the trace of the server’s execution. + * The amount and content of these notifications depends on the current trace configuration. + * + * @param LogTrace $logTrace + * @return void + */ + public function logTrace(LogTrace $logTrace): void { + //If trace is 'off', the server should not send any logTrace notification. + if(is_null($this->server->trace) || $this->server->trace === 'off') { + return; } + //If trace is 'messages', the server should not add the 'verbose' field in the LogTraceParams. + if($this->server->trace === 'messages') { + $logTrace->verbose = null; + } + + $this->handler->notify( + '$/logTrace', + $logTrace + ); + } + + /** + * Send a log message to the client. + * + * @param LogMessage $message + */ + public function logMessage(LogMessage $logMessage): void + { $this->handler->notify( - $method, - [ - 'type' => $type, - 'message' => $message - ] + 'window/logMessage', + $logMessage ); } + + /** + * The telemetry notification is sent from the + * server to the client to ask the client to log + * a telemetry event. + * + * The protocol doesn’t specify the payload since no + * interpretation of the data happens in the protocol. + * Most clients even don’t handle the event directly + * but forward them to the extensions owing the corresponding + * server issuing the event. + * + * @param LogMessage $logMessage + * @return void + */ + public function event(LogMessage $logMessage) { + $this->handler->notify( + 'telemetry/event', + $logMessage + ); + } + + private function configurationRefreshed() { + //do things when the config is refreshed + + if(!is_null($this->clientConfiguration->provideCompletion)) { + //$this->server->project_analyzer->provide_completion = $this->clientConfiguration->provideCompletion; + } + } } diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index b78e41d3a30..337b5a07252 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -16,10 +16,14 @@ use Generator; use InvalidArgumentException; use LanguageServerProtocol\ClientCapabilities; +use LanguageServerProtocol\ClientInfo; use LanguageServerProtocol\CompletionOptions; use LanguageServerProtocol\Diagnostic; use LanguageServerProtocol\DiagnosticSeverity; +use LanguageServerProtocol\ExecuteCommandOptions; use LanguageServerProtocol\InitializeResult; +use LanguageServerProtocol\LogMessage; +use LanguageServerProtocol\MessageType; use LanguageServerProtocol\Position; use LanguageServerProtocol\Range; use LanguageServerProtocol\SaveOptions; @@ -27,6 +31,7 @@ use LanguageServerProtocol\SignatureHelpOptions; use LanguageServerProtocol\TextDocumentSyncKind; use LanguageServerProtocol\TextDocumentSyncOptions; +use LanguageServerProtocol\WorkspaceFolder; use Psalm\Config; use Psalm\Internal\Analyzer\IssueData; use Psalm\Internal\Analyzer\ProjectAnalyzer; @@ -88,6 +93,16 @@ class LanguageServer extends Dispatcher */ public $client; + /** + * @var ClientCapabilities + */ + public $clientCapabilities; + + /** + * @var string|null + */ + public $trace; + /** * @var ProjectAnalyzer */ @@ -111,7 +126,8 @@ class LanguageServer extends Dispatcher public function __construct( ProtocolReader $reader, ProtocolWriter $writer, - ProjectAnalyzer $project_analyzer + ProjectAnalyzer $project_analyzer, + ClientConfiguration $clientConfiguration ) { parent::__construct($this, '/'); $this->project_analyzer = $project_analyzer; @@ -142,11 +158,6 @@ function (Message $msg): Generator { return; } - /** @psalm-suppress UndefinedPropertyFetch */ - if ($msg->body->method === 'textDocument/signatureHelp') { - $this->doAnalysis(); - } - $result = null; $error = null; try { @@ -190,13 +201,16 @@ function (Message $msg): Generator { $this->protocolReader->on( 'readMessageGroup', function (): void { - $this->doAnalysis(); + //$this->verboseLog('Received message group'); + //$this->doAnalysis(); } ); - $this->client = new LanguageClient($reader, $writer); + $this->client = new LanguageClient($reader, $writer, $this, $clientConfiguration); + + $this->project_analyzer->progress = new Progress($this); - $this->verboseLog("Language server has started."); + $this->logInfo("Language server has started."); } /** @@ -207,18 +221,37 @@ function (): void { * @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. + * @param ClientInfo|null $clientInfo Information about the client + * @param string|null $locale The locale the client is currently showing the user interface + * in. This must not necessarily be the locale of the operating + * system. + * @param string|null $rootPath The rootPath of the workspace. Is null if no folder is open. + * @param mixed $initializationOptions + * @param string|null $trace The initial trace setting. If omitted trace is disabled ('off'). + * @param array|null $workspaceFolders The workspace folders configured in the client when + * the server starts. This property is only available if the client supports workspace folders. + * It can be `null` if the client supports workspace folders but none are + * configured. * @psalm-return Promise * @psalm-suppress PossiblyUnusedMethod */ public function initialize( ClientCapabilities $capabilities, + ?int $processId = null, + ?ClientInfo $clientInfo = null, + ?string $locale = null, ?string $rootPath = null, - ?int $processId = null + ?string $rootUri = null, + $initializationOptions = null, + ?string $trace = null, + //?array $workspaceFolders = null //error in json-dispatcher ): Promise { + $this->clientCapabilities = $capabilities; + $this->trace = $trace; return call( /** @return Generator */ - function () { - $this->verboseLog("Initializing..."); + function () use($capabilities) { + $this->logInfo("Initializing..."); $this->clientStatus('initializing'); // Eventually, this might block on something. Leave it as a generator. @@ -227,15 +260,15 @@ function () { yield true; } - $this->verboseLog("Initializing: Getting code base..."); + $this->logInfo("Initializing: Getting code base..."); $this->clientStatus('initializing', 'getting code base'); $codebase = $this->project_analyzer->getCodebase(); - $this->verboseLog("Initializing: Scanning files..."); + $this->logInfo("Initializing: Scanning files..."); $this->clientStatus('initializing', 'scanning files'); $codebase->scanFiles($this->project_analyzer->threads); - $this->verboseLog("Initializing: Registering stub files..."); + $this->logInfo("Initializing: Registering stub files..."); $this->clientStatus('initializing', 'registering stub files'); $codebase->config->visitStubFiles($codebase); @@ -257,6 +290,8 @@ function () { $serverCapabilities = new ServerCapabilities(); + $serverCapabilities->executeCommandProvider = new ExecuteCommandOptions(['test']); + $textDocumentSyncOptions = new TextDocumentSyncOptions(); $textDocumentSyncOptions->openClose = true; @@ -284,8 +319,16 @@ function () { // Support "Hover" $serverCapabilities->hoverProvider = true; // Support "Completion" - $serverCapabilities->codeActionProvider = true; - // Support "Code Actions" + + // Support "Code Actions" if we support data + if( + $this->clientCapabilities && + $this->clientCapabilities->textDocument && + $this->clientCapabilities->textDocument->publishDiagnostics && + $this->clientCapabilities->textDocument->publishDiagnostics->dataSupport + ) { + $serverCapabilities->codeActionProvider = true; + } if ($this->project_analyzer->provide_completion) { $serverCapabilities->completionProvider = new CompletionOptions(); @@ -300,7 +343,7 @@ function () { $serverCapabilities->xdefinitionProvider = false; $serverCapabilities->dependenciesProvider = false; - $this->verboseLog("Initializing: Complete."); + $this->logInfo("Initializing: Complete."); $this->clientStatus('initialized'); return new InitializeResult($serverCapabilities); } @@ -313,66 +356,83 @@ function () { */ public function initialized(): void { + $this->logInfo("Initialized."); + try { + $this->client->refreshConfiguration(); + } catch(Throwable $e) { + error_log($e->getMessage()); + } + $this->logInfo("Initialized. After"); + $this->clientStatus('running'); } - public function queueTemporaryFileAnalysis(string $file_path, string $uri): void + public function queueTemporaryFileAnalysis(string $file_path, string $uri, ?int $version=null): void { - $this->onchange_paths_to_analyze[$file_path] = $uri; + $this->logDebug("queueTemporaryFileAnalysis", ['version' => $version, 'file_path' => $file_path, 'uri' => $uri]); + $this->onchange_paths_to_analyze[$version][$file_path] = $uri; + $this->debounceVersionedAnalysis($version); } - public function queueFileAnalysis(string $file_path, string $uri): void + public function queueFileAnalysis(string $file_path, string $uri, ?int $version=null): void { - $this->onsave_paths_to_analyze[$file_path] = $uri; + //$this->logDebug("queueFileAnalysis", ['version' => $version, 'file_path' => $file_path, 'uri' => $uri]); + $this->onsave_paths_to_analyze[$version][$file_path] = $uri; + $this->debounceVersionedAnalysis($version); } - public function doAnalysis(): void - { - $this->clientStatus('analyzing'); + public function debounceVersionedAnalysis(?int $version=null) { + //$this->logDebug("debounceVersionedAnalysis", ['version' => $version]); + $onchange_paths_to_analyze = $this->onchange_paths_to_analyze[$version] ?? []; + $onsave_paths_to_analyze = $this->onsave_paths_to_analyze[$version] ?? []; + $all_files_to_analyze = $onchange_paths_to_analyze + $onsave_paths_to_analyze; + + $this->doVersionedAnalysis('', [$all_files_to_analyze, $version]); + } + + public function doVersionedAnalysis(string $watcherId, $data = []):void { + //$this->logDebug("doVersionedAnalysis"); + + [$all_files_to_analyze, $version] = $data; try { - $codebase = $this->project_analyzer->getCodebase(); - $all_files_to_analyze = $this->onchange_paths_to_analyze + $this->onsave_paths_to_analyze; - if (!$all_files_to_analyze) { + if(empty($all_files_to_analyze)) { + $this->logWarning("No versioned analysis to do."); return; } - if ($this->onsave_paths_to_analyze) { - $codebase->reloadFiles($this->project_analyzer, array_keys($this->onsave_paths_to_analyze)); - } - if ($this->onchange_paths_to_analyze) { - $codebase->reloadFiles($this->project_analyzer, array_keys($this->onchange_paths_to_analyze)); - } + /** @var array */ + $files = $all_files_to_analyze; + + $codebase = $this->project_analyzer->getCodebase(); + $codebase->reloadFiles( + $this->project_analyzer, + array_keys($files) + ); - $all_file_paths_to_analyze = array_keys($all_files_to_analyze); $codebase->analyzer->addFilesToAnalyze( - array_combine($all_file_paths_to_analyze, $all_file_paths_to_analyze) + array_combine(array_keys($files), array_keys($files)) ); $codebase->analyzer->analyzeFiles($this->project_analyzer, 1, false); - $this->emitIssues($all_files_to_analyze); - - $this->onchange_paths_to_analyze = []; - $this->onsave_paths_to_analyze = []; + $this->emitVersionedIssues($files,$version); + } catch(\Throwable $e) { + $this->logError($e->getMessage(). $e->getLine()); } finally { - // we are done, so set the status back to running - $this->clientStatus('running'); + unset($this->onchange_paths_to_analyze[$version]); + unset($this->onsave_paths_to_analyze[$version]); } + } - /** - * @param array $uris - * - */ - public function emitIssues(array $uris): void - { + public function emitVersionedIssues(array $files, ?int $version = null): void { $data = IssueBuffer::clear(); $this->current_issues = $data; - foreach ($uris as $file_path => $uri) { + foreach ($files as $file_path => $uri) { $diagnostics = array_map( function (IssueData $issue_data): Diagnostic { //$check_name = $issue->check_name; @@ -402,18 +462,21 @@ function (IssueData $issue_data): Diagnostic { $range, null, $diagnostic_severity, - 'Psalm' + 'psalm' ); + $diagnostic->data = [ + 'type' => $issue_data->type, + 'snippet' => $issue_data->snippet, + 'line_from' => $issue_data->line_from, + 'line_to' => $issue_data->line_to + ]; + //$code = 'PS' . \str_pad((string) $issue_data->shortcode, 3, "0", \STR_PAD_LEFT); $code = $issue_data->link; - if ($this->project_analyzer->language_server_use_extended_diagnostic_codes) { - // Added in VSCode 1.43.0 and will be part of the LSP 3.16.0 standard. - // Since this new functionality is not backwards compatible, we use a - // configuration option so the end user must opt in to it using the cli argument. - // https://github.com/microsoft/vscode/blob/1.43.0/src/vs/vscode.d.ts#L4688-L4699 - + if ($this->client->clientConfiguration->VSCodeExtendedDiagnosticCodes) { + //This is a Vscode violation of the spec /** @psalm-suppress InvalidPropertyAssignmentValue */ $diagnostic->code = [ "value" => $code, @@ -427,10 +490,21 @@ function (IssueData $issue_data): Diagnostic { return $diagnostic; }, - $data[$file_path] ?? [] + array_filter( + $data[$file_path] ?? [], + function (IssueData $issue_data) { + if ($issue_data->severity === Config::REPORT_INFO && + $this->client->clientConfiguration->hideWarnings + ) { + return false; + } + + return true; + } + ) ); - $this->client->textDocument->publishDiagnostics($uri, $diagnostics); + $this->client->textDocument->publishDiagnostics($uri, $diagnostics, $version); } } @@ -443,7 +517,7 @@ function (IssueData $issue_data): Diagnostic { public function shutdown(): Promise { $this->clientStatus('closing'); - $this->verboseLog("Shutting down..."); + $this->logInfo("Shutting down..."); $codebase = $this->project_analyzer->getCodebase(); $scanned_files = $codebase->scanner->getScannedFiles(); $codebase->file_reference_provider->updateReferenceCache( @@ -467,27 +541,48 @@ public function exit(): void /** * Send log message to the client * - * @param string $message The log message to send to the client. * @psalm-param 1|2|3|4 $type * @param int $type The log type: * - 1 = Error * - 2 = Warning * - 3 = Info * - 4 = Log + * @see MessageType + * @param string $message The log message to send to the client. + * @param mixed[] $context The log context */ - public function verboseLog(string $message, int $type = 4): void + public function log(int $type, string $message, array $context = []): void { - if ($this->project_analyzer->language_server_verbose) { - try { - $this->client->logMessage( - '[Psalm ' .PSALM_VERSION. ' - PHP Language Server] ' . $message, - $type - ); - } catch (Throwable $err) { - // do nothing - } + $full = $type === MessageType::LOG ? $message : \sprintf('[Psalm ' .PSALM_VERSION. ' - PHP Language Server] %s', $message); + if(!empty($context)) { + $full .= "\n" . \json_encode($context, JSON_PRETTY_PRINT); + } + try { + $this->client->logMessage( + new LogMessage( + $type, + $full, + ) + ); + } catch (Throwable $err) { + // do nothing } - new Success(null); + } + + public function logError(string $message, array $context = []) { + $this->log(MessageType::ERROR, $message, $context); + } + + public function logWarning(string $message, array $context = []) { + $this->log(MessageType::WARNING, $message, $context); + } + + public function logInfo(string $message, array $context = []) { + $this->log(MessageType::INFO, $message, $context); + } + + public function logDebug(string $message, array $context = []) { + $this->log(MessageType::LOG, $message, $context); } /** @@ -501,16 +596,15 @@ public function verboseLog(string $message, int $type = 4): void private function clientStatus(string $status, ?string $additional_info = null): void { try { - // here we send a notification to the client using the telemetry notification method - $this->client->logMessage( - $status . (!empty($additional_info) ? ': ' . $additional_info : ''), - 3, - 'telemetry/event' + $this->client->event( + new LogMessage( + MessageType::INFO, + $status . (!empty($additional_info) ? ': ' . $additional_info : '') + ) ); } catch (Throwable $err) { // do nothing } - new Success(null); } /** diff --git a/src/Psalm/Internal/LanguageServer/Message.php b/src/Psalm/Internal/LanguageServer/Message.php index be3362530de..8a5af1ab8cf 100644 --- a/src/Psalm/Internal/LanguageServer/Message.php +++ b/src/Psalm/Internal/LanguageServer/Message.php @@ -60,6 +60,7 @@ public function __construct(?MessageBody $body = null, array $headers = []) public function __toString(): string { + $body = (string)$this->body; $contentLength = strlen($body); $this->headers['Content-Length'] = (string) $contentLength; diff --git a/src/Psalm/Internal/LanguageServer/Progress.php b/src/Psalm/Internal/LanguageServer/Progress.php new file mode 100644 index 00000000000..7297be6cd22 --- /dev/null +++ b/src/Psalm/Internal/LanguageServer/Progress.php @@ -0,0 +1,33 @@ +server = $server; + } + + public function debug(string $message): void + { + $this->server->logDebug($message); + } + + public function write(string $message): void + { + $this->server->logInfo($message); + } +} \ No newline at end of file diff --git a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php index d495090e242..7ba6839455a 100644 --- a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php +++ b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php @@ -7,6 +7,10 @@ use Amp\Promise; use Amp\Success; use InvalidArgumentException; +use LanguageServerProtocol\CodeAction; +use LanguageServerProtocol\CodeActionContext; +use LanguageServerProtocol\CodeActionKind; +use LanguageServerProtocol\Command; use LanguageServerProtocol\CompletionList; use LanguageServerProtocol\Hover; use LanguageServerProtocol\Location; @@ -77,6 +81,11 @@ public function __construct( */ public function didOpen(TextDocumentItem $textDocument): void { + $this->server->logDebug( + 'textDocument/didOpen', + ['version' => $textDocument->version, 'uri' => $textDocument->uri] + ); + $file_path = LanguageServer::uriToPath($textDocument->uri); if (!$this->codebase->config->isInProjectDirs($file_path)) { @@ -85,16 +94,23 @@ public function didOpen(TextDocumentItem $textDocument): void $this->codebase->file_provider->openFile($file_path); - $this->server->queueFileAnalysis($file_path, $textDocument->uri); + $this->server->queueFileAnalysis($file_path, $textDocument->uri, $textDocument->version); } /** * The document save notification is sent from the client to the server when the document was saved in the client * * @param TextDocumentItem $textDocument the document that was opened + * @param string|null $text Optional the content when saved. Depends on the includeText value + * when the save notification was requested. */ - public function didSave(TextDocumentItem $textDocument): void + public function didSave(TextDocumentItem $textDocument, ?string $text = null): void { + $this->server->logDebug( + 'textDocument/didSave', + ['version' => $textDocument->version, 'uri' => $textDocument->uri] + ); + $file_path = LanguageServer::uriToPath($textDocument->uri); if (!$this->codebase->config->isInProjectDirs($file_path)) { @@ -103,7 +119,7 @@ public function didSave(TextDocumentItem $textDocument): void // reopen file $this->codebase->removeTemporaryFileChanges($file_path); - $this->codebase->file_provider->setOpenContents($file_path, $textDocument->text); + $this->codebase->file_provider->setOpenContents($file_path, $text); $this->server->queueFileAnalysis($file_path, $textDocument->uri); } @@ -116,6 +132,11 @@ public function didSave(TextDocumentItem $textDocument): void */ public function didChange(VersionedTextDocumentIdentifier $textDocument, array $contentChanges): void { + $this->server->logDebug( + 'textDocument/didChange', + ['version' => $textDocument->version, 'uri' => $textDocument->uri] + ); + $file_path = LanguageServer::uriToPath($textDocument->uri); if (!$this->codebase->config->isInProjectDirs($file_path)) { @@ -138,8 +159,12 @@ public function didChange(VersionedTextDocumentIdentifier $textDocument, array $ } } + $this->server->logDebug( + $new_content + ); + $this->codebase->addTemporaryFileChanges($file_path, $new_content); - $this->server->queueTemporaryFileAnalysis($file_path, $textDocument->uri); + $this->server->queueTemporaryFileAnalysis($file_path, $textDocument->uri, $textDocument->version); } /** @@ -155,6 +180,11 @@ public function didChange(VersionedTextDocumentIdentifier $textDocument, array $ */ public function didClose(TextDocumentIdentifier $textDocument): void { + $this->server->logDebug( + 'textDocument/didClose', + ['version' => $textDocument->version, 'uri' => $textDocument->uri] + ); + $file_path = LanguageServer::uriToPath($textDocument->uri); $this->codebase->file_provider->closeFile($file_path); @@ -171,6 +201,10 @@ public function didClose(TextDocumentIdentifier $textDocument): void */ public function definition(TextDocumentIdentifier $textDocument, Position $position): Promise { + $this->server->logDebug( + 'definition' + ); + $file_path = LanguageServer::uriToPath($textDocument->uri); try { @@ -215,6 +249,10 @@ public function definition(TextDocumentIdentifier $textDocument, Position $posit */ public function hover(TextDocumentIdentifier $textDocument, Position $position): Promise { + $this->server->logDebug( + 'hover' + ); + $file_path = LanguageServer::uriToPath($textDocument->uri); try { @@ -266,7 +304,9 @@ public function hover(TextDocumentIdentifier $textDocument, Position $position): */ public function completion(TextDocumentIdentifier $textDocument, Position $position): Promise { - $this->server->doAnalysis(); + $this->server->logDebug( + 'completion' + ); $file_path = LanguageServer::uriToPath($textDocument->uri); if (!$this->codebase->config->isInProjectDirs($file_path)) { @@ -322,6 +362,10 @@ public function completion(TextDocumentIdentifier $textDocument, Position $posit */ public function signatureHelp(TextDocumentIdentifier $textDocument, Position $position): Promise { + $this->server->logDebug( + 'signatureHelp' + ); + $file_path = LanguageServer::uriToPath($textDocument->uri); try { @@ -343,9 +387,17 @@ public function signatureHelp(TextDocumentIdentifier $textDocument, Position $po return new Success(new SignatureHelp()); } - return new Success(new SignatureHelp([ - $signature_information, - ], 0, $argument_location[1])); + $this->server->logDebug( + 'signatureHelpSuccess!' + ); + + return new Success( + new SignatureHelp( + [$signature_information], + 0, + $argument_location[1] + ) + ); } /** @@ -354,80 +406,56 @@ public function signatureHelp(TextDocumentIdentifier $textDocument, Position $po * either fix problems or to beautify/refactor code. * */ - public function codeAction(TextDocumentIdentifier $textDocument, Range $range): Promise + public function codeAction(TextDocumentIdentifier $textDocument, Range $range, CodeActionContext $context): Promise { - $file_path = LanguageServer::uriToPath($textDocument->uri); - if (!$this->codebase->file_provider->isOpen($file_path)) { - return new Success(null); - } - - $all_file_paths_to_analyze = [$file_path]; - $this->codebase->analyzer->addFilesToAnalyze( - array_combine($all_file_paths_to_analyze, $all_file_paths_to_analyze) + $this->server->logDebug( + 'codeAction' ); - try { - $this->codebase->analyzer->analyzeFiles($this->project_analyzer, 1, false); - } catch (UnexpectedValueException | InvalidArgumentException $e) { - error_log('codeAction errored on file ' . $file_path. ', Reason: '.$e->getMessage()); - return new Success(null); - } - - $issues = $this->server->getCurrentIssues(); - - if (empty($issues[$file_path])) { - return new Success(null); - } - - $file_contents = $this->codebase->getFileContents($file_path); - - $offsetStart = $range->start->toOffset($file_contents); - $offsetEnd = $range->end->toOffset($file_contents); - $fixers = []; - foreach ($issues[$file_path] as $issue) { - if ($offsetStart === $issue->from && $offsetEnd === $issue->to) { - $snippetRange = new Range( - new Position($issue->line_from-1), - new Position($issue->line_to) - ); + foreach($context->diagnostics as $diagnostic) { + if($diagnostic->source !== 'psalm') { + continue; + } + $snippetRange = new Range( + new Position($diagnostic->data->line_from-1), + new Position($diagnostic->data->line_to) + ); + + $indentation = ''; + if (preg_match('/^(\s*)/', $diagnostic->data->snippet, $matches)) { + $indentation = $matches[1] ?? ''; + } - $indentation = ''; - if (preg_match('/^(\s*)/', $issue->snippet, $matches)) { - $indentation = $matches[1] ?? ''; - } - - - /** - * Suppress Psalm because ther are bugs in how - * LanguageServer's signature of WorkspaceEdit is declared: - * - * See: - * https://github.com/felixfbecker/php-language-server-protocol - * See: - * https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#workspaceEdit - * - * @psalm-suppress InvalidArgument - */ - $edit = new WorkspaceEdit([ + //Suppress Ability + $fixers["suppress.{$diagnostic->data->type}"] = new CodeAction( + "Suppress {$diagnostic->data->type} for this line", + CodeActionKind::QUICK_FIX, + null, + null, + null, + new WorkspaceEdit([ $textDocument->uri => [ new TextEdit( $snippetRange, "{$indentation}/**\n". - "{$indentation} * @psalm-suppress {$issue->type}\n". + "{$indentation} * @psalm-suppress {$diagnostic->data->type}\n". "{$indentation} */\n". - "{$issue->snippet}\n" + "{$diagnostic->data->snippet}\n" ) ] - ]); - - //Suppress Ability - $fixers["suppress.{$issue->type}"] = [ - 'title' => "Suppress {$issue->type} for this line", - 'kind' => 'quickfix', - 'edit' => $edit - ]; - } + ]) + ); + + $fixers["fixAll.{$diagnostic->data->type}"] = new CodeAction( + "FixAll {$diagnostic->data->type} for this file", + CodeActionKind::QUICK_FIX, + null, + null, + null, + null, + new Command("Test","test",['one']) + ); } if (empty($fixers)) { diff --git a/src/Psalm/Internal/LanguageServer/Server/Workspace.php b/src/Psalm/Internal/LanguageServer/Server/Workspace.php index c1bc2bd7073..5d291239db8 100644 --- a/src/Psalm/Internal/LanguageServer/Server/Workspace.php +++ b/src/Psalm/Internal/LanguageServer/Server/Workspace.php @@ -75,4 +75,15 @@ public function didChangeWatchedFiles(array $changes): void } } } + + /** + * A notification sent from the client to the server to signal the change of configuration settings. + * + * @param mixed $settings + * @psalm-suppress PossiblyUnusedMethod + */ + public function didChangeConfiguration($settings): void + { + $this->server->client->refreshConfiguration(); + } } diff --git a/src/Psalm/Internal/Provider/FileProvider.php b/src/Psalm/Internal/Provider/FileProvider.php index 6d85e1458e9..de6d58733da 100644 --- a/src/Psalm/Internal/Provider/FileProvider.php +++ b/src/Psalm/Internal/Provider/FileProvider.php @@ -67,11 +67,11 @@ public function setContents(string $file_path, string $file_contents): void file_put_contents($file_path, $file_contents); } - public function setOpenContents(string $file_path, string $file_contents): void + public function setOpenContents(string $file_path, ?string $file_contents=null): void { $file_path_lc = strtolower($file_path); if (isset($this->open_files[$file_path_lc])) { - $this->open_files[$file_path_lc] = $file_contents; + $this->open_files[$file_path_lc] = $file_contents ?? $this->getContents($file_path, true); } } From cfdf5c4a68c1e6bfe57aaa52e223748ddcf30309 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Mon, 31 Jan 2022 22:38:22 +0000 Subject: [PATCH 02/78] additional debugging --- .../LanguageServer/Server/TextDocument.php | 18 +++++------------- .../LanguageServer/Server/Workspace.php | 6 ++++++ 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php index 7ba6839455a..78779356360 100644 --- a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php +++ b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php @@ -159,10 +159,6 @@ public function didChange(VersionedTextDocumentIdentifier $textDocument, array $ } } - $this->server->logDebug( - $new_content - ); - $this->codebase->addTemporaryFileChanges($file_path, $new_content); $this->server->queueTemporaryFileAnalysis($file_path, $textDocument->uri, $textDocument->version); } @@ -202,7 +198,7 @@ public function didClose(TextDocumentIdentifier $textDocument): void public function definition(TextDocumentIdentifier $textDocument, Position $position): Promise { $this->server->logDebug( - 'definition' + 'textDocument/definition' ); $file_path = LanguageServer::uriToPath($textDocument->uri); @@ -250,7 +246,7 @@ public function definition(TextDocumentIdentifier $textDocument, Position $posit public function hover(TextDocumentIdentifier $textDocument, Position $position): Promise { $this->server->logDebug( - 'hover' + 'textDocument/hover' ); $file_path = LanguageServer::uriToPath($textDocument->uri); @@ -305,7 +301,7 @@ public function hover(TextDocumentIdentifier $textDocument, Position $position): public function completion(TextDocumentIdentifier $textDocument, Position $position): Promise { $this->server->logDebug( - 'completion' + 'textDocument/completion' ); $file_path = LanguageServer::uriToPath($textDocument->uri); @@ -363,7 +359,7 @@ public function completion(TextDocumentIdentifier $textDocument, Position $posit public function signatureHelp(TextDocumentIdentifier $textDocument, Position $position): Promise { $this->server->logDebug( - 'signatureHelp' + 'textDocument/signatureHelp' ); $file_path = LanguageServer::uriToPath($textDocument->uri); @@ -387,10 +383,6 @@ public function signatureHelp(TextDocumentIdentifier $textDocument, Position $po return new Success(new SignatureHelp()); } - $this->server->logDebug( - 'signatureHelpSuccess!' - ); - return new Success( new SignatureHelp( [$signature_information], @@ -409,7 +401,7 @@ public function signatureHelp(TextDocumentIdentifier $textDocument, Position $po public function codeAction(TextDocumentIdentifier $textDocument, Range $range, CodeActionContext $context): Promise { $this->server->logDebug( - 'codeAction' + 'textDocument/codeAction' ); $fixers = []; diff --git a/src/Psalm/Internal/LanguageServer/Server/Workspace.php b/src/Psalm/Internal/LanguageServer/Server/Workspace.php index 5d291239db8..629f195b0a8 100644 --- a/src/Psalm/Internal/LanguageServer/Server/Workspace.php +++ b/src/Psalm/Internal/LanguageServer/Server/Workspace.php @@ -53,6 +53,9 @@ public function __construct( */ public function didChangeWatchedFiles(array $changes): void { + $this->server->logDebug( + 'workspace/didChangeWatchedFiles' + ); foreach ($changes as $change) { $file_path = LanguageServer::uriToPath($change->uri); @@ -84,6 +87,9 @@ public function didChangeWatchedFiles(array $changes): void */ public function didChangeConfiguration($settings): void { + $this->server->logDebug( + 'workspace/didChangeConfiguration' + ); $this->server->client->refreshConfiguration(); } } From 55c65573ce089e8f4d95a1903ed6ba7522ff87a6 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Tue, 1 Feb 2022 06:21:07 +0000 Subject: [PATCH 03/78] add command to fix all and analyize single file --- src/Psalm/Internal/Cli/LanguageServer.php | 6 +-- .../LanguageServer/ClientConfiguration.php | 10 ----- .../LanguageServer/LanguageServer.php | 32 ++++++++------- .../LanguageServer/Server/TextDocument.php | 9 ++++- .../LanguageServer/Server/Workspace.php | 39 +++++++++++++++++++ 5 files changed, 65 insertions(+), 31 deletions(-) diff --git a/src/Psalm/Internal/Cli/LanguageServer.php b/src/Psalm/Internal/Cli/LanguageServer.php index 009b2951be6..3ad0ff6e214 100644 --- a/src/Psalm/Internal/Cli/LanguageServer.php +++ b/src/Psalm/Internal/Cli/LanguageServer.php @@ -186,7 +186,7 @@ function (string $arg) use ($valid_long_options): void { --enable-autocomplete[=BOOL] Enables or disables autocomplete on methods and properties. Default is true. - --use-extended-diagnostic-codes + --use-extended-diagnostic-codes (DEPRECATED) Enables sending help uri links with the code in diagnostic messages. --verbose @@ -325,10 +325,6 @@ function () use ($current_dir, $options, $vendor_dir): ?\Composer\Autoload\Class $project_analyzer->getCodebase()->reportUnusedCode($find_unused_code); } - if (isset($options['use-extended-diagnostic-codes'])) { - $clientConfiguration->VSCodeExtendedDiagnosticCodes = true; - } - if (isset($options['verbose'])) { $clientConfiguration->logLevel = $options['verbose'] ? MessageType::LOG : MessageType::INFO; } diff --git a/src/Psalm/Internal/LanguageServer/ClientConfiguration.php b/src/Psalm/Internal/LanguageServer/ClientConfiguration.php index 68a32a96f90..b7bb5d46c87 100644 --- a/src/Psalm/Internal/LanguageServer/ClientConfiguration.php +++ b/src/Psalm/Internal/LanguageServer/ClientConfiguration.php @@ -60,16 +60,6 @@ class ClientConfiguration */ public $logLevel; - /** - * Added in VSCode 1.43.0 and will be part of the LSP 3.16.0 standard. - * Since this new functionality is not backwards compatible, we use a - * configuration option so the end user must opt in to it using the cli argument. - * - * @var bool|null - * @see https://github.com/microsoft/vscode/blob/1.43.0/src/vs/vscode.d.ts#L4688-L4699 - */ - public $VSCodeExtendedDiagnosticCodes; - public function __construct( bool $hideWarnings = null, bool $provideCompletion = null diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 337b5a07252..95856f0e95b 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -17,11 +17,13 @@ use InvalidArgumentException; use LanguageServerProtocol\ClientCapabilities; use LanguageServerProtocol\ClientInfo; +use LanguageServerProtocol\CodeDescription; use LanguageServerProtocol\CompletionOptions; use LanguageServerProtocol\Diagnostic; use LanguageServerProtocol\DiagnosticSeverity; use LanguageServerProtocol\ExecuteCommandOptions; use LanguageServerProtocol\InitializeResult; +use LanguageServerProtocol\InitializeResultServerInfo; use LanguageServerProtocol\LogMessage; use LanguageServerProtocol\MessageType; use LanguageServerProtocol\Position; @@ -345,7 +347,10 @@ function () use($capabilities) { $this->logInfo("Initializing: Complete."); $this->clientStatus('initialized'); - return new InitializeResult($serverCapabilities); + + $initializeResultServerInfo = new InitializeResultServerInfo('Psalm Language Server', PSALM_VERSION); + + return new InitializeResult($serverCapabilities, $initializeResultServerInfo); } ); } @@ -429,6 +434,10 @@ public function doVersionedAnalysis(string $watcherId, $data = []):void { } public function emitVersionedIssues(array $files, ?int $version = null): void { + $this->logDebug("Perform Analysis",[ + 'files' => array_keys($files), + 'version' => $version + ]); $data = IssueBuffer::clear(); $this->current_issues = $data; @@ -472,20 +481,13 @@ function (IssueData $issue_data): Diagnostic { 'line_to' => $issue_data->line_to ]; - //$code = 'PS' . \str_pad((string) $issue_data->shortcode, 3, "0", \STR_PAD_LEFT); - $code = $issue_data->link; - - if ($this->client->clientConfiguration->VSCodeExtendedDiagnosticCodes) { - //This is a Vscode violation of the spec - /** @psalm-suppress InvalidPropertyAssignmentValue */ - $diagnostic->code = [ - "value" => $code, - "target" => $issue_data->link, - ]; - } else { - // the Diagnostic constructor only takes `int` for the code, but the property can be - // `int` or `string`, so we set the property directly because we want to use a `string` - $diagnostic->code = $code; + $diagnostic->code = $issue_data->shortcode; + + if ($this->clientCapabilities->textDocument && + $this->clientCapabilities->textDocument->publishDiagnostics && + $this->clientCapabilities->textDocument->publishDiagnostics->codeDescriptionSupport + ) { + $diagnostic->codeDescription = new CodeDescription($issue_data->link); } return $diagnostic; diff --git a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php index 78779356360..ea4412b96d3 100644 --- a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php +++ b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php @@ -446,7 +446,14 @@ public function codeAction(TextDocumentIdentifier $textDocument, Range $range, C null, null, null, - new Command("Test","test",['one']) + new Command( + "Fix All", + "psalm.fixall", + [ + 'uri' => $textDocument->uri, + 'type' => $diagnostic->data->type + ] + ) ); } diff --git a/src/Psalm/Internal/LanguageServer/Server/Workspace.php b/src/Psalm/Internal/LanguageServer/Server/Workspace.php index 629f195b0a8..84d75a8055f 100644 --- a/src/Psalm/Internal/LanguageServer/Server/Workspace.php +++ b/src/Psalm/Internal/LanguageServer/Server/Workspace.php @@ -4,6 +4,7 @@ namespace Psalm\Internal\LanguageServer\Server; +use Amp\Success; use LanguageServerProtocol\FileChangeType; use LanguageServerProtocol\FileEvent; use Psalm\Codebase; @@ -92,4 +93,42 @@ public function didChangeConfiguration($settings): void ); $this->server->client->refreshConfiguration(); } + + /** + * The workspace/executeCommand request is sent from the client to the server to + * trigger command execution on the server. + * + * @param string $command + * @param mixed $arguments + * @psalm-suppress PossiblyUnusedMethod + */ + public function executeCommand($command, $arguments) { + $this->server->logDebug( + 'workspace/executeCommand', + [ + 'command' => $command, + 'arguments' => $arguments, + ] + ); + + switch($command) { + case 'psalm.analyze.uri': + $file = LanguageServer::uriToPath($arguments->uri); + $codebase = $this->project_analyzer->getCodebase(); + $codebase->reloadFiles( + $this->project_analyzer, + [$file] + ); + + $codebase->analyzer->addFilesToAnalyze( + [$file => $file] + ); + $codebase->analyzer->analyzeFiles($this->project_analyzer, 1, false); + + $this->server->emitVersionedIssues([$file]); + break; + } + + return new Success(null); + } } From 870e1ba95976f4356604cb19df30fc9455b4e074 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Tue, 1 Feb 2022 12:10:22 -0800 Subject: [PATCH 04/78] additional work to caching engine --- composer.json | 6 ++-- src/Psalm/Codebase.php | 10 +++++-- .../LanguageServer/LanguageServer.php | 14 ++++------ .../LanguageServer/Server/TextDocument.php | 2 ++ .../LanguageServer/Server/Workspace.php | 28 +++++++++++++++++-- src/Psalm/Internal/Provider/FileProvider.php | 5 ++++ 6 files changed, 48 insertions(+), 17 deletions(-) diff --git a/composer.json b/composer.json index 8fae461bf89..925a0037cfc 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ "composer/xdebug-handler": "^1.1 || ^2.0 || ^3.0", "dnoegel/php-xdg-base-dir": "^0.1.1", "felixfbecker/advanced-json-rpc": "^3.0.3", - "felixfbecker/language-server-protocol": "@dev", + "felixfbecker/language-server-protocol": "dev-feature/spec-3.16", "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", "nikic/php-parser": "^4.13", "openlss/lib-array2xml": "^1.0", @@ -97,8 +97,8 @@ "url": "examples/plugins/composer-based/echo-checker" }, { - "type": "path", - "url": "/usr/src/php-language-server-protocol" + "type": "vcs", + "url": "https://github.com/tm1000/php-language-server-protocol" } ], "minimum-stability": "dev", diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index cb72efa8b76..a9fbb4279a6 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -409,15 +409,19 @@ private function loadAnalyzer(): void * @param array $candidate_files * */ - public function reloadFiles(ProjectAnalyzer $project_analyzer, array $candidate_files): void + public function reloadFiles(ProjectAnalyzer $project_analyzer, array $candidate_files, bool $force = false): void { $this->loadAnalyzer(); - $this->file_reference_provider->loadReferenceCache(false); + if($force) { + FileReferenceProvider::clearCache(); + } + + $this->file_reference_provider->loadReferenceCache($force); FunctionLikeAnalyzer::clearCache(); - if (!$this->statements_provider->parser_cache_provider) { + if ($force || !$this->statements_provider->parser_cache_provider) { $diff_files = $candidate_files; } else { $diff_files = []; diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 95856f0e95b..27e267e432e 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -190,6 +190,7 @@ function (Message $msg): Generator { */ if (Request::isRequest($msg->body)) { if ($error !== null) { + $this->logError($error->message); $responseBody = new ErrorResponse($msg->body->id, $error); } else { $responseBody = new SuccessResponse($msg->body->id, $result); @@ -212,7 +213,7 @@ function (): void { $this->project_analyzer->progress = new Progress($this); - $this->logInfo("Language server has started."); + $this->logInfo("Psalm Language Server ".PSALM_VERSION." has started."); } /** @@ -361,14 +362,11 @@ function () use($capabilities) { */ public function initialized(): void { - $this->logInfo("Initialized."); try { $this->client->refreshConfiguration(); } catch(Throwable $e) { error_log($e->getMessage()); } - $this->logInfo("Initialized. After"); - $this->clientStatus('running'); } @@ -555,19 +553,19 @@ public function exit(): void */ public function log(int $type, string $message, array $context = []): void { - $full = $type === MessageType::LOG ? $message : \sprintf('[Psalm ' .PSALM_VERSION. ' - PHP Language Server] %s', $message); if(!empty($context)) { - $full .= "\n" . \json_encode($context, JSON_PRETTY_PRINT); + $message .= "\n" . \json_encode($context, JSON_PRETTY_PRINT); } try { $this->client->logMessage( new LogMessage( $type, - $full, + $message, ) ); } catch (Throwable $err) { - // do nothing + // do nothing as we could potentially go into a loop here is not careful + //TODO: Investigate if we can use error_log instead } } diff --git a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php index ea4412b96d3..a755918f470 100644 --- a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php +++ b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php @@ -439,6 +439,7 @@ public function codeAction(TextDocumentIdentifier $textDocument, Range $range, C ]) ); + /* $fixers["fixAll.{$diagnostic->data->type}"] = new CodeAction( "FixAll {$diagnostic->data->type} for this file", CodeActionKind::QUICK_FIX, @@ -455,6 +456,7 @@ public function codeAction(TextDocumentIdentifier $textDocument, Range $range, C ] ) ); + */ } if (empty($fixers)) { diff --git a/src/Psalm/Internal/LanguageServer/Server/Workspace.php b/src/Psalm/Internal/LanguageServer/Server/Workspace.php index 84d75a8055f..bf16a2ab538 100644 --- a/src/Psalm/Internal/LanguageServer/Server/Workspace.php +++ b/src/Psalm/Internal/LanguageServer/Server/Workspace.php @@ -9,7 +9,9 @@ use LanguageServerProtocol\FileEvent; use Psalm\Codebase; use Psalm\Internal\Analyzer\ProjectAnalyzer; +use Psalm\Internal\Composer; use Psalm\Internal\LanguageServer\LanguageServer; +use Psalm\Internal\Provider\FileReferenceProvider; /** * Provides method handlers for all workspace/* methods @@ -57,9 +59,28 @@ public function didChangeWatchedFiles(array $changes): void $this->server->logDebug( 'workspace/didChangeWatchedFiles' ); + + $realFiles = array_map(function(FileEvent $change) { + return LanguageServer::uriToPath($change->uri); + }, $changes); + + $composerLockFile = realpath(Composer::getLockFilePath($this->codebase->config->base_dir)); + if(in_array($composerLockFile, $realFiles)) { + $this->server->logInfo('Composer.lock file changed. Reloading codebase'); + FileReferenceProvider::clearCache(); + foreach($this->codebase->file_provider->getOpenFiles() as $file) { + $this->server->queueFileAnalysis($file, $this->server->pathToUri($file)); + } + return; + } + foreach ($changes as $change) { $file_path = LanguageServer::uriToPath($change->uri); + if($composerLockFile === $file_path) { + continue; + } + if ($change->type === FileChangeType::DELETED) { $this->codebase->invalidateInformationForFile($file_path); continue; @@ -73,7 +94,7 @@ public function didChangeWatchedFiles(array $changes): void continue; } - //If the file is currently open then dont analyse it because its tracked by the client + //If the file is currently open then dont analize it because its tracked in didChange if (!$this->codebase->file_provider->isOpen($file_path)) { $this->server->queueFileAnalysis($file_path, $change->uri); } @@ -117,7 +138,8 @@ public function executeCommand($command, $arguments) { $codebase = $this->project_analyzer->getCodebase(); $codebase->reloadFiles( $this->project_analyzer, - [$file] + [$file], + true ); $codebase->analyzer->addFilesToAnalyze( @@ -125,7 +147,7 @@ public function executeCommand($command, $arguments) { ); $codebase->analyzer->analyzeFiles($this->project_analyzer, 1, false); - $this->server->emitVersionedIssues([$file]); + $this->server->emitVersionedIssues([$file => $arguments->uri]); break; } diff --git a/src/Psalm/Internal/Provider/FileProvider.php b/src/Psalm/Internal/Provider/FileProvider.php index de6d58733da..02674c51900 100644 --- a/src/Psalm/Internal/Provider/FileProvider.php +++ b/src/Psalm/Internal/Provider/FileProvider.php @@ -94,6 +94,11 @@ public function removeTemporaryFileChanges(string $file_path): void unset($this->temp_files[strtolower($file_path)]); } + public function getOpenFiles(): array + { + return array_keys($this->open_files); + } + public function openFile(string $file_path): void { $this->open_files[strtolower($file_path)] = $this->getContents($file_path, true); From 1b98fe52efc32e5eaea8ff9a5ba1e599ffa5d996 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Wed, 2 Feb 2022 18:10:10 +0000 Subject: [PATCH 05/78] analyze files even outside of the scope because this is how IDEs work --- src/Psalm/Codebase.php | 175 +++++++++--------- src/Psalm/Internal/Cli/LanguageServer.php | 39 ++-- .../LanguageServer/LanguageServer.php | 10 +- .../LanguageServer/Server/TextDocument.php | 38 ++-- 4 files changed, 127 insertions(+), 135 deletions(-) diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index a9fbb4279a6..fb10580479b 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -1017,126 +1017,121 @@ public function getSymbolInformation(string $file_path, string $symbol): ?array return ['type' => preg_replace('/^[^:]*:/', '', $symbol)]; } - try { - if (strpos($symbol, '::')) { - if (strpos($symbol, '()')) { - $symbol = substr($symbol, 0, -2); - - /** @psalm-suppress ArgumentTypeCoercion */ - $method_id = new MethodIdentifier(...explode('::', $symbol)); - - $declaring_method_id = $this->methods->getDeclaringMethodId($method_id); + if (strpos($symbol, '::')) { + if (strpos($symbol, '()')) { + $symbol = substr($symbol, 0, -2); - if (!$declaring_method_id) { - return null; - } + /** @psalm-suppress ArgumentTypeCoercion */ + $method_id = new MethodIdentifier(...explode('::', $symbol)); - $storage = $this->methods->getStorage($declaring_method_id); + $declaring_method_id = $this->methods->getDeclaringMethodId($method_id); - return [ - 'type' => 'getSignature(true), - 'description' => $storage->description, - ]; + if (!$declaring_method_id) { + return null; } - [, $symbol_name] = explode('::', $symbol); - - if (strpos($symbol, '$') !== false) { - $storage = $this->properties->getStorage($symbol); - - return [ - 'type' => 'getInfo() . ' ' . $symbol_name, - 'description' => $storage->description, - ]; - } + $storage = $this->methods->getStorage($declaring_method_id); - [$fq_classlike_name, $const_name] = explode('::', $symbol); + return [ + 'type' => 'getSignature(true), + 'description' => $storage->description, + ]; + } - $class_constants = $this->classlikes->getConstantsForClass( - $fq_classlike_name, - ReflectionProperty::IS_PRIVATE - ); + [, $symbol_name] = explode('::', $symbol); - if (!isset($class_constants[$const_name])) { - return null; - } + if (strpos($symbol, '$') !== false) { + $storage = $this->properties->getStorage($symbol); return [ - 'type' => ' $class_constants[$const_name]->description, + 'type' => 'getInfo() . ' ' . $symbol_name, + 'description' => $storage->description, ]; } - if (strpos($symbol, '()')) { - $function_id = strtolower(substr($symbol, 0, -2)); - $file_storage = $this->file_storage_provider->get($file_path); + [$fq_classlike_name, $const_name] = explode('::', $symbol); - if (isset($file_storage->functions[$function_id])) { - $function_storage = $file_storage->functions[$function_id]; + $class_constants = $this->classlikes->getConstantsForClass( + $fq_classlike_name, + ReflectionProperty::IS_PRIVATE + ); - return [ - 'type' => 'getSignature(true), - 'description' => $function_storage->description, - ]; - } + if (!isset($class_constants[$const_name])) { + return null; + } - if (!$function_id) { - return null; - } + return [ + 'type' => ' $class_constants[$const_name]->description, + ]; + } + + if (strpos($symbol, '()')) { + $function_id = strtolower(substr($symbol, 0, -2)); + $file_storage = $this->file_storage_provider->get($file_path); + + if (isset($file_storage->functions[$function_id])) { + $function_storage = $file_storage->functions[$function_id]; - $function = $this->functions->getStorage(null, $function_id); return [ - 'type' => 'getSignature(true), - 'description' => $function->description, + 'type' => 'getSignature(true), + 'description' => $function_storage->description, ]; } - if (strpos($symbol, '$') === 0) { - $type = VariableFetchAnalyzer::getGlobalType($symbol); - if (!$type->isMixed()) { - return ['type' => 'classlike_storage_provider->get($symbol); - return [ - 'type' => 'abstract ? 'abstract ' : '') . 'class ' . $storage->name, - 'description' => $storage->description, - ]; - } catch (InvalidArgumentException $e) { + $function = $this->functions->getStorage(null, $function_id); + return [ + 'type' => 'getSignature(true), + 'description' => $function->description, + ]; + } + + if (strpos($symbol, '$') === 0) { + $type = VariableFetchAnalyzer::getGlobalType($symbol); + if (!$type->isMixed()) { + return ['type' => 'classlike_storage_provider->get($symbol); + return [ + 'type' => 'abstract ? 'abstract ' : '') . 'class ' . $storage->name, + 'description' => $storage->description, + ]; + } catch (InvalidArgumentException $e) { + //continue on as normal + } - $namespace_constants = NamespaceAnalyzer::getConstantsForNamespace( - $namespace_name, - ReflectionProperty::IS_PUBLIC - ); - if (isset($namespace_constants[$const_name])) { - $type = $namespace_constants[$const_name]; - return ['type' => 'file_storage_provider->get($file_path); - if (isset($file_storage->constants[$symbol])) { - return ['type' => 'constants[$symbol]]; - } - $constant = ConstFetchAnalyzer::getGlobalConstType($this, $symbol, $symbol); + if (strpos($symbol, '\\')) { + $const_name_parts = explode('\\', $symbol); + $const_name = array_pop($const_name_parts); + $namespace_name = implode('\\', $const_name_parts); - if ($constant) { - return ['type' => ' 'getMessage()); + } else { + $file_storage = $this->file_storage_provider->get($file_path); + if (isset($file_storage->constants[$symbol])) { + return ['type' => 'constants[$symbol]]; + } + $constant = ConstFetchAnalyzer::getGlobalConstType($this, $symbol, $symbol); - return null; + if ($constant) { + return ['type' => 'setServerMode(); - if (isset($options['clear-cache'])) { + if (isset($options['clear-cache']) || isset($options['clear-cache-on-boot'])) { $cache_directory = $config->getCacheDirectory(); if ($cache_directory !== null) { Config::removeCacheDirectory($cache_directory); } - echo 'Cache directory deleted' . PHP_EOL; - exit; - } - - if (isset($options['no-cache']) || isset($options['i'])) { - $providers = new Providers( - new FileProvider - ); - } else { - $providers = new Providers( - new FileProvider, - new ParserCacheProvider($config), - new FileStorageCacheProvider($config), - new ClassLikeStorageCacheProvider($config), - new FileReferenceCacheProvider($config), - new ProjectCacheProvider(Composer::getLockFilePath($current_dir)) - ); + if(!isset($options['clear-cache-on-boot'])) { + echo 'Cache directory deleted' . PHP_EOL; + exit; + } } + //no-cache mode does not work in the LSP + $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, diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 27e267e432e..78f12419564 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -365,7 +365,7 @@ public function initialized(): void try { $this->client->refreshConfiguration(); } catch(Throwable $e) { - error_log($e->getMessage()); + error_log((string) $e); } $this->clientStatus('running'); } @@ -422,8 +422,8 @@ public function doVersionedAnalysis(string $watcherId, $data = []):void { $codebase->analyzer->analyzeFiles($this->project_analyzer, 1, false); $this->emitVersionedIssues($files,$version); - } catch(\Throwable $e) { - $this->logError($e->getMessage(). $e->getLine()); + } catch(Throwable $e) { + error_log((string) $e); } finally { unset($this->onchange_paths_to_analyze[$version]); unset($this->onsave_paths_to_analyze[$version]); @@ -440,6 +440,10 @@ public function emitVersionedIssues(array $files, ?int $version = null): void { $this->current_issues = $data; foreach ($files as $file_path => $uri) { + //Dont report errors in files we are not watching + if (!$this->project_analyzer->getCodebase()->config->isInProjectDirs($file_path)) { + continue; + } $diagnostics = array_map( function (IssueData $issue_data): Diagnostic { //$check_name = $issue->check_name; diff --git a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php index a755918f470..4ac7087147f 100644 --- a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php +++ b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php @@ -88,10 +88,6 @@ public function didOpen(TextDocumentItem $textDocument): void $file_path = LanguageServer::uriToPath($textDocument->uri); - if (!$this->codebase->config->isInProjectDirs($file_path)) { - return; - } - $this->codebase->file_provider->openFile($file_path); $this->server->queueFileAnalysis($file_path, $textDocument->uri, $textDocument->version); @@ -113,10 +109,6 @@ public function didSave(TextDocumentItem $textDocument, ?string $text = null): v $file_path = LanguageServer::uriToPath($textDocument->uri); - if (!$this->codebase->config->isInProjectDirs($file_path)) { - return; - } - // reopen file $this->codebase->removeTemporaryFileChanges($file_path); $this->codebase->file_provider->setOpenContents($file_path, $text); @@ -139,10 +131,6 @@ public function didChange(VersionedTextDocumentIdentifier $textDocument, array $ $file_path = LanguageServer::uriToPath($textDocument->uri); - if (!$this->codebase->config->isInProjectDirs($file_path)) { - return; - } - if ($this->project_analyzer->onchange_line_limit === 0) { return; } @@ -266,7 +254,12 @@ public function hover(TextDocumentIdentifier $textDocument, Position $position): [$reference, $range] = $reference_location; - $symbol_information = $this->codebase->getSymbolInformation($file_path, $reference); + try { + $symbol_information = $this->codebase->getSymbolInformation($file_path, $reference); + } catch(UnexpectedValueException $e) { + error_log((string) $e); + return new Success(null); + } if ($symbol_information === null) { return new Success(null); @@ -305,9 +298,6 @@ public function completion(TextDocumentIdentifier $textDocument, Position $posit ); $file_path = LanguageServer::uriToPath($textDocument->uri); - if (!$this->codebase->config->isInProjectDirs($file_path)) { - return new Success([]); - } try { $completion_data = $this->codebase->getCompletionDataAtPosition($file_path, $position); @@ -315,20 +305,19 @@ public function completion(TextDocumentIdentifier $textDocument, Position $posit $this->codebase->file_provider->openFile($file_path); $this->server->queueFileAnalysis($file_path, $textDocument->uri); - return new Success([]); + return new Success(null); } try { $type_context = $this->codebase->getTypeContextAtPosition($file_path, $position); } catch (UnexpectedValueException $e) { - error_log('completion errored at ' . $position->line . ':' . $position->character. - ', Reason: '.$e->getMessage()); - return new Success([]); + error_log((string) $e); + return new Success(null); } if (!$completion_data && !$type_context) { error_log('completion not found at ' . $position->line . ':' . $position->character); - return new Success([]); + return new Success(null); } if ($completion_data) { @@ -404,6 +393,13 @@ public function codeAction(TextDocumentIdentifier $textDocument, Range $range, C 'textDocument/codeAction' ); + $file_path = LanguageServer::uriToPath($textDocument->uri); + + //Don't report code actions for files we arent watching + if (!$this->codebase->config->isInProjectDirs($file_path)) { + return new Success(null); + } + $fixers = []; foreach($context->diagnostics as $diagnostic) { if($diagnostic->source !== 'psalm') { From 81418e2ce05d23d246a0218c6bd98f38a3c0c4a2 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Wed, 2 Feb 2022 22:17:54 +0000 Subject: [PATCH 06/78] improve file caching logic with versions and refreshes --- src/Psalm/Codebase.php | 8 +- .../LanguageServer/LanguageServer.php | 82 ++++++------------- .../LanguageServer/Server/TextDocument.php | 71 ++++++++++------ src/Psalm/Internal/Provider/FileProvider.php | 33 ++++++-- 4 files changed, 99 insertions(+), 95 deletions(-) diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index fb10580479b..e0682bf9808 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -1366,12 +1366,12 @@ public function getSignatureInformation( $callables = InternalCallMapHandler::getCallablesFromCallMap($function_symbol); if (!$callables || !$callables[0]->params) { - return null; + throw $exception; } $params = $callables[0]->params; } else { - return null; + throw $exception; } } } @@ -1866,9 +1866,9 @@ private static function getPositionFromOffset(int $offset, string $file_contents ); } - public function addTemporaryFileChanges(string $file_path, string $new_content): void + public function addTemporaryFileChanges(string $file_path, string $new_content, ?int $version = null): void { - $this->file_provider->addTemporaryFileChanges($file_path, $new_content); + $this->file_provider->addTemporaryFileChanges($file_path, $new_content, $version ); } public function removeTemporaryFileChanges(string $file_path): void diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 78f12419564..09ad5965f9a 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -110,21 +110,6 @@ class LanguageServer extends Dispatcher */ protected $project_analyzer; - /** - * @var array - */ - protected $onsave_paths_to_analyze = []; - - /** - * @var array - */ - protected $onchange_paths_to_analyze = []; - - /** - * @var array> - */ - protected $current_issues = []; - public function __construct( ProtocolReader $reader, ProtocolWriter $writer, @@ -365,42 +350,40 @@ public function initialized(): void try { $this->client->refreshConfiguration(); } catch(Throwable $e) { - error_log((string) $e); + $this->server->logError((string) $e); } $this->clientStatus('running'); } - public function queueTemporaryFileAnalysis(string $file_path, string $uri, ?int $version=null): void - { - $this->logDebug("queueTemporaryFileAnalysis", ['version' => $version, 'file_path' => $file_path, 'uri' => $uri]); - $this->onchange_paths_to_analyze[$version][$file_path] = $uri; - $this->debounceVersionedAnalysis($version); + public function queueChangeFileAnalysis(string $file_path, string $uri, ?int $version=null) { + $this->doVersionedAnalysis([$file_path => $uri], $version); } - public function queueFileAnalysis(string $file_path, string $uri, ?int $version=null): void - { - //$this->logDebug("queueFileAnalysis", ['version' => $version, 'file_path' => $file_path, 'uri' => $uri]); - $this->onsave_paths_to_analyze[$version][$file_path] = $uri; - $this->debounceVersionedAnalysis($version); + public function queueOpenFileAnalysis(string $file_path, string $uri, ?int $version=null) { + $this->doVersionedAnalysis([$file_path => $uri], $version); } - public function debounceVersionedAnalysis(?int $version=null) { - //$this->logDebug("debounceVersionedAnalysis", ['version' => $version]); - $onchange_paths_to_analyze = $this->onchange_paths_to_analyze[$version] ?? []; - $onsave_paths_to_analyze = $this->onsave_paths_to_analyze[$version] ?? []; - $all_files_to_analyze = $onchange_paths_to_analyze + $onsave_paths_to_analyze; - - $this->doVersionedAnalysis('', [$all_files_to_analyze, $version]); + /** + * Queue Saved File Analysis + * @param string $file_path + * @param string $uri + * @return void + */ + public function queueSaveFileAnalysis(string $file_path, string $uri) { + //Always reanalzye open files because of things changing elsewhere + $opened = array_reduce( + $this->project_analyzer->getCodebase()->file_provider->getOpenFilesPath(), + function (array $opened, string $file_path) { + $opened[$file_path] = $this->pathToUri($file_path); + return $opened; + }, + [$file_path => $this->pathToUri($file_path)]); + + $this->doVersionedAnalysis($opened); } - public function doVersionedAnalysis(string $watcherId, $data = []):void { - //$this->logDebug("doVersionedAnalysis"); - - [$all_files_to_analyze, $version] = $data; - + public function doVersionedAnalysis($all_files_to_analyze, ?int $version=null):void { try { - - if(empty($all_files_to_analyze)) { $this->logWarning("No versioned analysis to do."); return; @@ -423,12 +406,8 @@ public function doVersionedAnalysis(string $watcherId, $data = []):void { $this->emitVersionedIssues($files,$version); } catch(Throwable $e) { - error_log((string) $e); - } finally { - unset($this->onchange_paths_to_analyze[$version]); - unset($this->onsave_paths_to_analyze[$version]); + $this->server->logError((string) $e); } - } public function emitVersionedIssues(array $files, ?int $version = null): void { @@ -436,9 +415,8 @@ public function emitVersionedIssues(array $files, ?int $version = null): void { 'files' => array_keys($files), 'version' => $version ]); - $data = IssueBuffer::clear(); - $this->current_issues = $data; + $data = IssueBuffer::clear(); foreach ($files as $file_path => $uri) { //Dont report errors in files we are not watching if (!$this->project_analyzer->getCodebase()->config->isInProjectDirs($file_path)) { @@ -664,14 +642,4 @@ public static function uriToPath(string $uri): string return $filepath; } - - /** - * Get the value of current_issues - * - * @return array> - */ - public function getCurrentIssues(): array - { - return $this->current_issues; - } } diff --git a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php index 4ac7087147f..d09a6a602f1 100644 --- a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php +++ b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php @@ -88,23 +88,25 @@ public function didOpen(TextDocumentItem $textDocument): void $file_path = LanguageServer::uriToPath($textDocument->uri); + $this->codebase->removeTemporaryFileChanges($file_path); $this->codebase->file_provider->openFile($file_path); + $this->codebase->file_provider->setOpenContents($file_path, $textDocument->text); - $this->server->queueFileAnalysis($file_path, $textDocument->uri, $textDocument->version); + $this->server->queueOpenFileAnalysis($file_path, $textDocument->uri, $textDocument->version); } /** * The document save notification is sent from the client to the server when the document was saved in the client * - * @param TextDocumentItem $textDocument the document that was opened + * @param TextDocumentIdentifier $textDocument the document that was opened * @param string|null $text Optional the content when saved. Depends on the includeText value * when the save notification was requested. */ - public function didSave(TextDocumentItem $textDocument, ?string $text = null): void + public function didSave(TextDocumentIdentifier $textDocument, ?string $text = null): void { $this->server->logDebug( 'textDocument/didSave', - ['version' => $textDocument->version, 'uri' => $textDocument->uri] + ['uri' => (array) $textDocument] ); $file_path = LanguageServer::uriToPath($textDocument->uri); @@ -113,7 +115,7 @@ public function didSave(TextDocumentItem $textDocument, ?string $text = null): v $this->codebase->removeTemporaryFileChanges($file_path); $this->codebase->file_provider->setOpenContents($file_path, $text); - $this->server->queueFileAnalysis($file_path, $textDocument->uri); + $this->server->queueSaveFileAnalysis($file_path, $textDocument->uri); } /** @@ -147,8 +149,8 @@ public function didChange(VersionedTextDocumentIdentifier $textDocument, array $ } } - $this->codebase->addTemporaryFileChanges($file_path, $new_content); - $this->server->queueTemporaryFileAnalysis($file_path, $textDocument->uri, $textDocument->version); + $this->codebase->addTemporaryFileChanges($file_path, $new_content, $textDocument->version); + $this->server->queueChangeFileAnalysis($file_path, $textDocument->uri, $textDocument->version); } /** @@ -191,12 +193,15 @@ public function definition(TextDocumentIdentifier $textDocument, Position $posit $file_path = LanguageServer::uriToPath($textDocument->uri); + //This currently doesnt work right with out of project files + if (!$this->codebase->config->isInProjectDirs($file_path)) { + return new Success(null); + } + try { $reference_location = $this->codebase->getReferenceAtPosition($file_path, $position); } catch (UnanalyzedFileException $e) { - $this->codebase->file_provider->openFile($file_path); - $this->server->queueFileAnalysis($file_path, $textDocument->uri); - + $this->server->logError((string) $e); return new Success(null); } @@ -239,12 +244,15 @@ public function hover(TextDocumentIdentifier $textDocument, Position $position): $file_path = LanguageServer::uriToPath($textDocument->uri); + //This currently doesnt work right with out of project files + if (!$this->codebase->config->isInProjectDirs($file_path)) { + return new Success(null); + } + try { $reference_location = $this->codebase->getReferenceAtPosition($file_path, $position); } catch (UnanalyzedFileException $e) { - $this->codebase->file_provider->openFile($file_path); - $this->server->queueFileAnalysis($file_path, $textDocument->uri); - + $this->server->logError((string) $e); return new Success(null); } @@ -257,7 +265,7 @@ public function hover(TextDocumentIdentifier $textDocument, Position $position): try { $symbol_information = $this->codebase->getSymbolInformation($file_path, $reference); } catch(UnexpectedValueException $e) { - error_log((string) $e); + $this->server->logError((string) $e); return new Success(null); } @@ -299,24 +307,27 @@ public function completion(TextDocumentIdentifier $textDocument, Position $posit $file_path = LanguageServer::uriToPath($textDocument->uri); + //This currently doesnt work right with out of project files + if (!$this->codebase->config->isInProjectDirs($file_path)) { + return new Success(null); + } + try { $completion_data = $this->codebase->getCompletionDataAtPosition($file_path, $position); } catch (UnanalyzedFileException $e) { - $this->codebase->file_provider->openFile($file_path); - $this->server->queueFileAnalysis($file_path, $textDocument->uri); - + $this->server->logError((string) $e); return new Success(null); } try { $type_context = $this->codebase->getTypeContextAtPosition($file_path, $position); } catch (UnexpectedValueException $e) { - error_log((string) $e); + $this->server->logError((string) $e); return new Success(null); } if (!$completion_data && !$type_context) { - error_log('completion not found at ' . $position->line . ':' . $position->character); + $this->server->logError('completion not found at ' . $position->line . ':' . $position->character); return new Success(null); } @@ -353,23 +364,31 @@ public function signatureHelp(TextDocumentIdentifier $textDocument, Position $po $file_path = LanguageServer::uriToPath($textDocument->uri); + //This currently doesnt work right with out of project files + if (!$this->codebase->config->isInProjectDirs($file_path)) { + return new Success(null); + } + try { $argument_location = $this->codebase->getFunctionArgumentAtPosition($file_path, $position); } catch (UnanalyzedFileException $e) { - $this->codebase->file_provider->openFile($file_path); - $this->server->queueFileAnalysis($file_path, $textDocument->uri); - - return new Success(new SignatureHelp()); + $this->server->logError((string) $e); + return new Success(null); } if ($argument_location === null) { - return new Success(new SignatureHelp()); + return new Success(null); } - $signature_information = $this->codebase->getSignatureInformation($argument_location[0], $file_path); + try { + $signature_information = $this->codebase->getSignatureInformation($argument_location[0], $file_path); + } catch(UnexpectedValueException $e) { + $this->server->logError((string) $e); + return new Success(null); + } if (!$signature_information) { - return new Success(new SignatureHelp()); + return new Success(null); } return new Success( diff --git a/src/Psalm/Internal/Provider/FileProvider.php b/src/Psalm/Internal/Provider/FileProvider.php index 02674c51900..ed602ff97e9 100644 --- a/src/Psalm/Internal/Provider/FileProvider.php +++ b/src/Psalm/Internal/Provider/FileProvider.php @@ -31,11 +31,16 @@ class FileProvider */ protected $open_files = []; + /** + * @var array + */ + protected $open_files_paths = []; + public function getContents(string $file_path, bool $go_to_source = false): string { $file_path_lc = strtolower($file_path); if (!$go_to_source && isset($this->temp_files[$file_path_lc])) { - return $this->temp_files[$file_path_lc]; + return $this->temp_files[$file_path_lc]['content']; } if (isset($this->open_files[$file_path_lc])) { @@ -67,11 +72,11 @@ public function setContents(string $file_path, string $file_contents): void file_put_contents($file_path, $file_contents); } - public function setOpenContents(string $file_path, ?string $file_contents=null): void + public function setOpenContents(string $file_path, string $file_contents): void { $file_path_lc = strtolower($file_path); if (isset($this->open_files[$file_path_lc])) { - $this->open_files[$file_path_lc] = $file_contents ?? $this->getContents($file_path, true); + $this->open_files[$file_path_lc] = $file_contents; } } @@ -84,9 +89,20 @@ public function getModifiedTime(string $file_path): int return (int)filemtime($file_path); } - public function addTemporaryFileChanges(string $file_path, string $new_content): void + public function addTemporaryFileChanges(string $file_path, string $new_content, ?int $version = null): void { - $this->temp_files[strtolower($file_path)] = $new_content; + if( + isset($this->temp_files[strtolower($file_path)]) && + !is_null($version) && + !is_null($this->temp_files[strtolower($file_path)]['version']) && + $version < $this->temp_files[strtolower($file_path)]['version'] + ) { + return; + } + $this->temp_files[strtolower($file_path)] = [ + 'version' => $version, + 'content' => $new_content, + ]; } public function removeTemporaryFileChanges(string $file_path): void @@ -94,14 +110,15 @@ public function removeTemporaryFileChanges(string $file_path): void unset($this->temp_files[strtolower($file_path)]); } - public function getOpenFiles(): array + public function getOpenFilesPath(): array { - return array_keys($this->open_files); + return $this->open_files_paths; } public function openFile(string $file_path): void { $this->open_files[strtolower($file_path)] = $this->getContents($file_path, true); + $this->open_files_paths[strtolower($file_path)] = $file_path; } public function isOpen(string $file_path): bool @@ -113,7 +130,7 @@ public function isOpen(string $file_path): bool public function closeFile(string $file_path): void { $file_path_lc = strtolower($file_path); - unset($this->temp_files[$file_path_lc], $this->open_files[$file_path_lc]); + unset($this->temp_files[$file_path_lc], $this->open_files[$file_path_lc], $this->open_files_paths[$file_path_lc]); } public function fileExists(string $file_path): bool From 2093a2818259cb48bdcc7d10cac54f3f1d9f8ba9 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Wed, 2 Feb 2022 22:19:52 +0000 Subject: [PATCH 07/78] log an error outside of requests --- src/Psalm/Internal/LanguageServer/LanguageServer.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 09ad5965f9a..7f962a49efe 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -167,6 +167,9 @@ function (Message $msg): Generator { $e ); } + if ($error !== null) { + $this->logError($error->message); + } // Only send a Response for a Request // Notifications do not send Responses /** @@ -175,7 +178,6 @@ function (Message $msg): Generator { */ if (Request::isRequest($msg->body)) { if ($error !== null) { - $this->logError($error->message); $responseBody = new ErrorResponse($msg->body->id, $error); } else { $responseBody = new SuccessResponse($msg->body->id, $result); From de6d71431b1c218247814893aacf00a0cdb1afd1 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Wed, 2 Feb 2022 22:25:11 +0000 Subject: [PATCH 08/78] after bubble up we now need to not yield nulls --- src/Psalm/Internal/LanguageServer/LanguageServer.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 7f962a49efe..9dabff5cc96 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -150,11 +150,15 @@ function (Message $msg): Generator { try { // Invoke the method handler to get a result /** - * @var Promise + * @var Promise|null */ $dispatched = $this->dispatch($msg->body); - /** @psalm-suppress MixedAssignment */ - $result = yield $dispatched; + if(!is_null($dispatched)) { + /** @psalm-suppress MixedAssignment */ + $result = yield $dispatched; + } else { + $result = null; + } } catch (Error $e) { // If a ResponseError is thrown, send it back in the Response $error = $e; From f4e8e1ff325ac8a02f7176d831913c4b81697a95 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Wed, 2 Feb 2022 22:28:35 +0000 Subject: [PATCH 09/78] check nulls quicker --- src/Psalm/Internal/LanguageServer/LanguageServer.php | 2 +- src/Psalm/Internal/Provider/FileProvider.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 9dabff5cc96..4a354ff0389 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -153,7 +153,7 @@ function (Message $msg): Generator { * @var Promise|null */ $dispatched = $this->dispatch($msg->body); - if(!is_null($dispatched)) { + if($dispatched !== null) { /** @psalm-suppress MixedAssignment */ $result = yield $dispatched; } else { diff --git a/src/Psalm/Internal/Provider/FileProvider.php b/src/Psalm/Internal/Provider/FileProvider.php index ed602ff97e9..d9d4da155dc 100644 --- a/src/Psalm/Internal/Provider/FileProvider.php +++ b/src/Psalm/Internal/Provider/FileProvider.php @@ -93,8 +93,8 @@ public function addTemporaryFileChanges(string $file_path, string $new_content, { if( isset($this->temp_files[strtolower($file_path)]) && - !is_null($version) && - !is_null($this->temp_files[strtolower($file_path)]['version']) && + $version !== null && + $this->temp_files[strtolower($file_path)]['version'] !== null && $version < $this->temp_files[strtolower($file_path)]['version'] ) { return; From 9957f67353cd94018add1b19c7f426e47cba3e0a Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Wed, 2 Feb 2022 22:49:02 +0000 Subject: [PATCH 10/78] fix didClose --- src/Psalm/Internal/LanguageServer/Server/TextDocument.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php index d09a6a602f1..e2b60f3aff8 100644 --- a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php +++ b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php @@ -168,7 +168,7 @@ public function didClose(TextDocumentIdentifier $textDocument): void { $this->server->logDebug( 'textDocument/didClose', - ['version' => $textDocument->version, 'uri' => $textDocument->uri] + ['uri' => $textDocument->uri] ); $file_path = LanguageServer::uriToPath($textDocument->uri); From 9f6843bbb8e51ce510d2102123b2a73528413c53 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Wed, 2 Feb 2022 23:37:20 +0000 Subject: [PATCH 11/78] add psalm and phpcs checking and cleanup errors from that --- .gitignore | 2 +- src/Psalm/Codebase.php | 19 +++-- src/Psalm/Internal/Cli/LanguageServer.php | 2 +- .../LanguageServer/Client/TextDocument.php | 8 +- .../LanguageServer/Client/Workspace.php | 2 +- .../LanguageServer/ClientConfiguration.php | 2 +- .../Internal/LanguageServer/ClientHandler.php | 1 - .../LanguageServer/LanguageClient.php | 19 +++-- .../LanguageServer/LanguageServer.php | 82 +++++++++++++------ .../Internal/LanguageServer/Progress.php | 7 +- .../LanguageServer/Server/TextDocument.php | 41 ++++++---- .../LanguageServer/Server/Workspace.php | 30 ++++--- src/Psalm/Internal/Provider/FileProvider.php | 7 +- 13 files changed, 133 insertions(+), 89 deletions(-) diff --git a/.gitignore b/.gitignore index a060c7ed342..44462dfcd6e 100644 --- a/.gitignore +++ b/.gitignore @@ -10,5 +10,5 @@ /vendor-bin/*/composer.lock /vendor-bin/*/vendor/ /tests/fixtures/symlinktest/* - +.vscode .idea/ diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index e0682bf9808..7ae1a395e05 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -7,7 +7,6 @@ use LanguageServerProtocol\Command; use LanguageServerProtocol\CompletionItem; use LanguageServerProtocol\CompletionItemKind; -use LanguageServerProtocol\InsertTextFormat; use LanguageServerProtocol\ParameterInformation; use LanguageServerProtocol\Position; use LanguageServerProtocol\Range; @@ -413,7 +412,7 @@ public function reloadFiles(ProjectAnalyzer $project_analyzer, array $candidate_ { $this->loadAnalyzer(); - if($force) { + if ($force) { FileReferenceProvider::clearCache(); } @@ -1521,8 +1520,11 @@ public function getTypeContextAtPosition(string $file_path, Position $position): /** * @return list */ - public function getCompletionItemsForClassishThing(string $type_string, string $gap): array - { + public function getCompletionItemsForClassishThing( + string $type_string, + string $gap, + bool $snippets_supported = false + ): array { $completion_items = []; $type = Type::parseString($type_string); @@ -1543,7 +1545,7 @@ public function getCompletionItemsForClassishThing(string $type_string, string $ $method_storage->description, (string)$method_storage->visibility, $method_storage->cased_name, - $method_storage->cased_name . (count($method_storage->params) !== 0 ? '($0)' : '()'), + $method_storage->cased_name, null, null, new Command('Trigger parameter hints', 'editor.action.triggerParameterHints'), @@ -1551,7 +1553,10 @@ public function getCompletionItemsForClassishThing(string $type_string, string $ 2 ); - $completion_item->insertTextFormat = InsertTextFormat::SNIPPET; + if ($snippets_supported && count($method_storage->params) > 0) { + $completion_item->insertText .= '($0)'; + $completion_item->insertTextFormat = InsertTextFormat::SNIPPET; + } $completion_items[] = $completion_item; } @@ -1868,7 +1873,7 @@ private static function getPositionFromOffset(int $offset, string $file_contents public function addTemporaryFileChanges(string $file_path, string $new_content, ?int $version = null): void { - $this->file_provider->addTemporaryFileChanges($file_path, $new_content, $version ); + $this->file_provider->addTemporaryFileChanges($file_path, $new_content, $version); } public function removeTemporaryFileChanges(string $file_path): void diff --git a/src/Psalm/Internal/Cli/LanguageServer.php b/src/Psalm/Internal/Cli/LanguageServer.php index 5dae734897a..ef0059bfb13 100644 --- a/src/Psalm/Internal/Cli/LanguageServer.php +++ b/src/Psalm/Internal/Cli/LanguageServer.php @@ -281,7 +281,7 @@ function () use ($current_dir, $options, $vendor_dir): ?\Composer\Autoload\Class if ($cache_directory !== null) { Config::removeCacheDirectory($cache_directory); } - if(!isset($options['clear-cache-on-boot'])) { + if (!isset($options['clear-cache-on-boot'])) { echo 'Cache directory deleted' . PHP_EOL; exit; } diff --git a/src/Psalm/Internal/LanguageServer/Client/TextDocument.php b/src/Psalm/Internal/LanguageServer/Client/TextDocument.php index c17196afa66..9683ed65181 100644 --- a/src/Psalm/Internal/LanguageServer/Client/TextDocument.php +++ b/src/Psalm/Internal/LanguageServer/Client/TextDocument.php @@ -4,17 +4,11 @@ namespace Psalm\Internal\LanguageServer\Client; -use Amp\Promise; -use Generator; use JsonMapper; use LanguageServerProtocol\Diagnostic; -use LanguageServerProtocol\TextDocumentIdentifier; -use LanguageServerProtocol\TextDocumentItem; use Psalm\Internal\LanguageServer\ClientHandler; use Psalm\Internal\LanguageServer\LanguageServer; -use function Amp\call; - /** * Provides method handlers for all textDocument/* methods */ @@ -47,7 +41,7 @@ public function __construct(ClientHandler $handler, JsonMapper $mapper, Language * * @param Diagnostic[] $diagnostics */ - public function publishDiagnostics(string $uri, array $diagnostics, ?int $version=null): void + public function publishDiagnostics(string $uri, array $diagnostics, ?int $version = null): void { $this->server->logDebug("textDocument/publishDiagnostics"); diff --git a/src/Psalm/Internal/LanguageServer/Client/Workspace.php b/src/Psalm/Internal/LanguageServer/Client/Workspace.php index 1fbe1d3a942..ce39a2f987d 100644 --- a/src/Psalm/Internal/LanguageServer/Client/Workspace.php +++ b/src/Psalm/Internal/LanguageServer/Client/Workspace.php @@ -61,4 +61,4 @@ public function requestConfiguration(string $section, ?string $scopeUri = null): ] ]); } -} \ No newline at end of file +} diff --git a/src/Psalm/Internal/LanguageServer/ClientConfiguration.php b/src/Psalm/Internal/LanguageServer/ClientConfiguration.php index b7bb5d46c87..d6e290e4804 100644 --- a/src/Psalm/Internal/LanguageServer/ClientConfiguration.php +++ b/src/Psalm/Internal/LanguageServer/ClientConfiguration.php @@ -67,4 +67,4 @@ public function __construct( $this->hideWarnings = $hideWarnings; $this->provideCompletion = $provideCompletion; } -} \ No newline at end of file +} diff --git a/src/Psalm/Internal/LanguageServer/ClientHandler.php b/src/Psalm/Internal/LanguageServer/ClientHandler.php index 475797b0c37..335e4b19234 100644 --- a/src/Psalm/Internal/LanguageServer/ClientHandler.php +++ b/src/Psalm/Internal/LanguageServer/ClientHandler.php @@ -13,7 +13,6 @@ use Generator; use function Amp\call; -use function error_log; /** * @internal diff --git a/src/Psalm/Internal/LanguageServer/LanguageClient.php b/src/Psalm/Internal/LanguageServer/LanguageClient.php index 6a7b4b330a2..2a9043ef7c0 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageClient.php +++ b/src/Psalm/Internal/LanguageServer/LanguageClient.php @@ -10,6 +10,8 @@ use Psalm\Internal\LanguageServer\Client\TextDocument as ClientTextDocument; use Psalm\Internal\LanguageServer\Client\Workspace as ClientWorkspace; +use function is_null; + /** * @internal */ @@ -95,16 +97,16 @@ public function refreshConfiguration(): void * The amount and content of these notifications depends on the current trace configuration. * * @param LogTrace $logTrace - * @return void */ - public function logTrace(LogTrace $logTrace): void { + public function logTrace(LogTrace $logTrace): void + { //If trace is 'off', the server should not send any logTrace notification. - if(is_null($this->server->trace) || $this->server->trace === 'off') { + if (is_null($this->server->trace) || $this->server->trace === 'off') { return; } //If trace is 'messages', the server should not add the 'verbose' field in the LogTraceParams. - if($this->server->trace === 'messages') { + if ($this->server->trace === 'messages') { $logTrace->verbose = null; } @@ -139,19 +141,20 @@ public function logMessage(LogMessage $logMessage): void * server issuing the event. * * @param LogMessage $logMessage - * @return void */ - public function event(LogMessage $logMessage) { + public function event(LogMessage $logMessage): void + { $this->handler->notify( 'telemetry/event', $logMessage ); } - private function configurationRefreshed() { + private function configurationRefreshed(): void + { //do things when the config is refreshed - if(!is_null($this->clientConfiguration->provideCompletion)) { + if (!is_null($this->clientConfiguration->provideCompletion)) { //$this->server->project_analyzer->provide_completion = $this->clientConfiguration->provideCompletion; } } diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 4a354ff0389..9095b140773 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -33,7 +33,6 @@ use LanguageServerProtocol\SignatureHelpOptions; use LanguageServerProtocol\TextDocumentSyncKind; use LanguageServerProtocol\TextDocumentSyncOptions; -use LanguageServerProtocol\WorkspaceFolder; use Psalm\Config; use Psalm\Internal\Analyzer\IssueData; use Psalm\Internal\Analyzer\ProjectAnalyzer; @@ -45,12 +44,15 @@ use function Amp\asyncCoroutine; use function Amp\call; use function array_combine; +use function array_filter; use function array_keys; use function array_map; +use function array_reduce; use function array_shift; use function array_unshift; use function explode; use function implode; +use function json_encode; use function max; use function parse_url; use function rawurlencode; @@ -61,6 +63,8 @@ use function trim; use function urldecode; +use const JSON_PRETTY_PRINT; + /** * @internal */ @@ -153,7 +157,7 @@ function (Message $msg): Generator { * @var Promise|null */ $dispatched = $this->dispatch($msg->body); - if($dispatched !== null) { + if ($dispatched !== null) { /** @psalm-suppress MixedAssignment */ $result = yield $dispatched; } else { @@ -244,7 +248,7 @@ public function initialize( $this->trace = $trace; return call( /** @return Generator */ - function () use($capabilities) { + function () use ($capabilities) { $this->logInfo("Initializing..."); $this->clientStatus('initializing'); @@ -315,8 +319,7 @@ function () use($capabilities) { // Support "Completion" // Support "Code Actions" if we support data - if( - $this->clientCapabilities && + if ($this->clientCapabilities && $this->clientCapabilities->textDocument && $this->clientCapabilities->textDocument->publishDiagnostics && $this->clientCapabilities->textDocument->publishDiagnostics->dataSupport @@ -355,17 +358,19 @@ public function initialized(): void { try { $this->client->refreshConfiguration(); - } catch(Throwable $e) { - $this->server->logError((string) $e); + } catch (Throwable $e) { + $this->logError((string) $e); } $this->clientStatus('running'); } - public function queueChangeFileAnalysis(string $file_path, string $uri, ?int $version=null) { + public function queueChangeFileAnalysis(string $file_path, string $uri, ?int $version = null): void + { $this->doVersionedAnalysis([$file_path => $uri], $version); } - public function queueOpenFileAnalysis(string $file_path, string $uri, ?int $version=null) { + public function queueOpenFileAnalysis(string $file_path, string $uri, ?int $version = null): void + { $this->doVersionedAnalysis([$file_path => $uri], $version); } @@ -373,24 +378,48 @@ public function queueOpenFileAnalysis(string $file_path, string $uri, ?int $vers * Queue Saved File Analysis * @param string $file_path * @param string $uri - * @return void */ - public function queueSaveFileAnalysis(string $file_path, string $uri) { + public function queueSaveFileAnalysis(string $file_path, string $uri): void + { //Always reanalzye open files because of things changing elsewhere + $this->doVersionedAnalysis( + $this->queueFileAnalysisWithOpenedFiles([$file_path => $this->pathToUri($file_path)]) + ); + } + + public function queueFileAnalysisWithOpenedFiles(array $files = []): void + { $opened = array_reduce( $this->project_analyzer->getCodebase()->file_provider->getOpenFilesPath(), function (array $opened, string $file_path) { $opened[$file_path] = $this->pathToUri($file_path); return $opened; }, - [$file_path => $this->pathToUri($file_path)]); + $files + ); $this->doVersionedAnalysis($opened); } - public function doVersionedAnalysis($all_files_to_analyze, ?int $version=null):void { + public function queueClosedFileAnalysis(string $file_path, string $uri): void + { + //Always reanalzye open files because of things changing elsewhere + $opened = array_reduce( + $this->project_analyzer->getCodebase()->file_provider->getOpenFilesPath(), + function (array $opened, string $file_path) { + $opened[$file_path] = $this->pathToUri($file_path); + return $opened; + }, + [$file_path => $this->pathToUri($file_path)] + ); + + $this->doVersionedAnalysis($opened); + } + + public function doVersionedAnalysis($all_files_to_analyze, ?int $version = null): void + { try { - if(empty($all_files_to_analyze)) { + if (empty($all_files_to_analyze)) { $this->logWarning("No versioned analysis to do."); return; } @@ -410,14 +439,15 @@ public function doVersionedAnalysis($all_files_to_analyze, ?int $version=null):v ); $codebase->analyzer->analyzeFiles($this->project_analyzer, 1, false); - $this->emitVersionedIssues($files,$version); - } catch(Throwable $e) { + $this->emitVersionedIssues($files, $version); + } catch (Throwable $e) { $this->server->logError((string) $e); } } - public function emitVersionedIssues(array $files, ?int $version = null): void { - $this->logDebug("Perform Analysis",[ + public function emitVersionedIssues(array $files, ?int $version = null): void + { + $this->logDebug("Perform Analysis", [ 'files' => array_keys($files), 'version' => $version ]); @@ -541,8 +571,8 @@ public function exit(): void */ public function log(int $type, string $message, array $context = []): void { - if(!empty($context)) { - $message .= "\n" . \json_encode($context, JSON_PRETTY_PRINT); + if (!empty($context)) { + $message .= "\n" . json_encode($context, JSON_PRETTY_PRINT); } try { $this->client->logMessage( @@ -557,19 +587,23 @@ public function log(int $type, string $message, array $context = []): void } } - public function logError(string $message, array $context = []) { + public function logError(string $message, array $context = []): void + { $this->log(MessageType::ERROR, $message, $context); } - public function logWarning(string $message, array $context = []) { + public function logWarning(string $message, array $context = []): void + { $this->log(MessageType::WARNING, $message, $context); } - public function logInfo(string $message, array $context = []) { + public function logInfo(string $message, array $context = []): void + { $this->log(MessageType::INFO, $message, $context); } - public function logDebug(string $message, array $context = []) { + public function logDebug(string $message, array $context = []): void + { $this->log(MessageType::LOG, $message, $context); } diff --git a/src/Psalm/Internal/LanguageServer/Progress.php b/src/Psalm/Internal/LanguageServer/Progress.php index 7297be6cd22..27eeb4e7efc 100644 --- a/src/Psalm/Internal/LanguageServer/Progress.php +++ b/src/Psalm/Internal/LanguageServer/Progress.php @@ -1,9 +1,7 @@ server = $server; } @@ -30,4 +29,4 @@ public function write(string $message): void { $this->server->logInfo($message); } -} \ No newline at end of file +} diff --git a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php index e2b60f3aff8..a04cca6f61c 100644 --- a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php +++ b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php @@ -6,11 +6,9 @@ use Amp\Promise; use Amp\Success; -use InvalidArgumentException; use LanguageServerProtocol\CodeAction; use LanguageServerProtocol\CodeActionContext; use LanguageServerProtocol\CodeActionKind; -use LanguageServerProtocol\Command; use LanguageServerProtocol\CompletionList; use LanguageServerProtocol\Hover; use LanguageServerProtocol\Location; @@ -31,10 +29,8 @@ use Psalm\Internal\LanguageServer\LanguageServer; use UnexpectedValueException; -use function array_combine; use function array_values; use function count; -use function error_log; use function preg_match; use function substr_count; @@ -137,7 +133,7 @@ public function didChange(VersionedTextDocumentIdentifier $textDocument, array $ return; } - if (count($contentChanges) === 1 && $contentChanges[0]->range === null) { + if (count($contentChanges) === 1 && isset($contentChanges[0]) && $contentChanges[0]->range === null) { $new_content = $contentChanges[0]->text; } else { throw new UnexpectedValueException('Not expecting partial diff'); @@ -264,7 +260,7 @@ public function hover(TextDocumentIdentifier $textDocument, Position $position): try { $symbol_information = $this->codebase->getSymbolInformation($file_path, $reference); - } catch(UnexpectedValueException $e) { + } catch (UnexpectedValueException $e) { $this->server->logError((string) $e); return new Success(null); } @@ -297,7 +293,7 @@ public function hover(TextDocumentIdentifier $textDocument, Position $position): * * @param TextDocumentIdentifier $textDocument The text document * @param Position $position The position - * @psalm-return Promise>|Promise + * @psalm-return Promise>|Promise|Promise */ public function completion(TextDocumentIdentifier $textDocument, Position $position): Promise { @@ -335,7 +331,12 @@ public function completion(TextDocumentIdentifier $textDocument, Position $posit [$recent_type, $gap, $offset] = $completion_data; if ($gap === '->' || $gap === '::') { - $completion_items = $this->codebase->getCompletionItemsForClassishThing($recent_type, $gap); + $snippetSupport = ($this->server->clientCapabilities->textDocument && + $this->server->clientCapabilities->textDocument->completion && + $this->server->clientCapabilities->textDocument->completion->completionItem && + $this->server->clientCapabilities->textDocument->completion->completionItem->snippetSupport) + ? true : false; + $completion_items = $this->codebase->getCompletionItemsForClassishThing($recent_type, $gap, $snippetSupport); } elseif ($gap === '[') { $completion_items = $this->codebase->getCompletionItemsForArrayKeys($recent_type); } else { @@ -382,7 +383,7 @@ public function signatureHelp(TextDocumentIdentifier $textDocument, Position $po try { $signature_information = $this->codebase->getSignatureInformation($argument_location[0], $file_path); - } catch(UnexpectedValueException $e) { + } catch (UnexpectedValueException $e) { $this->server->logError((string) $e); return new Success(null); } @@ -420,23 +421,27 @@ public function codeAction(TextDocumentIdentifier $textDocument, Range $range, C } $fixers = []; - foreach($context->diagnostics as $diagnostic) { - if($diagnostic->source !== 'psalm') { + foreach ($context->diagnostics as $diagnostic) { + if ($diagnostic->source !== 'psalm') { continue; } + + /** @var array{type: string, snippet: string, line_from: int, line_to: int} */ + $data = (array)$diagnostic->data; + $snippetRange = new Range( - new Position($diagnostic->data->line_from-1), - new Position($diagnostic->data->line_to) + new Position($data['line_from']-1), + new Position($data['line_to']) ); $indentation = ''; - if (preg_match('/^(\s*)/', $diagnostic->data->snippet, $matches)) { + if (preg_match('/^(\s*)/', $data['snippet'], $matches)) { $indentation = $matches[1] ?? ''; } //Suppress Ability - $fixers["suppress.{$diagnostic->data->type}"] = new CodeAction( - "Suppress {$diagnostic->data->type} for this line", + $fixers["suppress.{$data['type']}"] = new CodeAction( + "Suppress {$data['type']} for this line", CodeActionKind::QUICK_FIX, null, null, @@ -446,9 +451,9 @@ public function codeAction(TextDocumentIdentifier $textDocument, Range $range, C new TextEdit( $snippetRange, "{$indentation}/**\n". - "{$indentation} * @psalm-suppress {$diagnostic->data->type}\n". + "{$indentation} * @psalm-suppress {$data['type']}\n". "{$indentation} */\n". - "{$diagnostic->data->snippet}\n" + "{$data['snippet']}\n" ) ] ]) diff --git a/src/Psalm/Internal/LanguageServer/Server/Workspace.php b/src/Psalm/Internal/LanguageServer/Server/Workspace.php index bf16a2ab538..bbf1564185f 100644 --- a/src/Psalm/Internal/LanguageServer/Server/Workspace.php +++ b/src/Psalm/Internal/LanguageServer/Server/Workspace.php @@ -4,6 +4,7 @@ namespace Psalm\Internal\LanguageServer\Server; +use Amp\Promise; use Amp\Success; use LanguageServerProtocol\FileChangeType; use LanguageServerProtocol\FileEvent; @@ -13,6 +14,10 @@ use Psalm\Internal\LanguageServer\LanguageServer; use Psalm\Internal\Provider\FileReferenceProvider; +use function array_map; +use function in_array; +use function realpath; + /** * Provides method handlers for all workspace/* methods */ @@ -60,24 +65,22 @@ public function didChangeWatchedFiles(array $changes): void 'workspace/didChangeWatchedFiles' ); - $realFiles = array_map(function(FileEvent $change) { + $realFiles = array_map(function (FileEvent $change) { return LanguageServer::uriToPath($change->uri); }, $changes); $composerLockFile = realpath(Composer::getLockFilePath($this->codebase->config->base_dir)); - if(in_array($composerLockFile, $realFiles)) { + if (in_array($composerLockFile, $realFiles)) { $this->server->logInfo('Composer.lock file changed. Reloading codebase'); FileReferenceProvider::clearCache(); - foreach($this->codebase->file_provider->getOpenFiles() as $file) { - $this->server->queueFileAnalysis($file, $this->server->pathToUri($file)); - } + $this->server->queueFileAnalysisWithOpenedFiles(); return; } foreach ($changes as $change) { $file_path = LanguageServer::uriToPath($change->uri); - if($composerLockFile === $file_path) { + if ($composerLockFile === $file_path) { continue; } @@ -96,7 +99,7 @@ public function didChangeWatchedFiles(array $changes): void //If the file is currently open then dont analize it because its tracked in didChange if (!$this->codebase->file_provider->isOpen($file_path)) { - $this->server->queueFileAnalysis($file_path, $change->uri); + $this->server->queueClosedFileAnalysis($file_path, $change->uri); } } } @@ -123,7 +126,8 @@ public function didChangeConfiguration($settings): void * @param mixed $arguments * @psalm-suppress PossiblyUnusedMethod */ - public function executeCommand($command, $arguments) { + public function executeCommand($command, $arguments): Promise + { $this->server->logDebug( 'workspace/executeCommand', [ @@ -132,9 +136,11 @@ public function executeCommand($command, $arguments) { ] ); - switch($command) { + switch ($command) { case 'psalm.analyze.uri': - $file = LanguageServer::uriToPath($arguments->uri); + /** @var array{uri: string} */ + $arguments = (array) $arguments; + $file = LanguageServer::uriToPath($arguments['uri']); $codebase = $this->project_analyzer->getCodebase(); $codebase->reloadFiles( $this->project_analyzer, @@ -147,8 +153,8 @@ public function executeCommand($command, $arguments) { ); $codebase->analyzer->analyzeFiles($this->project_analyzer, 1, false); - $this->server->emitVersionedIssues([$file => $arguments->uri]); - break; + $this->server->emitVersionedIssues([$file => $arguments['uri']]); + break; } return new Success(null); diff --git a/src/Psalm/Internal/Provider/FileProvider.php b/src/Psalm/Internal/Provider/FileProvider.php index d9d4da155dc..f8a674a2f11 100644 --- a/src/Psalm/Internal/Provider/FileProvider.php +++ b/src/Psalm/Internal/Provider/FileProvider.php @@ -72,11 +72,11 @@ public function setContents(string $file_path, string $file_contents): void file_put_contents($file_path, $file_contents); } - public function setOpenContents(string $file_path, string $file_contents): void + public function setOpenContents(string $file_path, ?string $file_contents = null): void { $file_path_lc = strtolower($file_path); if (isset($this->open_files[$file_path_lc])) { - $this->open_files[$file_path_lc] = $file_contents; + $this->open_files[$file_path_lc] = $file_contents ?? $this->getContents($file_path, true); } } @@ -91,8 +91,7 @@ public function getModifiedTime(string $file_path): int public function addTemporaryFileChanges(string $file_path, string $new_content, ?int $version = null): void { - if( - isset($this->temp_files[strtolower($file_path)]) && + if (isset($this->temp_files[strtolower($file_path)]) && $version !== null && $this->temp_files[strtolower($file_path)]['version'] !== null && $version < $this->temp_files[strtolower($file_path)]['version'] From d8394cb72f6b0474c7845d9c0763e8c72c72ebf0 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Thu, 3 Feb 2022 00:34:32 +0000 Subject: [PATCH 12/78] add class reference --- src/Psalm/Codebase.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index 7ae1a395e05..5dc7bb65b81 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -7,6 +7,7 @@ use LanguageServerProtocol\Command; use LanguageServerProtocol\CompletionItem; use LanguageServerProtocol\CompletionItemKind; +use LanguageServerProtocol\InsertTextFormat; use LanguageServerProtocol\ParameterInformation; use LanguageServerProtocol\Position; use LanguageServerProtocol\Range; From c8f50b9f078c869212da88facd06b2aec26ffce7 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Thu, 3 Feb 2022 19:06:25 +0000 Subject: [PATCH 13/78] move starting of server into server itself --- .../Internal/Analyzer/ProjectAnalyzer.php | 114 +---- src/Psalm/Internal/Cli/LanguageServer.php | 49 +-- .../LanguageServer/ClientConfiguration.php | 13 +- .../LanguageServer/LanguageServer.php | 395 +++++++++++++++--- 4 files changed, 361 insertions(+), 210 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php index 1bf7f671bcf..9c0557cdb13 100644 --- a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php @@ -2,7 +2,6 @@ namespace Psalm\Internal\Analyzer; -use Amp\Loop; use InvalidArgumentException; use LogicException; use Psalm\Codebase; @@ -13,10 +12,7 @@ use Psalm\FileManipulation; use Psalm\Internal\Codebase\TaintFlowGraph; use Psalm\Internal\FileManipulation\FileManipulationBuffer; -use Psalm\Internal\LanguageServer\ClientConfiguration; use Psalm\Internal\LanguageServer\LanguageServer; -use Psalm\Internal\LanguageServer\ProtocolStreamReader; -use Psalm\Internal\LanguageServer\ProtocolStreamWriter; use Psalm\Internal\MethodIdentifier; use Psalm\Internal\Provider\ClassLikeStorageProvider; use Psalm\Internal\Provider\FileProvider; @@ -64,7 +60,6 @@ use function array_map; use function array_merge; use function array_shift; -use function cli_set_process_title; use function count; use function defined; use function dirname; @@ -87,14 +82,9 @@ use function microtime; use function mkdir; use function number_format; -use function pcntl_fork; use function preg_match; use function rename; use function shell_exec; -use function stream_set_blocking; -use function stream_socket_accept; -use function stream_socket_client; -use function stream_socket_server; use function strlen; use function strpos; use function strtolower; @@ -110,8 +100,6 @@ use const PHP_VERSION; use const PSALM_VERSION; use const STDERR; -use const STDIN; -use const STDOUT; /** * @internal @@ -426,11 +414,12 @@ private function visitAutoloadFiles(): void ); } - //public function server(?string $address = '127.0.0.1:12345', bool $socket_server_mode = false): void - public function server(ClientConfiguration $clientConfiguration): void + public function serverMode(LanguageServer $server): void { + $server->logInfo("Initializing: Visiting Autoload Files..."); $this->visitAutoloadFiles(); $this->codebase->diff_methods = true; + $server->logInfo("Initializing: Loading Reference Cache..."); $this->file_reference_provider->loadReferenceCache(); $this->codebase->enterServerMode(); @@ -451,107 +440,12 @@ public function server(ClientConfiguration $clientConfiguration): void } } + $server->logInfo("Initializing: Initialize Plugins..."); $this->config->initializePlugins($this); foreach ($this->config->getProjectDirectories() as $dir_name) { $this->checkDirWithConfig($dir_name, $this->config); } - - @cli_set_process_title('Psalm ' . PSALM_VERSION . ' - PHP Language Server'); - - if (!$clientConfiguration->TCPServerMode && $clientConfiguration->TCPServerAddress) { - // Connect to a TCP server - $socket = stream_socket_client('tcp://' . $clientConfiguration->TCPServerAddress, $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, - $clientConfiguration - ); - Loop::run(); - } elseif ($clientConfiguration->TCPServerMode && $clientConfiguration->TCPServerAddress) { - // Run a TCP Server - $tcpServer = stream_socket_server('tcp://' . $clientConfiguration->TCPServerAddress, $errno, $errstr); - if ($tcpServer === false) { - fwrite(STDERR, "Could not listen on {$clientConfiguration->TCPServerAddress}. Error $errno\n$errstr"); - exit(1); - } - fwrite(STDOUT, "Server listening on {$clientConfiguration->TCPServerAddress}\n"); - - $fork_available = true; - if (!extension_loaded('pcntl')) { - fwrite(STDERR, "PCNTL is not available. Only a single connection will be accepted\n"); - $fork_available = false; - } - - $disabled_functions = array_map('trim', explode(',', ini_get('disable_functions'))); - if (in_array('pcntl_fork', $disabled_functions)) { - fwrite( - STDERR, - "pcntl_fork() is disabled by php configuration (disable_functions directive)." - . " Only a single connection will be accepted\n" - ); - $fork_available = false; - } - - while ($socket = stream_socket_accept($tcpServer, -1)) { - fwrite(STDOUT, "Connection accepted\n"); - stream_set_blocking($socket, false); - if ($fork_available) { - // 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', - function (): void { - fwrite(STDOUT, "Connection closed\n"); - } - ); - new LanguageServer( - $reader, - new ProtocolStreamWriter($socket), - $this, - $clientConfiguration - ); - // 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, - $clientConfiguration - ); - Loop::run(); - } - } - } else { - // Use STDIO - stream_set_blocking(STDIN, false); - new LanguageServer( - new ProtocolStreamReader(STDIN), - new ProtocolStreamWriter(STDOUT), - $this, - $clientConfiguration - ); - Loop::run(); - } } public static function getInstance(): ProjectAnalyzer diff --git a/src/Psalm/Internal/Cli/LanguageServer.php b/src/Psalm/Internal/Cli/LanguageServer.php index ef0059bfb13..f32f859e99d 100644 --- a/src/Psalm/Internal/Cli/LanguageServer.php +++ b/src/Psalm/Internal/Cli/LanguageServer.php @@ -4,20 +4,12 @@ use LanguageServerProtocol\MessageType; use Psalm\Config; -use Psalm\Internal\Analyzer\ProjectAnalyzer; use Psalm\Internal\CliUtils; -use Psalm\Internal\Composer; use Psalm\Internal\ErrorHandler; use Psalm\Internal\Fork\PsalmRestarter; use Psalm\Internal\IncludeCollector; use Psalm\Internal\LanguageServer\ClientConfiguration; -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\Internal\LanguageServer\LanguageServer as LanguageServerLanguageServer; use Psalm\Report; use function array_key_exists; @@ -258,8 +250,6 @@ function () use ($current_dir, $options, $vendor_dir): ?\Composer\Autoload\Class } } - $find_unused_code = isset($options['find-dead-code']) ? 'auto' : null; - $config = CliUtils::initializeConfig( $path_to_config, $current_dir, @@ -287,39 +277,20 @@ function () use ($current_dir, $options, $vendor_dir): ?\Composer\Autoload\Class } } - //no-cache mode does not work in the LSP - $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']; + $clientConfiguration->onchangeLineLimit = (int) $options['disable-on-change']; } $clientConfiguration->provideCompletion = !isset($options['enable-autocomplete']) || !is_string($options['enable-autocomplete']) || strtolower($options['enable-autocomplete']) !== 'false'; + $find_unused_code = isset($options['find-dead-code']) ? 'auto' : null; + if ($config->find_unused_code) { + $find_unused_code = 'auto'; + } if ($find_unused_code) { - $project_analyzer->getCodebase()->reportUnusedCode($find_unused_code); + $clientConfiguration->findUnusedCode = $find_unused_code; } if (isset($options['verbose'])) { @@ -329,10 +300,6 @@ function () use ($current_dir, $options, $vendor_dir): ?\Composer\Autoload\Class $clientConfiguration->TCPServerAddress = $options['tcp'] ?? null; $clientConfiguration->TCPServerMode = isset($options['tcp-server']); - //Setup Project Analyzer - $project_analyzer->provide_completion = $clientConfiguration->provideCompletion; - - - $project_analyzer->server($clientConfiguration); + LanguageServerLanguageServer::run($config, $clientConfiguration, $current_dir); } } diff --git a/src/Psalm/Internal/LanguageServer/ClientConfiguration.php b/src/Psalm/Internal/LanguageServer/ClientConfiguration.php index d6e290e4804..fbbf2e41de2 100644 --- a/src/Psalm/Internal/LanguageServer/ClientConfiguration.php +++ b/src/Psalm/Internal/LanguageServer/ClientConfiguration.php @@ -45,9 +45,9 @@ class ClientConfiguration public $findUnusedVariables; /** - * Provide Completion or not + * Look for dead code * - * @var bool|null + * @var 'always'|'auto'|null */ public $findUnusedCode; @@ -60,6 +60,15 @@ class ClientConfiguration */ public $logLevel; + /** + * 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. + * + * @var int|null + * + */ + public $onchangeLineLimit; + public function __construct( bool $hideWarnings = null, bool $provideCompletion = null diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 9095b140773..1b6957392e0 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -11,6 +11,7 @@ use AdvancedJsonRpc\Request; use AdvancedJsonRpc\Response; use AdvancedJsonRpc\SuccessResponse; +use Amp\Loop; use Amp\Promise; use Amp\Success; use Generator; @@ -36,8 +37,16 @@ use Psalm\Config; use Psalm\Internal\Analyzer\IssueData; use Psalm\Internal\Analyzer\ProjectAnalyzer; +use Psalm\Internal\Composer; use Psalm\Internal\LanguageServer\Server\TextDocument as ServerTextDocument; use Psalm\Internal\LanguageServer\Server\Workspace as ServerWorkspace; +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\IssueBuffer; use Throwable; @@ -50,20 +59,33 @@ use function array_reduce; use function array_shift; use function array_unshift; +use function cli_set_process_title; use function explode; +use function extension_loaded; +use function fwrite; use function implode; +use function in_array; +use function ini_get; use function json_encode; use function max; use function parse_url; +use function pcntl_fork; use function rawurlencode; use function realpath; use function str_replace; +use function stream_set_blocking; +use function stream_socket_accept; +use function stream_socket_client; +use function stream_socket_server; use function strpos; use function substr; use function trim; use function urldecode; use const JSON_PRETTY_PRINT; +use const STDERR; +use const STDIN; +use const STDOUT; /** * @internal @@ -100,7 +122,7 @@ class LanguageServer extends Dispatcher public $client; /** - * @var ClientCapabilities + * @var ClientCapabilities|null */ public $clientCapabilities; @@ -121,6 +143,7 @@ public function __construct( ClientConfiguration $clientConfiguration ) { parent::__construct($this, '/'); + $this->project_analyzer = $project_analyzer; $this->protocolWriter = $writer; @@ -211,11 +234,142 @@ function (): void { $this->logInfo("Psalm Language Server ".PSALM_VERSION." has started."); } + /** + * Start the Server + */ + public static function run(Config $config, ClientConfiguration $clientConfiguration, string $base_dir): void + { + //no-cache mode does not work in the LSP + $providers = new Providers( + new FileProvider, + new ParserCacheProvider($config), + new FileStorageCacheProvider($config), + new ClassLikeStorageCacheProvider($config), + new FileReferenceCacheProvider($config), + new ProjectCacheProvider(Composer::getLockFilePath($base_dir)) + ); + + $project_analyzer = new ProjectAnalyzer( + $config, + $providers + ); + + if ($config->find_unused_variables) { + $project_analyzer->getCodebase()->reportUnusedVariables(); + } + + if ($clientConfiguration->onchangeLineLimit) { + $project_analyzer->onchange_line_limit = $clientConfiguration->onchangeLineLimit; + } + + if ($clientConfiguration->findUnusedCode) { + $project_analyzer->getCodebase()->reportUnusedCode($clientConfiguration->findUnusedCode); + } + + //Setup Project Analyzer + $project_analyzer->provide_completion = (bool) $clientConfiguration->provideCompletion; + + @cli_set_process_title('Psalm ' . PSALM_VERSION . ' - PHP Language Server'); + + if (!$clientConfiguration->TCPServerMode && $clientConfiguration->TCPServerAddress) { + // Connect to a TCP server + $socket = stream_socket_client('tcp://' . $clientConfiguration->TCPServerAddress, $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 self( + new ProtocolStreamReader($socket), + new ProtocolStreamWriter($socket), + $project_analyzer, + $clientConfiguration + ); + Loop::run(); + } elseif ($clientConfiguration->TCPServerMode && $clientConfiguration->TCPServerAddress) { + // Run a TCP Server + $tcpServer = stream_socket_server('tcp://' . $clientConfiguration->TCPServerAddress, $errno, $errstr); + if ($tcpServer === false) { + fwrite(STDERR, "Could not listen on {$clientConfiguration->TCPServerAddress}. Error $errno\n$errstr"); + exit(1); + } + fwrite(STDOUT, "Server listening on {$clientConfiguration->TCPServerAddress}\n"); + + $fork_available = true; + if (!extension_loaded('pcntl')) { + fwrite(STDERR, "PCNTL is not available. Only a single connection will be accepted\n"); + $fork_available = false; + } + + $disabled_functions = array_map('trim', explode(',', ini_get('disable_functions'))); + if (in_array('pcntl_fork', $disabled_functions)) { + fwrite( + STDERR, + "pcntl_fork() is disabled by php configuration (disable_functions directive)." + . " Only a single connection will be accepted\n" + ); + $fork_available = false; + } + + while ($socket = stream_socket_accept($tcpServer, -1)) { + fwrite(STDOUT, "Connection accepted\n"); + stream_set_blocking($socket, false); + if ($fork_available) { + // 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', + function (): void { + fwrite(STDOUT, "Connection closed\n"); + } + ); + new self( + $reader, + new ProtocolStreamWriter($socket), + $project_analyzer, + $clientConfiguration + ); + // 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), + $project_analyzer, + $clientConfiguration + ); + Loop::run(); + } + } + } else { + // Use STDIO + stream_set_blocking(STDIN, false); + new LanguageServer( + new ProtocolStreamReader(STDIN), + new ProtocolStreamWriter(STDOUT), + $project_analyzer, + $clientConfiguration + ); + Loop::run(); + } + } + /** * 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. @@ -241,14 +395,14 @@ public function initialize( ?string $rootPath = null, ?string $rootUri = null, $initializationOptions = null, - ?string $trace = null, + ?string $trace = null //?array $workspaceFolders = null //error in json-dispatcher ): Promise { $this->clientCapabilities = $capabilities; $this->trace = $trace; return call( /** @return Generator */ - function () use ($capabilities) { + function () { $this->logInfo("Initializing..."); $this->clientStatus('initializing'); @@ -258,17 +412,19 @@ function () use ($capabilities) { yield true; } + $this->project_analyzer->serverMode($this); + $this->logInfo("Initializing: Getting code base..."); $this->clientStatus('initializing', 'getting code base'); $codebase = $this->project_analyzer->getCodebase(); - $this->logInfo("Initializing: Scanning files..."); + $this->logInfo("Initializing: Scanning files ({$this->project_analyzer->threads} Threads)..."); $this->clientStatus('initializing', 'scanning files'); $codebase->scanFiles($this->project_analyzer->threads); $this->logInfo("Initializing: Registering stub files..."); $this->clientStatus('initializing', 'registering stub files'); - $codebase->config->visitStubFiles($codebase); + $codebase->config->visitStubFiles($codebase, $this->project_analyzer->progress); if ($this->textDocument === null) { $this->textDocument = new ServerTextDocument( @@ -288,37 +444,107 @@ function () use ($capabilities) { $serverCapabilities = new ServerCapabilities(); + //The server provides execute command support. $serverCapabilities->executeCommandProvider = new ExecuteCommandOptions(['test']); $textDocumentSyncOptions = new TextDocumentSyncOptions(); + //Open and close notifications are sent to the server. $textDocumentSyncOptions->openClose = true; $saveOptions = new SaveOptions(); + //The client is supposed to include the content on save. $saveOptions->includeText = true; $textDocumentSyncOptions->save = $saveOptions; + /** + * Change notifications are sent to the server. See + * TextDocumentSyncKind.None, TextDocumentSyncKind.Full and + * TextDocumentSyncKind.Incremental. If omitted it defaults to + * TextDocumentSyncKind.None. + */ if ($this->project_analyzer->onchange_line_limit === 0) { + /** + * Documents should not be synced at all. + */ $textDocumentSyncOptions->change = TextDocumentSyncKind::NONE; } else { + /** + * Documents are synced by always sending the full content + * of the document. + */ $textDocumentSyncOptions->change = TextDocumentSyncKind::FULL; } + /** + * Defines how text documents are synced. Is either a detailed structure + * defining each notification or for backwards compatibility the + * TextDocumentSyncKind number. If omitted it defaults to + * `TextDocumentSyncKind.None`. + */ $serverCapabilities->textDocumentSync = $textDocumentSyncOptions; - // Support "Find all symbols" + /** + * The server provides document symbol support. + * Support "Find all symbols" + */ $serverCapabilities->documentSymbolProvider = false; - // Support "Find all symbols in workspace" + /** + * The server provides workspace symbol support. + * Support "Find all symbols in workspace" + */ $serverCapabilities->workspaceSymbolProvider = false; - // Support "Go to definition" + /** + * The server provides goto definition support. + * Support "Go to definition" + */ $serverCapabilities->definitionProvider = true; - // Support "Find all references" + /** + * The server provides find references support. + * Support "Find all references" + */ $serverCapabilities->referencesProvider = false; - // Support "Hover" + /** + * The server provides hover support. + * Support "Hover" + */ $serverCapabilities->hoverProvider = true; - // Support "Completion" - // Support "Code Actions" if we support data + /** + * The server provides completion support. + * Support "Completion" + */ + if ($this->project_analyzer->provide_completion) { + $serverCapabilities->completionProvider = new CompletionOptions(); + /** + * The server provides support to resolve additional + * information for a completion item. + */ + $serverCapabilities->completionProvider->resolveProvider = false; + /** + * Most tools trigger completion request automatically without explicitly + * requesting it using a keyboard shortcut (e.g. Ctrl+Space). Typically they + * do so when the user starts to type an identifier. For example if the user + * types `c` in a JavaScript file code complete will automatically pop up + * present `console` besides others as a completion item. Characters that + * make up identifiers don't need to be listed here. + * + * If code complete should automatically be trigger on characters not being + * valid inside an identifier (for example `.` in JavaScript) list them in + * `triggerCharacters`. + */ + $serverCapabilities->completionProvider->triggerCharacters = ['$', '>', ':',"[", "(", ",", " "]; + } + + /** + * Whether code action supports the `data` property which is + * preserved between a `textDocument/codeAction` and a + * `codeAction/resolve` request. + * + * Support "Code Actions" if we support data + * + * @since LSP 3.16.0 + */ if ($this->clientCapabilities && $this->clientCapabilities->textDocument && $this->clientCapabilities->textDocument->publishDiagnostics && @@ -327,22 +553,19 @@ function () use ($capabilities) { $serverCapabilities->codeActionProvider = true; } - if ($this->project_analyzer->provide_completion) { - $serverCapabilities->completionProvider = new CompletionOptions(); - $serverCapabilities->completionProvider->resolveProvider = false; - $serverCapabilities->completionProvider->triggerCharacters = ['$', '>', ':',"[", "(", ",", " "]; - } - + /** + * The server provides signature help support. + */ $serverCapabilities->signatureHelpProvider = new SignatureHelpOptions(['(', ',']); - // Support global references - $serverCapabilities->xworkspaceReferencesProvider = false; - $serverCapabilities->xdefinitionProvider = false; - $serverCapabilities->dependenciesProvider = false; - $this->logInfo("Initializing: Complete."); $this->clientStatus('initialized'); + /** + * Information about the server. + * + * @since LSP 3.15.0 + */ $initializeResultServerInfo = new InitializeResultServerInfo('Psalm Language Server', PSALM_VERSION); return new InitializeResult($serverCapabilities, $initializeResultServerInfo); @@ -351,8 +574,12 @@ function () use ($capabilities) { } /** - * @psalm-suppress PossiblyUnusedMethod + * The initialized notification is sent from the client to the server after the client received the result of the + * initialize request but before the client is sending any other request or notification to the server. + * The server can use the initialized notification for example to dynamically register capabilities. + * The initialized notification may only be sent once. * + * @psalm-suppress PossiblyUnusedMethod */ public function initialized(): void { @@ -364,16 +591,36 @@ public function initialized(): void $this->clientStatus('running'); } + /** + * Queue Change File Analysis + * @param string $file_path + * @param string $uri + */ public function queueChangeFileAnalysis(string $file_path, string $uri, ?int $version = null): void { $this->doVersionedAnalysis([$file_path => $uri], $version); } + /** + * Queue Open File Analysis + * @param string $file_path + * @param string $uri + */ public function queueOpenFileAnalysis(string $file_path, string $uri, ?int $version = null): void { $this->doVersionedAnalysis([$file_path => $uri], $version); } + /** + * Queue Closed File Analysis + * @param string $file_path + * @param string $uri + */ + public function queueClosedFileAnalysis(string $file_path, string $uri): void + { + $this->doVersionedAnalysis([$file_path => $uri]); + } + /** * Queue Saved File Analysis * @param string $file_path @@ -381,14 +628,18 @@ public function queueOpenFileAnalysis(string $file_path, string $uri, ?int $vers */ public function queueSaveFileAnalysis(string $file_path, string $uri): void { - //Always reanalzye open files because of things changing elsewhere - $this->doVersionedAnalysis( - $this->queueFileAnalysisWithOpenedFiles([$file_path => $this->pathToUri($file_path)]) - ); + $this->queueFileAnalysisWithOpenedFiles([$file_path => $this->pathToUri($file_path)]); } + /** + * Queue File Analysis appending any opened files + * + * This allows for reanalysis of files that have been opened + * @param array $files + */ public function queueFileAnalysisWithOpenedFiles(array $files = []): void { + /** @var array $opened */ $opened = array_reduce( $this->project_analyzer->getCodebase()->file_provider->getOpenFilesPath(), function (array $opened, string $file_path) { @@ -401,33 +652,20 @@ function (array $opened, string $file_path) { $this->doVersionedAnalysis($opened); } - public function queueClosedFileAnalysis(string $file_path, string $uri): void - { - //Always reanalzye open files because of things changing elsewhere - $opened = array_reduce( - $this->project_analyzer->getCodebase()->file_provider->getOpenFilesPath(), - function (array $opened, string $file_path) { - $opened[$file_path] = $this->pathToUri($file_path); - return $opened; - }, - [$file_path => $this->pathToUri($file_path)] - ); - - $this->doVersionedAnalysis($opened); - } - - public function doVersionedAnalysis($all_files_to_analyze, ?int $version = null): void + /** + * Queue File Analysis with optional version + * + * @param array $files + * @param int|null $version + */ + public function doVersionedAnalysis(array $files, ?int $version = null): void { try { - if (empty($all_files_to_analyze)) { + if (empty($files)) { $this->logWarning("No versioned analysis to do."); return; } - - /** @var array */ - $files = $all_files_to_analyze; - $codebase = $this->project_analyzer->getCodebase(); $codebase->reloadFiles( $this->project_analyzer, @@ -441,10 +679,16 @@ public function doVersionedAnalysis($all_files_to_analyze, ?int $version = null) $this->emitVersionedIssues($files, $version); } catch (Throwable $e) { - $this->server->logError((string) $e); + $this->logError((string) $e); } } + /** + * Emit Publish Diagnostics + * + * @param array $files + * @param int|null $version + */ public function emitVersionedIssues(array $files, ?int $version = null): void { $this->logDebug("Perform Analysis", [ @@ -499,7 +743,13 @@ function (IssueData $issue_data): Diagnostic { $diagnostic->code = $issue_data->shortcode; - if ($this->clientCapabilities->textDocument && + /** + * Client supports a codeDescription property + * + * @since LSP 3.16.0 + */ + if ($this->clientCapabilities !== null && + $this->clientCapabilities->textDocument && $this->clientCapabilities->textDocument->publishDiagnostics && $this->clientCapabilities->textDocument->publishDiagnostics->codeDescriptionSupport ) { @@ -511,6 +761,7 @@ function (IssueData $issue_data): Diagnostic { array_filter( $data[$file_path] ?? [], function (IssueData $issue_data) { + //Hide Warnings if ($issue_data->severity === Config::REPORT_INFO && $this->client->clientConfiguration->hideWarnings ) { @@ -527,9 +778,12 @@ function (IssueData $issue_data) { } /** - * 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. + * 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. Clients must not send any notifications other than exit or requests to a server to + * which they have sent a shutdown request. Clients should also wait with sending the exit notification until they + * have received a response from the shutdown request. + * * @psalm-suppress PossiblyUnusedReturnValue */ public function shutdown(): Promise @@ -548,6 +802,8 @@ public function shutdown(): Promise /** * A notification to ask the server to exit its process. + * The server should exit with success code 0 if the shutdown request has been received before; + * otherwise with error code 1. * */ public function exit(): void @@ -587,28 +843,52 @@ public function log(int $type, string $message, array $context = []): void } } + /** + * Log Error message to the client + * + * @param string $message + * @param array $context + */ public function logError(string $message, array $context = []): void { $this->log(MessageType::ERROR, $message, $context); } + /** + * Log Warning message to the client + * + * @param string $message + * @param array $context + */ public function logWarning(string $message, array $context = []): void { $this->log(MessageType::WARNING, $message, $context); } + /** + * Log Info message to the client + * + * @param string $message + * @param array $context + */ public function logInfo(string $message, array $context = []): void { $this->log(MessageType::INFO, $message, $context); } + /** + * Log Debug message to the client + * + * @param string $message + * @param array $context + */ public function logDebug(string $message, array $context = []): void { $this->log(MessageType::LOG, $message, $context); } /** - * Send status message to client. This is the same as sending a log message, + * Send status message to client. This is the same as sending a log message, * except this is meant for parsing by the client to present status updates in a UI. * * @param string $status The log message to send to the client. Should not contain colons `:`. @@ -632,6 +912,7 @@ private function clientStatus(string $status, ?string $additional_info = null): /** * Transforms an absolute file path into a URI as used by the language server protocol. * + * @param string $filepath * @psalm-pure */ public static function pathToUri(string $filepath): string @@ -653,7 +934,7 @@ public static function pathToUri(string $filepath): string /** * Transforms URI into file path * - * + * @param string $uri */ public static function uriToPath(string $uri): string { From cfc5a3ac91dcb589cb4b0fb140c461a1ec0f676d Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Thu, 3 Feb 2022 19:27:08 +0000 Subject: [PATCH 14/78] more fixes --- .../LanguageServer/Server/TextDocument.php | 6 ++++-- .../Internal/LanguageServer/Server/Workspace.php | 14 +++++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php index a04cca6f61c..c5dfe244d1c 100644 --- a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php +++ b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php @@ -331,12 +331,14 @@ public function completion(TextDocumentIdentifier $textDocument, Position $posit [$recent_type, $gap, $offset] = $completion_data; if ($gap === '->' || $gap === '::') { - $snippetSupport = ($this->server->clientCapabilities->textDocument && + $snippetSupport = ($this->server->clientCapabilities && + $this->server->clientCapabilities->textDocument && $this->server->clientCapabilities->textDocument->completion && $this->server->clientCapabilities->textDocument->completion->completionItem && $this->server->clientCapabilities->textDocument->completion->completionItem->snippetSupport) ? true : false; - $completion_items = $this->codebase->getCompletionItemsForClassishThing($recent_type, $gap, $snippetSupport); + $completion_items = + $this->codebase->getCompletionItemsForClassishThing($recent_type, $gap, $snippetSupport); } elseif ($gap === '[') { $completion_items = $this->codebase->getCompletionItemsForArrayKeys($recent_type); } else { diff --git a/src/Psalm/Internal/LanguageServer/Server/Workspace.php b/src/Psalm/Internal/LanguageServer/Server/Workspace.php index bbf1564185f..96b6f60390e 100644 --- a/src/Psalm/Internal/LanguageServer/Server/Workspace.php +++ b/src/Psalm/Internal/LanguageServer/Server/Workspace.php @@ -6,6 +6,7 @@ use Amp\Promise; use Amp\Success; +use InvalidArgumentException; use LanguageServerProtocol\FileChangeType; use LanguageServerProtocol\FileEvent; use Psalm\Codebase; @@ -14,6 +15,7 @@ use Psalm\Internal\LanguageServer\LanguageServer; use Psalm\Internal\Provider\FileReferenceProvider; +use function array_filter; use function array_map; use function in_array; use function realpath; @@ -65,9 +67,15 @@ public function didChangeWatchedFiles(array $changes): void 'workspace/didChangeWatchedFiles' ); - $realFiles = array_map(function (FileEvent $change) { - return LanguageServer::uriToPath($change->uri); - }, $changes); + $realFiles = array_filter( + array_map(function (FileEvent $change) { + try { + return LanguageServer::uriToPath($change->uri); + } catch (InvalidArgumentException $e) { + return null; + } + }, $changes) + ); $composerLockFile = realpath(Composer::getLockFilePath($this->codebase->config->base_dir)); if (in_array($composerLockFile, $realFiles)) { From 20e7316dfab1a5010a733a886ea4eceeedeabc84 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Thu, 3 Feb 2022 23:19:39 +0000 Subject: [PATCH 15/78] remove need for defining lsp elsewhere --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 925a0037cfc..e79b06a4690 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ "composer/xdebug-handler": "^1.1 || ^2.0 || ^3.0", "dnoegel/php-xdg-base-dir": "^0.1.1", "felixfbecker/advanced-json-rpc": "^3.0.3", - "felixfbecker/language-server-protocol": "dev-feature/spec-3.16", + "felixfbecker/language-server-protocol": "dev-feature/spec-3.16 as 1.5.1", "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", "nikic/php-parser": "^4.13", "openlss/lib-array2xml": "^1.0", From a7146b4e3740591738cd3d2f9945fbd83932f0f3 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Thu, 3 Feb 2022 23:21:28 +0000 Subject: [PATCH 16/78] bump --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index e79b06a4690..925a0037cfc 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ "composer/xdebug-handler": "^1.1 || ^2.0 || ^3.0", "dnoegel/php-xdg-base-dir": "^0.1.1", "felixfbecker/advanced-json-rpc": "^3.0.3", - "felixfbecker/language-server-protocol": "dev-feature/spec-3.16 as 1.5.1", + "felixfbecker/language-server-protocol": "dev-feature/spec-3.16", "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", "nikic/php-parser": "^4.13", "openlss/lib-array2xml": "^1.0", From e259d56627a831969f8a96f10bda8cd10fbc0bf6 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Thu, 3 Feb 2022 23:22:56 +0000 Subject: [PATCH 17/78] bump again --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 925a0037cfc..e79b06a4690 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ "composer/xdebug-handler": "^1.1 || ^2.0 || ^3.0", "dnoegel/php-xdg-base-dir": "^0.1.1", "felixfbecker/advanced-json-rpc": "^3.0.3", - "felixfbecker/language-server-protocol": "dev-feature/spec-3.16", + "felixfbecker/language-server-protocol": "dev-feature/spec-3.16 as 1.5.1", "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", "nikic/php-parser": "^4.13", "openlss/lib-array2xml": "^1.0", From a697c79a37605e51d2eb48b00b0b9d4ed90ca79b Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Fri, 4 Feb 2022 04:14:48 +0000 Subject: [PATCH 18/78] pass progress higher up --- .../LanguageServer/LanguageServer.php | 26 +++++++++++++------ .../Internal/LanguageServer/Progress.php | 18 ++++++++++--- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 1b6957392e0..1ace5f3c1bf 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -140,10 +140,13 @@ public function __construct( ProtocolReader $reader, ProtocolWriter $writer, ProjectAnalyzer $project_analyzer, - ClientConfiguration $clientConfiguration + ClientConfiguration $clientConfiguration, + Progress $progress ) { parent::__construct($this, '/'); + $progress->setServer($this); + $this->project_analyzer = $project_analyzer; $this->protocolWriter = $writer; @@ -229,8 +232,6 @@ function (): void { $this->client = new LanguageClient($reader, $writer, $this, $clientConfiguration); - $this->project_analyzer->progress = new Progress($this); - $this->logInfo("Psalm Language Server ".PSALM_VERSION." has started."); } @@ -239,6 +240,7 @@ function (): void { */ public static function run(Config $config, ClientConfiguration $clientConfiguration, string $base_dir): void { + $progress = new Progress(); //no-cache mode does not work in the LSP $providers = new Providers( new FileProvider, @@ -251,7 +253,11 @@ public static function run(Config $config, ClientConfiguration $clientConfigurat $project_analyzer = new ProjectAnalyzer( $config, - $providers + $providers, + null, + [], + 1, + $progress ); if ($config->find_unused_variables) { @@ -283,7 +289,8 @@ public static function run(Config $config, ClientConfiguration $clientConfigurat new ProtocolStreamReader($socket), new ProtocolStreamWriter($socket), $project_analyzer, - $clientConfiguration + $clientConfiguration, + $progress ); Loop::run(); } elseif ($clientConfiguration->TCPServerMode && $clientConfiguration->TCPServerAddress) { @@ -336,7 +343,8 @@ function (): void { $reader, new ProtocolStreamWriter($socket), $project_analyzer, - $clientConfiguration + $clientConfiguration, + $progress ); // Just for safety exit(0); @@ -348,7 +356,8 @@ function (): void { new ProtocolStreamReader($socket), new ProtocolStreamWriter($socket), $project_analyzer, - $clientConfiguration + $clientConfiguration, + $progress ); Loop::run(); } @@ -360,7 +369,8 @@ function (): void { new ProtocolStreamReader(STDIN), new ProtocolStreamWriter(STDOUT), $project_analyzer, - $clientConfiguration + $clientConfiguration, + $progress ); Loop::run(); } diff --git a/src/Psalm/Internal/LanguageServer/Progress.php b/src/Psalm/Internal/LanguageServer/Progress.php index 27eeb4e7efc..c5048f6fedd 100644 --- a/src/Psalm/Internal/LanguageServer/Progress.php +++ b/src/Psalm/Internal/LanguageServer/Progress.php @@ -3,6 +3,7 @@ namespace Psalm\Internal\LanguageServer; use Psalm\Progress\Progress as Base; +use function str_replace; /** * @internal @@ -11,22 +12,31 @@ class Progress extends Base { /** - * @var LanguageServer + * @var LanguageServer|null */ private $server; - public function __construct(LanguageServer $server) + public function __construct() + { + + } + + public function setServer(LanguageServer $server): void { $this->server = $server; } public function debug(string $message): void { - $this->server->logDebug($message); + if ($this->server) { + $this->server->logDebug(str_replace("\n", "", $message)); + } } public function write(string $message): void { - $this->server->logInfo($message); + if ($this->server) { + $this->server->logInfo(str_replace("\n", "", $message)); + } } } From 19a3f4d935ddc8efa29381e3078dfb84f1bbde96 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Fri, 4 Feb 2022 19:49:46 +0000 Subject: [PATCH 19/78] catch TypeParseTreeException --- .../LanguageServer/Server/TextDocument.php | 49 +++++++++++-------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php index c5dfe244d1c..89e7c930d50 100644 --- a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php +++ b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php @@ -24,6 +24,7 @@ use LanguageServerProtocol\VersionedTextDocumentIdentifier; use LanguageServerProtocol\WorkspaceEdit; use Psalm\Codebase; +use Psalm\Exception\TypeParseTreeException; use Psalm\Exception\UnanalyzedFileException; use Psalm\Internal\Analyzer\ProjectAnalyzer; use Psalm\Internal\LanguageServer\LanguageServer; @@ -327,31 +328,37 @@ public function completion(TextDocumentIdentifier $textDocument, Position $posit return new Success(null); } - if ($completion_data) { - [$recent_type, $gap, $offset] = $completion_data; - - if ($gap === '->' || $gap === '::') { - $snippetSupport = ($this->server->clientCapabilities && - $this->server->clientCapabilities->textDocument && - $this->server->clientCapabilities->textDocument->completion && - $this->server->clientCapabilities->textDocument->completion->completionItem && - $this->server->clientCapabilities->textDocument->completion->completionItem->snippetSupport) - ? true : false; - $completion_items = - $this->codebase->getCompletionItemsForClassishThing($recent_type, $gap, $snippetSupport); - } elseif ($gap === '[') { - $completion_items = $this->codebase->getCompletionItemsForArrayKeys($recent_type); + try { + if ($completion_data) { + [$recent_type, $gap, $offset] = $completion_data; + + if ($gap === '->' || $gap === '::') { + $snippetSupport = ($this->server->clientCapabilities && + $this->server->clientCapabilities->textDocument && + $this->server->clientCapabilities->textDocument->completion && + $this->server->clientCapabilities->textDocument->completion->completionItem && + $this->server->clientCapabilities->textDocument->completion->completionItem->snippetSupport) + ? true : false; + $completion_items = + $this->codebase->getCompletionItemsForClassishThing($recent_type, $gap, $snippetSupport); + } elseif ($gap === '[') { + $completion_items = $this->codebase->getCompletionItemsForArrayKeys($recent_type); + } else { + $completion_items = $this->codebase->getCompletionItemsForPartialSymbol( + $recent_type, + $offset, + $file_path + ); + } } else { - $completion_items = $this->codebase->getCompletionItemsForPartialSymbol( - $recent_type, - $offset, - $file_path - ); + $completion_items = $this->codebase->getCompletionItemsForType($type_context); } - } else { - $completion_items = $this->codebase->getCompletionItemsForType($type_context); + } catch (TypeParseTreeException $e) { + $this->server->logError((string) $e); + return new Success(null); } + return new Success(new CompletionList($completion_items, false)); } From 8855bedab60db98f276a944f018285a60b7e97f6 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Mon, 7 Feb 2022 17:12:56 +0000 Subject: [PATCH 20/78] bump --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index e79b06a4690..e6615565631 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ "composer/xdebug-handler": "^1.1 || ^2.0 || ^3.0", "dnoegel/php-xdg-base-dir": "^0.1.1", "felixfbecker/advanced-json-rpc": "^3.0.3", - "felixfbecker/language-server-protocol": "dev-feature/spec-3.16 as 1.5.1", + "felixfbecker/language-server-protocol": "dev-feature/spec-3.16-zobo as 1.5.1", "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", "nikic/php-parser": "^4.13", "openlss/lib-array2xml": "^1.0", From 867a4862f34b4ff3bc85f27b3a5a072aaf4d5bf8 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Mon, 7 Feb 2022 17:50:55 +0000 Subject: [PATCH 21/78] bump --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index e6615565631..e79b06a4690 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ "composer/xdebug-handler": "^1.1 || ^2.0 || ^3.0", "dnoegel/php-xdg-base-dir": "^0.1.1", "felixfbecker/advanced-json-rpc": "^3.0.3", - "felixfbecker/language-server-protocol": "dev-feature/spec-3.16-zobo as 1.5.1", + "felixfbecker/language-server-protocol": "dev-feature/spec-3.16 as 1.5.1", "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", "nikic/php-parser": "^4.13", "openlss/lib-array2xml": "^1.0", From 1376bd8cddcc3115dc8666ec65584981c398c8ce Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Mon, 14 Feb 2022 21:02:05 +0000 Subject: [PATCH 22/78] extend codebase and put all LSP in the extended, cleanup hover outputs --- src/Psalm/Codebase.php | 986 +--------------- .../Internal/Analyzer/ProjectAnalyzer.php | 17 +- src/Psalm/Internal/Cli/LanguageServer.php | 40 + .../LanguageServer/Client/TextDocument.php | 4 + .../LanguageServer/ClientConfiguration.php | 45 +- .../Internal/LanguageServer/Codebase.php | 1043 +++++++++++++++++ .../LanguageServer/LanguageServer.php | 54 +- .../LanguageServer/PHPMarkdownContent.php | 19 + .../Internal/LanguageServer/Reference.php | 34 + .../LanguageServer/Server/TextDocument.php | 52 +- .../LanguageServer/Server/Workspace.php | 9 +- src/Psalm/Storage/ClassConstantStorage.php | 31 + src/Psalm/Storage/FunctionLikeStorage.php | 48 +- 13 files changed, 1335 insertions(+), 1047 deletions(-) create mode 100644 src/Psalm/Internal/LanguageServer/Codebase.php create mode 100644 src/Psalm/Internal/LanguageServer/PHPMarkdownContent.php create mode 100644 src/Psalm/Internal/LanguageServer/Reference.php diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index 5dc7bb65b81..e1c689cf7f1 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -2,34 +2,15 @@ namespace Psalm; -use Exception; -use InvalidArgumentException; -use LanguageServerProtocol\Command; -use LanguageServerProtocol\CompletionItem; -use LanguageServerProtocol\CompletionItemKind; -use LanguageServerProtocol\InsertTextFormat; -use LanguageServerProtocol\ParameterInformation; -use LanguageServerProtocol\Position; -use LanguageServerProtocol\Range; -use LanguageServerProtocol\SignatureInformation; -use LanguageServerProtocol\TextEdit; use PhpParser; use PhpParser\Node\Arg; use Psalm\CodeLocation; -use Psalm\CodeLocation\Raw; -use Psalm\Exception\UnanalyzedFileException; use Psalm\Exception\UnpopulatedClasslikeException; -use Psalm\Internal\Analyzer\FunctionLikeAnalyzer; -use Psalm\Internal\Analyzer\NamespaceAnalyzer; -use Psalm\Internal\Analyzer\ProjectAnalyzer; use Psalm\Internal\Analyzer\Statements\Block\ForeachAnalyzer; -use Psalm\Internal\Analyzer\Statements\Expression\Fetch\ConstFetchAnalyzer; -use Psalm\Internal\Analyzer\Statements\Expression\Fetch\VariableFetchAnalyzer; use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Codebase\Analyzer; use Psalm\Internal\Codebase\ClassLikes; use Psalm\Internal\Codebase\Functions; -use Psalm\Internal\Codebase\InternalCallMapHandler; use Psalm\Internal\Codebase\Methods; use Psalm\Internal\Codebase\Populator; use Psalm\Internal\Codebase\Properties; @@ -55,40 +36,17 @@ use Psalm\Storage\FunctionStorage; use Psalm\Storage\MethodStorage; use Psalm\Type\Atomic; -use Psalm\Type\Atomic\TBool; -use Psalm\Type\Atomic\TClassConstant; -use Psalm\Type\Atomic\TKeyedArray; -use Psalm\Type\Atomic\TLiteralInt; -use Psalm\Type\Atomic\TLiteralString; -use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\TaintKindGroup; use Psalm\Type\Union; -use ReflectionProperty; use ReflectionType; use UnexpectedValueException; -use function array_combine; use function array_merge; -use function array_pop; -use function array_reverse; -use function count; -use function dirname; -use function error_log; use function explode; -use function implode; -use function in_array; -use function is_numeric; use function is_string; -use function krsort; -use function ksort; -use function preg_match; -use function preg_replace; -use function strlen; use function strpos; -use function strrpos; use function strtolower; use function substr; -use function substr_count; use const PHP_MAJOR_VERSION; use const PHP_MINOR_VERSION; @@ -395,7 +353,7 @@ public function __construct( $this->loadAnalyzer(); } - private function loadAnalyzer(): void + protected function loadAnalyzer(): void { $this->analyzer = new Analyzer( $this->config, @@ -405,72 +363,6 @@ private function loadAnalyzer(): void ); } - /** - * @param array $candidate_files - * - */ - public function reloadFiles(ProjectAnalyzer $project_analyzer, array $candidate_files, bool $force = false): void - { - $this->loadAnalyzer(); - - if ($force) { - FileReferenceProvider::clearCache(); - } - - $this->file_reference_provider->loadReferenceCache($force); - - FunctionLikeAnalyzer::clearCache(); - - if ($force || !$this->statements_provider->parser_cache_provider) { - $diff_files = $candidate_files; - } else { - $diff_files = []; - - $parser_cache_provider = $this->statements_provider->parser_cache_provider; - - foreach ($candidate_files as $candidate_file_path) { - if ($parser_cache_provider->loadExistingFileContentsFromCache($candidate_file_path) - !== $this->file_provider->getContents($candidate_file_path) - ) { - $diff_files[] = $candidate_file_path; - } - } - } - - $referenced_files = $project_analyzer->getReferencedFilesFromDiff($diff_files, false); - - foreach ($diff_files as $diff_file_path) { - $this->invalidateInformationForFile($diff_file_path); - } - - foreach ($referenced_files as $referenced_file_path) { - if (in_array($referenced_file_path, $diff_files, true)) { - continue; - } - - $file_storage = $this->file_storage_provider->get($referenced_file_path); - - foreach ($file_storage->classlikes_in_file as $fq_classlike_name) { - $this->classlike_storage_provider->remove($fq_classlike_name); - $this->classlikes->removeClassLike($fq_classlike_name); - } - - $this->file_storage_provider->remove($referenced_file_path); - $this->scanner->removeFile($referenced_file_path); - } - - $referenced_files = array_combine($referenced_files, $referenced_files); - - $this->scanner->addFilesToDeepScan($referenced_files); - $this->addFilesToAnalyze(array_combine($candidate_files, $candidate_files)); - - $this->scanner->scanFiles($this->classlikes); - - $this->file_reference_provider->updateReferenceCache($this, $referenced_files); - - $this->populator->populateCodebase(); - } - public function enterServerMode(): void { $this->server_mode = true; @@ -1006,882 +898,6 @@ public function getFunctionStorageForSymbol(string $file_path, string $symbol): return $this->functions->getStorage(null, $function_id); } - /** - * @param string $file_path - * @param string $symbol - * @return array{ type: string, description?: string|null}|null - */ - public function getSymbolInformation(string $file_path, string $symbol): ?array - { - if (is_numeric($symbol[0])) { - return ['type' => preg_replace('/^[^:]*:/', '', $symbol)]; - } - - if (strpos($symbol, '::')) { - if (strpos($symbol, '()')) { - $symbol = substr($symbol, 0, -2); - - /** @psalm-suppress ArgumentTypeCoercion */ - $method_id = new MethodIdentifier(...explode('::', $symbol)); - - $declaring_method_id = $this->methods->getDeclaringMethodId($method_id); - - if (!$declaring_method_id) { - return null; - } - - $storage = $this->methods->getStorage($declaring_method_id); - - return [ - 'type' => 'getSignature(true), - 'description' => $storage->description, - ]; - } - - [, $symbol_name] = explode('::', $symbol); - - if (strpos($symbol, '$') !== false) { - $storage = $this->properties->getStorage($symbol); - - return [ - 'type' => 'getInfo() . ' ' . $symbol_name, - 'description' => $storage->description, - ]; - } - - [$fq_classlike_name, $const_name] = explode('::', $symbol); - - $class_constants = $this->classlikes->getConstantsForClass( - $fq_classlike_name, - ReflectionProperty::IS_PRIVATE - ); - - if (!isset($class_constants[$const_name])) { - return null; - } - - return [ - 'type' => ' $class_constants[$const_name]->description, - ]; - } - - if (strpos($symbol, '()')) { - $function_id = strtolower(substr($symbol, 0, -2)); - $file_storage = $this->file_storage_provider->get($file_path); - - if (isset($file_storage->functions[$function_id])) { - $function_storage = $file_storage->functions[$function_id]; - - return [ - 'type' => 'getSignature(true), - 'description' => $function_storage->description, - ]; - } - - if (!$function_id) { - return null; - } - - $function = $this->functions->getStorage(null, $function_id); - return [ - 'type' => 'getSignature(true), - 'description' => $function->description, - ]; - } - - if (strpos($symbol, '$') === 0) { - $type = VariableFetchAnalyzer::getGlobalType($symbol); - if (!$type->isMixed()) { - return ['type' => 'classlike_storage_provider->get($symbol); - return [ - 'type' => 'abstract ? 'abstract ' : '') . 'class ' . $storage->name, - 'description' => $storage->description, - ]; - } catch (InvalidArgumentException $e) { - //continue on as normal - } - - if (strpos($symbol, '\\')) { - $const_name_parts = explode('\\', $symbol); - $const_name = array_pop($const_name_parts); - $namespace_name = implode('\\', $const_name_parts); - - $namespace_constants = NamespaceAnalyzer::getConstantsForNamespace( - $namespace_name, - ReflectionProperty::IS_PUBLIC - ); - if (isset($namespace_constants[$const_name])) { - $type = $namespace_constants[$const_name]; - return ['type' => 'file_storage_provider->get($file_path); - if (isset($file_storage->constants[$symbol])) { - return ['type' => 'constants[$symbol]]; - } - $constant = ConstFetchAnalyzer::getGlobalConstType($this, $symbol, $symbol); - - if ($constant) { - return ['type' => 'getFileContents($file_path); - - return new Raw( - $file_contents, - $file_path, - $this->config->shortenFileName($file_path), - (int) $symbol_parts[0], - (int) $symbol_parts[1] - ); - } - - try { - if (strpos($symbol, '::')) { - if (strpos($symbol, '()')) { - $symbol = substr($symbol, 0, -2); - - /** @psalm-suppress ArgumentTypeCoercion */ - $method_id = new MethodIdentifier(...explode('::', $symbol)); - - $declaring_method_id = $this->methods->getDeclaringMethodId($method_id); - - if (!$declaring_method_id) { - return null; - } - - $storage = $this->methods->getStorage($declaring_method_id); - - return $storage->location; - } - - if (strpos($symbol, '$') !== false) { - $storage = $this->properties->getStorage($symbol); - - return $storage->location; - } - - [$fq_classlike_name, $const_name] = explode('::', $symbol); - - $class_constants = $this->classlikes->getConstantsForClass( - $fq_classlike_name, - ReflectionProperty::IS_PRIVATE - ); - - if (!isset($class_constants[$const_name])) { - return null; - } - - return $class_constants[$const_name]->location; - } - - if (strpos($symbol, '()')) { - $file_storage = $this->file_storage_provider->get($file_path); - - $function_id = strtolower(substr($symbol, 0, -2)); - - if (isset($file_storage->functions[$function_id])) { - return $file_storage->functions[$function_id]->location; - } - - if (!$function_id) { - return null; - } - - return $this->functions->getStorage(null, $function_id)->location; - } - - return $this->classlike_storage_provider->get($symbol)->location; - } catch (UnexpectedValueException $e) { - error_log($e->getMessage()); - - return null; - } catch (InvalidArgumentException $e) { - return null; - } - } - - /** - * @return array{0: string, 1: Range}|null - */ - public function getReferenceAtPosition(string $file_path, Position $position): ?array - { - $is_open = $this->file_provider->isOpen($file_path); - - if (!$is_open) { - throw new UnanalyzedFileException($file_path . ' is not open'); - } - - $file_contents = $this->getFileContents($file_path); - - $offset = $position->toOffset($file_contents); - - [$reference_map, $type_map] = $this->analyzer->getMapsForFile($file_path); - - $reference = null; - - if (!$reference_map && !$type_map) { - return null; - } - - $reference_start_pos = null; - $reference_end_pos = null; - - ksort($reference_map); - - foreach ($reference_map as $start_pos => [$end_pos, $possible_reference]) { - if ($offset < $start_pos) { - break; - } - - if ($offset > $end_pos) { - continue; - } - $reference_start_pos = $start_pos; - $reference_end_pos = $end_pos; - $reference = $possible_reference; - } - - if ($reference === null || $reference_start_pos === null || $reference_end_pos === null) { - return null; - } - - $range = new Range( - self::getPositionFromOffset($reference_start_pos, $file_contents), - self::getPositionFromOffset($reference_end_pos, $file_contents) - ); - - return [$reference, $range]; - } - - /** - * @return array{0: non-empty-string, 1: int, 2: Range}|null - */ - public function getFunctionArgumentAtPosition(string $file_path, Position $position): ?array - { - $is_open = $this->file_provider->isOpen($file_path); - - if (!$is_open) { - throw new UnanalyzedFileException($file_path . ' is not open'); - } - - $file_contents = $this->getFileContents($file_path); - - $offset = $position->toOffset($file_contents); - - [, , $argument_map] = $this->analyzer->getMapsForFile($file_path); - - $reference = null; - $argument_number = null; - - if (!$argument_map) { - return null; - } - - $start_pos = null; - $end_pos = null; - - ksort($argument_map); - - foreach ($argument_map as $start_pos => [$end_pos, $possible_reference, $possible_argument_number]) { - if ($offset < $start_pos) { - break; - } - - if ($offset > $end_pos) { - continue; - } - - $reference = $possible_reference; - $argument_number = $possible_argument_number; - } - - if ($reference === null || $start_pos === null || $end_pos === null || $argument_number === null) { - return null; - } - - $range = new Range( - self::getPositionFromOffset($start_pos, $file_contents), - self::getPositionFromOffset($end_pos, $file_contents) - ); - - return [$reference, $argument_number, $range]; - } - - /** - * @param non-empty-string $function_symbol - */ - public function getSignatureInformation( - string $function_symbol, - string $file_path = null - ): ?SignatureInformation { - $signature_label = ''; - $signature_documentation = null; - if (strpos($function_symbol, '::') !== false) { - /** @psalm-suppress ArgumentTypeCoercion */ - $method_id = new MethodIdentifier(...explode('::', $function_symbol)); - - $declaring_method_id = $this->methods->getDeclaringMethodId($method_id); - - if ($declaring_method_id === null) { - return null; - } - - $method_storage = $this->methods->getStorage($declaring_method_id); - $params = $method_storage->params; - $signature_label = $method_storage->cased_name; - $signature_documentation = $method_storage->description; - } else { - try { - if ($file_path) { - $function_storage = $this->functions->getStorage( - null, - strtolower($function_symbol), - dirname($file_path), - $file_path - ); - } else { - $function_storage = $this->functions->getStorage(null, strtolower($function_symbol)); - } - $params = $function_storage->params; - $signature_label = $function_storage->cased_name; - $signature_documentation = $function_storage->description; - } catch (Exception $exception) { - if (InternalCallMapHandler::inCallMap($function_symbol)) { - $callables = InternalCallMapHandler::getCallablesFromCallMap($function_symbol); - - if (!$callables || !$callables[0]->params) { - throw $exception; - } - - $params = $callables[0]->params; - } else { - throw $exception; - } - } - } - - $signature_label .= '('; - $parameters = []; - - foreach ($params as $i => $param) { - $parameter_label = ($param->type ?: 'mixed') . ' $' . $param->name; - $parameters[] = new ParameterInformation( - [ - strlen($signature_label), - strlen($signature_label) + strlen($parameter_label), - ], - $param->description ?? null - ); - - $signature_label .= $parameter_label; - - if ($i < (count($params) - 1)) { - $signature_label .= ', '; - } - } - - $signature_label .= ')'; - - return new SignatureInformation( - $signature_label, - $parameters, - $signature_documentation - ); - } - - /** - * @return array{0: string, 1: '->'|'::'|'['|'symbol', 2: int}|null - */ - public function getCompletionDataAtPosition(string $file_path, Position $position): ?array - { - $is_open = $this->file_provider->isOpen($file_path); - - if (!$is_open) { - throw new UnanalyzedFileException($file_path . ' is not open'); - } - - $file_contents = $this->getFileContents($file_path); - - $offset = $position->toOffset($file_contents); - - [$reference_map, $type_map] = $this->analyzer->getMapsForFile($file_path); - - if (!$reference_map && !$type_map) { - return null; - } - - krsort($type_map); - - foreach ($type_map as $start_pos => [$end_pos_excluding_whitespace, $possible_type]) { - if ($offset < $start_pos) { - continue; - } - - $num_whitespace_bytes = preg_match('/\G\s+/', $file_contents, $matches, 0, $end_pos_excluding_whitespace) - ? strlen($matches[0]) - : 0; - $end_pos = $end_pos_excluding_whitespace + $num_whitespace_bytes; - - if ($offset - $end_pos === 1) { - $candidate_gap = substr($file_contents, $end_pos, 1); - - if ($candidate_gap === '[') { - $gap = $candidate_gap; - $recent_type = $possible_type; - - if ($recent_type === 'mixed') { - return null; - } - - return [$recent_type, $gap, $offset]; - } - } - - if ($offset - $end_pos === 2 || $offset - $end_pos === 3) { - $candidate_gap = substr($file_contents, $end_pos, 2); - - if ($candidate_gap === '->' || $candidate_gap === '::') { - $gap = $candidate_gap; - $recent_type = $possible_type; - - if ($recent_type === 'mixed') { - return null; - } - - return [$recent_type, $gap, $offset]; - } - } - } - - foreach ($reference_map as $start_pos => [$end_pos, $possible_reference]) { - if ($offset < $start_pos) { - continue; - } - // If the reference precedes a "::" then treat it as a class reference. - if ($offset - $end_pos === 2 && substr($file_contents, $end_pos, 2) === '::') { - return [$possible_reference, '::', $offset]; - } - - // Only continue for references that are partial / don't exist. - if ($possible_reference[0] !== '*') { - continue; - } - - if ($offset - $end_pos === 0) { - $recent_type = $possible_reference; - - return [$recent_type, 'symbol', $offset]; - } - } - - return null; - } - - public function getTypeContextAtPosition(string $file_path, Position $position): ?Union - { - $file_contents = $this->getFileContents($file_path); - $offset = $position->toOffset($file_contents); - - [$reference_map, $type_map, $argument_map] = $this->analyzer->getMapsForFile($file_path); - if (!$reference_map && !$type_map && !$argument_map) { - return null; - } - foreach ($argument_map as $start_pos => [$end_pos, $function, $argument_num]) { - if ($offset < $start_pos || $offset > $end_pos) { - continue; - } - // First parameter to a function-like - $function_storage = $this->getFunctionStorageForSymbol($file_path, $function . '()'); - if (!$function_storage || !$function_storage->params || !isset($function_storage->params[$argument_num])) { - return null; - } - - return $function_storage->params[$argument_num]->type; - } - - return null; - } - - /** - * @return list - */ - public function getCompletionItemsForClassishThing( - string $type_string, - string $gap, - bool $snippets_supported = false - ): array { - $completion_items = []; - - $type = Type::parseString($type_string); - - foreach ($type->getAtomicTypes() as $atomic_type) { - if ($atomic_type instanceof TNamedObject) { - try { - $class_storage = $this->classlike_storage_provider->get($atomic_type->value); - - foreach ($class_storage->appearing_method_ids as $declaring_method_id) { - $method_storage = $this->methods->getStorage($declaring_method_id); - - if ($method_storage->is_static || $gap === '->') { - $completion_item = new CompletionItem( - $method_storage->cased_name, - CompletionItemKind::METHOD, - (string)$method_storage, - $method_storage->description, - (string)$method_storage->visibility, - $method_storage->cased_name, - $method_storage->cased_name, - null, - null, - new Command('Trigger parameter hints', 'editor.action.triggerParameterHints'), - null, - 2 - ); - - if ($snippets_supported && count($method_storage->params) > 0) { - $completion_item->insertText .= '($0)'; - $completion_item->insertTextFormat = InsertTextFormat::SNIPPET; - } - - $completion_items[] = $completion_item; - } - } - - foreach ($class_storage->declaring_property_ids as $property_name => $declaring_class) { - $property_storage = $this->properties->getStorage( - $declaring_class . '::$' . $property_name - ); - - if ($property_storage->is_static || $gap === '->') { - $completion_items[] = new CompletionItem( - '$' . $property_name, - CompletionItemKind::PROPERTY, - $property_storage->getInfo(), - $property_storage->description, - (string)$property_storage->visibility, - $property_name, - ($gap === '::' ? '$' : '') . $property_name - ); - } - } - - foreach ($class_storage->constants as $const_name => $const) { - $completion_items[] = new CompletionItem( - $const_name, - CompletionItemKind::VARIABLE, - 'const ' . $const_name, - $const->description, - null, - $const_name, - $const_name - ); - } - } catch (Exception $e) { - error_log($e->getMessage()); - continue; - } - } - } - - return $completion_items; - } - - /** - * @return list - */ - public function getCompletionItemsForPartialSymbol( - string $type_string, - int $offset, - string $file_path - ): array { - $fq_suggestion = false; - - if (($type_string[1] ?? '') === '\\') { - $fq_suggestion = true; - } - - $matching_classlike_names = $this->classlikes->getMatchingClassLikeNames($type_string); - - $completion_items = []; - - $file_storage = $this->file_storage_provider->get($file_path); - - $aliases = null; - - foreach ($file_storage->classlikes_in_file as $fq_class_name => $_) { - try { - $class_storage = $this->classlike_storage_provider->get($fq_class_name); - } catch (Exception $e) { - continue; - } - - if (!$class_storage->stmt_location) { - continue; - } - - if ($offset > $class_storage->stmt_location->raw_file_start - && $offset < $class_storage->stmt_location->raw_file_end - ) { - $aliases = $class_storage->aliases; - break; - } - } - - if (!$aliases) { - foreach ($file_storage->namespace_aliases as $namespace_start => $namespace_aliases) { - if ($namespace_start < $offset) { - $aliases = $namespace_aliases; - break; - } - } - - if (!$aliases) { - $aliases = $file_storage->aliases; - } - } - - foreach ($matching_classlike_names as $fq_class_name) { - $extra_edits = []; - - $insertion_text = Type::getStringFromFQCLN( - $fq_class_name, - $aliases && $aliases->namespace ? $aliases->namespace : null, - $aliases->uses_flipped ?? [], - null - ); - - if ($aliases - && !$fq_suggestion - && $aliases->namespace - && $insertion_text === '\\' . $fq_class_name - && $aliases->namespace_first_stmt_start - ) { - $file_contents = $this->getFileContents($file_path); - - $class_name = preg_replace('/^.*\\\/', '', $fq_class_name); - - if ($aliases->uses_end) { - $position = self::getPositionFromOffset($aliases->uses_end, $file_contents); - $extra_edits[] = new TextEdit( - new Range( - $position, - $position - ), - "\n" . 'use ' . $fq_class_name . ';' - ); - } else { - $position = self::getPositionFromOffset($aliases->namespace_first_stmt_start, $file_contents); - $extra_edits[] = new TextEdit( - new Range( - $position, - $position - ), - 'use ' . $fq_class_name . ';' . "\n" . "\n" - ); - } - - $insertion_text = $class_name; - } - - try { - $class_storage = $this->classlike_storage_provider->get($fq_class_name); - $description = $class_storage->description; - } catch (Exception $e) { - $description = null; - } - - $completion_items[] = new CompletionItem( - $fq_class_name, - CompletionItemKind::CLASS_, - null, - $description, - null, - $fq_class_name, - $insertion_text, - null, - $extra_edits - ); - } - - $functions = $this->functions->getMatchingFunctionNames($type_string, $offset, $file_path, $this); - - $namespace_map = []; - if ($aliases) { - $namespace_map += $aliases->uses_flipped; - if ($aliases->namespace) { - $namespace_map[$aliases->namespace] = ''; - } - } - - // Sort the map by longest first, so we replace most specific - // used namespaces first. - ksort($namespace_map); - $namespace_map = array_reverse($namespace_map); - - foreach ($functions as $function_lowercase => $function) { - // Transform FQFN relative to all uses namespaces - $function_name = $function->cased_name; - if (!$function_name) { - continue; - } - $in_namespace_map = false; - foreach ($namespace_map as $namespace_name => $namespace_alias) { - if (strpos($function_lowercase, $namespace_name . '\\') === 0) { - $function_name = $namespace_alias . '\\' . substr($function_name, strlen($namespace_name) + 1); - $in_namespace_map = true; - } - } - // If the function is not use'd, and it's not a global function - // prepend it with a backslash. - if (!$in_namespace_map && strpos($function_name, '\\') !== false) { - $function_name = '\\' . $function_name; - } - $completion_items[] = new CompletionItem( - $function_name, - CompletionItemKind::FUNCTION, - $function->getSignature(false), - $function->description, - null, - $function_name, - $function_name . (count($function->params) !== 0 ? '($0)' : '()'), - null, - null, - new Command('Trigger parameter hints', 'editor.action.triggerParameterHints'), - null, - 2 - ); - } - - return $completion_items; - } - - /** - * @return list - */ - public function getCompletionItemsForType(Union $type): array - { - $completion_items = []; - foreach ($type->getAtomicTypes() as $atomic_type) { - if ($atomic_type instanceof TBool) { - $bools = (string) $atomic_type === 'bool' ? ['true', 'false'] : [(string) $atomic_type]; - foreach ($bools as $property_name) { - $completion_items[] = new CompletionItem( - $property_name, - CompletionItemKind::VALUE, - 'bool', - null, - null, - null, - $property_name - ); - } - } elseif ($atomic_type instanceof TLiteralString) { - $completion_items[] = new CompletionItem( - $atomic_type->value, - CompletionItemKind::VALUE, - $atomic_type->getId(), - null, - null, - null, - "'$atomic_type->value'" - ); - } elseif ($atomic_type instanceof TLiteralInt) { - $completion_items[] = new CompletionItem( - (string) $atomic_type->value, - CompletionItemKind::VALUE, - $atomic_type->getId(), - null, - null, - null, - (string) $atomic_type->value - ); - } elseif ($atomic_type instanceof TClassConstant) { - $const = $atomic_type->fq_classlike_name . '::' . $atomic_type->const_name; - $completion_items[] = new CompletionItem( - $const, - CompletionItemKind::VALUE, - $atomic_type->getId(), - null, - null, - null, - $const - ); - } - } - return $completion_items; - } - - /** - * @return list - */ - public function getCompletionItemsForArrayKeys( - string $type_string - ): array { - $completion_items = []; - $type = Type::parseString($type_string); - foreach ($type->getAtomicTypes() as $atomic_type) { - if ($atomic_type instanceof TKeyedArray) { - foreach ($atomic_type->properties as $property_name => $property) { - $completion_items[] = new CompletionItem( - (string) $property_name, - CompletionItemKind::PROPERTY, - (string) $property, - null, - null, - null, - "'$property_name'" - ); - } - } - } - return $completion_items; - } - - private static function getPositionFromOffset(int $offset, string $file_contents): Position - { - $file_contents = substr($file_contents, 0, $offset); - - $offsetLength = $offset - strlen($file_contents); - - //PHP 8.0: Argument #3 ($offset) must be contained in argument #1 ($haystack) - if (($textlen = strlen($file_contents)) < $offsetLength) { - $offsetLength = $textlen; - } - - $before_newline_count = strrpos($file_contents, "\n", $offsetLength); - - return new Position( - substr_count($file_contents, "\n"), - $offset - (int)$before_newline_count - 1 - ); - } - - public function addTemporaryFileChanges(string $file_path, string $new_content, ?int $version = null): void - { - $this->file_provider->addTemporaryFileChanges($file_path, $new_content, $version); - } - - public function removeTemporaryFileChanges(string $file_path): void - { - $this->file_provider->removeTemporaryFileChanges($file_path); - } - /** * Checks if type is a subtype of other * diff --git a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php index 9c0557cdb13..30f925e39bc 100644 --- a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php @@ -261,12 +261,21 @@ public function __construct( ?ReportOptions $stdout_report_options = null, array $generated_report_options = [], int $threads = 1, - ?Progress $progress = null + ?Progress $progress = null, + ?Codebase $codebase = null ) { if ($progress === null) { $progress = new VoidProgress(); } + if ($codebase === null) { + $codebase = new Codebase( + $config, + $providers, + $progress + ); + } + $this->parser_cache_provider = $providers->parser_cache_provider; $this->project_cache_provider = $providers->project_cache_provider; $this->file_provider = $providers->file_provider; @@ -279,11 +288,7 @@ public function __construct( $this->clearCacheDirectoryIfConfigOrComposerLockfileChanged(); - $this->codebase = new Codebase( - $config, - $providers, - $progress - ); + $this->codebase = $codebase; $this->stdout_report_options = $stdout_report_options; $this->generated_report_options = $generated_report_options; diff --git a/src/Psalm/Internal/Cli/LanguageServer.php b/src/Psalm/Internal/Cli/LanguageServer.php index f32f859e99d..e990e83b6e6 100644 --- a/src/Psalm/Internal/Cli/LanguageServer.php +++ b/src/Psalm/Internal/Cli/LanguageServer.php @@ -76,6 +76,11 @@ public static function run(array $argv): void 'tcp-server', 'disable-on-change::', 'enable-autocomplete::', + 'enable-code-actions::', + 'enable-provide-diagnostics::', + 'enable-provide-hover::', + 'enable-provide-signature-help::', + 'enable-provide-definition::', 'use-extended-diagnostic-codes', 'verbose' ]; @@ -176,9 +181,24 @@ function (string $arg) use ($valid_long_options): void { 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-code-actions[=BOOL] + Enables or disables code actions. Default is true. + + --enable-provide-diagnostics[=BOOL] + Enables or disables providing diagnostics. Default is true. + --enable-autocomplete[=BOOL] Enables or disables autocomplete on methods and properties. Default is true. + --enable-provide-hover[=BOOL] + Enables or disables providing hover. Default is true. + + --enable-provide-signature-help[=BOOL] + Enables or disables providing signature help. Default is true. + + --enable-provide-definition[=BOOL] + Enables or disables providing definition. Default is true. + --use-extended-diagnostic-codes (DEPRECATED) Enables sending help uri links with the code in diagnostic messages. @@ -281,6 +301,26 @@ function () use ($current_dir, $options, $vendor_dir): ?\Composer\Autoload\Class $clientConfiguration->onchangeLineLimit = (int) $options['disable-on-change']; } + $clientConfiguration->provideDefinition = !isset($options['enable-provide-definition']) + || !is_string($options['enable-provide-definition']) + || strtolower($options['enable-provide-definition']) !== 'false'; + + $clientConfiguration->provideSignatureHelp = !isset($options['enable-provide-signature-help']) + || !is_string($options['enable-provide-signature-help']) + || strtolower($options['enable-provide-signature-help']) !== 'false'; + + $clientConfiguration->provideHover = !isset($options['enable-provide-hover']) + || !is_string($options['enable-provide-hover']) + || strtolower($options['enable-provide-hover']) !== 'false'; + + $clientConfiguration->provideDiagnostics = !isset($options['enable-provide-diagnostics']) + || !is_string($options['enable-provide-diagnostics']) + || strtolower($options['enable-provide-diagnostics']) !== 'false'; + + $clientConfiguration->provideCodeActions = !isset($options['enable-code-actions']) + || !is_string($options['enable-code-actions']) + || strtolower($options['enable-code-actions']) !== 'false'; + $clientConfiguration->provideCompletion = !isset($options['enable-autocomplete']) || !is_string($options['enable-autocomplete']) || strtolower($options['enable-autocomplete']) !== 'false'; diff --git a/src/Psalm/Internal/LanguageServer/Client/TextDocument.php b/src/Psalm/Internal/LanguageServer/Client/TextDocument.php index 9683ed65181..d35646f5b6c 100644 --- a/src/Psalm/Internal/LanguageServer/Client/TextDocument.php +++ b/src/Psalm/Internal/LanguageServer/Client/TextDocument.php @@ -43,6 +43,10 @@ public function __construct(ClientHandler $handler, JsonMapper $mapper, Language */ public function publishDiagnostics(string $uri, array $diagnostics, ?int $version = null): void { + if (!$this->server->client->clientConfiguration->provideDiagnostics) { + return; + } + $this->server->logDebug("textDocument/publishDiagnostics"); $this->handler->notify('textDocument/publishDiagnostics', [ diff --git a/src/Psalm/Internal/LanguageServer/ClientConfiguration.php b/src/Psalm/Internal/LanguageServer/ClientConfiguration.php index fbbf2e41de2..8e577483456 100644 --- a/src/Psalm/Internal/LanguageServer/ClientConfiguration.php +++ b/src/Psalm/Internal/LanguageServer/ClientConfiguration.php @@ -37,6 +37,41 @@ class ClientConfiguration */ public $provideCompletion; + /** + * Provide GoTo Definitions or not + * + * @var bool|null + */ + public $provideDefinition; + + /** + * Provide Hover Requests or not + * + * @var bool|null + */ + public $provideHover; + + /** + * Provide Signature Help or not + * + * @var bool|null + */ + public $provideSignatureHelp; + + /** + * Provide Code Actions or not + * + * @var bool|null + */ + public $provideCodeActions; + + /** + * Provide Diagnostics or not + * + * @var bool|null + */ + public $provideDiagnostics; + /** * Provide Completion or not * @@ -71,9 +106,17 @@ class ClientConfiguration public function __construct( bool $hideWarnings = null, - bool $provideCompletion = null + bool $provideCompletion = null, + bool $provideDefinition = null, + bool $provideHover = null, + bool $provideSignatureHelp = null, + bool $provideCodeActions = null, ) { $this->hideWarnings = $hideWarnings; $this->provideCompletion = $provideCompletion; + $this->provideDefinition = $provideDefinition; + $this->provideHover = $provideHover; + $this->provideSignatureHelp = $provideSignatureHelp; + $this->provideCodeActions = $provideCodeActions; } } diff --git a/src/Psalm/Internal/LanguageServer/Codebase.php b/src/Psalm/Internal/LanguageServer/Codebase.php new file mode 100644 index 00000000000..cf9d03c064b --- /dev/null +++ b/src/Psalm/Internal/LanguageServer/Codebase.php @@ -0,0 +1,1043 @@ +file_provider->isOpen($file_path); + + if (!$is_open) { + throw new UnanalyzedFileException($file_path . ' is not open'); + } + + $file_contents = $this->getFileContents($file_path); + + $offset = $position->toOffset($file_contents); + + [$reference_map, $type_map] = $this->analyzer->getMapsForFile($file_path); + + $symbol = null; + + if (!$reference_map && !$type_map) { + return null; + } + + $reference_start_pos = null; + $reference_end_pos = null; + + ksort($reference_map); + + foreach ($reference_map as $start_pos => [$end_pos, $possible_reference]) { + if ($offset < $start_pos) { + break; + } + + if ($offset > $end_pos) { + continue; + } + $reference_start_pos = $start_pos; + $reference_end_pos = $end_pos; + $symbol = $possible_reference; + } + + if ($symbol === null || $reference_start_pos === null || $reference_end_pos === null) { + return null; + } + + $range = new Range( + self::getPositionFromOffset($reference_start_pos, $file_contents), + self::getPositionFromOffset($reference_end_pos, $file_contents) + ); + + return new Reference($file_path, $symbol, $range); + } + + /** + * Get Markup content from Reference + * + * @param Reference $reference + */ + public function getMarkupContentForSymbol(Reference $reference): ?PHPMarkdownContent + { + if (is_numeric($reference->symbol[0])) { + return new PHPMarkdownContent( + '?1', + preg_replace('/^[^:]*:/', '', $reference->symbol) + ); + } + + //Class + if (strpos($reference->symbol, '::')) { + //Class Method + if (strpos($reference->symbol, '()')) { + $symbol = substr($reference->symbol, 0, -2); + + /** @psalm-suppress ArgumentTypeCoercion */ + $method_id = new MethodIdentifier(...explode('::', $symbol)); + + $declaring_method_id = $this->methods->getDeclaringMethodId($method_id); + + if (!$declaring_method_id) { + return null; + } + + $storage = $this->methods->getStorage($declaring_method_id); + + return new PHPMarkdownContent( + "{$storage->defining_fqcln}::{$storage->cased_name}", + $storage->getHoverMarkdown(), + $storage->description + ); + } + + [, $symbol_name] = explode('::', $reference->symbol); + + //Class Property + if (strpos($reference->symbol, '$') !== false) { + $storage = $this->properties->getStorage($reference->symbol); + + return new PHPMarkdownContent( + $reference->symbol, + "{$storage->getInfo()} {$symbol_name}", + $storage->description + ); + } + + [$fq_classlike_name, $const_name] = explode('::', $reference->symbol); + + $class_constants = $this->classlikes->getConstantsForClass( + $fq_classlike_name, + ReflectionProperty::IS_PRIVATE + ); + + if (!isset($class_constants[$const_name])) { + return null; + } + + //Class Constant + return new PHPMarkdownContent( + $fq_classlike_name.'::'.$const_name, + $class_constants[$const_name]->getHoverMarkdown($const_name), + $class_constants[$const_name]->description + ); + } + + //Procedural Function + if (strpos($reference->symbol, '()')) { + $function_id = strtolower(substr($reference->symbol, 0, -2)); + $file_storage = $this->file_storage_provider->get($reference->file_path); + + if (isset($file_storage->functions[$function_id])) { + $function_storage = $file_storage->functions[$function_id]; + + return new PHPMarkdownContent( + $function_id, + $function_storage->getHoverMarkdown(), + $function_storage->description + ); + } + + if (!$function_id) { + return null; + } + + $function = $this->functions->getStorage(null, $function_id); + + return new PHPMarkdownContent( + $function_id, + $function->getHoverMarkdown(), + $function->description + ); + } + + //Procedural Variable + if (strpos($reference->symbol, '$') === 0) { + $type = VariableFetchAnalyzer::getGlobalType($reference->symbol); + if (!$type->isMixed()) { + return new PHPMarkdownContent( + $reference->symbol, + (string) $type + ); + } + } + + try { + $storage = $this->classlike_storage_provider->get($reference->symbol); + return new PHPMarkdownContent( + $storage->name, + ($storage->abstract ? 'abstract ' : '') . 'class ' . $storage->name, + $storage->description + ); + } catch (InvalidArgumentException $e) { + //continue on as normal + } + + if (strpos($reference->symbol, '\\')) { + $const_name_parts = explode('\\', $reference->symbol); + $const_name = array_pop($const_name_parts); + $namespace_name = implode('\\', $const_name_parts); + + $namespace_constants = NamespaceAnalyzer::getConstantsForNamespace( + $namespace_name, + ReflectionProperty::IS_PUBLIC + ); + //Namespace Constant + if (isset($namespace_constants[$const_name])) { + $type = $namespace_constants[$const_name]; + return new PHPMarkdownContent( + $reference->symbol, + $reference->symbol . ' ' . $type + ); + } + } else { + $file_storage = $this->file_storage_provider->get($reference->file_path); + // ? + if (isset($file_storage->constants[$reference->symbol])) { + return new PHPMarkdownContent( + $reference->symbol, + 'const' . $reference->symbol . ' ' . $file_storage->constants[$reference->symbol] + ); + } + $type = ConstFetchAnalyzer::getGlobalConstType($this, $reference->symbol, $reference->symbol); + + //Global Constant + if ($type) { + return new PHPMarkdownContent( + $reference->symbol, + 'const ' . $reference->symbol . ' ' . $type + ); + } + } + return null; + } + + private static function getPositionFromOffset(int $offset, string $file_contents): Position + { + $file_contents = substr($file_contents, 0, $offset); + + $offsetLength = $offset - strlen($file_contents); + + //PHP 8.0: Argument #3 ($offset) must be contained in argument #1 ($haystack) + if (($textlen = strlen($file_contents)) < $offsetLength) { + $offsetLength = $textlen; + } + + $before_newline_count = strrpos($file_contents, "\n", $offsetLength); + + return new Position( + substr_count($file_contents, "\n"), + $offset - (int)$before_newline_count - 1 + ); + } + + /** + * @return array{0: string, 1: '->'|'::'|'['|'symbol', 2: int}|null + */ + public function getCompletionDataAtPosition(string $file_path, Position $position): ?array + { + $is_open = $this->file_provider->isOpen($file_path); + + if (!$is_open) { + throw new UnanalyzedFileException($file_path . ' is not open'); + } + + $file_contents = $this->getFileContents($file_path); + + $offset = $position->toOffset($file_contents); + + [$reference_map, $type_map] = $this->analyzer->getMapsForFile($file_path); + + if (!$reference_map && !$type_map) { + return null; + } + + krsort($type_map); + + foreach ($type_map as $start_pos => [$end_pos_excluding_whitespace, $possible_type]) { + if ($offset < $start_pos) { + continue; + } + + $num_whitespace_bytes = preg_match('/\G\s+/', $file_contents, $matches, 0, $end_pos_excluding_whitespace) + ? strlen($matches[0]) + : 0; + $end_pos = $end_pos_excluding_whitespace + $num_whitespace_bytes; + + if ($offset - $end_pos === 1) { + $candidate_gap = substr($file_contents, $end_pos, 1); + + if ($candidate_gap === '[') { + $gap = $candidate_gap; + $recent_type = $possible_type; + + if ($recent_type === 'mixed') { + return null; + } + + return [$recent_type, $gap, $offset]; + } + } + + if ($offset - $end_pos === 2 || $offset - $end_pos === 3) { + $candidate_gap = substr($file_contents, $end_pos, 2); + + if ($candidate_gap === '->' || $candidate_gap === '::') { + $gap = $candidate_gap; + $recent_type = $possible_type; + + if ($recent_type === 'mixed') { + return null; + } + + return [$recent_type, $gap, $offset]; + } + } + } + + foreach ($reference_map as $start_pos => [$end_pos, $possible_reference]) { + if ($offset < $start_pos) { + continue; + } + // If the reference precedes a "::" then treat it as a class reference. + if ($offset - $end_pos === 2 && substr($file_contents, $end_pos, 2) === '::') { + return [$possible_reference, '::', $offset]; + } + + // Only continue for references that are partial / don't exist. + if ($possible_reference[0] !== '*') { + continue; + } + + if ($offset - $end_pos === 0) { + $recent_type = $possible_reference; + + return [$recent_type, 'symbol', $offset]; + } + } + + return null; + } + + public function getTypeContextAtPosition(string $file_path, Position $position): ?Union + { + $file_contents = $this->getFileContents($file_path); + $offset = $position->toOffset($file_contents); + + [$reference_map, $type_map, $argument_map] = $this->analyzer->getMapsForFile($file_path); + if (!$reference_map && !$type_map && !$argument_map) { + return null; + } + foreach ($argument_map as $start_pos => [$end_pos, $function, $argument_num]) { + if ($offset < $start_pos || $offset > $end_pos) { + continue; + } + // First parameter to a function-like + $function_storage = $this->getFunctionStorageForSymbol($file_path, $function . '()'); + if (!$function_storage || !$function_storage->params || !isset($function_storage->params[$argument_num])) { + return null; + } + + return $function_storage->params[$argument_num]->type; + } + + return null; + } + + /** + * @return list + */ + public function getCompletionItemsForClassishThing( + string $type_string, + string $gap, + bool $snippets_supported = false + ): array { + $completion_items = []; + + $type = Type::parseString($type_string); + + foreach ($type->getAtomicTypes() as $atomic_type) { + if ($atomic_type instanceof TNamedObject) { + try { + $class_storage = $this->classlike_storage_provider->get($atomic_type->value); + + foreach ($class_storage->appearing_method_ids as $declaring_method_id) { + $method_storage = $this->methods->getStorage($declaring_method_id); + + if ($method_storage->is_static || $gap === '->') { + $completion_item = new CompletionItem( + $method_storage->cased_name, + CompletionItemKind::METHOD, + $method_storage->getCompletionSignature(), + $method_storage->description, + (string)$method_storage->visibility, + $method_storage->cased_name, + $method_storage->cased_name, + null, + null, + new Command('Trigger parameter hints', 'editor.action.triggerParameterHints'), + null, + 2 + ); + + if ($snippets_supported && count($method_storage->params) > 0) { + $completion_item->insertText .= '($0)'; + $completion_item->insertTextFormat = InsertTextFormat::SNIPPET; + } else { + $completion_item->insertText .= '()'; + } + + $completion_items[] = $completion_item; + } + } + + foreach ($class_storage->declaring_property_ids as $property_name => $declaring_class) { + $property_storage = $this->properties->getStorage( + $declaring_class . '::$' . $property_name + ); + + if ($property_storage->is_static || $gap === '->') { + $completion_items[] = new CompletionItem( + '$' . $property_name, + CompletionItemKind::PROPERTY, + $property_storage->getInfo(), + $property_storage->description, + (string)$property_storage->visibility, + $property_name, + ($gap === '::' ? '$' : '') . $property_name + ); + } + } + + foreach ($class_storage->constants as $const_name => $const) { + $completion_items[] = new CompletionItem( + $const_name, + CompletionItemKind::VARIABLE, + 'const ' . $const_name, + $const->description, + null, + $const_name, + $const_name + ); + } + } catch (Exception $e) { + error_log($e->getMessage()); + continue; + } + } + } + + return $completion_items; + } + + /** + * @return list + */ + public function getCompletionItemsForArrayKeys( + string $type_string + ): array { + $completion_items = []; + $type = Type::parseString($type_string); + foreach ($type->getAtomicTypes() as $atomic_type) { + if ($atomic_type instanceof TKeyedArray) { + foreach ($atomic_type->properties as $property_name => $property) { + $completion_items[] = new CompletionItem( + (string) $property_name, + CompletionItemKind::PROPERTY, + (string) $property, + null, + null, + null, + "'$property_name'" + ); + } + } + } + return $completion_items; + } + + /** + * @return list + */ + public function getCompletionItemsForPartialSymbol( + string $type_string, + int $offset, + string $file_path + ): array { + $fq_suggestion = false; + + if (($type_string[1] ?? '') === '\\') { + $fq_suggestion = true; + } + + $matching_classlike_names = $this->classlikes->getMatchingClassLikeNames($type_string); + + $completion_items = []; + + $file_storage = $this->file_storage_provider->get($file_path); + + $aliases = null; + + foreach ($file_storage->classlikes_in_file as $fq_class_name => $_) { + try { + $class_storage = $this->classlike_storage_provider->get($fq_class_name); + } catch (Exception $e) { + continue; + } + + if (!$class_storage->stmt_location) { + continue; + } + + if ($offset > $class_storage->stmt_location->raw_file_start + && $offset < $class_storage->stmt_location->raw_file_end + ) { + $aliases = $class_storage->aliases; + break; + } + } + + if (!$aliases) { + foreach ($file_storage->namespace_aliases as $namespace_start => $namespace_aliases) { + if ($namespace_start < $offset) { + $aliases = $namespace_aliases; + break; + } + } + + if (!$aliases) { + $aliases = $file_storage->aliases; + } + } + + foreach ($matching_classlike_names as $fq_class_name) { + $extra_edits = []; + + $insertion_text = Type::getStringFromFQCLN( + $fq_class_name, + $aliases && $aliases->namespace ? $aliases->namespace : null, + $aliases->uses_flipped ?? [], + null + ); + + if ($aliases + && !$fq_suggestion + && $aliases->namespace + && $insertion_text === '\\' . $fq_class_name + && $aliases->namespace_first_stmt_start + ) { + $file_contents = $this->getFileContents($file_path); + + $class_name = preg_replace('/^.*\\\/', '', $fq_class_name); + + if ($aliases->uses_end) { + $position = self::getPositionFromOffset($aliases->uses_end, $file_contents); + $extra_edits[] = new TextEdit( + new Range( + $position, + $position + ), + "\n" . 'use ' . $fq_class_name . ';' + ); + } else { + $position = self::getPositionFromOffset($aliases->namespace_first_stmt_start, $file_contents); + $extra_edits[] = new TextEdit( + new Range( + $position, + $position + ), + 'use ' . $fq_class_name . ';' . "\n" . "\n" + ); + } + + $insertion_text = $class_name; + } + + try { + $class_storage = $this->classlike_storage_provider->get($fq_class_name); + $description = $class_storage->description; + } catch (Exception $e) { + $description = null; + } + + $completion_items[] = new CompletionItem( + $fq_class_name, + CompletionItemKind::CLASS_, + null, + $description, + null, + $fq_class_name, + $insertion_text, + null, + $extra_edits + ); + } + + $functions = $this->functions->getMatchingFunctionNames($type_string, $offset, $file_path, $this); + + $namespace_map = []; + if ($aliases) { + $namespace_map += $aliases->uses_flipped; + if ($aliases->namespace) { + $namespace_map[$aliases->namespace] = ''; + } + } + + // Sort the map by longest first, so we replace most specific + // used namespaces first. + ksort($namespace_map); + $namespace_map = array_reverse($namespace_map); + + foreach ($functions as $function_lowercase => $function) { + // Transform FQFN relative to all uses namespaces + $function_name = $function->cased_name; + if (!$function_name) { + continue; + } + $in_namespace_map = false; + foreach ($namespace_map as $namespace_name => $namespace_alias) { + if (strpos($function_lowercase, $namespace_name . '\\') === 0) { + $function_name = $namespace_alias . '\\' . substr($function_name, strlen($namespace_name) + 1); + $in_namespace_map = true; + } + } + // If the function is not use'd, and it's not a global function + // prepend it with a backslash. + if (!$in_namespace_map && strpos($function_name, '\\') !== false) { + $function_name = '\\' . $function_name; + } + $completion_items[] = new CompletionItem( + $function_name, + CompletionItemKind::FUNCTION, + $function->getCompletionSignature(), + $function->description, + null, + $function_name, + $function_name . (count($function->params) !== 0 ? '($0)' : '()'), + null, + null, + new Command('Trigger parameter hints', 'editor.action.triggerParameterHints'), + null, + 2 + ); + } + + return $completion_items; + } + + /** + * @return list + */ + public function getCompletionItemsForType(Union $type): array + { + $completion_items = []; + foreach ($type->getAtomicTypes() as $atomic_type) { + if ($atomic_type instanceof TBool) { + $bools = (string) $atomic_type === 'bool' ? ['true', 'false'] : [(string) $atomic_type]; + foreach ($bools as $property_name) { + $completion_items[] = new CompletionItem( + $property_name, + CompletionItemKind::VALUE, + 'bool', + null, + null, + null, + $property_name + ); + } + } elseif ($atomic_type instanceof TLiteralString) { + $completion_items[] = new CompletionItem( + $atomic_type->value, + CompletionItemKind::VALUE, + $atomic_type->getId(), + null, + null, + null, + "'$atomic_type->value'" + ); + } elseif ($atomic_type instanceof TLiteralInt) { + $completion_items[] = new CompletionItem( + (string) $atomic_type->value, + CompletionItemKind::VALUE, + $atomic_type->getId(), + null, + null, + null, + (string) $atomic_type->value + ); + } elseif ($atomic_type instanceof TClassConstant) { + $const = $atomic_type->fq_classlike_name . '::' . $atomic_type->const_name; + $completion_items[] = new CompletionItem( + $const, + CompletionItemKind::VALUE, + $atomic_type->getId(), + null, + null, + null, + $const + ); + } + } + return $completion_items; + } + + /** + * @return array{0: non-empty-string, 1: int, 2: Range}|null + */ + public function getFunctionArgumentAtPosition(string $file_path, Position $position): ?array + { + $is_open = $this->file_provider->isOpen($file_path); + + if (!$is_open) { + throw new UnanalyzedFileException($file_path . ' is not open'); + } + + $file_contents = $this->getFileContents($file_path); + + $offset = $position->toOffset($file_contents); + + [, , $argument_map] = $this->analyzer->getMapsForFile($file_path); + + $reference = null; + $argument_number = null; + + if (!$argument_map) { + return null; + } + + $start_pos = null; + $end_pos = null; + + ksort($argument_map); + + foreach ($argument_map as $start_pos => [$end_pos, $possible_reference, $possible_argument_number]) { + if ($offset < $start_pos) { + break; + } + + if ($offset > $end_pos) { + continue; + } + + $reference = $possible_reference; + $argument_number = $possible_argument_number; + } + + if ($reference === null || $start_pos === null || $end_pos === null || $argument_number === null) { + return null; + } + + $range = new Range( + self::getPositionFromOffset($start_pos, $file_contents), + self::getPositionFromOffset($end_pos, $file_contents) + ); + + return [$reference, $argument_number, $range]; + } + + /** + * @param non-empty-string $function_symbol + */ + public function getSignatureInformation( + string $function_symbol, + string $file_path = null + ): ?SignatureInformation { + $signature_label = ''; + $signature_documentation = null; + if (strpos($function_symbol, '::') !== false) { + /** @psalm-suppress ArgumentTypeCoercion */ + $method_id = new MethodIdentifier(...explode('::', $function_symbol)); + + $declaring_method_id = $this->methods->getDeclaringMethodId($method_id); + + if ($declaring_method_id === null) { + return null; + } + + $method_storage = $this->methods->getStorage($declaring_method_id); + $params = $method_storage->params; + $signature_label = $method_storage->cased_name; + $signature_documentation = $method_storage->description; + } else { + try { + if ($file_path) { + $function_storage = $this->functions->getStorage( + null, + strtolower($function_symbol), + dirname($file_path), + $file_path + ); + } else { + $function_storage = $this->functions->getStorage(null, strtolower($function_symbol)); + } + $params = $function_storage->params; + $signature_label = $function_storage->cased_name; + $signature_documentation = $function_storage->description; + } catch (Exception $exception) { + if (InternalCallMapHandler::inCallMap($function_symbol)) { + $callables = InternalCallMapHandler::getCallablesFromCallMap($function_symbol); + + if (!$callables || !$callables[0]->params) { + throw $exception; + } + + $params = $callables[0]->params; + } else { + throw $exception; + } + } + } + + $signature_label .= '('; + $parameters = []; + + foreach ($params as $i => $param) { + $parameter_label = ($param->type ?: 'mixed') . ' $' . $param->name; + $parameters[] = new ParameterInformation( + [ + strlen($signature_label), + strlen($signature_label) + strlen($parameter_label), + ], + $param->description ?? null + ); + + $signature_label .= $parameter_label; + + if ($i < (count($params) - 1)) { + $signature_label .= ', '; + } + } + + $signature_label .= ')'; + + return new SignatureInformation( + $signature_label, + $parameters, + $signature_documentation + ); + } + + public function getSymbolLocation(Reference $reference): ?CodeLocation + { + if (is_numeric($reference->symbol[0])) { + $symbol = preg_replace('/:.*/', '', $reference->symbol); + $symbol_parts = explode('-', $symbol); + + $file_contents = $this->getFileContents($reference->file_path); + + return new Raw( + $file_contents, + $reference->file_path, + $this->config->shortenFileName($reference->file_path), + (int) $symbol_parts[0], + (int) $symbol_parts[1] + ); + } + + try { + if (strpos($reference->symbol, '::')) { + if (strpos($reference->symbol, '()')) { + $symbol = substr($reference->symbol, 0, -2); + + /** @psalm-suppress ArgumentTypeCoercion */ + $method_id = new MethodIdentifier(...explode('::', $symbol)); + + $declaring_method_id = $this->methods->getDeclaringMethodId($method_id); + + if (!$declaring_method_id) { + return null; + } + + $storage = $this->methods->getStorage($declaring_method_id); + + return $storage->location; + } + + if (strpos($reference->symbol, '$') !== false) { + $storage = $this->properties->getStorage($reference->symbol); + + return $storage->location; + } + + [$fq_classlike_name, $const_name] = explode('::', $reference->symbol); + + $class_constants = $this->classlikes->getConstantsForClass( + $fq_classlike_name, + ReflectionProperty::IS_PRIVATE + ); + + if (!isset($class_constants[$const_name])) { + return null; + } + + return $class_constants[$const_name]->location; + } + + if (strpos($reference->symbol, '()')) { + $file_storage = $this->file_storage_provider->get($reference->file_path); + + $function_id = strtolower(substr($reference->symbol, 0, -2)); + + if (isset($file_storage->functions[$function_id])) { + return $file_storage->functions[$function_id]->location; + } + + if (!$function_id) { + return null; + } + + return $this->functions->getStorage(null, $function_id)->location; + } + + return $this->classlike_storage_provider->get($reference->symbol)->location; + } catch (UnexpectedValueException $e) { + error_log($e->getMessage()); + + return null; + } catch (InvalidArgumentException $e) { + return null; + } + } + + public function addTemporaryFileChanges(string $file_path, string $new_content, ?int $version = null): void + { + $this->file_provider->addTemporaryFileChanges($file_path, $new_content, $version); + } + + public function removeTemporaryFileChanges(string $file_path): void + { + $this->file_provider->removeTemporaryFileChanges($file_path); + } + + /** + * @param array $candidate_files + * + */ + public function reloadFiles(ProjectAnalyzer $project_analyzer, array $candidate_files, bool $force = false): void + { + $this->loadAnalyzer(); + + if ($force) { + FileReferenceProvider::clearCache(); + } + + $this->file_reference_provider->loadReferenceCache($force); + + FunctionLikeAnalyzer::clearCache(); + + if ($force || !$this->statements_provider->parser_cache_provider) { + $diff_files = $candidate_files; + } else { + $diff_files = []; + + $parser_cache_provider = $this->statements_provider->parser_cache_provider; + + foreach ($candidate_files as $candidate_file_path) { + if ($parser_cache_provider->loadExistingFileContentsFromCache($candidate_file_path) + !== $this->file_provider->getContents($candidate_file_path) + ) { + $diff_files[] = $candidate_file_path; + } + } + } + + $referenced_files = $project_analyzer->getReferencedFilesFromDiff($diff_files, false); + + foreach ($diff_files as $diff_file_path) { + $this->invalidateInformationForFile($diff_file_path); + } + + foreach ($referenced_files as $referenced_file_path) { + if (in_array($referenced_file_path, $diff_files, true)) { + continue; + } + + $file_storage = $this->file_storage_provider->get($referenced_file_path); + + foreach ($file_storage->classlikes_in_file as $fq_classlike_name) { + $this->classlike_storage_provider->remove($fq_classlike_name); + $this->classlikes->removeClassLike($fq_classlike_name); + } + + $this->file_storage_provider->remove($referenced_file_path); + $this->scanner->removeFile($referenced_file_path); + } + + $referenced_files = array_combine($referenced_files, $referenced_files); + + $this->scanner->addFilesToDeepScan($referenced_files); + $this->addFilesToAnalyze(array_combine($candidate_files, $candidate_files)); + + $this->scanner->scanFiles($this->classlikes); + + $this->file_reference_provider->updateReferenceCache($this, $referenced_files); + + $this->populator->populateCodebase(); + } +} diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 1ace5f3c1bf..740d6ce2cfa 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -136,10 +136,16 @@ class LanguageServer extends Dispatcher */ protected $project_analyzer; + /** + * @var Codebase + */ + protected $codebase; + public function __construct( ProtocolReader $reader, ProtocolWriter $writer, ProjectAnalyzer $project_analyzer, + Codebase $codebase, ClientConfiguration $clientConfiguration, Progress $progress ) { @@ -149,6 +155,8 @@ public function __construct( $this->project_analyzer = $project_analyzer; + $this->codebase = $codebase; + $this->protocolWriter = $writer; $this->protocolReader = $reader; @@ -241,6 +249,7 @@ function (): void { public static function run(Config $config, ClientConfiguration $clientConfiguration, string $base_dir): void { $progress = new Progress(); + //no-cache mode does not work in the LSP $providers = new Providers( new FileProvider, @@ -251,25 +260,32 @@ public static function run(Config $config, ClientConfiguration $clientConfigurat new ProjectCacheProvider(Composer::getLockFilePath($base_dir)) ); - $project_analyzer = new ProjectAnalyzer( + $codebase = new Codebase( $config, $providers, - null, - [], - 1, $progress ); if ($config->find_unused_variables) { - $project_analyzer->getCodebase()->reportUnusedVariables(); + $codebase->reportUnusedVariables(); } - if ($clientConfiguration->onchangeLineLimit) { - $project_analyzer->onchange_line_limit = $clientConfiguration->onchangeLineLimit; + if ($clientConfiguration->findUnusedCode) { + $codebase->reportUnusedCode($clientConfiguration->findUnusedCode); } - if ($clientConfiguration->findUnusedCode) { - $project_analyzer->getCodebase()->reportUnusedCode($clientConfiguration->findUnusedCode); + $project_analyzer = new ProjectAnalyzer( + $config, + $providers, + null, + [], + 1, + $progress, + $codebase + ); + + if ($clientConfiguration->onchangeLineLimit) { + $project_analyzer->onchange_line_limit = $clientConfiguration->onchangeLineLimit; } //Setup Project Analyzer @@ -289,6 +305,7 @@ public static function run(Config $config, ClientConfiguration $clientConfigurat new ProtocolStreamReader($socket), new ProtocolStreamWriter($socket), $project_analyzer, + $codebase, $clientConfiguration, $progress ); @@ -343,6 +360,7 @@ function (): void { $reader, new ProtocolStreamWriter($socket), $project_analyzer, + $codebase, $clientConfiguration, $progress ); @@ -356,6 +374,7 @@ function (): void { new ProtocolStreamReader($socket), new ProtocolStreamWriter($socket), $project_analyzer, + $codebase, $clientConfiguration, $progress ); @@ -369,6 +388,7 @@ function (): void { new ProtocolStreamReader(STDIN), new ProtocolStreamWriter(STDOUT), $project_analyzer, + $codebase, $clientConfiguration, $progress ); @@ -426,20 +446,19 @@ function () { $this->logInfo("Initializing: Getting code base..."); $this->clientStatus('initializing', 'getting code base'); - $codebase = $this->project_analyzer->getCodebase(); $this->logInfo("Initializing: Scanning files ({$this->project_analyzer->threads} Threads)..."); $this->clientStatus('initializing', 'scanning files'); - $codebase->scanFiles($this->project_analyzer->threads); + $this->codebase->scanFiles($this->project_analyzer->threads); $this->logInfo("Initializing: Registering stub files..."); $this->clientStatus('initializing', 'registering stub files'); - $codebase->config->visitStubFiles($codebase, $this->project_analyzer->progress); + $this->codebase->config->visitStubFiles($this->codebase, $this->project_analyzer->progress); if ($this->textDocument === null) { $this->textDocument = new ServerTextDocument( $this, - $codebase, + $this->codebase, $this->project_analyzer ); } @@ -447,7 +466,7 @@ function () { if ($this->workspace === null) { $this->workspace = new ServerWorkspace( $this, - $codebase, + $this->codebase, $this->project_analyzer ); } @@ -676,16 +695,15 @@ public function doVersionedAnalysis(array $files, ?int $version = null): void return; } - $codebase = $this->project_analyzer->getCodebase(); - $codebase->reloadFiles( + $this->codebase->reloadFiles( $this->project_analyzer, array_keys($files) ); - $codebase->analyzer->addFilesToAnalyze( + $this->codebase->analyzer->addFilesToAnalyze( array_combine(array_keys($files), array_keys($files)) ); - $codebase->analyzer->analyzeFiles($this->project_analyzer, 1, false); + $this->codebase->analyzer->analyzeFiles($this->project_analyzer, 1, false); $this->emitVersionedIssues($files, $version); } catch (Throwable $e) { diff --git a/src/Psalm/Internal/LanguageServer/PHPMarkdownContent.php b/src/Psalm/Internal/LanguageServer/PHPMarkdownContent.php new file mode 100644 index 00000000000..86428ca8eb5 --- /dev/null +++ b/src/Psalm/Internal/LanguageServer/PHPMarkdownContent.php @@ -0,0 +1,19 @@ +file_path = $file_path; + $this->symbol = $symbol; + $this->range = $range; + } +} \ No newline at end of file diff --git a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php index 89e7c930d50..566440d62a1 100644 --- a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php +++ b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php @@ -12,8 +12,6 @@ use LanguageServerProtocol\CompletionList; use LanguageServerProtocol\Hover; use LanguageServerProtocol\Location; -use LanguageServerProtocol\MarkupContent; -use LanguageServerProtocol\MarkupKind; use LanguageServerProtocol\Position; use LanguageServerProtocol\Range; use LanguageServerProtocol\SignatureHelp; @@ -23,10 +21,10 @@ use LanguageServerProtocol\TextEdit; use LanguageServerProtocol\VersionedTextDocumentIdentifier; use LanguageServerProtocol\WorkspaceEdit; -use Psalm\Codebase; use Psalm\Exception\TypeParseTreeException; use Psalm\Exception\UnanalyzedFileException; use Psalm\Internal\Analyzer\ProjectAnalyzer; +use Psalm\Internal\LanguageServer\Codebase; use Psalm\Internal\LanguageServer\LanguageServer; use UnexpectedValueException; @@ -184,6 +182,10 @@ public function didClose(TextDocumentIdentifier $textDocument): void */ public function definition(TextDocumentIdentifier $textDocument, Position $position): Promise { + if (!$this->server->client->clientConfiguration->provideDefinition) { + return new Success(null); + } + $this->server->logDebug( 'textDocument/definition' ); @@ -196,19 +198,18 @@ public function definition(TextDocumentIdentifier $textDocument, Position $posit } try { - $reference_location = $this->codebase->getReferenceAtPosition($file_path, $position); + $reference = $this->codebase->getReferenceAtPosition($file_path, $position); } catch (UnanalyzedFileException $e) { $this->server->logError((string) $e); return new Success(null); } - if ($reference_location === null) { + if ($reference === null) { return new Success(null); } - [$reference] = $reference_location; - $code_location = $this->codebase->getSymbolLocation($file_path, $reference); + $code_location = $this->codebase->getSymbolLocation($reference); if (!$code_location) { return new Success(null); @@ -235,6 +236,10 @@ public function definition(TextDocumentIdentifier $textDocument, Position $posit */ public function hover(TextDocumentIdentifier $textDocument, Position $position): Promise { + if (!$this->server->client->clientConfiguration->provideHover) { + return new Success(null); + } + $this->server->logDebug( 'textDocument/hover' ); @@ -247,39 +252,30 @@ public function hover(TextDocumentIdentifier $textDocument, Position $position): } try { - $reference_location = $this->codebase->getReferenceAtPosition($file_path, $position); + $reference = $this->codebase->getReferenceAtPosition($file_path, $position); } catch (UnanalyzedFileException $e) { $this->server->logError((string) $e); return new Success(null); } - if ($reference_location === null) { + if ($reference === null) { return new Success(null); } - [$reference, $range] = $reference_location; + $this->server->logDebug('hover', ['reference' => $reference]); try { - $symbol_information = $this->codebase->getSymbolInformation($file_path, $reference); + $markup = $this->codebase->getMarkupContentForSymbol($reference); } catch (UnexpectedValueException $e) { $this->server->logError((string) $e); return new Success(null); } - if ($symbol_information === null) { + if ($markup === null) { return new Success(null); } - $content = "```php\n" . $symbol_information['type'] . "\n```"; - if (isset($symbol_information['description'])) { - $content .= "\n---\n" . $symbol_information['description']; - } - $contents = new MarkupContent( - MarkupKind::MARKDOWN, - $content - ); - - return new Success(new Hover($contents, $range)); + return new Success(new Hover($markup, $reference->range)); } /** @@ -298,6 +294,10 @@ public function hover(TextDocumentIdentifier $textDocument, Position $position): */ public function completion(TextDocumentIdentifier $textDocument, Position $position): Promise { + if (!$this->server->client->clientConfiguration->provideCompletion) { + return new Success(null); + } + $this->server->logDebug( 'textDocument/completion' ); @@ -368,6 +368,10 @@ public function completion(TextDocumentIdentifier $textDocument, Position $posit */ public function signatureHelp(TextDocumentIdentifier $textDocument, Position $position): Promise { + if (!$this->server->client->clientConfiguration->provideSignatureHelp) { + return new Success(null); + } + $this->server->logDebug( 'textDocument/signatureHelp' ); @@ -418,6 +422,10 @@ public function signatureHelp(TextDocumentIdentifier $textDocument, Position $po */ public function codeAction(TextDocumentIdentifier $textDocument, Range $range, CodeActionContext $context): Promise { + if (!$this->server->client->clientConfiguration->provideCodeActions) { + return new Success(null); + } + $this->server->logDebug( 'textDocument/codeAction' ); diff --git a/src/Psalm/Internal/LanguageServer/Server/Workspace.php b/src/Psalm/Internal/LanguageServer/Server/Workspace.php index 96b6f60390e..47b40b0a52e 100644 --- a/src/Psalm/Internal/LanguageServer/Server/Workspace.php +++ b/src/Psalm/Internal/LanguageServer/Server/Workspace.php @@ -9,9 +9,9 @@ use InvalidArgumentException; use LanguageServerProtocol\FileChangeType; use LanguageServerProtocol\FileEvent; -use Psalm\Codebase; use Psalm\Internal\Analyzer\ProjectAnalyzer; use Psalm\Internal\Composer; +use Psalm\Internal\LanguageServer\Codebase; use Psalm\Internal\LanguageServer\LanguageServer; use Psalm\Internal\Provider\FileReferenceProvider; @@ -149,17 +149,16 @@ public function executeCommand($command, $arguments): Promise /** @var array{uri: string} */ $arguments = (array) $arguments; $file = LanguageServer::uriToPath($arguments['uri']); - $codebase = $this->project_analyzer->getCodebase(); - $codebase->reloadFiles( + $this->codebase->reloadFiles( $this->project_analyzer, [$file], true ); - $codebase->analyzer->addFilesToAnalyze( + $this->codebase->analyzer->addFilesToAnalyze( [$file => $file] ); - $codebase->analyzer->analyzeFiles($this->project_analyzer, 1, false); + $this->codebase->analyzer->analyzeFiles($this->project_analyzer, 1, false); $this->server->emitVersionedIssues([$file => $arguments['uri']]); break; diff --git a/src/Psalm/Storage/ClassConstantStorage.php b/src/Psalm/Storage/ClassConstantStorage.php index bcbe39dcdb6..43fc5a494a8 100644 --- a/src/Psalm/Storage/ClassConstantStorage.php +++ b/src/Psalm/Storage/ClassConstantStorage.php @@ -59,4 +59,35 @@ public function __construct(?Union $type, int $visibility, ?CodeLocation $locati $this->location = $location; $this->type = $type; } + + /** + * Used in the Language Server + */ + public function getHoverMarkdown(string $const): string + { + switch ($this->visibility) { + case ClassLikeAnalyzer::VISIBILITY_PRIVATE: + $visibility_text = 'private'; + break; + + case ClassLikeAnalyzer::VISIBILITY_PROTECTED: + $visibility_text = 'protected'; + break; + + default: + $visibility_text = 'public'; + } + + $value = ''; + if ($this->type) { + $types = $this->type->getAtomicTypes(); + $type = array_values($types)[0]; + if (property_exists($type, 'value')) { + $value = " = {$type->value};"; + } + } + + + return "$visibility_text const $const$value"; + } } diff --git a/src/Psalm/Storage/FunctionLikeStorage.php b/src/Psalm/Storage/FunctionLikeStorage.php index e9928ce565a..4ca0cdada72 100644 --- a/src/Psalm/Storage/FunctionLikeStorage.php +++ b/src/Psalm/Storage/FunctionLikeStorage.php @@ -234,24 +234,52 @@ abstract class FunctionLikeStorage */ public $description; - public function __toString(): string + /** + * Used in the Language Server + */ + public function getHoverMarkdown(): string { - return $this->getSignature(false); + $symbol_text = 'function ' . $this->cased_name . '(' . "\n" . implode( + ',' . "\n", + array_map( + function (FunctionLikeParameter $param): string { + return ' ' . ($param->type ?: 'mixed') . ' $' . $param->name; + }, + $this->params + ) + ) . "\n" . ') : ' . ($this->return_type ?: 'mixed'); + + if (!$this instanceof MethodStorage) { + return $symbol_text; + } + + switch ($this->visibility) { + case ClassLikeAnalyzer::VISIBILITY_PRIVATE: + $visibility_text = 'private'; + break; + + case ClassLikeAnalyzer::VISIBILITY_PROTECTED: + $visibility_text = 'protected'; + break; + + default: + $visibility_text = 'public'; + } + + return $visibility_text . ' ' . $symbol_text; } - public function getSignature(bool $allow_newlines): string + public function getCompletionSignature(): string { - $newlines = $allow_newlines && !empty($this->params); - - $symbol_text = 'function ' . $this->cased_name . '(' . ($newlines ? "\n" : '') . implode( - ',' . ($newlines ? "\n" : ' '), + $symbol_text = 'function ' . $this->cased_name . '(' . implode( + ',', array_map( - function (FunctionLikeParameter $param) use ($newlines): string { - return ($newlines ? ' ' : '') . ($param->type ?: 'mixed') . ' $' . $param->name; + function (FunctionLikeParameter $param): string { + return ($param->type ?: 'mixed') . ' $' . $param->name; }, $this->params ) - ) . ($newlines ? "\n" : '') . ') : ' . ($this->return_type ?: 'mixed'); + ) . ') : ' . ($this->return_type ?: 'mixed'); if (!$this instanceof MethodStorage) { return $symbol_text; From e1306779a774fa896e80e464e09addac2638d236 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Mon, 14 Feb 2022 21:38:58 +0000 Subject: [PATCH 23/78] add ability to hide warnings --- src/Psalm/Internal/Cli/LanguageServer.php | 10 +++++ .../LanguageServer/ClientConfiguration.php | 2 +- .../Internal/LanguageServer/Codebase.php | 37 +++++++++---------- .../LanguageServer/PHPMarkdownContent.php | 11 +++++- .../LanguageServer/Server/TextDocument.php | 2 - 5 files changed, 38 insertions(+), 24 deletions(-) diff --git a/src/Psalm/Internal/Cli/LanguageServer.php b/src/Psalm/Internal/Cli/LanguageServer.php index e990e83b6e6..fa1e09a4bde 100644 --- a/src/Psalm/Internal/Cli/LanguageServer.php +++ b/src/Psalm/Internal/Cli/LanguageServer.php @@ -81,6 +81,7 @@ public static function run(array $argv): void 'enable-provide-hover::', 'enable-provide-signature-help::', 'enable-provide-definition::', + 'show-diagnostic-warnings::', 'use-extended-diagnostic-codes', 'verbose' ]; @@ -199,6 +200,9 @@ function (string $arg) use ($valid_long_options): void { --enable-provide-definition[=BOOL] Enables or disables providing definition. Default is true. + --show-diagnostic-warnings[=BOOL] + Enables or disables showing diagnostic warnings. Default is true. + --use-extended-diagnostic-codes (DEPRECATED) Enables sending help uri links with the code in diagnostic messages. @@ -325,6 +329,12 @@ function () use ($current_dir, $options, $vendor_dir): ?\Composer\Autoload\Class || !is_string($options['enable-autocomplete']) || strtolower($options['enable-autocomplete']) !== 'false'; + $clientConfiguration->hideWarnings = !( + !isset($options['show-diagnostic-warnings']) + || !is_string($options['show-diagnostic-warnings']) + || strtolower($options['show-diagnostic-warnings']) !== 'false' + ); + $find_unused_code = isset($options['find-dead-code']) ? 'auto' : null; if ($config->find_unused_code) { $find_unused_code = 'auto'; diff --git a/src/Psalm/Internal/LanguageServer/ClientConfiguration.php b/src/Psalm/Internal/LanguageServer/ClientConfiguration.php index 8e577483456..78f87dcbb25 100644 --- a/src/Psalm/Internal/LanguageServer/ClientConfiguration.php +++ b/src/Psalm/Internal/LanguageServer/ClientConfiguration.php @@ -105,7 +105,7 @@ class ClientConfiguration public $onchangeLineLimit; public function __construct( - bool $hideWarnings = null, + bool $hideWarnings = true, bool $provideCompletion = null, bool $provideDefinition = null, bool $provideHover = null, diff --git a/src/Psalm/Internal/LanguageServer/Codebase.php b/src/Psalm/Internal/LanguageServer/Codebase.php index cf9d03c064b..b0ae951ea28 100644 --- a/src/Psalm/Internal/LanguageServer/Codebase.php +++ b/src/Psalm/Internal/LanguageServer/Codebase.php @@ -10,12 +10,13 @@ use LanguageServerProtocol\CompletionItem; use LanguageServerProtocol\CompletionItemKind; use LanguageServerProtocol\InsertTextFormat; -use LanguageServerProtocol\MarkupContent; use LanguageServerProtocol\ParameterInformation; use LanguageServerProtocol\Position; use LanguageServerProtocol\Range; use LanguageServerProtocol\SignatureInformation; use LanguageServerProtocol\TextEdit; +use Psalm\CodeLocation; +use Psalm\CodeLocation\Raw; use Psalm\Codebase as PsalmCodebase; use Psalm\Exception\UnanalyzedFileException; use Psalm\Internal\Analyzer\FunctionLikeAnalyzer; @@ -26,6 +27,7 @@ use Psalm\Internal\Codebase\InternalCallMapHandler; use Psalm\Internal\MethodIdentifier; use Psalm\Internal\Provider\FileReferenceProvider; +use Psalm\Type; use Psalm\Type\Atomic\TBool; use Psalm\Type\Atomic\TClassConstant; use Psalm\Type\Atomic\TKeyedArray; @@ -33,9 +35,6 @@ use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Union; -use Psalm\Type; -use Psalm\CodeLocation\Raw; -use Psalm\CodeLocation; use ReflectionProperty; use UnexpectedValueException; @@ -126,9 +125,9 @@ public function getReferenceAtPosition(string $file_path, Position $position): ? */ public function getMarkupContentForSymbol(Reference $reference): ?PHPMarkdownContent { + //Direct Assignment if (is_numeric($reference->symbol[0])) { return new PHPMarkdownContent( - '?1', preg_replace('/^[^:]*:/', '', $reference->symbol) ); } @@ -151,8 +150,8 @@ public function getMarkupContentForSymbol(Reference $reference): ?PHPMarkdownCon $storage = $this->methods->getStorage($declaring_method_id); return new PHPMarkdownContent( - "{$storage->defining_fqcln}::{$storage->cased_name}", $storage->getHoverMarkdown(), + "{$storage->defining_fqcln}::{$storage->cased_name}", $storage->description ); } @@ -164,8 +163,8 @@ public function getMarkupContentForSymbol(Reference $reference): ?PHPMarkdownCon $storage = $this->properties->getStorage($reference->symbol); return new PHPMarkdownContent( - $reference->symbol, "{$storage->getInfo()} {$symbol_name}", + $reference->symbol, $storage->description ); } @@ -183,8 +182,8 @@ public function getMarkupContentForSymbol(Reference $reference): ?PHPMarkdownCon //Class Constant return new PHPMarkdownContent( - $fq_classlike_name.'::'.$const_name, $class_constants[$const_name]->getHoverMarkdown($const_name), + $fq_classlike_name.'::'.$const_name, $class_constants[$const_name]->description ); } @@ -198,8 +197,8 @@ public function getMarkupContentForSymbol(Reference $reference): ?PHPMarkdownCon $function_storage = $file_storage->functions[$function_id]; return new PHPMarkdownContent( - $function_id, $function_storage->getHoverMarkdown(), + $function_id, $function_storage->description ); } @@ -211,8 +210,8 @@ public function getMarkupContentForSymbol(Reference $reference): ?PHPMarkdownCon $function = $this->functions->getStorage(null, $function_id); return new PHPMarkdownContent( - $function_id, $function->getHoverMarkdown(), + $function_id, $function->description ); } @@ -222,8 +221,8 @@ public function getMarkupContentForSymbol(Reference $reference): ?PHPMarkdownCon $type = VariableFetchAnalyzer::getGlobalType($reference->symbol); if (!$type->isMixed()) { return new PHPMarkdownContent( - $reference->symbol, - (string) $type + (string) $type, + $reference->symbol ); } } @@ -231,8 +230,8 @@ public function getMarkupContentForSymbol(Reference $reference): ?PHPMarkdownCon try { $storage = $this->classlike_storage_provider->get($reference->symbol); return new PHPMarkdownContent( - $storage->name, ($storage->abstract ? 'abstract ' : '') . 'class ' . $storage->name, + $storage->name, $storage->description ); } catch (InvalidArgumentException $e) { @@ -252,8 +251,8 @@ public function getMarkupContentForSymbol(Reference $reference): ?PHPMarkdownCon if (isset($namespace_constants[$const_name])) { $type = $namespace_constants[$const_name]; return new PHPMarkdownContent( - $reference->symbol, - $reference->symbol . ' ' . $type + $reference->symbol . ' ' . $type, + $reference->symbol ); } } else { @@ -261,8 +260,8 @@ public function getMarkupContentForSymbol(Reference $reference): ?PHPMarkdownCon // ? if (isset($file_storage->constants[$reference->symbol])) { return new PHPMarkdownContent( - $reference->symbol, - 'const' . $reference->symbol . ' ' . $file_storage->constants[$reference->symbol] + 'const' . $reference->symbol . ' ' . $file_storage->constants[$reference->symbol], + $reference->symbol ); } $type = ConstFetchAnalyzer::getGlobalConstType($this, $reference->symbol, $reference->symbol); @@ -270,8 +269,8 @@ public function getMarkupContentForSymbol(Reference $reference): ?PHPMarkdownCon //Global Constant if ($type) { return new PHPMarkdownContent( - $reference->symbol, - 'const ' . $reference->symbol . ' ' . $type + 'const ' . $reference->symbol . ' ' . $type, + $reference->symbol ); } } diff --git a/src/Psalm/Internal/LanguageServer/PHPMarkdownContent.php b/src/Psalm/Internal/LanguageServer/PHPMarkdownContent.php index 86428ca8eb5..c581382a10c 100644 --- a/src/Psalm/Internal/LanguageServer/PHPMarkdownContent.php +++ b/src/Psalm/Internal/LanguageServer/PHPMarkdownContent.php @@ -9,11 +9,18 @@ class PHPMarkdownContent extends MarkupContent { - public function __construct(string $title, string $code, ?string $description = '') + public function __construct(string $code, ?string $title = null, ?string $description = null) { + $markdown = ''; + if ($title !== null) { + $markdown = "**$title**\n\n$markdown"; + } + if ($description !== null) { + $markdown = "$description\n$markdown"; + } parent::__construct( MarkupKind::MARKDOWN, - "**$title**\n\n$description\n```php\nserver->logDebug('hover', ['reference' => $reference]); - try { $markup = $this->codebase->getMarkupContentForSymbol($reference); } catch (UnexpectedValueException $e) { From f8fd0fef604f11319013471216ee3fba6f13ad6b Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Mon, 14 Feb 2022 22:03:07 +0000 Subject: [PATCH 24/78] fix hover --- src/Psalm/Internal/LanguageServer/PHPMarkdownContent.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/LanguageServer/PHPMarkdownContent.php b/src/Psalm/Internal/LanguageServer/PHPMarkdownContent.php index c581382a10c..a0e05aeb31f 100644 --- a/src/Psalm/Internal/LanguageServer/PHPMarkdownContent.php +++ b/src/Psalm/Internal/LanguageServer/PHPMarkdownContent.php @@ -13,10 +13,10 @@ public function __construct(string $code, ?string $title = null, ?string $descri { $markdown = ''; if ($title !== null) { - $markdown = "**$title**\n\n$markdown"; + $markdown = "**$title**\n\n"; } if ($description !== null) { - $markdown = "$description\n$markdown"; + $markdown = "$markdown$description\n\n"; } parent::__construct( MarkupKind::MARKDOWN, From 6394e2d548891d29e9bbb7b6678af4a5106047b9 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Mon, 14 Feb 2022 22:18:01 +0000 Subject: [PATCH 25/78] fix new lines in funcs with no params --- src/Psalm/Storage/FunctionLikeStorage.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Psalm/Storage/FunctionLikeStorage.php b/src/Psalm/Storage/FunctionLikeStorage.php index 4ca0cdada72..5f6acd2cae5 100644 --- a/src/Psalm/Storage/FunctionLikeStorage.php +++ b/src/Psalm/Storage/FunctionLikeStorage.php @@ -10,6 +10,7 @@ use function array_column; use function array_fill_keys; use function array_map; +use function count; use function implode; abstract class FunctionLikeStorage @@ -239,15 +240,18 @@ abstract class FunctionLikeStorage */ public function getHoverMarkdown(): string { - $symbol_text = 'function ' . $this->cased_name . '(' . "\n" . implode( - ',' . "\n", + $params = count($this->params) > 0 ? "\n" . implode( + ",\n", array_map( function (FunctionLikeParameter $param): string { - return ' ' . ($param->type ?: 'mixed') . ' $' . $param->name; + $realType = $param->type ?: 'mixed'; + return " {$realType} \${$param->name}"; }, $this->params ) - ) . "\n" . ') : ' . ($this->return_type ?: 'mixed'); + ) . "\n" : ''; + $return_type = $this->return_type ?: 'mixed'; + $symbol_text = "function {$this->cased_name}({$params}): {$return_type}"; if (!$this instanceof MethodStorage) { return $symbol_text; From 29f0159b21af9993537ec3216d3dd72f50816c2c Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Mon, 14 Feb 2022 22:19:19 +0000 Subject: [PATCH 26/78] fix spacing --- src/Psalm/Storage/FunctionLikeStorage.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Storage/FunctionLikeStorage.php b/src/Psalm/Storage/FunctionLikeStorage.php index 5f6acd2cae5..a108513ba92 100644 --- a/src/Psalm/Storage/FunctionLikeStorage.php +++ b/src/Psalm/Storage/FunctionLikeStorage.php @@ -245,7 +245,7 @@ public function getHoverMarkdown(): string array_map( function (FunctionLikeParameter $param): string { $realType = $param->type ?: 'mixed'; - return " {$realType} \${$param->name}"; + return " {$realType} \${$param->name}"; }, $this->params ) From cc8c775abfa2071df8554caa0fa0df16fb20cfe1 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Tue, 15 Feb 2022 00:18:49 +0000 Subject: [PATCH 27/78] fix linting --- src/Psalm/Codebase.php | 73 ++++ .../Internal/Analyzer/ProjectAnalyzer.php | 7 - src/Psalm/Internal/Cli/LanguageServer.php | 5 +- .../LanguageServer/Client/TextDocument.php | 8 +- .../LanguageServer/ClientConfiguration.php | 2 + .../Internal/LanguageServer/Codebase.php | 76 +--- .../LanguageServer/LanguageClient.php | 7 +- .../LanguageServer/LanguageServer.php | 6 +- .../LanguageServer/PHPMarkdownContent.php | 2 +- .../Internal/LanguageServer/Progress.php | 6 +- .../Internal/LanguageServer/Reference.php | 2 +- .../Internal/Provider/FakeFileProvider.php | 6 +- src/Psalm/Internal/Provider/FileProvider.php | 13 +- src/Psalm/Storage/ClassConstantStorage.php | 4 + tests/FileUpdates/TemporaryUpdateTest.php | 16 +- tests/LanguageServer/CompletionTest.php | 112 +++--- tests/LanguageServer/FileMapTest.php | 26 +- tests/LanguageServer/SymbolLookupTest.php | 325 +++++++++++------- 18 files changed, 415 insertions(+), 281 deletions(-) diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index e1c689cf7f1..c0d9760be9c 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -2,10 +2,13 @@ namespace Psalm; +use InvalidArgumentException; use PhpParser; use PhpParser\Node\Arg; use Psalm\CodeLocation; use Psalm\Exception\UnpopulatedClasslikeException; +use Psalm\Internal\Analyzer\FunctionLikeAnalyzer; +use Psalm\Internal\Analyzer\ProjectAnalyzer; use Psalm\Internal\Analyzer\Statements\Block\ForeachAnalyzer; use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Codebase\Analyzer; @@ -41,8 +44,10 @@ use ReflectionType; use UnexpectedValueException; +use function array_combine; use function array_merge; use function explode; +use function in_array; use function is_string; use function strpos; use function strtolower; @@ -52,6 +57,7 @@ use const PHP_MINOR_VERSION; use const PHP_VERSION_ID; + class Codebase { /** @@ -143,6 +149,7 @@ class Codebase /** * @var Analyzer + * @psalm-suppress PropertyNotSetInConstructor */ public $analyzer; @@ -1031,4 +1038,70 @@ public function addTaintSink( $this->taint_flow_graph->addSink($sink); } + + /** + * @param array $candidate_files + * + */ + public function reloadFiles(ProjectAnalyzer $project_analyzer, array $candidate_files, bool $force = false): void + { + $this->loadAnalyzer(); + + if ($force) { + FileReferenceProvider::clearCache(); + } + + $this->file_reference_provider->loadReferenceCache($force); + + FunctionLikeAnalyzer::clearCache(); + + if ($force || !$this->statements_provider->parser_cache_provider) { + $diff_files = $candidate_files; + } else { + $diff_files = []; + + $parser_cache_provider = $this->statements_provider->parser_cache_provider; + + foreach ($candidate_files as $candidate_file_path) { + if ($parser_cache_provider->loadExistingFileContentsFromCache($candidate_file_path) + !== $this->file_provider->getContents($candidate_file_path) + ) { + $diff_files[] = $candidate_file_path; + } + } + } + + $referenced_files = $project_analyzer->getReferencedFilesFromDiff($diff_files, false); + + foreach ($diff_files as $diff_file_path) { + $this->invalidateInformationForFile($diff_file_path); + } + + foreach ($referenced_files as $referenced_file_path) { + if (in_array($referenced_file_path, $diff_files, true)) { + continue; + } + + $file_storage = $this->file_storage_provider->get($referenced_file_path); + + foreach ($file_storage->classlikes_in_file as $fq_classlike_name) { + $this->classlike_storage_provider->remove($fq_classlike_name); + $this->classlikes->removeClassLike($fq_classlike_name); + } + + $this->file_storage_provider->remove($referenced_file_path); + $this->scanner->removeFile($referenced_file_path); + } + + $referenced_files = array_combine($referenced_files, $referenced_files); + + $this->scanner->addFilesToDeepScan($referenced_files); + $this->addFilesToAnalyze(array_combine($candidate_files, $candidate_files)); + + $this->scanner->scanFiles($this->classlikes); + + $this->file_reference_provider->updateReferenceCache($this, $referenced_files); + + $this->populator->populateCodebase(); + } } diff --git a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php index 30f925e39bc..9dcd493c27e 100644 --- a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php @@ -245,13 +245,6 @@ class ProjectAnalyzer UnnecessaryVarAnnotation::class, ]; - /** - * When this is true, the language server will send the diagnostic code with a help link. - * - * @var bool - */ - public $language_server_use_extended_diagnostic_codes = false; - /** * @param array $generated_report_options */ diff --git a/src/Psalm/Internal/Cli/LanguageServer.php b/src/Psalm/Internal/Cli/LanguageServer.php index fa1e09a4bde..6cd7b4e33aa 100644 --- a/src/Psalm/Internal/Cli/LanguageServer.php +++ b/src/Psalm/Internal/Cli/LanguageServer.php @@ -49,7 +49,10 @@ final class LanguageServer { - /** @param array $argv */ + /** + * @param array $argv + * @psalm-suppress ComplexMethod + */ public static function run(array $argv): void { $clientConfiguration = new ClientConfiguration(); diff --git a/src/Psalm/Internal/LanguageServer/Client/TextDocument.php b/src/Psalm/Internal/LanguageServer/Client/TextDocument.php index d35646f5b6c..9446bc41316 100644 --- a/src/Psalm/Internal/LanguageServer/Client/TextDocument.php +++ b/src/Psalm/Internal/LanguageServer/Client/TextDocument.php @@ -19,20 +19,14 @@ class TextDocument */ private $handler; - /** - * @var JsonMapper - */ - private $mapper; - /** * @var LanguageServer */ private $server; - public function __construct(ClientHandler $handler, JsonMapper $mapper, LanguageServer $server) + public function __construct(ClientHandler $handler, LanguageServer $server) { $this->handler = $handler; - $this->mapper = $mapper; $this->server = $server; } diff --git a/src/Psalm/Internal/LanguageServer/ClientConfiguration.php b/src/Psalm/Internal/LanguageServer/ClientConfiguration.php index 78f87dcbb25..e6242eb6700 100644 --- a/src/Psalm/Internal/LanguageServer/ClientConfiguration.php +++ b/src/Psalm/Internal/LanguageServer/ClientConfiguration.php @@ -76,6 +76,7 @@ class ClientConfiguration * Provide Completion or not * * @var bool|null + * @psalm-suppress PossiblyUnusedProperty */ public $findUnusedVariables; @@ -92,6 +93,7 @@ class ClientConfiguration * @var int|null * * @see MessageType + * @psalm-suppress PossiblyUnusedProperty */ public $logLevel; diff --git a/src/Psalm/Internal/LanguageServer/Codebase.php b/src/Psalm/Internal/LanguageServer/Codebase.php index b0ae951ea28..de7927d2c45 100644 --- a/src/Psalm/Internal/LanguageServer/Codebase.php +++ b/src/Psalm/Internal/LanguageServer/Codebase.php @@ -19,14 +19,11 @@ use Psalm\CodeLocation\Raw; use Psalm\Codebase as PsalmCodebase; use Psalm\Exception\UnanalyzedFileException; -use Psalm\Internal\Analyzer\FunctionLikeAnalyzer; use Psalm\Internal\Analyzer\NamespaceAnalyzer; -use Psalm\Internal\Analyzer\ProjectAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\Fetch\ConstFetchAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\Fetch\VariableFetchAnalyzer; use Psalm\Internal\Codebase\InternalCallMapHandler; use Psalm\Internal\MethodIdentifier; -use Psalm\Internal\Provider\FileReferenceProvider; use Psalm\Type; use Psalm\Type\Atomic\TBool; use Psalm\Type\Atomic\TClassConstant; @@ -38,7 +35,6 @@ use ReflectionProperty; use UnexpectedValueException; -use function array_combine; use function array_pop; use function array_reverse; use function count; @@ -46,7 +42,6 @@ use function error_log; use function explode; use function implode; -use function in_array; use function is_numeric; use function krsort; use function ksort; @@ -324,6 +319,7 @@ public function getCompletionDataAtPosition(string $file_path, Position $positio continue; } + /** @psalm-suppress PossiblyUndefinedIntArrayOffset */ $num_whitespace_bytes = preg_match('/\G\s+/', $file_contents, $matches, 0, $end_pos_excluding_whitespace) ? strlen($matches[0]) : 0; @@ -888,6 +884,10 @@ public function getSymbolLocation(Reference $reference): ?CodeLocation $symbol = preg_replace('/:.*/', '', $reference->symbol); $symbol_parts = explode('-', $symbol); + if (!isset($symbol_parts[0]) || !isset($symbol_parts[1])) { + return null; + } + $file_contents = $this->getFileContents($reference->file_path); return new Raw( @@ -973,70 +973,4 @@ public function removeTemporaryFileChanges(string $file_path): void { $this->file_provider->removeTemporaryFileChanges($file_path); } - - /** - * @param array $candidate_files - * - */ - public function reloadFiles(ProjectAnalyzer $project_analyzer, array $candidate_files, bool $force = false): void - { - $this->loadAnalyzer(); - - if ($force) { - FileReferenceProvider::clearCache(); - } - - $this->file_reference_provider->loadReferenceCache($force); - - FunctionLikeAnalyzer::clearCache(); - - if ($force || !$this->statements_provider->parser_cache_provider) { - $diff_files = $candidate_files; - } else { - $diff_files = []; - - $parser_cache_provider = $this->statements_provider->parser_cache_provider; - - foreach ($candidate_files as $candidate_file_path) { - if ($parser_cache_provider->loadExistingFileContentsFromCache($candidate_file_path) - !== $this->file_provider->getContents($candidate_file_path) - ) { - $diff_files[] = $candidate_file_path; - } - } - } - - $referenced_files = $project_analyzer->getReferencedFilesFromDiff($diff_files, false); - - foreach ($diff_files as $diff_file_path) { - $this->invalidateInformationForFile($diff_file_path); - } - - foreach ($referenced_files as $referenced_file_path) { - if (in_array($referenced_file_path, $diff_files, true)) { - continue; - } - - $file_storage = $this->file_storage_provider->get($referenced_file_path); - - foreach ($file_storage->classlikes_in_file as $fq_classlike_name) { - $this->classlike_storage_provider->remove($fq_classlike_name); - $this->classlikes->removeClassLike($fq_classlike_name); - } - - $this->file_storage_provider->remove($referenced_file_path); - $this->scanner->removeFile($referenced_file_path); - } - - $referenced_files = array_combine($referenced_files, $referenced_files); - - $this->scanner->addFilesToDeepScan($referenced_files); - $this->addFilesToAnalyze(array_combine($candidate_files, $candidate_files)); - - $this->scanner->scanFiles($this->classlikes); - - $this->file_reference_provider->updateReferenceCache($this, $referenced_files); - - $this->populator->populateCodebase(); - } } diff --git a/src/Psalm/Internal/LanguageServer/LanguageClient.php b/src/Psalm/Internal/LanguageServer/LanguageClient.php index 2a9043ef7c0..791a86dec1a 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageClient.php +++ b/src/Psalm/Internal/LanguageServer/LanguageClient.php @@ -69,7 +69,7 @@ public function __construct( $this->mapper = new JsonMapper; $this->server = $server; - $this->textDocument = new ClientTextDocument($this->handler, $this->mapper, $this->server); + $this->textDocument = new ClientTextDocument($this->handler, $this->server); $this->workspace = new ClientWorkspace($this->handler, $this->mapper, $this->server); $this->clientConfiguration = $clientConfiguration; } @@ -85,7 +85,9 @@ public function refreshConfiguration(): void if ($error) { $this->server->logError('There was an error getting configuration'); } else { - $this->mapper->map($value[0], $this->clientConfiguration); + /** @var array $value */ + [$config] = $value; + $this->mapper->map($config, $this->clientConfiguration); $this->configurationRefreshed(); } }); @@ -97,6 +99,7 @@ public function refreshConfiguration(): void * The amount and content of these notifications depends on the current trace configuration. * * @param LogTrace $logTrace + * @psalm-suppress PossiblyUnusedMethod */ public function logTrace(LogTrace $logTrace): void { diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 740d6ce2cfa..73dbf236e95 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -657,7 +657,7 @@ public function queueClosedFileAnalysis(string $file_path, string $uri): void */ public function queueSaveFileAnalysis(string $file_path, string $uri): void { - $this->queueFileAnalysisWithOpenedFiles([$file_path => $this->pathToUri($file_path)]); + $this->queueFileAnalysisWithOpenedFiles([$file_path => $uri]); } /** @@ -855,6 +855,10 @@ public function exit(): void */ public function log(int $type, string $message, array $context = []): void { + if ($this->client->clientConfiguration->logLevel < $type) { + return; + } + if (!empty($context)) { $message .= "\n" . json_encode($context, JSON_PRETTY_PRINT); } diff --git a/src/Psalm/Internal/LanguageServer/PHPMarkdownContent.php b/src/Psalm/Internal/LanguageServer/PHPMarkdownContent.php index a0e05aeb31f..5ac0d09071a 100644 --- a/src/Psalm/Internal/LanguageServer/PHPMarkdownContent.php +++ b/src/Psalm/Internal/LanguageServer/PHPMarkdownContent.php @@ -23,4 +23,4 @@ public function __construct(string $code, ?string $title = null, ?string $descri "$markdown```php\nserver = $server; diff --git a/src/Psalm/Internal/LanguageServer/Reference.php b/src/Psalm/Internal/LanguageServer/Reference.php index 16965bdb641..d43699aae61 100644 --- a/src/Psalm/Internal/LanguageServer/Reference.php +++ b/src/Psalm/Internal/LanguageServer/Reference.php @@ -31,4 +31,4 @@ public function __construct(string $file_path, string $symbol, Range $range) $this->symbol = $symbol; $this->range = $range; } -} \ No newline at end of file +} diff --git a/src/Psalm/Internal/Provider/FakeFileProvider.php b/src/Psalm/Internal/Provider/FakeFileProvider.php index 5d506bdeb2d..3972dcb1d34 100644 --- a/src/Psalm/Internal/Provider/FakeFileProvider.php +++ b/src/Psalm/Internal/Provider/FakeFileProvider.php @@ -26,7 +26,7 @@ public function fileExists(string $file_path): bool public function getContents(string $file_path, bool $go_to_source = false): string { if (!$go_to_source && isset($this->temp_files[strtolower($file_path)])) { - return $this->temp_files[strtolower($file_path)]; + return $this->temp_files[strtolower($file_path)]['content']; } return $this->fake_files[$file_path] ?? parent::getContents($file_path); @@ -37,10 +37,10 @@ public function setContents(string $file_path, string $file_contents): void $this->fake_files[$file_path] = $file_contents; } - public function setOpenContents(string $file_path, string $file_contents): void + public function setOpenContents(string $file_path, ?string $file_contents = null): void { if (isset($this->fake_files[strtolower($file_path)])) { - $this->fake_files[strtolower($file_path)] = $file_contents; + $this->fake_files[strtolower($file_path)] = $file_contents ?? $this->getContents($file_path, true); } } diff --git a/src/Psalm/Internal/Provider/FileProvider.php b/src/Psalm/Internal/Provider/FileProvider.php index f8a674a2f11..a96cfae3cfe 100644 --- a/src/Psalm/Internal/Provider/FileProvider.php +++ b/src/Psalm/Internal/Provider/FileProvider.php @@ -22,7 +22,7 @@ class FileProvider { /** - * @var array + * @var array */ protected $temp_files = []; @@ -66,7 +66,10 @@ public function setContents(string $file_path, string $file_contents): void } if (isset($this->temp_files[$file_path_lc])) { - $this->temp_files[$file_path_lc] = $file_contents; + $this->temp_files[$file_path_lc] = [ + 'version'=> null, + 'content' => $file_contents + ]; } file_put_contents($file_path, $file_contents); @@ -129,7 +132,11 @@ public function isOpen(string $file_path): bool public function closeFile(string $file_path): void { $file_path_lc = strtolower($file_path); - unset($this->temp_files[$file_path_lc], $this->open_files[$file_path_lc], $this->open_files_paths[$file_path_lc]); + unset( + $this->temp_files[$file_path_lc], + $this->open_files[$file_path_lc], + $this->open_files_paths[$file_path_lc] + ); } public function fileExists(string $file_path): bool diff --git a/src/Psalm/Storage/ClassConstantStorage.php b/src/Psalm/Storage/ClassConstantStorage.php index 43fc5a494a8..536a8b2fda4 100644 --- a/src/Psalm/Storage/ClassConstantStorage.php +++ b/src/Psalm/Storage/ClassConstantStorage.php @@ -7,6 +7,9 @@ use Psalm\Internal\Scanner\UnresolvedConstantComponent; use Psalm\Type\Union; +use function array_values; +use function property_exists; + class ClassConstantStorage { /** @@ -83,6 +86,7 @@ public function getHoverMarkdown(string $const): string $types = $this->type->getAtomicTypes(); $type = array_values($types)[0]; if (property_exists($type, 'value')) { + /** @psalm-suppress UndefinedPropertyFetch */ $value = " = {$type->value};"; } } diff --git a/tests/FileUpdates/TemporaryUpdateTest.php b/tests/FileUpdates/TemporaryUpdateTest.php index 529b8f2ff07..4c323b13aaf 100644 --- a/tests/FileUpdates/TemporaryUpdateTest.php +++ b/tests/FileUpdates/TemporaryUpdateTest.php @@ -4,6 +4,7 @@ use Psalm\Config; use Psalm\Internal\Analyzer\ProjectAnalyzer; +use Psalm\Internal\LanguageServer\Codebase; use Psalm\Internal\Provider\FakeFileProvider; use Psalm\Internal\Provider\Providers; use Psalm\IssueBuffer; @@ -24,6 +25,9 @@ class TemporaryUpdateTest extends TestCase { + /** @var Codebase */ + protected $codebase; + public function setUp(): void { parent::setUp(); @@ -42,10 +46,18 @@ public function setUp(): void new ProjectCacheProvider() ); + $this->codebase = new Codebase($config, $providers); + $this->project_analyzer = new ProjectAnalyzer( $config, - $providers + $providers, + null, + [], + 1, + null, + $this->codebase ); + $this->project_analyzer->setPhpVersion('7.3', 'tests'); } @@ -64,7 +76,7 @@ public function testErrorFix( bool $test_save = true, bool $check_unused_code = false ): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $codebase->diff_methods = true; if ($check_unused_code) { diff --git a/tests/LanguageServer/CompletionTest.php b/tests/LanguageServer/CompletionTest.php index ce44833a224..31bc2491bfa 100644 --- a/tests/LanguageServer/CompletionTest.php +++ b/tests/LanguageServer/CompletionTest.php @@ -5,6 +5,7 @@ use LanguageServerProtocol\Position; use Psalm\Context; use Psalm\Internal\Analyzer\ProjectAnalyzer; +use Psalm\Internal\LanguageServer\Codebase; use Psalm\Internal\Provider\FakeFileProvider; use Psalm\Internal\Provider\Providers; use Psalm\Tests\Internal\Provider\FakeFileReferenceCacheProvider; @@ -18,6 +19,9 @@ class CompletionTest extends TestCase { + /** @var Codebase */ + protected $codebase; + public function setUp(): void { parent::setUp(); @@ -35,17 +39,25 @@ public function setUp(): void new ProjectCacheProvider() ); + $this->codebase = new Codebase($config, $providers); + $this->project_analyzer = new ProjectAnalyzer( $config, - $providers + $providers, + null, + [], + 1, + null, + $this->codebase ); + $this->project_analyzer->setPhpVersion('7.3', 'tests'); $this->project_analyzer->getCodebase()->store_node_types = true; } public function testCompletionOnThisWithNoAssignment(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -73,7 +85,7 @@ public function foo() { public function testCompletionOnThisWithAssignmentBelow(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -103,7 +115,7 @@ public function foo() : self { public function testCompletionOnThisWithIfBelow(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -152,7 +164,7 @@ public function foo() : self { public function testCompletionOnSelfWithIfBelow(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -201,7 +213,7 @@ public function foo() : self { public function testCompletionOnSelfWithListBelow(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -250,7 +262,7 @@ public function foo() : self { public function testCompletionOnThisProperty(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -277,7 +289,7 @@ public function foo() : void { }' ); - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $codebase->file_provider->openFile('somefile.php'); $codebase->scanFiles(); @@ -288,7 +300,7 @@ public function foo() : void { public function testCompletionOnThisPropertyWithCharacter(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -315,7 +327,7 @@ public function foo() : void { }' ); - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $codebase->file_provider->openFile('somefile.php'); $codebase->scanFiles(); @@ -326,7 +338,7 @@ public function foo() : void { public function testCompletionOnThisPropertyWithAnotherCharacter(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -353,7 +365,7 @@ public function foo() : void { }' ); - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $codebase->file_provider->openFile('somefile.php'); $codebase->scanFiles(); @@ -364,7 +376,7 @@ public function foo() : void { public function testCompletionOnTemplatedThisProperty(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -400,7 +412,7 @@ public function foo() : void { }' ); - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $codebase->file_provider->openFile('somefile.php'); $codebase->scanFiles(); @@ -417,7 +429,7 @@ public function foo() : void { public function testCompletionOnMethodReturnValue(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -446,7 +458,7 @@ function foo(A $a) { public function testCompletionOnMethodArgument(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -477,7 +489,7 @@ function bar(A $a, C $c) { public function testCompletionOnMethodReturnValueWithArgument(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -508,7 +520,7 @@ function bar(A $a, C $c) { public function testCompletionOnVariableWithWhitespace(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -533,7 +545,7 @@ function bar(A $a) { public function testCompletionOnVariableWithWhitespaceAndReturn(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -560,7 +572,7 @@ function baz(A $a) { public function testCompletionOnMethodReturnValueWithWhitespace(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -590,7 +602,7 @@ function bar(A $a) { public function testCompletionOnMethodReturnValueWithWhitespaceAndReturn(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -621,7 +633,7 @@ function baz(A $a) { public function testCompletionOnMethodReturnValueWhereParamIsClosure(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -650,7 +662,7 @@ function bar(Collection $a) { public function testCompletionOnMethodReturnValueWhereParamIsClosureWithStmt(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -679,7 +691,7 @@ function baz(Collection $a) { public function testCursorPositionOnMethodCompletion(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -716,7 +728,7 @@ public function baz() {} public function testCompletionOnNewExceptionWithoutNamespace(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -737,7 +749,7 @@ function foo() : void { public function testCompletionOnNewExceptionWithNamespaceNoUse(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -784,7 +796,7 @@ function foo() : void { public function testCompletionOnNewExceptionWithNamespaceAndUse(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -833,7 +845,7 @@ function foo() : void { public function testCompletionOnNamespaceWithFullyQualified(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -876,7 +888,7 @@ public function foo() : void { public function testCompletionOnExceptionWithNamespaceAndUseInClass(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -920,7 +932,7 @@ public function foo() : void { public function testCompletionForFunctionNames(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -956,7 +968,7 @@ function my_function_in_bar() : void { public function testCompletionForNamespacedOverriddenFunctionNames(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -986,7 +998,7 @@ function strlen() : void { public function testCompletionForFunctionNamesRespectUsedNamespaces(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -1013,7 +1025,7 @@ public function testCompletionForFunctionNamesRespectUsedNamespaces(): void public function testCompletionForFunctionNamesRespectCase(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -1039,7 +1051,7 @@ public function testCompletionForFunctionNamesRespectCase(): void public function testGetMatchingFunctionNames(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -1064,7 +1076,7 @@ function my_function() { public function testGetMatchingFunctionNamesFromPredefinedFunctions(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -1083,7 +1095,7 @@ public function testGetMatchingFunctionNamesFromPredefinedFunctions(): void public function testGetMatchingFunctionNamesFromUsedFunction(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -1106,7 +1118,7 @@ public function testGetMatchingFunctionNamesFromUsedFunction(): void public function testGetMatchingFunctionNamesFromUsedNamespace(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -1129,7 +1141,7 @@ public function testGetMatchingFunctionNamesFromUsedNamespace(): void public function testGetMatchingFunctionNamesFromUsedNamespaceRespectFirstCharCase(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -1152,7 +1164,7 @@ public function testGetMatchingFunctionNamesFromUsedNamespaceRespectFirstCharCas public function testGetMatchingFunctionNamesWithNamespace(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -1177,7 +1189,7 @@ function my_function() { public function testCompletionOnInstanceofWithNamespaceAndUse(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -1220,7 +1232,7 @@ function foo($a) : void { public function testCompletionOnClassReference(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -1252,7 +1264,7 @@ static function add() : void { public function testCompletionOnClassInstanceReferenceWithAssignmentAfter(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -1287,7 +1299,7 @@ public function add() : void {} public function testCompletionOnClassStaticReferenceWithAssignmentAfter(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -1320,7 +1332,7 @@ static function add() : void {} public function testNoCrashOnLoopId(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -1338,7 +1350,7 @@ public function testNoCrashOnLoopId(): void public function testCompletionOnArrayKey(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -1372,7 +1384,7 @@ public function testCompletionOnArrayKey(): void public function testTypeContextForFunctionArgument(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -1396,7 +1408,7 @@ function my_func(string $arg_a, bool $arg_b) : string { public function testTypeContextForFunctionArgumentWithWhiteSpace(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -1420,7 +1432,7 @@ function my_func(string $arg_a, bool $arg_b) : string { public function testCallStaticInInstance(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -1439,7 +1451,7 @@ public function baz() : void {} }' ); - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $codebase->file_provider->openFile('somefile.php'); $codebase->scanFiles(); @@ -1456,7 +1468,7 @@ public function baz() : void {} public function testCompletionsForType(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; diff --git a/tests/LanguageServer/FileMapTest.php b/tests/LanguageServer/FileMapTest.php index 1e3bc9f63b6..5ed280317ab 100644 --- a/tests/LanguageServer/FileMapTest.php +++ b/tests/LanguageServer/FileMapTest.php @@ -4,6 +4,7 @@ use Psalm\Context; use Psalm\Internal\Analyzer\ProjectAnalyzer; +use Psalm\Internal\LanguageServer\Codebase; use Psalm\Internal\Provider\FakeFileProvider; use Psalm\Internal\Provider\Providers; use Psalm\Tests\Internal\Provider\FakeFileReferenceCacheProvider; @@ -16,6 +17,9 @@ class FileMapTest extends TestCase { + /** @var Codebase */ + protected $codebase; + public function setUp(): void { parent::setUp(); @@ -33,17 +37,25 @@ public function setUp(): void new ProjectCacheProvider() ); + $this->codebase = new Codebase($config, $providers); + $this->project_analyzer = new ProjectAnalyzer( $config, - $providers + $providers, + null, + [], + 1, + null, + $this->codebase ); + $this->project_analyzer->setPhpVersion('7.3', 'tests'); $this->project_analyzer->getCodebase()->store_node_types = true; } public function testMapIsUpdatedOnReloadFiles(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -73,7 +85,7 @@ function __construct( string $var ) { public function testGetTypeMap(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $config = $codebase->config; $config->throw_exception = false; @@ -109,7 +121,7 @@ function __construct( string $var ) { public function testMapIsUpdatedAfterEditingMethod(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $codebase->diff_methods = true; $config = $codebase->config; $config->throw_exception = false; @@ -163,7 +175,7 @@ public function second(\DateTimeImmutable $d) : void { public function testMapIsUpdatedAfterDeletingFirstMethod(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $codebase->diff_methods = true; $config = $codebase->config; $config->throw_exception = false; @@ -211,7 +223,7 @@ public function second_method(\DateTimeImmutable $d) : void { public function testMapIsUpdatedAfterDeletingSecondMethod(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $codebase->diff_methods = true; $config = $codebase->config; $config->throw_exception = false; @@ -257,7 +269,7 @@ public function second(\DateTimeImmutable $d) : void { public function testMapIsUpdatedAfterAddingMethod(): void { - $codebase = $this->project_analyzer->getCodebase(); + $codebase = $this->codebase; $codebase->diff_methods = true; $config = $codebase->config; $config->throw_exception = false; diff --git a/tests/LanguageServer/SymbolLookupTest.php b/tests/LanguageServer/SymbolLookupTest.php index c5ab588f23f..47a6abef6bf 100644 --- a/tests/LanguageServer/SymbolLookupTest.php +++ b/tests/LanguageServer/SymbolLookupTest.php @@ -3,9 +3,12 @@ namespace Psalm\Tests\LanguageServer; use LanguageServerProtocol\Position; +use LanguageServerProtocol\Range; use Psalm\Context; use Psalm\Internal\Analyzer\FileAnalyzer; use Psalm\Internal\Analyzer\ProjectAnalyzer; +use Psalm\Internal\LanguageServer\Codebase; +use Psalm\Internal\LanguageServer\Reference; use Psalm\Internal\Provider\FakeFileProvider; use Psalm\Internal\Provider\Providers; use Psalm\Tests\Internal\Provider\FakeFileReferenceCacheProvider; @@ -16,6 +19,9 @@ class SymbolLookupTest extends TestCase { + /** @var Codebase */ + protected $codebase; + public function setUp(): void { parent::setUp(); @@ -33,9 +39,16 @@ public function setUp(): void new ProjectCacheProvider() ); + $this->codebase = new Codebase($config, $providers); + $this->project_analyzer = new ProjectAnalyzer( $config, - $providers + $providers, + null, + [], + 1, + null, + $this->codebase ); $this->project_analyzer->setPhpVersion('7.3', 'tests'); @@ -83,41 +96,89 @@ function qux(int $a, int $b) : int { new FileAnalyzer($this->project_analyzer, 'somefile.php', 'somefile.php'); - $codebase = $this->project_analyzer->getCodebase(); - $this->analyzeFile('somefile.php', new Context()); - $information = $codebase->getSymbolInformation('somefile.php', 'B\A::foo()'); - $this->assertNotNull($information); - $this->assertSame('getSymbolInformation('somefile.php', 'B\A::$a'); + $information = $this->codebase->getMarkupContentForSymbol( + new Reference( + 'somefile.php', + 'B\A::foo()', + $range + ) + ); $this->assertNotNull($information); - $this->assertSame('getSymbolInformation('somefile.php', 'B\bar()'); + $this->assertSame('value); + + $information = $this->codebase->getMarkupContentForSymbol( + new Reference( + 'somefile.php', + 'B\A::$a', + $range + ) + ); $this->assertNotNull($information); - $this->assertSame('getSymbolInformation('somefile.php', 'B\A::BANANA'); + $this->assertSame('value); + + $information = $this->codebase->getMarkupContentForSymbol( + new Reference( + 'somefile.php', + 'B\bar()', + $range + ) + ); $this->assertNotNull($information); - $this->assertSame('getSymbolInformation('somefile.php', 'B\baz()'); + $this->assertSame('value); + + $information = $this->codebase->getMarkupContentForSymbol( + new Reference( + 'somefile.php', + 'B\A::BANANA', + $range + ) + ); $this->assertNotNull($information); - $this->assertSame("getSymbolInformation('somefile.php', 'B\qux()'); + $this->assertSame('value); + + $information = $this->codebase->getMarkupContentForSymbol( + new Reference( + 'somefile.php', + 'B\baz()', + $range + ) + ); $this->assertNotNull($information); - $this->assertSame("getSymbolInformation('somefile.php', '$_SERVER'); + $this->assertSame("value); + + $information = $this->codebase->getMarkupContentForSymbol( + new Reference( + 'somefile.php', + 'B\qux()', + $range + ) + ); $this->assertNotNull($information); - $this->assertSame("", $information['type']); - - $information = $codebase->getSymbolInformation('somefile.php', '$my_global'); + $this->assertSame("value); + + $information = $this->codebase->getMarkupContentForSymbol( + new Reference( + 'somefile.php', + '$_SERVER', + $range + ) + ); $this->assertNotNull($information); - $this->assertSame("assertSame("", $information->value); + + $information = $this->codebase->getMarkupContentForSymbol( + new Reference( + 'somefile.php', + '$my_global', + $range + ) + ); + $this->assertNotNull($information); + $this->assertSame("value); } public function testSimpleSymbolLookupGlobalConst(): void @@ -131,16 +192,29 @@ public function testSimpleSymbolLookupGlobalConst(): void new FileAnalyzer($this->project_analyzer, 'somefile.php', 'somefile.php'); - $codebase = $this->project_analyzer->getCodebase(); + $range = new Range(new Position(1, 1), new Position(1, 1)); $this->analyzeFile('somefile.php', new Context()); - $information = $codebase->getSymbolInformation('somefile.php', 'APPLE'); - $this->assertNotNull($information); - $this->assertSame("getSymbolInformation('somefile.php', 'BANANA'); + $information = $this->codebase->getMarkupContentForSymbol( + new Reference( + 'somefile.php', + 'APPLE', + $range + ) + ); + $this->assertNotNull($information); + $this->assertSame("value); + + $information = $this->codebase->getMarkupContentForSymbol( + new Reference( + 'somefile.php', + 'BANANA', + $range + ) + ); $this->assertNotNull($information); - $this->assertSame("assertSame("value); } public function testSimpleSymbolLocation(): void @@ -169,35 +243,56 @@ function bar() : int { new FileAnalyzer($this->project_analyzer, 'somefile.php', 'somefile.php'); - $codebase = $this->project_analyzer->getCodebase(); $this->analyzeFile('somefile.php', new Context()); - $method_symbol_location = $codebase->getSymbolLocation('somefile.php', 'B\A::foo()'); + $range = new Range(new Position(1, 1), new Position(1, 1)); + + $method_symbol_location = $this->codebase->getSymbolLocation(new Reference( + 'somefile.php', + 'B\A::foo()', + $range + )); $this->assertNotNull($method_symbol_location); $this->assertSame(10, $method_symbol_location->getLineNumber()); $this->assertSame(37, $method_symbol_location->getColumn()); - $property_symbol_location = $codebase->getSymbolLocation('somefile.php', 'B\A::$a'); + $property_symbol_location = $this->codebase->getSymbolLocation(new Reference( + 'somefile.php', + 'B\A::$a', + $range + )); $this->assertNotNull($property_symbol_location); $this->assertSame(6, $property_symbol_location->getLineNumber()); $this->assertSame(31, $property_symbol_location->getColumn()); - $constant_symbol_location = $codebase->getSymbolLocation('somefile.php', 'B\A::BANANA'); + $constant_symbol_location = $this->codebase->getSymbolLocation(new Reference( + 'somefile.php', + 'B\A::BANANA', + $range + )); $this->assertNotNull($constant_symbol_location); $this->assertSame(8, $constant_symbol_location->getLineNumber()); $this->assertSame(27, $constant_symbol_location->getColumn()); - $function_symbol_location = $codebase->getSymbolLocation('somefile.php', 'B\bar()'); + $function_symbol_location = $this->codebase->getSymbolLocation(new Reference( + 'somefile.php', + 'B\bar()', + $range + )); $this->assertNotNull($function_symbol_location); $this->assertSame(16, $function_symbol_location->getLineNumber()); $this->assertSame(26, $function_symbol_location->getColumn()); - $function_symbol_location = $codebase->getSymbolLocation('somefile.php', '257-259'); + $function_symbol_location = $this->codebase->getSymbolLocation(new Reference( + 'somefile.php', + '257-259', + $range + )); $this->assertNotNull($function_symbol_location); $this->assertSame(11, $function_symbol_location->getLineNumber()); @@ -206,8 +301,7 @@ function bar() : int { public function testSymbolLookupAfterAlteration(): void { - $codebase = $this->project_analyzer->getCodebase(); - $config = $codebase->config; + $config = $this->codebase->config; $config->throw_exception = false; $this->addFile( @@ -234,11 +328,11 @@ public function bar() : void { }' ); - $codebase->file_provider->openFile('somefile.php'); - $codebase->scanFiles(); + $this->codebase->file_provider->openFile('somefile.php'); + $this->codebase->scanFiles(); $this->analyzeFile('somefile.php', new Context()); - $codebase->addTemporaryFileChanges( + $this->codebase->addTemporaryFileChanges( 'somefile.php', 'reloadFiles($this->project_analyzer, ['somefile.php']); + $this->codebase->reloadFiles($this->project_analyzer, ['somefile.php']); - $codebase->analyzer->analyzeFiles($this->project_analyzer, 1, false); + $this->codebase->analyzer->analyzeFiles($this->project_analyzer, 1, false); - $symbol_at_position = $codebase->getReferenceAtPosition('somefile.php', new Position(10, 30)); + $reference = $this->codebase->getReferenceAtPosition('somefile.php', new Position(10, 30)); - $this->assertNotNull($symbol_at_position); + $this->assertNotNull($reference); - $this->assertSame('245-246:int|null', $symbol_at_position[0]); + $this->assertSame('245-246:int|null', $reference->symbol); - $symbol_at_position = $codebase->getReferenceAtPosition('somefile.php', new Position(12, 30)); + $reference = $this->codebase->getReferenceAtPosition('somefile.php', new Position(12, 30)); - $this->assertNotNull($symbol_at_position); + $this->assertNotNull($reference); - $this->assertSame('213-214:1', $symbol_at_position[0]); + $this->assertSame('213-214:1', $reference->symbol); - $symbol_at_position = $codebase->getReferenceAtPosition('somefile.php', new Position(17, 30)); + $reference = $this->codebase->getReferenceAtPosition('somefile.php', new Position(17, 30)); - $this->assertNotNull($symbol_at_position); + $this->assertNotNull($reference); - $this->assertSame('425-426:2', $symbol_at_position[0]); + $this->assertSame('425-426:2', $reference->symbol); } public function testGetSymbolPositionMissingArg(): void { - $codebase = $this->project_analyzer->getCodebase(); - $config = $codebase->config; + $config = $this->codebase->config; $config->throw_exception = false; $this->addFile( @@ -307,22 +400,21 @@ public function bar() : void { }' ); - $codebase->file_provider->openFile('somefile.php'); - $codebase->scanFiles(); + $this->codebase->file_provider->openFile('somefile.php'); + $this->codebase->scanFiles(); $this->analyzeFile('somefile.php', new Context()); - $symbol_at_position = $codebase->getReferenceAtPosition('somefile.php', new Position(9, 33)); + $reference = $this->codebase->getReferenceAtPosition('somefile.php', new Position(9, 33)); - $this->assertNotNull($symbol_at_position); + $this->assertNotNull($reference); - $this->assertSame('B\A::foo()', $symbol_at_position[0]); + $this->assertSame('B\A::foo()', $reference->symbol); } public function testGetSymbolPositionGlobalVariable(): void { - $codebase = $this->project_analyzer->getCodebase(); - $codebase->reportUnusedVariables(); - $config = $codebase->config; + $this->codebase->reportUnusedVariables(); + $config = $this->codebase->config; $config->throw_exception = false; $config->globals['$my_global'] = 'string'; @@ -335,23 +427,22 @@ function foo() : void { }' ); - $codebase->file_provider->openFile('somefile.php'); - $codebase->scanFiles(); + $this->codebase->file_provider->openFile('somefile.php'); + $this->codebase->scanFiles(); $this->analyzeFile('somefile.php', new Context()); - $symbol_at_position = $codebase->getReferenceAtPosition('somefile.php', new Position(2, 31)); - $this->assertNotNull($symbol_at_position); - $this->assertSame('$my_global', $symbol_at_position[0]); + $reference = $this->codebase->getReferenceAtPosition('somefile.php', new Position(2, 31)); + $this->assertNotNull($reference); + $this->assertSame('$my_global', $reference->symbol); - $symbol_at_position = $codebase->getReferenceAtPosition('somefile.php', new Position(3, 28)); - $this->assertNotNull($symbol_at_position); - $this->assertSame('73-82:string', $symbol_at_position[0]); + $reference = $this->codebase->getReferenceAtPosition('somefile.php', new Position(3, 28)); + $this->assertNotNull($reference); + $this->assertSame('73-82:string', $reference->symbol); } public function testGetSymbolPositionNullableArg(): void { - $codebase = $this->project_analyzer->getCodebase(); - $config = $codebase->config; + $config = $this->codebase->config; $config->throw_exception = false; $this->addFile( @@ -364,20 +455,19 @@ function B( ?AClass $class ) { }' ); - $codebase->file_provider->openFile('somefile.php'); - $codebase->scanFiles(); + $this->codebase->file_provider->openFile('somefile.php'); + $this->codebase->scanFiles(); $this->analyzeFile('somefile.php', new Context()); - $symbol_at_position = $codebase->getReferenceAtPosition('somefile.php', new Position(4, 33)); - $this->assertNotNull($symbol_at_position); + $reference = $this->codebase->getReferenceAtPosition('somefile.php', new Position(4, 33)); + $this->assertNotNull($reference); - $this->assertSame('B\AClass', $symbol_at_position[0]); + $this->assertSame('B\AClass', $reference->symbol); } public function testGetSymbolPositionMethodWrongReturnType(): void { - $codebase = $this->project_analyzer->getCodebase(); - $config = $codebase->config; + $config = $this->codebase->config; $config->throw_exception = false; $this->addFile( @@ -394,20 +484,19 @@ protected function get_command() : AClass { ' ); - $codebase->file_provider->openFile('somefile.php'); - $codebase->scanFiles(); + $this->codebase->file_provider->openFile('somefile.php'); + $this->codebase->scanFiles(); $this->analyzeFile('somefile.php', new Context()); - $symbol_at_position = $codebase->getReferenceAtPosition('somefile.php', new Position(6, 60)); - $this->assertNotNull($symbol_at_position); + $reference = $this->codebase->getReferenceAtPosition('somefile.php', new Position(6, 60)); + $this->assertNotNull($reference); - $this->assertSame('B\AClass', $symbol_at_position[0]); + $this->assertSame('B\AClass', $reference->symbol); } public function testGetSymbolPositionUseStatement(): void { - $codebase = $this->project_analyzer->getCodebase(); - $config = $codebase->config; + $config = $this->codebase->config; $config->throw_exception = false; $this->addFile( @@ -418,20 +507,19 @@ public function testGetSymbolPositionUseStatement(): void ' ); - $codebase->file_provider->openFile('somefile.php'); - $codebase->scanFiles(); + $this->codebase->file_provider->openFile('somefile.php'); + $this->codebase->scanFiles(); $this->analyzeFile('somefile.php', new Context()); - $symbol_at_position = $codebase->getReferenceAtPosition('somefile.php', new Position(2, 25)); - $this->assertNotNull($symbol_at_position); + $reference = $this->codebase->getReferenceAtPosition('somefile.php', new Position(2, 25)); + $this->assertNotNull($reference); - $this->assertSame('StreamWrapper', $symbol_at_position[0]); + $this->assertSame('StreamWrapper', $reference->symbol); } public function testGetSymbolPositionRange(): void { - $codebase = $this->project_analyzer->getCodebase(); - $config = $codebase->config; + $config = $this->codebase->config; $config->throw_exception = false; $this->addFile( @@ -445,23 +533,22 @@ function foo() : string { $active_symbol = foo();' ); - $codebase->file_provider->openFile('somefile.php'); - $codebase->scanFiles(); + $this->codebase->file_provider->openFile('somefile.php'); + $this->codebase->scanFiles(); $this->analyzeFile('somefile.php', new Context()); // This is focusing the $active_symbol variable, the LSP Range that is // returned should also point to the same variable (that's where hover popovers will show) - $symbol_at_position = $codebase->getReferenceAtPosition('somefile.php', new Position(6, 26)); + $reference = $this->codebase->getReferenceAtPosition('somefile.php', new Position(6, 26)); - $this->assertNotNull($symbol_at_position); - $this->assertSame(16, $symbol_at_position[1]->start->character); - $this->assertSame(30, $symbol_at_position[1]->end->character); + $this->assertNotNull($reference); + $this->assertSame(16, $reference->range->start->character); + $this->assertSame(30, $reference->range->end->character); } public function testGetTypeInDocblock(): void { - $codebase = $this->project_analyzer->getCodebase(); - $config = $codebase->config; + $config = $this->codebase->config; $config->throw_exception = false; $this->addFile( @@ -475,15 +562,15 @@ class A { }' ); - $codebase->file_provider->openFile('somefile.php'); - $codebase->scanFiles(); + $this->codebase->file_provider->openFile('somefile.php'); + $this->codebase->scanFiles(); $this->analyzeFile('somefile.php', new Context()); - $symbol_at_position = $codebase->getReferenceAtPosition('somefile.php', new Position(4, 35)); + $reference = $this->codebase->getReferenceAtPosition('somefile.php', new Position(4, 35)); - $this->assertNotNull($symbol_at_position); + $this->assertNotNull($reference); - $this->assertSame('Exception', $symbol_at_position[0]); + $this->assertSame('Exception', $reference->symbol); } /** @@ -523,8 +610,7 @@ public function testGetSignatureHelp( ?int $expected_argument_number, ?int $expected_param_count ): void { - $codebase = $this->project_analyzer->getCodebase(); - $config = $codebase->config; + $config = $this->codebase->config; $config->throw_exception = false; $this->addFile( @@ -559,11 +645,11 @@ function foo(string $a) { }' ); - $codebase->file_provider->openFile('somefile.php'); - $codebase->scanFiles(); + $this->codebase->file_provider->openFile('somefile.php'); + $this->codebase->scanFiles(); $this->analyzeFile('somefile.php', new Context()); - $reference_location = $codebase->getFunctionArgumentAtPosition('somefile.php', $position); + $reference_location = $this->codebase->getFunctionArgumentAtPosition('somefile.php', $position); if ($expected_symbol !== null) { $this->assertNotNull($reference_location); @@ -571,7 +657,7 @@ function foo(string $a) { $this->assertSame($expected_symbol, $symbol); $this->assertSame($expected_argument_number, $argument_number); - $symbol_information = $codebase->getSignatureInformation($reference_location[0]); + $symbol_information = $this->codebase->getSignatureInformation($reference_location[0]); if ($expected_param_count === null) { $this->assertNull($symbol_information); @@ -587,8 +673,7 @@ function foo(string $a) { public function testGetSignatureHelpIncludesParamDescription(): void { - $codebase = $this->project_analyzer->getCodebase(); - $config = $codebase->config; + $config = $this->codebase->config; $config->throw_exception = false; $this->addFile( @@ -603,13 +688,13 @@ function foo(string $a) { foo();' ); - $codebase->file_provider->openFile('somefile.php'); - $codebase->scanFiles(); + $this->codebase->file_provider->openFile('somefile.php'); + $this->codebase->scanFiles(); $this->analyzeFile('somefile.php', new Context()); - $reference_location = $codebase->getFunctionArgumentAtPosition('somefile.php', new Position(7, 20)); + $reference_location = $this->codebase->getFunctionArgumentAtPosition('somefile.php', new Position(7, 20)); $this->assertNotNull($reference_location); - $symbol_information = $codebase->getSignatureInformation($reference_location[0], 'somefile.php'); + $symbol_information = $this->codebase->getSignatureInformation($reference_location[0], 'somefile.php'); $this->assertNotNull($symbol_information); $this->assertNotNull($symbol_information->parameters); $this->assertEquals('The first param, a.', $symbol_information->parameters[0]->documentation); From 045bbe23a923109cf8cdb405c29e5f29ed44b332 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Fri, 18 Feb 2022 17:29:09 +0000 Subject: [PATCH 28/78] basic testing, disable log filtering --- composer.json | 1 + .../Internal/LanguageServer/Codebase.php | 2 +- .../LanguageServer/LanguageServer.php | 2 +- .../LanguageServer/PHPMarkdownContent.php | 39 +- tests/AsyncTestCase.php | 198 +++++ tests/LanguageServer/DiagnosticTest.php | 680 ++++++++++++++++++ tests/LanguageServer/Message.php | 45 ++ tests/LanguageServer/MockProtocolStream.php | 40 ++ tests/LanguageServer/SymbolLookupTest.php | 43 +- 9 files changed, 1035 insertions(+), 15 deletions(-) create mode 100644 tests/AsyncTestCase.php create mode 100644 tests/LanguageServer/DiagnosticTest.php create mode 100644 tests/LanguageServer/Message.php create mode 100644 tests/LanguageServer/MockProtocolStream.php diff --git a/composer.json b/composer.json index e79b06a4690..01c9ceb383e 100644 --- a/composer.json +++ b/composer.json @@ -42,6 +42,7 @@ }, "require-dev": { "ext-curl": "*", + "amphp/phpunit-util": "^2.0", "bamarni/composer-bin-plugin": "^1.2", "brianium/paratest": "^4.0||^6.0", "php-parallel-lint/php-parallel-lint": "^1.2", diff --git a/src/Psalm/Internal/LanguageServer/Codebase.php b/src/Psalm/Internal/LanguageServer/Codebase.php index de7927d2c45..ab3954c6231 100644 --- a/src/Psalm/Internal/LanguageServer/Codebase.php +++ b/src/Psalm/Internal/LanguageServer/Codebase.php @@ -255,7 +255,7 @@ public function getMarkupContentForSymbol(Reference $reference): ?PHPMarkdownCon // ? if (isset($file_storage->constants[$reference->symbol])) { return new PHPMarkdownContent( - 'const' . $reference->symbol . ' ' . $file_storage->constants[$reference->symbol], + 'const ' . $reference->symbol . ' ' . $file_storage->constants[$reference->symbol], $reference->symbol ); } diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 73dbf236e95..3083ffef931 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -856,7 +856,7 @@ public function exit(): void public function log(int $type, string $message, array $context = []): void { if ($this->client->clientConfiguration->logLevel < $type) { - return; + //return; } if (!empty($context)) { diff --git a/src/Psalm/Internal/LanguageServer/PHPMarkdownContent.php b/src/Psalm/Internal/LanguageServer/PHPMarkdownContent.php index 5ac0d09071a..1f4471a557c 100644 --- a/src/Psalm/Internal/LanguageServer/PHPMarkdownContent.php +++ b/src/Psalm/Internal/LanguageServer/PHPMarkdownContent.php @@ -4,13 +4,36 @@ namespace Psalm\Internal\LanguageServer; +use JsonSerializable; use LanguageServerProtocol\MarkupContent; use LanguageServerProtocol\MarkupKind; +use ReturnTypeWillChange; -class PHPMarkdownContent extends MarkupContent +use function get_object_vars; + +class PHPMarkdownContent extends MarkupContent implements JsonSerializable { + /** + * @var string + */ + public $code; + + /** + * @var string|null + */ + public $title; + + /** + * @var string|null + */ + public $description; + public function __construct(string $code, ?string $title = null, ?string $description = null) { + $this->code = $code; + $this->title = $title; + $this->description = $description; + $markdown = ''; if ($title !== null) { $markdown = "**$title**\n\n"; @@ -23,4 +46,18 @@ public function __construct(string $code, ?string $title = null, ?string $descri "$markdown```php\nfile_provider = new FakeFileProvider(); + + $this->testConfig = $this->makeConfig(); + + $providers = new Providers( + $this->file_provider, + new FakeParserCacheProvider() + ); + + $this->project_analyzer = new ProjectAnalyzer( + $this->testConfig, + $providers + ); + + $this->project_analyzer->setPhpVersion('7.4', 'tests'); + } + + public function tearDown(): void + { + unset($this->project_analyzer, $this->file_provider, $this->testConfig); + RuntimeCaches::clearAll(); + } + + /** + * @param string $file_path + * @param string $contents + * + */ + public function addFile($file_path, $contents): void + { + $this->file_provider->registerFile($file_path, $contents); + $this->project_analyzer->getCodebase()->scanner->addFileToShallowScan($file_path); + } + + /** + * @param string $file_path + * + */ + public function analyzeFile($file_path, Context $context, bool $track_unused_suppressions = true, bool $taint_flow_tracking = false): void + { + $codebase = $this->project_analyzer->getCodebase(); + + if ($taint_flow_tracking) { + $this->project_analyzer->trackTaintedInputs(); + } + + $codebase->addFilesToAnalyze([$file_path => $file_path]); + + $codebase->scanFiles(); + + $codebase->config->visitStubFiles($codebase); + + if ($codebase->alter_code) { + $this->project_analyzer->interpretRefactors(); + } + + $this->project_analyzer->trackUnusedSuppressions(); + + $file_analyzer = new FileAnalyzer( + $this->project_analyzer, + $file_path, + $codebase->config->shortenFileName($file_path) + ); + $file_analyzer->analyze($context); + + if ($codebase->taint_flow_graph) { + $codebase->taint_flow_graph->connectSinksAndSources(); + } + + if ($track_unused_suppressions) { + IssueBuffer::processUnusedSuppressions($codebase->file_provider); + } + } + + /** + * @param bool $withDataSet + * + */ + protected function getTestName($withDataSet = true): string + { + return $this->getName($withDataSet); + } + + public static function assertArrayKeysAreStrings(array $array, string $message = ''): void + { + $validKeys = array_filter($array, 'is_string', ARRAY_FILTER_USE_KEY); + self::assertTrue(count($array) === count($validKeys), $message); + } + + public static function assertArrayKeysAreZeroOrString(array $array, string $message = ''): void + { + $isZeroOrString = /** @param mixed $key */ function ($key): bool { + return $key === 0 || is_string($key); + }; + $validKeys = array_filter($array, $isZeroOrString, ARRAY_FILTER_USE_KEY); + self::assertTrue(count($array) === count($validKeys), $message); + } + + public static function assertArrayValuesAreArrays(array $array, string $message = ''): void + { + $validValues = array_filter($array, 'is_array'); + self::assertTrue(count($array) === count($validValues), $message); + } + + public static function assertArrayValuesAreStrings(array $array, string $message = ''): void + { + $validValues = array_filter($array, 'is_string'); + self::assertTrue(count($array) === count($validValues), $message); + } + + public static function assertStringIsParsableType(string $type, string $message = ''): void + { + if ($type === '') { + // Ignore empty types for now, as these are quite common for pecl libraries + self::assertTrue(true); + } else { + $union = null; + try { + $tokens = TypeTokenizer::tokenize($type); + $union = TypeParser::parseTokens($tokens); + } catch (Throwable $_e) { + } + self::assertInstanceOf(Union::class, $union, $message); + } + } +} diff --git a/tests/LanguageServer/DiagnosticTest.php b/tests/LanguageServer/DiagnosticTest.php new file mode 100644 index 00000000000..ee55ecf0ec1 --- /dev/null +++ b/tests/LanguageServer/DiagnosticTest.php @@ -0,0 +1,680 @@ +file_provider = new FakeFileProvider(); + + $config = new TestConfig(); + + $providers = new Providers( + $this->file_provider, + new ParserInstanceCacheProvider(), + null, + null, + new FakeFileReferenceCacheProvider(), + new ProjectCacheProvider() + ); + + $this->codebase = new Codebase($config, $providers); + + $this->project_analyzer = new ProjectAnalyzer( + $config, + $providers, + null, + [], + 1, + null, + $this->codebase + ); + + $this->project_analyzer->setPhpVersion('7.4', 'tests'); + $this->project_analyzer->getCodebase()->store_node_types = true; + } + + public function testSnippetSupportDisabled() + { + // Create a new promisor + $deferred = new Deferred; + + $this->setTimeout(5000); + $clientConfiguration = new ClientConfiguration(); + + $read = new MockProtocolStream(); + $write = new MockProtocolStream(); + + $array = $this->generateInitializeRequest(); + $array['params']['capabilities']['textDocument']['completion']['completionItem']['snippetSupport'] = false; + $read->write(new Message(MessageBody::parseArray($array))); + + $server = new LanguageServer( + $read, + $write, + $this->project_analyzer, + $this->codebase, + $clientConfiguration, + new Progress + ); + + $write->on('message', function (Message $message) use ($deferred, $server): void { + if ($message->body->method === 'telemetry/event' && $message->body->params->message === 'initialized') { + $this->assertFalse($server->clientCapabilities->textDocument->completion->completionItem->snippetSupport); + $deferred->resolve(null); + } + }); + + \Amp\Promise\wait($deferred->promise()); + } + + public function jestRun(): void + { + $config = $this->codebase->config; + $config->throw_exception = false; + + $this->addFile( + 'somefile.php', + '' + ); + + $issues = $this->changeFile( + 'somefile.php', + 'assertEmpty($issues); + + $issues = $this->changeFile( + 'somefile.php', + 'assertArrayHasKey('somefile.php', $issues); + $this->assertSame('Argument 1 of strpos expects a non-literal value, "" provided', $issues['somefile.php'][0]->message); + + $issues = $this->changeFile( + 'somefile.php', + 'assertArrayHasKey('somefile.php', $issues); + $this->assertSame('Argument 1 of strpos expects a non-literal value, "" provided', $issues['somefile.php'][0]->message); + $this->assertSame('Argument 1 of strpos expects a non-literal value, "" provided', $issues['somefile.php'][1]->message); + + $issues = $this->changeFile( + 'somefile.php', + 'changeFile( + 'somefile.php', + 'changeFile( + 'somefile.php', + 'changeFile( + 'somefile.php', + 'assertEmpty($issues); + } + + /** + * @return array> + */ + private function changeFile(string $file_path, string $contents): array + { + $this->codebase->addTemporaryFileChanges( + $file_path, + $contents, + $this->increment + ); + + $this->codebase->reloadFiles( + $this->project_analyzer, + [$file_path] + ); + $this->codebase->analyzer->addFilesToAnalyze( + [$file_path => $file_path] + ); + $this->codebase->analyzer->analyzeFiles($this->project_analyzer, 1, false); + + $this->increment++; + + return IssueBuffer::clear(); + } + + private function generateInitializeRequest(): array + { + return [ + 'method' => 'initialize', + 'params' => [ + 'processId' => rand(), + 'locale' => 'en-us', + 'capabilities' => [ + 'workspace' => [ + 'applyEdit' => true, + 'workspaceEdit' => [ + 'documentChanges' => true, + 'resourceOperations' => [ + 0 => 'create', + 1 => 'rename', + 2 => 'delete', + ], + 'failureHandling' => 'textOnlyTransactional', + 'normalizesLineEndings' => true, + 'changeAnnotationSupport' => [ + 'groupsOnLabel' => true, + ], + ], + 'didChangeConfiguration' => [ + 'dynamicRegistration' => true, + ], + 'didChangeWatchedFiles' => [ + 'dynamicRegistration' => true, + ], + 'symbol' => [ + 'dynamicRegistration' => true, + 'symbolKind' => [ + 'valueSet' => [ + 0 => 1, + 1 => 2, + 2 => 3, + 3 => 4, + 4 => 5, + 5 => 6, + 6 => 7, + 7 => 8, + 8 => 9, + 9 => 10, + 10 => 11, + 11 => 12, + 12 => 13, + 13 => 14, + 14 => 15, + 15 => 16, + 16 => 17, + 17 => 18, + 18 => 19, + 19 => 20, + 20 => 21, + 21 => 22, + 22 => 23, + 23 => 24, + 24 => 25, + 25 => 26, + ], + ], + 'tagSupport' => [ + 'valueSet' => [ + 0 => 1, + ], + ], + ], + 'codeLens' => [ + 'refreshSupport' => true, + ], + 'executeCommand' => [ + 'dynamicRegistration' => true, + ], + 'configuration' => true, + 'workspaceFolders' => true, + 'semanticTokens' => [ + 'refreshSupport' => true, + ], + 'fileOperations' => [ + 'dynamicRegistration' => true, + 'didCreate' => true, + 'didRename' => true, + 'didDelete' => true, + 'willCreate' => true, + 'willRename' => true, + 'willDelete' => true, + ], + ], + 'textDocument' => [ + 'publishDiagnostics' => [ + 'relatedInformation' => true, + 'versionSupport' => false, + 'tagSupport' => [ + 'valueSet' => [ + 0 => 1, + 1 => 2, + ], + ], + 'codeDescriptionSupport' => true, + 'dataSupport' => true, + ], + 'synchronization' => [ + 'dynamicRegistration' => true, + 'willSave' => true, + 'willSaveWaitUntil' => true, + 'didSave' => true, + ], + 'completion' => [ + 'dynamicRegistration' => true, + 'contextSupport' => true, + 'completionItem' => [ + 'snippetSupport' => true, + 'commitCharactersSupport' => true, + 'documentationFormat' => [ + 0 => 'markdown', + 1 => 'plaintext', + ], + 'deprecatedSupport' => true, + 'preselectSupport' => true, + 'tagSupport' => [ + 'valueSet' => [ + 0 => 1, + ], + ], + 'insertReplaceSupport' => true, + 'resolveSupport' => [ + 'properties' => [ + 0 => 'documentation', + 1 => 'detail', + 2 => 'additionalTextEdits', + ], + ], + 'insertTextModeSupport' => [ + 'valueSet' => [ + 0 => 1, + 1 => 2, + ], + ], + ], + 'completionItemKind' => [ + 'valueSet' => [ + 0 => 1, + 1 => 2, + 2 => 3, + 3 => 4, + 4 => 5, + 5 => 6, + 6 => 7, + 7 => 8, + 8 => 9, + 9 => 10, + 10 => 11, + 11 => 12, + 12 => 13, + 13 => 14, + 14 => 15, + 15 => 16, + 16 => 17, + 17 => 18, + 18 => 19, + 19 => 20, + 20 => 21, + 21 => 22, + 22 => 23, + 23 => 24, + 24 => 25, + ], + ], + ], + 'hover' => [ + 'dynamicRegistration' => true, + 'contentFormat' => [ + 0 => 'markdown', + 1 => 'plaintext', + ], + ], + 'signatureHelp' => [ + 'dynamicRegistration' => true, + 'signatureInformation' => [ + 'documentationFormat' => [ + 0 => 'markdown', + 1 => 'plaintext', + ], + 'parameterInformation' => [ + 'labelOffsetSupport' => true, + ], + 'activeParameterSupport' => true, + ], + 'contextSupport' => true, + ], + 'definition' => [ + 'dynamicRegistration' => true, + 'linkSupport' => true, + ], + 'references' => [ + 'dynamicRegistration' => true, + ], + 'documentHighlight' => [ + 'dynamicRegistration' => true, + ], + 'documentSymbol' => [ + 'dynamicRegistration' => true, + 'symbolKind' => [ + 'valueSet' => [ + 0 => 1, + 1 => 2, + 2 => 3, + 3 => 4, + 4 => 5, + 5 => 6, + 6 => 7, + 7 => 8, + 8 => 9, + 9 => 10, + 10 => 11, + 11 => 12, + 12 => 13, + 13 => 14, + 14 => 15, + 15 => 16, + 16 => 17, + 17 => 18, + 18 => 19, + 19 => 20, + 20 => 21, + 21 => 22, + 22 => 23, + 23 => 24, + 24 => 25, + 25 => 26, + ], + ], + 'hierarchicalDocumentSymbolSupport' => true, + 'tagSupport' => [ + 'valueSet' => [ + 0 => 1, + ], + ], + 'labelSupport' => true, + ], + 'codeAction' => [ + 'dynamicRegistration' => true, + 'isPreferredSupport' => true, + 'disabledSupport' => true, + 'dataSupport' => true, + 'resolveSupport' => [ + 'properties' => [ + 0 => 'edit', + ], + ], + 'codeActionLiteralSupport' => [ + 'codeActionKind' => [ + 'valueSet' => [ + 0 => '', + 1 => 'quickfix', + 2 => 'refactor', + 3 => 'refactor.extract', + 4 => 'refactor.inline', + 5 => 'refactor.rewrite', + 6 => 'source', + 7 => 'source.organizeImports', + ], + ], + ], + 'honorsChangeAnnotations' => false, + ], + 'codeLens' => [ + 'dynamicRegistration' => true, + ], + 'formatting' => [ + 'dynamicRegistration' => true, + ], + 'rangeFormatting' => [ + 'dynamicRegistration' => true, + ], + 'onTypeFormatting' => [ + 'dynamicRegistration' => true, + ], + 'rename' => [ + 'dynamicRegistration' => true, + 'prepareSupport' => true, + 'prepareSupportDefaultBehavior' => 1, + 'honorsChangeAnnotations' => true, + ], + 'documentLink' => [ + 'dynamicRegistration' => true, + 'tooltipSupport' => true, + ], + 'typeDefinition' => [ + 'dynamicRegistration' => true, + 'linkSupport' => true, + ], + 'implementation' => [ + 'dynamicRegistration' => true, + 'linkSupport' => true, + ], + 'colorProvider' => [ + 'dynamicRegistration' => true, + ], + 'foldingRange' => [ + 'dynamicRegistration' => true, + 'rangeLimit' => 5000, + 'lineFoldingOnly' => true, + ], + 'declaration' => [ + 'dynamicRegistration' => true, + 'linkSupport' => true, + ], + 'selectionRange' => [ + 'dynamicRegistration' => true, + ], + 'callHierarchy' => [ + 'dynamicRegistration' => true, + ], + 'semanticTokens' => [ + 'dynamicRegistration' => true, + 'tokenTypes' => [ + 0 => 'namespace', + 1 => 'type', + 2 => 'class', + 3 => 'enum', + 4 => 'interface', + 5 => 'struct', + 6 => 'typeParameter', + 7 => 'parameter', + 8 => 'variable', + 9 => 'property', + 10 => 'enumMember', + 11 => 'event', + 12 => 'function', + 13 => 'method', + 14 => 'macro', + 15 => 'keyword', + 16 => 'modifier', + 17 => 'comment', + 18 => 'string', + 19 => 'number', + 20 => 'regexp', + 21 => 'operator', + ], + 'tokenModifiers' => [ + 0 => 'declaration', + 1 => 'definition', + 2 => 'readonly', + 3 => 'static', + 4 => 'deprecated', + 5 => 'abstract', + 6 => 'async', + 7 => 'modification', + 8 => 'documentation', + 9 => 'defaultLibrary', + ], + 'formats' => [ + 0 => 'relative', + ], + 'requests' => [ + 'range' => true, + 'full' => [ + 'delta' => true, + ], + ], + 'multilineTokenSupport' => false, + 'overlappingTokenSupport' => false, + ], + 'linkedEditingRange' => [ + 'dynamicRegistration' => true, + ], + ], + 'window' => [ + 'showMessage' => [ + 'messageActionItem' => [ + 'additionalPropertiesSupport' => true, + ], + ], + 'showDocument' => [ + 'support' => true, + ], + 'workDoneProgress' => true, + ], + 'general' => [ + 'regularExpressions' => [ + 'engine' => 'ECMAScript', + 'version' => 'ES2020', + ], + 'markdown' => [ + 'parser' => 'marked', + 'version' => '1.1.0', + ], + ], + ], + 'trace' => 'off', + ] + ]; + } +} diff --git a/tests/LanguageServer/Message.php b/tests/LanguageServer/Message.php new file mode 100644 index 00000000000..81fd0dc2420 --- /dev/null +++ b/tests/LanguageServer/Message.php @@ -0,0 +1,45 @@ +method, $decoded->params ?? null); + } else if (Request::isRequest($decoded)) { + $obj = new Request($decoded->id, $decoded->method, $decoded->params ?? null); + } else if (SuccessResponse::isSuccessResponse($decoded)) { + $obj = new SuccessResponse($decoded->id, $decoded->result); + } else if (ErrorResponse::isErrorResponse($decoded)) { + $obj = new ErrorResponse($decoded->id, new Error($decoded->error->message, $decoded->error->code, $decoded->error->data ?? null)); + } else { + throw new Error('Invalid message', ErrorCode::INVALID_REQUEST); + } + return $obj; + } +} diff --git a/tests/LanguageServer/MockProtocolStream.php b/tests/LanguageServer/MockProtocolStream.php new file mode 100644 index 00000000000..f0c3176f2e5 --- /dev/null +++ b/tests/LanguageServer/MockProtocolStream.php @@ -0,0 +1,40 @@ +emit('message', [Message::parse((string)$msg)]); + }); + + // Create a new promisor + $deferred = new Deferred; + + $deferred->resolve(null); + + return $deferred->promise(); + } +} \ No newline at end of file diff --git a/tests/LanguageServer/SymbolLookupTest.php b/tests/LanguageServer/SymbolLookupTest.php index 47a6abef6bf..b3ad0d8cd77 100644 --- a/tests/LanguageServer/SymbolLookupTest.php +++ b/tests/LanguageServer/SymbolLookupTest.php @@ -57,8 +57,7 @@ public function setUp(): void public function testSimpleSymbolLookup(): void { - $codebase = $this->project_analyzer->getCodebase(); - $config = $codebase->config; + $config = $this->codebase->config; $config->globals['$my_global'] = 'string'; $this->addFile( 'somefile.php', @@ -108,7 +107,9 @@ function qux(int $a, int $b) : int { ) ); $this->assertNotNull($information); - $this->assertSame('value); + $this->assertSame("public function foo(): void", $information->code); + $this->assertSame("B\A::foo", $information->title); + $this->assertNull($information->description); $information = $this->codebase->getMarkupContentForSymbol( new Reference( @@ -118,7 +119,9 @@ function qux(int $a, int $b) : int { ) ); $this->assertNotNull($information); - $this->assertSame('value); + $this->assertSame('protected int|null $a', $information->code); + $this->assertSame('B\A::$a', $information->title); + $this->assertSame('', $information->description); $information = $this->codebase->getMarkupContentForSymbol( new Reference( @@ -128,7 +131,9 @@ function qux(int $a, int $b) : int { ) ); $this->assertNotNull($information); - $this->assertSame('value); + $this->assertSame('function B\bar(): int', $information->code); + $this->assertSame('b\bar', $information->title); + $this->assertNull($information->description); $information = $this->codebase->getMarkupContentForSymbol( new Reference( @@ -138,7 +143,9 @@ function qux(int $a, int $b) : int { ) ); $this->assertNotNull($information); - $this->assertSame('value); + $this->assertSame('public const BANANA = 🍌;', $information->code); + $this->assertSame('B\A::BANANA', $information->title); + $this->assertNull($information->description); $information = $this->codebase->getMarkupContentForSymbol( new Reference( @@ -148,7 +155,9 @@ function qux(int $a, int $b) : int { ) ); $this->assertNotNull($information); - $this->assertSame("value); + $this->assertSame("function B\baz(\n int \$a\n): int", $information->code); + $this->assertSame('b\baz', $information->title); + $this->assertNull($information->description); $information = $this->codebase->getMarkupContentForSymbol( new Reference( @@ -158,7 +167,9 @@ function qux(int $a, int $b) : int { ) ); $this->assertNotNull($information); - $this->assertSame("value); + $this->assertSame("function B\qux(\n int \$a,\n int \$b\n): int", $information->code); + $this->assertSame('b\qux', $information->title); + $this->assertNull($information->description); $information = $this->codebase->getMarkupContentForSymbol( new Reference( @@ -168,7 +179,9 @@ function qux(int $a, int $b) : int { ) ); $this->assertNotNull($information); - $this->assertSame("", $information->value); + $this->assertSame("array", $information->code); + $this->assertSame('$_SERVER', $information->title); + $this->assertNull($information->description); $information = $this->codebase->getMarkupContentForSymbol( new Reference( @@ -178,7 +191,9 @@ function qux(int $a, int $b) : int { ) ); $this->assertNotNull($information); - $this->assertSame("value); + $this->assertSame("string", $information->code); + $this->assertSame('$my_global', $information->title); + $this->assertNull($information->description); } public function testSimpleSymbolLookupGlobalConst(): void @@ -204,7 +219,9 @@ public function testSimpleSymbolLookupGlobalConst(): void ) ); $this->assertNotNull($information); - $this->assertSame("value); + $this->assertSame("const APPLE string", $information->code); + $this->assertSame("APPLE", $information->title); + $this->assertNull($information->description); $information = $this->codebase->getMarkupContentForSymbol( new Reference( @@ -214,7 +231,9 @@ public function testSimpleSymbolLookupGlobalConst(): void ) ); $this->assertNotNull($information); - $this->assertSame("value); + $this->assertSame("const BANANA string", $information->code); + $this->assertSame("BANANA", $information->title); + $this->assertNull($information->description); } public function testSimpleSymbolLocation(): void From ceb7b9516978e4a8cb1c1126860bf10e92aedb69 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Fri, 18 Feb 2022 17:37:17 +0000 Subject: [PATCH 29/78] bump --- src/Psalm/Internal/LanguageServer/LanguageServer.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 3083ffef931..5878d1f1256 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -695,6 +695,7 @@ public function doVersionedAnalysis(array $files, ?int $version = null): void return; } + $this->logDebug("Reloading Files"); $this->codebase->reloadFiles( $this->project_analyzer, array_keys($files) @@ -703,6 +704,8 @@ public function doVersionedAnalysis(array $files, ?int $version = null): void $this->codebase->analyzer->addFilesToAnalyze( array_combine(array_keys($files), array_keys($files)) ); + + $this->logDebug("Analyze Files"); $this->codebase->analyzer->analyzeFiles($this->project_analyzer, 1, false); $this->emitVersionedIssues($files, $version); From e509aad26313856c2258b1953878c649bf97388b Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Fri, 4 Mar 2022 18:04:19 +0000 Subject: [PATCH 30/78] ability to debounce onchange events for large projects --- src/Psalm/Internal/Cli/LanguageServer.php | 8 ++++ .../LanguageServer/ClientConfiguration.php | 33 +++++++++++++++++ .../LanguageServer/LanguageServer.php | 37 +++++++++++++++---- 3 files changed, 70 insertions(+), 8 deletions(-) diff --git a/src/Psalm/Internal/Cli/LanguageServer.php b/src/Psalm/Internal/Cli/LanguageServer.php index 6cd7b4e33aa..6887f36e743 100644 --- a/src/Psalm/Internal/Cli/LanguageServer.php +++ b/src/Psalm/Internal/Cli/LanguageServer.php @@ -85,6 +85,7 @@ public static function run(array $argv): void 'enable-provide-signature-help::', 'enable-provide-definition::', 'show-diagnostic-warnings::', + 'on-change-debounce-ms::', 'use-extended-diagnostic-codes', 'verbose' ]; @@ -209,6 +210,9 @@ function (string $arg) use ($valid_long_options): void { --use-extended-diagnostic-codes (DEPRECATED) Enables sending help uri links with the code in diagnostic messages. + --on-change-debounce-ms=[INT] + The number of milliseconds to debounce onChange events. + --verbose Will send log messages to the client with information. @@ -308,6 +312,10 @@ function () use ($current_dir, $options, $vendor_dir): ?\Composer\Autoload\Class $clientConfiguration->onchangeLineLimit = (int) $options['disable-on-change']; } + if (isset($options['on-change-debounce-ms'])) { + $clientConfiguration->onChangeDebounceMs = (int) $options['on-change-debounce-ms']; + } + $clientConfiguration->provideDefinition = !isset($options['enable-provide-definition']) || !is_string($options['enable-provide-definition']) || strtolower($options['enable-provide-definition']) !== 'false'; diff --git a/src/Psalm/Internal/LanguageServer/ClientConfiguration.php b/src/Psalm/Internal/LanguageServer/ClientConfiguration.php index e6242eb6700..c5fd948d145 100644 --- a/src/Psalm/Internal/LanguageServer/ClientConfiguration.php +++ b/src/Psalm/Internal/LanguageServer/ClientConfiguration.php @@ -106,6 +106,29 @@ class ClientConfiguration */ public $onchangeLineLimit; + /** + * Debounce time in milliseconds for onChange events + * + * @var int|null + * + */ + public $onChangeDebounceMs; + + /** + * Undocumented function + * + * @param boolean $hideWarnings + * @param boolean|null $provideCompletion + * @param boolean|null $provideDefinition + * @param boolean|null $provideHover + * @param boolean|null $provideSignatureHelp + * @param boolean|null $provideCodeActions + * @param boolean|null $provideDiagnostics + * @param boolean|null $findUnusedVariables + * @param 'always'|'auto'|null $findUnusedCode + * @param integer|null $logLevel + * @param integer|null $onchangeLineLimit + */ public function __construct( bool $hideWarnings = true, bool $provideCompletion = null, @@ -113,6 +136,11 @@ public function __construct( bool $provideHover = null, bool $provideSignatureHelp = null, bool $provideCodeActions = null, + bool $provideDiagnostics = null, + bool $findUnusedVariables = null, + string $findUnusedCode = null, + int $logLevel = null, + int $onchangeLineLimit = null, ) { $this->hideWarnings = $hideWarnings; $this->provideCompletion = $provideCompletion; @@ -120,5 +148,10 @@ public function __construct( $this->provideHover = $provideHover; $this->provideSignatureHelp = $provideSignatureHelp; $this->provideCodeActions = $provideCodeActions; + $this->provideDiagnostics = $provideDiagnostics; + $this->findUnusedVariables = $findUnusedVariables; + $this->findUnusedCode = $findUnusedCode; + $this->logLevel = $logLevel; + $this->onchangeLineLimit = $onchangeLineLimit; } } diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 5878d1f1256..27257480635 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -141,6 +141,12 @@ class LanguageServer extends Dispatcher */ protected $codebase; + /** + * The AMP Delay token + * @var string + */ + protected $versionedAnalysisDelayToken = ''; + public function __construct( ProtocolReader $reader, ProtocolWriter $writer, @@ -627,7 +633,7 @@ public function initialized(): void */ public function queueChangeFileAnalysis(string $file_path, string $uri, ?int $version = null): void { - $this->doVersionedAnalysis([$file_path => $uri], $version); + $this->doVersionedAnalysisDebounce([$file_path => $uri], $version); } /** @@ -681,6 +687,25 @@ function (array $opened, string $file_path) { $this->doVersionedAnalysis($opened); } + /** + * Debounced Queue File Analysis with optional version + * + * @param array $files + * @param int|null $version + */ + public function doVersionedAnalysisDebounce(array $files, ?int $version = null): void + { + Loop::cancel($this->versionedAnalysisDelayToken); + if ($this->client->clientConfiguration->onChangeDebounceMs === null) { + $this->doVersionedAnalysis($files, $version); + } else { + $this->versionedAnalysisDelayToken = Loop::delay( + $this->client->clientConfiguration->onChangeDebounceMs, + fn() => $this->doVersionedAnalysis($files, $version) + ); + } + } + /** * Queue File Analysis with optional version * @@ -689,13 +714,9 @@ function (array $opened, string $file_path) { */ public function doVersionedAnalysis(array $files, ?int $version = null): void { + Loop::cancel($this->versionedAnalysisDelayToken); try { - if (empty($files)) { - $this->logWarning("No versioned analysis to do."); - return; - } - - $this->logDebug("Reloading Files"); + $this->logDebug("Doing Analysis from version: $version"); $this->codebase->reloadFiles( $this->project_analyzer, array_keys($files) @@ -705,7 +726,7 @@ public function doVersionedAnalysis(array $files, ?int $version = null): void array_combine(array_keys($files), array_keys($files)) ); - $this->logDebug("Analyze Files"); + $this->logDebug("Reloading Files"); $this->codebase->analyzer->analyzeFiles($this->project_analyzer, 1, false); $this->emitVersionedIssues($files, $version); From 0944ca52d7b078796d831bf155392b70913fec41 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Thu, 31 Mar 2022 18:01:25 +0000 Subject: [PATCH 31/78] use latest language server --- composer.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/composer.json b/composer.json index 391e1881979..481cfa75197 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ "composer/xdebug-handler": "^1.1 || ^2.0 || ^3.0", "dnoegel/php-xdg-base-dir": "^0.1.1", "felixfbecker/advanced-json-rpc": "^3.0.3", - "felixfbecker/language-server-protocol": "dev-feature/spec-3.16 as 1.5.1", + "felixfbecker/language-server-protocol": "^1.5.2", "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", "nikic/php-parser": "^4.13", "openlss/lib-array2xml": "^1.0", @@ -97,10 +97,6 @@ { "type": "path", "url": "examples/plugins/composer-based/echo-checker" - }, - { - "type": "vcs", - "url": "https://github.com/tm1000/php-language-server-protocol" } ], "minimum-stability": "dev", From 8740c17d7fc62f37eaca87705aad6ff0bba301a5 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Wed, 27 Apr 2022 17:32:45 +0000 Subject: [PATCH 32/78] make sure publishDiagnostics returns an array not an object --- src/Psalm/Internal/LanguageServer/LanguageServer.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 27257480635..e58dcc43c9a 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -59,6 +59,7 @@ use function array_reduce; use function array_shift; use function array_unshift; +use function array_values; use function cli_set_process_title; use function explode; use function extension_loaded; @@ -825,7 +826,7 @@ function (IssueData $issue_data) { ) ); - $this->client->textDocument->publishDiagnostics($uri, $diagnostics, $version); + $this->client->textDocument->publishDiagnostics($uri, array_values($diagnostics), $version); } } From 4191a9d9143d6a1bc0fc2ff56b51da1cf06f1e4a Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Wed, 27 Apr 2022 17:46:20 +0000 Subject: [PATCH 33/78] fix log levels --- src/Psalm/Internal/Cli/LanguageServer.php | 2 ++ src/Psalm/Internal/LanguageServer/LanguageServer.php | 9 +++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Cli/LanguageServer.php b/src/Psalm/Internal/Cli/LanguageServer.php index 6887f36e743..d871b993a8e 100644 --- a/src/Psalm/Internal/Cli/LanguageServer.php +++ b/src/Psalm/Internal/Cli/LanguageServer.php @@ -356,6 +356,8 @@ function () use ($current_dir, $options, $vendor_dir): ?\Composer\Autoload\Class if (isset($options['verbose'])) { $clientConfiguration->logLevel = $options['verbose'] ? MessageType::LOG : MessageType::INFO; + } else { + $clientConfiguration->logLevel = MessageType::INFO; } $clientConfiguration->TCPServerAddress = $options['tcp'] ?? null; diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index e58dcc43c9a..5a60335d3ba 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -880,8 +880,13 @@ public function exit(): void */ public function log(int $type, string $message, array $context = []): void { - if ($this->client->clientConfiguration->logLevel < $type) { - //return; + $logLevel = $this->client->clientConfiguration->logLevel; + if ($logLevel === null) { + return; + } + + if ($logLevel < $type) { + return; } if (!empty($context)) { From b3cc5c7f6798b84d50ecbfe7ee0f459b2791a547 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Wed, 27 Apr 2022 17:48:51 +0000 Subject: [PATCH 34/78] fix logic --- src/Psalm/Internal/LanguageServer/LanguageServer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 5a60335d3ba..46903da4f8d 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -885,7 +885,7 @@ public function log(int $type, string $message, array $context = []): void return; } - if ($logLevel < $type) { + if ($type < $logLevel) { return; } From 6855c2307dc62740ab0320a1081a4b0315dbbe1f Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Wed, 27 Apr 2022 19:34:46 +0000 Subject: [PATCH 35/78] dont allow clients to destroy our local configurations --- .../Internal/LanguageServer/LanguageClient.php | 16 ++++++++++++---- .../Internal/LanguageServer/LanguageServer.php | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/Psalm/Internal/LanguageServer/LanguageClient.php b/src/Psalm/Internal/LanguageServer/LanguageClient.php index 791a86dec1a..a0afe20fbba 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageClient.php +++ b/src/Psalm/Internal/LanguageServer/LanguageClient.php @@ -85,10 +85,9 @@ public function refreshConfiguration(): void if ($error) { $this->server->logError('There was an error getting configuration'); } else { - /** @var array $value */ + /** @var array $value */ [$config] = $value; - $this->mapper->map($config, $this->clientConfiguration); - $this->configurationRefreshed(); + $this->configurationRefreshed((array) $config); } }); } @@ -153,10 +152,19 @@ public function event(LogMessage $logMessage): void ); } - private function configurationRefreshed(): void + /** + * Configuration Refreshed from Client + * + * @param array $config + */ + private function configurationRefreshed(array $config): void { //do things when the config is refreshed + if (empty($config)) { + return; + } + if (!is_null($this->clientConfiguration->provideCompletion)) { //$this->server->project_analyzer->provide_completion = $this->clientConfiguration->provideCompletion; } diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 46903da4f8d..694b1f12b50 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -885,7 +885,7 @@ public function log(int $type, string $message, array $context = []): void return; } - if ($type < $logLevel) { + if ($type > $logLevel) { return; } From 27b9522d94be3a569802410f0ab37dd64af3979f Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Thu, 28 Apr 2022 16:49:19 +0000 Subject: [PATCH 36/78] expose additional types --- .../Internal/LanguageServer/Codebase.php | 593 ++++++++++++------ .../LanguageServer/Server/TextDocument.php | 1 - 2 files changed, 417 insertions(+), 177 deletions(-) diff --git a/src/Psalm/Internal/LanguageServer/Codebase.php b/src/Psalm/Internal/LanguageServer/Codebase.php index ab3954c6231..1c616733648 100644 --- a/src/Psalm/Internal/LanguageServer/Codebase.php +++ b/src/Psalm/Internal/LanguageServer/Codebase.php @@ -59,12 +59,13 @@ */ class Codebase extends PsalmCodebase { - /** * Get Reference from Position */ - public function getReferenceAtPosition(string $file_path, Position $position): ?Reference - { + public function getReferenceAtPosition( + string $file_path, + Position $position, + ): ?Reference { $is_open = $this->file_provider->isOpen($file_path); if (!$is_open) { @@ -75,39 +76,53 @@ public function getReferenceAtPosition(string $file_path, Position $position): ? $offset = $position->toOffset($file_contents); - [$reference_map, $type_map] = $this->analyzer->getMapsForFile($file_path); - - $symbol = null; + $reference_maps = $this->analyzer->getMapsForFile($file_path); - if (!$reference_map && !$type_map) { - return null; - } + [$r] = $reference_maps; $reference_start_pos = null; $reference_end_pos = null; + $symbol = null; - ksort($reference_map); + foreach ($reference_maps as $reference_map) { + ksort($reference_map); - foreach ($reference_map as $start_pos => [$end_pos, $possible_reference]) { - if ($offset < $start_pos) { - break; + foreach ( + $reference_map + as $start_pos => [$end_pos, $possible_reference] + ) { + if ($offset < $start_pos) { + break; + } + + if ($offset > $end_pos) { + continue; + } + $reference_start_pos = $start_pos; + $reference_end_pos = $end_pos; + $symbol = $possible_reference; } - if ($offset > $end_pos) { - continue; + if ( + $symbol !== null && + $reference_start_pos !== null && + $reference_end_pos !== null + ) { + break; } - $reference_start_pos = $start_pos; - $reference_end_pos = $end_pos; - $symbol = $possible_reference; } - if ($symbol === null || $reference_start_pos === null || $reference_end_pos === null) { + if ( + $symbol === null || + $reference_start_pos === null || + $reference_end_pos === null + ) { return null; } $range = new Range( self::getPositionFromOffset($reference_start_pos, $file_contents), - self::getPositionFromOffset($reference_end_pos, $file_contents) + self::getPositionFromOffset($reference_end_pos, $file_contents), ); return new Reference($file_path, $symbol, $range); @@ -118,12 +133,13 @@ public function getReferenceAtPosition(string $file_path, Position $position): ? * * @param Reference $reference */ - public function getMarkupContentForSymbol(Reference $reference): ?PHPMarkdownContent - { + public function getMarkupContentForSymbol( + Reference $reference, + ): ?PHPMarkdownContent { //Direct Assignment if (is_numeric($reference->symbol[0])) { return new PHPMarkdownContent( - preg_replace('/^[^:]*:/', '', $reference->symbol) + preg_replace('/^[^:]*:/', '', $reference->symbol), ); } @@ -136,7 +152,9 @@ public function getMarkupContentForSymbol(Reference $reference): ?PHPMarkdownCon /** @psalm-suppress ArgumentTypeCoercion */ $method_id = new MethodIdentifier(...explode('::', $symbol)); - $declaring_method_id = $this->methods->getDeclaringMethodId($method_id); + $declaring_method_id = $this->methods->getDeclaringMethodId( + $method_id, + ); if (!$declaring_method_id) { return null; @@ -147,7 +165,7 @@ public function getMarkupContentForSymbol(Reference $reference): ?PHPMarkdownCon return new PHPMarkdownContent( $storage->getHoverMarkdown(), "{$storage->defining_fqcln}::{$storage->cased_name}", - $storage->description + $storage->description, ); } @@ -155,20 +173,53 @@ public function getMarkupContentForSymbol(Reference $reference): ?PHPMarkdownCon //Class Property if (strpos($reference->symbol, '$') !== false) { - $storage = $this->properties->getStorage($reference->symbol); + $property_id = preg_replace('/^\\\\/', '', $reference->symbol); + [$fq_class_name, $property_name] = explode('::$', $property_id); + $class_storage = $this->classlikes->getStorageFor($fq_class_name); + + //Get Real Properties + if (isset($class_storage->declaring_property_ids[$property_name])) { + $declaring_property_class = $class_storage->declaring_property_ids[$property_name]; + $declaring_class_storage = $this->classlike_storage_provider->get($declaring_property_class); + + if (isset($declaring_class_storage->properties[$property_name])) { + $storage = $declaring_class_storage->properties[$property_name]; + return new PHPMarkdownContent( + "{$storage->getInfo()} {$symbol_name}", + $reference->symbol, + $storage->description, + ); + } + } + + //Get Docblock properties + if (isset($class_storage->pseudo_property_set_types['$'.$property_name])) { + return new PHPMarkdownContent( + 'public '.(string) $class_storage->pseudo_property_set_types['$'.$property_name].' $'.$property_name, + $reference->symbol + ); + } + + //Get Docblock properties + if (isset($class_storage->pseudo_property_get_types['$'.$property_name])) { + return new PHPMarkdownContent( + 'public '.(string) $class_storage->pseudo_property_get_types['$'.$property_name].' $'.$property_name, + $reference->symbol + ); + } + + return null; - return new PHPMarkdownContent( - "{$storage->getInfo()} {$symbol_name}", - $reference->symbol, - $storage->description - ); } - [$fq_classlike_name, $const_name] = explode('::', $reference->symbol); + [$fq_classlike_name, $const_name] = explode( + '::', + $reference->symbol, + ); $class_constants = $this->classlikes->getConstantsForClass( $fq_classlike_name, - ReflectionProperty::IS_PRIVATE + ReflectionProperty::IS_PRIVATE, ); if (!isset($class_constants[$const_name])) { @@ -178,15 +229,17 @@ public function getMarkupContentForSymbol(Reference $reference): ?PHPMarkdownCon //Class Constant return new PHPMarkdownContent( $class_constants[$const_name]->getHoverMarkdown($const_name), - $fq_classlike_name.'::'.$const_name, - $class_constants[$const_name]->description + $fq_classlike_name . '::' . $const_name, + $class_constants[$const_name]->description, ); } //Procedural Function if (strpos($reference->symbol, '()')) { $function_id = strtolower(substr($reference->symbol, 0, -2)); - $file_storage = $this->file_storage_provider->get($reference->file_path); + $file_storage = $this->file_storage_provider->get( + $reference->file_path, + ); if (isset($file_storage->functions[$function_id])) { $function_storage = $file_storage->functions[$function_id]; @@ -194,7 +247,7 @@ public function getMarkupContentForSymbol(Reference $reference): ?PHPMarkdownCon return new PHPMarkdownContent( $function_storage->getHoverMarkdown(), $function_id, - $function_storage->description + $function_storage->description, ); } @@ -207,7 +260,7 @@ public function getMarkupContentForSymbol(Reference $reference): ?PHPMarkdownCon return new PHPMarkdownContent( $function->getHoverMarkdown(), $function_id, - $function->description + $function->description, ); } @@ -217,17 +270,21 @@ public function getMarkupContentForSymbol(Reference $reference): ?PHPMarkdownCon if (!$type->isMixed()) { return new PHPMarkdownContent( (string) $type, - $reference->symbol + $reference->symbol, ); } } try { - $storage = $this->classlike_storage_provider->get($reference->symbol); + $storage = $this->classlike_storage_provider->get( + $reference->symbol, + ); return new PHPMarkdownContent( - ($storage->abstract ? 'abstract ' : '') . 'class ' . $storage->name, + ($storage->abstract ? 'abstract ' : '') . + 'class ' . + $storage->name, $storage->name, - $storage->description + $storage->description, ); } catch (InvalidArgumentException $e) { //continue on as normal @@ -240,40 +297,52 @@ public function getMarkupContentForSymbol(Reference $reference): ?PHPMarkdownCon $namespace_constants = NamespaceAnalyzer::getConstantsForNamespace( $namespace_name, - ReflectionProperty::IS_PUBLIC + ReflectionProperty::IS_PUBLIC, ); //Namespace Constant if (isset($namespace_constants[$const_name])) { $type = $namespace_constants[$const_name]; return new PHPMarkdownContent( $reference->symbol . ' ' . $type, - $reference->symbol + $reference->symbol, ); } } else { - $file_storage = $this->file_storage_provider->get($reference->file_path); + $file_storage = $this->file_storage_provider->get( + $reference->file_path, + ); // ? if (isset($file_storage->constants[$reference->symbol])) { return new PHPMarkdownContent( - 'const ' . $reference->symbol . ' ' . $file_storage->constants[$reference->symbol], - $reference->symbol + 'const ' . + $reference->symbol . + ' ' . + $file_storage->constants[$reference->symbol], + $reference->symbol, ); } - $type = ConstFetchAnalyzer::getGlobalConstType($this, $reference->symbol, $reference->symbol); + $type = ConstFetchAnalyzer::getGlobalConstType( + $this, + $reference->symbol, + $reference->symbol, + ); //Global Constant if ($type) { return new PHPMarkdownContent( 'const ' . $reference->symbol . ' ' . $type, - $reference->symbol + $reference->symbol, ); } } - return null; + + return new PHPMarkdownContent($reference->symbol); } - private static function getPositionFromOffset(int $offset, string $file_contents): Position - { + private static function getPositionFromOffset( + int $offset, + string $file_contents, + ): Position { $file_contents = substr($file_contents, 0, $offset); $offsetLength = $offset - strlen($file_contents); @@ -287,15 +356,17 @@ private static function getPositionFromOffset(int $offset, string $file_contents return new Position( substr_count($file_contents, "\n"), - $offset - (int)$before_newline_count - 1 + $offset - (int) $before_newline_count - 1, ); } /** * @return array{0: string, 1: '->'|'::'|'['|'symbol', 2: int}|null */ - public function getCompletionDataAtPosition(string $file_path, Position $position): ?array - { + public function getCompletionDataAtPosition( + string $file_path, + Position $position, + ): ?array { $is_open = $this->file_provider->isOpen($file_path); if (!$is_open) { @@ -306,7 +377,9 @@ public function getCompletionDataAtPosition(string $file_path, Position $positio $offset = $position->toOffset($file_contents); - [$reference_map, $type_map] = $this->analyzer->getMapsForFile($file_path); + [$reference_map, $type_map] = $this->analyzer->getMapsForFile( + $file_path, + ); if (!$reference_map && !$type_map) { return null; @@ -314,13 +387,22 @@ public function getCompletionDataAtPosition(string $file_path, Position $positio krsort($type_map); - foreach ($type_map as $start_pos => [$end_pos_excluding_whitespace, $possible_type]) { + foreach ( + $type_map + as $start_pos => [$end_pos_excluding_whitespace, $possible_type] + ) { if ($offset < $start_pos) { continue; } /** @psalm-suppress PossiblyUndefinedIntArrayOffset */ - $num_whitespace_bytes = preg_match('/\G\s+/', $file_contents, $matches, 0, $end_pos_excluding_whitespace) + $num_whitespace_bytes = preg_match( + '/\G\s+/', + $file_contents, + $matches, + 0, + $end_pos_excluding_whitespace, + ) ? strlen($matches[0]) : 0; $end_pos = $end_pos_excluding_whitespace + $num_whitespace_bytes; @@ -356,12 +438,18 @@ public function getCompletionDataAtPosition(string $file_path, Position $positio } } - foreach ($reference_map as $start_pos => [$end_pos, $possible_reference]) { + foreach ( + $reference_map + as $start_pos => [$end_pos, $possible_reference] + ) { if ($offset < $start_pos) { continue; } // If the reference precedes a "::" then treat it as a class reference. - if ($offset - $end_pos === 2 && substr($file_contents, $end_pos, 2) === '::') { + if ( + $offset - $end_pos === 2 && + substr($file_contents, $end_pos, 2) === '::' + ) { return [$possible_reference, '::', $offset]; } @@ -380,22 +468,38 @@ public function getCompletionDataAtPosition(string $file_path, Position $positio return null; } - public function getTypeContextAtPosition(string $file_path, Position $position): ?Union - { + public function getTypeContextAtPosition( + string $file_path, + Position $position, + ): ?Union { $file_contents = $this->getFileContents($file_path); $offset = $position->toOffset($file_contents); - [$reference_map, $type_map, $argument_map] = $this->analyzer->getMapsForFile($file_path); + [ + $reference_map, + $type_map, + $argument_map, + ] = $this->analyzer->getMapsForFile($file_path); if (!$reference_map && !$type_map && !$argument_map) { return null; } - foreach ($argument_map as $start_pos => [$end_pos, $function, $argument_num]) { + foreach ( + $argument_map + as $start_pos => [$end_pos, $function, $argument_num] + ) { if ($offset < $start_pos || $offset > $end_pos) { continue; } // First parameter to a function-like - $function_storage = $this->getFunctionStorageForSymbol($file_path, $function . '()'); - if (!$function_storage || !$function_storage->params || !isset($function_storage->params[$argument_num])) { + $function_storage = $this->getFunctionStorageForSymbol( + $file_path, + $function . '()', + ); + if ( + !$function_storage || + !$function_storage->params || + !isset($function_storage->params[$argument_num]) + ) { return null; } @@ -411,7 +515,7 @@ public function getTypeContextAtPosition(string $file_path, Position $position): public function getCompletionItemsForClassishThing( string $type_string, string $gap, - bool $snippets_supported = false + bool $snippets_supported = false, ): array { $completion_items = []; @@ -420,57 +524,122 @@ public function getCompletionItemsForClassishThing( foreach ($type->getAtomicTypes() as $atomic_type) { if ($atomic_type instanceof TNamedObject) { try { - $class_storage = $this->classlike_storage_provider->get($atomic_type->value); - - foreach ($class_storage->appearing_method_ids as $declaring_method_id) { - $method_storage = $this->methods->getStorage($declaring_method_id); - - if ($method_storage->is_static || $gap === '->') { - $completion_item = new CompletionItem( - $method_storage->cased_name, - CompletionItemKind::METHOD, - $method_storage->getCompletionSignature(), - $method_storage->description, - (string)$method_storage->visibility, - $method_storage->cased_name, - $method_storage->cased_name, - null, - null, - new Command('Trigger parameter hints', 'editor.action.triggerParameterHints'), - null, - 2 + $class_storage = $this->classlike_storage_provider->get( + $atomic_type->value, + ); + + foreach ( + $class_storage->appearing_method_ids + as $declaring_method_id + ) { + try { + $method_storage = $this->methods->getStorage( + $declaring_method_id, ); - if ($snippets_supported && count($method_storage->params) > 0) { - $completion_item->insertText .= '($0)'; - $completion_item->insertTextFormat = InsertTextFormat::SNIPPET; - } else { - $completion_item->insertText .= '()'; + if ($method_storage->is_static || $gap === '->') { + $completion_item = new CompletionItem( + $method_storage->cased_name, + CompletionItemKind::METHOD, + $method_storage->getCompletionSignature(), + $method_storage->description, + (string) $method_storage->visibility, + $method_storage->cased_name, + $method_storage->cased_name, + null, + null, + new Command( + 'Trigger parameter hints', + 'editor.action.triggerParameterHints', + ), + null, + 2, + ); + + if ( + $snippets_supported && + count($method_storage->params) > 0 + ) { + $completion_item->insertText .= '($0)'; + $completion_item->insertTextFormat = + InsertTextFormat::SNIPPET; + } else { + $completion_item->insertText .= '()'; + } + + $completion_items[] = $completion_item; } - - $completion_items[] = $completion_item; + } catch (Exception $e) { + error_log($e->getMessage()); + continue; } } - foreach ($class_storage->declaring_property_ids as $property_name => $declaring_class) { - $property_storage = $this->properties->getStorage( - $declaring_class . '::$' . $property_name + $pseudo_property_types = []; + foreach ( + $class_storage->pseudo_property_get_types + as $property_name => $type + ) { + $pseudo_property_types[$property_name] = new CompletionItem( + str_replace('$', '', $property_name), + CompletionItemKind::PROPERTY, + $type->__toString(), + null, + '1', //sort text + str_replace('$', '', $property_name), + ($gap === '::' ? '$' : '') . + str_replace('$', '', $property_name), + ); + } + + foreach ( + $class_storage->pseudo_property_set_types + as $property_name => $type + ) { + $pseudo_property_types[$property_name] = new CompletionItem( + str_replace('$', '', $property_name), + CompletionItemKind::PROPERTY, + $type->__toString(), + null, + '1', + str_replace('$', '', $property_name), + ($gap === '::' ? '$' : '') . + str_replace('$', '', $property_name), ); + } - if ($property_storage->is_static || $gap === '->') { - $completion_items[] = new CompletionItem( - '$' . $property_name, - CompletionItemKind::PROPERTY, - $property_storage->getInfo(), - $property_storage->description, - (string)$property_storage->visibility, - $property_name, - ($gap === '::' ? '$' : '') . $property_name + $completion_items = array_merge($completion_items, array_values($pseudo_property_types)); + + foreach ( + $class_storage->declaring_property_ids + as $property_name => $declaring_class + ) { + try { + $property_storage = $this->properties->getStorage( + $declaring_class . '::$' . $property_name, ); + + if ($property_storage->is_static || $gap === '->') { + $completion_items[] = new CompletionItem( + $property_name, + CompletionItemKind::PROPERTY, + $property_storage->getInfo(), + $property_storage->description, + (string) $property_storage->visibility, + $property_name, + ($gap === '::' ? '$' : '') . $property_name, + ); + } + } catch (Exception $e) { + error_log($e->getMessage()); + continue; } } - foreach ($class_storage->constants as $const_name => $const) { + foreach ( + $class_storage->constants + as $const_name => $const + ) { $completion_items[] = new CompletionItem( $const_name, CompletionItemKind::VARIABLE, @@ -478,7 +647,7 @@ public function getCompletionItemsForClassishThing( $const->description, null, $const_name, - $const_name + $const_name, ); } } catch (Exception $e) { @@ -494,14 +663,16 @@ public function getCompletionItemsForClassishThing( /** * @return list */ - public function getCompletionItemsForArrayKeys( - string $type_string - ): array { + public function getCompletionItemsForArrayKeys(string $type_string): array + { $completion_items = []; $type = Type::parseString($type_string); foreach ($type->getAtomicTypes() as $atomic_type) { if ($atomic_type instanceof TKeyedArray) { - foreach ($atomic_type->properties as $property_name => $property) { + foreach ( + $atomic_type->properties + as $property_name => $property + ) { $completion_items[] = new CompletionItem( (string) $property_name, CompletionItemKind::PROPERTY, @@ -509,7 +680,7 @@ public function getCompletionItemsForArrayKeys( null, null, null, - "'$property_name'" + "'$property_name'", ); } } @@ -523,7 +694,7 @@ public function getCompletionItemsForArrayKeys( public function getCompletionItemsForPartialSymbol( string $type_string, int $offset, - string $file_path + string $file_path, ): array { $fq_suggestion = false; @@ -531,7 +702,9 @@ public function getCompletionItemsForPartialSymbol( $fq_suggestion = true; } - $matching_classlike_names = $this->classlikes->getMatchingClassLikeNames($type_string); + $matching_classlike_names = $this->classlikes->getMatchingClassLikeNames( + $type_string, + ); $completion_items = []; @@ -541,7 +714,9 @@ public function getCompletionItemsForPartialSymbol( foreach ($file_storage->classlikes_in_file as $fq_class_name => $_) { try { - $class_storage = $this->classlike_storage_provider->get($fq_class_name); + $class_storage = $this->classlike_storage_provider->get( + $fq_class_name, + ); } catch (Exception $e) { continue; } @@ -550,8 +725,9 @@ public function getCompletionItemsForPartialSymbol( continue; } - if ($offset > $class_storage->stmt_location->raw_file_start - && $offset < $class_storage->stmt_location->raw_file_end + if ( + $offset > $class_storage->stmt_location->raw_file_start && + $offset < $class_storage->stmt_location->raw_file_end ) { $aliases = $class_storage->aliases; break; @@ -559,7 +735,10 @@ public function getCompletionItemsForPartialSymbol( } if (!$aliases) { - foreach ($file_storage->namespace_aliases as $namespace_start => $namespace_aliases) { + foreach ( + $file_storage->namespace_aliases + as $namespace_start => $namespace_aliases + ) { if ($namespace_start < $offset) { $aliases = $namespace_aliases; break; @@ -578,36 +757,37 @@ public function getCompletionItemsForPartialSymbol( $fq_class_name, $aliases && $aliases->namespace ? $aliases->namespace : null, $aliases->uses_flipped ?? [], - null + null, ); - if ($aliases - && !$fq_suggestion - && $aliases->namespace - && $insertion_text === '\\' . $fq_class_name - && $aliases->namespace_first_stmt_start + if ( + $aliases && + !$fq_suggestion && + $aliases->namespace && + $insertion_text === '\\' . $fq_class_name && + $aliases->namespace_first_stmt_start ) { $file_contents = $this->getFileContents($file_path); $class_name = preg_replace('/^.*\\\/', '', $fq_class_name); if ($aliases->uses_end) { - $position = self::getPositionFromOffset($aliases->uses_end, $file_contents); + $position = self::getPositionFromOffset( + $aliases->uses_end, + $file_contents, + ); $extra_edits[] = new TextEdit( - new Range( - $position, - $position - ), - "\n" . 'use ' . $fq_class_name . ';' + new Range($position, $position), + "\n" . 'use ' . $fq_class_name . ';', ); } else { - $position = self::getPositionFromOffset($aliases->namespace_first_stmt_start, $file_contents); + $position = self::getPositionFromOffset( + $aliases->namespace_first_stmt_start, + $file_contents, + ); $extra_edits[] = new TextEdit( - new Range( - $position, - $position - ), - 'use ' . $fq_class_name . ';' . "\n" . "\n" + new Range($position, $position), + 'use ' . $fq_class_name . ';' . "\n" . "\n", ); } @@ -615,7 +795,9 @@ public function getCompletionItemsForPartialSymbol( } try { - $class_storage = $this->classlike_storage_provider->get($fq_class_name); + $class_storage = $this->classlike_storage_provider->get( + $fq_class_name, + ); $description = $class_storage->description; } catch (Exception $e) { $description = null; @@ -630,11 +812,16 @@ public function getCompletionItemsForPartialSymbol( $fq_class_name, $insertion_text, null, - $extra_edits + $extra_edits, ); } - $functions = $this->functions->getMatchingFunctionNames($type_string, $offset, $file_path, $this); + $functions = $this->functions->getMatchingFunctionNames( + $type_string, + $offset, + $file_path, + $this, + ); $namespace_map = []; if ($aliases) { @@ -658,7 +845,10 @@ public function getCompletionItemsForPartialSymbol( $in_namespace_map = false; foreach ($namespace_map as $namespace_name => $namespace_alias) { if (strpos($function_lowercase, $namespace_name . '\\') === 0) { - $function_name = $namespace_alias . '\\' . substr($function_name, strlen($namespace_name) + 1); + $function_name = + $namespace_alias . + '\\' . + substr($function_name, strlen($namespace_name) + 1); $in_namespace_map = true; } } @@ -674,19 +864,23 @@ public function getCompletionItemsForPartialSymbol( $function->description, null, $function_name, - $function_name . (count($function->params) !== 0 ? '($0)' : '()'), + $function_name . + (count($function->params) !== 0 ? '($0)' : '()'), null, null, - new Command('Trigger parameter hints', 'editor.action.triggerParameterHints'), + new Command( + 'Trigger parameter hints', + 'editor.action.triggerParameterHints', + ), null, - 2 + 2, ); } return $completion_items; } - /** + /** * @return list */ public function getCompletionItemsForType(Union $type): array @@ -694,7 +888,10 @@ public function getCompletionItemsForType(Union $type): array $completion_items = []; foreach ($type->getAtomicTypes() as $atomic_type) { if ($atomic_type instanceof TBool) { - $bools = (string) $atomic_type === 'bool' ? ['true', 'false'] : [(string) $atomic_type]; + $bools = + (string) $atomic_type === 'bool' + ? ['true', 'false'] + : [(string) $atomic_type]; foreach ($bools as $property_name) { $completion_items[] = new CompletionItem( $property_name, @@ -703,7 +900,7 @@ public function getCompletionItemsForType(Union $type): array null, null, null, - $property_name + $property_name, ); } } elseif ($atomic_type instanceof TLiteralString) { @@ -714,7 +911,7 @@ public function getCompletionItemsForType(Union $type): array null, null, null, - "'$atomic_type->value'" + "'$atomic_type->value'", ); } elseif ($atomic_type instanceof TLiteralInt) { $completion_items[] = new CompletionItem( @@ -724,10 +921,13 @@ public function getCompletionItemsForType(Union $type): array null, null, null, - (string) $atomic_type->value + (string) $atomic_type->value, ); } elseif ($atomic_type instanceof TClassConstant) { - $const = $atomic_type->fq_classlike_name . '::' . $atomic_type->const_name; + $const = + $atomic_type->fq_classlike_name . + '::' . + $atomic_type->const_name; $completion_items[] = new CompletionItem( $const, CompletionItemKind::VALUE, @@ -735,7 +935,7 @@ public function getCompletionItemsForType(Union $type): array null, null, null, - $const + $const, ); } } @@ -745,8 +945,10 @@ public function getCompletionItemsForType(Union $type): array /** * @return array{0: non-empty-string, 1: int, 2: Range}|null */ - public function getFunctionArgumentAtPosition(string $file_path, Position $position): ?array - { + public function getFunctionArgumentAtPosition( + string $file_path, + Position $position, + ): ?array { $is_open = $this->file_provider->isOpen($file_path); if (!$is_open) { @@ -771,7 +973,11 @@ public function getFunctionArgumentAtPosition(string $file_path, Position $posit ksort($argument_map); - foreach ($argument_map as $start_pos => [$end_pos, $possible_reference, $possible_argument_number]) { + foreach ( + $argument_map + as $start_pos => + [$end_pos, $possible_reference, $possible_argument_number] + ) { if ($offset < $start_pos) { break; } @@ -784,13 +990,18 @@ public function getFunctionArgumentAtPosition(string $file_path, Position $posit $argument_number = $possible_argument_number; } - if ($reference === null || $start_pos === null || $end_pos === null || $argument_number === null) { + if ( + $reference === null || + $start_pos === null || + $end_pos === null || + $argument_number === null + ) { return null; } $range = new Range( self::getPositionFromOffset($start_pos, $file_contents), - self::getPositionFromOffset($end_pos, $file_contents) + self::getPositionFromOffset($end_pos, $file_contents), ); return [$reference, $argument_number, $range]; @@ -801,15 +1012,19 @@ public function getFunctionArgumentAtPosition(string $file_path, Position $posit */ public function getSignatureInformation( string $function_symbol, - string $file_path = null + string $file_path = null, ): ?SignatureInformation { $signature_label = ''; $signature_documentation = null; if (strpos($function_symbol, '::') !== false) { /** @psalm-suppress ArgumentTypeCoercion */ - $method_id = new MethodIdentifier(...explode('::', $function_symbol)); + $method_id = new MethodIdentifier( + ...explode('::', $function_symbol), + ); - $declaring_method_id = $this->methods->getDeclaringMethodId($method_id); + $declaring_method_id = $this->methods->getDeclaringMethodId( + $method_id, + ); if ($declaring_method_id === null) { return null; @@ -826,17 +1041,22 @@ public function getSignatureInformation( null, strtolower($function_symbol), dirname($file_path), - $file_path + $file_path, ); } else { - $function_storage = $this->functions->getStorage(null, strtolower($function_symbol)); + $function_storage = $this->functions->getStorage( + null, + strtolower($function_symbol), + ); } $params = $function_storage->params; $signature_label = $function_storage->cased_name; $signature_documentation = $function_storage->description; } catch (Exception $exception) { if (InternalCallMapHandler::inCallMap($function_symbol)) { - $callables = InternalCallMapHandler::getCallablesFromCallMap($function_symbol); + $callables = InternalCallMapHandler::getCallablesFromCallMap( + $function_symbol, + ); if (!$callables || !$callables[0]->params) { throw $exception; @@ -859,12 +1079,12 @@ public function getSignatureInformation( strlen($signature_label), strlen($signature_label) + strlen($parameter_label), ], - $param->description ?? null + $param->description ?? null, ); $signature_label .= $parameter_label; - if ($i < (count($params) - 1)) { + if ($i < count($params) - 1) { $signature_label .= ', '; } } @@ -874,7 +1094,7 @@ public function getSignatureInformation( return new SignatureInformation( $signature_label, $parameters, - $signature_documentation + $signature_documentation, ); } @@ -895,7 +1115,7 @@ public function getSymbolLocation(Reference $reference): ?CodeLocation $reference->file_path, $this->config->shortenFileName($reference->file_path), (int) $symbol_parts[0], - (int) $symbol_parts[1] + (int) $symbol_parts[1], ); } @@ -905,9 +1125,13 @@ public function getSymbolLocation(Reference $reference): ?CodeLocation $symbol = substr($reference->symbol, 0, -2); /** @psalm-suppress ArgumentTypeCoercion */ - $method_id = new MethodIdentifier(...explode('::', $symbol)); + $method_id = new MethodIdentifier( + ...explode('::', $symbol), + ); - $declaring_method_id = $this->methods->getDeclaringMethodId($method_id); + $declaring_method_id = $this->methods->getDeclaringMethodId( + $method_id, + ); if (!$declaring_method_id) { return null; @@ -919,16 +1143,21 @@ public function getSymbolLocation(Reference $reference): ?CodeLocation } if (strpos($reference->symbol, '$') !== false) { - $storage = $this->properties->getStorage($reference->symbol); + $storage = $this->properties->getStorage( + $reference->symbol, + ); return $storage->location; } - [$fq_classlike_name, $const_name] = explode('::', $reference->symbol); + [$fq_classlike_name, $const_name] = explode( + '::', + $reference->symbol, + ); $class_constants = $this->classlikes->getConstantsForClass( $fq_classlike_name, - ReflectionProperty::IS_PRIVATE + ReflectionProperty::IS_PRIVATE, ); if (!isset($class_constants[$const_name])) { @@ -939,7 +1168,9 @@ public function getSymbolLocation(Reference $reference): ?CodeLocation } if (strpos($reference->symbol, '()')) { - $file_storage = $this->file_storage_provider->get($reference->file_path); + $file_storage = $this->file_storage_provider->get( + $reference->file_path, + ); $function_id = strtolower(substr($reference->symbol, 0, -2)); @@ -951,10 +1182,13 @@ public function getSymbolLocation(Reference $reference): ?CodeLocation return null; } - return $this->functions->getStorage(null, $function_id)->location; + return $this->functions->getStorage(null, $function_id) + ->location; } - return $this->classlike_storage_provider->get($reference->symbol)->location; + return $this->classlike_storage_provider->get( + $reference->symbol, + )->location; } catch (UnexpectedValueException $e) { error_log($e->getMessage()); @@ -964,9 +1198,16 @@ public function getSymbolLocation(Reference $reference): ?CodeLocation } } - public function addTemporaryFileChanges(string $file_path, string $new_content, ?int $version = null): void - { - $this->file_provider->addTemporaryFileChanges($file_path, $new_content, $version); + public function addTemporaryFileChanges( + string $file_path, + string $new_content, + ?int $version = null, + ): void { + $this->file_provider->addTemporaryFileChanges( + $file_path, + $new_content, + $version, + ); } public function removeTemporaryFileChanges(string $file_path): void diff --git a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php index 64fae98036a..73202d05fa7 100644 --- a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php +++ b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php @@ -352,7 +352,6 @@ public function completion(TextDocumentIdentifier $textDocument, Position $posit return new Success(null); } - return new Success(new CompletionList($completion_items, false)); } From e5c99529635f85581a3f12d96cb736912153a849 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Thu, 28 Apr 2022 18:12:50 +0000 Subject: [PATCH 37/78] cleanups --- src/Psalm/Codebase.php | 1 - .../LanguageServer/Client/TextDocument.php | 1 - .../Internal/LanguageServer/Codebase.php | 122 +++++------------- .../LanguageServer/LanguageServer.php | 18 +++ .../LanguageServer/Server/TextDocument.php | 53 ++++---- tests/AsyncTestCase.php | 1 - tests/LanguageServer/DiagnosticTest.php | 46 +++---- tests/LanguageServer/Message.php | 15 ++- tests/LanguageServer/MockProtocolStream.php | 12 +- 9 files changed, 111 insertions(+), 158 deletions(-) diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index c0d9760be9c..be4ffe58e83 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -57,7 +57,6 @@ use const PHP_MINOR_VERSION; use const PHP_VERSION_ID; - class Codebase { /** diff --git a/src/Psalm/Internal/LanguageServer/Client/TextDocument.php b/src/Psalm/Internal/LanguageServer/Client/TextDocument.php index 9446bc41316..f6a07a56390 100644 --- a/src/Psalm/Internal/LanguageServer/Client/TextDocument.php +++ b/src/Psalm/Internal/LanguageServer/Client/TextDocument.php @@ -4,7 +4,6 @@ namespace Psalm\Internal\LanguageServer\Client; -use JsonMapper; use LanguageServerProtocol\Diagnostic; use Psalm\Internal\LanguageServer\ClientHandler; use Psalm\Internal\LanguageServer\LanguageServer; diff --git a/src/Psalm/Internal/LanguageServer/Codebase.php b/src/Psalm/Internal/LanguageServer/Codebase.php index 1c616733648..94f021144c7 100644 --- a/src/Psalm/Internal/LanguageServer/Codebase.php +++ b/src/Psalm/Internal/LanguageServer/Codebase.php @@ -35,8 +35,10 @@ use ReflectionProperty; use UnexpectedValueException; +use function array_merge; use function array_pop; use function array_reverse; +use function array_values; use function count; use function dirname; use function error_log; @@ -47,6 +49,7 @@ use function ksort; use function preg_match; use function preg_replace; +use function str_replace; use function strlen; use function strpos; use function strrpos; @@ -78,8 +81,6 @@ public function getReferenceAtPosition( $reference_maps = $this->analyzer->getMapsForFile($file_path); - [$r] = $reference_maps; - $reference_start_pos = null; $reference_end_pos = null; $symbol = null; @@ -87,10 +88,7 @@ public function getReferenceAtPosition( foreach ($reference_maps as $reference_map) { ksort($reference_map); - foreach ( - $reference_map - as $start_pos => [$end_pos, $possible_reference] - ) { + foreach ($reference_map as $start_pos => [$end_pos, $possible_reference]) { if ($offset < $start_pos) { break; } @@ -103,8 +101,7 @@ public function getReferenceAtPosition( $symbol = $possible_reference; } - if ( - $symbol !== null && + if ($symbol !== null && $reference_start_pos !== null && $reference_end_pos !== null ) { @@ -112,11 +109,7 @@ public function getReferenceAtPosition( } } - if ( - $symbol === null || - $reference_start_pos === null || - $reference_end_pos === null - ) { + if ($symbol === null || $reference_start_pos === null || $reference_end_pos === null) { return null; } @@ -195,7 +188,8 @@ public function getMarkupContentForSymbol( //Get Docblock properties if (isset($class_storage->pseudo_property_set_types['$'.$property_name])) { return new PHPMarkdownContent( - 'public '.(string) $class_storage->pseudo_property_set_types['$'.$property_name].' $'.$property_name, + 'public '. + (string) $class_storage->pseudo_property_set_types['$'.$property_name].' $'.$property_name, $reference->symbol ); } @@ -203,13 +197,13 @@ public function getMarkupContentForSymbol( //Get Docblock properties if (isset($class_storage->pseudo_property_get_types['$'.$property_name])) { return new PHPMarkdownContent( - 'public '.(string) $class_storage->pseudo_property_get_types['$'.$property_name].' $'.$property_name, + 'public '. + (string) $class_storage->pseudo_property_get_types['$'.$property_name].' $'.$property_name, $reference->symbol ); } return null; - } [$fq_classlike_name, $const_name] = explode( @@ -387,10 +381,7 @@ public function getCompletionDataAtPosition( krsort($type_map); - foreach ( - $type_map - as $start_pos => [$end_pos_excluding_whitespace, $possible_type] - ) { + foreach ($type_map as $start_pos => [$end_pos_excluding_whitespace, $possible_type]) { if ($offset < $start_pos) { continue; } @@ -438,18 +429,12 @@ public function getCompletionDataAtPosition( } } - foreach ( - $reference_map - as $start_pos => [$end_pos, $possible_reference] - ) { + foreach ($reference_map as $start_pos => [$end_pos, $possible_reference]) { if ($offset < $start_pos) { continue; } // If the reference precedes a "::" then treat it as a class reference. - if ( - $offset - $end_pos === 2 && - substr($file_contents, $end_pos, 2) === '::' - ) { + if ($offset - $end_pos === 2 && substr($file_contents, $end_pos, 2) === '::') { return [$possible_reference, '::', $offset]; } @@ -483,10 +468,7 @@ public function getTypeContextAtPosition( if (!$reference_map && !$type_map && !$argument_map) { return null; } - foreach ( - $argument_map - as $start_pos => [$end_pos, $function, $argument_num] - ) { + foreach ($argument_map as $start_pos => [$end_pos, $function, $argument_num]) { if ($offset < $start_pos || $offset > $end_pos) { continue; } @@ -495,11 +477,7 @@ public function getTypeContextAtPosition( $file_path, $function . '()', ); - if ( - !$function_storage || - !$function_storage->params || - !isset($function_storage->params[$argument_num]) - ) { + if (!$function_storage || !$function_storage->params || !isset($function_storage->params[$argument_num])) { return null; } @@ -510,7 +488,7 @@ public function getTypeContextAtPosition( } /** - * @return list + * @return CompletionItem[] */ public function getCompletionItemsForClassishThing( string $type_string, @@ -528,10 +506,7 @@ public function getCompletionItemsForClassishThing( $atomic_type->value, ); - foreach ( - $class_storage->appearing_method_ids - as $declaring_method_id - ) { + foreach ($class_storage->appearing_method_ids as $declaring_method_id) { try { $method_storage = $this->methods->getStorage( $declaring_method_id, @@ -556,10 +531,7 @@ public function getCompletionItemsForClassishThing( 2, ); - if ( - $snippets_supported && - count($method_storage->params) > 0 - ) { + if ($snippets_supported && count($method_storage->params) > 0) { $completion_item->insertText .= '($0)'; $completion_item->insertTextFormat = InsertTextFormat::SNIPPET; @@ -576,10 +548,7 @@ public function getCompletionItemsForClassishThing( } $pseudo_property_types = []; - foreach ( - $class_storage->pseudo_property_get_types - as $property_name => $type - ) { + foreach ($class_storage->pseudo_property_get_types as $property_name => $type) { $pseudo_property_types[$property_name] = new CompletionItem( str_replace('$', '', $property_name), CompletionItemKind::PROPERTY, @@ -592,10 +561,7 @@ public function getCompletionItemsForClassishThing( ); } - foreach ( - $class_storage->pseudo_property_set_types - as $property_name => $type - ) { + foreach ($class_storage->pseudo_property_set_types as $property_name => $type) { $pseudo_property_types[$property_name] = new CompletionItem( str_replace('$', '', $property_name), CompletionItemKind::PROPERTY, @@ -610,10 +576,7 @@ public function getCompletionItemsForClassishThing( $completion_items = array_merge($completion_items, array_values($pseudo_property_types)); - foreach ( - $class_storage->declaring_property_ids - as $property_name => $declaring_class - ) { + foreach ($class_storage->declaring_property_ids as $property_name => $declaring_class) { try { $property_storage = $this->properties->getStorage( $declaring_class . '::$' . $property_name, @@ -636,10 +599,7 @@ public function getCompletionItemsForClassishThing( } } - foreach ( - $class_storage->constants - as $const_name => $const - ) { + foreach ($class_storage->constants as $const_name => $const) { $completion_items[] = new CompletionItem( $const_name, CompletionItemKind::VARIABLE, @@ -661,7 +621,7 @@ public function getCompletionItemsForClassishThing( } /** - * @return list + * @return CompletionItem[] */ public function getCompletionItemsForArrayKeys(string $type_string): array { @@ -669,10 +629,7 @@ public function getCompletionItemsForArrayKeys(string $type_string): array $type = Type::parseString($type_string); foreach ($type->getAtomicTypes() as $atomic_type) { if ($atomic_type instanceof TKeyedArray) { - foreach ( - $atomic_type->properties - as $property_name => $property - ) { + foreach ($atomic_type->properties as $property_name => $property) { $completion_items[] = new CompletionItem( (string) $property_name, CompletionItemKind::PROPERTY, @@ -689,7 +646,7 @@ public function getCompletionItemsForArrayKeys(string $type_string): array } /** - * @return list + * @return CompletionItem[] */ public function getCompletionItemsForPartialSymbol( string $type_string, @@ -725,20 +682,15 @@ public function getCompletionItemsForPartialSymbol( continue; } - if ( - $offset > $class_storage->stmt_location->raw_file_start && - $offset < $class_storage->stmt_location->raw_file_end - ) { + if ($offset > $class_storage->stmt_location->raw_file_start + && $offset < $class_storage->stmt_location->raw_file_end) { $aliases = $class_storage->aliases; break; } } if (!$aliases) { - foreach ( - $file_storage->namespace_aliases - as $namespace_start => $namespace_aliases - ) { + foreach ($file_storage->namespace_aliases as $namespace_start => $namespace_aliases) { if ($namespace_start < $offset) { $aliases = $namespace_aliases; break; @@ -760,8 +712,7 @@ public function getCompletionItemsForPartialSymbol( null, ); - if ( - $aliases && + if ($aliases && !$fq_suggestion && $aliases->namespace && $insertion_text === '\\' . $fq_class_name && @@ -881,7 +832,7 @@ public function getCompletionItemsForPartialSymbol( } /** - * @return list + * @return CompletionItem[] */ public function getCompletionItemsForType(Union $type): array { @@ -973,11 +924,7 @@ public function getFunctionArgumentAtPosition( ksort($argument_map); - foreach ( - $argument_map - as $start_pos => - [$end_pos, $possible_reference, $possible_argument_number] - ) { + foreach ($argument_map as $start_pos => [$end_pos, $possible_reference, $possible_argument_number]) { if ($offset < $start_pos) { break; } @@ -990,12 +937,7 @@ public function getFunctionArgumentAtPosition( $argument_number = $possible_argument_number; } - if ( - $reference === null || - $start_pos === null || - $end_pos === null || - $argument_number === null - ) { + if ($reference === null || $start_pos === null || $end_pos === null || $argument_number === null) { return null; } diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 694b1f12b50..ecffe3b0a20 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -148,6 +148,12 @@ class LanguageServer extends Dispatcher */ protected $versionedAnalysisDelayToken = ''; + /** + * Whether analysis is queued/processing + * @var bool + */ + public $analyzing = false; + public function __construct( ProtocolReader $reader, ProtocolWriter $writer, @@ -696,6 +702,7 @@ function (array $opened, string $file_path) { */ public function doVersionedAnalysisDebounce(array $files, ?int $version = null): void { + $this->analyzing = true; Loop::cancel($this->versionedAnalysisDelayToken); if ($this->client->clientConfiguration->onChangeDebounceMs === null) { $this->doVersionedAnalysis($files, $version); @@ -734,6 +741,7 @@ public function doVersionedAnalysis(array $files, ?int $version = null): void } catch (Throwable $e) { $this->logError((string) $e); } + $this->analyzing = false; } /** @@ -905,6 +913,16 @@ public function log(int $type, string $message, array $context = []): void } } + /** + * Log Throwable Error + * + * @param Throwable $throwable + */ + public function logThrowable(Throwable $throwable): void + { + $this->log(MessageType::ERROR, (string) $throwable); + } + /** * Log Error message to the client * diff --git a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php index 73202d05fa7..43c38942529 100644 --- a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php +++ b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php @@ -128,7 +128,7 @@ public function didChange(VersionedTextDocumentIdentifier $textDocument, array $ $file_path = LanguageServer::uriToPath($textDocument->uri); - if (count($contentChanges) === 1 && $contentChanges[0]->range === null) { + if (count($contentChanges) === 1 && isset($contentChanges[0]) && $contentChanges[0]->range === null) { $new_content = $contentChanges[0]->text; } else { throw new UnexpectedValueException('Not expecting partial diff'); @@ -196,7 +196,7 @@ public function definition(TextDocumentIdentifier $textDocument, Position $posit try { $reference = $this->codebase->getReferenceAtPosition($file_path, $position); } catch (UnanalyzedFileException $e) { - $this->server->logError((string) $e); + $this->server->logThrowable($e); return new Success(null); } @@ -250,7 +250,7 @@ public function hover(TextDocumentIdentifier $textDocument, Position $position): try { $reference = $this->codebase->getReferenceAtPosition($file_path, $position); } catch (UnanalyzedFileException $e) { - $this->server->logError((string) $e); + $this->server->logThrowable($e); return new Success(null); } @@ -261,7 +261,7 @@ public function hover(TextDocumentIdentifier $textDocument, Position $position): try { $markup = $this->codebase->getMarkupContentForSymbol($reference); } catch (UnexpectedValueException $e) { - $this->server->logError((string) $e); + $this->server->logThrowable($e); return new Success(null); } @@ -305,24 +305,6 @@ public function completion(TextDocumentIdentifier $textDocument, Position $posit try { $completion_data = $this->codebase->getCompletionDataAtPosition($file_path, $position); - } catch (UnanalyzedFileException $e) { - $this->server->logError((string) $e); - return new Success(null); - } - - try { - $type_context = $this->codebase->getTypeContextAtPosition($file_path, $position); - } catch (UnexpectedValueException $e) { - $this->server->logError((string) $e); - return new Success(null); - } - - if (!$completion_data && !$type_context) { - $this->server->logError('completion not found at ' . $position->line . ':' . $position->character); - return new Success(null); - } - - try { if ($completion_data) { [$recent_type, $gap, $offset] = $completion_data; @@ -344,15 +326,32 @@ public function completion(TextDocumentIdentifier $textDocument, Position $posit $file_path ); } - } else { + return new Success(new CompletionList($completion_items, false)); + } + } catch (UnanalyzedFileException $e) { + $this->server->logThrowable($e); + return new Success(null); + } catch (TypeParseTreeException $e) { + $this->server->logThrowable($e); + return new Success(null); + } + + try { + $type_context = $this->codebase->getTypeContextAtPosition($file_path, $position); + if ($type_context) { $completion_items = $this->codebase->getCompletionItemsForType($type_context); + return new Success(new CompletionList($completion_items, false)); } + } catch (UnexpectedValueException $e) { + $this->server->logThrowable($e); + return new Success(null); } catch (TypeParseTreeException $e) { - $this->server->logError((string) $e); + $this->server->logThrowable($e); return new Success(null); } - return new Success(new CompletionList($completion_items, false)); + $this->server->logError('completion not found at ' . $position->line . ':' . $position->character); + return new Success(null); } /** @@ -379,7 +378,7 @@ public function signatureHelp(TextDocumentIdentifier $textDocument, Position $po try { $argument_location = $this->codebase->getFunctionArgumentAtPosition($file_path, $position); } catch (UnanalyzedFileException $e) { - $this->server->logError((string) $e); + $this->server->logThrowable($e); return new Success(null); } @@ -390,7 +389,7 @@ public function signatureHelp(TextDocumentIdentifier $textDocument, Position $po try { $signature_information = $this->codebase->getSignatureInformation($argument_location[0], $file_path); } catch (UnexpectedValueException $e) { - $this->server->logError((string) $e); + $this->server->logThrowable($e); return new Success(null); } diff --git a/tests/AsyncTestCase.php b/tests/AsyncTestCase.php index 86e35107b5e..27f6da4fefe 100644 --- a/tests/AsyncTestCase.php +++ b/tests/AsyncTestCase.php @@ -3,7 +3,6 @@ namespace Psalm\Tests; use Amp\PHPUnit\AsyncTestCase as BaseAsyncTestCase; -use PHPUnit\Framework\TestCase as BaseTestCase; use Psalm\Config; use Psalm\Context; use Psalm\Internal\Analyzer\FileAnalyzer; diff --git a/tests/LanguageServer/DiagnosticTest.php b/tests/LanguageServer/DiagnosticTest.php index ee55ecf0ec1..e69aa665393 100644 --- a/tests/LanguageServer/DiagnosticTest.php +++ b/tests/LanguageServer/DiagnosticTest.php @@ -2,37 +2,27 @@ namespace Psalm\Tests\LanguageServer; -use Amp\ByteStream\InMemoryStream; -use Amp\ByteStream\ResourceInputStream; -use Amp\ByteStream\ResourceOutputStream; -use LanguageServerProtocol\Position; -use LanguageServerProtocol\Range; -use Psalm\Context; -use Psalm\Internal\Analyzer\FileAnalyzer; +use Amp\Deferred; use Psalm\Internal\Analyzer\IssueData; use Psalm\Internal\Analyzer\ProjectAnalyzer; +use Psalm\Internal\LanguageServer\ClientConfiguration; use Psalm\Internal\LanguageServer\Codebase; -use Psalm\Internal\LanguageServer\Reference; +use Psalm\Internal\LanguageServer\LanguageServer; +use Psalm\Internal\LanguageServer\Message; +use Psalm\Internal\LanguageServer\Progress; use Psalm\Internal\Provider\FakeFileProvider; use Psalm\Internal\Provider\Providers; use Psalm\IssueBuffer; +use Psalm\Tests\AsyncTestCase; use Psalm\Tests\Internal\Provider\FakeFileReferenceCacheProvider; use Psalm\Tests\Internal\Provider\ParserInstanceCacheProvider; use Psalm\Tests\Internal\Provider\ProjectCacheProvider; -use Psalm\Tests\AsyncTestCase; -use Psalm\Tests\TestConfig; -use Psalm\Tests\LanguageServer\MockProtocolStream; -use Psalm\Internal\LanguageServer\ClientConfiguration; -use Psalm\Internal\LanguageServer\LanguageServer; -use Psalm\Internal\LanguageServer\Message; -use Psalm\Internal\LanguageServer\Progress; -use Psalm\Internal\LanguageServer\ProtocolStreamReader; -use Psalm\Internal\LanguageServer\ProtocolStreamWriter; -use Psalm\Progress\VoidProgress; use Psalm\Tests\LanguageServer\Message as MessageBody; -use AdvancedJsonRpc\Request; -use AdvancedJsonRpc\SuccessResponse; -use Amp\Deferred; +use Psalm\Tests\LanguageServer\MockProtocolStream; +use Psalm\Tests\TestConfig; + +use function Amp\Promise\wait; +use function rand; class DiagnosticTest extends AsyncTestCase { @@ -84,7 +74,7 @@ public function setUp(): void $this->project_analyzer->getCodebase()->store_node_types = true; } - public function testSnippetSupportDisabled() + public function testSnippetSupportDisabled(): void { // Create a new promisor $deferred = new Deferred; @@ -96,6 +86,7 @@ public function testSnippetSupportDisabled() $write = new MockProtocolStream(); $array = $this->generateInitializeRequest(); + /** @psalm-suppress MixedArrayAssignment */ $array['params']['capabilities']['textDocument']['completion']['completionItem']['snippetSupport'] = false; $read->write(new Message(MessageBody::parseArray($array))); @@ -108,14 +99,15 @@ public function testSnippetSupportDisabled() new Progress ); - $write->on('message', function (Message $message) use ($deferred, $server): void { + $write->on('message', function (Message $message) use ($deferred, $server): void { + /** @psalm-suppress PossiblyNullPropertyFetch,UndefinedPropertyFetch,MixedPropertyFetch */ if ($message->body->method === 'telemetry/event' && $message->body->params->message === 'initialized') { $this->assertFalse($server->clientCapabilities->textDocument->completion->completionItem->snippetSupport); $deferred->resolve(null); } }); - \Amp\Promise\wait($deferred->promise()); + wait($deferred->promise()); } public function jestRun(): void @@ -195,7 +187,7 @@ public function foobar(): void {} }' ); - print_r($issues); + $this->assertArrayHasKey('somefile.php', $issues); $issues = $this->changeFile( 'somefile.php', @@ -216,7 +208,7 @@ public function foobar(): void {} }' ); - print_r($issues); + $this->assertArrayHasKey('somefile.php', $issues); $issues = $this->changeFile( 'somefile.php', @@ -234,7 +226,7 @@ public function foobar(): void {} }' ); - print_r($issues); + $this->assertArrayHasKey('somefile.php', $issues); $issues = $this->changeFile( 'somefile.php', diff --git a/tests/LanguageServer/Message.php b/tests/LanguageServer/Message.php index 81fd0dc2420..48b4ec6d819 100644 --- a/tests/LanguageServer/Message.php +++ b/tests/LanguageServer/Message.php @@ -2,11 +2,13 @@ namespace Psalm\Tests\LanguageServer; +use AdvancedJsonRpc\Error; +use AdvancedJsonRpc\ErrorCode; +use AdvancedJsonRpc\ErrorResponse; use AdvancedJsonRpc\Message as AdvancedJsonRpcMessage; use AdvancedJsonRpc\Notification; use AdvancedJsonRpc\Request; use AdvancedJsonRpc\SuccessResponse; -use AdvancedJsonRpc\ErrorResponse; /** * Base message @@ -24,18 +26,21 @@ abstract class Message extends AdvancedJsonRpcMessage * Returns the appropriate Message subclass * * @param array $msg - * @return AdvancedJsonRpcMessage */ public static function parseArray(array $msg): AdvancedJsonRpcMessage { $decoded = (object) $msg; if (Notification::isNotification($decoded)) { + /** @psalm-suppress MixedArgument */ $obj = new Notification($decoded->method, $decoded->params ?? null); - } else if (Request::isRequest($decoded)) { + } elseif (Request::isRequest($decoded)) { + /** @psalm-suppress MixedArgument */ $obj = new Request($decoded->id, $decoded->method, $decoded->params ?? null); - } else if (SuccessResponse::isSuccessResponse($decoded)) { + } elseif (SuccessResponse::isSuccessResponse($decoded)) { + /** @psalm-suppress MixedArgument */ $obj = new SuccessResponse($decoded->id, $decoded->result); - } else if (ErrorResponse::isErrorResponse($decoded)) { + } elseif (ErrorResponse::isErrorResponse($decoded)) { + /** @psalm-suppress MixedArgument, MixedPropertyFetch */ $obj = new ErrorResponse($decoded->id, new Error($decoded->error->message, $decoded->error->code, $decoded->error->data ?? null)); } else { throw new Error('Invalid message', ErrorCode::INVALID_REQUEST); diff --git a/tests/LanguageServer/MockProtocolStream.php b/tests/LanguageServer/MockProtocolStream.php index f0c3176f2e5..6d61b7b2e67 100644 --- a/tests/LanguageServer/MockProtocolStream.php +++ b/tests/LanguageServer/MockProtocolStream.php @@ -1,16 +1,17 @@ emit('message', [Message::parse((string)$msg)]); }); @@ -37,4 +37,4 @@ public function write(Message $msg): Promise return $deferred->promise(); } -} \ No newline at end of file +} From 2d8cc8d623770494c162ce814e79ecf5d4c6b935 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Thu, 28 Apr 2022 18:20:51 +0000 Subject: [PATCH 38/78] fixes from shepard --- .../Internal/LanguageServer/LanguageClient.php | 10 +--------- .../Internal/LanguageServer/LanguageServer.php | 18 +++++++++--------- src/Psalm/Internal/LanguageServer/Message.php | 3 --- .../LanguageServer/PHPMarkdownContent.php | 1 + tests/AsyncTestCase.php | 4 ++-- 5 files changed, 13 insertions(+), 23 deletions(-) diff --git a/src/Psalm/Internal/LanguageServer/LanguageClient.php b/src/Psalm/Internal/LanguageServer/LanguageClient.php index a0afe20fbba..e58b390fe16 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageClient.php +++ b/src/Psalm/Internal/LanguageServer/LanguageClient.php @@ -52,13 +52,6 @@ class LanguageClient */ public $clientConfiguration; - /** - * The JsonMapper - * - * @var JsonMapper - */ - private $mapper; - public function __construct( ProtocolReader $reader, ProtocolWriter $writer, @@ -66,11 +59,10 @@ public function __construct( ClientConfiguration $clientConfiguration ) { $this->handler = new ClientHandler($reader, $writer); - $this->mapper = new JsonMapper; $this->server = $server; $this->textDocument = new ClientTextDocument($this->handler, $this->server); - $this->workspace = new ClientWorkspace($this->handler, $this->mapper, $this->server); + $this->workspace = new ClientWorkspace($this->handler, new JsonMapper, $this->server); $this->clientConfiguration = $clientConfiguration; } diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index ecffe3b0a20..890ecf14524 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -148,12 +148,6 @@ class LanguageServer extends Dispatcher */ protected $versionedAnalysisDelayToken = ''; - /** - * Whether analysis is queued/processing - * @var bool - */ - public $analyzing = false; - public function __construct( ProtocolReader $reader, ProtocolWriter $writer, @@ -702,14 +696,16 @@ function (array $opened, string $file_path) { */ public function doVersionedAnalysisDebounce(array $files, ?int $version = null): void { - $this->analyzing = true; Loop::cancel($this->versionedAnalysisDelayToken); if ($this->client->clientConfiguration->onChangeDebounceMs === null) { $this->doVersionedAnalysis($files, $version); } else { + /** @psalm-suppress MixedAssignment */ $this->versionedAnalysisDelayToken = Loop::delay( $this->client->clientConfiguration->onChangeDebounceMs, - fn() => $this->doVersionedAnalysis($files, $version) + function () use ($files, $version) { + return $this->doVersionedAnalysis($files, $version); + } ); } } @@ -741,7 +737,6 @@ public function doVersionedAnalysis(array $files, ?int $version = null): void } catch (Throwable $e) { $this->logError((string) $e); } - $this->analyzing = false; } /** @@ -917,6 +912,7 @@ public function log(int $type, string $message, array $context = []): void * Log Throwable Error * * @param Throwable $throwable + * @psalm-suppress PossiblyUnusedMethod */ public function logThrowable(Throwable $throwable): void { @@ -928,6 +924,7 @@ public function logThrowable(Throwable $throwable): void * * @param string $message * @param array $context + * @psalm-suppress PossiblyUnusedMethod */ public function logError(string $message, array $context = []): void { @@ -939,6 +936,7 @@ public function logError(string $message, array $context = []): void * * @param string $message * @param array $context + * @psalm-suppress PossiblyUnusedMethod */ public function logWarning(string $message, array $context = []): void { @@ -950,6 +948,7 @@ public function logWarning(string $message, array $context = []): void * * @param string $message * @param array $context + * @psalm-suppress PossiblyUnusedMethod */ public function logInfo(string $message, array $context = []): void { @@ -961,6 +960,7 @@ public function logInfo(string $message, array $context = []): void * * @param string $message * @param array $context + * @psalm-suppress PossiblyUnusedMethod */ public function logDebug(string $message, array $context = []): void { diff --git a/src/Psalm/Internal/LanguageServer/Message.php b/src/Psalm/Internal/LanguageServer/Message.php index 8a5af1ab8cf..f67199e4340 100644 --- a/src/Psalm/Internal/LanguageServer/Message.php +++ b/src/Psalm/Internal/LanguageServer/Message.php @@ -27,9 +27,6 @@ class Message /** * Parses a message - * - * - * @psalm-suppress UnusedMethod */ public static function parse(string $msg): Message { diff --git a/src/Psalm/Internal/LanguageServer/PHPMarkdownContent.php b/src/Psalm/Internal/LanguageServer/PHPMarkdownContent.php index 1f4471a557c..de732713149 100644 --- a/src/Psalm/Internal/LanguageServer/PHPMarkdownContent.php +++ b/src/Psalm/Internal/LanguageServer/PHPMarkdownContent.php @@ -52,6 +52,7 @@ public function __construct(string $code, ?string $title = null, ?string $descri * meaning if a null is sent then this will not compute * * @return mixed + * @psalm-suppress UnusedMethod */ #[ReturnTypeWillChange] public function jsonSerialize() diff --git a/tests/AsyncTestCase.php b/tests/AsyncTestCase.php index 27f6da4fefe..cef9b512d1c 100644 --- a/tests/AsyncTestCase.php +++ b/tests/AsyncTestCase.php @@ -105,7 +105,7 @@ public function addFile($file_path, $contents): void /** * @param string $file_path - * + * @psalm-suppress UnusedMethod */ public function analyzeFile($file_path, Context $context, bool $track_unused_suppressions = true, bool $taint_flow_tracking = false): void { @@ -145,7 +145,7 @@ public function analyzeFile($file_path, Context $context, bool $track_unused_sup /** * @param bool $withDataSet - * + * @psalm-suppress UnusedMethod */ protected function getTestName($withDataSet = true): string { From b0bd2e0831d0cc05f3bb4d53edda8eb7411a1d17 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Thu, 28 Apr 2022 18:24:17 +0000 Subject: [PATCH 39/78] more fixes --- src/Psalm/Internal/LanguageServer/LanguageServer.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 890ecf14524..4dd0056476a 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -700,7 +700,7 @@ public function doVersionedAnalysisDebounce(array $files, ?int $version = null): if ($this->client->clientConfiguration->onChangeDebounceMs === null) { $this->doVersionedAnalysis($files, $version); } else { - /** @psalm-suppress MixedAssignment */ + /** @psalm-suppress MixedAssignment,UnusedPsalmSuppress */ $this->versionedAnalysisDelayToken = Loop::delay( $this->client->clientConfiguration->onChangeDebounceMs, function () use ($files, $version) { @@ -912,7 +912,6 @@ public function log(int $type, string $message, array $context = []): void * Log Throwable Error * * @param Throwable $throwable - * @psalm-suppress PossiblyUnusedMethod */ public function logThrowable(Throwable $throwable): void { @@ -924,7 +923,6 @@ public function logThrowable(Throwable $throwable): void * * @param string $message * @param array $context - * @psalm-suppress PossiblyUnusedMethod */ public function logError(string $message, array $context = []): void { @@ -948,7 +946,6 @@ public function logWarning(string $message, array $context = []): void * * @param string $message * @param array $context - * @psalm-suppress PossiblyUnusedMethod */ public function logInfo(string $message, array $context = []): void { @@ -960,7 +957,6 @@ public function logInfo(string $message, array $context = []): void * * @param string $message * @param array $context - * @psalm-suppress PossiblyUnusedMethod */ public function logDebug(string $message, array $context = []): void { From 7819821be91a6feefe747a0a1e3e92a7088eae6e Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Thu, 28 Apr 2022 18:26:38 +0000 Subject: [PATCH 40/78] fix tests --- tests/LanguageServer/CompletionTest.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/LanguageServer/CompletionTest.php b/tests/LanguageServer/CompletionTest.php index 31bc2491bfa..0bdfb79211e 100644 --- a/tests/LanguageServer/CompletionTest.php +++ b/tests/LanguageServer/CompletionTest.php @@ -422,7 +422,7 @@ public function foo() : void { $this->assertSame(['B\C', '->', 726], $completion_data); - $completion_items = $codebase->getCompletionItemsForClassishThing($completion_data[0], $completion_data[1]); + $completion_items = $codebase->getCompletionItemsForClassishThing($completion_data[0], $completion_data[1], true); $this->assertCount(3, $completion_items); } @@ -718,7 +718,7 @@ public function baz() {} $this->assertSame(['B\A&static', '->', 146], $completion_data); - $completion_items = $codebase->getCompletionItemsForClassishThing($completion_data[0], $completion_data[1]); + $completion_items = $codebase->getCompletionItemsForClassishThing($completion_data[0], $completion_data[1], true); $this->assertCount(2, $completion_items); @@ -1257,7 +1257,7 @@ static function add() : void { $this->assertSame(['Bar\Alpha', '::', 221], $completion_data); - $completion_items = $codebase->getCompletionItemsForClassishThing($completion_data[0], $completion_data[1]); + $completion_items = $codebase->getCompletionItemsForClassishThing($completion_data[0], $completion_data[1], true); $this->assertCount(2, $completion_items); } @@ -1292,7 +1292,7 @@ public function add() : void {} $this->assertSame(['Bar\Alpha', '->', 200], $completion_data); - $completion_items = $codebase->getCompletionItemsForClassishThing($completion_data[0], $completion_data[1]); + $completion_items = $codebase->getCompletionItemsForClassishThing($completion_data[0], $completion_data[1], true); $this->assertCount(1, $completion_items); } @@ -1326,7 +1326,7 @@ static function add() : void {} $this->assertSame(['Bar\Alpha', '::', 201], $completion_data); - $completion_items = $codebase->getCompletionItemsForClassishThing($completion_data[0], $completion_data[1]); + $completion_items = $codebase->getCompletionItemsForClassishThing($completion_data[0], $completion_data[1], true); $this->assertCount(2, $completion_items); } @@ -1461,7 +1461,7 @@ public function baz() : void {} $this->assertSame(['Foo&static', '->', 129], $completion_data); - $completion_items = $codebase->getCompletionItemsForClassishThing($completion_data[0], $completion_data[1]); + $completion_items = $codebase->getCompletionItemsForClassishThing($completion_data[0], $completion_data[1], true); $this->assertCount(3, $completion_items); } From 2264fd5b50f2992d72355a5c2a1e35c9d36edee4 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Thu, 28 Apr 2022 18:28:59 +0000 Subject: [PATCH 41/78] more cleanup --- tests/AsyncTestCase.php | 15 +++++++++++++++ tests/LanguageServer/DiagnosticTest.php | 3 +++ tests/LanguageServer/MockProtocolStream.php | 1 + 3 files changed, 19 insertions(+) diff --git a/tests/AsyncTestCase.php b/tests/AsyncTestCase.php index cef9b512d1c..207facfcc12 100644 --- a/tests/AsyncTestCase.php +++ b/tests/AsyncTestCase.php @@ -152,12 +152,18 @@ protected function getTestName($withDataSet = true): string return $this->getName($withDataSet); } + /** + * @psalm-suppress UnusedMethod + */ public static function assertArrayKeysAreStrings(array $array, string $message = ''): void { $validKeys = array_filter($array, 'is_string', ARRAY_FILTER_USE_KEY); self::assertTrue(count($array) === count($validKeys), $message); } + /** + * @psalm-suppress UnusedMethod + */ public static function assertArrayKeysAreZeroOrString(array $array, string $message = ''): void { $isZeroOrString = /** @param mixed $key */ function ($key): bool { @@ -167,18 +173,27 @@ public static function assertArrayKeysAreZeroOrString(array $array, string $mess self::assertTrue(count($array) === count($validKeys), $message); } + /** + * @psalm-suppress UnusedMethod + */ public static function assertArrayValuesAreArrays(array $array, string $message = ''): void { $validValues = array_filter($array, 'is_array'); self::assertTrue(count($array) === count($validValues), $message); } + /** + * @psalm-suppress UnusedMethod + */ public static function assertArrayValuesAreStrings(array $array, string $message = ''): void { $validValues = array_filter($array, 'is_string'); self::assertTrue(count($array) === count($validValues), $message); } + /** + * @psalm-suppress UnusedMethod + */ public static function assertStringIsParsableType(string $type, string $message = ''): void { if ($type === '') { diff --git a/tests/LanguageServer/DiagnosticTest.php b/tests/LanguageServer/DiagnosticTest.php index e69aa665393..f449b24527d 100644 --- a/tests/LanguageServer/DiagnosticTest.php +++ b/tests/LanguageServer/DiagnosticTest.php @@ -110,6 +110,9 @@ public function testSnippetSupportDisabled(): void wait($deferred->promise()); } + /** + * @psalm-suppress UnusedMethod + */ public function jestRun(): void { $config = $this->codebase->config; diff --git a/tests/LanguageServer/MockProtocolStream.php b/tests/LanguageServer/MockProtocolStream.php index 6d61b7b2e67..084e47f0375 100644 --- a/tests/LanguageServer/MockProtocolStream.php +++ b/tests/LanguageServer/MockProtocolStream.php @@ -23,6 +23,7 @@ class MockProtocolStream implements ProtocolReader, ProtocolWriter, EmitterInter * Sends a Message to the client * * @param Message $msg + * @psalm-suppress PossiblyUnusedReturnValue */ public function write(Message $msg): Promise { From 5d1cd7c3f8323454540cb3ea28db418e4fbbbbd2 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Thu, 28 Apr 2022 18:31:30 +0000 Subject: [PATCH 42/78] fix non 8.1 syntax --- .../LanguageServer/ClientConfiguration.php | 2 +- .../Internal/LanguageServer/Codebase.php | 20 +++++++++---------- .../LanguageServer/LanguageServer.php | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Psalm/Internal/LanguageServer/ClientConfiguration.php b/src/Psalm/Internal/LanguageServer/ClientConfiguration.php index c5fd948d145..b2f218c3a07 100644 --- a/src/Psalm/Internal/LanguageServer/ClientConfiguration.php +++ b/src/Psalm/Internal/LanguageServer/ClientConfiguration.php @@ -140,7 +140,7 @@ public function __construct( bool $findUnusedVariables = null, string $findUnusedCode = null, int $logLevel = null, - int $onchangeLineLimit = null, + int $onchangeLineLimit = null ) { $this->hideWarnings = $hideWarnings; $this->provideCompletion = $provideCompletion; diff --git a/src/Psalm/Internal/LanguageServer/Codebase.php b/src/Psalm/Internal/LanguageServer/Codebase.php index 94f021144c7..ef54daa3c00 100644 --- a/src/Psalm/Internal/LanguageServer/Codebase.php +++ b/src/Psalm/Internal/LanguageServer/Codebase.php @@ -67,7 +67,7 @@ class Codebase extends PsalmCodebase */ public function getReferenceAtPosition( string $file_path, - Position $position, + Position $position ): ?Reference { $is_open = $this->file_provider->isOpen($file_path); @@ -127,7 +127,7 @@ public function getReferenceAtPosition( * @param Reference $reference */ public function getMarkupContentForSymbol( - Reference $reference, + Reference $reference ): ?PHPMarkdownContent { //Direct Assignment if (is_numeric($reference->symbol[0])) { @@ -335,7 +335,7 @@ public function getMarkupContentForSymbol( private static function getPositionFromOffset( int $offset, - string $file_contents, + string $file_contents ): Position { $file_contents = substr($file_contents, 0, $offset); @@ -359,7 +359,7 @@ private static function getPositionFromOffset( */ public function getCompletionDataAtPosition( string $file_path, - Position $position, + Position $position ): ?array { $is_open = $this->file_provider->isOpen($file_path); @@ -455,7 +455,7 @@ public function getCompletionDataAtPosition( public function getTypeContextAtPosition( string $file_path, - Position $position, + Position $position ): ?Union { $file_contents = $this->getFileContents($file_path); $offset = $position->toOffset($file_contents); @@ -493,7 +493,7 @@ public function getTypeContextAtPosition( public function getCompletionItemsForClassishThing( string $type_string, string $gap, - bool $snippets_supported = false, + bool $snippets_supported = false ): array { $completion_items = []; @@ -651,7 +651,7 @@ public function getCompletionItemsForArrayKeys(string $type_string): array public function getCompletionItemsForPartialSymbol( string $type_string, int $offset, - string $file_path, + string $file_path ): array { $fq_suggestion = false; @@ -898,7 +898,7 @@ public function getCompletionItemsForType(Union $type): array */ public function getFunctionArgumentAtPosition( string $file_path, - Position $position, + Position $position ): ?array { $is_open = $this->file_provider->isOpen($file_path); @@ -954,7 +954,7 @@ public function getFunctionArgumentAtPosition( */ public function getSignatureInformation( string $function_symbol, - string $file_path = null, + string $file_path = null ): ?SignatureInformation { $signature_label = ''; $signature_documentation = null; @@ -1143,7 +1143,7 @@ public function getSymbolLocation(Reference $reference): ?CodeLocation public function addTemporaryFileChanges( string $file_path, string $new_content, - ?int $version = null, + ?int $version = null ): void { $this->file_provider->addTemporaryFileChanges( $file_path, diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 4dd0056476a..3d924f6f8f4 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -899,7 +899,7 @@ public function log(int $type, string $message, array $context = []): void $this->client->logMessage( new LogMessage( $type, - $message, + $message ) ); } catch (Throwable $err) { From 7910daad3762b2ca5b1b21724261e94b56c30e83 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Thu, 28 Apr 2022 18:35:10 +0000 Subject: [PATCH 43/78] remove failing trailing commas --- .../Internal/LanguageServer/Codebase.php | 104 +++++++++--------- 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/src/Psalm/Internal/LanguageServer/Codebase.php b/src/Psalm/Internal/LanguageServer/Codebase.php index ef54daa3c00..2b4a2298f90 100644 --- a/src/Psalm/Internal/LanguageServer/Codebase.php +++ b/src/Psalm/Internal/LanguageServer/Codebase.php @@ -115,7 +115,7 @@ public function getReferenceAtPosition( $range = new Range( self::getPositionFromOffset($reference_start_pos, $file_contents), - self::getPositionFromOffset($reference_end_pos, $file_contents), + self::getPositionFromOffset($reference_end_pos, $file_contents) ); return new Reference($file_path, $symbol, $range); @@ -132,7 +132,7 @@ public function getMarkupContentForSymbol( //Direct Assignment if (is_numeric($reference->symbol[0])) { return new PHPMarkdownContent( - preg_replace('/^[^:]*:/', '', $reference->symbol), + preg_replace('/^[^:]*:/', '', $reference->symbol) ); } @@ -158,7 +158,7 @@ public function getMarkupContentForSymbol( return new PHPMarkdownContent( $storage->getHoverMarkdown(), "{$storage->defining_fqcln}::{$storage->cased_name}", - $storage->description, + $storage->description ); } @@ -180,7 +180,7 @@ public function getMarkupContentForSymbol( return new PHPMarkdownContent( "{$storage->getInfo()} {$symbol_name}", $reference->symbol, - $storage->description, + $storage->description ); } } @@ -224,7 +224,7 @@ public function getMarkupContentForSymbol( return new PHPMarkdownContent( $class_constants[$const_name]->getHoverMarkdown($const_name), $fq_classlike_name . '::' . $const_name, - $class_constants[$const_name]->description, + $class_constants[$const_name]->description ); } @@ -232,7 +232,7 @@ public function getMarkupContentForSymbol( if (strpos($reference->symbol, '()')) { $function_id = strtolower(substr($reference->symbol, 0, -2)); $file_storage = $this->file_storage_provider->get( - $reference->file_path, + $reference->file_path ); if (isset($file_storage->functions[$function_id])) { @@ -241,7 +241,7 @@ public function getMarkupContentForSymbol( return new PHPMarkdownContent( $function_storage->getHoverMarkdown(), $function_id, - $function_storage->description, + $function_storage->description ); } @@ -254,7 +254,7 @@ public function getMarkupContentForSymbol( return new PHPMarkdownContent( $function->getHoverMarkdown(), $function_id, - $function->description, + $function->description ); } @@ -264,21 +264,21 @@ public function getMarkupContentForSymbol( if (!$type->isMixed()) { return new PHPMarkdownContent( (string) $type, - $reference->symbol, + $reference->symbol ); } } try { $storage = $this->classlike_storage_provider->get( - $reference->symbol, + $reference->symbol ); return new PHPMarkdownContent( ($storage->abstract ? 'abstract ' : '') . 'class ' . $storage->name, $storage->name, - $storage->description, + $storage->description ); } catch (InvalidArgumentException $e) { //continue on as normal @@ -298,12 +298,12 @@ public function getMarkupContentForSymbol( $type = $namespace_constants[$const_name]; return new PHPMarkdownContent( $reference->symbol . ' ' . $type, - $reference->symbol, + $reference->symbol ); } } else { $file_storage = $this->file_storage_provider->get( - $reference->file_path, + $reference->file_path ); // ? if (isset($file_storage->constants[$reference->symbol])) { @@ -312,20 +312,20 @@ public function getMarkupContentForSymbol( $reference->symbol . ' ' . $file_storage->constants[$reference->symbol], - $reference->symbol, + $reference->symbol ); } $type = ConstFetchAnalyzer::getGlobalConstType( $this, $reference->symbol, - $reference->symbol, + $reference->symbol ); //Global Constant if ($type) { return new PHPMarkdownContent( 'const ' . $reference->symbol . ' ' . $type, - $reference->symbol, + $reference->symbol ); } } @@ -350,7 +350,7 @@ private static function getPositionFromOffset( return new Position( substr_count($file_contents, "\n"), - $offset - (int) $before_newline_count - 1, + $offset - (int) $before_newline_count - 1 ); } @@ -475,7 +475,7 @@ public function getTypeContextAtPosition( // First parameter to a function-like $function_storage = $this->getFunctionStorageForSymbol( $file_path, - $function . '()', + $function . '()' ); if (!$function_storage || !$function_storage->params || !isset($function_storage->params[$argument_num])) { return null; @@ -503,13 +503,13 @@ public function getCompletionItemsForClassishThing( if ($atomic_type instanceof TNamedObject) { try { $class_storage = $this->classlike_storage_provider->get( - $atomic_type->value, + $atomic_type->value ); foreach ($class_storage->appearing_method_ids as $declaring_method_id) { try { $method_storage = $this->methods->getStorage( - $declaring_method_id, + $declaring_method_id ); if ($method_storage->is_static || $gap === '->') { @@ -528,7 +528,7 @@ public function getCompletionItemsForClassishThing( 'editor.action.triggerParameterHints', ), null, - 2, + 2 ); if ($snippets_supported && count($method_storage->params) > 0) { @@ -557,7 +557,7 @@ public function getCompletionItemsForClassishThing( '1', //sort text str_replace('$', '', $property_name), ($gap === '::' ? '$' : '') . - str_replace('$', '', $property_name), + str_replace('$', '', $property_name) ); } @@ -570,7 +570,7 @@ public function getCompletionItemsForClassishThing( '1', str_replace('$', '', $property_name), ($gap === '::' ? '$' : '') . - str_replace('$', '', $property_name), + str_replace('$', '', $property_name) ); } @@ -579,7 +579,7 @@ public function getCompletionItemsForClassishThing( foreach ($class_storage->declaring_property_ids as $property_name => $declaring_class) { try { $property_storage = $this->properties->getStorage( - $declaring_class . '::$' . $property_name, + $declaring_class . '::$' . $property_name ); if ($property_storage->is_static || $gap === '->') { @@ -590,7 +590,7 @@ public function getCompletionItemsForClassishThing( $property_storage->description, (string) $property_storage->visibility, $property_name, - ($gap === '::' ? '$' : '') . $property_name, + ($gap === '::' ? '$' : '') . $property_name ); } } catch (Exception $e) { @@ -607,7 +607,7 @@ public function getCompletionItemsForClassishThing( $const->description, null, $const_name, - $const_name, + $const_name ); } } catch (Exception $e) { @@ -637,7 +637,7 @@ public function getCompletionItemsForArrayKeys(string $type_string): array null, null, null, - "'$property_name'", + "'$property_name'" ); } } @@ -672,7 +672,7 @@ public function getCompletionItemsForPartialSymbol( foreach ($file_storage->classlikes_in_file as $fq_class_name => $_) { try { $class_storage = $this->classlike_storage_provider->get( - $fq_class_name, + $fq_class_name ); } catch (Exception $e) { continue; @@ -725,20 +725,20 @@ public function getCompletionItemsForPartialSymbol( if ($aliases->uses_end) { $position = self::getPositionFromOffset( $aliases->uses_end, - $file_contents, + $file_contents ); $extra_edits[] = new TextEdit( new Range($position, $position), - "\n" . 'use ' . $fq_class_name . ';', + "\n" . 'use ' . $fq_class_name . ';' ); } else { $position = self::getPositionFromOffset( $aliases->namespace_first_stmt_start, - $file_contents, + $file_contents ); $extra_edits[] = new TextEdit( new Range($position, $position), - 'use ' . $fq_class_name . ';' . "\n" . "\n", + 'use ' . $fq_class_name . ';' . "\n" . "\n" ); } @@ -747,7 +747,7 @@ public function getCompletionItemsForPartialSymbol( try { $class_storage = $this->classlike_storage_provider->get( - $fq_class_name, + $fq_class_name ); $description = $class_storage->description; } catch (Exception $e) { @@ -763,7 +763,7 @@ public function getCompletionItemsForPartialSymbol( $fq_class_name, $insertion_text, null, - $extra_edits, + $extra_edits ); } @@ -771,7 +771,7 @@ public function getCompletionItemsForPartialSymbol( $type_string, $offset, $file_path, - $this, + $this ); $namespace_map = []; @@ -824,7 +824,7 @@ public function getCompletionItemsForPartialSymbol( 'editor.action.triggerParameterHints', ), null, - 2, + 2 ); } @@ -851,7 +851,7 @@ public function getCompletionItemsForType(Union $type): array null, null, null, - $property_name, + $property_name ); } } elseif ($atomic_type instanceof TLiteralString) { @@ -862,7 +862,7 @@ public function getCompletionItemsForType(Union $type): array null, null, null, - "'$atomic_type->value'", + "'$atomic_type->value'" ); } elseif ($atomic_type instanceof TLiteralInt) { $completion_items[] = new CompletionItem( @@ -872,7 +872,7 @@ public function getCompletionItemsForType(Union $type): array null, null, null, - (string) $atomic_type->value, + (string) $atomic_type->value ); } elseif ($atomic_type instanceof TClassConstant) { $const = @@ -886,7 +886,7 @@ public function getCompletionItemsForType(Union $type): array null, null, null, - $const, + $const ); } } @@ -943,7 +943,7 @@ public function getFunctionArgumentAtPosition( $range = new Range( self::getPositionFromOffset($start_pos, $file_contents), - self::getPositionFromOffset($end_pos, $file_contents), + self::getPositionFromOffset($end_pos, $file_contents) ); return [$reference, $argument_number, $range]; @@ -961,7 +961,7 @@ public function getSignatureInformation( if (strpos($function_symbol, '::') !== false) { /** @psalm-suppress ArgumentTypeCoercion */ $method_id = new MethodIdentifier( - ...explode('::', $function_symbol), + ...explode('::', $function_symbol) ); $declaring_method_id = $this->methods->getDeclaringMethodId( @@ -983,12 +983,12 @@ public function getSignatureInformation( null, strtolower($function_symbol), dirname($file_path), - $file_path, + $file_path ); } else { $function_storage = $this->functions->getStorage( null, - strtolower($function_symbol), + strtolower($function_symbol) ); } $params = $function_storage->params; @@ -1057,7 +1057,7 @@ public function getSymbolLocation(Reference $reference): ?CodeLocation $reference->file_path, $this->config->shortenFileName($reference->file_path), (int) $symbol_parts[0], - (int) $symbol_parts[1], + (int) $symbol_parts[1] ); } @@ -1068,11 +1068,11 @@ public function getSymbolLocation(Reference $reference): ?CodeLocation /** @psalm-suppress ArgumentTypeCoercion */ $method_id = new MethodIdentifier( - ...explode('::', $symbol), + ...explode('::', $symbol) ); $declaring_method_id = $this->methods->getDeclaringMethodId( - $method_id, + $method_id ); if (!$declaring_method_id) { @@ -1086,7 +1086,7 @@ public function getSymbolLocation(Reference $reference): ?CodeLocation if (strpos($reference->symbol, '$') !== false) { $storage = $this->properties->getStorage( - $reference->symbol, + $reference->symbol ); return $storage->location; @@ -1099,7 +1099,7 @@ public function getSymbolLocation(Reference $reference): ?CodeLocation $class_constants = $this->classlikes->getConstantsForClass( $fq_classlike_name, - ReflectionProperty::IS_PRIVATE, + ReflectionProperty::IS_PRIVATE ); if (!isset($class_constants[$const_name])) { @@ -1111,7 +1111,7 @@ public function getSymbolLocation(Reference $reference): ?CodeLocation if (strpos($reference->symbol, '()')) { $file_storage = $this->file_storage_provider->get( - $reference->file_path, + $reference->file_path ); $function_id = strtolower(substr($reference->symbol, 0, -2)); @@ -1129,7 +1129,7 @@ public function getSymbolLocation(Reference $reference): ?CodeLocation } return $this->classlike_storage_provider->get( - $reference->symbol, + $reference->symbol )->location; } catch (UnexpectedValueException $e) { error_log($e->getMessage()); @@ -1148,7 +1148,7 @@ public function addTemporaryFileChanges( $this->file_provider->addTemporaryFileChanges( $file_path, $new_content, - $version, + $version ); } From 96e7b428a02195cf2c0002be93934da2dac30e40 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Thu, 28 Apr 2022 18:39:14 +0000 Subject: [PATCH 44/78] another comma! --- src/Psalm/Internal/LanguageServer/Codebase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/LanguageServer/Codebase.php b/src/Psalm/Internal/LanguageServer/Codebase.php index 2b4a2298f90..5d36b2059eb 100644 --- a/src/Psalm/Internal/LanguageServer/Codebase.php +++ b/src/Psalm/Internal/LanguageServer/Codebase.php @@ -146,7 +146,7 @@ public function getMarkupContentForSymbol( $method_id = new MethodIdentifier(...explode('::', $symbol)); $declaring_method_id = $this->methods->getDeclaringMethodId( - $method_id, + $method_id ); if (!$declaring_method_id) { From e5c5b4ae73307fbf092b211511552a2277687aa0 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Thu, 28 Apr 2022 18:42:20 +0000 Subject: [PATCH 45/78] these commas man! --- src/Psalm/Internal/LanguageServer/Codebase.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Psalm/Internal/LanguageServer/Codebase.php b/src/Psalm/Internal/LanguageServer/Codebase.php index 5d36b2059eb..a554798df64 100644 --- a/src/Psalm/Internal/LanguageServer/Codebase.php +++ b/src/Psalm/Internal/LanguageServer/Codebase.php @@ -208,12 +208,12 @@ public function getMarkupContentForSymbol( [$fq_classlike_name, $const_name] = explode( '::', - $reference->symbol, + $reference->symbol ); $class_constants = $this->classlikes->getConstantsForClass( $fq_classlike_name, - ReflectionProperty::IS_PRIVATE, + ReflectionProperty::IS_PRIVATE ); if (!isset($class_constants[$const_name])) { @@ -291,7 +291,7 @@ public function getMarkupContentForSymbol( $namespace_constants = NamespaceAnalyzer::getConstantsForNamespace( $namespace_name, - ReflectionProperty::IS_PUBLIC, + ReflectionProperty::IS_PUBLIC ); //Namespace Constant if (isset($namespace_constants[$const_name])) { @@ -392,7 +392,7 @@ public function getCompletionDataAtPosition( $file_contents, $matches, 0, - $end_pos_excluding_whitespace, + $end_pos_excluding_whitespace ) ? strlen($matches[0]) : 0; From ed46c772d82f4184295e52f32bec847a67eff56e Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Thu, 28 Apr 2022 18:46:30 +0000 Subject: [PATCH 46/78] commas --- src/Psalm/Internal/LanguageServer/Codebase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/LanguageServer/Codebase.php b/src/Psalm/Internal/LanguageServer/Codebase.php index a554798df64..bdb7839c6cc 100644 --- a/src/Psalm/Internal/LanguageServer/Codebase.php +++ b/src/Psalm/Internal/LanguageServer/Codebase.php @@ -372,7 +372,7 @@ public function getCompletionDataAtPosition( $offset = $position->toOffset($file_contents); [$reference_map, $type_map] = $this->analyzer->getMapsForFile( - $file_path, + $file_path ); if (!$reference_map && !$type_map) { From 14309f07fc381a36abed4ebd878b3e7b9673041f Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Thu, 28 Apr 2022 18:50:00 +0000 Subject: [PATCH 47/78] more commas --- src/Psalm/Internal/LanguageServer/Codebase.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Psalm/Internal/LanguageServer/Codebase.php b/src/Psalm/Internal/LanguageServer/Codebase.php index bdb7839c6cc..a9a0053f7b6 100644 --- a/src/Psalm/Internal/LanguageServer/Codebase.php +++ b/src/Psalm/Internal/LanguageServer/Codebase.php @@ -525,7 +525,7 @@ public function getCompletionItemsForClassishThing( null, new Command( 'Trigger parameter hints', - 'editor.action.triggerParameterHints', + 'editor.action.triggerParameterHints' ), null, 2 @@ -709,7 +709,7 @@ public function getCompletionItemsForPartialSymbol( $fq_class_name, $aliases && $aliases->namespace ? $aliases->namespace : null, $aliases->uses_flipped ?? [], - null, + null ); if ($aliases && @@ -1021,7 +1021,7 @@ public function getSignatureInformation( strlen($signature_label), strlen($signature_label) + strlen($parameter_label), ], - $param->description ?? null, + $param->description ?? null ); $signature_label .= $parameter_label; @@ -1036,7 +1036,7 @@ public function getSignatureInformation( return new SignatureInformation( $signature_label, $parameters, - $signature_documentation, + $signature_documentation ); } @@ -1094,7 +1094,7 @@ public function getSymbolLocation(Reference $reference): ?CodeLocation [$fq_classlike_name, $const_name] = explode( '::', - $reference->symbol, + $reference->symbol ); $class_constants = $this->classlikes->getConstantsForClass( From e10ad2b5672a92d79d673852e7191ea367695be5 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Thu, 28 Apr 2022 18:52:07 +0000 Subject: [PATCH 48/78] more --- src/Psalm/Internal/LanguageServer/Codebase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/LanguageServer/Codebase.php b/src/Psalm/Internal/LanguageServer/Codebase.php index a9a0053f7b6..b938161dae6 100644 --- a/src/Psalm/Internal/LanguageServer/Codebase.php +++ b/src/Psalm/Internal/LanguageServer/Codebase.php @@ -660,7 +660,7 @@ public function getCompletionItemsForPartialSymbol( } $matching_classlike_names = $this->classlikes->getMatchingClassLikeNames( - $type_string, + $type_string ); $completion_items = []; From 30c885e1a664757cba5c2d4dffcbce36f08f4de4 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Thu, 28 Apr 2022 18:55:24 +0000 Subject: [PATCH 49/78] commas --- src/Psalm/Internal/LanguageServer/Codebase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/LanguageServer/Codebase.php b/src/Psalm/Internal/LanguageServer/Codebase.php index b938161dae6..8bdf7fddb2f 100644 --- a/src/Psalm/Internal/LanguageServer/Codebase.php +++ b/src/Psalm/Internal/LanguageServer/Codebase.php @@ -821,7 +821,7 @@ public function getCompletionItemsForPartialSymbol( null, new Command( 'Trigger parameter hints', - 'editor.action.triggerParameterHints', + 'editor.action.triggerParameterHints' ), null, 2 From 8613ebb9a4a857bb321939a9741a9ca3e1272ee7 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Thu, 28 Apr 2022 18:57:37 +0000 Subject: [PATCH 50/78] so close! --- src/Psalm/Internal/LanguageServer/Codebase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/LanguageServer/Codebase.php b/src/Psalm/Internal/LanguageServer/Codebase.php index 8bdf7fddb2f..c165ffbae3e 100644 --- a/src/Psalm/Internal/LanguageServer/Codebase.php +++ b/src/Psalm/Internal/LanguageServer/Codebase.php @@ -965,7 +965,7 @@ public function getSignatureInformation( ); $declaring_method_id = $this->methods->getDeclaringMethodId( - $method_id, + $method_id ); if ($declaring_method_id === null) { From 599ba0e755424c6d07d89c36751e11bb9632d96b Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Thu, 28 Apr 2022 19:00:23 +0000 Subject: [PATCH 51/78] bump --- src/Psalm/Internal/LanguageServer/Codebase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/LanguageServer/Codebase.php b/src/Psalm/Internal/LanguageServer/Codebase.php index c165ffbae3e..0caf85ace15 100644 --- a/src/Psalm/Internal/LanguageServer/Codebase.php +++ b/src/Psalm/Internal/LanguageServer/Codebase.php @@ -997,7 +997,7 @@ public function getSignatureInformation( } catch (Exception $exception) { if (InternalCallMapHandler::inCallMap($function_symbol)) { $callables = InternalCallMapHandler::getCallablesFromCallMap( - $function_symbol, + $function_symbol ); if (!$callables || !$callables[0]->params) { From 5f50a9e212623485b103979d5ca7d759c4d8c074 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Thu, 21 Jul 2022 21:05:39 +0000 Subject: [PATCH 52/78] fix invalid variable --- src/Psalm/Internal/Provider/FileProvider.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Provider/FileProvider.php b/src/Psalm/Internal/Provider/FileProvider.php index bf71dc92d0c..72f478485d2 100644 --- a/src/Psalm/Internal/Provider/FileProvider.php +++ b/src/Psalm/Internal/Provider/FileProvider.php @@ -37,8 +37,8 @@ class FileProvider public function getContents(string $file_path, bool $go_to_source = false): string { - if (!$go_to_source && isset($this->temp_files[$file_path_lc])) { - return $this->temp_files[$file_path_lc]['content']; + if (!$go_to_source && isset($this->temp_files[$file_path])) { + return $this->temp_files[$file_path]['content']; } if (isset(self::$open_files[$file_path])) { From 87184d7c9c6e2e5c66ec530c9b37521c46f0c84f Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Thu, 21 Jul 2022 21:44:14 +0000 Subject: [PATCH 53/78] fixes for CI --- src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php | 2 -- src/Psalm/Internal/Provider/FileProvider.php | 10 +++++----- src/Psalm/Type/Union.php | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index cd885a0c87b..385a954c946 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -65,8 +65,6 @@ use function array_merge; use function array_search; use function array_values; -use function assert; -use function class_exists; use function count; use function end; use function in_array; diff --git a/src/Psalm/Internal/Provider/FileProvider.php b/src/Psalm/Internal/Provider/FileProvider.php index 72f478485d2..93dbc9360d9 100644 --- a/src/Psalm/Internal/Provider/FileProvider.php +++ b/src/Psalm/Internal/Provider/FileProvider.php @@ -31,7 +31,7 @@ class FileProvider protected static $open_files = []; /** - * @var array + * @var array */ protected $open_files_paths = []; @@ -78,8 +78,8 @@ public function setContents(string $file_path, string $file_contents): void public function setOpenContents(string $file_path, ?string $file_contents = null): void { - if (isset($this->open_files[$file_path])) { - $this->open_files[$file_path] = $file_contents ?? $this->getContents($file_path, true); + if (isset(self::$open_files[$file_path])) { + self::$open_files[$file_path] = $file_contents ?? $this->getContents($file_path, true); } } @@ -119,7 +119,7 @@ public function getOpenFilesPath(): array public function openFile(string $file_path): void { - $this->open_files[$file_path] = $this->getContents($file_path, true); + self::$open_files[$file_path] = $this->getContents($file_path, true); $this->open_files_paths[$file_path] = $file_path; } @@ -132,7 +132,7 @@ public function closeFile(string $file_path): void { unset( $this->temp_files[$file_path], - $this->open_files[$file_path], + self::$open_files[$file_path], $this->open_files_paths[$file_path] ); } diff --git a/src/Psalm/Type/Union.php b/src/Psalm/Type/Union.php index 6ff34be3156..0b01feb4ecf 100644 --- a/src/Psalm/Type/Union.php +++ b/src/Psalm/Type/Union.php @@ -1328,7 +1328,7 @@ public function allSpecificLiterals(): bool /** * @psalm-assert-if-true array< * array-key, - * TLiteralString|TLiteralInt|TLiteralFloat|TNonspecificLiteralString|TNonSpecificLiteralInt|TFalse|TTrue + * TLiteralString|TLiteralInt|TLiteralFloat|TNonspecificLiteralString|TNonspecificLiteralInt|TFalse|TTrue * > $this->getAtomicTypes() */ public function allLiterals(): bool From 20d7a95f831397f88fd094e3f3a9ef81149062e6 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Thu, 21 Jul 2022 21:48:47 +0000 Subject: [PATCH 54/78] remove amphp/phpunit-util on test run in ci --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd02316abe4..5b3dc6e738b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,7 @@ jobs: ${{ runner.os }}-composer- - name: Drop incompatible packages - run: composer remove --no-interaction --dev phpunit/phpunit brianium/paratest psalm/plugin-phpunit weirdan/prophecy-shim + run: composer remove --no-interaction --dev amphp/phpunit-util phpunit/phpunit brianium/paratest psalm/plugin-phpunit weirdan/prophecy-shim - name: Run composer install run: composer install -o From b0705b4e292ff577f282dc44c94a8f5ef12868ec Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Wed, 30 Nov 2022 18:14:06 +0000 Subject: [PATCH 55/78] fix tests --- src/Psalm/Internal/Cli/LanguageServer.php | 4 ++-- tests/LanguageServer/SymbolLookupTest.php | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Internal/Cli/LanguageServer.php b/src/Psalm/Internal/Cli/LanguageServer.php index 78e4ab8f5d3..a74909e3091 100644 --- a/src/Psalm/Internal/Cli/LanguageServer.php +++ b/src/Psalm/Internal/Cli/LanguageServer.php @@ -310,10 +310,10 @@ function () use ($current_dir, $options, $vendor_dir): ?\Composer\Autoload\Class } if (isset($options['disable-on-change']) && is_numeric($options['disable-on-change'])) { - $project_analyzer->onchange_line_limit = (int) $options['disable-on-change']; + $clientConfiguration->onchangeLineLimit = (int) $options['disable-on-change']; } - if (isset($options['on-change-debounce-ms'])) { + if (isset($options['on-change-debounce-ms']) && is_numeric($options['on-change-debounce-ms'])) { $clientConfiguration->onChangeDebounceMs = (int) $options['on-change-debounce-ms']; } diff --git a/tests/LanguageServer/SymbolLookupTest.php b/tests/LanguageServer/SymbolLookupTest.php index f23ae15e9cb..71affeac647 100644 --- a/tests/LanguageServer/SymbolLookupTest.php +++ b/tests/LanguageServer/SymbolLookupTest.php @@ -171,9 +171,11 @@ function qux(int $a, int $b) : int { $this->assertSame('b\qux', $information->title); $this->assertNull($information->description); - $information = $codebase->getSymbolInformation('somefile.php', '$_SESSION'); + /* TODO: what was the intention here? + $information = $this->codebase->getSymbolInformation('somefile.php', '$_SESSION'); $this->assertNotNull($information); $this->assertSame("", $information['type']); + */ $information = $this->codebase->getMarkupContentForSymbol( new Reference( From e16a4f3f85f9c16f895c0d24c0608e8238a3a146 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Wed, 30 Nov 2022 18:28:13 +0000 Subject: [PATCH 56/78] more testing fixes --- tests/LanguageServer/SymbolLookupTest.php | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/tests/LanguageServer/SymbolLookupTest.php b/tests/LanguageServer/SymbolLookupTest.php index 71affeac647..74be5f35427 100644 --- a/tests/LanguageServer/SymbolLookupTest.php +++ b/tests/LanguageServer/SymbolLookupTest.php @@ -171,22 +171,16 @@ function qux(int $a, int $b) : int { $this->assertSame('b\qux', $information->title); $this->assertNull($information->description); - /* TODO: what was the intention here? - $information = $this->codebase->getSymbolInformation('somefile.php', '$_SESSION'); - $this->assertNotNull($information); - $this->assertSame("", $information['type']); - */ - $information = $this->codebase->getMarkupContentForSymbol( new Reference( 'somefile.php', - '$_SERVER', + '$_SESSION', $range ) ); $this->assertNotNull($information); - $this->assertSame("array", $information->code); - $this->assertSame('$_SERVER', $information->title); + $this->assertSame("array", $information->code); + $this->assertSame('$_SESSION', $information->title); $this->assertNull($information->description); $information = $this->codebase->getMarkupContentForSymbol( From a13ad3cf4ae6522fe5f12442cf8a89452cdcd5a3 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Tue, 20 Dec 2022 20:58:15 +0000 Subject: [PATCH 57/78] phpcs --- src/Psalm/Internal/Provider/FileProvider.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Provider/FileProvider.php b/src/Psalm/Internal/Provider/FileProvider.php index a8b22d518a7..22438cdfff2 100644 --- a/src/Psalm/Internal/Provider/FileProvider.php +++ b/src/Psalm/Internal/Provider/FileProvider.php @@ -77,7 +77,7 @@ public function setContents(string $file_path, string $file_contents): void if (isset($this->temp_files[$file_path])) { $this->temp_files[$file_path] = [ 'version'=> null, - 'content' => $file_contents + 'content' => $file_contents, ]; } @@ -141,7 +141,7 @@ public function closeFile(string $file_path): void unset( $this->temp_files[$file_path], self::$open_files[$file_path], - $this->open_files_paths[$file_path] + $this->open_files_paths[$file_path], ); } From b9791d24dc402b0d26e2cf70506986d3cf1551d4 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Tue, 20 Dec 2022 21:09:34 +0000 Subject: [PATCH 58/78] fix some tests --- tests/LanguageServer/CompletionTest.php | 7 ++- tests/LanguageServer/DiagnosticTest.php | 49 ++++++++---------- tests/LanguageServer/FileMapTest.php | 7 ++- tests/LanguageServer/MockProtocolStream.php | 1 - tests/LanguageServer/SymbolLookupTest.php | 57 ++++++++++----------- 5 files changed, 54 insertions(+), 67 deletions(-) diff --git a/tests/LanguageServer/CompletionTest.php b/tests/LanguageServer/CompletionTest.php index 055876da25a..290562509bb 100644 --- a/tests/LanguageServer/CompletionTest.php +++ b/tests/LanguageServer/CompletionTest.php @@ -3,9 +3,9 @@ namespace Psalm\Tests\LanguageServer; use LanguageServerProtocol\Position; +use Psalm\Codebase; use Psalm\Context; use Psalm\Internal\Analyzer\ProjectAnalyzer; -use Psalm\Internal\LanguageServer\Codebase; use Psalm\Internal\Provider\FakeFileProvider; use Psalm\Internal\Provider\Providers; use Psalm\Tests\Internal\Provider\FakeFileReferenceCacheProvider; @@ -19,8 +19,7 @@ class CompletionTest extends TestCase { - /** @var Codebase */ - protected $codebase; + protected Codebase $codebase; public function setUp(): void { @@ -48,7 +47,7 @@ public function setUp(): void [], 1, null, - $this->codebase + $this->codebase, ); $this->project_analyzer->setPhpVersion('7.3', 'tests'); diff --git a/tests/LanguageServer/DiagnosticTest.php b/tests/LanguageServer/DiagnosticTest.php index f449b24527d..e9319105738 100644 --- a/tests/LanguageServer/DiagnosticTest.php +++ b/tests/LanguageServer/DiagnosticTest.php @@ -3,10 +3,10 @@ namespace Psalm\Tests\LanguageServer; use Amp\Deferred; +use Psalm\Codebase; use Psalm\Internal\Analyzer\IssueData; use Psalm\Internal\Analyzer\ProjectAnalyzer; use Psalm\Internal\LanguageServer\ClientConfiguration; -use Psalm\Internal\LanguageServer\Codebase; use Psalm\Internal\LanguageServer\LanguageServer; use Psalm\Internal\LanguageServer\Message; use Psalm\Internal\LanguageServer\Progress; @@ -26,20 +26,11 @@ class DiagnosticTest extends AsyncTestCase { - /** @var Codebase */ - protected $codebase; - - /** @var int */ - private $increment = 0; - - /** @var ProjectAnalyzer */ - protected $project_analyzer; - - /** @var FakeFileProvider */ - protected $file_provider; - - /** @var string */ - protected static $src_dir_path; + protected Codebase $codebase; + private int $increment = 0; + protected ProjectAnalyzer $project_analyzer; + protected FakeFileProvider $file_provider; + protected static string $src_dir_path; public function setUp(): void { @@ -67,7 +58,7 @@ public function setUp(): void [], 1, null, - $this->codebase + $this->codebase, ); $this->project_analyzer->setPhpVersion('7.4', 'tests'); @@ -96,7 +87,7 @@ public function testSnippetSupportDisabled(): void $this->project_analyzer, $this->codebase, $clientConfiguration, - new Progress + new Progress, ); $write->on('message', function (Message $message) use ($deferred, $server): void { @@ -120,7 +111,7 @@ public function jestRun(): void $this->addFile( 'somefile.php', - '' + '', ); $issues = $this->changeFile( @@ -131,7 +122,7 @@ class SomeController public function __construct() { } - }' + }', ); $this->assertEmpty($issues); @@ -145,7 +136,7 @@ public function __construct() { strpos("", ""); } - }' + }', ); $this->assertArrayHasKey('somefile.php', $issues); @@ -164,7 +155,7 @@ public function __construct() } public function foobar(): void {} - }' + }', ); $this->assertArrayHasKey('somefile.php', $issues); @@ -187,7 +178,7 @@ public function __construct() } public function foobar(): void {} - }' + }', ); $this->assertArrayHasKey('somefile.php', $issues); @@ -208,7 +199,7 @@ public function __construct() } public function foobar(): void {} - }' + }', ); $this->assertArrayHasKey('somefile.php', $issues); @@ -226,7 +217,7 @@ public function __construct() } public function foobar(): void {} - }' + }', ); $this->assertArrayHasKey('somefile.php', $issues); @@ -247,7 +238,7 @@ public function __construct() } public function foobar(): void {} - }' + }', ); $this->assertEmpty($issues); @@ -261,15 +252,15 @@ private function changeFile(string $file_path, string $contents): array $this->codebase->addTemporaryFileChanges( $file_path, $contents, - $this->increment + $this->increment, ); $this->codebase->reloadFiles( $this->project_analyzer, - [$file_path] + [$file_path], ); $this->codebase->analyzer->addFilesToAnalyze( - [$file_path => $file_path] + [$file_path => $file_path], ); $this->codebase->analyzer->analyzeFiles($this->project_analyzer, 1, false); @@ -669,7 +660,7 @@ private function generateInitializeRequest(): array ], ], 'trace' => 'off', - ] + ], ]; } } diff --git a/tests/LanguageServer/FileMapTest.php b/tests/LanguageServer/FileMapTest.php index 9568c3f4b57..e17b19be5d1 100644 --- a/tests/LanguageServer/FileMapTest.php +++ b/tests/LanguageServer/FileMapTest.php @@ -2,9 +2,9 @@ namespace Psalm\Tests\LanguageServer; +use Psalm\Codebase; use Psalm\Context; use Psalm\Internal\Analyzer\ProjectAnalyzer; -use Psalm\Internal\LanguageServer\Codebase; use Psalm\Internal\Provider\FakeFileProvider; use Psalm\Internal\Provider\Providers; use Psalm\Tests\Internal\Provider\FakeFileReferenceCacheProvider; @@ -17,8 +17,7 @@ class FileMapTest extends TestCase { - /** @var Codebase */ - protected $codebase; + protected Codebase $codebase; public function setUp(): void { @@ -46,7 +45,7 @@ public function setUp(): void [], 1, null, - $this->codebase + $this->codebase, ); $this->project_analyzer->setPhpVersion('7.3', 'tests'); diff --git a/tests/LanguageServer/MockProtocolStream.php b/tests/LanguageServer/MockProtocolStream.php index 084e47f0375..34f8072c9cc 100644 --- a/tests/LanguageServer/MockProtocolStream.php +++ b/tests/LanguageServer/MockProtocolStream.php @@ -22,7 +22,6 @@ class MockProtocolStream implements ProtocolReader, ProtocolWriter, EmitterInter /** * Sends a Message to the client * - * @param Message $msg * @psalm-suppress PossiblyUnusedReturnValue */ public function write(Message $msg): Promise diff --git a/tests/LanguageServer/SymbolLookupTest.php b/tests/LanguageServer/SymbolLookupTest.php index e854ce6ca00..aace1a15f26 100644 --- a/tests/LanguageServer/SymbolLookupTest.php +++ b/tests/LanguageServer/SymbolLookupTest.php @@ -4,10 +4,10 @@ use LanguageServerProtocol\Position; use LanguageServerProtocol\Range; +use Psalm\Codebase; use Psalm\Context; use Psalm\Internal\Analyzer\FileAnalyzer; use Psalm\Internal\Analyzer\ProjectAnalyzer; -use Psalm\Internal\LanguageServer\Codebase; use Psalm\Internal\LanguageServer\Reference; use Psalm\Internal\Provider\FakeFileProvider; use Psalm\Internal\Provider\Providers; @@ -19,8 +19,7 @@ class SymbolLookupTest extends TestCase { - /** @var Codebase */ - protected $codebase; + protected Codebase $codebase; public function setUp(): void { @@ -48,7 +47,7 @@ public function setUp(): void [], 1, null, - $this->codebase + $this->codebase, ); $this->project_analyzer->setPhpVersion('7.3', 'tests'); @@ -103,8 +102,8 @@ function qux(int $a, int $b) : int { new Reference( 'somefile.php', 'B\A::foo()', - $range - ) + $range, + ), ); $this->assertNotNull($information); $this->assertSame("public function foo(): void", $information->code); @@ -115,8 +114,8 @@ function qux(int $a, int $b) : int { new Reference( 'somefile.php', 'B\A::$a', - $range - ) + $range, + ), ); $this->assertNotNull($information); $this->assertSame('protected int|null $a', $information->code); @@ -127,8 +126,8 @@ function qux(int $a, int $b) : int { new Reference( 'somefile.php', 'B\bar()', - $range - ) + $range, + ), ); $this->assertNotNull($information); $this->assertSame('function B\bar(): int', $information->code); @@ -139,8 +138,8 @@ function qux(int $a, int $b) : int { new Reference( 'somefile.php', 'B\A::BANANA', - $range - ) + $range, + ), ); $this->assertNotNull($information); $this->assertSame('public const BANANA = 🍌;', $information->code); @@ -151,8 +150,8 @@ function qux(int $a, int $b) : int { new Reference( 'somefile.php', 'B\baz()', - $range - ) + $range, + ), ); $this->assertNotNull($information); $this->assertSame("function B\baz(\n int \$a\n): int", $information->code); @@ -163,8 +162,8 @@ function qux(int $a, int $b) : int { new Reference( 'somefile.php', 'B\qux()', - $range - ) + $range, + ), ); $this->assertNotNull($information); $this->assertSame("function B\qux(\n int \$a,\n int \$b\n): int", $information->code); @@ -175,8 +174,8 @@ function qux(int $a, int $b) : int { new Reference( 'somefile.php', '$_SESSION', - $range - ) + $range, + ), ); $this->assertNotNull($information); $this->assertSame("array", $information->code); @@ -187,8 +186,8 @@ function qux(int $a, int $b) : int { new Reference( 'somefile.php', '$my_global', - $range - ) + $range, + ), ); $this->assertNotNull($information); $this->assertSame("string", $information->code); @@ -215,8 +214,8 @@ public function testSimpleSymbolLookupGlobalConst(): void new Reference( 'somefile.php', 'APPLE', - $range - ) + $range, + ), ); $this->assertNotNull($information); $this->assertSame("const APPLE string", $information->code); @@ -227,8 +226,8 @@ public function testSimpleSymbolLookupGlobalConst(): void new Reference( 'somefile.php', 'BANANA', - $range - ) + $range, + ), ); $this->assertNotNull($information); $this->assertSame("const BANANA string", $information->code); @@ -270,7 +269,7 @@ function bar() : int { $method_symbol_location = $this->codebase->getSymbolLocation(new Reference( 'somefile.php', 'B\A::foo()', - $range + $range, )); $this->assertNotNull($method_symbol_location); @@ -280,7 +279,7 @@ function bar() : int { $property_symbol_location = $this->codebase->getSymbolLocation(new Reference( 'somefile.php', 'B\A::$a', - $range + $range, )); $this->assertNotNull($property_symbol_location); @@ -290,7 +289,7 @@ function bar() : int { $constant_symbol_location = $this->codebase->getSymbolLocation(new Reference( 'somefile.php', 'B\A::BANANA', - $range + $range, )); $this->assertNotNull($constant_symbol_location); @@ -300,7 +299,7 @@ function bar() : int { $function_symbol_location = $this->codebase->getSymbolLocation(new Reference( 'somefile.php', 'B\bar()', - $range + $range, )); $this->assertNotNull($function_symbol_location); @@ -310,7 +309,7 @@ function bar() : int { $function_symbol_location = $this->codebase->getSymbolLocation(new Reference( 'somefile.php', '257-259', - $range + $range, )); $this->assertNotNull($function_symbol_location); From f1b0c4fa6d10fac9491d6711aa5b360d6859ac14 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Tue, 20 Dec 2022 21:16:29 +0000 Subject: [PATCH 59/78] additional fixes --- tests/FileUpdates/TemporaryUpdateTest.php | 4 ++-- tests/LanguageServer/DiagnosticTest.php | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/FileUpdates/TemporaryUpdateTest.php b/tests/FileUpdates/TemporaryUpdateTest.php index 7d7613387c9..5acae2112f9 100644 --- a/tests/FileUpdates/TemporaryUpdateTest.php +++ b/tests/FileUpdates/TemporaryUpdateTest.php @@ -2,9 +2,9 @@ namespace Psalm\Tests\FileUpdates; +use Psalm\Codebase; use Psalm\Config; use Psalm\Internal\Analyzer\ProjectAnalyzer; -use Psalm\Internal\LanguageServer\Codebase; use Psalm\Internal\Provider\FakeFileProvider; use Psalm\Internal\Provider\Providers; use Psalm\IssueBuffer; @@ -54,7 +54,7 @@ public function setUp(): void [], 1, null, - $this->codebase + $this->codebase, ); $this->project_analyzer->setPhpVersion('7.3', 'tests'); diff --git a/tests/LanguageServer/DiagnosticTest.php b/tests/LanguageServer/DiagnosticTest.php index e9319105738..9ad14e9e758 100644 --- a/tests/LanguageServer/DiagnosticTest.php +++ b/tests/LanguageServer/DiagnosticTest.php @@ -30,7 +30,6 @@ class DiagnosticTest extends AsyncTestCase private int $increment = 0; protected ProjectAnalyzer $project_analyzer; protected FakeFileProvider $file_provider; - protected static string $src_dir_path; public function setUp(): void { @@ -46,7 +45,7 @@ public function setUp(): void null, null, new FakeFileReferenceCacheProvider(), - new ProjectCacheProvider() + new ProjectCacheProvider(), ); $this->codebase = new Codebase($config, $providers); From 30f8331da63eb3b26f03895d9d0967f282a653c5 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Tue, 20 Dec 2022 21:25:04 +0000 Subject: [PATCH 60/78] additional fixes --- tests/AsyncTestCase.php | 19 ++++++------------- tests/LanguageServer/DiagnosticTest.php | 2 -- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/tests/AsyncTestCase.php b/tests/AsyncTestCase.php index 207facfcc12..5a7e70a8e23 100644 --- a/tests/AsyncTestCase.php +++ b/tests/AsyncTestCase.php @@ -30,28 +30,21 @@ class AsyncTestCase extends BaseAsyncTestCase { - /** @var string */ - protected static $src_dir_path; - - /** @var ProjectAnalyzer */ - protected $project_analyzer; - - /** @var FakeFileProvider */ - protected $file_provider; - - /** @var Config */ - protected $testConfig; + protected static string $src_dir_path; + protected ProjectAnalyzer $project_analyzer; + protected FakeFileProvider $file_provider; + protected Config $testConfig; public static function setUpBeforeClass(): void { ini_set('memory_limit', '-1'); if (!defined('PSALM_VERSION')) { - define('PSALM_VERSION', '4.0.0'); + define('PSALM_VERSION', '5.0.0'); } if (!defined('PHP_PARSER_VERSION')) { - define('PHP_PARSER_VERSION', '4.0.0'); + define('PHP_PARSER_VERSION', '5.0.0'); } parent::setUpBeforeClass(); diff --git a/tests/LanguageServer/DiagnosticTest.php b/tests/LanguageServer/DiagnosticTest.php index 9ad14e9e758..690c008ea95 100644 --- a/tests/LanguageServer/DiagnosticTest.php +++ b/tests/LanguageServer/DiagnosticTest.php @@ -28,8 +28,6 @@ class DiagnosticTest extends AsyncTestCase { protected Codebase $codebase; private int $increment = 0; - protected ProjectAnalyzer $project_analyzer; - protected FakeFileProvider $file_provider; public function setUp(): void { From 5f9487e9a9c803948af2d6450ad061e68a63d28c Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Tue, 20 Dec 2022 21:26:25 +0000 Subject: [PATCH 61/78] async test case fixes --- tests/AsyncTestCase.php | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/tests/AsyncTestCase.php b/tests/AsyncTestCase.php index 5a7e70a8e23..e9834d7af89 100644 --- a/tests/AsyncTestCase.php +++ b/tests/AsyncTestCase.php @@ -68,12 +68,12 @@ public function setUp(): void $providers = new Providers( $this->file_provider, - new FakeParserCacheProvider() + new FakeParserCacheProvider(), ); $this->project_analyzer = new ProjectAnalyzer( $this->testConfig, - $providers + $providers, ); $this->project_analyzer->setPhpVersion('7.4', 'tests'); @@ -85,22 +85,16 @@ public function tearDown(): void RuntimeCaches::clearAll(); } - /** - * @param string $file_path - * @param string $contents - * - */ - public function addFile($file_path, $contents): void + public function addFile(string $file_path, string $contents): void { $this->file_provider->registerFile($file_path, $contents); $this->project_analyzer->getCodebase()->scanner->addFileToShallowScan($file_path); } /** - * @param string $file_path * @psalm-suppress UnusedMethod */ - public function analyzeFile($file_path, Context $context, bool $track_unused_suppressions = true, bool $taint_flow_tracking = false): void + public function analyzeFile(string $file_path, Context $context, bool $track_unused_suppressions = true, bool $taint_flow_tracking = false): void { $codebase = $this->project_analyzer->getCodebase(); @@ -123,7 +117,7 @@ public function analyzeFile($file_path, Context $context, bool $track_unused_sup $file_analyzer = new FileAnalyzer( $this->project_analyzer, $file_path, - $codebase->config->shortenFileName($file_path) + $codebase->config->shortenFileName($file_path), ); $file_analyzer->analyze($context); @@ -137,10 +131,9 @@ public function analyzeFile($file_path, Context $context, bool $track_unused_sup } /** - * @param bool $withDataSet * @psalm-suppress UnusedMethod */ - protected function getTestName($withDataSet = true): string + protected function getTestName(bool $withDataSet = true): string { return $this->getName($withDataSet); } @@ -159,9 +152,7 @@ public static function assertArrayKeysAreStrings(array $array, string $message = */ public static function assertArrayKeysAreZeroOrString(array $array, string $message = ''): void { - $isZeroOrString = /** @param mixed $key */ function ($key): bool { - return $key === 0 || is_string($key); - }; + $isZeroOrString = /** @param mixed $key */ fn($key): bool => $key === 0 || is_string($key); $validKeys = array_filter($array, $isZeroOrString, ARRAY_FILTER_USE_KEY); self::assertTrue(count($array) === count($validKeys), $message); } From 24017059ee9d059c698b23438c1065e23e927959 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Tue, 20 Dec 2022 23:31:56 +0000 Subject: [PATCH 62/78] fix phpcs issues --- src/Psalm/Codebase.php | 7 +++++-- .../LanguageServer/Client/TextDocument.php | 5 +---- .../LanguageServer/Client/Workspace.php | 19 +++++++------------ .../LanguageServer/LanguageServer.php | 2 +- tests/LanguageServer/Message.php | 7 ------- 5 files changed, 14 insertions(+), 26 deletions(-) diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index aebf7153281..4d87839914c 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -1608,8 +1608,11 @@ public function getTypeContextAtPosition(string $file_path, Position $position): /** * @return list */ - public function getCompletionItemsForClassishThing(string $type_string, string $gap, bool $snippets_supported = false): array - { + public function getCompletionItemsForClassishThing( + string $type_string, + string $gap, + bool $snippets_supported = false + ): array { $completion_items = []; $type = Type::parseString($type_string); diff --git a/src/Psalm/Internal/LanguageServer/Client/TextDocument.php b/src/Psalm/Internal/LanguageServer/Client/TextDocument.php index bafa46982de..c7f4e378656 100644 --- a/src/Psalm/Internal/LanguageServer/Client/TextDocument.php +++ b/src/Psalm/Internal/LanguageServer/Client/TextDocument.php @@ -17,10 +17,7 @@ class TextDocument { private ClientHandler $handler; - /** - * @var LanguageServer - */ - private $server; + private LanguageServer $server; public function __construct(ClientHandler $handler, LanguageServer $server) { diff --git a/src/Psalm/Internal/LanguageServer/Client/Workspace.php b/src/Psalm/Internal/LanguageServer/Client/Workspace.php index ce39a2f987d..f9d9cf39e90 100644 --- a/src/Psalm/Internal/LanguageServer/Client/Workspace.php +++ b/src/Psalm/Internal/LanguageServer/Client/Workspace.php @@ -11,24 +11,19 @@ /** * Provides method handlers for all textDocument/* methods + * + * @internal */ class Workspace { - /** - * @var ClientHandler - */ - private $handler; + private ClientHandler $handler; /** - * @var JsonMapper * @psalm-suppress UnusedProperty */ - private $mapper; + private JsonMapper $mapper; - /** - * @var LanguageServer - */ - private $server; + private LanguageServer $server; public function __construct(ClientHandler $handler, JsonMapper $mapper, LanguageServer $server) { @@ -57,8 +52,8 @@ public function requestConfiguration(string $section, ?string $scopeUri = null): [ 'section' => $section, 'scopeUri' => $scopeUri, - ] - ] + ], + ], ]); } } diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index b9f4abf277c..b41dff23de0 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -935,7 +935,7 @@ private function clientStatus(string $status, ?string $additional_info = null): $this->client->event( new LogMessage( MessageType::INFO, - $status . (!empty($additional_info) ? ': ' . $additional_info : '') + $status . (!empty($additional_info) ? ': ' . $additional_info : ''), ), ); } catch (Throwable $err) { diff --git a/tests/LanguageServer/Message.php b/tests/LanguageServer/Message.php index 48b4ec6d819..075a50a8134 100644 --- a/tests/LanguageServer/Message.php +++ b/tests/LanguageServer/Message.php @@ -15,13 +15,6 @@ */ abstract class Message extends AdvancedJsonRpcMessage { - /** - * A String specifying the version of the JSON-RPC protocol. MUST be exactly "2.0". - * - * @var string - */ - public $jsonrpc = '2.0'; - /** * Returns the appropriate Message subclass * From 142c9da2fdb1c86cd0bc06bba244ffb9f0fd2877 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Tue, 20 Dec 2022 23:46:17 +0000 Subject: [PATCH 63/78] wrap comment --- src/Psalm/Internal/Cli/LanguageServer.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Cli/LanguageServer.php b/src/Psalm/Internal/Cli/LanguageServer.php index 68c2cd990db..fdd2bc9b73d 100644 --- a/src/Psalm/Internal/Cli/LanguageServer.php +++ b/src/Psalm/Internal/Cli/LanguageServer.php @@ -175,7 +175,8 @@ static function (string $arg) use ($valid_long_options): void { Clears all cache files that the language server uses for this specific project (exits after) --clear-cache-on-boot - Clears all cache files that the language server uses for this specific project on boot (does not exit) + Clears all cache files that the language server uses for this specific project on boot + (does not exit) --use-ini-defaults Use PHP-provided ini defaults for memory and error display From eab70e176cda3c7b5d8b5327b2e10c2f18ebf3a1 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Mon, 26 Dec 2022 21:18:46 +0000 Subject: [PATCH 64/78] allow array-to-xml 3 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index daaa59f6efa..a43f729f2e4 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", "nikic/php-parser": "^4.13", "sebastian/diff": "^4.0", - "spatie/array-to-xml": "^2.17.0", + "spatie/array-to-xml": "^2.17.0 || ^3.0", "symfony/console": "^4.1.6 || ^5.0 || ^6.0", "symfony/filesystem": "^5.4 || ^6.0", "symfony/polyfill-php80": "^1.25" From 27c0caf2fbeb5e35fe181d578a89db16a9b70c59 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Wed, 25 Jan 2023 18:13:27 +0000 Subject: [PATCH 65/78] Fixes #9180 allow baseline file --- src/Psalm/Internal/Cli/LanguageServer.php | 8 +++ .../LanguageServer/ClientConfiguration.php | 9 +++- .../LanguageServer/LanguageServer.php | 54 ++++++++++++++++++- 3 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Cli/LanguageServer.php b/src/Psalm/Internal/Cli/LanguageServer.php index fdd2bc9b73d..88f36b41d51 100644 --- a/src/Psalm/Internal/Cli/LanguageServer.php +++ b/src/Psalm/Internal/Cli/LanguageServer.php @@ -82,6 +82,7 @@ public static function run(array $argv): void 'tcp:', 'tcp-server', 'disable-on-change::', + 'use-baseline:', 'enable-autocomplete::', 'enable-code-actions::', 'enable-provide-diagnostics::', @@ -181,6 +182,9 @@ static function (string $arg) use ($valid_long_options): void { --use-ini-defaults Use PHP-provided ini defaults for memory and error display + --use-baseline=PATH + Allows you to use a baseline other than the default baseline provided in your config + --tcp=url Use TCP mode (by default Psalm uses STDIO) @@ -312,6 +316,10 @@ static function (string $arg) use ($valid_long_options): void { } } + if (isset($options['use-baseline']) && is_string($options['use-baseline'])) { + $clientConfiguration->baseline = $options['use-baseline']; + } + if (isset($options['disable-on-change']) && is_numeric($options['disable-on-change'])) { $clientConfiguration->onchangeLineLimit = (int) $options['disable-on-change']; } diff --git a/src/Psalm/Internal/LanguageServer/ClientConfiguration.php b/src/Psalm/Internal/LanguageServer/ClientConfiguration.php index 973439cf95b..1684287ec14 100644 --- a/src/Psalm/Internal/LanguageServer/ClientConfiguration.php +++ b/src/Psalm/Internal/LanguageServer/ClientConfiguration.php @@ -12,6 +12,11 @@ class ClientConfiguration { + /** + * Location of Baseline file + */ + public ?string $baseline = null; + /** * TCP Server Address */ @@ -106,7 +111,8 @@ public function __construct( ?bool $findUnusedVariables = null, ?string $findUnusedCode = null, ?int $logLevel = null, - ?int $onchangeLineLimit = null + ?int $onchangeLineLimit = null, + ?string $baseline = null ) { $this->hideWarnings = $hideWarnings; $this->provideCompletion = $provideCompletion; @@ -119,5 +125,6 @@ public function __construct( $this->findUnusedCode = $findUnusedCode; $this->logLevel = $logLevel; $this->onchangeLineLimit = $onchangeLineLimit; + $this->baseline = $baseline; } } diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index b41dff23de0..8c318c79a75 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -37,6 +37,7 @@ use LanguageServerProtocol\TextDocumentSyncOptions; use Psalm\Codebase; use Psalm\Config; +use Psalm\ErrorBaseline; use Psalm\Internal\Analyzer\IssueData; use Psalm\Internal\Analyzer\ProjectAnalyzer; use Psalm\Internal\Composer; @@ -59,10 +60,13 @@ use function array_keys; use function array_map; use function array_reduce; +use function array_search; use function array_shift; +use function array_splice; use function array_unshift; use function array_values; use function cli_set_process_title; +use function count; use function explode; use function extension_loaded; use function fwrite; @@ -125,6 +129,9 @@ class LanguageServer extends Dispatcher */ protected string $versionedAnalysisDelayToken = ''; + /** @var array}>> */ + protected array $issue_baseline = []; + /** * This should actually be a private property on `parent` * @@ -578,6 +585,14 @@ function () { */ $serverCapabilities->signatureHelpProvider = new SignatureHelpOptions(['(', ',']); + if ($this->client->clientConfiguration->baseline !== null) { + $this->logInfo('Utilizing Baseline: '.$this->client->clientConfiguration->baseline); + $this->issue_baseline= ErrorBaseline::read( + new FileProvider, + $this->client->clientConfiguration->baseline, + ); + } + $this->logInfo("Initializing: Complete."); $this->clientStatus('initialized'); @@ -724,6 +739,9 @@ public function emitVersionedIssues(array $files, ?int $version = null): void 'version' => $version, ]); + //Copy variable here to be able to process it + $issue_baseline = $this->issue_baseline; + $data = IssueBuffer::clear(); foreach ($files as $file_path => $uri) { //Dont report errors in files we are not watching @@ -787,7 +805,41 @@ function (IssueData $issue_data): Diagnostic { return $diagnostic; }, array_filter( - $data[$file_path] ?? [], + array_map(function (IssueData $issue_data) use (&$issue_baseline) { + if (empty($issue_baseline)) { + return $issue_data; + } + //Process Baseline + $file = $issue_data->file_name; + $type = $issue_data->type; + /** @psalm-suppress MixedArrayAccess */ + if (isset($issue_baseline[$file][$type]) && $issue_baseline[$file][$type]['o'] > 0) { + /** @psalm-suppress MixedArrayAccess, MixedArgument */ + if ($issue_baseline[$file][$type]['o'] === count($issue_baseline[$file][$type]['s'])) { + /** @psalm-suppress MixedArrayAccess, MixedAssignment */ + $position = array_search( + trim($issue_data->selected_text), + $issue_baseline[$file][$type]['s'], + true, + ); + + if ($position !== false) { + $issue_data->severity = Config::REPORT_INFO; + /** @psalm-suppress MixedArgument */ + array_splice($issue_baseline[$file][$type]['s'], $position, 1); + /** @psalm-suppress MixedArrayAssignment, MixedOperand, MixedAssignment */ + $issue_baseline[$file][$type]['o']--; + } + } else { + /** @psalm-suppress MixedArrayAssignment, MixedOperand, MixedAssignment */ + $issue_baseline[$file][$type]['s'] = []; + $issue_data->severity = Config::REPORT_INFO; + /** @psalm-suppress MixedArrayAssignment, MixedOperand, MixedAssignment */ + $issue_baseline[$file][$type]['o']--; + } + } + return $issue_data; + }, $data[$file_path] ?? []), function (IssueData $issue_data) { //Hide Warnings if ($issue_data->severity === Config::REPORT_INFO && From 468e7e7d457c41093d97023d17a5d4df02f94a49 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Thu, 26 Jan 2023 09:12:43 +0000 Subject: [PATCH 66/78] add cli command to turn off xdebug turnoff functionality, fix bug with docblock --- src/Psalm/Internal/Cli/LanguageServer.php | 14 ++++++++++++-- .../LanguageServer/Server/TextDocument.php | 1 + 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Cli/LanguageServer.php b/src/Psalm/Internal/Cli/LanguageServer.php index 88f36b41d51..4813bbc59c2 100644 --- a/src/Psalm/Internal/Cli/LanguageServer.php +++ b/src/Psalm/Internal/Cli/LanguageServer.php @@ -90,6 +90,7 @@ public static function run(array $argv): void 'enable-provide-signature-help::', 'enable-provide-definition::', 'show-diagnostic-warnings::', + 'disable-xdebug::', 'on-change-debounce-ms::', 'use-extended-diagnostic-codes', 'verbose', @@ -222,6 +223,9 @@ static function (string $arg) use ($valid_long_options): void { --on-change-debounce-ms=[INT] The number of milliseconds to debounce onChange events. + --disable-xdebug[=BOOL] + Disable xdebug for performance reasons. Enable for debugging + --verbose Will send log messages to the client with information. @@ -275,8 +279,14 @@ static function (string $arg) use ($valid_long_options): void { $ini_handler->disableExtension('grpc'); - // If Xdebug is enabled, restart without it - $ini_handler->check(); + $diableXdebug = !isset($options['disable-xdebug']) + || !is_string($options['disable-xdebug']) + || strtolower($options['disable-xdebug']) !== 'false'; + + // If Xdebug is enabled, restart without it based on cli + if ($diableXdebug) { + $ini_handler->check(); + } setlocale(LC_CTYPE, 'C'); diff --git a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php index 35e8afbf0bd..309680c66d5 100644 --- a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php +++ b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php @@ -109,6 +109,7 @@ public function didSave(TextDocumentIdentifier $textDocument, ?string $text = nu /** * The document change notification is sent from the client to the server to signal changes to a text document. * + * @param VersionedTextDocumentIdentifier $textDocument * @param TextDocumentContentChangeEvent[] $contentChanges */ public function didChange(VersionedTextDocumentIdentifier $textDocument, array $contentChanges): void From 3c38e02b623fc910046d723cdd1b1d9ebfcd9549 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Thu, 26 Jan 2023 09:40:07 +0000 Subject: [PATCH 67/78] allow dynamically changing configuration without restarting --- .../LanguageServer/ClientConfiguration.php | 2 +- .../LanguageServer/LanguageClient.php | 37 ++++++++++++++++++- .../LanguageServer/LanguageServer.php | 3 ++ 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Internal/LanguageServer/ClientConfiguration.php b/src/Psalm/Internal/LanguageServer/ClientConfiguration.php index 1684287ec14..902f9f66147 100644 --- a/src/Psalm/Internal/LanguageServer/ClientConfiguration.php +++ b/src/Psalm/Internal/LanguageServer/ClientConfiguration.php @@ -45,7 +45,7 @@ class ClientConfiguration /** * Provide Hover Requests or not */ - public ?bool $provideHover= null; + public ?bool $provideHover = null; /** * Provide Signature Help or not diff --git a/src/Psalm/Internal/LanguageServer/LanguageClient.php b/src/Psalm/Internal/LanguageServer/LanguageClient.php index e6225f1bdc3..f5d70e77374 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageClient.php +++ b/src/Psalm/Internal/LanguageServer/LanguageClient.php @@ -11,6 +11,8 @@ use Psalm\Internal\LanguageServer\Client\Workspace as ClientWorkspace; use function is_null; +use function json_decode; +use function json_encode; /** * @internal @@ -144,8 +146,39 @@ private function configurationRefreshed(array $config): void return; } - if (!is_null($this->clientConfiguration->provideCompletion)) { - //$this->server->project_analyzer->provide_completion = $this->clientConfiguration->provideCompletion; + /** @var array */ + $array = json_decode(json_encode($config), true); + + if (isset($array['hideWarnings'])) { + $this->clientConfiguration->hideWarnings = (bool) $array['hideWarnings']; + } + + if (isset($array['provideCompletion'])) { + $this->clientConfiguration->provideCompletion = (bool) $array['provideCompletion']; + } + + if (isset($array['provideDefinition'])) { + $this->clientConfiguration->provideDefinition = (bool) $array['provideDefinition']; + } + + if (isset($array['provideHover'])) { + $this->clientConfiguration->provideHover = (bool) $array['provideHover']; + } + + if (isset($array['provideSignatureHelp'])) { + $this->clientConfiguration->provideSignatureHelp = (bool) $array['provideSignatureHelp']; + } + + if (isset($array['provideCodeActions'])) { + $this->clientConfiguration->provideCodeActions = (bool) $array['provideCodeActions']; + } + + if (isset($array['provideDiagnostics'])) { + $this->clientConfiguration->provideDiagnostics = (bool) $array['provideDiagnostics']; + } + + if (isset($array['findUnusedVariables'])) { + $this->clientConfiguration->findUnusedVariables = (bool) $array['findUnusedVariables']; } } } diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 8c318c79a75..3d5aeaa6ac5 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -109,6 +109,8 @@ class LanguageServer extends Dispatcher */ public ?ServerWorkspace $workspace = null; + public ?ClientInfo $clientInfo = null; + protected ProtocolReader $protocolReader; protected ProtocolWriter $protocolWriter; @@ -426,6 +428,7 @@ public function initialize( ?string $trace = null //?array $workspaceFolders = null //error in json-dispatcher ): Promise { + $this->clientInfo = $clientInfo; $this->clientCapabilities = $capabilities; $this->trace = $trace; return call( From 2efd9c81e00f4eba763061d53559ac26d1de7511 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Thu, 2 Mar 2023 17:05:12 +0000 Subject: [PATCH 68/78] bump to 5.7.7 --- src/Psalm/Internal/LanguageServer/ClientConfiguration.php | 2 +- src/Psalm/Internal/LanguageServer/Server/TextDocument.php | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Psalm/Internal/LanguageServer/ClientConfiguration.php b/src/Psalm/Internal/LanguageServer/ClientConfiguration.php index 902f9f66147..dc8ce9783db 100644 --- a/src/Psalm/Internal/LanguageServer/ClientConfiguration.php +++ b/src/Psalm/Internal/LanguageServer/ClientConfiguration.php @@ -50,7 +50,7 @@ class ClientConfiguration /** * Provide Signature Help or not */ - public ?bool $provideSignatureHelp= null; + public ?bool $provideSignatureHelp = null; /** * Provide Code Actions or not diff --git a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php index 1ebeba169fd..a8e426c4410 100644 --- a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php +++ b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php @@ -430,6 +430,9 @@ public function codeAction(TextDocumentIdentifier $textDocument, Range $range, C /** @var array{type: string, snippet: string, line_from: int, line_to: int} */ $data = (array)$diagnostic->data; + //$file_path = LanguageServer::uriToPath($textDocument->uri); + //$contents = $this->codebase->file_provider->getContents($file_path); + $snippetRange = new Range( new Position($data['line_from']-1), new Position($data['line_to']), @@ -451,9 +454,7 @@ public function codeAction(TextDocumentIdentifier $textDocument, Range $range, C $textDocument->uri => [ new TextEdit( $snippetRange, - "{$indentation}/**\n". - "{$indentation} * @psalm-suppress {$data['type']}\n". - "{$indentation} */\n". + "{$indentation}/** @psalm-suppress {$data['type']} */\n". "{$data['snippet']}\n", ), ], From 5eed370fa5698133d5209d58c0eceb7698ff4a3b Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Thu, 2 Mar 2023 18:02:23 +0000 Subject: [PATCH 69/78] fix outstanding CI issues --- psalm-baseline.xml | 36 --- src/Psalm/CodeLocation.php | 1 + src/Psalm/Codebase.php | 237 +++++++++++++++++- .../LanguageServer/ClientConfiguration.php | 1 - .../LanguageServer/LanguageClient.php | 2 - .../LanguageServer/LanguageServer.php | 17 +- src/Psalm/Internal/LanguageServer/Message.php | 2 - .../LanguageServer/PHPMarkdownContent.php | 2 +- .../LanguageServer/Server/TextDocument.php | 11 +- .../LanguageServer/Server/Workspace.php | 2 +- src/Psalm/Storage/FunctionLikeStorage.php | 14 ++ tests/LanguageServer/SymbolLookupTest.php | 52 ++-- 12 files changed, 286 insertions(+), 91 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index c6faeb97b8d..8b414851c9e 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -16,8 +16,6 @@ $const_name $const_name - $matches[0] - $property_name $symbol_name $symbol_parts[1] @@ -228,11 +226,6 @@ expr->getArgs()[0]]]> - - - - - $identifier_name @@ -304,35 +297,6 @@ props[0]]]> - - - - 4]]> - 4]]> - - - - - $capabilities - $processId - $rootPath - - - - - $pair[1] - - - - - $parts[1] - - - - - $contentChanges[0] - - $method_id_parts[1] diff --git a/src/Psalm/CodeLocation.php b/src/Psalm/CodeLocation.php index 04790dfcc73..0bdfd64a683 100644 --- a/src/Psalm/CodeLocation.php +++ b/src/Psalm/CodeLocation.php @@ -169,6 +169,7 @@ private function calculateRealLocation(): void $codebase = $project_analyzer->getCodebase(); + /** @psalm-suppress ImpureMethodCall */ $file_contents = $codebase->getFileContents($this->file_path); $file_length = strlen($file_contents); diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index 4d87839914c..16187ac6767 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -981,7 +981,7 @@ public function getFunctionStorageForSymbol(string $file_path, string $symbol): /** * Get Markup content from Reference */ - public function getMarkupContentForSymbol( + public function getMarkupContentForSymbolByReference( Reference $reference ): ?PHPMarkdownContent { //Direct Assignment @@ -1195,7 +1195,225 @@ public function getMarkupContentForSymbol( return new PHPMarkdownContent($reference->symbol); } - public function getSymbolLocation(Reference $reference): ?CodeLocation + /** + * @psalm-suppress PossiblyUnusedMethod + * @deprecated will be removed in Psalm 6. use {@see Codebase::getSymbolLocationByReference()} instead + */ + public function getSymbolInformation(string $file_path, string $symbol): ?array + { + if (is_numeric($symbol[0])) { + return ['type' => preg_replace('/^[^:]*:/', '', $symbol)]; + } + + try { + if (strpos($symbol, '::')) { + if (strpos($symbol, '()')) { + $symbol = substr($symbol, 0, -2); + + /** @psalm-suppress ArgumentTypeCoercion */ + $method_id = new MethodIdentifier(...explode('::', $symbol)); + + $declaring_method_id = $this->methods->getDeclaringMethodId($method_id); + + if (!$declaring_method_id) { + return null; + } + + $storage = $this->methods->getStorage($declaring_method_id); + + return [ + 'type' => 'getCompletionSignature(), + 'description' => $storage->description, + ]; + } + + [, $symbol_name] = explode('::', $symbol); + + if (strpos($symbol, '$') !== false) { + $storage = $this->properties->getStorage($symbol); + + return [ + 'type' => 'getInfo() . ' ' . $symbol_name, + 'description' => $storage->description, + ]; + } + + [$fq_classlike_name, $const_name] = explode('::', $symbol); + + $class_constants = $this->classlikes->getConstantsForClass( + $fq_classlike_name, + ReflectionProperty::IS_PRIVATE, + ); + + if (!isset($class_constants[$const_name])) { + return null; + } + + return [ + 'type' => ' $class_constants[$const_name]->description, + ]; + } + + if (strpos($symbol, '()')) { + $function_id = strtolower(substr($symbol, 0, -2)); + $file_storage = $this->file_storage_provider->get($file_path); + + if (isset($file_storage->functions[$function_id])) { + $function_storage = $file_storage->functions[$function_id]; + + return [ + 'type' => 'getCompletionSignature(), + 'description' => $function_storage->description, + ]; + } + + if (!$function_id) { + return null; + } + + $function = $this->functions->getStorage(null, $function_id); + return [ + 'type' => 'getCompletionSignature(), + 'description' => $function->description, + ]; + } + + if (strpos($symbol, '$') === 0) { + $type = VariableFetchAnalyzer::getGlobalType($symbol, $this->analysis_php_version_id); + if (!$type->isMixed()) { + return ['type' => 'classlike_storage_provider->get($symbol); + return [ + 'type' => 'abstract ? 'abstract ' : '') . 'class ' . $storage->name, + 'description' => $storage->description, + ]; + } catch (InvalidArgumentException $e) { + } + + if (strpos($symbol, '\\')) { + $const_name_parts = explode('\\', $symbol); + $const_name = array_pop($const_name_parts); + $namespace_name = implode('\\', $const_name_parts); + + $namespace_constants = NamespaceAnalyzer::getConstantsForNamespace( + $namespace_name, + ReflectionProperty::IS_PUBLIC, + ); + if (isset($namespace_constants[$const_name])) { + $type = $namespace_constants[$const_name]; + return ['type' => 'file_storage_provider->get($file_path); + if (isset($file_storage->constants[$symbol])) { + return ['type' => 'constants[$symbol]]; + } + $constant = ConstFetchAnalyzer::getGlobalConstType($this, $symbol, $symbol); + + if ($constant) { + return ['type' => 'getMessage()); + + return null; + } + } + + /** + * @psalm-suppress PossiblyUnusedMethod + * @deprecated will be removed in Psalm 6. use {@see Codebase::getSymbolLocationByReference()} instead + */ + public function getSymbolLocation(string $file_path, string $symbol): ?CodeLocation + { + if (is_numeric($symbol[0])) { + $symbol = preg_replace('/:.*/', '', $symbol); + $symbol_parts = explode('-', $symbol); + + $file_contents = $this->getFileContents($file_path); + + return new Raw( + $file_contents, + $file_path, + $this->config->shortenFileName($file_path), + (int) $symbol_parts[0], + (int) $symbol_parts[1], + ); + } + + try { + if (strpos($symbol, '::')) { + if (strpos($symbol, '()')) { + $symbol = substr($symbol, 0, -2); + + /** @psalm-suppress ArgumentTypeCoercion */ + $method_id = new MethodIdentifier(...explode('::', $symbol)); + + $declaring_method_id = $this->methods->getDeclaringMethodId($method_id); + + if (!$declaring_method_id) { + return null; + } + + $storage = $this->methods->getStorage($declaring_method_id); + + return $storage->location; + } + + if (strpos($symbol, '$') !== false) { + $storage = $this->properties->getStorage($symbol); + + return $storage->location; + } + + [$fq_classlike_name, $const_name] = explode('::', $symbol); + + $class_constants = $this->classlikes->getConstantsForClass( + $fq_classlike_name, + ReflectionProperty::IS_PRIVATE, + ); + + if (!isset($class_constants[$const_name])) { + return null; + } + + return $class_constants[$const_name]->location; + } + + if (strpos($symbol, '()')) { + $file_storage = $this->file_storage_provider->get($file_path); + + $function_id = strtolower(substr($symbol, 0, -2)); + + if (isset($file_storage->functions[$function_id])) { + return $file_storage->functions[$function_id]->location; + } + + if (!$function_id) { + return null; + } + + return $this->functions->getStorage(null, $function_id)->location; + } + + return $this->classlike_storage_provider->get($symbol)->location; + } catch (UnexpectedValueException $e) { + error_log($e->getMessage()); + + return null; + } catch (InvalidArgumentException $e) { + return null; + } + } + + public function getSymbolLocationByReference(Reference $reference): ?CodeLocation { if (is_numeric($reference->symbol[0])) { $symbol = preg_replace('/:.*/', '', $reference->symbol); @@ -1296,10 +1514,23 @@ public function getSymbolLocation(Reference $reference): ?CodeLocation } } + /** + * @psalm-suppress PossiblyUnusedMethod + * @return array{0: string, 1: Range}|null + */ + public function getReferenceAtPosition(string $file_path, Position $position): ?array + { + $ref = $this->getReferenceAtPositionAsReference($file_path, $position); + if($ref === null) { + return null; + } + return [$ref->symbol, $ref->range]; + } + /** * Get Reference from Position */ - public function getReferenceAtPosition( + public function getReferenceAtPositionAsReference( string $file_path, Position $position ): ?Reference { diff --git a/src/Psalm/Internal/LanguageServer/ClientConfiguration.php b/src/Psalm/Internal/LanguageServer/ClientConfiguration.php index dc8ce9783db..115a38f6567 100644 --- a/src/Psalm/Internal/LanguageServer/ClientConfiguration.php +++ b/src/Psalm/Internal/LanguageServer/ClientConfiguration.php @@ -80,7 +80,6 @@ class ClientConfiguration * Log Level * * @see MessageType - * @psalm-suppress PossiblyUnusedProperty */ public ?int $logLevel = null; diff --git a/src/Psalm/Internal/LanguageServer/LanguageClient.php b/src/Psalm/Internal/LanguageServer/LanguageClient.php index f5d70e77374..096177d9d5c 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageClient.php +++ b/src/Psalm/Internal/LanguageServer/LanguageClient.php @@ -103,8 +103,6 @@ public function logTrace(LogTrace $logTrace): void /** * Send a log message to the client. - * - * @param LogMessage $message */ public function logMessage(LogMessage $logMessage): void { diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 3d5aeaa6ac5..78f85048252 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -95,6 +95,7 @@ use const STDOUT; /** + * @psalm-api * @internal */ class LanguageServer extends Dispatcher @@ -124,7 +125,6 @@ class LanguageServer extends Dispatcher protected ProjectAnalyzer $project_analyzer; protected Codebase $codebase; - protected array $onsave_paths_to_analyze = []; /** * The AMP Delay token @@ -192,7 +192,6 @@ function (Message $msg): Generator { */ $dispatched = $this->dispatch($msg->body); if ($dispatched !== null) { - /** @psalm-suppress MixedAssignment */ $result = yield $dispatched; } else { $result = null; @@ -410,12 +409,8 @@ function (): void { * @param string|null $rootPath The rootPath of the workspace. Is null if no folder is open. * @param mixed $initializationOptions * @param string|null $trace The initial trace setting. If omitted trace is disabled ('off'). - * @param array|null $workspaceFolders The workspace folders configured in the client when - * the server starts. This property is only available if the client supports workspace folders. - * It can be `null` if the client supports workspace folders but none are - * configured. * @psalm-return Promise - * @psalm-suppress PossiblyUnusedMethod + * @psalm-suppress PossiblyUnusedParam */ public function initialize( ClientCapabilities $capabilities, @@ -616,8 +611,6 @@ function () { * initialize request but before the client is sending any other request or notification to the server. * The server can use the initialized notification for example to dynamically register capabilities. * The initialized notification may only be sent once. - * - * @psalm-suppress PossiblyUnusedMethod */ public function initialized(): void { @@ -834,7 +827,7 @@ function (IssueData $issue_data): Diagnostic { $issue_baseline[$file][$type]['o']--; } } else { - /** @psalm-suppress MixedArrayAssignment, MixedOperand, MixedAssignment */ + /** @psalm-suppress MixedArrayAssignment, MixedAssignment */ $issue_baseline[$file][$type]['s'] = []; $issue_data->severity = Config::REPORT_INFO; /** @psalm-suppress MixedArrayAssignment, MixedOperand, MixedAssignment */ @@ -866,8 +859,6 @@ function (IssueData $issue_data) { * that asks the server to exit. Clients must not send any notifications other than exit or requests to a server to * which they have sent a shutdown request. Clients should also wait with sending the exit notification until they * have received a response from the shutdown request. - * - * @psalm-suppress PossiblyUnusedReturnValue */ public function shutdown(): Promise { @@ -952,8 +943,6 @@ public function logError(string $message, array $context = []): void /** * Log Warning message to the client - * - * @psalm-suppress PossiblyUnusedMethod */ public function logWarning(string $message, array $context = []): void { diff --git a/src/Psalm/Internal/LanguageServer/Message.php b/src/Psalm/Internal/LanguageServer/Message.php index 34785ff1515..56b46c23350 100644 --- a/src/Psalm/Internal/LanguageServer/Message.php +++ b/src/Psalm/Internal/LanguageServer/Message.php @@ -24,8 +24,6 @@ class Message /** * Parses a message - * - * @psalm-suppress UnusedMethod */ public static function parse(string $msg): Message { diff --git a/src/Psalm/Internal/LanguageServer/PHPMarkdownContent.php b/src/Psalm/Internal/LanguageServer/PHPMarkdownContent.php index acc76dafa47..3290ea5cd4c 100644 --- a/src/Psalm/Internal/LanguageServer/PHPMarkdownContent.php +++ b/src/Psalm/Internal/LanguageServer/PHPMarkdownContent.php @@ -12,6 +12,7 @@ use function get_object_vars; /** + * @psalm-api * @internal */ class PHPMarkdownContent extends MarkupContent implements JsonSerializable @@ -46,7 +47,6 @@ public function __construct(string $code, ?string $title = null, ?string $descri * meaning if a null is sent then this will not compute * * @return mixed - * @psalm-suppress UnusedMethod */ #[ReturnTypeWillChange] public function jsonSerialize() diff --git a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php index a8e426c4410..b9c4bcca8f4 100644 --- a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php +++ b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php @@ -186,7 +186,7 @@ public function definition(TextDocumentIdentifier $textDocument, Position $posit } try { - $reference = $this->codebase->getReferenceAtPosition($file_path, $position); + $reference = $this->codebase->getReferenceAtPositionAsReference($file_path, $position); } catch (UnanalyzedFileException $e) { $this->server->logThrowable($e); return new Success(null); @@ -197,7 +197,7 @@ public function definition(TextDocumentIdentifier $textDocument, Position $posit } - $code_location = $this->codebase->getSymbolLocation($reference); + $code_location = $this->codebase->getSymbolLocationByReference($reference); if (!$code_location) { return new Success(null); @@ -240,7 +240,7 @@ public function hover(TextDocumentIdentifier $textDocument, Position $position): } try { - $reference = $this->codebase->getReferenceAtPosition($file_path, $position); + $reference = $this->codebase->getReferenceAtPositionAsReference($file_path, $position); } catch (UnanalyzedFileException $e) { $this->server->logThrowable($e); return new Success(null); @@ -251,7 +251,7 @@ public function hover(TextDocumentIdentifier $textDocument, Position $position): } try { - $markup = $this->codebase->getMarkupContentForSymbol($reference); + $markup = $this->codebase->getMarkupContentForSymbolByReference($reference); } catch (UnexpectedValueException $e) { $this->server->logThrowable($e); return new Success(null); @@ -261,7 +261,6 @@ public function hover(TextDocumentIdentifier $textDocument, Position $position): return new Success(null); } - /** @psalm-suppress InvalidArgument */ return new Success(new Hover($markup, $reference->range)); } @@ -403,6 +402,8 @@ public function signatureHelp(TextDocumentIdentifier $textDocument, Position $po * The code action request is sent from the client to the server to compute commands * for a given text document and range. These commands are typically code fixes to * either fix problems or to beautify/refactor code. + * + * @psalm-suppress PossiblyUnusedParam */ public function codeAction(TextDocumentIdentifier $textDocument, Range $range, CodeActionContext $context): Promise { diff --git a/src/Psalm/Internal/LanguageServer/Server/Workspace.php b/src/Psalm/Internal/LanguageServer/Server/Workspace.php index bf19df62420..af49619c356 100644 --- a/src/Psalm/Internal/LanguageServer/Server/Workspace.php +++ b/src/Psalm/Internal/LanguageServer/Server/Workspace.php @@ -109,7 +109,7 @@ public function didChangeWatchedFiles(array $changes): void * A notification sent from the client to the server to signal the change of configuration settings. * * @param mixed $settings - * @psalm-suppress PossiblyUnusedMethod + * @psalm-suppress PossiblyUnusedMethod, PossiblyUnusedParam */ public function didChangeConfiguration($settings): void { diff --git a/src/Psalm/Storage/FunctionLikeStorage.php b/src/Psalm/Storage/FunctionLikeStorage.php index b1d3c5e97f6..1333f61d6ca 100644 --- a/src/Psalm/Storage/FunctionLikeStorage.php +++ b/src/Psalm/Storage/FunctionLikeStorage.php @@ -339,4 +339,18 @@ public function getAttributeStorages(): array { return $this->attributes; } + + public function __toString(): string + { + return $this->getCompletionSignature(); + } + + /** + * @deprecated will be removed in Psalm 6. use {@see FunctionLikeStorage::getCompletionSignature()} instead + * @psalm-suppress PossiblyUnusedParam, PossiblyUnusedMethod + */ + public function getSignature(bool $allow_newlines): string + { + return $this->getCompletionSignature(); + } } diff --git a/tests/LanguageServer/SymbolLookupTest.php b/tests/LanguageServer/SymbolLookupTest.php index aace1a15f26..d235bc33442 100644 --- a/tests/LanguageServer/SymbolLookupTest.php +++ b/tests/LanguageServer/SymbolLookupTest.php @@ -98,7 +98,7 @@ function qux(int $a, int $b) : int { $range = new Range(new Position(1, 1), new Position(1, 1)); - $information = $this->codebase->getMarkupContentForSymbol( + $information = $this->codebase->getMarkupContentForSymbolByReference( new Reference( 'somefile.php', 'B\A::foo()', @@ -110,7 +110,7 @@ function qux(int $a, int $b) : int { $this->assertSame("B\A::foo", $information->title); $this->assertNull($information->description); - $information = $this->codebase->getMarkupContentForSymbol( + $information = $this->codebase->getMarkupContentForSymbolByReference( new Reference( 'somefile.php', 'B\A::$a', @@ -122,7 +122,7 @@ function qux(int $a, int $b) : int { $this->assertSame('B\A::$a', $information->title); $this->assertSame('', $information->description); - $information = $this->codebase->getMarkupContentForSymbol( + $information = $this->codebase->getMarkupContentForSymbolByReference( new Reference( 'somefile.php', 'B\bar()', @@ -134,7 +134,7 @@ function qux(int $a, int $b) : int { $this->assertSame('b\bar', $information->title); $this->assertNull($information->description); - $information = $this->codebase->getMarkupContentForSymbol( + $information = $this->codebase->getMarkupContentForSymbolByReference( new Reference( 'somefile.php', 'B\A::BANANA', @@ -146,7 +146,7 @@ function qux(int $a, int $b) : int { $this->assertSame('B\A::BANANA', $information->title); $this->assertNull($information->description); - $information = $this->codebase->getMarkupContentForSymbol( + $information = $this->codebase->getMarkupContentForSymbolByReference( new Reference( 'somefile.php', 'B\baz()', @@ -158,7 +158,7 @@ function qux(int $a, int $b) : int { $this->assertSame('b\baz', $information->title); $this->assertNull($information->description); - $information = $this->codebase->getMarkupContentForSymbol( + $information = $this->codebase->getMarkupContentForSymbolByReference( new Reference( 'somefile.php', 'B\qux()', @@ -170,7 +170,7 @@ function qux(int $a, int $b) : int { $this->assertSame('b\qux', $information->title); $this->assertNull($information->description); - $information = $this->codebase->getMarkupContentForSymbol( + $information = $this->codebase->getMarkupContentForSymbolByReference( new Reference( 'somefile.php', '$_SESSION', @@ -182,7 +182,7 @@ function qux(int $a, int $b) : int { $this->assertSame('$_SESSION', $information->title); $this->assertNull($information->description); - $information = $this->codebase->getMarkupContentForSymbol( + $information = $this->codebase->getMarkupContentForSymbolByReference( new Reference( 'somefile.php', '$my_global', @@ -210,7 +210,7 @@ public function testSimpleSymbolLookupGlobalConst(): void $this->analyzeFile('somefile.php', new Context()); - $information = $this->codebase->getMarkupContentForSymbol( + $information = $this->codebase->getMarkupContentForSymbolByReference( new Reference( 'somefile.php', 'APPLE', @@ -222,7 +222,7 @@ public function testSimpleSymbolLookupGlobalConst(): void $this->assertSame("APPLE", $information->title); $this->assertNull($information->description); - $information = $this->codebase->getMarkupContentForSymbol( + $information = $this->codebase->getMarkupContentForSymbolByReference( new Reference( 'somefile.php', 'BANANA', @@ -266,7 +266,7 @@ function bar() : int { $range = new Range(new Position(1, 1), new Position(1, 1)); - $method_symbol_location = $this->codebase->getSymbolLocation(new Reference( + $method_symbol_location = $this->codebase->getSymbolLocationByReference(new Reference( 'somefile.php', 'B\A::foo()', $range, @@ -276,7 +276,7 @@ function bar() : int { $this->assertSame(10, $method_symbol_location->getLineNumber()); $this->assertSame(37, $method_symbol_location->getColumn()); - $property_symbol_location = $this->codebase->getSymbolLocation(new Reference( + $property_symbol_location = $this->codebase->getSymbolLocationByReference(new Reference( 'somefile.php', 'B\A::$a', $range, @@ -286,7 +286,7 @@ function bar() : int { $this->assertSame(6, $property_symbol_location->getLineNumber()); $this->assertSame(31, $property_symbol_location->getColumn()); - $constant_symbol_location = $this->codebase->getSymbolLocation(new Reference( + $constant_symbol_location = $this->codebase->getSymbolLocationByReference(new Reference( 'somefile.php', 'B\A::BANANA', $range, @@ -296,7 +296,7 @@ function bar() : int { $this->assertSame(8, $constant_symbol_location->getLineNumber()); $this->assertSame(27, $constant_symbol_location->getColumn()); - $function_symbol_location = $this->codebase->getSymbolLocation(new Reference( + $function_symbol_location = $this->codebase->getSymbolLocationByReference(new Reference( 'somefile.php', 'B\bar()', $range, @@ -306,7 +306,7 @@ function bar() : int { $this->assertSame(16, $function_symbol_location->getLineNumber()); $this->assertSame(26, $function_symbol_location->getColumn()); - $function_symbol_location = $this->codebase->getSymbolLocation(new Reference( + $function_symbol_location = $this->codebase->getSymbolLocationByReference(new Reference( 'somefile.php', '257-259', $range, @@ -378,19 +378,19 @@ public function bar() : void { $this->codebase->analyzer->analyzeFiles($this->project_analyzer, 1, false); - $reference = $this->codebase->getReferenceAtPosition('somefile.php', new Position(10, 30)); + $reference = $this->codebase->getReferenceAtPositionAsReference('somefile.php', new Position(10, 30)); $this->assertNotNull($reference); $this->assertSame('245-246:int|null', $reference->symbol); - $reference = $this->codebase->getReferenceAtPosition('somefile.php', new Position(12, 30)); + $reference = $this->codebase->getReferenceAtPositionAsReference('somefile.php', new Position(12, 30)); $this->assertNotNull($reference); $this->assertSame('213-214:1', $reference->symbol); - $reference = $this->codebase->getReferenceAtPosition('somefile.php', new Position(17, 30)); + $reference = $this->codebase->getReferenceAtPositionAsReference('somefile.php', new Position(17, 30)); $this->assertNotNull($reference); @@ -422,7 +422,7 @@ public function bar() : void { $this->codebase->scanFiles(); $this->analyzeFile('somefile.php', new Context()); - $reference = $this->codebase->getReferenceAtPosition('somefile.php', new Position(9, 33)); + $reference = $this->codebase->getReferenceAtPositionAsReference('somefile.php', new Position(9, 33)); $this->assertNotNull($reference); @@ -449,11 +449,11 @@ function foo() : void { $this->codebase->scanFiles(); $this->analyzeFile('somefile.php', new Context()); - $reference = $this->codebase->getReferenceAtPosition('somefile.php', new Position(2, 31)); + $reference = $this->codebase->getReferenceAtPositionAsReference('somefile.php', new Position(2, 31)); $this->assertNotNull($reference); $this->assertSame('$my_global', $reference->symbol); - $reference = $this->codebase->getReferenceAtPosition('somefile.php', new Position(3, 28)); + $reference = $this->codebase->getReferenceAtPositionAsReference('somefile.php', new Position(3, 28)); $this->assertNotNull($reference); $this->assertSame('73-82:string', $reference->symbol); } @@ -477,7 +477,7 @@ function B( ?AClass $class ) { $this->codebase->scanFiles(); $this->analyzeFile('somefile.php', new Context()); - $reference = $this->codebase->getReferenceAtPosition('somefile.php', new Position(4, 33)); + $reference = $this->codebase->getReferenceAtPositionAsReference('somefile.php', new Position(4, 33)); $this->assertNotNull($reference); $this->assertSame('B\AClass', $reference->symbol); @@ -506,7 +506,7 @@ protected function get_command() : AClass { $this->codebase->scanFiles(); $this->analyzeFile('somefile.php', new Context()); - $reference = $this->codebase->getReferenceAtPosition('somefile.php', new Position(6, 60)); + $reference = $this->codebase->getReferenceAtPositionAsReference('somefile.php', new Position(6, 60)); $this->assertNotNull($reference); $this->assertSame('B\AClass', $reference->symbol); @@ -529,7 +529,7 @@ public function testGetSymbolPositionUseStatement(): void $this->codebase->scanFiles(); $this->analyzeFile('somefile.php', new Context()); - $reference = $this->codebase->getReferenceAtPosition('somefile.php', new Position(2, 25)); + $reference = $this->codebase->getReferenceAtPositionAsReference('somefile.php', new Position(2, 25)); $this->assertNotNull($reference); $this->assertSame('StreamWrapper', $reference->symbol); @@ -557,7 +557,7 @@ function foo() : string { // This is focusing the $active_symbol variable, the LSP Range that is // returned should also point to the same variable (that's where hover popovers will show) - $reference = $this->codebase->getReferenceAtPosition('somefile.php', new Position(6, 26)); + $reference = $this->codebase->getReferenceAtPositionAsReference('somefile.php', new Position(6, 26)); $this->assertNotNull($reference); $this->assertSame(16, $reference->range->start->character); @@ -584,7 +584,7 @@ class A { $this->codebase->scanFiles(); $this->analyzeFile('somefile.php', new Context()); - $reference = $this->codebase->getReferenceAtPosition('somefile.php', new Position(4, 35)); + $reference = $this->codebase->getReferenceAtPositionAsReference('somefile.php', new Position(4, 35)); $this->assertNotNull($reference); From 34678169006cf186dc2666321ee546d706cd6d33 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Thu, 2 Mar 2023 18:07:50 +0000 Subject: [PATCH 70/78] fix --- src/Psalm/Codebase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index 16187ac6767..915b45399d2 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -1521,7 +1521,7 @@ public function getSymbolLocationByReference(Reference $reference): ?CodeLocatio public function getReferenceAtPosition(string $file_path, Position $position): ?array { $ref = $this->getReferenceAtPositionAsReference($file_path, $position); - if($ref === null) { + if ($ref === null) { return null; } return [$ref->symbol, $ref->range]; From 0e4b878cbe0a4e2a53e735c2277f7dba22f4d2c1 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Thu, 2 Mar 2023 18:14:20 +0000 Subject: [PATCH 71/78] fix outstanding issues --- src/Psalm/Internal/LanguageServer/LanguageServer.php | 2 +- src/Psalm/Internal/Provider/FileProvider.php | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 78f85048252..729b337c56e 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -827,7 +827,7 @@ function (IssueData $issue_data): Diagnostic { $issue_baseline[$file][$type]['o']--; } } else { - /** @psalm-suppress MixedArrayAssignment, MixedAssignment */ + /** @psalm-suppress MixedArrayAssignment */ $issue_baseline[$file][$type]['s'] = []; $issue_data->severity = Config::REPORT_INFO; /** @psalm-suppress MixedArrayAssignment, MixedOperand, MixedAssignment */ diff --git a/src/Psalm/Internal/Provider/FileProvider.php b/src/Psalm/Internal/Provider/FileProvider.php index 22438cdfff2..c18960de19b 100644 --- a/src/Psalm/Internal/Provider/FileProvider.php +++ b/src/Psalm/Internal/Provider/FileProvider.php @@ -44,25 +44,20 @@ public function getContents(string $file_path, bool $go_to_source = false): stri return $this->temp_files[$file_path]['content']; } - /** @psalm-suppress ImpureStaticProperty Used only for caching */ if (isset(self::$open_files[$file_path])) { return self::$open_files[$file_path]; } - /** @psalm-suppress ImpureFunctionCall For our purposes, this should not mutate external state */ if (!file_exists($file_path)) { throw new UnexpectedValueException('File ' . $file_path . ' should exist to get contents'); } - /** @psalm-suppress ImpureFunctionCall For our purposes, this should not mutate external state */ if (is_dir($file_path)) { throw new UnexpectedValueException('File ' . $file_path . ' is a directory'); } - /** @psalm-suppress ImpureFunctionCall For our purposes, this should not mutate external state */ $file_contents = (string) file_get_contents($file_path); - /** @psalm-suppress ImpureStaticProperty Used only for caching */ self::$open_files[$file_path] = $file_contents; return $file_contents; From 24b6a97891a0c7ae02bc1bf62030638c5453e4f4 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Thu, 2 Mar 2023 18:45:23 +0000 Subject: [PATCH 72/78] use in-memory cache thanks @ging-dev, remove pnctl fork --- .../LanguageServer/LanguageServer.php | 93 ++---- .../ClassLikeStorageCacheProvider.php | 44 +++ .../Provider/FileReferenceCacheProvider.php | 278 ++++++++++++++++++ .../Provider/FileStorageCacheProvider.php | 48 +++ .../Provider/ParserCacheProvider.php | 102 +++++++ .../Provider/ProjectCacheProvider.php | 38 +++ 6 files changed, 530 insertions(+), 73 deletions(-) create mode 100644 src/Psalm/Internal/LanguageServer/Provider/ClassLikeStorageCacheProvider.php create mode 100644 src/Psalm/Internal/LanguageServer/Provider/FileReferenceCacheProvider.php create mode 100644 src/Psalm/Internal/LanguageServer/Provider/FileStorageCacheProvider.php create mode 100644 src/Psalm/Internal/LanguageServer/Provider/ParserCacheProvider.php create mode 100644 src/Psalm/Internal/LanguageServer/Provider/ProjectCacheProvider.php diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 729b337c56e..b24a74ec6bb 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -40,15 +40,14 @@ use Psalm\ErrorBaseline; use Psalm\Internal\Analyzer\IssueData; use Psalm\Internal\Analyzer\ProjectAnalyzer; -use Psalm\Internal\Composer; +use Psalm\Internal\LanguageServer\Provider\ClassLikeStorageCacheProvider; +use Psalm\Internal\LanguageServer\Provider\FileReferenceCacheProvider; +use Psalm\Internal\LanguageServer\Provider\FileStorageCacheProvider; +use Psalm\Internal\LanguageServer\Provider\ParserCacheProvider; +use Psalm\Internal\LanguageServer\Provider\ProjectCacheProvider; use Psalm\Internal\LanguageServer\Server\TextDocument as ServerTextDocument; use Psalm\Internal\LanguageServer\Server\Workspace as ServerWorkspace; -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\IssueBuffer; use Throwable; @@ -68,15 +67,11 @@ use function cli_set_process_title; use function count; use function explode; -use function extension_loaded; use function fwrite; use function implode; -use function in_array; -use function ini_get; use function json_encode; use function max; use function parse_url; -use function pcntl_fork; use function rawurlencode; use function realpath; use function str_replace; @@ -249,14 +244,13 @@ public static function run(Config $config, ClientConfiguration $clientConfigurat { $progress = new Progress(); - //no-cache mode does not work in the LSP $providers = new Providers( new FileProvider, - new ParserCacheProvider($config), - new FileStorageCacheProvider($config), - new ClassLikeStorageCacheProvider($config), + new ParserCacheProvider, + new FileStorageCacheProvider, + new ClassLikeStorageCacheProvider, new FileReferenceCacheProvider($config), - new ProjectCacheProvider(Composer::getLockFilePath($base_dir)), + new ProjectCacheProvider, ); $codebase = new Codebase( @@ -318,67 +312,20 @@ public static function run(Config $config, ClientConfiguration $clientConfigurat } fwrite(STDOUT, "Server listening on {$clientConfiguration->TCPServerAddress}\n"); - $fork_available = true; - if (!extension_loaded('pcntl')) { - fwrite(STDERR, "PCNTL is not available. Only a single connection will be accepted\n"); - $fork_available = false; - } - - $disabled_functions = array_map('trim', explode(',', ini_get('disable_functions'))); - if (in_array('pcntl_fork', $disabled_functions)) { - fwrite( - STDERR, - "pcntl_fork() is disabled by php configuration (disable_functions directive)." - . " Only a single connection will be accepted\n", - ); - $fork_available = false; - } - while ($socket = stream_socket_accept($tcpServer, -1)) { fwrite(STDOUT, "Connection accepted\n"); stream_set_blocking($socket, false); - if ($fork_available) { - // 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', - function (): void { - fwrite(STDOUT, "Connection closed\n"); - }, - ); - new self( - $reader, - new ProtocolStreamWriter($socket), - $project_analyzer, - $codebase, - $clientConfiguration, - $progress, - ); - // 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), - $project_analyzer, - $codebase, - $clientConfiguration, - $progress, - ); - Loop::run(); - } + //we only accept one connection. + //An exit notification will terminate the server + new LanguageServer( + new ProtocolStreamReader($socket), + new ProtocolStreamWriter($socket), + $project_analyzer, + $codebase, + $clientConfiguration, + $progress, + ); + Loop::run(); } } else { // Use STDIO diff --git a/src/Psalm/Internal/LanguageServer/Provider/ClassLikeStorageCacheProvider.php b/src/Psalm/Internal/LanguageServer/Provider/ClassLikeStorageCacheProvider.php new file mode 100644 index 00000000000..88c0723480b --- /dev/null +++ b/src/Psalm/Internal/LanguageServer/Provider/ClassLikeStorageCacheProvider.php @@ -0,0 +1,44 @@ + */ + private array $cache = []; + + public function __construct() + { + } + + public function writeToCache(ClassLikeStorage $storage, ?string $file_path, ?string $file_contents): void + { + $fq_classlike_name_lc = strtolower($storage->name); + $this->cache[$fq_classlike_name_lc] = $storage; + } + + public function getLatestFromCache(string $fq_classlike_name_lc, ?string $file_path, ?string $file_contents): ClassLikeStorage + { + $cached_value = $this->loadFromCache($fq_classlike_name_lc); + + if (!$cached_value) { + throw new UnexpectedValueException('Should be in cache'); + } + + return $cached_value; + } + + private function loadFromCache(string $fq_classlike_name_lc): ?ClassLikeStorage + { + return $this->cache[$fq_classlike_name_lc] ?? null; + } +} diff --git a/src/Psalm/Internal/LanguageServer/Provider/FileReferenceCacheProvider.php b/src/Psalm/Internal/LanguageServer/Provider/FileReferenceCacheProvider.php new file mode 100644 index 00000000000..0d8e02fc401 --- /dev/null +++ b/src/Psalm/Internal/LanguageServer/Provider/FileReferenceCacheProvider.php @@ -0,0 +1,278 @@ +> */ + private array $cached_correct_methods = []; + + /** + * @var array< + * string, + * array{ + * 0: array, + * 1: array, + * 2: array + * } + * > + */ + private array $cached_file_maps = []; + + public function __construct(Config $config) + { + $this->config = $config; + } + + public function getCachedFileReferences(): ?array + { + return $this->cached_file_references; + } + + public function getCachedClassLikeFiles(): ?array + { + return $this->cached_classlike_files; + } + + public function getCachedMethodClassReferences(): ?array + { + return $this->cached_method_class_references; + } + + public function getCachedNonMethodClassReferences(): ?array + { + return $this->cached_nonmethod_class_references; + } + + public function getCachedFileMemberReferences(): ?array + { + return $this->cached_file_member_references; + } + + public function getCachedFilePropertyReferences(): ?array + { + return $this->cached_file_property_references; + } + + public function getCachedFileMethodReturnReferences(): ?array + { + return $this->cached_file_method_return_references; + } + + public function getCachedMethodMemberReferences(): ?array + { + return $this->cached_method_member_references; + } + + public function getCachedMethodDependencies(): ?array + { + return $this->cached_method_dependencies; + } + + public function getCachedMethodPropertyReferences(): ?array + { + return $this->cached_method_property_references; + } + + public function getCachedMethodMethodReturnReferences(): ?array + { + return $this->cached_method_method_return_references; + } + + public function getCachedFileMissingMemberReferences(): ?array + { + return $this->cached_file_missing_member_references; + } + + public function getCachedMixedMemberNameReferences(): ?array + { + return $this->cached_unknown_member_references; + } + + public function getCachedMethodMissingMemberReferences(): ?array + { + return $this->cached_method_missing_member_references; + } + + public function getCachedMethodParamUses(): ?array + { + return $this->cached_method_param_uses; + } + + public function getCachedIssues(): ?array + { + return $this->cached_issues; + } + + public function setCachedFileReferences(array $file_references): void + { + $this->cached_file_references = $file_references; + } + + public function setCachedClassLikeFiles(array $file_references): void + { + $this->cached_classlike_files = $file_references; + } + + public function setCachedMethodClassReferences(array $method_class_references): void + { + $this->cached_method_class_references = $method_class_references; + } + + public function setCachedNonMethodClassReferences(array $file_class_references): void + { + $this->cached_nonmethod_class_references = $file_class_references; + } + + public function setCachedMethodMemberReferences(array $member_references): void + { + $this->cached_method_member_references = $member_references; + } + + public function setCachedMethodDependencies(array $member_references): void + { + $this->cached_method_dependencies = $member_references; + } + + public function setCachedMethodPropertyReferences(array $property_references): void + { + $this->cached_method_property_references = $property_references; + } + + public function setCachedMethodMethodReturnReferences(array $method_return_references): void + { + $this->cached_method_method_return_references = $method_return_references; + } + + public function setCachedMethodMissingMemberReferences(array $member_references): void + { + $this->cached_method_missing_member_references = $member_references; + } + + public function setCachedFileMemberReferences(array $member_references): void + { + $this->cached_file_member_references = $member_references; + } + + public function setCachedFilePropertyReferences(array $property_references): void + { + $this->cached_file_property_references = $property_references; + } + + public function setCachedFileMethodReturnReferences(array $method_return_references): void + { + $this->cached_file_method_return_references = $method_return_references; + } + + public function setCachedFileMissingMemberReferences(array $member_references): void + { + $this->cached_file_missing_member_references = $member_references; + } + + public function setCachedMixedMemberNameReferences(array $references): void + { + $this->cached_unknown_member_references = $references; + } + + public function setCachedMethodParamUses(array $uses): void + { + $this->cached_method_param_uses = $uses; + } + + public function setCachedIssues(array $issues): void + { + $this->cached_issues = $issues; + } + + /** + * @return array> + */ + public function getAnalyzedMethodCache(): array + { + return $this->cached_correct_methods; + } + + /** + * @param array> $analyzed_methods + */ + public function setAnalyzedMethodCache(array $analyzed_methods): void + { + $this->cached_correct_methods = $analyzed_methods; + } + + /** + * @return array< + * string, + * array{ + * 0: array, + * 1: array, + * 2: array + * } + * > + */ + public function getFileMapCache(): array + { + return $this->cached_file_maps; + } + + /** + * @param array< + * string, + * array{ + * 0: array, + * 1: array, + * 2: array + * } + * > $file_maps + */ + public function setFileMapCache(array $file_maps): void + { + $this->cached_file_maps = $file_maps; + } + + /** + * @param array $mixed_counts + */ + public function setTypeCoverage(array $mixed_counts): void + { + } +} diff --git a/src/Psalm/Internal/LanguageServer/Provider/FileStorageCacheProvider.php b/src/Psalm/Internal/LanguageServer/Provider/FileStorageCacheProvider.php new file mode 100644 index 00000000000..c91ffcc14ab --- /dev/null +++ b/src/Psalm/Internal/LanguageServer/Provider/FileStorageCacheProvider.php @@ -0,0 +1,48 @@ + */ + private array $cache = []; + + public function __construct() + { + } + + public function writeToCache(FileStorage $storage, string $file_contents): void + { + $file_path = strtolower($storage->file_path); + $this->cache[$file_path] = $storage; + } + + public function getLatestFromCache(string $file_path, string $file_contents): ?FileStorage + { + $cached_value = $this->loadFromCache(strtolower($file_path)); + + if (!$cached_value) { + return null; + } + + return $cached_value; + } + + public function removeCacheForFile(string $file_path): void + { + unset($this->cache[strtolower($file_path)]); + } + + private function loadFromCache(string $file_path): ?FileStorage + { + return $this->cache[strtolower($file_path)] ?? null; + } +} diff --git a/src/Psalm/Internal/LanguageServer/Provider/ParserCacheProvider.php b/src/Psalm/Internal/LanguageServer/Provider/ParserCacheProvider.php new file mode 100644 index 00000000000..07218dce5e9 --- /dev/null +++ b/src/Psalm/Internal/LanguageServer/Provider/ParserCacheProvider.php @@ -0,0 +1,102 @@ + + */ + private array $file_contents_cache = []; + + /** + * @var array + */ + private array $file_content_hash = []; + + /** + * @var array> + */ + private array $statements_cache = []; + + /** + * @var array + */ + private array $statements_cache_time = []; + + public function __construct() + { + } + + public function loadStatementsFromCache(string $file_path, int $file_modified_time, string $file_content_hash): ?array + { + if (isset($this->statements_cache[$file_path]) + && $this->statements_cache_time[$file_path] >= $file_modified_time + && $this->file_content_hash[$file_path] === $file_content_hash + ) { + return $this->statements_cache[$file_path]; + } + + return null; + } + + /** + * @return list|null + */ + public function loadExistingStatementsFromCache(string $file_path): ?array + { + if (isset($this->statements_cache[$file_path])) { + return $this->statements_cache[$file_path]; + } + + return null; + } + + /** + * @param list $stmts + */ + public function saveStatementsToCache(string $file_path, string $file_content_hash, array $stmts, bool $touch_only): void + { + $this->statements_cache[$file_path] = $stmts; + $this->statements_cache_time[$file_path] = microtime(true); + $this->file_content_hash[$file_path] = $file_content_hash; + } + + public function loadExistingFileContentsFromCache(string $file_path): ?string + { + if (isset($this->file_contents_cache[$file_path])) { + return $this->file_contents_cache[$file_path]; + } + + return null; + } + + public function cacheFileContents(string $file_path, string $file_contents): void + { + $this->file_contents_cache[$file_path] = $file_contents; + } + + public function deleteOldParserCaches(float $time_before): int + { + $this->existing_file_content_hashes = null; + $this->new_file_content_hashes = []; + + $this->file_contents_cache = []; + $this->file_content_hash = []; + $this->statements_cache = []; + $this->statements_cache_time = []; + return 0; + } + + public function saveFileContentHashes(): void + { + } +} diff --git a/src/Psalm/Internal/LanguageServer/Provider/ProjectCacheProvider.php b/src/Psalm/Internal/LanguageServer/Provider/ProjectCacheProvider.php new file mode 100644 index 00000000000..f3611d371a7 --- /dev/null +++ b/src/Psalm/Internal/LanguageServer/Provider/ProjectCacheProvider.php @@ -0,0 +1,38 @@ +last_run; + } + + public function processSuccessfulRun(float $start_time, string $psalm_version): void + { + $this->last_run = (int) $start_time; + } + + public function canDiffFiles(): bool + { + return $this->last_run > 0; + } + + public function hasLockfileChanged(): bool + { + return false; + } + + public function updateComposerLockHash(): void + { + } +} From 221eecf2e387b01477aa2949d8ec418a28adbf29 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Thu, 2 Mar 2023 18:57:27 +0000 Subject: [PATCH 73/78] remove options to clear and boot cache --- src/Psalm/Internal/Cli/LanguageServer.php | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/src/Psalm/Internal/Cli/LanguageServer.php b/src/Psalm/Internal/Cli/LanguageServer.php index 838d5ffb49b..50c4b653535 100644 --- a/src/Psalm/Internal/Cli/LanguageServer.php +++ b/src/Psalm/Internal/Cli/LanguageServer.php @@ -71,8 +71,6 @@ public static function run(array $argv): void ]; $valid_long_options = [ - 'clear-cache', - 'clear-cache-on-boot', 'config:', 'find-dead-code', 'help', @@ -173,13 +171,6 @@ static function (string $arg) use ($valid_long_options): void { --find-dead-code Look for dead code - --clear-cache - Clears all cache files that the language server uses for this specific project (exits after) - - --clear-cache-on-boot - Clears all cache files that the language server uses for this specific project on boot - (does not exit) - --use-ini-defaults Use PHP-provided ini defaults for memory and error display @@ -320,17 +311,8 @@ static function (string $arg) use ($valid_long_options): void { $config->setServerMode(); - if (isset($options['clear-cache']) || isset($options['clear-cache-on-boot'])) { - $cache_directory = $config->getCacheDirectory(); - - if ($cache_directory !== null) { - Config::removeCacheDirectory($cache_directory); - } - if (!isset($options['clear-cache-on-boot'])) { - echo 'Cache directory deleted' . PHP_EOL; - exit; - } - } + //Theres no cache in LSP land + $config->cache_directory = null; if (isset($options['use-baseline']) && is_string($options['use-baseline'])) { $clientConfiguration->baseline = $options['use-baseline']; From fc4c98050e63dd09f9a671d26fdfda5d32c600b9 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Thu, 2 Mar 2023 19:02:56 +0000 Subject: [PATCH 74/78] cleanup --- src/Psalm/Internal/Cli/LanguageServer.php | 3 +-- src/Psalm/Internal/LanguageServer/LanguageServer.php | 2 +- .../LanguageServer/Provider/FileReferenceCacheProvider.php | 2 ++ .../Internal/LanguageServer/Provider/ProjectCacheProvider.php | 3 +++ 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Internal/Cli/LanguageServer.php b/src/Psalm/Internal/Cli/LanguageServer.php index 50c4b653535..c89abb4b861 100644 --- a/src/Psalm/Internal/Cli/LanguageServer.php +++ b/src/Psalm/Internal/Cli/LanguageServer.php @@ -3,7 +3,6 @@ namespace Psalm\Internal\Cli; use LanguageServerProtocol\MessageType; -use Psalm\Config; use Psalm\Internal\CliUtils; use Psalm\Internal\ErrorHandler; use Psalm\Internal\Fork\PsalmRestarter; @@ -379,6 +378,6 @@ static function (string $arg) use ($valid_long_options): void { $clientConfiguration->TCPServerAddress = $options['tcp'] ?? null; $clientConfiguration->TCPServerMode = isset($options['tcp-server']); - LanguageServerLanguageServer::run($config, $clientConfiguration, $current_dir); + LanguageServerLanguageServer::run($config, $clientConfiguration; } } diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index b24a74ec6bb..e0db1c1fe6f 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -240,7 +240,7 @@ function (): void { /** * Start the Server */ - public static function run(Config $config, ClientConfiguration $clientConfiguration, string $base_dir): void + public static function run(Config $config, ClientConfiguration $clientConfiguration): void { $progress = new Progress(); diff --git a/src/Psalm/Internal/LanguageServer/Provider/FileReferenceCacheProvider.php b/src/Psalm/Internal/LanguageServer/Provider/FileReferenceCacheProvider.php index 0d8e02fc401..79d54534448 100644 --- a/src/Psalm/Internal/LanguageServer/Provider/FileReferenceCacheProvider.php +++ b/src/Psalm/Internal/LanguageServer/Provider/FileReferenceCacheProvider.php @@ -8,6 +8,8 @@ /** * Used to determine which files reference other files, necessary for using the --diff * option from the command line. + * + * @internal */ class FileReferenceCacheProvider extends InternalFileReferenceCacheProvider { diff --git a/src/Psalm/Internal/LanguageServer/Provider/ProjectCacheProvider.php b/src/Psalm/Internal/LanguageServer/Provider/ProjectCacheProvider.php index f3611d371a7..ed210fa0ae2 100644 --- a/src/Psalm/Internal/LanguageServer/Provider/ProjectCacheProvider.php +++ b/src/Psalm/Internal/LanguageServer/Provider/ProjectCacheProvider.php @@ -4,6 +4,9 @@ use Psalm\Internal\Provider\ProjectCacheProvider as PsalmProjectCacheProvider; +/** + * @internal + */ class ProjectCacheProvider extends PsalmProjectCacheProvider { private int $last_run = 0; From 7760c399cc40fc3ee0ac5c320947b12710dd6d83 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Thu, 2 Mar 2023 19:05:14 +0000 Subject: [PATCH 75/78] syntax --- src/Psalm/Internal/Cli/LanguageServer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Cli/LanguageServer.php b/src/Psalm/Internal/Cli/LanguageServer.php index c89abb4b861..cc39db4f0a1 100644 --- a/src/Psalm/Internal/Cli/LanguageServer.php +++ b/src/Psalm/Internal/Cli/LanguageServer.php @@ -378,6 +378,6 @@ static function (string $arg) use ($valid_long_options): void { $clientConfiguration->TCPServerAddress = $options['tcp'] ?? null; $clientConfiguration->TCPServerMode = isset($options['tcp-server']); - LanguageServerLanguageServer::run($config, $clientConfiguration; + LanguageServerLanguageServer::run($config, $clientConfiguration); } } From 97eae625e9a33422077825f98eaee99404a9a186 Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Thu, 2 Mar 2023 19:12:29 +0000 Subject: [PATCH 76/78] fixes --- .../Provider/ClassLikeStorageCacheProvider.php | 7 +++++-- .../Provider/ParserCacheProvider.php | 15 +++++++++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/Psalm/Internal/LanguageServer/Provider/ClassLikeStorageCacheProvider.php b/src/Psalm/Internal/LanguageServer/Provider/ClassLikeStorageCacheProvider.php index 88c0723480b..f1925fe7904 100644 --- a/src/Psalm/Internal/LanguageServer/Provider/ClassLikeStorageCacheProvider.php +++ b/src/Psalm/Internal/LanguageServer/Provider/ClassLikeStorageCacheProvider.php @@ -26,8 +26,11 @@ public function writeToCache(ClassLikeStorage $storage, ?string $file_path, ?str $this->cache[$fq_classlike_name_lc] = $storage; } - public function getLatestFromCache(string $fq_classlike_name_lc, ?string $file_path, ?string $file_contents): ClassLikeStorage - { + public function getLatestFromCache( + string $fq_classlike_name_lc, + ?string $file_path, + ?string $file_contents + ): ClassLikeStorage { $cached_value = $this->loadFromCache($fq_classlike_name_lc); if (!$cached_value) { diff --git a/src/Psalm/Internal/LanguageServer/Provider/ParserCacheProvider.php b/src/Psalm/Internal/LanguageServer/Provider/ParserCacheProvider.php index 07218dce5e9..0b30a2dda57 100644 --- a/src/Psalm/Internal/LanguageServer/Provider/ParserCacheProvider.php +++ b/src/Psalm/Internal/LanguageServer/Provider/ParserCacheProvider.php @@ -36,8 +36,11 @@ public function __construct() { } - public function loadStatementsFromCache(string $file_path, int $file_modified_time, string $file_content_hash): ?array - { + public function loadStatementsFromCache( + string $file_path, + int $file_modified_time, + string $file_content_hash + ): ?array { if (isset($this->statements_cache[$file_path]) && $this->statements_cache_time[$file_path] >= $file_modified_time && $this->file_content_hash[$file_path] === $file_content_hash @@ -63,8 +66,12 @@ public function loadExistingStatementsFromCache(string $file_path): ?array /** * @param list $stmts */ - public function saveStatementsToCache(string $file_path, string $file_content_hash, array $stmts, bool $touch_only): void - { + public function saveStatementsToCache( + string $file_path, + string $file_content_hash, + array $stmts, + bool $touch_only + ): void { $this->statements_cache[$file_path] = $stmts; $this->statements_cache_time[$file_path] = microtime(true); $this->file_content_hash[$file_path] = $file_content_hash; From f908b9ad855f96881b518a5bc328f2a128affd1e Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Fri, 3 Mar 2023 07:20:41 +0000 Subject: [PATCH 77/78] fix failing test --- src/Psalm/Codebase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index 915b45399d2..d6caa46d1da 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -1864,7 +1864,7 @@ public function getCompletionItemsForClassishThing( $method_storage->description, (string)$method_storage->visibility, $method_storage->cased_name, - $method_storage->cased_name . (count($method_storage->params) !== 0 ? '($0)' : '()'), + $method_storage->cased_name, null, null, new Command('Trigger parameter hints', 'editor.action.triggerParameterHints'), From 3271b1b8f37ca070f7ec6c9aa82f5845d2a1f58a Mon Sep 17 00:00:00 2001 From: Andrew Nagy Date: Fri, 3 Mar 2023 08:11:10 +0000 Subject: [PATCH 78/78] add experimental in memory option --- src/Psalm/Internal/Cli/LanguageServer.php | 26 ++++++++-- .../LanguageServer/LanguageServer.php | 51 +++++++++++++------ 2 files changed, 57 insertions(+), 20 deletions(-) diff --git a/src/Psalm/Internal/Cli/LanguageServer.php b/src/Psalm/Internal/Cli/LanguageServer.php index cc39db4f0a1..429144b6808 100644 --- a/src/Psalm/Internal/Cli/LanguageServer.php +++ b/src/Psalm/Internal/Cli/LanguageServer.php @@ -3,6 +3,7 @@ namespace Psalm\Internal\Cli; use LanguageServerProtocol\MessageType; +use Psalm\Config; use Psalm\Internal\CliUtils; use Psalm\Internal\ErrorHandler; use Psalm\Internal\Fork\PsalmRestarter; @@ -87,6 +88,7 @@ public static function run(array $argv): void 'enable-provide-signature-help::', 'enable-provide-definition::', 'show-diagnostic-warnings::', + 'in-memory::', 'disable-xdebug::', 'on-change-debounce-ms::', 'use-extended-diagnostic-codes', @@ -216,6 +218,9 @@ static function (string $arg) use ($valid_long_options): void { --disable-xdebug[=BOOL] Disable xdebug for performance reasons. Enable for debugging + --in-memory[=BOOL] + Use in-memory mode. Default is false. Experimental. + --verbose Will send log messages to the client with information. @@ -275,12 +280,12 @@ static function (string $arg) use ($valid_long_options): void { 'blackfire', ]); - $diableXdebug = !isset($options['disable-xdebug']) + $disableXdebug = !isset($options['disable-xdebug']) || !is_string($options['disable-xdebug']) || strtolower($options['disable-xdebug']) !== 'false'; // If Xdebug is enabled, restart without it based on cli - if ($diableXdebug) { + if ($disableXdebug) { $ini_handler->check(); } @@ -310,8 +315,19 @@ static function (string $arg) use ($valid_long_options): void { $config->setServerMode(); - //Theres no cache in LSP land - $config->cache_directory = null; + $inMemory = isset($options['in-memory']) && + is_string($options['in-memory']) && + strtolower($options['in-memory']) === 'true'; + + if ($inMemory) { + $config->cache_directory = null; + } else { + $cache_directory = $config->getCacheDirectory(); + + if ($cache_directory !== null) { + Config::removeCacheDirectory($cache_directory); + } + } if (isset($options['use-baseline']) && is_string($options['use-baseline'])) { $clientConfiguration->baseline = $options['use-baseline']; @@ -378,6 +394,6 @@ static function (string $arg) use ($valid_long_options): void { $clientConfiguration->TCPServerAddress = $options['tcp'] ?? null; $clientConfiguration->TCPServerMode = isset($options['tcp-server']); - LanguageServerLanguageServer::run($config, $clientConfiguration); + LanguageServerLanguageServer::run($config, $clientConfiguration, $current_dir, $inMemory); } } diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index e0db1c1fe6f..2f99b784cb6 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -40,14 +40,20 @@ use Psalm\ErrorBaseline; use Psalm\Internal\Analyzer\IssueData; use Psalm\Internal\Analyzer\ProjectAnalyzer; -use Psalm\Internal\LanguageServer\Provider\ClassLikeStorageCacheProvider; -use Psalm\Internal\LanguageServer\Provider\FileReferenceCacheProvider; -use Psalm\Internal\LanguageServer\Provider\FileStorageCacheProvider; -use Psalm\Internal\LanguageServer\Provider\ParserCacheProvider; -use Psalm\Internal\LanguageServer\Provider\ProjectCacheProvider; +use Psalm\Internal\Composer; +use Psalm\Internal\LanguageServer\Provider\ClassLikeStorageCacheProvider as InMemoryClassLikeStorageCacheProvider; +use Psalm\Internal\LanguageServer\Provider\FileReferenceCacheProvider as InMemoryFileReferenceCacheProvider; +use Psalm\Internal\LanguageServer\Provider\FileStorageCacheProvider as InMemoryFileStorageCacheProvider; +use Psalm\Internal\LanguageServer\Provider\ParserCacheProvider as InMemoryParserCacheProvider; +use Psalm\Internal\LanguageServer\Provider\ProjectCacheProvider as InMemoryProjectCacheProvider; use Psalm\Internal\LanguageServer\Server\TextDocument as ServerTextDocument; use Psalm\Internal\LanguageServer\Server\Workspace as ServerWorkspace; +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\IssueBuffer; use Throwable; @@ -240,18 +246,33 @@ function (): void { /** * Start the Server */ - public static function run(Config $config, ClientConfiguration $clientConfiguration): void - { + public static function run( + Config $config, + ClientConfiguration $clientConfiguration, + string $base_dir, + bool $inMemory = false + ): void { $progress = new Progress(); - $providers = new Providers( - new FileProvider, - new ParserCacheProvider, - new FileStorageCacheProvider, - new ClassLikeStorageCacheProvider, - new FileReferenceCacheProvider($config), - new ProjectCacheProvider, - ); + if ($inMemory) { + $providers = new Providers( + new FileProvider, + new InMemoryParserCacheProvider, + new InMemoryFileStorageCacheProvider, + new InMemoryClassLikeStorageCacheProvider, + new InMemoryFileReferenceCacheProvider($config), + new InMemoryProjectCacheProvider, + ); + } else { + $providers = new Providers( + new FileProvider, + new ParserCacheProvider($config), + new FileStorageCacheProvider($config), + new ClassLikeStorageCacheProvider($config), + new FileReferenceCacheProvider($config), + new ProjectCacheProvider(Composer::getLockFilePath($base_dir)), + ); + } $codebase = new Codebase( $config,