fix(solidlsp): gracefully degrade when a language server lacks textDocument/documentSymbol - #1758
Conversation
…Symbol ansible-language-server never implements this method (see github.com/ansible/vscode-ansible/issues/601, closed not-planned), so any diagnostic-to-symbol attribution on an Ansible file crashed the whole get_diagnostics_for_file call instead of falling back to the <file> bucket as already documented. Mirrors the existing pull-diagnostics fallback pattern: catch SolidLSPException at the request site and return None. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
AmirF194
left a comment
There was a problem hiding this comment.
Nice fix, and the reasoning for a plain try/except over a pre-emptive capability check (since a missing documentSymbol handler returns a normal -32601 here instead of crashing the process, unlike the Julia case) makes sense.
One thing worth checking before merge: except SolidLSPException in get_raw_document_symbols is broad enough to also catch a language server that has terminated mid-request. SolidLSPException.cause can be a LanguageServerTerminatedException (ls_process.py), and tools_base.py's _apply relies on that exact exception reaching it to restart the language server and retry the tool call (checks e.is_language_server_terminated(), then calls restart_language_server and retries). With the new catch sitting inside get_raw_document_symbols, a termination during a document_symbol request would be swallowed as "no symbols" instead of propagating to that restart path, for every caller that funnels through here: get_diagnostics_for_file, GetSymbolsOverviewTool, and FindReferencingSymbolsTool, per your own PR description.
The existing pull-diagnostics fallback a few hundred lines up has the identical shape, so this isn't new to your change, but your diff explicitly widens it to three more tools. Is it worth narrowing the catch (skip the swallow when ex.is_language_server_terminated() and let it propagate), or is a dead server already handled some other way before it gets this far?
| try: | ||
| response = self.server.send.document_symbol({"textDocument": {"uri": self._resolve_file_uri(relative_file_path)}}) | ||
| except SolidLSPException as ex: | ||
| # some servers (e.g. ansible-language-server, see | ||
| # https://github.com/ansible/vscode-ansible/issues/601) don't implement | ||
| # textDocument/documentSymbol at all; degrade to "no symbols" instead of crashing | ||
| log.debug("Failed to retrieve document symbols for %s: %s", relative_file_path, ex) | ||
| return None |
There was a problem hiding this comment.
We don't generally want to gloss over exceptions in this manner.
Instead, fix this at the source of the problem, i.e. in the Ansible language server: Simply override get_raw_document_symbols there with a plain return None.
There was a problem hiding this comment.
Good call, done. Pushed a commit that:
- Reverts the try/except in
ls.pyentirely —get_raw_document_symbolsis back to the single-linedocument_symbolcall, unchanged frommain. - Adds
AnsibleLanguageServer._request_document_symbols, overriding the base class to unconditionallyreturn Nonewith a comment pointing at Support vscode outline ansible/vscode-ansible#601. This matches the existing pattern ineclipse_jdtls.py/angular_language_server.pyof overriding_request_document_symbolsper-server rather than teaching the base class about server quirks. - Added a unit test (
test_request_document_symbols_returns_none_without_contacting_server) that asserts the override short-circuits without needing a running language server, node, or npm.
No change to any other language server's behavior.
There was a problem hiding this comment.
Great. Please add the change to the changelog, then we can merge this.
There was a problem hiding this comment.
I just added the change to the changelog. Thank you for your guidance.
Per review on oraios#1758: instead of a broad try/except SolidLSPException around document_symbol in ls.py (which also risked swallowing LanguageServerTerminatedException for every language server), override _request_document_symbols in AnsibleLanguageServer to unconditionally return None, matching the existing per-server override pattern used by eclipse_jdtls.py and angular_language_server.py. ls.py is now unchanged from main. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
@AmirF194 Good catch. With opcode81's suggested approach, this is moot for Ansible specifically: You're right that the identical shape exists already in the pull-diagnostics fallback a few hundred lines up ( |
Requested by opcode81 in review on oraios#1758.
|
Thanks for your contribution! |
Problem
get_diagnostics_for_filecrashes outright (ToolCallError/SolidLSPException: Unhandled method textDocument/documentSymbol (-32601)) for any Ansible YAML file that has at least one realansible-lintdiagnostic to report.The tool groups each diagnostic under the symbol that owns it via
find_diagnostic_owner_symbol, which requeststextDocument/documentSymbolfrom the language server. Its docstring already promises graceful degradation for this: "If a diagnostic cannot be mapped to a symbol, it is grouped under the special name path<file>." But that fallback was never implemented for the case where the server doesn't supportdocumentSymbolat all — the request just throws, and the exception propagates all the way up throughsymbol.py'sfind_diagnostic_owner_symbolandsymbol_tools.py's diagnostics grouping, failing the whole tool call.This isn't a one-off or a config issue:
ansible-language-server(the standard, actively-maintained Ansible LSP, now folded intovscode-ansible) does not implementtextDocument/documentSymboland the maintainers have explicitly declined to add it — see ansible/vscode-ansible#601, closedNOT_PLANNEDin Jan 2024. So this reliably breaksget_diagnostics_for_filefor every Ansible role/task/playbook file that has lint findings.Example from a real session, before the fix:
Fix
Updated approach (per review): the fix is scoped to
AnsibleLanguageServerrather than the shared base class.src/solidlsp/language_servers/ansible_language_server.pynow overrides_request_document_symbolsto unconditionallyreturn None, sinceansible-language-servernever implementstextDocument/documentSymbol— there's no point sending a request known to always fail. This matches the existing per-server override pattern already used byeclipse_jdtls.pyandangular_language_server.pyfor other server-specific quirks.src/solidlsp/ls.pyis unchanged frommain: the original version of this PR added a generaltry/except SolidLSPExceptionaround thedocument_symbolcall in the sharedget_raw_document_symbols, but review (see discussion below) pointed out this widened the blast radius to every language server and risked maskingLanguageServerTerminatedException— the exceptiontools_base.pyrelies on to trigger language-server restart-and-retry — for three tools beyond just diagnostics (GetSymbolsOverviewTool,FindReferencingSymbolsTool). Scoping the fix to Ansible avoids all of that: no exception is ever thrown or caught, because the request is never sent.I traced the full call chain before picking this spot:
request_document_symbols(the caller of_request_document_symbols) already tolerates aNonereturn — it logs a warning and returns an emptyDocumentSymbols([])._get_document_symbols_with_locationsand_request_symbol_at_locationiterate over that (now-empty) result without special-casing.find_diagnostic_owner_symbol(serena/symbol.py) already handlesNonefromrequest_symbol_at_location.So every layer above
_request_document_symbolswas alreadyNone/empty-safe; the only gap was that Ansible had no override telling it to skip the doomed request in the first place.Verification
Confirmed against a real ce-tools repo with Ansible + Terraform files, before/after:
Before:
get_diagnostics_for_fileonroles/columnstore_setup/tasks/multi_node.ymlthrewToolCallError, failing the task after ~2s.After: same file, same request — the log now shows
WARNING ... Received None response from the Language Server for document symbols in .../multi_node.yml ... Returning empty list, and the tool call completes normally with the diagnostics grouped under<file>:{"multi_node.yml": {"Error": {"<file>": [{"message": "Trailing spaces", "code": "yaml[trailing-spaces]", "source": "ansible-lint"}, ...]}}}Added
test/solidlsp/ansible/test_ansible_basic.py::test_request_document_symbols_returns_none_without_contacting_server, a fast unit test asserting the override short-circuits without needing a running language server, node, or npm — closing the gap that the original fix only had manual verification for the crash itself.Also ran the existing test suite (
uv run pytest) against:test/serena/test_serena_agent.py -k test_get_diagnostics_for_filetest/solidlsp/ansible/test/solidlsp/python/test_symbol_retrieval.pytest/solidlsp/toml/test_toml_symbol_retrieval.py94 passed, 1 pre-existing xfail (
test_bare_symbol_names, ansible), 1 skipped, 1 failed due to a missing Go binary in the test environment (unrelated to this change, not a regression).