Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 4 additions & 4 deletions slither_lsp/app/feature_analyses/slither_diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand All @@ -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:
Expand Down
6 changes: 3 additions & 3 deletions slither_lsp/app/logging/lsp_handler.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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))
Original file line number Diff line number Diff line change
@@ -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


Expand Down
2 changes: 1 addition & 1 deletion slither_lsp/app/request_handlers/analysis/get_version.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down
Original file line number Diff line number Diff line change
@@ -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):
Expand Down
36 changes: 22 additions & 14 deletions slither_lsp/app/slither_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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.
Expand All @@ -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)

Expand Down
42 changes: 18 additions & 24 deletions tests/integration/test_slither_diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand All @@ -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()
Expand Down Expand Up @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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()