diff --git a/pyproject.toml b/pyproject.toml index 151c7c4..bbfa16a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ readme = "README.md" dependencies = [ "slither-analyzer>=0.10.2", "semantic-version>=2.10.0", - "pygls>=1.3.0", + "pygls>=2.0.0", ] classifiers = [ "License :: OSI Approved :: GNU Affero General Public License v3", diff --git a/slither_lsp/app/feature_analyses/slither_diagnostics.py b/slither_lsp/app/feature_analyses/slither_diagnostics.py index 7bd40a4..9876933 100644 --- a/slither_lsp/app/feature_analyses/slither_diagnostics.py +++ b/slither_lsp/app/feature_analyses/slither_diagnostics.py @@ -114,9 +114,7 @@ def update( # Loop for each diagnostic and broadcast all of them. for diagnostic_params in self.diagnostics.values(): - self.context.publish_diagnostics( - diagnostic_params.uri, diagnostics=diagnostic_params.diagnostics - ) + self.context.text_document_publish_diagnostics(diagnostic_params) def _clear_single(self, file_uri: str, clear_from_lookup: bool = False) -> None: """ @@ -129,7 +127,9 @@ def _clear_single(self, file_uri: str, clear_from_lookup: bool = False) -> None: :return: None """ # Send empty diagnostics for this file to the client. - self.context.publish_diagnostics(file_uri, diagnostics=[]) + self.context.text_document_publish_diagnostics( + lsp.PublishDiagnosticsParams(uri=file_uri, diagnostics=[]) + ) # Optionally clear this item from the diagnostic lookup if clear_from_lookup: diff --git a/slither_lsp/app/logging/lsp_handler.py b/slither_lsp/app/logging/lsp_handler.py index 8bc7cb1..a1a6b86 100644 --- a/slither_lsp/app/logging/lsp_handler.py +++ b/slither_lsp/app/logging/lsp_handler.py @@ -1,7 +1,7 @@ from logging import DEBUG, ERROR, FATAL, INFO, NOTSET, WARNING, Handler, LogRecord -from lsprotocol.types import MessageType -from pygls.server import LanguageServer +from lsprotocol.types import LogMessageParams, MessageType +from pygls.lsp.server import LanguageServer _level_to_type = { FATAL: MessageType.Error, @@ -25,4 +25,4 @@ def __init__(self, server: LanguageServer): def emit(self, record: LogRecord): msg = self.format(record) msg_type = _level_to_type[record.levelno] - self.server.show_message_log(msg, msg_type=msg_type) + self.server.window_log_message(LogMessageParams(type=msg_type, message=msg)) diff --git a/slither_lsp/app/request_handlers/analysis/get_detector_list.py b/slither_lsp/app/request_handlers/analysis/get_detector_list.py index f0d9fa8..8d1bfbc 100644 --- a/slither_lsp/app/request_handlers/analysis/get_detector_list.py +++ b/slither_lsp/app/request_handlers/analysis/get_detector_list.py @@ -1,4 +1,4 @@ -from pygls.server import LanguageServer +from pygls.lsp.server import LanguageServer from slither.__main__ import get_detectors_and_printers, output_detectors_json diff --git a/slither_lsp/app/request_handlers/analysis/get_version.py b/slither_lsp/app/request_handlers/analysis/get_version.py index f7b6987..0529d4e 100644 --- a/slither_lsp/app/request_handlers/analysis/get_version.py +++ b/slither_lsp/app/request_handlers/analysis/get_version.py @@ -1,6 +1,6 @@ from importlib.metadata import version as pkg_version -from pygls.server import LanguageServer +from pygls.lsp.server import LanguageServer def get_version(ls: LanguageServer, params): diff --git a/slither_lsp/app/request_handlers/compilation/get_command_line_args.py b/slither_lsp/app/request_handlers/compilation/get_command_line_args.py index 5e12e26..d62ed98 100644 --- a/slither_lsp/app/request_handlers/compilation/get_command_line_args.py +++ b/slither_lsp/app/request_handlers/compilation/get_command_line_args.py @@ -1,7 +1,7 @@ from argparse import ArgumentParser from crytic_compile.cryticparser.cryticparser import init as crytic_parser_init -from pygls.server import LanguageServer +from pygls.lsp.server import LanguageServer def get_command_line_args(ls: LanguageServer, params): diff --git a/slither_lsp/app/slither_server.py b/slither_lsp/app/slither_server.py index 1fc32d4..7d117a8 100644 --- a/slither_lsp/app/slither_server.py +++ b/slither_lsp/app/slither_server.py @@ -8,8 +8,8 @@ import lsprotocol.types as lsp from crytic_compile.crytic_compile import CryticCompile from pygls.lsp import METHOD_TO_OPTIONS +from pygls.lsp.server import LanguageServer from pygls.protocol import LanguageServerProtocol -from pygls.server import LanguageServer from slither import Slither from slither.__main__ import ( _process as process_detectors_and_printers, @@ -98,7 +98,9 @@ def on_initialize(ls: SlitherServer, params): @self.feature(lsp.INITIALIZED) def on_initialized(ls: SlitherServer, params): - ls.show_message("slither-lsp initialized", lsp.MessageType.Debug) + ls.window_show_message( + lsp.ShowMessageParams(type=lsp.MessageType.Debug, message="slither-lsp initialized") + ) @self.thread() @self.feature(lsp.WORKSPACE_DID_CHANGE_WORKSPACE_FOLDERS) @@ -160,9 +162,11 @@ def _on_analyze(self, params: AnalysisRequestParams): path = uri_to_fs_path(uri) workspace_name = split(path)[1] if self.workspace_in_progress[uri].locked(): - self.show_message( - f"Analysis for {workspace_name} is already in progress", - lsp.MessageType.Warning, + self.window_show_message( + lsp.ShowMessageParams( + type=lsp.MessageType.Warning, + message=f"Analysis for {workspace_name} is already in progress", + ) ) continue self.queue_compile_workspace(uri) @@ -177,9 +181,11 @@ def queue_compile_workspace(self, uri: str): def do_compile(): detector_classes, _ = get_detectors_and_printers() with self.workspace_in_progress[uri]: - self.show_message( - f"Compilation for {workspace_name} has started", - lsp.MessageType.Info, + self.window_show_message( + lsp.ShowMessageParams( + type=lsp.MessageType.Info, + message=f"Compilation for {workspace_name} has started", + ) ) try: compilation = CryticCompile(path) @@ -197,9 +203,11 @@ def do_compile(): detector_results = None analyzed_successfully = True analysis_error = None - self.show_message( - f"Compilation for {workspace_name} has completed successfully", - lsp.MessageType.Info, + self.window_show_message( + lsp.ShowMessageParams( + type=lsp.MessageType.Info, + message=f"Compilation for {workspace_name} has completed successfully", + ) ) except Exception as err: # If we encounter an error, set our status. @@ -208,9 +216,9 @@ def do_compile(): analyzed_successfully = False analysis_error = err detector_results = None - self.show_message( - f"Compilation for {workspace_name} has failed. See log for details.", - lsp.MessageType.Info, + msg = f"Compilation for {workspace_name} has failed. See log for details." + self.window_show_message( + lsp.ShowMessageParams(type=lsp.MessageType.Info, message=msg) ) self._logger.log(logging.ERROR, "Compiling %s has failed: %s", path, err) diff --git a/tests/integration/test_slither_diagnostics.py b/tests/integration/test_slither_diagnostics.py index 8806b01..30bd5c9 100644 --- a/tests/integration/test_slither_diagnostics.py +++ b/tests/integration/test_slither_diagnostics.py @@ -17,7 +17,7 @@ def make_mock_server(): """Create a mock SlitherServer for testing.""" server = MagicMock() - server.publish_diagnostics = MagicMock() + server.text_document_publish_diagnostics = MagicMock() return server @@ -85,10 +85,11 @@ def test_publishes_diagnostics_for_detector_results(self): diagnostics.update([analysis], settings) # Should publish diagnostics - server.publish_diagnostics.assert_called() - call_args = server.publish_diagnostics.call_args - assert "test.sol" in call_args[0][0] # URI contains filename - assert len(call_args[1]["diagnostics"]) == 1 + server.text_document_publish_diagnostics.assert_called() + call_args = server.text_document_publish_diagnostics.call_args + params = call_args[0][0] # PublishDiagnosticsParams object + assert "test.sol" in params.uri # URI contains filename + assert len(params.diagnostics) == 1 def test_filters_hidden_checks(self): server = make_mock_server() @@ -110,10 +111,8 @@ def test_filters_hidden_checks(self): diagnostics.update([analysis], settings) # Should not publish any diagnostics (check is hidden) - # publish_diagnostics is called but with empty list - if server.publish_diagnostics.called: - call_args = server.publish_diagnostics.call_args - assert len(call_args[1]["diagnostics"]) == 0 + # text_document_publish_diagnostics is not called when there are no diagnostics + assert not server.text_document_publish_diagnostics.called def test_disabled_detectors_produces_no_diagnostics(self): server = make_mock_server() @@ -132,14 +131,7 @@ def test_disabled_detectors_produces_no_diagnostics(self): diagnostics.update([analysis], settings) # Should not produce any diagnostics when disabled - if server.publish_diagnostics.called: - call_args = server.publish_diagnostics.call_args - # Either not called or called with empty diagnostics - assert ( - not call_args - or "diagnostics" not in call_args[1] - or len(call_args[1].get("diagnostics", [])) == 0 - ) + assert not server.text_document_publish_diagnostics.called def test_skips_results_without_source_mapping(self): server = make_mock_server() @@ -201,8 +193,9 @@ def test_diagnostic_message_format(self): diagnostics.update([analysis], settings) - call_args = server.publish_diagnostics.call_args - diag = call_args[1]["diagnostics"][0] + call_args = server.text_document_publish_diagnostics.call_args + params = call_args[0][0] # PublishDiagnosticsParams object + diag = params.diagnostics[0] assert "[MEDIUM]" in diag.message assert "Low-level call" in diag.message assert diag.code == "unchecked-lowlevel" @@ -226,8 +219,9 @@ def test_diagnostic_range_calculation(self): diagnostics.update([analysis], settings) - call_args = server.publish_diagnostics.call_args - diag = call_args[1]["diagnostics"][0] + call_args = server.text_document_publish_diagnostics.call_args + params = call_args[0][0] # PublishDiagnosticsParams object + diag = params.diagnostics[0] # Range should be 0-indexed assert diag.range.start.line == 9 # Line 10 -> 9 @@ -259,7 +253,7 @@ def test_multiple_files_get_separate_diagnostics(self): diagnostics.update([analysis], settings) # Should be called twice (once per file) - assert server.publish_diagnostics.call_count == 2 + assert server.text_document_publish_diagnostics.call_count == 2 def test_clears_old_diagnostics_on_update(self): server = make_mock_server() @@ -277,10 +271,10 @@ def test_clears_old_diagnostics_on_update(self): diagnostics_obj.update([make_analysis_result([result])], settings) # Reset mock - server.publish_diagnostics.reset_mock() + server.text_document_publish_diagnostics.reset_mock() # Second update with no results diagnostics_obj.update([make_analysis_result([])], settings) # Should clear diagnostics for the file that no longer has issues - server.publish_diagnostics.assert_called() + server.text_document_publish_diagnostics.assert_called()