mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-11 03:13:51 +00:00
fix(dart): wait for analyzer readiness signal before returning from start()
DartLanguageServer registered handlers for $/analyzerStatus and experimental/serverStatus but both were no-ops, so start() returned as soon as initialized was sent instead of waiting for either signal, unlike every other backend (pyright, basedpyright, rust-analyzer, clojure-lsp, jedi, omnisharp, clangd) that receives the same notifications. A request issued right after activation could return before the workspace scan finished. Gate on a threading.Event set by either notification, bounded by 60s, mirroring pyright_server.py's analysis_complete pattern.
This commit is contained in:
1 parent
813fd98f4f
commit
e1322a3b99
3 files changed
+137
-2
No files matched your search
@@ -26,6 +26,12 @@ Status of the `main` branch. Changes prior to the next official version change w
|
||||
- Fix: Exceptions raised during `LanguageServerManager.start` did not stop the language server subprocess if it was
|
||||
already started (#1949)
|
||||
- Fix: Dart's `$/analyzerStatus` notifications were logged as unhandled-method warnings during analysis (#1855)
|
||||
- Fix: `DartLanguageServer._start_server` discarded both `$/analyzerStatus` and
|
||||
`experimental/serverStatus`, the two notifications the Dart analysis server sends to report
|
||||
indexing progress, and returned as soon as `initialized` was sent instead of waiting for either
|
||||
one; a request issued right after activation (`find_symbol`, `find_referencing_symbols`) could
|
||||
return before the workspace scan finished. Serena now waits (bounded by 60s) for either signal to
|
||||
report completion, matching the pattern already used for pyright, basedpyright and rust-analyzer
|
||||
- Fix: clojure-lsp was not told that Serena sends `workspace/didChangeWatchedFiles`, so changes made
|
||||
outside Serena's own edit tools (a git checkout, another editor, a build step) need not invalidate
|
||||
its analysis; symbol queries could then answer from a stale index, e.g. `find_symbol` returning a
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
from collections.abc import Hashable
|
||||
|
||||
from overrides import override
|
||||
@@ -53,6 +54,11 @@ class DartLanguageServer(SolidLanguageServer):
|
||||
(default: the bundled Serena version).
|
||||
"""
|
||||
|
||||
# Mirrors pyright_server.py / basedpyright_server.py: a bounded wait for the server's own
|
||||
# readiness signal, not an indefinite one, so a Dart analysis server that never reports
|
||||
# quiescence (or a protocol variant that stops sending these notifications) cannot hang startup.
|
||||
_TIMEOUT_FOR_INITIAL_ANALYSIS = 60.0
|
||||
|
||||
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings) -> None:
|
||||
"""
|
||||
Creates a DartServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
|
||||
@@ -61,6 +67,9 @@ class DartLanguageServer(SolidLanguageServer):
|
||||
super().__init__(
|
||||
config, repository_root_path, ProcessLaunchInfo(cmd=executable_path, cwd=repository_root_path), "dart", solidlsp_settings
|
||||
)
|
||||
# Set once the Dart analysis server reports it has finished its initial workspace scan,
|
||||
# via either notification it sends for this (see _start_server).
|
||||
self.analysis_complete = threading.Event()
|
||||
|
||||
@override
|
||||
def _document_symbols_cache_fingerprint(self) -> Hashable:
|
||||
@@ -181,8 +190,17 @@ class DartLanguageServer(SolidLanguageServer):
|
||||
def do_nothing(params: dict) -> None:
|
||||
return
|
||||
|
||||
def check_analyzer_status(params: dict) -> None:
|
||||
# Legacy signal: isAnalyzing flips to False once the initial workspace scan finishes
|
||||
# (dart-lang/sdk lsp_analysis_server.dart AnalyzerStatusParams(isAnalyzing=...)).
|
||||
if params.get("isAnalyzing") is False:
|
||||
log.info("Received $/analyzerStatus with isAnalyzing=false")
|
||||
self.analysis_complete.set()
|
||||
|
||||
def check_experimental_status(params: dict) -> None:
|
||||
pass
|
||||
if params.get("quiescent") is True:
|
||||
log.info("Received experimental/serverStatus with quiescent=true")
|
||||
self.analysis_complete.set()
|
||||
|
||||
def window_log_message(msg: dict) -> None:
|
||||
log.info(f"LSP: window/logMessage: {msg}")
|
||||
@@ -192,7 +210,7 @@ class DartLanguageServer(SolidLanguageServer):
|
||||
self.server.on_notification("window/logMessage", window_log_message)
|
||||
self.server.on_request("workspace/executeClientCommand", execute_client_command_handler)
|
||||
self.server.on_notification("$/progress", do_nothing)
|
||||
self.server.on_notification("$/analyzerStatus", do_nothing)
|
||||
self.server.on_notification("$/analyzerStatus", check_analyzer_status)
|
||||
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
|
||||
self.server.on_notification("language/actionableNotification", do_nothing)
|
||||
self.server.on_notification("experimental/serverStatus", check_experimental_status)
|
||||
@@ -205,3 +223,10 @@ class DartLanguageServer(SolidLanguageServer):
|
||||
log.info(f"Received initialize response from dart-language-server: {init_response}")
|
||||
|
||||
self.server.notify.initialized({})
|
||||
|
||||
log.info(f"Waiting up to {self._TIMEOUT_FOR_INITIAL_ANALYSIS}s for dart-language-server to finish initial analysis...")
|
||||
if self.analysis_complete.wait(timeout=self._TIMEOUT_FOR_INITIAL_ANALYSIS):
|
||||
log.info("dart-language-server initial analysis complete, server ready")
|
||||
else:
|
||||
log.warning("Timeout waiting for dart-language-server analysis completion, proceeding anyway")
|
||||
self.analysis_complete.set()
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Regression test for Dart analyzer-status notification handling."""
|
||||
|
||||
import logging
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
@@ -61,6 +62,8 @@ def test_analyzer_status_notification_is_handled_after_startup(
|
||||
"_create_language_server_interface",
|
||||
return_value=server_interface,
|
||||
),
|
||||
# No readiness signal is ever sent here, so keep the fallback wait short.
|
||||
patch.object(DartLanguageServer, "_TIMEOUT_FOR_INITIAL_ANALYSIS", 0.05),
|
||||
):
|
||||
server = DartLanguageServer(
|
||||
LanguageServerConfig(ls_id=LanguageServerId.DART),
|
||||
@@ -73,3 +76,104 @@ def test_analyzer_status_notification_is_handled_after_startup(
|
||||
server_interface.receive_notification("$/analyzerStatus", {"isAnalyzing": True})
|
||||
|
||||
assert "Unhandled method '$/analyzerStatus'" not in caplog.messages
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("method", "params"),
|
||||
[
|
||||
("experimental/serverStatus", {"quiescent": True}),
|
||||
("$/analyzerStatus", {"isAnalyzing": False}),
|
||||
],
|
||||
)
|
||||
def test_start_waits_for_analysis_readiness_signal(
|
||||
tmp_path: Path,
|
||||
method: str,
|
||||
params: dict[str, Any],
|
||||
) -> None:
|
||||
"""`start()` must block until the Dart analysis server reports it has finished its
|
||||
initial workspace scan, instead of returning as soon as `initialized` is sent.
|
||||
"""
|
||||
settings = SolidLSPSettings(
|
||||
solidlsp_dir=str(tmp_path / "global"),
|
||||
project_data_path=str(tmp_path / "project"),
|
||||
ls_specific_settings={LanguageServerId.DART: {}},
|
||||
)
|
||||
server_interface = _FakeLanguageServerInterface()
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
DartLanguageServer,
|
||||
"_setup_runtime_dependencies",
|
||||
return_value=str(tmp_path / "dart-sdk"),
|
||||
),
|
||||
patch.object(
|
||||
DartLanguageServer,
|
||||
"_create_language_server_interface",
|
||||
return_value=server_interface,
|
||||
),
|
||||
patch.object(DartLanguageServer, "_TIMEOUT_FOR_INITIAL_ANALYSIS", 2.0, create=True),
|
||||
):
|
||||
server = DartLanguageServer(
|
||||
LanguageServerConfig(ls_id=LanguageServerId.DART),
|
||||
str(tmp_path),
|
||||
settings,
|
||||
)
|
||||
|
||||
start_thread = threading.Thread(target=server.start)
|
||||
start_thread.start()
|
||||
try:
|
||||
# No readiness signal has been sent yet: start() must still be blocked.
|
||||
start_thread.join(timeout=0.3)
|
||||
assert start_thread.is_alive(), "start() returned before any readiness signal was received"
|
||||
|
||||
server_interface.receive_notification(method, params)
|
||||
|
||||
start_thread.join(timeout=2.0)
|
||||
assert not start_thread.is_alive(), f"start() did not resume after {method} {params}"
|
||||
finally:
|
||||
if start_thread.is_alive():
|
||||
server_interface.receive_notification("experimental/serverStatus", {"quiescent": True})
|
||||
start_thread.join(timeout=2.0)
|
||||
|
||||
|
||||
def test_start_proceeds_after_readiness_timeout_if_no_signal_arrives(
|
||||
tmp_path: Path,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""If neither readiness notification ever arrives, start() must still return once
|
||||
`_TIMEOUT_FOR_INITIAL_ANALYSIS` elapses, rather than blocking forever.
|
||||
"""
|
||||
settings = SolidLSPSettings(
|
||||
solidlsp_dir=str(tmp_path / "global"),
|
||||
project_data_path=str(tmp_path / "project"),
|
||||
ls_specific_settings={LanguageServerId.DART: {}},
|
||||
)
|
||||
server_interface = _FakeLanguageServerInterface()
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
DartLanguageServer,
|
||||
"_setup_runtime_dependencies",
|
||||
return_value=str(tmp_path / "dart-sdk"),
|
||||
),
|
||||
patch.object(
|
||||
DartLanguageServer,
|
||||
"_create_language_server_interface",
|
||||
return_value=server_interface,
|
||||
),
|
||||
patch.object(DartLanguageServer, "_TIMEOUT_FOR_INITIAL_ANALYSIS", 0.2, create=True),
|
||||
):
|
||||
server = DartLanguageServer(
|
||||
LanguageServerConfig(ls_id=LanguageServerId.DART),
|
||||
str(tmp_path),
|
||||
settings,
|
||||
)
|
||||
|
||||
start_thread = threading.Thread(target=server.start)
|
||||
with caplog.at_level(logging.WARNING):
|
||||
start_thread.start()
|
||||
start_thread.join(timeout=2.0)
|
||||
|
||||
assert not start_thread.is_alive(), "start() never returned despite the readiness timeout elapsing"
|
||||
assert any("Timeout waiting for dart-language-server analysis completion" in m for m in caplog.messages)
|
||||
assert server.analysis_complete.is_set()
|
||||
Reference in new issue
Block a user