From dcdf22dac38b1650d4347c397e2d78f6e6eb2390 Mon Sep 17 00:00:00 2001 From: Henrik Jess Nielsen Date: Fri, 14 Aug 2026 17:44:11 +0200 Subject: [PATCH] fix(bicep): stop corrupting hover/definition/references/symbols/formatting/codeAction with completion injections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _ls_to_client called _inject_completions() on EVERY response with an id+result, not just textDocument/completion responses. pop_context() defaulted to {'type': 'unknown'} for any untracked id, which _inject_completions treats as 'inject module-name completions' — so list-shaped LS responses for definition/references/documentSymbol/formatting/codeAction were silently replaced or prefixed with the Bicep module catalog list instead of returning real Bicep LangServer results. Found via a full LSP-capability audit against production: definition, references, documentSymbol, formatting and codeAction all incorrectly returned module-completion items instead of real results. Fix: pop_context() now returns None when the response id wasn't recorded as a pending completion request, and _ls_to_client only calls _inject_completions() when a real completion context was found — every other response now passes through untouched. --- ilsp/bicep_lsp/proxy.py | 16 ++++++++++++---- tests/test_proxy.py | 5 +++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/ilsp/bicep_lsp/proxy.py b/ilsp/bicep_lsp/proxy.py index c618dc8..04555e8 100644 --- a/ilsp/bicep_lsp/proxy.py +++ b/ilsp/bicep_lsp/proxy.py @@ -187,8 +187,12 @@ class _ProxySession: return {"type": "unknown"} - def pop_context(self, msg_id) -> dict: - return self.pending.pop(msg_id, {"type": "unknown"}) + def pop_context(self, msg_id) -> dict | None: + """Return the tracked completion context for msg_id, or None if this + response id does not correspond to a textDocument/completion request + we recorded (e.g. hover/definition/references/... responses must never + be treated as completion responses).""" + return self.pending.pop(msg_id, None) # ── Completion injection ─────────────────────────────────────────────────────── @@ -316,10 +320,14 @@ def _ls_to_client( logger.debug("LS→Client: %d bytes", len(body)) try: msg = json.loads(body) - context: dict = {} + context = None if "id" in msg and "result" in msg: context = session.pop_context(msg["id"]) - out = _inject_completions(msg, context) + # Only rewrite responses that actually correspond to a + # textDocument/completion request we tracked — any other + # response (hover, definition, references, documentSymbol, + # formatting, codeAction, ...) must pass through untouched. + out = _inject_completions(msg, context) if context is not None else body except json.JSONDecodeError: out = body conn.sendall(_frame(out)) diff --git a/tests/test_proxy.py b/tests/test_proxy.py index 091bde8..e03f81f 100644 --- a/tests/test_proxy.py +++ b/tests/test_proxy.py @@ -502,6 +502,7 @@ def test_session_records_and_pops_context(): assert ctx["type"] == "param" assert ctx["module"] == "appservice" - # Second pop returns unknown - assert session.pop_context(42)["type"] == "unknown" + # Second pop (or any untracked id) returns None — the response must pass + # through untouched rather than being mistaken for a completion response. + assert session.pop_context(42) is None