fix(lsp): 拉取诊断客户端不再重复收到 push 诊断
initialize 记录 textDocument.diagnostic 能力;客户端支持 pull 时不再发送 publishDiagnostics,避免 Problems 面板重复。gate2 增加溢出诊断断言:仅 1 条、range 覆盖完整字面量、错误恢复后函数语义 token 仍在,并验证 pull 客户端不会同时收到 push。
This commit is contained in:
1 parent
9d298b5ae9
commit
86cced94dc
2 files changed
+87
No files matched your search
@@ -33,6 +33,10 @@ define LSP_CANCEL_CAP = 64
|
||||
_cancel_int: list[int]* = None
|
||||
_cancel_str: list[str]* = None
|
||||
|
||||
# 1 when the client advertises pull diagnostics (`textDocument.diagnostic`). Pushing
|
||||
# publishDiagnostics as well would show every problem twice in the Problems panel.
|
||||
_pull_diagnostics: int = 0
|
||||
|
||||
|
||||
def _CancelAdd(pool: memhub.MemManager*, id_node: json.JsonValue*):
|
||||
if id_node is None:
|
||||
@@ -146,9 +150,24 @@ def _BuildInitializeResult(pool: memhub.MemManager*) -> json.JsonValue*:
|
||||
return result
|
||||
|
||||
|
||||
def _ClientSupportsPull(params: json.JsonValue*) -> int:
|
||||
"""Whether initialize params advertise textDocument/diagnostic (pull diagnostics)."""
|
||||
if params is None:
|
||||
return 0
|
||||
caps: json.JsonValue* = params["capabilities"]
|
||||
if caps is None:
|
||||
return 0
|
||||
td: json.JsonValue* = caps["textDocument"]
|
||||
if td is None:
|
||||
return 0
|
||||
return 1 if td["diagnostic"] is not None else 0
|
||||
|
||||
|
||||
def _HandleInitialize(pool: memhub.MemManager*, tr: LspTransport.LspTransport*,
|
||||
ws: LspWorkspace.LspWorkspace*, id_node: json.JsonValue*,
|
||||
params: json.JsonValue*) -> int:
|
||||
global _pull_diagnostics
|
||||
_pull_diagnostics = _ClientSupportsPull(params)
|
||||
root_uri: str = None
|
||||
if params is not None:
|
||||
root_uri = LspRpc.GetStringField(params, "rootUri")
|
||||
@@ -188,6 +207,8 @@ def _HandleDidOpen(pool: memhub.MemManager*, tr: LspTransport.LspTransport*,
|
||||
if docs.Open(uri, text, version) is None:
|
||||
LspTransport.StderrWrite("viperc-lsp: didOpen failed to store document\n")
|
||||
return -1
|
||||
if _pull_diagnostics != 0:
|
||||
return 0
|
||||
return LspDiagnostics.Publish(pool, tr, docs, uri)
|
||||
|
||||
|
||||
@@ -227,6 +248,8 @@ def _HandleDidChange(pool: memhub.MemManager*, tr: LspTransport.LspTransport*,
|
||||
LspTransport.StderrWrite("viperc-lsp: didChange failed to apply a content change\n")
|
||||
return -1
|
||||
i += 1
|
||||
if _pull_diagnostics != 0:
|
||||
return 0
|
||||
return LspDiagnostics.Publish(pool, tr, docs, uri)
|
||||
|
||||
|
||||
@@ -239,6 +262,8 @@ def _HandleDidClose(pool: memhub.MemManager*, tr: LspTransport.LspTransport*,
|
||||
if uri is None:
|
||||
return -1
|
||||
docs.Close(uri)
|
||||
if _pull_diagnostics != 0:
|
||||
return 0
|
||||
return LspDiagnostics.PublishEmpty(pool, tr, uri)
|
||||
|
||||
|
||||
@@ -250,6 +275,8 @@ def _HandleDidSave(pool: memhub.MemManager*, tr: LspTransport.LspTransport*,
|
||||
uri: str = LspRpc.GetStringField(td, "uri")
|
||||
if uri is None:
|
||||
return -1
|
||||
if _pull_diagnostics != 0:
|
||||
return 0
|
||||
return LspDiagnostics.Publish(pool, tr, docs, uri)
|
||||
|
||||
|
||||
|
||||
@@ -395,12 +395,72 @@ def gate2(exe: str) -> int:
|
||||
if not isinstance(diagnostics, list) or len(diagnostics) != 0:
|
||||
raise AssertionError(f"didClose did not clear diagnostics: {publish}")
|
||||
|
||||
# Lexer overflow: exactly one diagnostic, the range covers the whole literal, and the rest of
|
||||
# the file keeps its symbols/semantic tokens (recovery parsing).
|
||||
overflow = "def a(l: int):\n pass\n\ndef main():\n a(1024000000000000000000)\n"
|
||||
client.notify(
|
||||
"textDocument/didOpen",
|
||||
{"textDocument": {"uri": uri, "languageId": "viper", "version": 3, "text": overflow}},
|
||||
)
|
||||
publish = client.wait_notification("textDocument/publishDiagnostics")
|
||||
diagnostics = publish.get("params", {}).get("diagnostics")
|
||||
if not isinstance(diagnostics, list) or len(diagnostics) != 1:
|
||||
raise AssertionError(f"expected exactly one overflow diagnostic: {publish}")
|
||||
overflow_diag = diagnostics[0]
|
||||
if overflow_diag["range"]["start"] != {"line": 4, "character": 6} or \
|
||||
overflow_diag["range"]["end"] != {"line": 4, "character": 28}:
|
||||
raise AssertionError(f"overflow range does not cover the literal: {overflow_diag}")
|
||||
if "1024000000000000000000" not in overflow_diag["message"]:
|
||||
raise AssertionError(f"overflow diagnostic lost the literal text: {overflow_diag}")
|
||||
sem = client.request(
|
||||
"textDocument/semanticTokens/full", {"textDocument": {"uri": uri}}, timeout=120.0
|
||||
).get("result")
|
||||
sem_tokens = _decode_tokens(sem.get("data", [])) if isinstance(sem, dict) else []
|
||||
legend = response.get("result", {}).get("capabilities", {}) \
|
||||
.get("semanticTokensProvider", {}).get("legend", {}).get("tokenTypes", [])
|
||||
function_type = legend.index("function")
|
||||
functions = {(t["line"], t["character"]) for t in sem_tokens if t["type"] == function_type}
|
||||
if (0, 4) not in functions or (3, 4) not in functions:
|
||||
raise AssertionError(f"semantic tokens lost after a lexer error: {sem_tokens}")
|
||||
client.notify("textDocument/didClose", {"textDocument": {"uri": uri}})
|
||||
client.wait_notification("textDocument/publishDiagnostics")
|
||||
|
||||
client.request("shutdown")
|
||||
code = client.close()
|
||||
if code != 0:
|
||||
raise AssertionError(f"exit code is {code}, expected 0")
|
||||
|
||||
# A pull-diagnostics client must not also receive publishDiagnostics (that shows every
|
||||
# problem twice in the Problems panel).
|
||||
pull_client = Client(exe)
|
||||
pull_client.request(
|
||||
"initialize",
|
||||
{"processId": os.getpid(), "rootUri": root.as_uri(),
|
||||
"capabilities": {"textDocument": {"diagnostic": {}}}},
|
||||
)
|
||||
pull_client.notify("initialized", {})
|
||||
pull_client.notify(
|
||||
"textDocument/didOpen",
|
||||
{"textDocument": {"uri": uri, "languageId": "viper", "version": 4, "text": overflow}},
|
||||
)
|
||||
time.sleep(0.4)
|
||||
pushed: list[dict[str, Any]] = []
|
||||
while not pull_client.reader.messages.empty():
|
||||
pushed.append(pull_client.reader.messages.get())
|
||||
if any(m.get("method") == "textDocument/publishDiagnostics" for m in pushed):
|
||||
raise AssertionError(f"pull client received duplicate push diagnostics: {pushed}")
|
||||
pulled = pull_client.request(
|
||||
"textDocument/diagnostic", {"textDocument": {"uri": uri}}, timeout=60.0
|
||||
).get("result")
|
||||
items = pulled.get("items", []) if isinstance(pulled, dict) else []
|
||||
if len(items) != 1:
|
||||
raise AssertionError(f"pull diagnostics returned {len(items)} items: {pulled}")
|
||||
pull_client.request("shutdown")
|
||||
pull_client.close()
|
||||
|
||||
print("diagnostics: OK")
|
||||
print("recovery: OK")
|
||||
print("overflow-diagnostics: OK")
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user